fix(cursor): correlate subagentStop without subagent_id via task description - #2067
fix(cursor): correlate subagentStop without subagent_id via task description#2067suhaanthayyil wants to merge 10 commits into
Conversation
…ription Cursor's subagentStop payload has no subagent_id field (only subagentStart does), so parseSubagentStop produced an empty ToolUseID/SubagentID. That caused LoadPreTaskState to miss the real pre-task-<id>.json file written at SubagentStart, spuriously treating all untracked files as new, orphaning the pre-task file, and colliding across parallel subagents on the same empty key. Add ResolvePreTaskToolUseID to recover the real id: trust a non-empty ToolUseID as before, else match pre-task files by stored TaskDescription (most recent on multiple matches), else fall back to the single active pre-task file when unambiguous. handleLifecycleSubagentEnd now resolves the id before any pre-task-state lookup so SaveTaskStep/Cleanup key off the right file.
There was a problem hiding this comment.
Pull request overview
This PR fixes Cursor subagent Stop correlation when subagent_id is missing by recovering the correct pre-task correlation key from data captured at SubagentStart (primarily the task description), preventing orphaned pre-task state files and collisions between parallel subagents.
Changes:
- Extend
PreTaskStateto persist aTaskDescription, and addCapturePreTaskStateWithMetato record it at SubagentStart. - Add
ResolvePreTaskToolUseIDto map SubagentEnd events with an empty ID back to the correctpre-task-<id>.jsonusing task description (or a single unambiguous active pre-task file). - Update lifecycle handling and tests to exercise the missing-ID Cursor Stop path and the resolver behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| cmd/entire/cli/state.go | Stores task description in pre-task state and introduces ID resolution logic for ID-less SubagentEnd payloads. |
| cmd/entire/cli/state_test.go | Adds unit tests covering resolver behavior and TaskDescription persistence. |
| cmd/entire/cli/lifecycle.go | Captures task description at SubagentStart and resolves missing IDs at SubagentEnd before pre-task state lookup/cleanup. |
| cmd/entire/cli/lifecycle_test.go | Adds an integration-style lifecycle test reproducing Cursor’s missing-ID Stop payload and asserting correct cleanup/correlation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Copilot review on #2067: - The TaskDescription fallback in handleLifecycleSubagentStart only parsed ToolInput when both TaskDescription and SubagentType were empty. If an agent populates SubagentType but leaves TaskDescription empty (while still providing Task tool_input.description), we failed to record TaskDescription at SubagentStart, breaking ResolvePreTaskToolUseID correlation at SubagentEnd. Now falls back whenever TaskDescription alone is empty. - ResolvePreTaskToolUseID read candidate pre-task files via a raw os.ReadFile on a filepath.Join path; switched to os.Root + osroot.ReadFile for traversal-resistant access, consistent with the rest of this file's pre-task state I/O. - ResolvePreTaskToolUseID treated any pre-task-*.json filename as an active candidate even when the extracted tool_use_id failed ValidateToolUseID (e.g. a malformed pre-task-.json). Such candidates are now skipped so they can't be returned as a resolved (empty/invalid) ID. Entire-Checkpoint: 01M0DMFK3E8XV61ZCZ3WK0C3TE
|
Addressed Copilot review in 8eb518e: fell back to ToolInput parsing whenever TaskDescription alone is empty (not gated on SubagentType too); ResolvePreTaskToolUseID now reads candidate pre-task files via os.Root/osroot.ReadFile instead of raw os.ReadFile+filepath.Join; malformed pre-task-*.json filenames with invalid tool_use_id are now skipped instead of being returned as a resolved empty/invalid ID. |
Entire-Checkpoint: 01M0DN0D1RC61TPQCTBQ7KQQBG
TestDeviceFlow_FollowsApexRedirectToRegionalTokenEndpoint (test-core CI) was flaking with "transport connection broken: http: CloseIdleConnections called". NewClient falls back to http.DefaultTransport when handed a nil *http.Client, so parallel tests sharing that pool raced with each other's httptest.Server.Close(), which courtesy-calls DefaultTransport.CloseIdleConnections. Give each test its own cloned transport, matching the existing pattern in internal/entireclient/clusterdiscovery/discovery_test.go and internal/coreapi/cross_juris_transport_test.go. Entire-Checkpoint: 01M0DP9Q3GKVSZZWTE2QMR13C5
…seID Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M0DSK04SQWGZ04W296F3H1Q2
scripts/entire-dev was committed as a symlink to an absolute, machine-specific path, which does not exist on any other machine or in CI. The local-dev launcher script was intentionally removed by a9a676e (refactor: remove local_dev mode); this was a stray local convenience symlink swept into an unrelated commit by mistake. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M0DV0WFWHH4508TF8AKKR8SR
… mid-resolve ResolvePreTaskToolUseID's ambiguous fallback (task-description match or single-active-file) and the later LoadPreTaskState call are separate steps in the same handler, but a concurrent SubagentEnd for another subagent resolved via the same fallback can run CleanupPreTaskState on the just-named pre-task file in between. Previously this fell through to DetectFileChanges(ctx, nil), which treats every untracked file as new and mints a spurious checkpoint out of files the subagent never touched. Track whether the ID came from the ambiguous fallback, and when the subsequent load then finds nothing, skip the checkpoint instead of guessing at a baseline. Added a test seam (afterAmbiguousSubagentEndResolve) to deterministically simulate the race and a regression test that reproduces the spurious-checkpoint bug without the guard and passes with it. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M0DV2D5FCKK6J3CXZYXXCH4R
|
Fixed both findings:
|
…e scan
ValidateToolUseID("") returns nil (empty is a valid optional field on other
call paths), so a malformed pre-task-.json file's empty candidateID passed
the scan's validation check unfiltered. As the sole active file, that let
the single-active-file fallback return ("", true) instead of failing
resolution — the caller's ambiguous-resolve guard treats this as a genuine
match rather than "nothing usable," even though downstream guards happen to
still prevent a spurious checkpoint. Reject empty explicitly, matching the
comment's own stated intent ("pre-task-.json ... not a usable candidate").
Co-authored-by: Cursor <cursoragent@cursor.com>
Entire-Checkpoint: 01M0DVT7D190RCTM4R607A15QW
|
Fixed: the pre-task-file scan in |
The vanished-state guard covered preState == nil, but the no-file-changes branch still deleted a pre-task file that resolution had only guessed at. When the single-active-file fallback names a sibling that is still running and has not written anything yet, that delete destroys the sibling's baseline: its own SubagentEnd then hits the vanished-state guard and drops a real checkpoint. Only clean up when the resolve is corroborated by an exact task-description match, which is Cursor's designed path, so ordinary subagents still do not leak a pre-task file. The cleanup after SaveTaskStep stays ungated because the baseline is spent once a checkpoint has been minted from it. Co-authored-by: Cursor <cursoragent@cursor.com> Entire-Checkpoint: 01M0KQ99EHYGQZGHRSVX4V9GMR
Trail: https://entire.io/gh/entireio/cli/trails/1098
What: Recover the Cursor subagent correlation id on Stop when Cursor omits
subagent_id, using the stored task description (or a single unambiguous active pre-task).Why / how it helps: Official Cursor
subagentStoppayloads have nosubagent_id(only Start does). Empty ToolUseID made Stop misspre-task-<id>.json, orphan state files, and collide parallel Stops.How: Store
TaskDescriptiononPreTaskState; addResolvePreTaskToolUseID; resolve empty ids inhandleLifecycleSubagentEndbefore load/cleanup.Testing:
go test ./cmd/entire/cli/ -run 'TestResolvePreTask|TestHandleLifecycleSubagentEnd_Resolves|TestFindActivePreTask|TestCapturePreTask'go test ./cmd/entire/cli/agent/cursor/./cmd/entire/clisuite green in worktree validationFixes #2061