Skip to content

feat(lineage): tell concurrent Cursor MCP calls apart by the arguments the pre-tool hook recorded (#424) - #483

Merged
ScriptedAlchemy merged 2 commits into
mainfrom
feat/cursor-lineage-mcp-correlation
Sep 4, 2026
Merged

feat(lineage): tell concurrent Cursor MCP calls apart by the arguments the pre-tool hook recorded (#424)#483
ScriptedAlchemy merged 2 commits into
mainfrom
feat/cursor-lineage-mcp-correlation

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Summary

Framework-side support for gap 3 of #424 (Cursor tools/call _meta carries no conversation or tool-call id). No vendor contact; nothing is fabricated — the change only narrows an existing refusal using a fact both payloads already carry.

Gap (captured evidence)

fixtures/host-lineage/cursor-3.18.25.ndjson, live Cursor 3.18.25 desktop capture (docs/audits/2026-09-03-host-lineage-matrix.md §3):

  • Row 44 — preToolUse, tool_name: "MCP:probe", tool_input: {"note":"subagent"}, conversation_id: bf617dfd….
  • Row 45 — the server's own record of the following tools/call: _meta: { progressToken: 1 } only, client cursor-vscode 1.0.0, arguments note: "subagent".
  • Rows 53/54 — the nested conversation (46efda32…) repeats the pattern with {"note":"nested"}.

So the hook's tool_input is the call's arguments, verbatim, while _meta names nothing. Until now the registry matched Cursor calls only by the open MCP:<tool> window's tool name and refused (id-not-resolvable) whenever several conversations had the same tool open — the parallel-worker case.

What changes

  • packages/rsc-runtime/src/lineage/state.tsOpenToolCall.inputDigest? (sha256 prefix of the canonical tool_input; optional, so existing journals read unchanged, no state version bump).
  • packages/rsc-runtime/src/lineage/registry.tstool/before records the digest; LineageToolCallQuery.arguments?; the name-match fallback narrows by digest only when the matches span several conversations. One conversation left → resolves (resolution: 'inferred', source: 'derived'); zero or several → still id-not-resolvable. A lone open conversation is never made ambiguous by a digest mismatch, and undefined / {} arguments digest alike.
  • packages/agent-bundle/src/mcp-server-runtime.ts — the generated server passes the tool input to resolveToolCall.
  • cursor-2026-08-28.json lineage.mcp-correlation reason + evidence (row stays degraded: _meta still carries nothing natively); docs/entry-conventions.md host table.

Claude keeps its exact claudecode/toolUseId path and Codex its _meta lineage; the narrowing only applies to the shared name-match fallback.

Tests

packages/rsc-runtime/tests/lineage-registry.test.ts: two Cursor conversations with MCP:probe open using the captured {"note":"subagent"} / {"note":"nested"} inputs — no arguments → refused (unchanged), matching arguments → the right conversation, unrecorded arguments → refused, identical {} inputs in both → refused, a lone window resolves regardless of arguments. Existing 45 lineage tests unchanged. pnpm typecheck, pnpm lint green.

Review status

  • Codex reviewed af898c9 (PR opened): P1 — correlate against raw wire arguments, not schema-parsed input; P2 — a pre-upgrade window without a digest must stay in contention. Both addressed in c09abbdc: the generated server captures tools/call params.arguments off the wire (per request id, consumed once by the tool callback) and omits arguments entirely when nothing was captured; undigested windows are never filtered out. Tests: mcp-server-runtime.test.ts (raw {} vs { label: 'probe' } with a defaulting schema), lineage-registry.test.ts (legacy window keeps the call id-not-resolvable).
  • Head not re-reviewed by Codex: c09abbdc. Per maintainer instruction no review re-request is posted; merging on green CI.

Refs #424.

@changeset-bot

changeset-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c09abbd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@agent-bundle/runtime Patch
agent-bundle Patch

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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-03T23:51:49.990981Z af898c9 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pkg-pr-new

pkg-pr-new Bot commented Sep 3, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@483
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@483
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@483

commit: c09abbd

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af898c99fe

ℹ️ 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".

input,
context,
{ clientName, lineage: await toolCallLineage(options.lineage, context, route.name, clientName, options.lineageHost) },
{ clientName, lineage: await toolCallLineage(options.lineage, context, route.name, input, clientName, options.lineageHost) },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Correlate against raw MCP arguments

When a generated route's input schema supplies defaults, the registerTool callback receives schema-parsed input rather than the raw MCP arguments recorded by Cursor's hook. For example, the existing layout-probe schema maps both raw {} and raw { "label": "probe" } to { "label": "probe" }; if those calls are concurrent in different conversations, their hook digests differ but this line gives both requests the second digest, silently attributing the omitted-argument call to the wrong conversation. Capture the raw request arguments before schema parsing instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in c09abbd. The generated server now captures every tools/call's params.arguments off the wire (wrapping the transport's onmessage when the server connects, keyed by JSON-RPC request id) and hands that raw value to resolveToolCall; the schema-parsed callback input is never compared. When no raw capture exists for a request (a transport the server did not connect itself) the arguments property is omitted and no narrowing happens. Regression test: mcp-server-runtime.test.ts registers a tool with label: z.string().default('probe') and asserts the registry receives {}, { label: 'probe' }, and { label: 'other' } verbatim.

Comment on lines +621 to +623
if (conversations(matches) > 1 && argumentsDigest !== undefined) {
narrowed = matches.filter((candidate) => candidate.inputDigest === argumentsDigest);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep legacy undigested windows ambiguous

After upgrading a durable registry that still contains an open pre-upgrade window, that window has no inputDigest because the field remains optional for v1 compatibility. If a new window for the same tool opens in another conversation with identical arguments, this filter discards the legacy contender and treats the new conversation as uniquely matched, even though either window could own the request. Any competing window without a digest must preserve id-not-resolvable rather than allowing attribution.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in c09abbd. A candidate window without an inputDigest (pre-upgrade journal, or a hook whose tool_input was not an object) is never filtered out: narrowed keeps every undigested window alongside the digest matches, so a digested competitor in another conversation leaves the call id-not-resolvable. Regression test: lineage-registry.test.ts 'keeps a window without a recorded digest in contention' — legacy root-a window plus digested root-b → refused; once root-b closes the legacy window resolves alone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant