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)
The rebase onto a tree where 17.1.5 had been cut landed this branch's
entries inside that released section. Released sections are immutable,
so they belong under [Unreleased] — the release script folds them into
whatever version ships next.
Both released sections are now byte-identical to upstream/main, no
bullet was dropped in the move, and the `Added` heading the
coding-agent entry needs came back with it.
(cherry picked from commit c1c6566dccb7255395e463c940cca6c76a707a9d)
Both are constructed rather than looked up, and `executeTool` prefers a
constructed override over the registry — so a session that withheld
either still got a working frame. Native `pi_edit`/`pi_grep` arrive
regardless of the advertised catalog, so a restricted agent
(`toolNames` without them, or `restrictToolNames: true`) could modify
and search files it was never granted.
Both now check the registry for the grant before building. The edit
check reads it before the Cursor-specific delete, since that delete is
about not advertising the tool, not about revoking it. This is the same
escalation the `delete` frame already guards against (#5680); the
advisor path got the grep gate in the previous commit and the primary
session was missed.
(cherry picked from commit 68f82b0ce08bb93db941e0fbe1bd2a515d45c2cb)
`timeout` is `optional int32` and `bash` documents `0` as "disables the
command deadline". Both the bridge and the provider's synthesized block
gated on `timeout && timeout > 0`, folding a supplied `0` into unset —
so the 300s default applied and the long-running command that asked not
to be killed was killed.
The expression was duplicated across the two sides, which is the drift
the shared translation exists to prevent, so it moves into
`cursor-pi-args` as `piTimeout` alongside the other presence-sensitive
mappings. Negatives have no local meaning and would clamp to `bash`'s 1s
floor, so those still fall back to the default.
Verified against a real BashTool: `timeout: 0` yields
`timeoutDisabled: true`, omitted yields the default, `42` passes
through, and `-5` matches the omitted case rather than the 1s clamp.
Mutation-checked on both branches.
(cherry picked from commit db442ae5aed90dc2268b2d898ef99ef4e0961c10)
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)
Two truncation records exist locally. `read`/`grep` set
`details.truncation` (`TruncationResult`), which carries an explicit
`truncated` boolean. `bash` sets `details.meta.truncation`
(`TruncationMeta`), which has no such flag — its presence is the signal.
`piTruncation` read only the first and required the boolean, so every
real Bash truncation was dropped: Cursor got clipped output with no
indication it was clipped. Both shapes now translate; `TruncationResult`
stays authoritative when present so an explicit `false` still suppresses.
Also drops the legacy pi shim's copies of the regex-literal escaper and
the path/glob join. Both were verbatim duplicates of the modern bridge's
helpers, which is the drift the shared translation exists to prevent.
Verified producer-to-consumer, not against a hand-built bag: the test
runs a real `BashTool`, asserts its output has no top-level `truncation`
and no `truncated` flag under `meta`, then feeds those exact details to
`piTruncation`. Typed against the producer's own `TruncationMeta`, so a
renamed field fails compilation rather than silently reverting the bug.
Mutation-checked: reverting to the top-level lookup, restoring the flag
requirement, or dropping the null guard each fails a test.
(cherry picked from commit 6699672d52061b832677dd45315f4aba8d330db1)
`pi_grep` carries a context width and a total match cap. Neither is
expressible in the model-facing `grep` schema — context comes from
`grep.contextBefore`/`grep.contextAfter`, fixed when the shared tool is
constructed — so both were dropped.
`GrepTool` now takes them as constructor options. The model-facing
schema is unchanged: this is a seam for wire bridges whose protocol
supplies the values, mirroring `GlobTool`'s existing options bag. The
bridge builds a per-call `grep` only for frames that supply them;
everything else keeps the shared instance and session defaults.
`pi_ls`'s `limit` stays unmapped, now deliberately and documented. It
caps directory entries, while the local `read` renders a depth-2 tree
and slices rendered lines — nested rows, headers and elision summaries
all count — so `:1+K` would cap a different unit while looking honored.
Verified against real files in a temp dir, not captured arguments:
match counts and context lines are asserted from actual search output.
Mutation-checked — ignoring either option, or dropping the scoped tool
in the bridge, fails a test.
(cherry picked from commit 299ded5a274427c2c2d5de27c00a2056a709581c)
Review of the modern exec wire protocol surfaced defects the committed
suite did not pin.
The Pi bridge dropped frame arguments: `pi_read`'s offset/limit (ranged
reads returned whole files), `pi_grep`'s literal (fixed strings ran as
regexes), and the path/glob join emitted `./`-prefixed specs. These are
`optional int32`, so a present `0` is a value, not "unset" — `limit: 0`
now answers empty rather than reading everything, and `pi_find` clamps
to 1 like the reference client.
The provider synthesized its transcript block from a second, divergent
translation of the same frame, so the displayed operation differed from
the executed one. Both sides now share one mapper in `exec-modern.ts`.
End-of-transport cleanup reparsed every open block's streamed argument
buffer; blocks whose args arrive whole never set that buffer, and
`parseStreamingJson(undefined)` is `{}`, so a truncated turn erased
their arguments.
All fixes are mutation-verified: reverting each one fails a test.
(cherry picked from commit bb7bcfebce4200d436e17d6e39320da13fc85ca8)
Current Cursor CLI builds emit exec frames this client did not model. A
frame whose oneof number is absent from `agent.proto` decodes with
`message.case` unset, so the dispatcher found no handler, ran no tool and
sent no result — the server was left waiting on an execution that never
happened.
Every recognised frame now gets a typed answer:
- The seven Pi tools (45-51) run their local equivalents. They are a
separate wire family from the legacy args, not aliases: `pi_grep`'s
`ignore_case` is the inverse of the local `case` flag, `pi_find`
searches filenames (so it routes to `glob`, not `grep`), and
`pi_edit`'s replacements are renamed to snake_case pairs.
- Hooks, subagents, prechecks, MCP state, smart-mode, canvas,
conversation search and agent-store answer with the error, not-found or
empty-but-valid variant that is true of this client.
- Unnameable frames raise `ExecClientControlMessage.throw`
(`unknown_exec_variant`); recognised frames with no truthful answer —
`git_diff_request`, whose `GetDiffResponse` has no error variant —
raise `exec_variant_unsupported`.
Four frames previously answered `create(XSchema, {})`. In proto3 that is
not an empty result: the oneof is unset and the server reads it as "the
tool ran and produced nothing", indistinguishable from success. They now
send real variants.
`connect_scm` lost its repository (the target rides in a oneof, so the
flat property was always undefined) and settled on a fixed failure at the
announcement, before the server's `success`/`error`/`rejected` verdict
arrived on the completion frame.
The stream decoder tracked a single "current" tool-call block and settled
it on any `toolCallCompleted`, ignoring the envelope `call_id`: an
unrelated completion paired the wrong block, and `start A, start B`
orphaned A so nothing ever paired it — which strips the whole interaction
from every rebuilt transcript. Blocks are now retained per envelope id.
`lsp` is advertised as MCP again; the native `diagnostics` frame covers
one of ~10 actions.
(cherry picked from commit 4d269724a3a448886d13b4323ac02aadbfe38de3)
- 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.
- Reserve the `code_mode_tool_names` metadata key to prevent caller-supplied client extra collisions.
- Preserve the `encrypted_function_args` plaintext-collaboration marker on replayed function calls.
Waited briefly for process exit publication after clean stdout EOF so the process handler preserves the real exit code and stderr, while genuine reader errors still tear down immediately.
Cleared only the matching initialization failure for explicit reloads and added regressions for quick exits, reader errors, ordinary backoff, and immediate reload retries.
Fixes#7041
(cherry picked from commit a76522b759f14421202d4cc437ec611b78be20d1)
buildSystemPrompt hand-rolled systemPrompt?.map(...) and devin's request
builder called (context.systemPrompt ?? []).join(...); both crash when
Context.systemPrompt is a bare string, as legacy @earendil-works/pi-ai
extensions remapped onto the fork pass it. The failure surfaced as
stopReason "error" with "systemPrompt?.map is not a function".
Route both through the existing normalizeSystemPrompts() helper, which
already accepts readonly string[] | string, matching the other providers.
Fixes#7037
(cherry picked from commit d681de5daa7e3316f118e6fb4cf8805ec5318c32)
parseConnectEndStream repeats the default status phrase in the message
body, so the trailer reads "resource_exhausted: resource exhausted".
Using a global strip removes both occurrences, so the leftover
"exhausted" no longer trips the generic quota branch and reintroduces
the 30-minute credential block for an otherwise bare status.
Fixes#7032
(cherry picked from commit 3d46a042c649e3892d9c68abbc3789a7719f0667)
Stripped the resource_exhausted status token before classifying the
remaining provider message. Bare or opaque status errors still use the
short model-capacity backoff, while explicit quota, rate-limit, capacity,
or server details remain authoritative.
Added regression coverage for a Connect resource_exhausted trailer with
an explicit quota-exceeded body.
Fixes#7032
(cherry picked from commit ee04c065692e34ccabe1a07b45b5f5987297ff84)
Connect/gRPC end-streams carry the status name `resource_exhausted`
(underscore), but parseRateLimitReason only matched the space phrase
"resource exhausted" in its MODEL_CAPACITY branch. The underscore form
fell through to the generic includes("exhausted") catch-all and was
classified QUOTA_EXHAUSTED, producing a 30-min credential block and the
retry.maxDelayMs fail-fast at the session layer.
Match both forms via /resource.?exhausted/i, consistent with the
existing resource.?exhausted clause in USAGE_LIMIT_PATTERN, which is left
untouched so stream/session credential rotation is preserved.
Fixes#7032
(cherry picked from commit bc18cbb5b9bcf5c2bf41dc494581ae31061fcfcb)