fix(claude-code): attribute parallel subagent TodoWrite checkpoints via agent_id - #2068
fix(claude-code): attribute parallel subagent TodoWrite checkpoints via agent_id#2068suhaanthayyil wants to merge 9 commits into
Conversation
…ia agent_id Sibling parallel Tasks each write their own pre-task-*.json file, and FindActivePreTaskFile's "most recently modified" heuristic misattributes TodoWrite checkpoints to whichever sibling's file is newest. Capture the top-level agent_id Claude Code sends on PostToolUse hooks and remember a durable agent->task link on first resolution so later checkpoints from the same subagent instance stick to their own task.
There was a problem hiding this comment.
Pull request overview
This PR fixes Claude Code’s incremental TodoWrite checkpoint attribution when multiple sibling parallel Tasks run concurrently, by using the subagent’s agent_id to reliably map incremental checkpoints back to the correct parent Task instead of relying on “most recently modified” pre-task file heuristics.
Changes:
- Capture Claude Code’s top-level
agent_idinPostToolUsehook payloads and use it to resolve the correct parent Task for incrementalTodoWritecheckpoints. - Persist an
agent_id→tool_use_idlink under.entire/tmp/and clean up links when the corresponding task pre-task state is cleaned up. - Add unit tests covering link persistence/cleanup and the sibling-parallel-task attribution scenario.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| cmd/entire/cli/state.go | Adds agent→task link persistence/lookup and cleanup tied to pre-task state cleanup. |
| cmd/entire/cli/state_test.go | Adds tests for agent→task link lifecycle and cleanup behavior. |
| cmd/entire/cli/hooks.go | Extends hook input model to capture Claude Code’s top-level agent_id. |
| cmd/entire/cli/hooks_test.go | Adds coverage ensuring agent_id is parsed from hook payloads. |
| cmd/entire/cli/hooks_claudecode_posttodo.go | Resolves incremental checkpoint task attribution via agent_id-based link before falling back to mtime heuristic. |
| cmd/entire/cli/hooks_claudecode_posttodo_test.go | New tests validating bootstrap + sibling-parallel attribution using the remembered link. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Copilot review on #2068: - ValidateToolUseID allows an empty string (optional field elsewhere), so RememberAgentTaskLink could write an agent-task link file with an empty tool_use_id. LookupAgentTaskLink rejects empty ToolUseID on read, so that link would be written and then silently never found. Reject empty taskToolUseID explicitly before validation. - setupTmpDirRepo hand-rolled a partial .git/ directory; switched to the existing testutil.InitRepo helper for consistency with the rest of the suite. Entire-Checkpoint: 01M0DMHTVP68KPF50N3MY702HE
|
Addressed Copilot review in 8015d65: RememberAgentTaskLink now explicitly rejects an empty taskToolUseID before validation (ValidateToolUseID alone allows empty, which would have written an unusable link file that LookupAgentTaskLink can never find); setupTmpDirRepo now uses testutil.InitRepo instead of hand-rolling a partial .git/ directory. |
Entire-Checkpoint: 01M0DN2EVMV7WD1072G7R1VQ6A
Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M0DSM507MG5ZE6188N7NWW9J
|
Fixed: bootstrap now prefers an unclaimed pre-task file ( |
FindUnclaimedActivePreTaskFile's read and RememberAgentTaskLink's write are separate OS processes per PostTodo hook invocation. Two siblings' first PostTodos firing at the same instant could both read the same unclaimed pre-task before either had written its claim, double-claiming one task and starving another. Guard the check-then-write with a fixed flock (agentTaskBootstrapLockPath), re-checking LookupAgentTaskLink after acquiring it in case a racing sibling already resolved us while we waited. Added a goroutine-based regression test that reliably reproduces the double-claim without the lock and passes with it, including under -race. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M0DT4WFNNYRVA6BB2MAZQW9K
|
Fixed: the unclaimed pre-task lookup and the agent-task link write are now serialized with a flock ( |
scripts/entire-dev was committed as a symlink to an absolute, machine-specific path (/Users/suhaan/devenv/cli/scripts/entire-dev), swept into an unrelated commit (3e518bd) by mistake. The real launcher script was intentionally removed from the repo; untrack the stray local symlink. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M0DVG9QBG52HX9X69RR00FQ3
…st-effort flock.Acquire failing during resolveIncrementalCheckpointTask's bootstrap was only logged, then execution fell through to the unprotected FindUnclaimedActivePreTaskFile + RememberAgentTaskLink sequence — recreating the exact double-claim TOCTOU race the lock exists to close. Return early on lock failure instead: this call loses at most one incremental checkpoint, and the next TodoWrite from the same agentID retries the whole bootstrap. Added a regression test that forces flock.Acquire to fail deterministically (a directory at the lock path) and asserts no link is written. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M0DVHDCGM6NX5G6R20E41FHJ
|
Fixed two more findings from the re-review of the previous fix:
|
Four gaps in the agent->task link path: Lookups trusted any non-empty stored tool_use_id. The value is re-validated on read, since it keys the task metadata paths, and the link is only honored while its task is still active. Cleanup's link removal is best effort, so a failed cleanup previously pinned a resumed agent to a finished task forever; a dangling link is now deleted so the caller bootstraps fresh. Cleanup dropped the links before the pre-task file, briefly exposing a task as unclaimed while its state file still existed, so a sibling could claim a task already being torn down and leave a link cleanup no longer saw. Remove the target first and serialize both removals under the bootstrap lock. A failed claim write was logged and the task returned anyway, checkpointing against a claim no sibling could see. Fail closed like the lock-acquisition path and retry on a later TodoWrite. Bootstrap claimed the newest unclaimed pre-task. Siblings are spawned in tool-call order, so mtimes ascend in spawn order and the newest file names the most recently spawned task, making the assignment the reverse of the truth whenever subagents report in launch order. Claim oldest-first, and warn when several unclaimed candidates exist so a misattribution is diagnosable. This narrows the error, it does not eliminate it: agent_id and the parent tool_use_id only appear together on SubagentEnd, after every one of that subagent's TodoWrites. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M0KQYKB7K4VA7R69HCRVA8QP
Trail: https://entire.io/gh/entireio/cli/trails/1099
What: Attribute Claude Code incremental TodoWrite checkpoints to the correct parallel Task using
agent_id→ remembered task links instead of mtime-onlyFindActivePreTaskFile.Why / how it helps: Sibling parallel Tasks each write
pre-task-*.json; the newest-file heuristic misfiles progress under the wrongtool_use_id. Claude sendsagent_idon in-subagent PostToolUse hooks but Entire dropped it.How: Capture
agent_idonSubagentCheckpointHookInput; remember/lookup.entire/tmp/agent-task-<id>.json; prefer link over mtime; purge links on pre-task cleanup.Testing:
go test ./cmd/entire/cli/ -run 'AgentTask|ResolveIncremental|PostTodo|SubagentCheckpoint'Fixes #2062