From f1b866e8545fe87f961f25ed37a2890d10222605 Mon Sep 17 00:00:00 2001 From: spiceoogway Date: Thu, 29 Jan 2026 23:39:31 -0500 Subject: [PATCH 1/4] Fix #4355: Make session write lock timeout configurable - Increase default timeout from 10s to 60s to prevent premature termination when multiple subagents compete for session file locks - Add sessionWriteLockTimeoutMs config option to agent defaults - Update call sites to use config value when available - Maintain backwards compatibility for explicit timeoutMs callers This fixes the issue where subagents would terminate prematurely with 'terminated' error when session write lock timeout (10s) was exceeded during concurrent subagent execution. --- pnpm-lock.yaml | 53 ++++++++++---------- src/agents/pi-embedded-runner/compact.ts | 1 + src/agents/pi-embedded-runner/run/attempt.ts | 1 + src/agents/session-write-lock.ts | 4 +- src/config/zod-schema.agent-defaults.ts | 1 + 5 files changed, 32 insertions(+), 28 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7d3c6776..5992513ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -172,6 +172,13 @@ importers: zod: specifier: ^4.3.6 version: 4.3.6 + optionalDependencies: + '@napi-rs/canvas': + specifier: ^0.1.88 + version: 0.1.88 + node-llama-cpp: + specifier: 3.15.0 + version: 3.15.0(typescript@5.9.3) devDependencies: '@grammyjs/types': specifier: ^3.23.0 @@ -254,13 +261,6 @@ importers: wireit: specifier: ^0.14.12 version: 0.14.12 - optionalDependencies: - '@napi-rs/canvas': - specifier: ^0.1.88 - version: 0.1.88 - node-llama-cpp: - specifier: 3.15.0 - version: 3.15.0(typescript@5.9.3) extensions/bluebubbles: {} @@ -314,17 +314,17 @@ importers: specifier: ^10.5.0 version: 10.5.0 devDependencies: - moltbot: + openclaw: specifier: workspace:* - version: link:../../packages/moltbot + version: link:../.. extensions/imessage: {} extensions/line: devDependencies: - moltbot: + openclaw: specifier: workspace:* - version: link:../../packages/moltbot + version: link:../.. extensions/llm-task: {} @@ -348,17 +348,17 @@ importers: specifier: ^4.3.6 version: 4.3.6 devDependencies: - moltbot: + openclaw: specifier: workspace:* - version: link:../../packages/moltbot + version: link:../.. extensions/mattermost: {} extensions/memory-core: devDependencies: - moltbot: + openclaw: specifier: workspace:* - version: link:../../packages/moltbot + version: link:../.. extensions/memory-lancedb: dependencies: @@ -386,9 +386,9 @@ importers: express: specifier: ^5.2.1 version: 5.2.1 - moltbot: + openclaw: specifier: workspace:* - version: link:../../packages/moltbot + version: link:../.. proper-lockfile: specifier: ^4.1.2 version: 4.1.2 @@ -397,12 +397,12 @@ importers: extensions/nostr: dependencies: - moltbot: - specifier: workspace:* - version: link:../../packages/moltbot nostr-tools: specifier: ^2.20.0 version: 2.20.0(typescript@5.9.3) + openclaw: + specifier: workspace:* + version: link:../.. zod: specifier: ^4.3.6 version: 4.3.6 @@ -439,9 +439,9 @@ importers: specifier: ^4.3.5 version: 4.3.6 devDependencies: - moltbot: + openclaw: specifier: workspace:* - version: link:../../packages/moltbot + version: link:../.. extensions/voice-call: dependencies: @@ -459,9 +459,9 @@ importers: extensions/zalo: dependencies: - moltbot: + openclaw: specifier: workspace:* - version: link:../../packages/moltbot + version: link:../.. undici: specifier: 7.19.0 version: 7.19.0 @@ -471,9 +471,9 @@ importers: '@sinclair/typebox': specifier: 0.34.47 version: 0.34.47 - moltbot: + openclaw: specifier: workspace:* - version: link:../../packages/moltbot + version: link:../.. packages/clawdbot: dependencies: @@ -1328,7 +1328,6 @@ packages: '@lancedb/lancedb@0.23.0': resolution: {integrity: sha512-aYrIoEG24AC+wILCL57Ius/Y4yU+xFHDPKLvmjzzN4byAjzeIGF0TC86S5RBt4Ji+dxS7yIWV5Q/gE5/fybIFQ==} engines: {node: '>= 18'} - cpu: [x64, arm64] os: [darwin, linux, win32] peerDependencies: apache-arrow: '>=15.0.0 <=18.1.0' diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index 2dc4c5325..cf1191d2c 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -351,6 +351,7 @@ export async function compactEmbeddedPiSessionDirect( const sessionLock = await acquireSessionWriteLock({ sessionFile: params.sessionFile, + timeoutMs: params.config?.agents?.defaults?.sessionWriteLockTimeoutMs, }); try { await prewarmSessionFile(params.sessionFile); diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index e83c3ae4a..dae26bb36 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -386,6 +386,7 @@ export async function runEmbeddedAttempt( const sessionLock = await acquireSessionWriteLock({ sessionFile: params.sessionFile, + timeoutMs: params.config?.agents?.defaults?.sessionWriteLockTimeoutMs, }); let sessionManager: ReturnType | undefined; diff --git a/src/agents/session-write-lock.ts b/src/agents/session-write-lock.ts index 82a2428da..24c88f5da 100644 --- a/src/agents/session-write-lock.ts +++ b/src/agents/session-write-lock.ts @@ -107,7 +107,9 @@ export async function acquireSessionWriteLock(params: { release: () => Promise; }> { registerCleanupHandlers(); - const timeoutMs = params.timeoutMs ?? 10_000; + // Default timeout increased from 10s to 60s to prevent premature termination + // when multiple subagents compete for session file locks (see issue #4355) + const timeoutMs = params.timeoutMs ?? 60_000; const staleMs = params.staleMs ?? 30 * 60 * 1000; const sessionFile = path.resolve(params.sessionFile); const sessionDir = path.dirname(sessionFile); diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index a849078ed..906719132 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -123,6 +123,7 @@ export const AgentDefaultsSchema = z blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), humanDelay: HumanDelaySchema.optional(), timeoutSeconds: z.number().int().positive().optional(), + sessionWriteLockTimeoutMs: z.number().int().positive().optional(), mediaMaxMb: z.number().positive().optional(), typingIntervalSeconds: z.number().int().positive().optional(), typingMode: z From 3a9d5788fae7a35f2120f8ba922030d234317b7e Mon Sep 17 00:00:00 2001 From: spiceoogway Date: Thu, 29 Jan 2026 23:46:04 -0500 Subject: [PATCH 2/4] fix: add sessionWriteLockTimeoutMs to AgentDefaultsConfig type Adds the missing TypeScript type definition for the sessionWriteLockTimeoutMs config option, fixing the build error (TS2339). Part of #4355 --- src/config/types.agent-defaults.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index 9c6ce0211..e5cdb18bf 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -196,6 +196,8 @@ export type AgentDefaultsConfig = { }; /** Max concurrent agent runs across all conversations. Default: 1 (sequential). */ maxConcurrent?: number; + /** Timeout (ms) for acquiring per-session write lock. Default: 10000. */ + sessionWriteLockTimeoutMs?: number; /** Sub-agent defaults (spawned via sessions_spawn). */ subagents?: { /** Max concurrent sub-agent runs (global lane: "subagent"). Default: 1. */ From 85ca0ec7892e07f5ffcfa0c2c69ffb5edc2355f6 Mon Sep 17 00:00:00 2001 From: spiceoogway Date: Thu, 29 Jan 2026 23:57:17 -0500 Subject: [PATCH 3/4] fix: format onboard-helpers.ts --- src/commands/onboard-helpers.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts index f56da78e9..774893213 100644 --- a/src/commands/onboard-helpers.ts +++ b/src/commands/onboard-helpers.ts @@ -64,12 +64,12 @@ export function randomToken(): string { export function printWizardHeader(runtime: RuntimeEnv) { const header = [ - "▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄", - "██░▄▄▄░██░▄▄░██░▄▄▄██░▀██░██░▄▄▀██░████░▄▄▀██░███░██", - "██░███░██░▀▀░██░▄▄▄██░█░█░██░█████░████░▀▀░██░█░█░██", - "██░▀▀▀░██░█████░▀▀▀██░██▄░██░▀▀▄██░▀▀░█░██░██▄▀▄▀▄██", - "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀", - " 🦞 OPENCLAW 🦞 ", + "▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄", + "██░▄▄▄░██░▄▄░██░▄▄▄██░▀██░██░▄▄▀██░████░▄▄▀██░███░██", + "██░███░██░▀▀░██░▄▄▄██░█░█░██░█████░████░▀▀░██░█░█░██", + "██░▀▀▀░██░█████░▀▀▀██░██▄░██░▀▀▄██░▀▀░█░██░██▄▀▄▀▄██", + "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀", + " 🦞 OPENCLAW 🦞 ", " ", ].join("\n"); runtime.log(header); From 7c5d26aa9cfd9507aa5e410d1fae4f1bb0990636 Mon Sep 17 00:00:00 2001 From: spiceoogway Date: Fri, 30 Jan 2026 02:36:14 -0500 Subject: [PATCH 4/4] test: add tests for sessionWriteLockTimeoutMs config (#4371) - Test default timeout value (60_000ms) - Test custom timeout from config - Test config type includes the field - Test zod schema validation (positive int, rejects negative/zero/float) --- .../config.session-write-lock-timeout.test.ts | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/config/config.session-write-lock-timeout.test.ts diff --git a/src/config/config.session-write-lock-timeout.test.ts b/src/config/config.session-write-lock-timeout.test.ts new file mode 100644 index 000000000..eecd1c115 --- /dev/null +++ b/src/config/config.session-write-lock-timeout.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "vitest"; +import type { AgentDefaultsConfig } from "./types.agent-defaults.js"; +import { AgentDefaultsSchema } from "./zod-schema.agent-defaults.js"; + +/** + * Helper to resolve session write lock timeout from config. + * Matches the pattern in session-write-lock.ts where default is 60_000ms. + */ +function resolveSessionWriteLockTimeout(config: { + agents?: { defaults?: { sessionWriteLockTimeoutMs?: number } }; +}): number { + return config.agents?.defaults?.sessionWriteLockTimeoutMs ?? 60_000; +} + +describe("sessionWriteLockTimeoutMs config", () => { + it("uses default timeout when unset", () => { + const config = {}; + expect(resolveSessionWriteLockTimeout(config)).toBe(60_000); + }); + + it("uses default timeout when agents.defaults is empty", () => { + const config = { agents: { defaults: {} } }; + expect(resolveSessionWriteLockTimeout(config)).toBe(60_000); + }); + + it("uses custom timeout from config", () => { + const config = { + agents: { + defaults: { + sessionWriteLockTimeoutMs: 30_000, + }, + }, + }; + expect(resolveSessionWriteLockTimeout(config)).toBe(30_000); + }); + + it("accepts large timeout values", () => { + const config = { + agents: { + defaults: { + sessionWriteLockTimeoutMs: 120_000, + }, + }, + }; + expect(resolveSessionWriteLockTimeout(config)).toBe(120_000); + }); + + it("config type includes sessionWriteLockTimeoutMs field", () => { + const config: AgentDefaultsConfig = { + sessionWriteLockTimeoutMs: 45_000, + }; + expect(config.sessionWriteLockTimeoutMs).toBe(45_000); + }); + + it("validates positive integer timeout via zod schema", () => { + const validConfig = { sessionWriteLockTimeoutMs: 30_000 }; + const result = AgentDefaultsSchema.safeParse(validConfig); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.sessionWriteLockTimeoutMs).toBe(30_000); + } + }); + + it("rejects negative timeout values", () => { + const invalidConfig = { sessionWriteLockTimeoutMs: -1000 }; + const result = AgentDefaultsSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + }); + + it("rejects zero timeout value", () => { + const invalidConfig = { sessionWriteLockTimeoutMs: 0 }; + const result = AgentDefaultsSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + }); + + it("rejects non-integer timeout values", () => { + const invalidConfig = { sessionWriteLockTimeoutMs: 1000.5 }; + const result = AgentDefaultsSchema.safeParse(invalidConfig); + expect(result.success).toBe(false); + }); + + it("allows undefined/optional timeout", () => { + const config = {}; + const result = AgentDefaultsSchema.safeParse(config); + expect(result.success).toBe(true); + }); +});