fix(agents): preserve empty CLI subagent completions (#126379)

* fix(agents): preserve empty CLI subagent completions

* chore(qa): remove retired Matrix coverage leaf

* refactor: consolidate shared runtime ownership

* fix(scripts): keep runtime build coercion dependency-light

* chore: remove release-owned changelog entry
This commit is contained in:
Peter Steinberger
2026-08-19 11:30:43 -07:00
committed by GitHub
parent 8a9e21d3bc
commit 341551937e
27 changed files with 105 additions and 110 deletions
@@ -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
-1
View File
@@ -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
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -4,8 +4,8 @@ import { A2UI_PATH, isA2uiPath } from "./a2ui-shared.js";
import { resolveFileWithinRoot } from "./file-resolver.js";
type A2uiRootResolver = () => Promise<string | null>;
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;
+5 -8
View File
@@ -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<string | null> | 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<string | null> {
const here = path.dirname(fileURLToPath(import.meta.url));
const entryDir = process.argv[1] ? path.dirname(path.resolve(process.argv[1])) : null;
@@ -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,
+5 -5
View File
@@ -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";
@@ -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,
+1 -1
View File
@@ -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,
+3 -3
View File
@@ -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,
+6 -2
View File
@@ -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,
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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";
+2 -2
View File
@@ -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 & {
@@ -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,
+5 -5
View File
@@ -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";
+1 -6
View File
@@ -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) : [];
}
+7 -6
View File
@@ -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,
+2
View File
@@ -0,0 +1,2 @@
export declare function isRecord(value: unknown): value is Record<string, unknown>;
export declare function trimString(value: unknown): string;
+3 -5
View File
@@ -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<string, unknown> {
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];
@@ -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);
+4 -4
View File
@@ -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,
@@ -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<string, unknown> {
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") {
+4 -10
View File
@@ -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",
);
+2 -24
View File
@@ -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<T>(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<T>(
database: OpenClawStateLeaseDatabase,
operationLabel: string,
@@ -216,7 +194,7 @@ function withLeaseWriteTransaction<T>(
database.options,
{ operationLabel, busyTimeoutMs },
);
return withBusyTimeout(stateDatabase.db, busyTimeoutMs, run);
return runWithSqliteBusyTimeout(stateDatabase.db, busyTimeoutMs, run);
}
function withLeaseRead<T>(
+1 -6
View File
@@ -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