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.
Extension commands (e.g. /sonnet) and TypeScript custom commands that
consume the input without calling the LLM return early from
session.prompt() with no agent turn. In ACP mode this left the pending
prompt promise unresolved, hanging the client forever.
Change session.prompt() to return Promise<boolean>: true when the LLM
was invoked, false when the command was fully handled locally.
#runPromptOrCommand calls #finishPrompt immediately on a false return so
the ACP turn completes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ACP_BUILTIN_SLASH_COMMANDS only carries primary names; the reserved set
passed to getRegisteredCommands was therefore missing aliases like
"models" (/model) and "force:" (/force). An extension registering one
of these aliases would appear in the palette but the builtin would win
at dispatch time (lookupBuiltinSlashCommand searches aliases too).
Export ACP_BUILTIN_RESERVED_NAMES from acp-builtins — the union of all
primary names and aliases for ACP-surfaced builtins — and use it as the
reserved set. Widen getRegisteredCommands parameter to ReadonlySet since
it only calls .has().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- 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>
`#buildAvailableCommands` was omitting commands registered by extensions
via `extensionRunner.getRegisteredCommands()`. ACP clients (e.g. Zed)
never saw these in the `available_commands_update` notification, so they
couldn't forward the corresponding slash commands to the agent.
Mirrors the interactive-mode pattern: pass ACP builtin names as the
reserved set so extensions cannot shadow core commands.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The native arboard backend cannot retain X11 / Wayland selection ownership
after the calling process exits, and for short-lived napi calls it can drop
ownership before any consumer sees the selection — leaving the clipboard
empty even though set_text returned success. tmux + QTerminal made this
visible because OSC 52 is also dropped by libqtermwidget, so both backends
fail and the UI's 'Copied to clipboard' status reads as a lie.
utils/clipboard.ts now:
- Tries wl-copy / xclip / xsel before arboard on Linux. These CLIs fork
after reading stdin and serve the selection until another app claims it,
so the payload survives our process exit.
- Honors OMP_CLIPBOARD_COMMAND as a shell-string escape hatch (e.g.
`xclip -selection clipboard -in -silent`).
- Logs a single warning when every backend fails, so the silent-success
regression from #2075 cannot recur unnoticed.
Tests assert dispatch order: Linux+X11 prefers xclip, Wayland prefers
wl-copy, xclip→xsel fallback works, all-fail falls back to native,
macOS still goes straight to native, and OMP_CLIPBOARD_COMMAND wins
over the CLI chain.
Fixes#2075
runEphemeralTurn (IRC//btw) called streamSimple directly with the raw
system prompt, bypassing the SDK-level obfuscateProviderContext wrapper;
and #obfuscatePreparationForProvider skipped previousPreserveData, so a
pre-fix openaiRemoteCompaction.replacementHistory could resend raw
secrets on the next remote compaction.
Addresses review feedback on #2147.
Obfuscated preparation.previousSummary, hook prompt/context, before forwarding to compact() so prior pi- or extension-supplied summaries do not leak verbatim secrets on subsequent compactions.
Fixes#2146
Obfuscated custom instructions before handoff and related side-request provider calls, then deobfuscated generated handoff output before persistence.
Fixes#2146
Converted provider-facing tool parameters to wire JSON Schema before redaction so live Zod instances are not deep-cloned into plain objects.
Fixes#2146
Redacted configured secrets across provider-facing system prompts, tool definitions, developer reminders, and assistant tool-call payloads before LLM requests.
Fixes#2146
main's CachedOutputBlock.render now returns readonly string[]; the
cache slot added by this PR was still mutable, failing check:types.
Addresses review feedback on #2083.
After rebasing onto a13e9827f, #createFileSink's head-retention flush
wrote directly to the sink, bypassing #emitToSink's budget accounting —
the on-disk artifact could grow past artifactMaxBytes by up to the head
window. Route the flush through #emitToSink and pin it with a
regression test (fails 24B vs 16B cap without the fix).
Addresses review feedback on #2083.
Codex review on PR #2083 caught a corruption case in `OutputSink`:
when a stream exceeds `artifactHeadBytes` but still fits below
`artifactMaxBytes`, post-head bytes flow into the tail ring without any
eviction (`droppedBytes === 0`) — yet `#flushArtifactTailIfCapped`
unconditionally injected `[ARTIFACT TRUNCATED: kept first … + last … of
…; 0 B elided from the middle]` between head and tail. The resulting
artifact-on-disk is then no longer verbatim and falsely advertises
truncation for outputs that actually fit. With the default 4 MiB / 3 MiB
split, every ~3–4 MiB bash capture in this band tripped the bug.
The notice is now gated on `droppedBytes > 0`. The tail ring is still
flushed unconditionally so head + tail still equal the verbatim stream
in this band. Regression pinned by a new test in
`test/streaming-output.test.ts` that pushes 24 bytes into a 16-head /
16-tail cap and asserts the file equals the payload byte-for-byte with
no `[ARTIFACT TRUNCATED:` marker.
Refs #2081
Follow-up to the loop guard + artifact cap that addressed the root cause
of issue #2081's runaway captures. The reporter then noted Ctrl+X/Ctrl+C
remaining unresponsive — confirming the secondary symptom: per-keystroke
TUI repaints walked every visible bash row and re-ran `split` /
`replaceTabs` / `truncateToVisualLines` over the stored output. With a
1,000+ message transcript and a 50KB-tail per row, that string work was
what pinned the main thread, not the loop itself.
The eval renderer already caches its computed lines keyed by `(width,
previewLines)` — see `eval-render.ts:709-752`. Mirrored that pattern in
the bash result renderer with a slightly wider key (`width`,
`previewLines`, `expanded`, `rawOutput`, `isPartial`) so the cache is
busted whenever any input that affects the produced lines actually
changes. `invalidate()` continues to clear `CachedOutputBlock` and now
also clears the lines cache, so callers that already drive invalidation
keep working unchanged.
A render() with cache-equivalent inputs is now an array-reference
return; the `CachedOutputBlock` round trip is skipped entirely. New
test in `test/tools/bash-sixel-render.test.ts` pins the contract:
identical inputs → same array reference; width change → cache miss;
invalidate() → fresh array.
Refs #2081
Two pathologies surfaced in the same captured failure (#2081): a subagent
spent 16 minutes hammering 205 `edit` calls (182 byte-identical no-ops)
against a file that already matched its payload, while a sibling bash
invocation persisted 7.6MB of PowerShell rich-object metadata to
`~/.omp/agent/artifacts/<id>.bash.log` from what was intended as a small
tail. Both are addressed independently here:
- Hashline executor now consults a per-ToolSession `noopLoopGuard` that
hashes the raw patch input and tracks consecutive no-ops per canonical
path. After NOOP_HARD_LIMIT (3) repeats of the same payload the soft
"byte-identical" hint escalates to a thrown ToolError, which the agent
loop surfaces as a tool failure rather than success-with-text — far
more effective at breaking the loop than the soft hint alone. A
non-noop commit (or any variant payload) resets the counter; state is
isolated per ToolSession so subagents cannot inherit each other's
history.
- OutputSink artifact-on-disk writes are now bounded by
`artifactMaxBytes` (default 4 MiB = 3 MiB head + 1 MiB rolling tail).
Once the head budget is exhausted, subsequent chunks divert into a
fixed-size tail ring; `dump()` replays the ring behind a single
`[ARTIFACT TRUNCATED: kept first … + last … of …; … elided from the
middle]` notice before closing the sink. Setting `artifactMaxBytes: 0`
restores the historical unbounded behavior. Sized comfortably above
anything a model would reasonably scroll through via the artifact URL
scheme while preventing the captured 7.6MB spray from sitting on disk.
The terminal-typing lag the reporter observed has multiple compounding
causes (transcript-render freezing is disabled on win32; the bash result
renderer lacks the per-render cache that the eval renderer already has).
Those land in a follow-up — the loop guard + artifact cap address the
root pathologies that turned the session into a multi-MB transcript in
the first place.
Fixes#2081
The streaming-spinner relocation in 61a57c1d2 re-added the trailing
"(preview)" label to expanded approval previews whenever a spinner frame
was active, breaking the #1992 contract. Expanded previews now drop the
label but keep the animated glyph so the volatile tail stays live.
- Raised Anthropic provider retries to 10 attempts and used a shared jittered exponential backoff for each retry.
- Updated coding-agent retry defaults and session delay calculation to a 500ms base with an 8,000ms jittered cap.
- Added tests that verify capped ten-step backoff sequences and recovery after repeated 502 errors.
- Separated non-contiguous diff regions with a single blank gap row, normalized after block-context insertion.
- Rendered gap rows as one dim ellipsis in the TUI and HTML export.
- Applied the same blank-separator dedupe and edge-trimming to hashline's compact diff preview.
- Added "available" status so recognized servers show under lazy mode without warmup.
- Rendered full welcome box as pre-TUI splash with fixed slot heights to avoid layout shift.
- Reported lazy servers as available in /status instead of omitting the section.