Skip to content

fix(claude-code): attribute parallel subagent TodoWrite checkpoints via agent_id - #2068

Open
suhaanthayyil wants to merge 9 commits into
mainfrom
fix/track-2062
Open

fix(claude-code): attribute parallel subagent TodoWrite checkpoints via agent_id#2068
suhaanthayyil wants to merge 9 commits into
mainfrom
fix/track-2062

Conversation

@suhaanthayyil

@suhaanthayyil suhaanthayyil commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

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-only FindActivePreTaskFile.

Why / how it helps: Sibling parallel Tasks each write pre-task-*.json; the newest-file heuristic misfiles progress under the wrong tool_use_id. Claude sends agent_id on in-subagent PostToolUse hooks but Entire dropped it.

How: Capture agent_id on SubagentCheckpointHookInput; 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'
  • Package tests green in worktree validation

Fixes #2062

…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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_id in PostToolUse hook payloads and use it to resolve the correct parent Task for incremental TodoWrite checkpoints.
  • Persist an agent_idtool_use_id link 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.

Comment thread cmd/entire/cli/state.go
Comment thread cmd/entire/cli/state_test.go Outdated
@suhaanthayyil
suhaanthayyil marked this pull request as ready for review August 19, 2026 18:07
@suhaanthayyil
suhaanthayyil requested a review from a team as a code owner August 19, 2026 18:07
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
@suhaanthayyil

Copy link
Copy Markdown
Contributor Author

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.

suhaanthayyil and others added 3 commits August 19, 2026 14:38
Entire-Checkpoint: 01M0DN2EVMV7WD1072G7R1VQ6A
Co-authored-by: Cursor <cursoragent@cursor.com>
Entire-Checkpoint: 01M0DSM507MG5ZE6188N7NWW9J
@suhaanthayyil

Copy link
Copy Markdown
Contributor Author

Fixed: bootstrap now prefers an unclaimed pre-task file (FindUnclaimedActivePreTaskFile) so two siblings racing on their first PostTodo don't both latch onto the same mtime winner; a warning is logged when it must fall back to the plain mtime heuristic (no unclaimed pre-task left). Landed in 3e518bd.

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
@suhaanthayyil

Copy link
Copy Markdown
Contributor Author

Fixed: the unclaimed pre-task lookup and the agent-task link write are now serialized with a flock (agentTaskBootstrapLockPath), re-checking LookupAgentTaskLink after acquiring it in case a racing sibling resolved us first. Added a goroutine-based regression test that reliably reproduces the double-claim without the lock and passes with it under -race. Landed in f2de78d.

suhaanthayyil and others added 2 commits August 19, 2026 16:30
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
@suhaanthayyil

Copy link
Copy Markdown
Contributor Author

Fixed two more findings from the re-review of the previous fix:

  • Broken symlink (same accidental-commit pattern as fix(cursor): correlate subagentStop without subagent_id via task description #2067): untracked scripts/entire-dev in d0dcd47 — it was swept into 3e518bd by mistake.
  • Lock treated as best-effort: flock.Acquire failing during bootstrap was only logged, then execution fell through to the unprotected unclaimed-lookup + link-write, recreating the exact race the lock was meant to close. Now returns early on lock failure (a6b6c95), with a regression test that forces the lock to fail deterministically.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Claude Code parallel Task subagents misattribute TodoWrite incremental checkpoints

2 participants