diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95b940c97..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: {} @@ -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/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); + }); +}); 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. */ 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