feat(lineage): request.lineage is the only identity-adjacent surface; scoped, self-correcting Cursor child binding - #444
Conversation
🦋 Changeset detectedLatest commit: 6bea637 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7bed5afc5b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (host === 'cursor' && CURSOR_USER_ONLY_EVENTS.has(event) && carrier.conversation !== undefined) { | ||
| await correctMisboundChild(carrier.conversation, observedAt, keys); |
There was a problem hiding this comment.
Correct misbindings before processing every root-only event
When an existing Cursor chat is blindly bound to another root's pending child and is then closed before submitting another prompt, its session/end is not corrected by this prompt-only gate. Because observeSessionEnd runs first, it follows the erroneous child node's root and stops and retires the unrelated parent session and all its descendants. Apply correction before retirement for session/end and any other event that only a root conversation can emit.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 6c11d97. The correction now runs before the event dispatch switch and fires on every root-only Cursor event (CURSOR_ROOT_EVENTS: prompt/submit, stop, session/end, compact/*), not just prompt/submit, so a session/end on a misbound chat retires that chat and never the parent it was filed under. Test: "corrects a blind binding before session/end so the misbound chat retires itself, not the parent it was filed under" asserts the original root, its pending child and pendingChildren are untouched and the real child still binds afterwards.
| const { [conversation]: _moved, ...rest } = state.nodes; | ||
| return { | ||
| ...state, | ||
| nodes: { ...rest, [subagentId]: { ...bound, id: subagentId } }, |
There was a problem hiding this comment.
Re-root descendants when undoing a blind binding
If the misbound conversation starts a subagent before its next prompt, childUnbound moves only the conversation node back to the original pending ID. The newly started descendants still reference the corrected conversation as their parent but retain the unrelated root and inflated depth; their pending children can then remain ambiguous with the restored pending child, and session retirement targets the wrong tree. Re-root or otherwise reconcile nodes and correlation state created beneath the conversation when correcting it.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 6c11d97. childUnbound now walks the remaining nodes and re-roots every descendant of the corrected conversation (root → the conversation, depth rebased by the misbound depth), and rewrites root on openCalls/pendingSpawns belonging to that subtree. Test: "re-roots what the misbound conversation started beneath it when the binding is undone" covers a grandchild bound beneath the misbound chat, an open MCP call resolving with the corrected root, and session/end on the corrected root retiring only its own subtree. Pending children beneath the corrected conversation stay pending alongside the restored one; if both are live in one workspace the next never-seen conversation is refused as ambiguous rather than guessed.
|
|
||
| `request.lineage` is the only identity-adjacent surface: who a request's parent conversation is, what its root is, and — for subagents — the parent-of-subagent chain. The framework no longer reads, validates, or surfaces operator identity anywhere: the Cursor `workspaceOpen` envelope validator stops inspecting `user_email` (the field still passes through inside `native` untouched), the dev-playground lifecycle fixture drops it, and the docs describe `actor` as the HTTP-authenticated MCP client and nothing more (#391, closed as not planned). | ||
|
|
||
| Cursor child binding in the lineage registry is now scoped and self-correcting: a never-seen conversation on a tool hook binds to a pending `subagentStart` only when exactly one is pending in the same `workspace_roots` (nodes record a digest of the roots, never the paths), so two windows sharing one durable registry never bind each other's children; and a blind binding is undone when the bound conversation later receives `beforeSubmitPrompt`, which a subagent never does, so a chat tab whose prompt predates the registry becomes the root it is while the pending child waits for its real conversation. The `LineageNode` schema gains an optional `workspace` field and the journal gains a `childUnbound` event. Capability tables cite the host trackers (#422/#423/#424) and the Cursor desktop hooks-service evidence (`workspaceOpen` and `sessionEnd` reach plugin-scoped hooks; `sessionStart` is never dispatched on the desktop). |
There was a problem hiding this comment.
Rewrite the changeset as the required imperative summary
The changeset ends with implementation and evidence details rather than the required PR-number suffix, and neither paragraph is an imperative user-facing summary. Condense it into the behavior users receive, name the affected public surface, and end it with (#<PR>) as required.
AGENTS.md reference: AGENTS.md:L57-L58
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Rewritten in 6c11d97 as one imperative user-facing summary naming request.lineage, the Cursor workspaceOpen validator / user_email, actor, and the binding rule, ending with (#444). Also corrected the bump: both packages are patch — the runtime change is additive (optional workspace node field, new childUnbound event), and AGENTS.md reserves minor for breaking pre-1.0.
7bed5af to
6c11d97
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6c11d9739c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (host === 'cursor' && CURSOR_ROOT_EVENTS.has(event) && carrier.conversation !== undefined) { | ||
| await correctMisboundChild(carrier.conversation, observedAt, keys); |
There was a problem hiding this comment.
Preserve lineage for corrected session-end events
When a blind-bound conversation's first root-only event is session/end, this call dispatches childUnbound, which removes state.nodes[conversation]; observeSessionEnd then retires its descendants, and the later event !== 'session/end' guard skips recreating the proven root. Consequently observe() returns id-not-resolvable for this session-end event, unlike session-end events for roots already known to the registry and despite docs/entry-conventions.md stating that correction makes the conversation a root. Preserve an ephemeral lineage result or materialize the root as stopped before retiring it.
AGENTS.md reference: AGENTS.md:L31-L34
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 0d4154f. correctMisboundChild now dispatches nodeStarted for the proven root right after childUnbound, carrying the bound node's generation, workspace and first-seen time, so the correcting event resolves against a real root node like any known root's would. For session/end that root is then stopped and retired by observeSessionEnd, leaving a stopped depth-0 node that retention prunes — same shape as a known root's session end. The session/end test now asserts observe() returns { conversation: 'other-root', depth: 0, root: 'other-root' } and the node is materialized as stopped with its original startedAt.
|
@codex review |
|
Codex Review: Didn't find any major issues. Can't wait for the next one! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…e; scope and self-correct Cursor child binding Maintainer decision (2026-09-03, #391 closed as not planned): agent-bundle does not surface operator/user identity. Remove the only framework code that read it (the Cursor workspaceOpen `user_email` validation; the field passes through inside `native` unread), drop it from the dev-playground lifecycle fixture, and rewrite the docs so `actor` is the HTTP-authenticated MCP client and nothing more, while `request.lineage` answers "who is my parent / root / are we a subagent". Lineage registry (Cursor): bind a never-seen conversation to a pending subagentStart only when exactly one is pending in the same workspace_roots (nodes carry a digest, never the paths), and undo a blind binding when the bound conversation later receives beforeSubmitPrompt, which a subagent never does. Cursor desktop hooks-service logs show 28 of 35 conversations first seen on a tool hook and sessionStart never dispatched, so roots cannot rely on it. Capability tables cite #422/#423/#424 and the desktop evidence (workspaceOpen and sessionEnd do reach plugin-scoped hooks); the lineage matrix gains §8 and drops the identity paragraph; vendor feedback drafts added.
… and re-root its subtree; patch changeset
…ndor feedback Maintainer decision (2026-09-03): nothing is submitted to Cursor, Anthropic, or OpenAI. The two "ready to submit" feedback drafts become observation records that describe each host gap and what a future build would need to carry for the lineage/portable capability rows to move; the audits, capability evidence lines, and #424 wording follow ("gap", not "ask"). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
0d4154f to
6bea637
Compare
Maintainer decision (2026-09-03): no feedback is submitted to any host vendor, and the repo keeps no vendor-addressed write-ups. Delete the two documents #444 had renamed from `*-feedback.md` to `*-observations.md`, drop the vendor-addressed "documentation discrepancies" section from the hooks-registration audit, and repoint the two IDE-proof notes that linked the deleted files. Engineering audits (lineage matrix, hooks-registration evidence, IDE proofs) stay.
…host-test, mcp-app, skills-starter (#471) * chore(examples/host-test): record request.lineage through the public context member request.lineage is a first-class Observed<AgentLineage> on AgentRequestContext since #444, so the probe reads it directly instead of casting for an optional member and records it on every line (available or unavailable with reason). renderLineage types the serialized value against the public AgentLineage and Observed types. The route-unit suite asserts a mounted lineage is recorded and rendered. * chore(examples/skills-starter): type outcome graders against agent-bundle/eval Both graders declare themselves as the public EvalGraderFunction instead of restating the grader context and outcome shapes by hand. The README names the build output directory the CLI actually writes (artifact/, not dist/). * chore(examples/mcp-app): type the outcome grader against agent-bundle/eval The status grader declares itself as the public EvalGraderFunction instead of restating the grader context and outcome shapes. The README's mcp run example reuses the build output the CLI actually writes (--artifact artifact).
Why
Maintainer decision (2026-09-03): agent-bundle will not surface operator/user identity. #391 (actor principal for event scopes) is closed as not planned; the only identity-adjacent question the framework answers is lineage — who a request's parent conversation is, what its root is, and, for subagents, the parent-of-subagent chain — on
request.lineage.Identity removal
packages/agent-bundle/src/events/projection.ts: the CursorworkspaceOpenvalidator no longer inspectsuser_email(the only framework code that read it). The field passes through insidenativeuntouched; the test now proves the validator ignores any value there.hook-contract.tslifecycle-replay fixture dropsuser_email: null(tests updated).docs/entry-conventions.md,routes/public.ts, websitemcp.mdxen/zh):actoris the HTTP-authenticated MCP client and nothing more;request.lineageis documented as the answer to "who is my parent / root / are we a subagent"; there is deliberately no operator identity axis.AgentRequestContextis unchanged (noprincipalslot ever existed onmain;actorstays as the HTTPauthInfo.clientIdaxis that Add recipient-aware notices and capability-gated delivery #99 recipient matching uses). No request-store version bump is needed.Lineage quality (#422 / #423 / #424)
Re-examined with the captures plus new host evidence: Cursor desktop's own hooks-service logs (
~/.config/Cursor/logs/*/cursor.hooks.*.log, 89,219 steps, 35 conversations, 3.14.7 + 3.18.25). Findings recorded indocs/audits/2026-09-03-host-lineage-matrix.md§8:workspaceOpenandsessionEnddo reach plugin-scoped hooks on the desktop (the Xvfb CLI capture missed them);sessionStartwas requested 0× whilebeforeSubmitPrompt/stop/sessionEndwere — a desktop dispatch gap, now the only lifecycle ask on Cursor 3.18.25 lineage gaps: subagentStart omits the child conversation id; child hooks carry no parent/root; MCP _meta carries no conversation; sessionStart not dispatched to plugin hooks on desktop #424.beforeSubmitPrompt; 28 were first seen on a tool hook. A registry binding the next unseen conversation to a lone pendingsubagentStartcould mis-bind a second chat tab.Registry changes (
@agent-bundle/runtimeminor,lineage/state.ts+registry.ts):workspace_roots(digest stored on nodes, never the paths). Two windows sharing one durable registry never bind each other's children; a payload without roots keeps the single-pending rule.beforeSubmitPrompt(subagents never do),childUnboundreturns the child to pending and the conversation becomes the root it is; an already-stopped mis-bound child stays a finished node.Capability tables (Claude/Codex/Cursor) now name the host trackers in each degraded
lineagereason and carry the desktop evidence.docs/audits/2026-09-03-host-lineage-observations.mdrecords each host gap as an internal reference (lineage-only); by maintainer decision nothing is submitted to any vendor, and the former Cursor${PLUGIN_ROOT}report is likewise kept as2026-09-03-cursor-agent-plugins-observations.md.Tracker bookkeeping (done)
#391 closed (not planned); #424 re-titled and re-scoped (identity fields stripped, ask 4 narrowed to
sessionStart); #422/#423 given an explicit lineage-only scope and precise refusal semantics; dated pointer comments on #233, #223, #107, #189, #99.Verification
pnpm typecheckgreen;pnpm buildgreen (publint passed); rslint clean on touched files.lineage-registry,agent-request,event-project,adapter-capability-states— 118 passed. Route-unit:lifecycle-replay,event-project— 19 passed. Integration:hooks.test.ts— 27 passed.Closes nothing automatically; #422/#423/#424 stay open as host-integration trackers.