Summary
EmbeddedAttemptSessionTakeoverError: session file changed while embedded prompt lock was released fires deterministically on long-running embedded agent turns because a single run executes across two command lanes at once — a wrapper lane (cron-nested for scheduled jobs, main for delivery/inbound) and the per-session lane (session:agent:<id>:...:run:<runId>). Both lanes append to the same session JSONL, so when one lane releases its prompt-lock during the model call, the other lane's append advances the file fingerprint and the session-file fence aborts the run.
The fence is working as designed — it is detecting a genuine concurrent write. The root cause is the double-lane co-execution of one run, not the fence.
This is the same symptom as #84460 (closed as not-planned) and related to #83510 / #31010, but this report adds a clean cron-path reproduction and a root-cause analysis, since the impact (scheduled jobs failing daily) is significant.
Affected version
OpenClaw 2026.5.24-beta.2 (ghcr.io/openclaw/openclaw:2026.5.24-beta.2), single-host docker gateway.
Evidence (one morning's scheduled cron runs, all aborted)
lane=cron-nested durationMs=180340 agents/cashflow/sessions/<sid>.jsonl
lane=cron-nested durationMs=97280 agents/main/sessions/<sid>.jsonl
lane=cron-nested durationMs=82233 agents/collections/sessions/<sid>.jsonl
lane=cron-nested durationMs=54274 agents/credit/sessions/<sid>.jsonl
Each event is emitted twice in the same millisecond — once for lane=cron-nested and once for lane=session:agent:<id>:cron:<cronId>:run:<runId> — referencing the same session file and the same runId. The interactive delivery path shows the identical pattern with lane=main + lane=session:agent:<id>:discord:channel:<cid>.
Key behavioural signal: the failure scales with run duration. Turns under ~50–55s complete cleanly; turns past that reliably abort (54s, 82s, 97s, 180s above). A direct openclaw agent turn (no wrapper lane) ran 102s clean — i.e. it only fails when a wrapper lane is also in play.
Root cause analysis
applyGatewayLaneConcurrency() wires independent lanes: cron, cron-nested, main, subagent. A scheduled/delivered run is enqueued on the wrapper lane and executed on a per-session lane; both end up invoking the embedded runner against the same session file concurrently.
- The session-file fence (
assertSessionFileFence → sessionFenceAdvanceIsBenign) only treats a concurrent append as benign when every appended line passes isTranscriptOnlyOpenClawAssistantLine. The wrapper lane's concurrent writes include non-transcript lines (tool calls, etc.), so the append is judged non-benign and the run throws EmbeddedAttemptSessionTakeoverError.
Proposed fix (at root)
Either:
- (Preferred) Single-lane execution: ensure one run executes in exactly one lane — the wrapper (
cron-nested/main) should delegate to and await the session lane rather than co-executing the embedded turn, so only one writer touches the session file; or
- Ownership tracking: register the wrapper lane's writes via
recordOwnedSessionFileWrite(...) so assertSessionFileFence recognises them as owned (same-process) writes instead of foreign takeovers.
Note: simply widening sessionFenceAdvanceIsBenign (e.g. return lines.length > 0) suppresses the error but masks real concurrent-write contention and risks interleaved session corruption — it is a workaround, not a fix.
Impact
Repro
- Configure an agent with a tool-using task that takes >60s of model time.
- Trigger it via the
cron-nested (scheduled) or main (delivery) path — not a direct agent invocation.
- Observe paired
lane task error entries for the wrapper lane and the session lane against the same session file, with EmbeddedAttemptSessionTakeoverError.
Summary
EmbeddedAttemptSessionTakeoverError: session file changed while embedded prompt lock was releasedfires deterministically on long-running embedded agent turns because a single run executes across two command lanes at once — a wrapper lane (cron-nestedfor scheduled jobs,mainfor delivery/inbound) and the per-session lane (session:agent:<id>:...:run:<runId>). Both lanes append to the same session JSONL, so when one lane releases its prompt-lock during the model call, the other lane's append advances the file fingerprint and the session-file fence aborts the run.The fence is working as designed — it is detecting a genuine concurrent write. The root cause is the double-lane co-execution of one run, not the fence.
This is the same symptom as #84460 (closed as not-planned) and related to #83510 / #31010, but this report adds a clean cron-path reproduction and a root-cause analysis, since the impact (scheduled jobs failing daily) is significant.
Affected version
OpenClaw 2026.5.24-beta.2(ghcr.io/openclaw/openclaw:2026.5.24-beta.2), single-host docker gateway.Evidence (one morning's scheduled cron runs, all aborted)
Each event is emitted twice in the same millisecond — once for
lane=cron-nestedand once forlane=session:agent:<id>:cron:<cronId>:run:<runId>— referencing the same session file and the same runId. The interactive delivery path shows the identical pattern withlane=main+lane=session:agent:<id>:discord:channel:<cid>.Key behavioural signal: the failure scales with run duration. Turns under ~50–55s complete cleanly; turns past that reliably abort (54s, 82s, 97s, 180s above). A direct
openclaw agentturn (no wrapper lane) ran 102s clean — i.e. it only fails when a wrapper lane is also in play.Root cause analysis
applyGatewayLaneConcurrency()wires independent lanes:cron,cron-nested,main,subagent. A scheduled/delivered run is enqueued on the wrapper lane and executed on a per-session lane; both end up invoking the embedded runner against the same session file concurrently.assertSessionFileFence→sessionFenceAdvanceIsBenign) only treats a concurrent append as benign when every appended line passesisTranscriptOnlyOpenClawAssistantLine. The wrapper lane's concurrent writes include non-transcript lines (tool calls, etc.), so the append is judged non-benign and the run throwsEmbeddedAttemptSessionTakeoverError.Proposed fix (at root)
Either:
cron-nested/main) should delegate to and await the session lane rather than co-executing the embedded turn, so only one writer touches the session file; orrecordOwnedSessionFileWrite(...)soassertSessionFileFencerecognises them as owned (same-process) writes instead of foreign takeovers.Note: simply widening
sessionFenceAdvanceIsBenign(e.g.return lines.length > 0) suppresses the error but masks real concurrent-write contention and risks interleaved session corruption — it is a workaround, not a fix.Impact
maxConcurrentRuns,fallbacksconfirmed ineffective in [Bug]: EmbeddedAttemptSessionTakeoverError on every message — secondary lane always fails with session file lock contention #84460).Repro
cron-nested(scheduled) ormain(delivery) path — not a directagentinvocation.lane task errorentries for the wrapper lane and the session lane against the same session file, withEmbeddedAttemptSessionTakeoverError.