Commit Graph
54 Commits
Author SHA1 Message Date
can1357 5691a039de refactor(packages/coding-agent): restructured acp 0.25 session state
- Removed `userMessageId` from `AcpAgent` prompt state and response payloads.
- Removed `models` from new/load/resume/fork session responses and deleted model-state building.
- Removed `unstable_setSessionModel` and routed model changes through `setSessionConfigOption`.
2026-06-13 00:20:05 +02:00
can1357 965c5d4000 Merge PR #2262: feat(rpc): expose slash command metadata 2026-06-11 17:59:40 +02:00
can1357 37e00b10c4 test(tui): stabilized ACP command tests and added TUI committed-rows coverage
- Replaced the ACP agent test's fixed bootstrap wait with a deterministic `/reload-plugins` flow and asserted against the latest available-commands advertisement.
- Added a TUI probe test that verifies native committed scrollback rows are passed to children before render.
2026-06-11 16:23:41 +02:00
danzaio 3351a15f80 fix(rpc): tolerate legacy command metadata 2026-06-10 16:04:26 -03:00
can1357 e3501e3f76 chore: fix merge issues 2026-06-10 08:35:51 +02:00
can1357 1c69483241 Merge pull request #2186: fix(acp): auto-cancel in-flight turn when new prompt arrives mid-flight 2026-06-10 08:26:59 +02:00
can1357 5187dc1a7b fix(acp): skip colon-namespaced extension commands shadowed by builtins
parseSlashCommand treats ':' as a name/args separator, so an extension
command like 'model:foo' was advertised in available_commands_update but
dispatched to the '/model' builtin. Filter such names via
isAcpBuiltinShadowedName, and fix the FakeAgentSession prompt stubs in
acp-agent.test.ts to return true now that AgentSession.prompt() reports
whether the agent was invoked (6 tests were failing against the new
early-finish path).

Addresses review feedback on #2052.
2026-06-10 08:26:08 +02:00
d8db20cb0d fix(acp): advertise extension commands before custom TS commands
Dispatch in AgentSession runs #tryExecuteExtensionCommand before
#tryExecuteCustomCommand, so the palette must reflect the same order.
Moving the extension-runner block before session.customCommands ensures
that on a name collision the advertised command matches what will
actually execute.

Update the test to assert the extension description wins over the
colliding custom TS description.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 08:26:07 +02:00
0d67407773 fix(acp): address review comments on extension-commands PR
- Update ordering comment in #buildAvailableCommands to document the
  extension tier and explain why skills/custom TS commands intentionally
  shadow extension commands (unlike interactive mode)
- Add CHANGELOG entry under [Unreleased]
- Add regression test: verifies extension commands surface in
  available_commands_update and that a builtin-colliding extension
  command is excluded via the reserved-set, with no duplicates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 08:26:07 +02:00
can1357 bbd93437ca fix(acp): close session when implicit cancel cleanup times out
The implicit-cancel overlap path dropped the cleanup promise, so an
abort() that hung past the cleanup timeout left the managed session
registered with a still-streaming AgentSession — the explicit cancel()
path closes it in that case. Mirror that handling and cover it with a
regression test.

Addresses review feedback on #2186.
2026-06-10 08:26:01 +02:00
Theo Mathieuandcan1357 bebd9dffa1 review: remove unrelated changelog bullets; assert abort() barrier in test 2026-06-10 08:26:01 +02:00
Theo Mathieuandcan1357 094c6e5fdc fix(acp): auto-cancel in-flight turn when new prompt arrives mid-flight
When the user presses Stop in Zed and immediately types a new message,
the new session/prompt RPC can arrive before (or without) a preceding
session/cancel notification. The previous guard threw an error in that
case, leaving the session stuck and blocking further interaction.

Replace the throw with an implicit cancel: call #beginCancelCleanup on
the unsettled turn so it resolves with stopReason:"cancelled", then let
#queuePrompt serialize the new prompt behind the abort cleanup as it
already does when session/cancel is called explicitly. #beginCancelCleanup
is idempotent so a concurrent explicit cancel notification is a no-op.

Updated the test to assert the new contract: overlapping prompt
auto-cancels the first turn and the second is processed normally.
2026-06-10 08:26:01 +02:00
can1357 ae415199dc feat: added build-time compatibility in ModelSpec/buildModel pipeline
- Centralized catalog and registry handling on `ModelSpec` and `buildModel`, resolving compatibility at model build time.
- Removed runtime compatibility detectors and switched provider request flows to direct `model.compat` reads.
- Added compat fields (`supportsReasoningParams`, `alwaysSendMaxTokens`, `strictResponsesPairing`, `whenThinking`).
- Persisted explicit compatibility overrides through `compatConfig` in discovery and cache merge paths.
2026-06-10 06:20:51 +02:00
can1357 9d457f73d9 test: migrated test imports to package subpath exports
- Replaced relative `../src` imports with `@oh-my-pi/pi-ai` and `@oh-my-pi/pi-agent-core` subpaths.
2026-06-08 19:03:55 +02:00
can1357 0f043d4c29 fix(coding-agent): preserved approved plan paths during plan apply resolution
- Replaced approved-plan renaming with `resolveApprovedPlan` resolution and state/slug lookup.
- Updated ACP and interactive apply flows to propagate canonical `planFilePath` instead of renamed paths.
- Added local plan fallback lookup by mtime for unresolved slugs after plan approval.
- Restricted plan-mode writes to `local://` plan artifacts and simplified path handling.
2026-06-07 07:17:23 +02:00
roboompandcan1357 18338e2088 fix(acp): refused dismissed plan approval
#requestAcpPlanApprovalChoice returned 'value !== REFINE_OPTION' which
treats any non-refine outcome — explicit dismissal, transport failure,
abort, or timeout — as approval. That meant closing the elicitation
dialog (or the request failing) granted the agent write access without
explicit user consent.

Tighten to 'value === APPROVE_OPTION' so only the explicit approve
selection passes the gate. Refine, dismissal, and every other outcome
fall through to refine semantics: the caller keeps plan mode active and
returns guidance text instead of renaming the plan and exiting.

Adds a regression test that drives a form-capable harness with a
cancelled elicitation and asserts the plan file is preserved, plan
mode/handler stay active, plan reference stays unset, and no mode-exit
notifications are emitted.
2026-06-04 18:30:22 +02:00
roboomp 493f743958 fix(acp): synced config option after plan approval
ACP plan approval exited plan mode internally and emitted the current-mode
notification, but it did not push the matching mode config-option update.
Clients that cache or render the config selector could therefore keep
showing mode=plan after approval switched the session back to default.

The approval path now emits the same config_option_update used by other
mode transitions, and the regression test asserts that the approval-driven
exit updates both current_mode_update and the mode config option.
2026-06-04 15:37:38 +00:00
roboomp fdb6077b7b fix(acp): wired plan-mode standing resolve handler
ACP plan mode set the plan-mode state but never installed the standing
resolve handler that interactive mode wires in #enterPlanMode. The agent
in plan mode dutifully called resolve { action: 'apply', extra.title }
to submit its plan; ResolveTool.execute then consulted
peekQueueInvoker() ?? peekStandingResolveHandler(), found neither, and
threw 'No pending action to resolve. Nothing to apply or discard.' —
stranding the agent with no path out of plan mode in Zed (and any other
ACP client).

#applyModeChange now registers a standing handler when entering
mode: 'plan' and clears it when leaving. The handler:
 - validates the plan file exists at the configured '/data/workspaces/can1357__oh-my-pi__1869/.omp-session/2026-06-04T15-12-15-302Z_019e9331-31c6-7000-9bce-6be119505a1d/local' path,
 - normalizes the agent-supplied title to a safe filename stem,
 - asks the ACP client to confirm via unstable_createElicitation when
   the client advertises elicitation.form (auto-approves otherwise so
   the agent never gets stuck on a client without the surface),
 - renames the plan to '/data/workspaces/can1357__oh-my-pi__1869/.omp-session/2026-06-04T15-12-15-302Z_019e9331-31c6-7000-9bce-6be119505a1d/local/<title>.md,'
 - sets the plan reference path so the next turn injects the plan as
   context, then clears the standing handler + plan-mode state and
   emits current_mode_update so the client UI reflects the exit.

Refinement (user picks 'Refine plan') returns guidance text and leaves
plan mode active so the agent can iterate.

Fixes #1869
2026-06-04 15:30:27 +00:00
can1357 dc4aeb7b88 refactor(coding-agent): renamed todo_write tool to todo
- Renamed `TodoWriteTool` to `TodoTool` and its source/prompt files.
- Updated tool registration, schema, renderers, and gating to `todo`.
- Adjusted cursor provider native tool names and tests to match.
- Renamed strike-animation constants and `todo-error-reminder` type.
2026-06-04 02:45:30 +02:00
can1357 e831c2c758 chore: reformat 2026-05-30 18:08:51 +02:00
can1357 bfae4d46c3 fix(coding-agent): tracked acp tool args by session for replay
- Tracked ACP tool-call inputs per session and replayed them via `toolArgsById`/`getToolArgs` plumbing.
- Merged ACP tool execution end content from start and result events so command output replay preserves original args.
- Scoped ACP async-job draining by session `ownerId` and `agentId` with in-flight tracking and permission-gated deferred turns.
- Refactored compaction telemetry and async tests with per-test telemetry setup and asynchronous teardown resets.
2026-05-17 13:15:07 +02:00
Can BölükandGitHub 6331953dc8 Merge branch 'main' into acp-todo-plan-sync 2026-05-17 13:12:36 +02:00
Can BölükandGitHub 34ce96e42f Merge pull request #1138 from jiwangyihao/acp-autonomous-continuation
fix(coding-agent): keep ACP async continuations owned
2026-05-17 13:12:15 +02:00
Can BölükandGitHub 1e66bf8bf3 Merge pull request #1132 from jiwangyihao/acp-tool-call-details-pr-d
ACP: keep command details stable across updates and replay
2026-05-17 13:11:59 +02:00
jiwangyihao 05e1e702dd fix(coding-agent): keep ACP async continuations owned 2026-05-17 15:30:09 +08:00
jiwangyihao 3057cbc00d fix(coding-agent): defer ACP session startup 2026-05-17 07:37:25 +08:00
jiwangyihao 204f10ae08 fix(acp): 保留回放与更新中的工具详情 2026-05-17 05:13:50 +08:00
jiwangyihao 32a8f109d1 fix(coding-agent): sync ACP todo plans 2026-05-17 02:52:06 +08:00
can1357 a40c5b1bee refactor(coding-agent): restructured ACP cancel cleanup slot management
- Replaced `finishCleanup` callback with `isPromptTurnInFlight` predicate to unify settled+cleanup gating.
- Extracted `#beginCancelCleanup` (idempotent) and `#runCancelCleanup` to clarify ownership of slot eviction.
- Fork, queue, and close paths now all gate on the combined settled+cleanup window.
2026-05-16 18:18:46 +02:00
jiwangyihao aaddf0631c fix(coding-agent): gate ACP close during cancel cleanup 2026-05-16 21:46:13 +08:00
jiwangyihao 66bf82123e fix(coding-agent): reject stale ACP cancel prompts 2026-05-16 13:27:48 +08:00
jiwangyihao 917535da2b fix(coding-agent): bound ACP cancel cleanup 2026-05-16 12:38:55 +08:00
Gerben Meijer 694f5e9a54 Refresh SSH hosts without restart 2026-05-16 00:20:00 +02:00
Can BölükandGitHub e7cd859d9b Merge pull request #1070 from jiwangyihao/acp-prompt-idle-cleanup-fresh
fix(coding-agent): wait for ACP prompt idle cleanup
2026-05-15 05:11:33 +02:00
David Marshallandomp d47f55c7fd fix(coding-agent/acp): narrow CreateElicitationRequest variant before reading sessionId in live-getter test
`CreateElicitationRequest` is a discriminated union — even after
narrowing on `mode === "form"`, both `ElicitationRequestScope` (no
`sessionId`) and `ElicitationSessionScope` (with `sessionId`)
remain in the union. The new live-getter regression test was reading
`calls[N]!.sessionId` directly, which CI tsgo rejected with TS2339.

Per-element `if (!call || call.mode !== "form" || !("sessionId"
in call))` narrows to the session-scoped variant. Spelled three times
because loop-style narrows don't propagate to the assertions below.
Matches the discriminator pattern used in the older 'translates select'
test at line ~927.

Co-Authored-By: omp <noreply@oh-my-pi.dev>
2026-05-14 10:24:21 -05:00
David Marshallandomp d7e8a7358b style(coding-agent/acp): wrap long createAcpExtensionUiContext test call-sites
`biome check` enforces print-width on the two test-only call-sites that
`ast_edit` collapsed onto a single line during the sessionId-getter
rewrite. Auto-formatter wrap, no behavior change.

Co-Authored-By: omp <noreply@oh-my-pi.dev>
2026-05-14 10:16:26 -05:00
David Marshallandomp 6b63903cf2 refactor(coding-agent/acp): read sessionId lazily on every elicitation
`AgentSession.sessionId` is a getter that reads through to
`sessionManager.getSessionId()` and mutates when an extension command
calls `ctx.newSession` or `ctx.switchSession` (both exposed in the
same #configureExtensions block). Snapshotting the id once at factory
time routed later elicitations to the pre-switch id — diverging from
every other sessionUpdate call in this file, which already reads
record.session.sessionId live.

createAcpExtensionUiContext now takes `getSessionId: () => string` and
calls it per elicitation. Caller passes `() => record.session.sessionId`
so each select / confirm / input picks up the current id.

Also simplifies elicitFromAcpClient: `onAbort` and `finish` had
identical settlement bodies differing only by the resolve value, with a
hand-rolled `removeEventListener` symmetry comment to keep them in
sync. Collapsed to a single settle path — `onAbort = () =>
finish(undefined)` — so future changes to settlement apply to both
paths automatically. Doc-comment tightened to call out that late SDK
`accept` responses (not just rejections) after abort/timeout are also
dropped silently.

New regression test asserts that mutating the captured sessionId
between elicitations is reflected in the next request.

Co-Authored-By: omp <noreply@oh-my-pi.dev>
2026-05-14 09:59:43 -05:00
jiwangyihao 07ca844936 fix(coding-agent): reject overlapping ACP prompts before queueing 2026-05-14 22:25:47 +08:00
jiwangyihao 03d7acb804 fix(coding-agent): serialize ACP idle cleanup prompts 2026-05-14 21:41:38 +08:00
jiwangyihao b15eb064f0 fix(coding-agent): wait for ACP prompt idle cleanup 2026-05-14 20:55:53 +08:00
David Marshallandomp cf623e5748 feat(coding-agent/acp): bridged ExtensionUIContext to ACP unstable_createElicitation
Promotes the stub acpExtensionUiContext to a createAcpExtensionUiContext
factory invoked per session inside #configureExtensions. select / confirm
/ input each map to a single-property `value` schema and round-trip
through a shared elicitFromAcpClient helper that mirrors
RpcExtensionUIContext.#createDialogPromise:

  - capability gating on clientCapabilities.elicitation.form
  - runtime typeof narrowing on accept payloads (wrong-type / missing
    key / no content all fall back to the stub return values)
  - dialogOptions.signal: pre-aborted short-circuits before any SDK
    call; mid-flight abort races the in-flight elicitation. Symmetric
    removeEventListener on both onAbort and finish paths.
  - dialogOptions.timeout: setTimeout(.unref()) settles the promise via
    onTimeout + stub fallback. A throwing onTimeout is caught and
    logged so the elicitation promise still settles.
  - late SDK rejections after abort/timeout are dropped silently;
    transport failures log via logger.warn with { sessionId, method,
    error }.

Empty/whitespace-only placeholders on `input` and empty/whitespace-only
messages on `confirm` are treated as absent (trim-aware), matching the
behavior documented in the CHANGELOG bullet.

15 new tests cover request shape, decline/cancel, missing capability,
transport failure, pre-abort, mid-flight abort, wrong-typed accept,
missing `value` key, no content, timeout, whitespace placeholder,
empty-message join, and throwing onTimeout.

Co-Authored-By: omp <noreply@oh-my-pi.dev>
2026-05-14 00:36:43 -05:00
can1357 0b62f2971b fix(ci): restore @napi-rs/cli dep and align tests with current types
The earlier exports/cleanup refactor removed @napi-rs/cli from
packages/natives devDeps, breaking every native job. It also left two
test files calling private symbols and a stale KeyId literal:

- packages/natives: re-add @napi-rs/cli (catalog) so node_modules/.bin/napi
  exists for build-native.ts.
- test/acp-agent.test.ts: import ACP_BOOTSTRAP_RACE_GUARD_MS from
  modes/acp/acp-agent (previously implicit via mass-export refactor).
- test/silent-abort-overlay-render.test.ts: lowercase 'Ctrl+S' -> 'ctrl+s'
  to match the KeyId union.
2026-05-14 05:12:07 +02:00
Can BölükandGitHub 8f1d98fa38 Merge branch 'main' into fix/skill-chip-and-silent-abort 2026-05-14 04:51:41 +02:00
Can BölükandGitHub 3ad9255b18 Merge branch 'main' into dmarsh/acp-thinking-level-push 2026-05-14 04:43:55 +02:00
can1357 f1f6516056 refactor: reorganized exports and removed obsolete helper branches
- Removed export leakage by demoting many helper and const symbols to module-local scope.
- Renamed underscore-prefixed internals and cache fields, then updated related references and `satisfies never` checks.
- Deleted obsolete logic branches and helpers, including harmony-stream interruption flow and unused benchmark runtime helpers.
- Updated Biome config and manifests by broadening lint coverage and removing an unused `@napi-rs/cli` dev dependency.
- Adjusted tests and utilities to use renamed test helpers and remove redundant private test-only helpers/locals.
2026-05-14 04:36:19 +02:00
cognitive c118eacdaa fix: gate SILENT_ABORT_MARKER at three unguarded render sites
Codex review flagged that the silent-abort sentinel
("__omp.silent_abort__") persists into AssistantMessage.errorMessage
but three downstream consumers render errorMessage verbatim:

- session-observer-overlay.ts: renders "✗ Error: __omp.silent_abort__"
  when content is empty (confirmed user-visible today)
- print-mode.ts: writes marker to stderr and exits non-zero (latent;
  plan-mode→compact not reachable from print mode today, but unguarded)
- acp-agent.ts: emits marker as agent_message_chunk text to ACP
  clients when message has no other notifications (latent)

Add isSilentAbort() guard at each site. Extend the SILENT_ABORT_MARKER
consumer list in messages.ts doc comment to include all six consumers.
Add regression tests: overlay (2 tests), print-mode (2 tests), ACP
replay (1 test).

Op: correct
Restores: spec:silent-abort-marker-never-surfaces
2026-05-13 23:58:13 +00:00
David Marshallandomp b626609314 refactor(coding-agent/acp): extracted ACP_BOOTSTRAP_RACE_GUARD_MS and consolidated race notes
/simplify pass on 4882d1e38. Three small cleanups, no behavior change.

* Extracted the inline 50ms bootstrap-race guard into an exported
  ACP_BOOTSTRAP_RACE_GUARD_MS constant at the top of acp-agent.ts.
  Source uses it in the #scheduleBootstrapUpdates setTimeout. Tests import
  it and call a new waitForBootstrapGuard() helper (constant + 30ms slack
  for setTimeout drift) instead of three hardcoded Bun.sleep(80) sites —
  tests now bind to the source-of-truth instead of dueling magic numbers.
* Consolidated four block comments that all narrated the same race story
  into one canonical explanation at the install site (#scheduleBootstrapUpdates).
  Field declaration, #registerPreparedSession, and the setSessionConfigOption
  handler keep brief one/two-line pointers. Net change is roughly 30 lines
  of comments removed without losing the diagnosis.
* Trimmed the handler-site thinkingHandledBySubscription comment from six
  lines to three; the local-variable name carries the intent.

Verified:
* bun test test/acp-agent.test.ts: 11/11 pass
* biome check on touched files: clean
* No behavior change (no test had to be updated)

Co-Authored-By: omp <noreply@oh-my-pi.dev>
2026-05-13 16:43:45 -05:00
David Marshallandomp 4882d1e386 fix(coding-agent/acp): deferred thinking-level lifetime subscription until after bootstrap-guard
Addresses codex review on #1060: an extension session_start handler that
calls setThinkingLevel via the exposed extension action (line 1541) would
have run BEFORE #registerPreparedSession set the record into #sessions and
BEFORE the session/new response was delivered to the client, causing
config_option_update to be pushed for a session id the client did not yet
know about. This is the exact race that #scheduleBootstrapUpdates already
documents and guards for available_commands_update / session_info_update
(Zed's 'Received session notification for unknown session' drop).

Moved the session.subscribe(...) installation out of #registerPreparedSession
and into #scheduleBootstrapUpdates's 50ms timer callback so the lifetime
subscription shares the same response-delivery guard as the existing
bootstrap notifications. The pre-bootstrap thinking level is still
communicated to the client through the response payload's configOptions
(newSession / loadSession / resumeSession / unstable_forkSession all return
it), so no state is lost; it is only the notification that is deferred.

For client-driven setSessionConfigOption({thinking}) the handler now only
skips its own push when the lifetime subscription is already installed.
Pre-bootstrap the handler keeps pushing (the client knows the session id
because they passed it in), post-bootstrap the subscription pushes
exactly once. No double-push, no missing pre-bootstrap notification.

Tests:
- updated existing pushes-config-option-update test to await past the 50ms
  bootstrap timer before driving the internal setThinkingLevel
- updated the single-config_option_update-per-setSessionConfigOption test
  the same way
- added 'suppresses lifetime config_option_update during the bootstrap
  window' regression that drives setThinkingLevel synchronously after
  newSession and asserts zero notifications, then asserts notifications
  resume after the bootstrap timer fires
- bun test test/acp-agent.test.ts: 11/11 pass

Co-Authored-By: omp <noreply@oh-my-pi.dev>
2026-05-13 16:34:54 -05:00
David Marshallandomp c7722838b7 fix(coding-agent/acp): pushed config_option_update on every thinking-level change
ACP clients (Zed, etc.) only received `config_option_update` notifications
when they themselves drove the change via `session/set_session_config_option`.
Internal thinking-level updates (slash commands, automatic model-driven
adjustments, extension UI) bypassed the notification path, so client config
panels went stale until the next user-initiated change.

AgentSession now emits a `thinking_level_changed` event from
`setThinkingLevel`, and AcpAgent installs a session-lifetime subscription on
each managed session that pushes a fresh `config_option_update` whenever the
event fires — independent of prompt-turn lifecycle. The
`session/set_session_config_option` handler no longer pushes its own
notification for the `thinking` config (lifetime subscription covers it);
the response still returns fresh `configOptions` so callers see the new
state synchronously. Subscriptions are released in `#disposeSessionRecord`.

Also consolidated four duplicate `config_option_update` send sites into a
new `#pushConfigOptionUpdate(record)` helper.

Tests: added two cases to `test/acp-agent.test.ts` — one verifying internal
`setThinkingLevel` calls produce a `config_option_update` and a no-op
re-set produces none, and one verifying client-driven
`setSessionConfigOption(thinking, …)` produces exactly one notification.

Co-Authored-By: omp <noreply@oh-my-pi.dev>
2026-05-13 16:14:18 -05:00
Ogrodevandcan1357 4e4e74be49 fix(coding-agent/acp): tighten ACP conformance per review feedback
Addresses the codex review comments on #1015 plus a sweep of adjacent
ACP conformance gaps surfaced while wiring them up.

Tool call + diff metadata
- acp-event-mapper: thread session cwd through and resolve every
  `ToolCallLocation` (initial args, in-flight updates, result details)
  to absolute paths against it; ACP requires absolute paths for
  client-side file mapping.
- edit/modes/patch: emit the destination path for moves in the diff
  result so post-edit "open file" actions land on the new file.

Permissions
- agent-session: pass cwd into `extractPermissionLocations` and resolve
  raw `path`/`file`/etc. fields against it before sending
  `session/request_permission`.
- agent-session: gate the permission wrapper on
  `bridge.capabilities.requestPermission && bridge.requestPermission`,
  matching the read/write/bash capability+method pattern.

acp-agent
- `authenticate`: validate `methodId` against the methods advertised by
  `initialize` and reject anything else, so malformed clients fail fast.
- `setSessionConfigOption(MODE_CONFIG_ID)`: also emit
  `current_mode_update` so clients tracking `modes.currentModeId` see
  the same transition `session/set_mode` would produce.
- Pass `runtime.notifyConfigChanged` to builtins; emit
  `available_commands_update` from a shared `reloadPlugins` helper
  reused by `/reload-plugins`, `/marketplace`, and `/plugins`.
- prompt resource handling: route `resource` content with `image/*`
  MIME into the `images` array instead of dropping it as an opaque
  blob; non-image blobs still fall back to the URI placeholder.
- pass session cwd to the event mapper.

Builtins
- model: call `runtime.notifyConfigChanged()` after a successful
  `setModel` so the ACP config selector reflects the new model
  immediately.
- mcp: redact query strings and userinfo from MCP server URLs before
  emitting them in `/mcp list` (prevents leaking `?exaApiKey=…` style
  secrets); wire `manager.setAuthStorage(...)` before `prepareConfig`
  in `/mcp test|resources|prompts` so OAuth servers can refresh tokens.
- ssh: reject non-integer `--port` values via a `^\d+$` guard instead
  of silently coercing through `Number.parseInt`; list project hosts
  first and dedupe user-scope duplicates to match capability-loader
  precedence.
- export: reject clipboard aliases (`--copy`, `clipboard`, `copy`)
  before passing them to `exportToHtml` as a filename.
- compact / force / move / browser: surface underlying failures via
  `usage(errorMessage(...))` instead of letting them crash the command.
- session save|delete: route through the active SessionManager so the
  persist writer is consulted and stale storage references are removed.
- marketplace / plugins / reload-plugins: call `runtime.reloadPlugins()`
  on install/uninstall/upgrade and enable/disable so slash command
  registries and command lists refresh consistently.
- shared.usage: make async and `await runtime.output(...)` so
  `sessionUpdate` text is never dropped or reordered.
- types: document the new `reloadPlugins` and `notifyConfigChanged`
  runtime hooks.

bash tool
- Use a shared `fireKill()` from the abort listener so `session/cancel`
  terminates the remote command immediately instead of waiting for the
  next `currentOutput()` round trip.
- Race `currentOutput()` against the abort signal so a stuck
  `terminal/output` RPC cannot delay cancellation.
- Kill the terminal before reading final output on timeout so a slow
  output read cannot let a timed-out command keep running past the
  enforced timeout.

Tests
- acp-agent.test: extend the existing config-option assertions to
  verify both `model` and `thinking_level` changes emit
  `config_option_update` notifications scoped to the right session.
- acp-builtins.test: cover `/model` emitting both
  `notifyTitleChanged` and `notifyConfigChanged`; lock in the parsed
  `mcp add` / `ssh add` call shapes so future arg-parser regressions
  fail the test instead of silently writing different configs; add a
  `reloadPlugins` stub plus a typed `notifyConfigChanged` slot to the
  shared test runtime factory.
- acp-stdout-hygiene.test: drain stderr in parallel and assert no
  JSON-RPC frame leaks onto it; terminate the spawned process so the
  stderr pump resolves deterministically.

CHANGELOG: itemize the above under `[Unreleased] > Fixed`.

CI
- bun run check: clean (TS + Rust)
- bun run test: 4128 pass / 689 skip / 0 fail (TS); 252 pass / 0 fail
  (Rust nextest)
- bun run ci:test:smoke: --version / --help / `stats --help` all OK
2026-05-13 06:00:45 +02:00