diff --git a/.agents/skills/claw-score/references/completeness/matrix.md b/.agents/skills/claw-score/references/completeness/matrix.md index 4398a1adb338..e5f7f3cbb5df 100644 --- a/.agents/skills/claw-score/references/completeness/matrix.md +++ b/.agents/skills/claw-score/references/completeness/matrix.md @@ -6,8 +6,8 @@ Use this rubric when assigning category Completeness scores for the ## Category Scope - Channel Setup and Operations: Matrix plugin identity, Setup wizard, Account discovery, Matrix doctor warnings, Matrix probe/status, Shared Matrix client resolution, Monitor startup, Startup maintenance, Matrix doctor warnings, Matrix probe/status, Monitor startup, Startup maintenance -- Access and Identity: DM policy, Direct-room classification, Inbound route selection across sender-bound DMs, Mention gates, Matrix thread reply routing, Persisted Matrix thread routing managers, ACP/subagent spawn hooks -- Conversation Routing and Delivery: DM policy, Direct-room classification, Inbound route selection across sender-bound DMs, Mention gates, Matrix thread reply routing, Persisted Matrix thread routing managers, ACP/subagent spawn hooks, Channel action discovery, Message send/read/edit/delete, Profile media loading, Outbound Matrix text, Message presentation metadata, Inbound media failure handling, Message send/read/edit/delete, Profile media loading, Outbound Matrix text, Message presentation metadata, Inbound media failure handling +- Access and Identity: DM policy, Direct-room classification, Inbound route selection across sender-bound DMs, Mention gates, Matrix thread reply routing, Persisted Matrix thread routing managers +- Conversation Routing and Delivery: DM policy, Direct-room classification, Inbound route selection across sender-bound DMs, Mention gates, Matrix thread reply routing, Persisted Matrix thread routing managers, Channel action discovery, Message send/read/edit/delete, Profile media loading, Outbound Matrix text, Message presentation metadata, Inbound media failure handling, Message send/read/edit/delete, Profile media loading, Outbound Matrix text, Message presentation metadata, Inbound media failure handling - Media and Rich Content: Channel action discovery, Message send/read/edit/delete, Profile media loading, Outbound Matrix text, Message presentation metadata, Inbound media failure handling - Native Controls and Approvals: Channel action discovery, Message send/read/edit/delete, Profile media loading, Outbound Matrix text, Message presentation metadata, Inbound media failure handling, Matrix native exec, Origin target resolution from Matrix turn, Approver DM target resolution, Matrix approval metadata, Origin target resolution from Matrix turn, Approver DM target resolution, Matrix approval metadata - Encryption and Verification: Encryption setup, Encrypted media upload/download, Legacy state diff --git a/config/assertion-safety-baseline.txt b/config/assertion-safety-baseline.txt index 41f489f9a50c..37ef27752b37 100644 --- a/config/assertion-safety-baseline.txt +++ b/config/assertion-safety-baseline.txt @@ -3850,7 +3850,6 @@ src/state/openclaw-state-db-schema-additive.ts 1 src/state/openclaw-state-db-schema-helpers.ts 4 src/state/openclaw-state-db-schema-migration-required.ts 2 src/state/openclaw-state-db-schema-repair.ts 7 -src/state/openclaw-state-lease.ts 1 src/state/openclaw-state-ownership.ts 1 src/state/openclaw-state-snapshot-sanitizer.ts 1 src/state/user-profiles-tailscale-avatar.ts 2 diff --git a/extensions/active-memory/config.ts b/extensions/active-memory/config.ts index bb92fae1bd4b..bd98cd13b470 100644 --- a/extensions/active-memory/config.ts +++ b/extensions/active-memory/config.ts @@ -1,11 +1,11 @@ import path from "node:path"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime"; import { parseStrictPositiveInteger, resolveIntegerOption, } from "openclaw/plugin-sdk/number-runtime"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; -import { isPathInside } from "openclaw/plugin-sdk/security-runtime"; import { asOptionalRecord, normalizeLowercaseStringOrEmpty, diff --git a/extensions/canvas/src/host/a2ui-route.ts b/extensions/canvas/src/host/a2ui-route.ts index 059c0976b0de..9f91b93f09ee 100644 --- a/extensions/canvas/src/host/a2ui-route.ts +++ b/extensions/canvas/src/host/a2ui-route.ts @@ -4,8 +4,8 @@ import { A2UI_PATH, isA2uiPath } from "./a2ui-shared.js"; import { resolveFileWithinRoot } from "./file-resolver.js"; type A2uiRootResolver = () => Promise; -type A2uiHttpRequest = { method?: string; url?: string }; -type A2uiHttpResponse = { +export type A2uiHttpRequest = { method?: string; url?: string }; +export type A2uiHttpResponse = { statusCode: number; setHeader(name: string, value: number | string | readonly string[]): void; end(chunk?: Buffer | string): void; diff --git a/extensions/canvas/src/host/a2ui.ts b/extensions/canvas/src/host/a2ui.ts index db307e2211f3..c3c94dcb23e7 100644 --- a/extensions/canvas/src/host/a2ui.ts +++ b/extensions/canvas/src/host/a2ui.ts @@ -4,7 +4,11 @@ import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { handleA2uiHttpRequestWithRootResolver } from "./a2ui-route.js"; +import { + handleA2uiHttpRequestWithRootResolver, + type A2uiHttpRequest, + type A2uiHttpResponse, +} from "./a2ui-route.js"; export { A2UI_PATH, CANVAS_HOST_PATH } from "./a2ui-shared.js"; @@ -13,13 +17,6 @@ let resolvingA2uiRoot: Promise | null = null; let cachedA2uiResolvedAtMs = 0; const A2UI_ROOT_RETRY_NULL_AFTER_MS = 10_000; -type A2uiHttpRequest = { method?: string; url?: string }; -type A2uiHttpResponse = { - statusCode: number; - setHeader(name: string, value: number | string | readonly string[]): void; - end(chunk?: Buffer | string): void; -}; - async function resolveA2uiRoot(): Promise { const here = path.dirname(fileURLToPath(import.meta.url)); const entryDir = process.argv[1] ? path.dirname(path.resolve(process.argv[1])) : null; diff --git a/extensions/codex/src/app-server/session-permission-policy.ts b/extensions/codex/src/app-server/session-permission-policy.ts index 5b2f881d4932..402c7883377d 100644 --- a/extensions/codex/src/app-server/session-permission-policy.ts +++ b/extensions/codex/src/app-server/session-permission-policy.ts @@ -1,6 +1,6 @@ import { hostname as readHostName } from "node:os"; import type { EmbeddedRunAttemptParamsV2 } from "openclaw/plugin-sdk/agent-harness-runtime"; -import { isPathInside } from "openclaw/plugin-sdk/security-runtime"; +import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime"; import type { CodexAppServerApprovalPolicy, CodexAppServerApprovalsReviewer, diff --git a/extensions/codex/src/migration/plan.ts b/extensions/codex/src/migration/plan.ts index 91aea0c20155..42971acccc9d 100644 --- a/extensions/codex/src/migration/plan.ts +++ b/extensions/codex/src/migration/plan.ts @@ -1,6 +1,10 @@ // Codex plugin module implements plan behavior. import fs from "node:fs/promises"; import path from "node:path"; +import { + canonicalPathFromExistingAncestor, + isPathInside, +} from "openclaw/plugin-sdk/file-access-runtime"; import { createMigrationItem, createMigrationManualItem, @@ -14,11 +18,7 @@ import type { MigrationPlan, MigrationProviderContext, } from "openclaw/plugin-sdk/plugin-entry"; -import { - canonicalPathFromExistingAncestor, - extractErrorCode, - isPathInside, -} from "openclaw/plugin-sdk/security-runtime"; +import { extractErrorCode } from "openclaw/plugin-sdk/security-runtime"; import { asBoolean, isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { CODEX_PLUGINS_MARKETPLACE_NAME } from "../app-server/config.js"; import { buildCodexAuthItems } from "./auth.js"; diff --git a/extensions/codex/src/migration/session-binding-sidecars.ts b/extensions/codex/src/migration/session-binding-sidecars.ts index 94ec37c1110d..58c8c125e16e 100644 --- a/extensions/codex/src/migration/session-binding-sidecars.ts +++ b/extensions/codex/src/migration/session-binding-sidecars.ts @@ -6,6 +6,10 @@ import { resolveAgentDir, resolveSessionAgentIds, } from "openclaw/plugin-sdk/agent-scope-runtime"; +import { + canonicalPathFromExistingAncestor, + isPathInside, +} from "openclaw/plugin-sdk/file-access-runtime"; import { withFileLock, type FileLockOptions } from "openclaw/plugin-sdk/file-lock"; import type { PluginStateKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime"; import { normalizeAgentId } from "openclaw/plugin-sdk/routing"; @@ -14,11 +18,7 @@ import { legacyStateFileExists, type PluginDoctorStateMigration, } from "openclaw/plugin-sdk/runtime-doctor-migrations"; -import { - canonicalPathFromExistingAncestor, - isPathInside, - pathExists, -} from "openclaw/plugin-sdk/security-runtime"; +import { pathExists } from "openclaw/plugin-sdk/security-runtime"; import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { CODEX_APP_SERVER_BINDING_MAX_ENTRIES, diff --git a/extensions/codex/src/migration/source.ts b/extensions/codex/src/migration/source.ts index 4f28f606e72d..54b5dc8d5563 100644 --- a/extensions/codex/src/migration/source.ts +++ b/extensions/codex/src/migration/source.ts @@ -1,7 +1,7 @@ // Codex plugin module implements source behavior. import path from "node:path"; import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime"; -import { isPathInside } from "openclaw/plugin-sdk/security-runtime"; +import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime"; import { defaultCodexAppInventoryCache, type CodexAppInventoryRequest, diff --git a/extensions/migrate-claude/memory.ts b/extensions/migrate-claude/memory.ts index 5d5edea64ebe..0e1ef3b927f7 100644 --- a/extensions/migrate-claude/memory.ts +++ b/extensions/migrate-claude/memory.ts @@ -2,12 +2,12 @@ import type { Dirent } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; -import { createMigrationItem, MIGRATION_REASON_TARGET_EXISTS } from "openclaw/plugin-sdk/migration"; -import type { MigrationItem } from "openclaw/plugin-sdk/plugin-entry"; import { canonicalPathFromExistingAncestor, isPathInside, -} from "openclaw/plugin-sdk/security-runtime"; +} from "openclaw/plugin-sdk/file-access-runtime"; +import { createMigrationItem, MIGRATION_REASON_TARGET_EXISTS } from "openclaw/plugin-sdk/migration"; +import type { MigrationItem } from "openclaw/plugin-sdk/plugin-entry"; import { CLAUDE_AUTO_MEMORY_MAX_FILES, CLAUDE_AUTO_MEMORY_MAX_SCAN_ENTRIES, diff --git a/extensions/mxc/src/fs-bridge.ts b/extensions/mxc/src/fs-bridge.ts index 0536a59edefc..8be8f6567046 100644 --- a/extensions/mxc/src/fs-bridge.ts +++ b/extensions/mxc/src/fs-bridge.ts @@ -1,5 +1,9 @@ import path from "node:path"; -import { removePathWithinRoot, root as fsRoot } from "openclaw/plugin-sdk/file-access-runtime"; +import { + isPathInside, + removePathWithinRoot, + root as fsRoot, +} from "openclaw/plugin-sdk/file-access-runtime"; import { createWritableRenameTargetResolver, type SandboxBackendHandle, @@ -7,7 +11,7 @@ import { type SandboxFsStat, type SandboxResolvedPath, } from "openclaw/plugin-sdk/sandbox"; -import { FsSafeError, isPathInside } from "openclaw/plugin-sdk/security-runtime"; +import { FsSafeError } from "openclaw/plugin-sdk/security-runtime"; import { resolveMxcReadOnlySkillMounts, type MxcReadOnlySkillMount, diff --git a/extensions/mxc/src/mxc-container-config.ts b/extensions/mxc/src/mxc-container-config.ts index ece28b306a52..1fa5f91e9aac 100644 --- a/extensions/mxc/src/mxc-container-config.ts +++ b/extensions/mxc/src/mxc-container-config.ts @@ -2,7 +2,7 @@ import { createHash } from "node:crypto"; import { statSync } from "node:fs"; import path from "node:path"; import type { ContainerConfig } from "@microsoft/mxc-sdk"; -import { isPathInside } from "openclaw/plugin-sdk/security-runtime"; +import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime"; import type { MxcConfig } from "./config.js"; import { resolveBaselineReadonlyPaths, type BaselineHostEnv } from "./sandbox-baseline.js"; import type { diff --git a/extensions/mxc/src/workspace-skill-mounts.ts b/extensions/mxc/src/workspace-skill-mounts.ts index af35e6a14be0..35b88f8c8af0 100644 --- a/extensions/mxc/src/workspace-skill-mounts.ts +++ b/extensions/mxc/src/workspace-skill-mounts.ts @@ -1,6 +1,6 @@ import { lstatSync, realpathSync } from "node:fs"; import path from "node:path"; -import { isPathInside } from "openclaw/plugin-sdk/security-runtime"; +import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime"; export type MxcWorkspaceAccess = "none" | "ro" | "rw"; diff --git a/extensions/mxc/test/mxc-backend.test.ts b/extensions/mxc/test/mxc-backend.test.ts index 00f75f440449..af81f8673f9d 100644 --- a/extensions/mxc/test/mxc-backend.test.ts +++ b/extensions/mxc/test/mxc-backend.test.ts @@ -11,8 +11,8 @@ import { } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; +import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime"; import type { CreateSandboxBackendParams } from "openclaw/plugin-sdk/sandbox"; -import { isPathInside } from "openclaw/plugin-sdk/security-runtime"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { resolveConfig, type MxcConfig } from "../src/config.js"; import { createMxcSandboxBackendFactory } from "../src/mxc-backend-factory.js"; diff --git a/extensions/openshell/src/fs-bridge.ts b/extensions/openshell/src/fs-bridge.ts index 405dc3e47d56..d847c60cf1e6 100644 --- a/extensions/openshell/src/fs-bridge.ts +++ b/extensions/openshell/src/fs-bridge.ts @@ -1,14 +1,14 @@ // Openshell plugin module implements fs bridge behavior. import fsPromises from "node:fs/promises"; import path from "node:path"; -import { root as fsRoot } from "openclaw/plugin-sdk/file-access-runtime"; +import { isPathInside, root as fsRoot } from "openclaw/plugin-sdk/file-access-runtime"; import type { SandboxFsBridge, SandboxFsStat, SandboxResolvedPath, } from "openclaw/plugin-sdk/sandbox"; import { createWritableRenameTargetResolver } from "openclaw/plugin-sdk/sandbox"; -import { FsSafeError, isPathInside } from "openclaw/plugin-sdk/security-runtime"; +import { FsSafeError } from "openclaw/plugin-sdk/security-runtime"; import type { OpenShellFsBridgeContext, OpenShellSandboxBackend } from "./backend.types.js"; type ResolvedMountPath = SandboxResolvedPath & { diff --git a/extensions/qa-lab/src/profile-evidence-sharding.ts b/extensions/qa-lab/src/profile-evidence-sharding.ts index 947f3ae7aab4..f5296f1deb68 100644 --- a/extensions/qa-lab/src/profile-evidence-sharding.ts +++ b/extensions/qa-lab/src/profile-evidence-sharding.ts @@ -4,9 +4,9 @@ import path from "node:path"; import { isCrablineServerChannel, OPENCLAW_CRABLINE_DEFAULT_CHANNEL } from "@openclaw/crabline"; import { canonicalPathFromExistingAncestor, - extractErrorCode, isPathInside, -} from "openclaw/plugin-sdk/security-runtime"; +} from "openclaw/plugin-sdk/file-access-runtime"; +import { extractErrorCode } from "openclaw/plugin-sdk/security-runtime"; import { mergeQaEvidenceSummaries, validateQaEvidenceSummaryJson, diff --git a/extensions/workboard/src/workspace-access.ts b/extensions/workboard/src/workspace-access.ts index 7d3cf8d8f95d..efec16e3471f 100644 --- a/extensions/workboard/src/workspace-access.ts +++ b/extensions/workboard/src/workspace-access.ts @@ -1,20 +1,20 @@ import type { WorkboardWorkspace, WorkboardWorkspaceAccess } from "@openclaw/workboard-contract"; -// Workboard workspace access follows the caller's canonical filesystem boundary. import { listAgentIds, resolveAgentConfig, resolveAgentWorkspaceDir, resolveDefaultAgentId, } from "openclaw/plugin-sdk/agent-runtime"; +// Workboard workspace access follows the caller's canonical filesystem boundary. +import { + canonicalPathFromExistingAncestor, + isPathInside, +} from "openclaw/plugin-sdk/file-access-runtime"; import type { AnyAgentTool, OpenClawPluginApi, OpenClawPluginToolContext, } from "openclaw/plugin-sdk/plugin-entry"; -import { - canonicalPathFromExistingAncestor, - isPathInside, -} from "openclaw/plugin-sdk/security-runtime"; import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; export type { WorkboardWorkspaceAccess } from "@openclaw/workboard-contract"; diff --git a/scripts/dev/computer-use-macos-live-proof.ts b/scripts/dev/computer-use-macos-live-proof.ts index d680ef4723f9..81d255f58e4c 100644 --- a/scripts/dev/computer-use-macos-live-proof.ts +++ b/scripts/dev/computer-use-macos-live-proof.ts @@ -2,6 +2,7 @@ import { execFile } from "node:child_process"; import { mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; import { parseArgs, promisify } from "node:util"; +import { asOptionalRecord as record } from "@openclaw/normalization-core/record-coerce"; import { createComputerTool } from "../../src/agents/tools/computer-tool.js"; import { listNodes } from "../../src/agents/tools/nodes-utils.js"; @@ -126,12 +127,6 @@ function wireResult(result: ToolResult): JsonRecord { throw new Error(`missing structured result: ${resultText(result)}`); } -function record(value: unknown): JsonRecord | undefined { - return value !== null && typeof value === "object" && !Array.isArray(value) - ? (value as JsonRecord) - : undefined; -} - function records(value: unknown): JsonRecord[] { return Array.isArray(value) ? value.map(record).filter((entry) => entry !== undefined) : []; } diff --git a/scripts/lib/plugin-npm-runtime-build.mts b/scripts/lib/plugin-npm-runtime-build.mts index 5379f5582794..904523f8a9ba 100644 --- a/scripts/lib/plugin-npm-runtime-build.mts +++ b/scripts/lib/plugin-npm-runtime-build.mts @@ -12,6 +12,7 @@ import { listMissingPackageStaticAssetSources, runPackageAssetBuild, } from "./plugin-npm-runtime-assets.mts"; +import { isRecord } from "./record-shared.mjs"; import { copyStaticExtensionAssetsForPackage } from "./static-extension-assets.mts"; const env = { @@ -98,10 +99,6 @@ function getStringRecord(value: unknown) { ); } -function getRecord(value: unknown) { - return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {}; -} - function createNeverBundleDependencyMatcher(packageJson: PluginPackageJson) { const externalDependencies = collectExternalDependencyNames(packageJson); return (id: string) => { @@ -298,8 +295,12 @@ function resolvePluginNpmRuntimePackagePeerMetadata(plan: { ); } const existingPeerDependencies = getStringRecord(plan.packageJson.peerDependencies); - const existingPeerDependenciesMeta = getRecord(plan.packageJson.peerDependenciesMeta); - const existingOpenClawMeta = getRecord(existingPeerDependenciesMeta.openclaw); + const existingPeerDependenciesMeta = isRecord(plan.packageJson.peerDependenciesMeta) + ? plan.packageJson.peerDependenciesMeta + : {}; + const existingOpenClawMeta = isRecord(existingPeerDependenciesMeta.openclaw) + ? existingPeerDependenciesMeta.openclaw + : {}; return { peerDependencies: { ...existingPeerDependencies, diff --git a/scripts/lib/record-shared.d.mts b/scripts/lib/record-shared.d.mts new file mode 100644 index 000000000000..df76f8002aca --- /dev/null +++ b/scripts/lib/record-shared.d.mts @@ -0,0 +1,2 @@ +export declare function isRecord(value: unknown): value is Record; +export declare function trimString(value: unknown): string; diff --git a/scripts/lib/windows-cmd-helpers-runtime.mts b/scripts/lib/windows-cmd-helpers-runtime.mts index 228e3de45463..061460f1747b 100644 --- a/scripts/lib/windows-cmd-helpers-runtime.mts +++ b/scripts/lib/windows-cmd-helpers-runtime.mts @@ -1,13 +1,11 @@ // Typed bridge to the plain-Node Windows command helpers. +import { isRecord } from "./record-shared.mjs"; + const runtimeSpecifier = "../windows-cmd-helpers.mjs"; const runtime: unknown = await import(runtimeSpecifier); -function isRuntimeRecord(value: unknown): value is Record { - return value !== null && typeof value === "object" && !Array.isArray(value); -} - function runtimeFunction(name: string): (...args: unknown[]) => unknown { - if (!isRuntimeRecord(runtime) || !(name in runtime)) { + if (!isRecord(runtime) || !(name in runtime)) { throw new Error(`windows command helper is missing ${name}`); } const value = runtime[name]; diff --git a/src/agents/command/attempt-execution.cli.test.ts b/src/agents/command/attempt-execution.cli.test.ts index 6f9420dc97fe..696849934387 100644 --- a/src/agents/command/attempt-execution.cli.test.ts +++ b/src/agents/command/attempt-execution.cli.test.ts @@ -2964,6 +2964,40 @@ describe("CLI attempt execution", () => { expect(embeddedArg.allowEmptyAssistantReplyAsSilent).toBe(true); }); + it.each([ + { + name: "subagent lane", + lane: "subagent" as const, + sessionKey: "agent:main:subagent:cli-empty-completion", + expected: true, + }, + { + name: "ordinary lane", + lane: undefined, + sessionKey: "agent:main:direct:cli-empty-completion", + expected: false, + }, + ])("allows empty CLI output only for $name runs", async ({ lane, sessionKey, expected }) => { + const sessionEntry = makeSessionEntry(`session-${lane ?? "ordinary"}`); + const sessionStore = { [sessionKey]: sessionEntry }; + await writeSessionStoreSeed(sessionStore); + runCliAgentMock.mockResolvedValueOnce(makeCliResult("cli completion")); + + await runStoredAttempt({ + providerOverride: "claude-cli", + modelOverride: "opus", + sessionEntry, + sessionKey, + body: "complete the task", + runId: `run-${lane ?? "ordinary"}-cli-empty-completion`, + opts: lane ? { lane } : {}, + sessionStore, + }); + + expect(firstRunCliAgentArg().allowEmptyAssistantReplyAsSilent).toBe(expected); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); + }); + it("forwards exact cron creator authority into embedded execution", async () => { const runId = "embedded-cron-creator-authority"; const capability = createCronCreatorAuthorityCapability(runId); diff --git a/src/agents/command/attempt-execution.ts b/src/agents/command/attempt-execution.ts index ab7d7d6ea72d..27f711e65653 100644 --- a/src/agents/command/attempt-execution.ts +++ b/src/agents/command/attempt-execution.ts @@ -559,6 +559,7 @@ export function runAgentAttempt(params: { ? { id: sessionAuthProfileId, source: sessionAuthProfileSource } : undefined; const isRawModelRun = params.opts.modelRun === true || params.opts.promptMode === "none"; + const isSubagentLane = params.opts.lane === AGENT_LANE_SUBAGENT; // A completion handoff relays frozen child output, so only a verified private // capability plus persisted requester lineage may restore its tool surface. const isSubagentAnnounceHandoff = isSubagentAnnounceCompletionHandoff({ @@ -1029,7 +1030,7 @@ export function runAgentAttempt(params: { onContextEngineTurnCandidate: params.onContextEngineTurnCandidate, suppressNextUserMessagePersistence: params.suppressPromptPersistenceOnRetry === true, disableTools, - allowEmptyAssistantReplyAsSilent: isSubagentAnnounceHandoff, + allowEmptyAssistantReplyAsSilent: isSubagentLane || isSubagentAnnounceHandoff, ...(forkStoreParams && !forkCliSessionOnResume ? { onBeforeForkedCliSessionRetry: async (retry) => { @@ -1140,7 +1141,7 @@ export function runAgentAttempt(params: { agentId: params.sessionAgentId, trigger: "user", // Subagent lifecycle owns the stricter explicit visible/silent/empty evidence check. - terminalReplyExpectation: params.opts.lane === AGENT_LANE_SUBAGENT ? "optional" : undefined, + terminalReplyExpectation: isSubagentLane ? "optional" : undefined, messageChannel: params.messageChannel, messageProvider: params.opts.messageProvider ?? params.messageChannel, agentAccountId: params.runContext.accountId, @@ -1230,8 +1231,7 @@ export function runAgentAttempt(params: { modelRun: params.opts.modelRun, promptMode: params.opts.promptMode, disableTools, - allowEmptyAssistantReplyAsSilent: - params.opts.lane === AGENT_LANE_SUBAGENT || isSubagentAnnounceHandoff, + allowEmptyAssistantReplyAsSilent: isSubagentLane || isSubagentAnnounceHandoff, onAgentEvent: params.onAgentEvent, deferTerminalLifecycle: params.deferTerminalLifecycle, suppressNextUserMessagePersistence: params.suppressPromptPersistenceOnRetry === true, diff --git a/src/audit/execution-identity-spawn-admission.ts b/src/audit/execution-identity-spawn-admission.ts index d323cebc5eda..b0b74795774b 100644 --- a/src/audit/execution-identity-spawn-admission.ts +++ b/src/audit/execution-identity-spawn-admission.ts @@ -1,3 +1,5 @@ +import { isRecord } from "@openclaw/normalization-core/record-coerce"; + const EXECUTION_IDENTITY_SPAWN_ADMISSION_FACTS = Symbol("executionIdentitySpawnAdmissionFacts"); type ExecutionIdentitySpawnLineage = { @@ -27,10 +29,6 @@ type ExecutionIdentitySpawnAdmissionInput = | { operation: "base-envelope"; value: unknown } | { operation: "read"; value: unknown }; -function isCarrierRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - function ownDataDescriptor(value: object, key: PropertyKey): PropertyDescriptor | undefined { const descriptor = Object.getOwnPropertyDescriptor(value, key); if (!descriptor) { @@ -86,7 +84,7 @@ function validateEnvelopeExtension( return { missingEvidence }; } if ( - !isCarrierRecord(lineage) || + !isRecord(lineage) || (lineage.parentContextId !== undefined && !validRef(lineage.parentContextId, 256)) || (lineage.parentExecutionId !== undefined && !validRef(lineage.parentExecutionId, 256)) || (lineage.parentRunId !== undefined && !validRef(lineage.parentRunId, 256)) || @@ -182,7 +180,7 @@ export function executionIdentitySpawnAdmission( const extension = validateEnvelopeExtension(parsed[0], parsed[1]); return [extension.lineage, extension.missingEvidence]; } - if (!isCarrierRecord(value)) { + if (!isRecord(value)) { throw new Error("execution identity spawn admission carrier is invalid"); } if (operation === "attach") { diff --git a/src/infra/sqlite-wal.ts b/src/infra/sqlite-wal.ts index 6b278fd6cf3e..6054ebe09c53 100644 --- a/src/infra/sqlite-wal.ts +++ b/src/infra/sqlite-wal.ts @@ -4,6 +4,7 @@ import path from "node:path"; import type { DatabaseSync } from "node:sqlite"; import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import type { Result } from "@openclaw/normalization-core/result"; +import { normalizeSqliteNonNegativeInteger } from "./sqlite-busy-timeout.js"; import { isSqliteLockError } from "./sqlite-transaction.js"; // WAL maintenance configures SQLite write-ahead logging and schedules bounded @@ -58,7 +59,7 @@ export type SqliteConnectionPragmaOptions = SqliteWalMaintenanceOptions & { }; function configureSqliteBusyTimeout(db: DatabaseSync, busyTimeoutMs: number): number { - const normalizedTimeoutMs = normalizeNonNegativeInteger(busyTimeoutMs, "busyTimeoutMs"); + const normalizedTimeoutMs = normalizeSqliteNonNegativeInteger(busyTimeoutMs, "busyTimeoutMs"); db.exec(`PRAGMA busy_timeout = ${normalizedTimeoutMs};`); return normalizedTimeoutMs; } @@ -86,13 +87,6 @@ export function configureSqlitePreSchemaPragmas( enableIncrementalAutoVacuumForFreshDatabase(db); } -function normalizeNonNegativeInteger(value: number, label: string): number { - if (!Number.isInteger(value) || value < 0) { - throw new Error(`${label} must be a non-negative integer`); - } - return value; -} - function findExistingVolumePaths( targetPath: string, ): { canonicalPath: string; originalPath: string } | null { @@ -439,11 +433,11 @@ export function configureSqliteWalMaintenance( ): SqliteWalMaintenance { const busyTimeoutMs = options.busyTimeoutMs === undefined ? 0 : configureSqliteBusyTimeout(db, options.busyTimeoutMs); - const autoCheckpointPages = normalizeNonNegativeInteger( + const autoCheckpointPages = normalizeSqliteNonNegativeInteger( options.autoCheckpointPages ?? DEFAULT_SQLITE_WAL_AUTOCHECKPOINT_PAGES, "autoCheckpointPages", ); - const checkpointIntervalMs = normalizeNonNegativeInteger( + const checkpointIntervalMs = normalizeSqliteNonNegativeInteger( options.checkpointIntervalMs ?? DEFAULT_SQLITE_WAL_CHECKPOINT_INTERVAL_MS, "checkpointIntervalMs", ); diff --git a/src/state/openclaw-state-lease.ts b/src/state/openclaw-state-lease.ts index 27b9bc25c517..80cc45925488 100644 --- a/src/state/openclaw-state-lease.ts +++ b/src/state/openclaw-state-lease.ts @@ -8,6 +8,7 @@ import { executeSqliteQueryTakeFirstSync, getNodeSqliteKysely, } from "../infra/kysely-sync.js"; +import { runWithSqliteBusyTimeout } from "../infra/sqlite-busy-timeout.js"; import { isSqliteLockError } from "../infra/sqlite-transaction.js"; import { loggingState } from "../logging/state.js"; import type { DB as OpenClawStateKyselyDatabase } from "./openclaw-state-db.generated.js"; @@ -180,29 +181,6 @@ function validateOptions(options: OpenClawStateLeaseOptions) { }; } -function readBusyTimeout(database: DatabaseSync): number { - const row = database // sqlite-allow-raw -- Narrow connection primitive for bounded lease admission. - .prepare("PRAGMA busy_timeout") - .get() as { busy_timeout?: unknown; timeout?: unknown } | undefined; - const value = row?.busy_timeout ?? row?.timeout; - return typeof value === "bigint" ? Number(value) : Number(value ?? 0); -} - -function withBusyTimeout(database: DatabaseSync, busyTimeoutMs: number, run: () => T): T { - const previousBusyTimeoutMs = readBusyTimeout(database); - if (previousBusyTimeoutMs === busyTimeoutMs) { - return run(); - } - database.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`); // sqlite-allow-raw -- Bound synchronous lease admission to waitMs. - try { - return run(); - } finally { - if (database.isOpen) { - database.exec(`PRAGMA busy_timeout = ${previousBusyTimeoutMs}`); // sqlite-allow-raw -- Restore canonical connection policy. - } - } -} - function withLeaseWriteTransaction( database: OpenClawStateLeaseDatabase, operationLabel: string, @@ -216,7 +194,7 @@ function withLeaseWriteTransaction( database.options, { operationLabel, busyTimeoutMs }, ); - return withBusyTimeout(stateDatabase.db, busyTimeoutMs, run); + return runWithSqliteBusyTimeout(stateDatabase.db, busyTimeoutMs, run); } function withLeaseRead( diff --git a/taxonomy.yaml b/taxonomy.yaml index 049dc1f60e2d..df9b2e6815be 100644 --- a/taxonomy.yaml +++ b/taxonomy.yaml @@ -9123,9 +9123,6 @@ surfaces: - name: Persisted Matrix thread routing managers coverageIds: [matrix.persisted-matrix-thread-routing-managers] description: Persisted Matrix thread binding managers, child session binding, and activity tracking. - - name: ACP/subagent spawn hooks - coverageIds: [matrix.acp-subagent-spawn-hooks] - description: ACP/subagent spawn hooks and Matrix delivery targets for child sessions docs: - docs/channels/matrix.md - docs/channels/groups.md @@ -9133,8 +9130,6 @@ surfaces: search_anchors: - matrix dm room routing and access policy - dm room routing and access policy - - matrix threads, acp, and subagent bindings - - threads, acp, and subagent bindings category_note: dm-room-routing-and-access-policy.md human_lts_override: false - name: Conversation Routing and Delivery @@ -9148,7 +9143,7 @@ surfaces: search_anchors: - matrix conversation routing and delivery - conversation routing and delivery - category_note: threads-acp-and-subagent-bindings.md + category_note: conversation-routing-and-delivery.md human_lts_override: false - name: Media and Rich Content id: media-and-rich-content