diff --git a/.changeset/cursor-3-18-desktop-recheck.md b/.changeset/cursor-3-18-desktop-recheck.md new file mode 100644 index 000000000..3d13f2fd3 --- /dev/null +++ b/.changeset/cursor-3-18-desktop-recheck.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": patch +--- + +Re-date the Cursor capability table's desktop hooks evidence against Cursor 3.18.25: `sessionStart` is dispatched to plugin-scoped hooks for newly created root chats — not for resumed roots or `Task` children — and the earlier 0× count came from 3.14.7 launches only; `subagentStart`/`subagentStop` delivery varies by instance on the same build, and where an instance does not deliver them `request.lineage` reports `id-not-resolvable` for the child rather than inferring a parent. Evidence notes only; no projection or `request.lineage` behaviour changes (#546) diff --git a/docs/audits/2026-09-03-host-lineage-matrix.md b/docs/audits/2026-09-03-host-lineage-matrix.md index 5735b6dd0..ca8463148 100644 --- a/docs/audits/2026-09-03-host-lineage-matrix.md +++ b/docs/audits/2026-09-03-host-lineage-matrix.md @@ -239,9 +239,11 @@ Not delivered to the plugin in this run: `sessionStart` (never fired for the plugin; the sibling raw-hooks probe on the same build saw none either), `workspaceOpen` (fired to user-level hooks only), `sessionEnd` (nothing arrived when the window was closed with the agent idle), `postToolUseFailure`, -`preCompact`. §9 below shows that on a real desktop `workspaceOpen` and -`sessionEnd` *do* reach plugin-scoped hooks and only `sessionStart` never -does. `preToolUse` was delivered twice for some `Read`/`Grep` calls with the +`preCompact`. §9 below shows that on a real desktop `workspaceOpen`, +`sessionEnd`, and (on 3.18.25) `sessionStart` *do* reach plugin-scoped hooks, +while `subagentStart`/`subagentStop` — delivered in this isolated run — were +not dispatched at all on the maintainer's daily instance of the same build +(§9.1). `preToolUse` was delivered twice for some `Read`/`Grep` calls with the same `tool_use_id` (pairs 3/4, 9/10, … in the fixture). Payload excerpt (subagentStart — the only place the parent link exists, and it @@ -356,8 +358,9 @@ root, and the parent-of-subagent chain — is the only identity-adjacent surface | No parent id on `SubagentStart`; child events carry no parent | Codex | §1, §2, §10 | Ours: read from the thread's own rollout head (`thread_spawn.parent_thread_id`, `depth`), which every hook names in `transcript_path`; spawn call matched by `agent_path`; inferred parent corrected at `SubagentStop`. Remaining host-side: the payload itself carries no `parent_thread_id`, so a hook on a machine that cannot read `CODEX_HOME/sessions` (or a rollout not yet flushed) falls back to inference (#423) | | Child conversation id absent from `subagentStart`; child events carry no parent/root | Cursor | §1, §2 | Bound by elimination in the registry (single pending start per workspace); refused while ambiguous for parallel workers; filed as #424 | | `_meta` carries no conversation/tool-call id | Cursor | §3 | Hook-correlated only; filed | -| `sessionStart` never dispatched on the desktop (`workspaceOpen`/`sessionEnd` are) | Cursor | §1, §9 | Host-side (#424 gap 4); lineage never depends on it to establish a root | -| Roots first seen on a tool hook (Cursor restart or plugin load mid-conversation) | Cursor | §9 | Ours: workspace-scoped child binding plus correction (subtree re-rooted) when a bound conversation later carries a root-only event (`beforeSubmitPrompt`, `stop`, `sessionEnd`, `preCompact`) | +| ~~`sessionStart` never dispatched on the desktop~~ | Cursor | §1, §9 | Closed 2026-09-04: 3.18.25 desktop dispatches `sessionStart` to plugin-scoped hooks for newly created root chats — not for resumed roots or `Task` children (§9.1); the 0× count came from 3.14.7 launches only. Lineage still treats it as one root-shaped event among several rather than a prerequisite, because roots are routinely first seen mid-conversation (§9) | +| `subagentStart`/`subagentStop` dispatch varies by instance on 3.18.25: delivered in the isolated §1 run, never requested on the maintainer's daily instance (six `Task` runs, §9.1) | Cursor | §1, §9.1 | Host-side; where the start is not delivered the child is first seen on its own tool hook with no pending start to bind to, so `request.lineage` reports `id-not-resolvable` for it rather than inferring a parent | +| Roots first seen on a tool hook (Cursor restart or plugin load mid-conversation) | Cursor | §9 | Ours: workspace-scoped child binding plus correction (subtree re-rooted) when a bound conversation later carries a root-only event (the registry's Cursor root set — in practice `sessionStart`, `sessionEnd`, `beforeSubmitPrompt`, `stop` or `preCompact`; see §9.1) | | Cursor CLI not exercised | Cursor | table above | Needs a signed-in `cursor-agent`; not attempted on the operator's account | | ~~Claude session used a scripted model~~ | Claude | §8 | Closed 2026-09-03: two live-model sessions replace the stand-in fixture; every stand-in claim held, see §8 | | ~~Claude `PostToolUse(Agent).tool_response.agentId` not consumed by the registry~~ | Claude | §1, §2 | Closed 2026-09-03 (#422 follow-up PR): the registry now treats the parent's `Agent` PostToolUse as the host's word on the edge — it confirms the spawn-window match (`resolution: confirmed` once every edge to the root is host-named), fills in sibling `toolCallId`s claimed blind, places a `SubagentStart` no window could (an unplaced start keeps id/type/time/stop, and any confirmations it issued for its own children, until its edge is known), moves a child filed under the wrong parent and re-bases its descendants, and holds a child named before its start (orchestration row 13 → 14). Replays of the orchestration capture with row 64 or row 81 withheld recover the sequential agent and the depth-2 child from rows 99/101 alone | @@ -406,9 +409,11 @@ Cursor desktop keeps a per-window hooks log that prints `Hook step requested: ` for **every** step before it looks up declared hooks — 58,717 `preToolUse` steps appear with no hook declared for them — so an absent step is non-dispatch, not a registration problem. The -retained logs on the maintainer's machine (cursor_version 3.14.7 for -2026-08-14 → 2026-08-25, 3.18.25 on 2026-09-03; 89,219 steps; 35 -conversations; a local plugin declaring `sessionStart`, `sessionEnd`, +retained logs on the maintainer's machine (89,219 steps: 89,218 from the +five 3.14.7 launches, 2026-08-13 → 2026-08-28 — every one of their 59,455 +`cursor_version` stamps reads 3.14.7; the remaining steps are requests with +no matching hook, which log no payload — plus the first step of the +2026-09-03 3.18.25 launch; 35 conversations; a local plugin declaring `sessionStart`, `sessionEnd`, `workspaceOpen`, `stop`, `postToolUse`, `preCompact`, `afterFileEdit`, `afterShellExecution`) show: @@ -423,13 +428,13 @@ conversations; a local plugin declaring `sessionStart`, `sessionEnd`, | `beforeSubmitPrompt` | 7 | yes — `prompt`, `attachments[]`, `composer_mode`, `generation_id` | | `sessionEnd` | 6 | yes — `reason: window_close`, `final_status: none`, `duration_ms`, `is_background_agent: false`, `session_id` = `conversation_id`, `generation_id: ""`, `transcript_path: null` | | `preCompact` | 4 | yes | -| **`sessionStart`** | **0** | — | +| **`sessionStart`** | **0** | — (superseded by §9.1: 3.18.25 does dispatch it) | Consequences for lineage: - `workspaceOpen` and `sessionEnd` are confirmed plugin-scoped deliveries on the desktop; the §1 run missed them because the Xvfb CLI session never - closed a window. Only `sessionStart` remains a host gap (#424 gap 4). + closed a window. `sessionStart` was a 3.14.7 gap only (§9.1). - Only 7 of the 35 conversations ever produced `beforeSubmitPrompt`; 28 were first seen on `postToolUse`/`preToolUse`/`afterShellExecution` (the log starts mid-conversation after a Cursor restart, or the plugin loaded @@ -440,8 +445,57 @@ Consequences for lineage: `workspace_roots` and undoes a binding when the bound conversation later receives `beforeSubmitPrompt`, which a subagent never does. - No `Task` tool call appears in these desktop logs, so desktop - `subagentStart`/`subagentStop` delivery is unobserved here; the §1 CLI - capture remains the evidence for the subagent families. + `subagentStart`/`subagentStop` delivery is unobserved here; see §9.1, which + closes that gap with a live `Task` run. + +### 9.1 Re-check on Cursor desktop 3.18.25 (added 2026-09-04) + +Same machine, same workspace (`19017dde…`), the maintainer's daily desktop +instance (`/usr/share/cursor`, workbench `280eca29`), hooks log +`~/.config/Cursor/logs/20260904T071544/window2_wb1/output_20260904T071550/cursor.hooks.workspaceId-19017dde….log`, +plugins `~/.cursor/plugins/local/tracedecay` (no matchers; `afterFileEdit`, +`afterShellExecution`, `postToolUse`, `preCompact`, `sessionEnd`, +`sessionStart`, `stop`, `workspaceOpen`) and `~/.cursor/plugins/local/cargo-hauler` +(an agent-bundle emitted `cursor` pack: `preToolUse`/`postToolUse` `^Shell$`, +`sessionStart`, `stop`). No `~/.cursor/hooks.json`, no project `hooks.json`. +Two root chats ran in the window, both resumed conversations rather than +newly created ones. Between them they launched six `Task` subagents — from +this chat a backgrounded `explore` (`d103df27-…`, 61 hook deliveries), a +foreground `general-purpose` (`300d51aa-…`, one `echo` step), a backgrounded +`ci-watcher` (`ba44c20f-…`, 643 deliveries over 17 minutes) and a +`change-risk-reviewer` (`025d6ca6-…`); the other root launched two more +(`217f76a2-…`, `228cecfe-…`) — which controls for subagent type, +backgrounding and lifetime. The service interleaves concurrent hook blocks +in the log, so every count below attributes a payload by the +`conversation_id` inside its own JSON block, never by proximity. Counts are +as of 2026-09-04T08:14Z. + +| Fact | 3.14.7 record (§9) | 3.18.25 observed 2026-09-04 | +| --- | --- | --- | +| `sessionStart` to plugin-scoped hooks | 0× requested | **dispatched for newly created root chats**: 6× across the two earlier 3.18.25 launches (`20260903T041607`, `20260904T062311`; 2026-09-03T20:30Z → 2026-09-04T06:42Z), each the first event its conversation ever logged, `is_background_agent: false`, `composer_mode: "agent"`, every one `Found n hook(s) … from claude-plugin config`; payload `conversation_id` = `session_id`, `generation_id: ""`, `model`, `model_id`, `model_params`, `is_background_agent`, `composer_mode`, `cursor_version`, `workspace_roots`, `user_email`, `transcript_path: null`. **Not requested** for a resumed root (this window's two roots: 0×) nor for any of the six `Task` children. The §9 0× is a 3.14.7 result: 89,218 of those 89,219 steps came from the 3.14.7 launches; the one remaining step was the first of the 2026-09-03 3.18.25 launch | +| Tool events to plugin-scoped hooks | yes | yes — in this window: `preToolUse` 3,635, `postToolUse` 1,912, `afterShellExecution` 227, `afterFileEdit` 60, `stop` 7, `preCompact` 1, `workspaceOpen` 1 requested; the emitted pack's `^Shell$` hooks ran from the plugin root with `${CURSOR_PLUGIN_ROOT}` expanded | +| Duplicate `preToolUse` for one `tool_use_id` | seen for `Read`/`Grep` in the §1 capture | none among the delivered `preToolUse` (all `Shell`; `Read`/`Grep` had no `preToolUse` hook declared, so unobserved for those tools). A `postToolUse` appearing twice in the log is two plugins (`Found 2 hook(s)`), not a duplicate delivery | +| `subagentStart` / `subagentStop` | unobserved on the desktop (§9); delivered in the isolated §1 run | **not requested at all** for any of the six `Task` runs — no `Hook step requested: subagentStart`/`subagentStop`, and no `preToolUse`/`postToolUse` naming `tool_name: "Task"`, neither while the children ran nor after they finished. No retained desktop log on this machine (3.14.7 or 3.18.25, 95,674 steps) has ever requested either step. Same build as §1, so delivery of the subagent family varies by instance or account state | +| A subagent's own hooks | fresh `conversation_id`, nothing names the parent | same: all six children's events (`preToolUse`/`postToolUse`/`afterShellExecution`) carry their own `conversation_id` = `session_id`, their own `generation_id`, `transcript_path: null`, no parent/root field | +| Root-only events on a subagent | never | never — none of the six children produced `stop`, `preCompact` or `sessionStart`; the window's 7 `stop` requests (28 payloads across four hooks) all carry one of the two roots' `conversation_id`. The roots also carry a non-null `transcript_path`, but one root's earliest events had `transcript_path: null` before its transcript file existed, so null does not mark a child either | +| `cwd: ""` on `Shell` payloads | yes | yes | + +Consequences: the `sessionStart` row in §7 is closed and #424 gap 4 with it. +`sessionStart` marks creation, not resumption: a resumed root still arrives +mid-conversation with no start, which is why lineage keeps treating it as one +root-shaped event among several rather than a prerequisite. +Where an instance does not deliver `subagentStart`, there is nothing for the +registry to bind a child to, so `request.lineage` for that child is +`id-not-resolvable` (`ensureRoot` finds no pending start and the event is not +root-shaped, so no node is created), which is the honest answer; nothing binds +blind. The root-only-event correction stays justified because a desktop child +still never carries any event in the registry's Cursor root set +(`CURSOR_ROOT_EVENTS` in `packages/rsc-runtime/src/lineage/registry.ts`: +canonical `session/start`, `session/end`, `prompt/submit`, `stop`, +`compact/before`, `compact/after`, `workspace/open` — on Cursor that means +`sessionStart`, `sessionEnd`, `beforeSubmitPrompt`, `stop` or `preCompact`, +since the host documents no post-compact hook and `workspaceOpen` is +sessionless). No framework code changes follow from this re-check. ## 10. Codex rollout heads: the parent the hook payloads omit (added 2026-09-03) diff --git a/packages/agent-bundle/src/adapters/capabilities/cursor-2026-08-28.json b/packages/agent-bundle/src/adapters/capabilities/cursor-2026-08-28.json index 2ed635215..f82c3e619 100644 --- a/packages/agent-bundle/src/adapters/capabilities/cursor-2026-08-28.json +++ b/packages/agent-bundle/src/adapters/capabilities/cursor-2026-08-28.json @@ -362,11 +362,11 @@ }, "root": { "state": "degraded", - "reason": "2026-09-03: a child's events carry only their own fresh conversation_id (host gap tracked in #424), so the root is known only through the registry that bound the child (resolution: inferred); a Cursor root itself is established only by a root-shaped event (beforeSubmitPrompt, stop, sessionEnd, preCompact, sessionStart when delivered), never by a tool hook." + "reason": "2026-09-03: a child's events carry only their own fresh conversation_id (host gap tracked in #424), so the root is known only through the registry that bound the child (resolution: inferred); a Cursor root itself is established only by a root-shaped event (the registry's Cursor root set is the canonical session/start, session/end, prompt/submit, stop, compact/before, compact/after and workspace/open; Cursor documents no post-compact hook and workspaceOpen is sessionless, so on this host that means sessionStart, sessionEnd, beforeSubmitPrompt, stop or preCompact — none of which a subagent's conversation carries), never by a tool hook." }, "parent": { "state": "degraded", - "reason": "2026-09-03: parent_conversation_id appears only on the parent's subagentStart/subagentStop, which also omit the child conversation id (#424). The registry binds a never-seen conversation to the pending subagentStart only when exactly one is pending in the same workspace_roots (so windows sharing one durable registry never bind each other's children); with several pending — parallel workers — it stays id-not-resolvable until all but one have stopped, and a blind binding is undone — with anything started beneath it re-rooted — the moment the conversation carries a root-only event (beforeSubmitPrompt, stop, sessionEnd, preCompact), which a subagent never does, so a sessionEnd on a misbound chat retires that chat and never the parent it was filed under." + "reason": "2026-09-03: parent_conversation_id appears only on the parent's subagentStart/subagentStop, which also omit the child conversation id (#424). The registry binds a never-seen conversation to the pending subagentStart only when exactly one is pending in the same workspace_roots (so windows sharing one durable registry never bind each other's children); with several pending — parallel workers — it stays id-not-resolvable until all but one have stopped, and a blind binding is undone — with anything started beneath it re-rooted — the moment the conversation carries a root-only event (the same Cursor root set — in practice sessionStart, sessionEnd, beforeSubmitPrompt, stop or preCompact), which a subagent never does, so a sessionEnd on a misbound chat retires that chat and never the parent it was filed under." }, "depth": { "state": "degraded", @@ -624,8 +624,9 @@ "retrieved 2026-09-02: https://cursor.com/docs/hooks documents beforeSubmitPrompt input as prompt plus file/rule attachments, matcher evaluation against the literal UserPromptSubmit, and output as continue plus optional user_message. continue:false blocks submission; no additional-context or prompt-rewrite channel is documented.", "retrieved 2026-09-02: https://cursor.com/docs/hooks documents sessionEnd input as session_id, reason, duration_ms, is_background_agent, final_status, and optional error_message. It is fire-and-forget; output is logged but unused. The same source states cloud agents have no sessionEnd because they lack an editor-lifetime session boundary.", "2026-09-02: local cursor-agent 2026.08.31-4057e58 capture was attempted with trusted project hooks, but the binary required Cursor authentication unavailable in the capture environment; the pinned official contract is the evidence source for beforeSubmitPrompt and sessionEnd.", - "2026-09-03: Cursor desktop hooks-service logs on the maintainer's machine (~/.config/Cursor/logs///output_*/cursor.hooks.workspaceId-*.log; cursor_version 3.14.7 for 2026-08-14 → 2026-08-25 and 3.18.25 on 2026-09-03; 89,219 `Hook step requested` lines across 35 conversations, with a local plugin declaring sessionStart, sessionEnd, workspaceOpen, stop, postToolUse, preCompact, afterFileEdit and afterShellExecution) delivered plugin-scoped (`from claude-plugin config`) workspaceOpen 12×, sessionEnd 6× (reason window_close, final_status none, session_id = conversation_id, generation_id \"\", transcript_path null — accepted by the session/end validator), beforeSubmitPrompt 7×, stop 8×, preCompact 4×, postToolUse 29,308× — so workspaceOpen and sessionEnd do reach plugin-scoped hooks on the desktop even though the 2026-09-03 Xvfb CLI capture (fixtures/host-lineage/cursor-3.18.25.ndjson) saw neither.", - "2026-09-03: the same desktop hooks-service logs requested sessionStart 0× in 89,219 steps. The service logs every requested step before matching declared hooks (58,717 preToolUse steps were requested with no hook declared for them), so this is non-dispatch by Cursor desktop 3.14.7/3.18.25, not a registration gap; the cursor-agent CLI run with trusted project hooks (2026-09-02) did dispatch sessionStart with session and conversation ids. Lineage therefore never depends on session/start to establish a Cursor root — beforeSubmitPrompt, stop, sessionEnd and preCompact are the root-shaped events — and only 7 of those 35 desktop conversations ever produced beforeSubmitPrompt (28 were first seen on a tool hook after a Cursor restart or plugin load mid-conversation), which is why a blind child binding is corrected as soon as the bound conversation carries any root-only event (tracked in #424, gap 4).", + "2026-09-03: Cursor desktop hooks-service logs on the maintainer's machine (~/.config/Cursor/logs///output_*/cursor.hooks.workspaceId-*.log; 89,219 `Hook step requested` lines across 35 conversations — 89,218 from the five 3.14.7 launches, 2026-08-13 → 2026-08-28, plus the first step of the 2026-09-03 3.18.25 launch — with a local plugin declaring sessionStart, sessionEnd, workspaceOpen, stop, postToolUse, preCompact, afterFileEdit and afterShellExecution) delivered plugin-scoped (`from claude-plugin config`) workspaceOpen 12×, sessionEnd 6× (reason window_close, final_status none, session_id = conversation_id, generation_id \"\", transcript_path null — accepted by the session/end validator), beforeSubmitPrompt 7×, stop 8×, preCompact 4×, postToolUse 29,308× — so workspaceOpen and sessionEnd do reach plugin-scoped hooks on the desktop even though the 2026-09-03 Xvfb CLI capture (fixtures/host-lineage/cursor-3.18.25.ndjson) saw neither.", + "2026-09-03: the same desktop hooks-service logs requested sessionStart 0× in 89,219 steps. Corrected 2026-09-04: 89,218 of those steps came from the five Cursor 3.14.7 launches retained on the machine (2026-08-13 → 2026-08-28; every one of their 59,455 cursor_version stamps reads 3.14.7, the rest being requested steps with no matching hook and therefore no logged payload); the remaining one was the first step of the 2026-09-03 3.18.25 launch. Cursor desktop 3.18.25 does dispatch sessionStart to plugin-scoped hooks, for newly created root chats: six `Hook step requested: sessionStart` entries across two launches (2026-09-03T20:30Z → 2026-09-04T06:42Z), each the first event its conversation ever logged, is_background_agent false, composer_mode agent, each delivered `from claude-plugin config` with the documented envelope (conversation_id = session_id, generation_id \"\", model, model_id, model_params, is_background_agent, composer_mode, cursor_version 3.18.25, workspace_roots, user_email, transcript_path null). It is not requested for a resumed root (a later window's two roots, both resumed, 0× over 5,843 steps) nor for any of that window's six Task children. Lineage therefore still treats session/start as one root-shaped event among several (the registry's Cursor root set — in practice sessionStart, sessionEnd, beforeSubmitPrompt, stop or preCompact, since Cursor documents no post-compact hook and workspaceOpen is sessionless) rather than a prerequisite: a resumed root arrives mid-conversation with no start, and only 7 of the 35 earlier desktop conversations ever produced beforeSubmitPrompt (28 were first seen on a tool hook after a Cursor restart or plugin load mid-conversation), which is why a blind child binding is corrected as soon as the bound conversation carries any root-only event (#424, closed 2026-09-04).", + "2026-09-04: six Task subagents launched from two resumed root chats on the maintainer's daily Cursor desktop 3.18.25 instance (a backgrounded `explore`, a foreground `general-purpose`, a backgrounded `ci-watcher` alive for 17 minutes with 643 hook deliveries, a `change-risk-reviewer`, and two from the other root; same hooks log, workspace 19017dde…, counts as of 08:14Z) produced no `Hook step requested: subagentStart` or `subagentStop` and no preToolUse/postToolUse naming tool_name Task, and no retained desktop log on this machine (3.14.7 or 3.18.25, 95,674 steps) has ever requested either step — whereas the 2026-09-03 isolated 3.18.25 instance (fixtures/host-lineage/cursor-3.18.25.ndjson rows 36, 48) did deliver both. Subagent-lifecycle delivery therefore varies by instance or account state on the same build. The children's own hooks arrived exactly as in that capture: a fresh conversation_id, their own generation_id, transcript_path null, nothing naming the parent, and no root-only event — the window's 7 stop requests (28 payloads across four hooks) all carry a root's conversation_id when each payload is attributed by the id inside its own JSON block rather than by proximity in the interleaved log. Where the start is not delivered there is no pending child for the registry to bind, so request.lineage reports id-not-resolvable for it (ensureRoot returns no node) rather than inferring a parent; the subagent-events row stays supported on the strength of the instance that delivered them.", "retrieved 2026-09-02: https://cursor.com/docs/hooks documents postToolUseFailure input as tool_name, tool_input, tool_use_id, cwd, error_message, failure_type, duration, and is_interrupt, with no supported output fields. Unlike Claude PostToolUseFailure, failure_type includes permission_denied.", "retrieved 2026-09-02: https://cursor.com/docs/hooks documents preCompact as observational and unable to block or modify compaction. Its optional user_message is user-facing rather than agent context, so the canonical Agent.Result and Agent.Context vocabulary exposes no output channel for it.", "retrieved 2026-09-02: the complete https://cursor.com/docs/hooks Agent and cloud event inventories document no postCompact event, so compact/after is unavailable rather than inferred from preCompact.",