Skip to content

fix(session-fork): skip fork when parent size is unresolved instead of forking (#101718)#101767

Closed
LZY3538 wants to merge 3 commits into
openclaw:mainfrom
LZY3538:fix/session-fork-token-timeout-101718
Closed

fix(session-fork): skip fork when parent size is unresolved instead of forking (#101718)#101767
LZY3538 wants to merge 3 commits into
openclaw:mainfrom
LZY3538:fix/session-fork-token-timeout-101718

Conversation

@LZY3538

@LZY3538 LZY3538 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

resolveParentForkTokenCount reads the parent session transcript to estimate token count when forking a session, but had no timeout. On large transcripts (hundreds of MB) or slow filesystems (NFS, FUSE, saturated SSD), this blocks session creation indefinitely, accumulating blocked async contexts that exhaust gateway connection slots (#101718).

Why This Change Was Made (revised after review)

The first revision added a 2-second Promise.race timeout that returned undefined on timeout. Review correctly flagged this as unsafe (P1): undefined makes resolveParentForkDecision return {status: "fork"}, and the fork runtime (forkSessionFromParentRuntimereadForkSourceTranscriptreadRegularFile) then reads the entire parent transcript with no bound. So a slow/huge parent could still stall session creation after the timeout fired, and an oversized parent that should start with isolated context was forked instead — bypassing the documented 100K-token skip.

This revision fixes the root cause: never fork after an unresolved size check.

  • The 2s deadline still guards the full token resolution (which can read up to ~1MB of transcript tail, or the whole transcript index when leaf-control is invalid).
  • On timeout, fall back to a fast, stat-only byte estimate (estimateParentForkTokensFromSizeRuntime). A single fs.stat is O(1) regardless of file size, so a multi-hundred-MB parent still yields an estimate → skip (isolated context), never a whole-file read.
  • If even the stat probe cannot answer within 1s, the parent size is treated as unresolved and the fork is skipped conservatively (new parent-size-unresolved decision), rather than forked.

Net effect: the fork read is only ever entered for a parent whose size was confirmed small, so it is inherently bounded. Oversized or unresolvable parents skip to isolated context — the safe, documented behavior.

Evidence

Layer 1 — Real behavior proof (slow filesystem, real modules, no mocks)

scripts/proof-session-fork-timeout.ts drives the real resolveParentForkDecision / forkSessionEntryFromParent against real on-disk transcripts, injecting fs latency to simulate a slow mount (the exact issue scenario). Elapsed times prove the deadline is enforced and the fork read is never entered for an oversized/unresolvable parent:

$ npx tsx scripts/proof-session-fork-timeout.ts
PR #101767 real-behavior proof (#101718) — slow-filesystem parent fork

HEAD: 437b4dd1da
worktree: clean

  ✅ Baseline: raw token resolution blocks on a slow transcript read — unguarded runtime took 3014ms (this is what the 2s deadline must bound)
  ✅ Case 1a: oversized parent decision is bounded by the deadline — decided in 2001ms (< 2.9s) instead of blocking on the 3s read
  ✅ Case 1b: oversized parent SKIPS the fork via the stat-only estimate — decision=skip/parent-too-large
  ✅ Case 1c: forkSessionEntryFromParent returns skipped (no whole-file read) — status=skipped in 2006ms
  ✅ Case 2: small parent forks with a preserved conservative estimate — decision=fork parentTokens=519 in 2004ms
  ✅ Case 3a: unresolvable size is still bounded (deadline + stat fallback) — decided in 3020ms even with stat itself hanging
  ✅ Case 3b: unresolvable size SKIPS conservatively instead of forking — decision=skip/parent-size-unresolved

7 passed, 0 failed

The Baseline case shows the unguarded runtime blocking 3s on a slow read (what the deadline must bound); Case 1c proves the storage-boundary entry point returns skipped in ~2s for an oversized parent — it never enters the unbounded fork read that review flagged.

Layer 2 — Focused unit tests

$ vitest run --config test/vitest/vitest.auto-reply-reply.config.ts src/auto-reply/reply/session-fork.test.ts

 ✓ forkSessionEntryFromParent > forks transcripts in the directory for the store being mutated
 ✓ forkSessionEntryFromParent > skips the fork read entirely when the parent size cannot be resolved (#101718)
 ✓ resolveParentForkDecision > returns fork with token count when the runtime resolves quickly
 ✓ resolveParentForkDecision > returns skip when parent is too large
 ✓ resolveParentForkDecision > falls back to the byte estimate and forks a small parent when the runtime hangs
 ✓ resolveParentForkDecision > falls back to the byte estimate and skips an oversized parent when the runtime hangs
 ✓ resolveParentForkDecision > skips with an unresolved reason when both the runtime and the size probe hang

 Test Files  1 passed (1)
      Tests  7 passed (7)

The prior revision's test asserted the buggy behavior (fork with no parentTokens on timeout); it is replaced with the fallback/skip coverage above.

Layer 3 — Static checks

$ oxfmt --check <changed files>       → All matched files use the correct format.
$ oxlint <changed files>              → clean (0 warnings)

Type checks (tsgo:core) run in CI on this branch.

Changes

File + -
src/auto-reply/reply/session-fork.ts +66 -3
src/auto-reply/reply/session-fork.runtime.ts +12 -0
src/auto-reply/reply/session-parent-fork-prepare.ts +7 -4
src/auto-reply/reply/session-fork.test.ts +140 -1
scripts/proof-session-fork-timeout.ts (proof only) +229 -0

Closes #101718.

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: M labels Jul 7, 2026
…penclaw#101718)

resolveParentForkTokenCount reads the parent session transcript file
to estimate token count, but had no timeout.  On large files or slow
filesystems this could block session creation indefinitely.

Race the runtime call against a 2-second deadline.  On timeout return
undefined, which resolveParentForkDecision already handles correctly
by proceeding with fork without a token estimate.

Co-Authored-By: Claude <[email protected]>
@LZY3538
LZY3538 force-pushed the fix/session-fork-token-timeout-101718 branch from 610462c to 153d111 Compare July 7, 2026 15:54
@openclaw-barnacle openclaw-barnacle Bot added size: S and removed agents Agent runtime and tooling size: M labels Jul 7, 2026
@clawsweeper

clawsweeper Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Close as superseded: the revised branch addresses the prior unsafe timeout-to-fork concern, but it is now conflicting against current main and the open proof-sufficient #101932 carries the same fix on the current accessor-based fork path.

Root-cause cluster
Relationship: superseded
Canonical: #101932
Summary: This PR and the canonical replacement PR address the same parent-fork token-probe timeout; the replacement is the viable current-base landing path.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Canonical path: Review and land or reject #101932 as the canonical current-base fix for #101718; reopen or rebase this PR only if that replacement path stops being viable.

So I’m closing this here and keeping the remaining discussion on #101932 and #101718.

Review details

Best possible solution:

Review and land or reject #101932 as the canonical current-base fix for #101718; reopen or rebase this PR only if that replacement path stops being viable.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection on current main shows parent-fork decisions still await token-count resolution before returning, and the linked issue plus PR proof describe the slow-transcript path.

Is this the best way to solve the issue?

No for this branch as the landing path. The revised fix shape is useful, but #101932 applies the same current behavior to the post-accessor code and is the safer canonical path.

Security review:

Security review cleared: The diff changes session-fork logic, tests, and a local proof script; no dependency, workflow, secret, package, or supply-chain surface changed.

AGENTS.md: found and applied where relevant.

What I checked:

  • Repository policy read: Root AGENTS.md and scripts/AGENTS.md were read and applied for whole-path PR review, session-state merge risk, proof requirements, and script-surface context. (AGENTS.md:17, 534ace4d8a49)
  • Current main still has the unbounded decision await: Current main awaits resolveParentForkTokenCount before returning fork or skip, so the linked availability bug is not fixed on main yet. (src/auto-reply/reply/session-fork.ts:107, 534ace4d8a49)
  • Current main fork decision type is accessor-owned: Current main aliases ParentForkDecision to SessionParentForkDecision in the accessor boundary, whose current skip union only includes parent-too-large. (src/config/sessions/session-accessor.ts:1385, 534ace4d8a49)
  • This PR fixes the earlier unsafe timeout shape: At this PR head, resolveParentForkDecision returns skip/parent-size-unresolved after both the full token resolution and stat-only size probe fail to resolve in time. (src/auto-reply/reply/session-fork.ts:136, 437b4dd1da4a)
  • This PR is not a clean current-base landing path: Live GitHub reports this PR as CONFLICTING/DIRTY at head 437b4dd. (437b4dd1da4a)
  • Canonical replacement PR is viable: The sibling PR is open, MERGEABLE/BEHIND, proof-sufficient, and ready for maintainer look while updating the current accessor decision type with timeout and unavailable skip reasons. (src/config/sessions/session-accessor.ts:1385, 36e6ece642f0)

Likely related people:

  • jalehman: Authored and merged the parent-session fork accessor-boundary refactor that now owns the fork transcript and entry persistence path this fix must patch. (role: current adjacent refactor owner; confidence: high; commits: a68caa185bf1; files: src/auto-reply/reply/session-fork.ts, src/auto-reply/reply/session-fork.runtime.ts, src/auto-reply/reply/session-parent-fork-prepare.ts)
  • steipete: Authored the merged session controls work that introduced the fork-capable session creation surface and touched the affected fork and gateway paths. (role: feature introducer; confidence: high; commits: 88f1ec38d4a5; files: src/auto-reply/reply/session-fork.ts, src/auto-reply/reply/session-fork.runtime.ts, src/gateway/session-create-service.ts)

Codex review notes: model internal, reasoning high; reviewed against 534ace4d8a49.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. labels Jul 7, 2026
…f forking (openclaw#101718)

The prior 2s token-resolution timeout returned `undefined`, which made
`resolveParentForkDecision` fork the parent. The fork runtime then reads the
whole parent transcript unbounded (`readForkSourceTranscript` → `readRegularFile`),
so a slow/huge parent still stalled session creation *after* the timeout fired
and bypassed the documented oversized-parent skip (review P1).

Never fork after an unresolved size check: on timeout, fall back to a fast,
stat-only byte estimate (`estimateParentForkTokensFromSizeRuntime`) so an
oversized parent still skips to isolated context; if even the stat probe times
out, skip conservatively via a new `parent-size-unresolved` decision. The fork
read is now only entered for a parent whose size was confirmed small.

Adds unit coverage for the fallback/skip paths and a self-contained
real-behavior proof (`scripts/proof-session-fork-timeout.ts`) that drives the
real modules against real transcripts with injected filesystem latency.

Co-Authored-By: Claude <[email protected]>
@openclaw-barnacle openclaw-barnacle Bot added scripts Repository scripts size: M and removed size: S labels Jul 8, 2026
@LZY3538 LZY3538 changed the title fix(session-fork): add timeout to parent-fork token count resolution (#101718) fix(session-fork): skip fork when parent size is unresolved instead of forking (#101718) Jul 8, 2026
…lpers

Wrap the setTimeout calls in the deadline/sleep Promise executors in block
bodies so the executor no longer returns the timer handle (oxlint
no-promise-executor-return).

Co-Authored-By: Claude <[email protected]>
@LZY3538

LZY3538 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the P1 ("Do not fork after a timed-out size check"). The revised fix no longer forks a parent whose size is unresolved.

What changed (437b4dd1da):

  • On the 2s token-resolution timeout, fall back to a fast, stat-only byte estimate (estimateParentForkTokensFromSizeRuntime). fs.stat is O(1) regardless of file size, so a multi-hundred-MB parent still resolves to an estimate → skip to isolated context, never the unbounded readForkSourceTranscript whole-file read.
  • If even the stat probe can't answer within 1s, the size is unresolved and the fork is skipped conservatively (new parent-size-unresolved decision) instead of forked.
  • The fork read is now only ever entered for a parent whose size was confirmed small — so it is inherently bounded, and the documented oversized-parent skip is preserved.

Real behavior proof (was the P1 proof ask): added scripts/proof-session-fork-timeout.ts, which drives the real resolveParentForkDecision/forkSessionEntryFromParent against real transcripts with injected fs latency (simulating the NFS/FUSE/saturated-SSD scenario). It shows the unguarded read blocking 3s, then the fixed path deciding in ~2s and returning skipped for an oversized parent — never entering the whole-file read. Full output + the 7 focused unit tests are in the PR body.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 8, 2026
@clawsweeper clawsweeper Bot added the merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. label Jul 8, 2026
@LZY3538 LZY3538 closed this Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. scripts Repository scripts size: M status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

session-fork parent token count resolution has no timeout and blocks session creation indefinitely

1 participant