Skip to content

fix(cursor): correlate subagentStop without subagent_id via task description - #2067

Open
suhaanthayyil wants to merge 10 commits into
mainfrom
fix/track-2061
Open

fix(cursor): correlate subagentStop without subagent_id via task description#2067
suhaanthayyil wants to merge 10 commits into
mainfrom
fix/track-2061

Conversation

@suhaanthayyil

@suhaanthayyil suhaanthayyil commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

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 subagentStop payloads have no subagent_id (only Start does). Empty ToolUseID made Stop miss pre-task-<id>.json, orphan state files, and collide parallel Stops.

How: Store TaskDescription on PreTaskState; add ResolvePreTaskToolUseID; resolve empty ids in handleLifecycleSubagentEnd before load/cleanup.

Testing:

  • go test ./cmd/entire/cli/ -run 'TestResolvePreTask|TestHandleLifecycleSubagentEnd_Resolves|TestFindActivePreTask|TestCapturePreTask'
  • go test ./cmd/entire/cli/agent/cursor/
  • Broader ./cmd/entire/cli suite green in worktree validation

Fixes #2061

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

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 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 PreTaskState to persist a TaskDescription, and add CapturePreTaskStateWithMeta to record it at SubagentStart.
  • Add ResolvePreTaskToolUseID to map SubagentEnd events with an empty ID back to the correct pre-task-<id>.json using 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.

Comment thread cmd/entire/cli/lifecycle.go
Comment thread cmd/entire/cli/state.go Outdated
Comment thread cmd/entire/cli/state.go
@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 #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
@suhaanthayyil

Copy link
Copy Markdown
Contributor Author

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.

suhaanthayyil and others added 6 commits August 19, 2026 14:37
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
@suhaanthayyil

Copy link
Copy Markdown
Contributor Author

Fixed both findings:

  • Broken symlink: scripts/entire-dev was accidentally committed as a symlink to an absolute, machine-specific path (swept into an unrelated commit by mistake). Untracked it in 5c96863 — the real launcher script was already intentionally removed from the repo by a9a676e.
  • SubagentEnd race: resolveAmbiguously now tracks when event.ToolUseID came from ResolvePreTaskToolUseID's fallback; if the subsequent LoadPreTaskState then finds nothing (a concurrent sibling's CleanupPreTaskState won the race), the handler skips the checkpoint instead of falling through to DetectFileChanges(ctx, nil), which would mint a spurious checkpoint treating every untracked file as new. Fixed in 2f51808, with a test seam + regression test that reproduces the bug without the guard and passes with it.

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

Copy link
Copy Markdown
Contributor Author

Fixed: the pre-task-file scan in ResolvePreTaskToolUseID now rejects an empty candidateID explicitly. ValidateToolUseID("") returns nil since empty is valid on other call paths (an optional field), so a malformed pre-task-.json could otherwise become the sole "active" candidate and resolve to an empty ID as if it were a real match. Fixed in d910316 with a regression test.

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

Cursor subagentStop has no subagent_id; Entire correlation key goes empty

2 participants