feat(cursor): support workspace/open event routes as fire-and-forget observation - #298
Conversation
Accept Cursor's sessionless lifecycle envelope while deliberately projecting no native output, preserving pluginPaths as an unmodeled host channel.
🦋 Changeset detectedLatest commit: d8d9753 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d8d9753207
ℹ️ 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 (canonicalEvent === 'workspace/open') { | ||
| if ( | ||
| !Array.isArray(native.workspace_roots) | ||
| || native.workspace_roots.length === 0 | ||
| || !native.workspace_roots.every((root) => typeof root === 'string' && root.trim() !== '') |
There was a problem hiding this comment.
Populate request workspace from the validated roots
For every Cursor workspaceOpen invocation, the newly accepted envelope contains workspace_roots but no cwd; however, both generated execution paths still derive the runAgentRequest workspace exclusively from native.cwd (hook-contract.ts for standalone and mcp-server-runtime.ts for shared). Consequently, (await agent()).workspace is reported as unavailable for this route despite the host supplying workspace identity, which also prevents workspace-scoped providers/notices from using the opened workspace. Thread an appropriate root from this validated field into both execution paths, with explicit semantics for multi-root workspaces.
Useful? React with 👍 / 👎.
|
Post-merge regression note: the new Cursor workspace/open test was already red at its introducing commit |
Summary
Re-verification of #221's
workspace/openruling against Cursor's current hooks reference, triggered by the challenge that Cursor does support a workspace-open hook. Outcome: partially expressible — implemented as fire-and-forget observation on the Cursor target.The #221 reason had two claims:
workspaceOpen) fire outside any agent session; the request omitsconversation_id,generation_id,model,session_id, andtranscript_pathand carrieshook_event_name,cursor_version,workspace_roots,user_email. But this was a defect of our wrapper's ownsession_idvalidation, not a native inexpressibility.pluginPaths" — wrong per current docs. The output field table markspluginPathsasstring[] (optional): an empty/no-output response is legal and the workspace open continues.Since the canonical
workspace/openfamily is observation-shaped (projection emits no native output), Cursor can honestly support it: the generated event-route wrapper validates the real sessionless envelope, renders the route, and emits nothing. The nativepluginPathsreturn channel is deliberately not modeled and never emitted — no code path can fabricate plugin loads.Native proof (live binary, 2026-09-02 UTC)
Captured on Cursor CLI
cursor-agent 2026.08.31-4057e58(trusted project hooks, non-interactive-prun):workspaceOpenfired with exactly the documented sessionless envelope{"hook_event_name":"workspaceOpen","cursor_version":"2026.08.31-4057e58","workspace_roots":["<workspace>"],"user_email":"<account email>"}while the same run's
sessionStartcarriedsession_id/conversation_id. The capture hook exited 0 with no stdout and the workspace open plus agent session proceeded — proving the pluginPaths-omitted empty response on the real binary, independent of agent-session wrappers (the #189 acceptance bar). Pinned as a dated evidence bullet in the capability table.Changes
events/projection.ts:validateNativeEventEnvelopegets a cursorworkspace/openbranch (requires nonemptyworkspace_rootsstring array +cursor_version, acceptsuser_emailstring|null, requires no session fields);projectEventDocumentexplicitly rejectsdeny/updatedInput(observation-only) and context text (no output channel) and returns no output otherwise.adapters/hook-contract.ts: the cursorworkspace/openlifecycle starter is the real sessionless envelope.adapters/capabilities/cursor-2026-08-28.json:workspace/openflipped to supported; stale "must return pluginPaths" evidence replaced with dated doc citation + live CLI capture; observedAt bumped to 2026-09-02.hooks.eventshandler hooks stay unavailable (diagnostic preserved): the session-scoped handler vocabulary genuinely cannot express the sessionless envelope. Compositeplugintarget stays unavailable (Claude ∩ Codex have no such event); its merged reason no longer mentions pluginPaths.ready), envelope validation, projection rejections through real Flight render, generated-wrapper execution with empty stdout, and feat(dev): semantic lifecycle replay (#105 stage 3) #279 lifecycle fixture replay assertingnativeResponseundefined.examples/rsc-agent-runtime/README.mdevent matrix row updated (Cursor: supported observe-only; Claude/Codex unchanged).Gates (local, after rebase onto 3c08e6e)
pnpm typecheckclean,pnpm lintclean (0/0 across 978 files)pnpm test:route-unit,pnpm test:projection: all passCloses the #189
workspaceOpenchecklist row ("Correct workspaceOpen input/output (pluginPaths) and prove it independently of agent-session wrappers").