Skip to content

fix(session-fork): add 2s timeout to parent-fork token count resolution#102218

Closed
hugenshen wants to merge 1 commit into
openclaw:mainfrom
hugenshen:fix/101718-parent-fork-token-count-timeout
Closed

fix(session-fork): add 2s timeout to parent-fork token count resolution#102218
hugenshen wants to merge 1 commit into
openclaw:mainfrom
hugenshen:fix/101718-parent-fork-token-count-timeout

Conversation

@hugenshen

Copy link
Copy Markdown
Contributor

Summary

  • Problem: resolveParentForkTokenCountRuntime reads the parent transcript file and parses recent usage with no timeout or abort boundary. On a large transcript (hundreds of MB) or a slow filesystem (NFS, FUSE, saturated SSD), the async call can hang indefinitely, blocking every new thread/reply in the session and exhausting async context slots in the gateway.
  • Fix: Wrap the entire transcript-stat + transcript-read path in a withTimeout(2 000 ms). Before the timeout fires, the function accumulates a bestEffortTokens value (cached totalTokens from the store entry, then the byte-size estimate from fs.stat). If the deadline is exceeded the timeout error is caught and the best-effort value is returned, keeping session creation moving.
  • Out of scope: Increasing the timeout value or making it configurable — 2 s is conservative enough for local SSDs and generous enough to survive a slow-cold read. Config surfacing can come later if needed.
  • Reviewers: Focus on the bestEffortTokens mutation inside the withTimeout closure and whether the catch path correctly returns the last accumulated value.

Linked context

Closes #101718

Real behavior proof (required for external PRs)

  • Behavior or issue addressed: Parent-fork token-count resolution no longer hangs when the parent transcript is unreadable within 2 seconds; session creation falls back to cached or byte-estimate token counts.
  • Real environment tested: macOS 15 (Apple M-series), Node 22.19, local dev checkout (pnpm dev not running — unit-test-only proof; no live gateway needed for this path).
  • Exact steps or command run after this patch:
    node scripts/run-vitest.mjs src/auto-reply/reply/session-fork.runtime.test.ts --run --reporter=verbose
    node scripts/run-vitest.mjs src/auto-reply/reply/session-fork.test.ts --run --reporter=verbose
    
  • Evidence after fix:
    ✓ resolveParentForkTokenCountRuntime > falls back to cached parent tokens when transcript usage hangs 2ms
    ✓ resolveParentForkTokenCountRuntime > keeps the byte estimate when transcript usage times out after stat succeeds 3ms
    ✓ resolveParentForkDecision > keeps forking when the runtime cannot produce a bounded parent token count 1ms
    
    Test Files  1 passed (1)  |  Tests  8 passed (8)
    Test Files  1 passed (1)  |  Tests  2 passed (2)
    
  • Observed result after fix: Both timeout cases resolve correctly — cached tokens returned when stat also hangs, byte estimate returned when stat succeeds before the deadline, and the fork decision degrades gracefully to { status: "fork", maxTokens: 100_000 } when runtime returns undefined.
  • What was not tested: Live gateway session creation against a real NFS/FUSE mount; macOS CI cross-path (Linux CI is the truth target). The 2 s wall-clock timeout is covered by vi.useFakeTimers + vi.advanceTimersByTimeAsync.
  • Proof limitations or environment constraints: vi.useFakeTimers drives the timeout, so this is deterministic unit proof rather than real-clock wall-time proof. The production path (withTimeout from infra/fs-safe) uses real setTimeout; the unit tests confirm the branching logic is correct.

Tests and validation

  • node scripts/run-vitest.mjs src/auto-reply/reply/session-fork.runtime.test.ts --run --reporter=verbose — 8/8 pass
  • node scripts/run-vitest.mjs src/auto-reply/reply/session-fork.test.ts --run --reporter=verbose — 2/2 pass
  • Regression test added: two new tests in session-fork.runtime.test.ts (timeout with cached fallback; timeout with byte-estimate fallback); one new test in session-fork.test.ts (fork decision when runtime returns undefined)

Risk checklist

  • Did user-visible behavior change? No — the resolution path is internal to session-fork; the fork/skip decision exposed to callers is unchanged for the normal (non-timeout) case.
  • Did config, environment, or migration behavior change? No — no new config keys or env vars.
  • Did security, auth, secrets, network, or tool execution behavior change? No.
  • Highest-risk area: the bestEffortTokens variable is mutated inside the async closure before withTimeout resolves; if estimateParentTranscriptTokensFromBytes throws synchronously that value stays undefined and the catch returns undefined — same as the old behavior.
  • Mitigation: estimateParentTranscriptTokensFromBytes already has its own try/catch and returns undefined on error; the only new failure mode is the timeout path, which is tested.

Current review state

  • Next action: awaiting maintainer review
  • Bot comments addressed: none yet
  • AI-assisted: yes (Cursor / Claude Sonnet 4.6); human-run Vitest output pasted above; codex review not run (no local Codex binary available in this checkout).

Wrap the transcript read + byte-stat path in a withTimeout so a slow or
very large parent transcript cannot stall session creation indefinitely.
On timeout the caller falls back to cached totalTokens or the byte-size
estimate already accumulated before the deadline, whichever is larger.

Fixes openclaw#101718
@clawsweeper

clawsweeper Bot commented Jul 8, 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: this branch is a useful focused attempt, but its timeout fallback can still proceed into parent transcript copying, while the sibling PR is open, mergeable, proof-positive, and handles unresolved parent sizing without inheriting unsafe parent context.

Root-cause cluster
Relationship: superseded
Canonical: #101932
Summary: This PR and sibling PRs target the same parent-fork token-probe timeout; the proof-positive sibling is the safer canonical landing candidate.

Members:

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

Canonical path: Continue review on #101932, or an equivalent fail-closed timeout shape, and land one canonical fix after maintainer acceptance of unknown-size parent-fork semantics.

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

Review details

Best possible solution:

Continue review on #101932, or an equivalent fail-closed timeout shape, and land one canonical fix after maintainer acceptance of unknown-size parent-fork semantics.

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

Yes from source inspection: current main awaits parent-token resolution with byte/line bounds but no wall-clock timeout, and this PR's fallback can still flow into the parent transcript copy path. I did not run a live slow-filesystem reproduction in this read-only review.

Is this the best way to solve the issue?

No. This branch is a plausible partial mitigation, but the safer solution is the sibling fail-closed path that prevents unresolved parent sizing from entering transcript copy and has production-like terminal proof.

Security review:

Security review cleared: The diff changes session-fork runtime logic and adjacent tests only; no dependency, workflow, secret, package, permission, or supply-chain surface is changed.

AGENTS.md: found and applied where relevant.

What I checked:

  • Current PR timeout fallback: At PR head, timeout/error handling returns bestEffortTokens; when that is cached, underestimated, or undefined, the caller can still treat the parent as forkable. (src/auto-reply/reply/session-fork.runtime.ts:121, 8c2fca47ed39)
  • Caller forks unknown-size parents: Current main returns fork whenever parentTokens is not a number, so an unresolved token probe can continue into transcript copying. (src/auto-reply/reply/session-fork.ts:124, c69724ef3b50)
  • Fork copy reads parent transcript: The parent transcript fork runtime reads the parent transcript with readRegularFile, so allowing a timed-out or unknown-size parent through the fork path can re-enter parent transcript reading. (src/config/sessions/session-fork-transcript.runtime.ts:86, c69724ef3b50)
  • Superseding candidate: The sibling PR is open, mergeable, labeled proof: sufficient, linked to the same issue, and changes timeout/unavailable parent sizing into explicit skip decisions with terminal proof showing zero fork transcripts created. (src/auto-reply/reply/session-fork.ts:124, 36e6ece642f0)
  • Canonical search: Live PR search found this PR plus sibling timeout PRs for the same issue; the proof-positive sibling is the only ready canonical landing candidate among the open duplicate attempts.
  • Current-line provenance: Blame for the current parent-fork token-count runtime and decision lines points to commit 4115e83, which carried the unbounded await shape on current main. (src/auto-reply/reply/session-fork.runtime.ts:54, 4115e832d833)

Likely related people:

  • Vincent Koc: Blame for the current parent-fork decision/runtime lines and prior adjacent session cleanup points to Vincent Koc’s recent commits. (role: recent area contributor; confidence: high; commits: 4115e832d833, bf0d2d70be51; files: src/auto-reply/reply/session-fork.ts, src/auto-reply/reply/session-fork.runtime.ts)
  • Peter Steinberger: The linked issue cites, and git history confirms, the session controls/operator-fork commit that made the gateway fork path visible. (role: prior feature author; confidence: medium; commits: 88f1ec38d4a5; files: src/gateway/session-create-service.ts, src/auto-reply/reply/session-fork.ts)

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

@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. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. labels Jul 8, 2026
@hugenshen hugenshen 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: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. size: M status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

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