Applied the extension-command eligibility check inside the shared AgentSession title gate so editor and CLI bootstrap submissions cannot diverge.
Added direct regression coverage for programmatic startup submissions.
Fixes#7166
Moved automatic title eligibility and persistence into AgentSession so editor and CLI bootstrap submissions share one path.
Added a PTY regression probe covering the positional-message launch flow.
Fixes#7166
- Removed copy and delete operations across tokenizer, parser, grammar, and clipboard logic.
- Standardized line-editing operations and block resolvers to use cut exclusively.
- Updated documentation, prompts, and test suites to reflect the removal of copy and delete syntax.
- Implemented clipboard register management, parsing, and execution rules for CUT, COPY, and PASTE operations in the hashline engine.
- Added session-persistent clipboard state and integration across agent session execution, diff previews, and streaming tools.
- Added comprehensive validation, error messages, recovery handling, and test coverage for clipboard and block operations.
Three remaining review findings:
- `list_mcp_resources` frames a handler answered now synthesize a
`list_mcp_resources` block and pair a result derived from the same
answer sent on the wire; the streamed `ListMcpResourcesToolCall` /
`ReadMcpResourceToolCall` announcements join the exec-owned set so
they cannot double-render. No-handler frames still synthesize
nothing, since nothing ran.
- Advisors receive the same `MCPManager`-backed resource adapter as the
primary bridge, so their `list_mcp_resources` no longer reports every
server as empty and `read_mcp_resource` no longer answers `not_found`
against live connections the advisor shares.
- An unavailable `pi_edit`/`pi_write` answers with the protocol's
`rejected` variant instead of `error`: refusal and failure are
separate oneof cases, and a denial reported as an execution error
invites a retry of an operation that was never permitted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSWZTe6YA2PX1cqtukZvYi
(cherry picked from commit 47ce936c8df05d6970504af19e5ef7d2e8c38d7b)
Download-mode resource reads created and overwrote workspace files
without running a registry tool - the same hole the native `delete`
frame had - so a session that withheld `write`/`edit`, or whose `write`
tier is `deny`/`always-ask`, still had files written. Both frames now
share one grant and one policy check, and the download refuses before
the read so a blocked call never fetches the resource.
`allowNativeDelete` is renamed `allowDirectFileMutation`: it now gates
more than deletion. The primary session derives it from the registry
BEFORE its own rewriting (Cursor moves `edit` out of the tool map and
`write` may be auto-registered later, so reading the map at bridge
construction would misjudge both) and unconditionally, since the bridge
is installed for every session and one that starts on another provider
can switch to Cursor later.
`pi_grep` with a match cap: the local tool windows to 20 files and
suggests `skip`, which `PiGrepExecArgs` cannot express - 100 matches
requested over 25 one-match files returned 20, with the cap reported
unreached. A capped search now reads cap+1 files, so a result landing
exactly on the cap is distinguishable from a clipped one, and
`match_limit_reached` is truthful either way.
`read_mcp_resource` synthesized no transcript block and paired no
result, so a read - including a download that mutates the workspace -
was invisible in the UI and stripped from every rebuilt history. It now
synthesizes a `read_mcp_resource` block (not `read`: the name drives
rendering and prune semantics) and pairs success, not-found and error.
(cherry picked from commit 5ff27a3efe8bec522d9d5dbd7763055eb03eae3b)
Two independent bugs found in review.
A stream that dies mid-turn takes the terminal-error path: `settleH2`
rejects when the transport closes without `turnEnded`, so the flush on
the success path never runs. `connect_scm` and native todo blocks are
stamped `kCursorExecResolved` at start, so `agent-loop.ts` synthesizes
no placeholder and only their completion frame pairs a result - the call
was left unpaired and its card animating, and `buildSessionContext`
strips a dangling call from every rebuilt transcript. The catch path now
closes open blocks and pairs those server-owned calls with an
interrupted result. Exec-settled MCP blocks are excluded: the dispatch
that ran them owns their result and `drainInFlightDispatches` awaits it,
so pairing here would duplicate against the same id.
Separately, the advisor bridge supplied no `getToolContext`.
`ExtensionToolWrapper` reads the approval mode, per-tool policies and
`autoApprove` only from that execute-time context, so every wrapped
advisor bridge tool resolved as `yolo` with empty policies - a
configured `ask` or `deny` on `edit`/`grep` did not apply to native
frames. Advisors now get the same `ToolContextStore` as the primary
bridge.
Both are covered against the real paths: the interrupted call through
the HTTP/2 fixture server (a helper-level test passes even with the
catch-path flush removed), and approval through real deny policies.
(cherry picked from commit 5ace682578af96708caf88db2d44b9077a1c0e74)
The primary bridge builds a `replace`-mode `EditTool` because
`PiEditExecArgs` carries `old_text`/`new_text` pairs that no other mode
accepts. The advisor roster passed its own instances straight through,
and those follow the session's configured `edit.mode` - `hashline` by
default, whose schema is a single `input` string - so every native
advisor edit failed validation instead of touching the file.
Both bridge-only tools now come from `cursor-bridge-tools.ts`:
`createBridgeEditTool` builds the wrapped `replace` instance, and
`bridgeToolMap` substitutes it into a granted map. The substitution is
gated on `edit` actually having been granted, since the tool is
constructed rather than looked up - handing one to a read-only roster is
the #5680 escalation. The advisor's own loop keeps its instance; only
the exec map is swapped.
(cherry picked from commit e6cf9f8046c595cab9793b4b2a5795d8488d8d22)
Three defects the exec bridge shipped with, all found by review.
`pi_edit` never worked. The session removes `edit` from the tool
registry for Cursor so the model is steered to full-file `write`
(8ba0498eb), but that same registry is the bridge's tool source, so the
native frame — which the server sends regardless of the advertised
catalog — resolved nothing and answered `Tool "edit" not available`.
Retaining the instance is not enough either: `PiEditExecArgs` carries
`old_text`/`new_text` pairs, which only `replace` accepts, while the
default mode is `hashline` (`{ input: string }`). `EditTool` now takes
an optional mode, and the bridge resolves a pinned `replace` instance
through its fallback resolver.
A `pi_grep` frame carrying `context` or `limit` escaped the approval
gate. Honoring those needs a per-call tool, and the per-call instance
was built raw while every registry tool is wrapped — so exactly those
calls skipped `tools.approval.grep` and the exec-tier SSH check. Both
callsites now go through one `createBridgeGrepFactory`.
Advisors ignored the same two fields: only the primary session supplied
the factory. They now get it too, gated on the advisor actually holding
`grep` so the factory cannot grant a denied tool.
Also moves the pure Pi arg translation to `providers/cursor-pi-args`.
The legacy shim shares it and is compiled into the bundled virtual
registry, where `./providers/*` cannot match a nested specifier — it
fell through to `Bun.resolveSync`, unsatisfiable under bunfs (#3442) —
and the exec module would have dragged the protobuf graph along.
Verified against real files and the real module graph: `pi_edit` mutates
a temp file, the bundled probe executes the shim's shared module in a
subprocess, and the grep test drives the shared factory. Mutation-
checked: returning a raw tool from the factory, ignoring the pinned edit
mode, dropping the `getTool` fallback, or moving the helpers back to a
nested path each fails a test.
(cherry picked from commit e46ba22b634e449005f7c22b6d0efd19a45ce1f8)
- Support parsing compacted records with replacement history and conversion to compaction entries.
- Add test coverage for foreign session import of Codex session compactions.
- Implemented session stores and metadata converters to import Claude and Codex sessions into OMP.
- Added `--from-claude` and `--from-codex` CLI flags and `/resume` command arguments for foreign session resolution.
- Updated session selector components and controllers to support listing and picking external agent sessions.
- Added comprehensive unit tests and documentation covering foreign session import functionality.
- reuse the centralized replay-unsafe predicate in Fast fallback
- exercise visible-text and replay-safe Fireworks branches
(cherry picked from commit f596392b891bb8ec82171a25c6651ed3cc8e0d61)
The previous reset dropped #pendingXdevMountDelta alongside the announced
baseline. Unlike /new and different-session switchSession, branch() does
not rebuild the base system prompt afterward, and because the device is
already in mountedNames no later refresh re-queues an add delta. Dropping
the undelivered delta therefore left the branched transcript unaware of a
still-mounted discoverable device.
Only the announced baseline is reset now; pending adds (still-live mounts
awaiting delivery) survive and announce on the next prompt in the new
transcript. Redundant on /new (the rebuilt prompt lists them too) but
harmless, and correct for branch.
Fixes#6921
(cherry picked from commit 5866440f27c15a320657e25fbfa0d2d65aad1fa2)
The announced-mount baseline persisted across /new, switchSession, and
branch, which replace agent.state.messages but only clear session-scoped
tool state. A device announced in the old transcript stayed in the cache,
so reconnecting it into the fresh history was filtered as already known
and never announced, leaving the new conversation unaware of the device.
Reset the announced baseline (and any undelivered pending delta) from
#clearSessionScopedToolState, so the next notice re-seeds from the new
transcript and a reconnecting device announces again.
Fixes#6921
(cherry picked from commit d06dde02b9de4aacacd7aea5ee51edc7e524e3fb)
Replayed the stable added and removed inventory sections from legacy
xdev-mount-notice content when structured details are absent. This keeps
the first post-upgrade resume from re-announcing devices that persisted
history already introduced.
Covered both structured and legacy resume histories, including removed
devices and inline docs that must not be interpreted as inventory.
Fixes#6921
(cherry picked from commit 634a4c2de75f99e219f408c56bed83c04fe1290a)
Mount-notice injection diff-gated only against the in-memory mountedNames
set, which is reseeded on every process resume / host reconnect. Dynamic
devices (MCP / RPC host) already announced in persisted history therefore
re-announced, splicing a redundant developer message that busts the
provider prompt-cache prefix and re-bills the whole suffix at full price
on metered providers.
Notices now persist a structured { added, removed } payload. On the first
consumption after resume the announced-device baseline is reconstructed
from history, and only a net change relative to what the model already
knows is announced, so a resume re-establishing the same inventory emits
nothing.
Fixes#6921
(cherry picked from commit 03c2ed5189510f41431bd164fa80187a69ed8de9)
#emit's listeners (ACP's #handleLifetimeEvent -> #pushConfigOptionUpdate
-> #buildConfigOptions) run synchronously up to their first await, and
#buildConfigOptions is evaluated as a synchronous argument expression
before that await. Emitting model_changed immediately after
agent.setModel(previousModel) but before #models.restoreThinkingSnapshot
ran meant ACP could push a { previousModel, target-session-thinking }
config that was never an actual session state -- neither the failed
target nor the restored previous session.
Move the emit after restoreThinkingSnapshot/restoreServiceTiers so it
observes fully-restored state, same as every other rollback consumer
in this catch block already does implicitly by running after both.
Found by Codex on PR #6908 (pullrequestreview-4801303428), against
a33aa07df from this same branch.
bun test test/acp-agent.test.ts (57) + agent-session-switch-prev-context,
agent-session-model-persistence, agent-session-model-switch-auth,
agent-session-openai-completions-model-switch, nonvision-model-switch
-- 90 pass, 0 fail. Workspace typecheck clean.
(cherry picked from commit 0ac190a39a1687ebf85849dde5e1af9c2f9147fc)
switchSession's success path may already have called
#setModelWithProviderSessionReset for the target session's model,
which emits model_changed for it. If a later step in the try block
then throws, the catch restores previousModel via a direct
agent.setModel(...) that bypasses that method entirely and emitted
nothing — ACP/RPC/TUI kept advertising the target model that was
never actually committed.
Emit model_changed from the rollback path too, but only when the
restore actually changes the model back (guards the common case where
switchSession never touched the model or restores the same one).
Flagged independently by @roboomp and @chatgpt-codex-connector on
PR #6908.
bun test test/acp-agent.test.ts (57), agent-session-switch-prev-context.test.ts
(3), agent-session-model-persistence.test.ts (10 files) -- 80 pass, 0 fail.
(cherry picked from commit a33aa07df6fd61508956d73cc5b5284051bbfa86)
model_changed has no extension-facing hook (#emitExtensionEvent never
maps it), unlike message_start/tool_execution_end/etc. Routing it
through #emitSessionEvent added an await on extension delivery plus
the FIFO subscriber gate inside every model switch, including
retry-fallback on the hot error-recovery path, for zero benefit.
Match the sibling thinking_level_changed event, which already goes
through the plain synchronous #emit. ACP/RPC/TUI still receive it
identically since they subscribe via #eventListeners either way.
No observable behavior change: bun test test/acp-agent.test.ts stays
at 57 pass, dedup logic for client-initiated model changes unaffected
(push still lands during the awaited #setModelById call).
(cherry picked from commit 3edc9f8495b1b06fd990a64756d92e60354451fc)
AgentSession#setModelWithProviderSessionReset is the single choke point
every model mutation runs through (explicit /model, prewalk hand-offs,
retry-fallback, model cycling). It previously changed agent.state.model
silently — no session event told subscribers (ACP, RPC, TUI) that the
active model moved.
Emit a new model_changed AgentSessionEvent from that choke point
whenever the model actually changes, and wire it into every consumer
that must exhaustively handle AgentSessionEvent: the TUI event
controller (invalidates the status line, same as thinking_level_changed)
and the RPC client's forwarded-event allowlist.
(cherry picked from commit f76325de2c7821dd7046ddb67546577c3575a263)
Anthropic's provider-scoped fastModeDisabled state was omitted from the session active predicate, and the unchanged-tier guard prevented explicit retries.