brush-core's Process::wait calls tokio::signal::unix::signal(SIGTSTP) to
detect when its children get stopped. Per tokio's documented contract,
the first call for a SignalKind permanently replaces the kernel-default
handler for the lifetime of the process. So once omp has executed any
bash tool call — even /usr/bin/true — SIGTSTP's default "stop" action
is gone, and InputController.handleCtrlZ's process.kill(0, "SIGTSTP")
gets swallowed by tokio. The TUI tore down via ui.stop() but the process
kept running in Sl+ state, leaving the user with a dead terminal that
only kill -9 could recover.
Send SIGSTOP to our own PID instead. SIGSTOP can't be caught, blocked,
or ignored — it stops the process at the kernel regardless of installed
handlers. Targeting self (not pgid=0) also leaves long-lived children
(MCP stdio servers, the persistent brush native shell) running across
the suspend, so they no longer freeze mid-IPC during a quick fg/bg
detour.
Fixes#3461
- Automatically rebind edits to the correct file when an authored path does not exist but the filename and snapshot tag uniquely match a file read earlier in the session.
- Prevent path recovery for paths that would escalate write privileges, ensuring compatibility with read-only internal URL targets.
- Surface warning messages to the model and user upon successful path recovery to encourage correct future path usage.
- Removed `onPasteFilePath` handler to prevent automatic background file attachment when pasting paths.
- Updated `CustomEditor` to treat non-image paths as literal text input.
- Cleaned up unused file system utilities and paste path resolution logic.
- Set logger to silent mode when no transports are active to avoid "no transports" errors during log emission.
- Added a regression test to verify that disabling all transports suppresses warnings and that log output resumes after re-enabling transports.
- Added a check for empty transport arrays during logger initialization.
- Set the logger to silent mode if no transports exist to prevent unnecessary warning messages on every log emit.
Review feedback on #3459: the per-id FIFO queue still carried a stale
malformed occurrence when its rejection toolResult never arrived before
conversation history moved on. If the same id was reused later by a
valid call, that valid call's real result shifted the stale `true` and
was dropped, forcing `transformMessages` to synthesize `No result
provided` despite real output existing.
Scope malformed-result pairing to a single assistant-to-tool-result
window: clear queues at assistant/user/developer/other non-result
boundaries, push current assistant occurrences, and pop only contiguous
matching tool results. Missing results remain handled by the existing
transform passes instead of poisoning later duplicate-id calls.
Adds a regression test for malformed-without-result followed by valid
reuse of the same id.
Review feedback on #3459: the set-based `malformedIds` drop killed
EVERY `toolResult` for the malformed id, including ones tied to a
separate VALID call replaying the same id. OpenAI-Responses composite
ids (`callId|itemId`) collapse on the wire to the same `callId`, and
`deduplicateToolCallIds` already supports that history shape — so a
sibling occurrence being malformed regressed the surviving valid
duplicate from "real result reaches the wire" to "synthetic
`No result provided` from the second pass".
Switch to a per-id FIFO queue: each `toolCall` occurrence pushes its
malformed-ness onto its id's queue in document order; each `toolResult`
pops the first entry for its id and drops only when the popped entry
was malformed. Other results — including the surviving valid
duplicate's real output — are left alone.
Adds a regression test covering shared id across two assistant turns
where one is malformed.
Models occasionally emit `{ "name": "", "arguments": "{}" }` (observed:
GLM-5.2 + thinking on long turns). The agent loop rejected the call at
execution time with `Tool not found`, but the malformed block plus its
error `toolResult` stayed in `currentContext.messages`, so every
subsequent request replayed them and 400'd on `tool_use.name` /
`tool_calls[i].function.name` validation. Sessions wedged in an
unrecoverable 400 loop until manual `/clear`.
`transformMessages` is the canonical sanitize boundary every provider
funnels through (`amazon-bedrock`, `anthropic`, `google-shared`,
`ollama`, `openai-codex-responses`, `openai-completions`,
`openai-shared`), so the defensive filter lives there. It drops
`toolCall` blocks with empty/whitespace `name`, drops matched
`toolResult` messages by `toolCallId`, and drops the assistant turn
when nothing replayable is left. Idempotent and provider-agnostic; one
round-trip after the fix lands self-heals an already-poisoned session.
Fixes#3458
Route mid-conversation developer messages by attribution: only user-attributed entries (auto-learn nudge, advisor cards, file mentions) drop to user; agent-owned reminders (retry, checkpoint, todo) stay on system role.
Refs #3456
Serialize Ollama context developer messages after the static prompt as user turns so llama.cpp can reuse its warm prefix for auto-learn capture follow-ups.
Fixes#3456
The streaming reader's NUL check only walked completed lines collected
from streamLinesFromFile, so a binary blob whose first newline lay past
the byte budget (videos, archives, packed JSON) left collectedLines
empty and slipped through to the firstLineExceedsLimit branch — which
emitted the decoded preview as text instead of the intended refusal.
Sniff firstLinePreview alongside collectedLines so the existing refusal
fires uniformly. Also added a regression test that uses a 256 KiB blob
with no 0x0A bytes to actually exercise the firstLineExceedsLimit
path — the previous 6-byte test fit in one collected line and never
covered the bug.
Fixes#3448
Routed file-backed '/data/workspaces/can1357__oh-my-pi__3448/.omp-session/2026-06-25T07-25-13-303Z_019efdab-28d7-7000-a7a4-e20282508056/local' reads through the normal filesystem reader so binary detection, document/image handling, and streaming safeguards apply before content is materialized.
Hardened the local protocol handler to return metadata-only refusals for binary/container resources instead of decoding them with Bun.file().text().
Fixes#3448
The first pass only enumerated non-wildcard `exports` entries, so
patterns like pi-ai's `./oauth/*` left every concrete target
(`@oh-my-pi/pi-ai/oauth/anthropic` and friends) outside the
bundled registry. Compiled-mode resolution then fell back through
`Bun.resolveSync` → original peer specifier → missing peer dep,
reproducing the original `Cannot find module` failure for any
plugin that imports a wildcard-only subpath (e.g.
`@mariozechner/pi-ai/utils/oauth/anthropic`, remapped via
PI_SUBPATH_REMAPS).
The generator now runs a second pass over wildcard exports,
parses each single-asterisk pattern into prefix/suffix halves,
globs the matching source directory, and emits a registry entry
per concrete `.ts` file. Root catch-all wildcards (`./*` /
`./*.js`) are skipped on purpose — they'd static-import top-level
files like the coding-agent's own `cli.ts` and explode the bundle
through the binary entry's transitive graph. Test, `.d`,
`.generated`, `.bench` files and `index` basenames are filtered
out so the registry stays focused on importable surfaces.
A new test case in
test/extensibility/legacy-pi-bundled-subpath-overrides.test.ts
asserts the reviewer's cited `@oh-my-pi/pi-ai/oauth/anthropic`
key now routes through the virtual namespace and that root
catch-all wildcards remain unbundled.
Fixes#3442
legacy-pi-bundled-registry.ts now static-imports
@oh-my-pi/pi-coding-agent/export/html, whose source pulls
./tool-views.generated.js. The root 'prepare' hook builds that
file on bun install, but a clean binary build that skips install
hooks would 'bun build --compile' against the registry entry and
fail resolving the missing generated bundle. build-binary.ts now
runs collab-web's build:tool-views before the compile, matching
what prepack already does for the npm bundle.
Fixes#3442
Reviewer caught that demoting the whole mixed payload to `user` (`@notes.md
@screenshot.png`) regressed the developer-priority treatment text-only
mentions still get for image-free turns. `generateFileMentionMessages` packs
every `@…` into one `fileMention`, so the previous `hasImage` toggle
collapsed the source-file context into the user slot whenever an image was
attached.
`convertToLlm` now returns up to two messages per `fileMention` via
`flatMap`: text-only files keep their existing `developer` envelope, and
image-bearing files emit a separate `user` envelope that carries their
`<file>` wrappers plus the `input_image` block. Pure-text and pure-image
turns still collapse to a single message.
Tests cover the mixed case (split into developer + user), the image-only case
(single user message), and the existing text-only case (single developer
message).
Fixes#3443
Compiled-binary extension validation rewrote @(scope)/pi-ai/oauth →
@oh-my-pi/pi-ai/oauth, but LEGACY_PI_PACKAGE_ROOT_OVERRIDES only
covered bare package roots. resolveCanonicalPiSpecifier therefore
fell through to Bun.resolveSync, which fails inside bunfs on Bun
1.3.14+, then the rewriteLegacyPiImports catch left the original
specifier alone. Bun's native resolver then failed because most
plugins (e.g. @charmland/pi-hyper-provider) declare @(scope)/pi-ai
as a peerDependency only and never materialize a real install.
A new scripts/generate-legacy-pi-bundled-registry.ts reads every
bundled pi-* package's non-wildcard exports field and emits both
the heavy legacy-pi-bundled-registry.ts (static imports + map) and
a light legacy-pi-bundled-keys.ts. legacy-pi-compat.ts statically
imports the keys file to seed the override map without paying the
legacy-pi-coding-agent-shim → ../index → export/html/... cascade,
so subpath imports now route to the same omp-legacy-pi-bundled:
virtual namespace that already serves the roots.
scripts/build-binary.ts runs the generator before bun build
--compile so new pi-* subpaths added under packages/*/package.json
ship without manual regeneration; --check verifies the committed
output stays in sync.
Fixes#3442
Codex GPT models on chatgpt.com /codex/responses rejected `@image` turns with
`Codex error event: [OneOfParam] [input[N].content[M]] [invalid_enum_value]
Invalid value: 'input_image'. Supported values are: 'input_text'.` —
`convertToLlm`'s `fileMention` arm always emitted a `developer`-role
Responses message, but a developer-role content slot only accepts
`input_text`. #3421's prior fix only suppressed the Codex Responses Lite
header on image-bearing turns; the full transport kept rejecting the same body.
`fileMention` now uses `user` role when any attached file carries an image;
text-only mentions keep `developer` so the auto-read context still rides at
instruction priority for the agent.
Fixes#3443
- Resolve the runtime region for geo-prefixed inference profiles by honoring the profile's geographical prefix instead of defaulting to `us-east-1`.
- Update regional resolution logic to use ambient `AWS_REGION` only when compatible with the model's geo, falling back to a regional default otherwise.
- Prevent HTTP 400 errors for non-US geo-prefixed models caused by improper routing to US-based endpoints.
#3437 (preserve cross-api reasoning replay) and #3439 (preserve 3p thinking blocks across cross-API switches) both rewrite the thinking-block path of transform-messages.ts and the encoder in openai-completions.ts to fix issues #3433/#3434, and overlap structurally. Reconciled into one resolved-compat design:
- transform-messages.ts threads the request-time resolved compat (from #3437) into the predicate, replacing #3439's base+whenThinking union traversal. resolveOpenAICompatPolicy already swaps in whenThinking for thinking-on requests, so the predicate and encoder stay in lockstep (closes both Codex 'use resolved compat' reviews and keeps the #1071/#1484 OpenCode paths correct).
- The predicate keeps #3439's broad coverage: preserve a native, signature-stripped thinking block when the resolved openai-completions target is reasoning-capable, not requiresThinkingAsText, and either requiresReasoningContentForToolCalls or thinkingFormat==='zai'. Other cross-API targets text-demote.
- openai-completions.ts keeps #3439's Z.AI-format encoder branch and #3437's compat threading, but drops #3439's text-fold fallback and isSameModelHistory: under threading they are unreachable (any preserved cross-model block is consumed by the requiresReasoningContentForToolCalls or zai branch; thinking-off targets text-demote in transform). Markup-healed same-model thinking still falls through unemitted, as before.
Regression tests from both PRs retained. Fixes#3433, #3434.
Markup-healed streams (MiniMax M3 \`<think>…</think>\`, Kimi K2 healed
reasoning, every \`getStreamMarkupHealingPattern === "thinking"\` host)
reconstruct thinking blocks from raw text deltas, so the resulting
\`ThinkingContent\` lands with \`thinkingSignature: undefined\`. On a
same-model continuation those blocks are PRIVATE reasoning the source
emitted, not cross-API preserved reasoning — promoting them into the next
request's visible \`content\` would leak hidden chain-of-thought.
The text-fold fallback added in 8f7fb1d for cross-API preserved blocks now
gates on \`!isSameModelHistory\` before firing. Same-model history keeps the
legacy behavior of silently dropping unsigned thinking when no per-compat
branch consumes it, matching pre-#3434 semantics. Cross-API replays
(different api, provider, or model id) still fold to content so the
reasoning chain at least survives as conversation context.
Tests: \`issue-3434-repro.test.ts\` adds a pin for same-model unsigned
thinking on \`opencode-go\`/Kimi — visible content stays exactly the model's
visible answer, no reasoning_content / reasoning / reasoning_text on the
wire.
\`bun test packages/ai\`: 2297 pass / 1 fail (the same \`proxy.test.ts\`
\`Bun.env\` leak that fails on \`main\`). \`bun check\` passes.
Addresses chatgpt-codex-connector P2 review on #3439.
The cross-API preservation predicate added in the parent commit read only
\`model.compat\`, so it missed the resolved \`compat.whenThinking\` view that
OpenCode-hosted reasoning models (Kimi/GLM/DeepSeek/Qwen/MiMo via \`opencode-go\`
/ \`opencode-zen\`) require:
- Base compat keeps \`requiresReasoningContentForToolCalls: false\` so
thinking-disabled requests dodge \`Extra inputs are not permitted\` (#1071).
- \`compat.whenThinking\` reactivates the flag for thinking-engaged requests
so they dodge \`thinking is enabled but reasoning_content is missing\` (#1484).
The old predicate read the base compat, so a cross-API switch into an
OpenCode reasoning model still demoted the prior thinking to text and the
next thinking-on request re-triggered #1484. The predicate now folds both
views (base + \`whenThinking\`) through a shared \`acceptsReasoningContentReplay\`
helper; if either view permits replay, the block is preserved.
Encoder companion fix in \`openai-completions.ts\`: when preserved cross-API
thinking reaches a request whose resolved compat can't surface
\`reasoning_content\` (most often OpenCode in thinking-disabled mode), fold
the reasoning into the visible content. Same shape as
\`requiresThinkingAsText\`, but gated on signature-stripped blocks only, so
same-model replays (which keep the streaming signature) are untouched. This
preserves the pre-#3434 text-demotion semantics for that branch without
silently dropping the reasoning chain.
Tests: \`packages/ai/test/issue-3434-repro.test.ts\` now also pins:
- OpenCode \`whenThinking\` compat surfaces \`reasoning_content\` from the
preserved block (guards against re-opening #1484 on cross-API switch).
- OpenCode base compat (thinking off) folds the preserved block into
content (guards against silently dropping the reasoning chain).
\`bun test packages/ai\`: 2296 pass / 1 fail; the lone failure
(\`proxy.test.ts > normalizes hyphenated provider ids to underscores\`)
is the same pre-existing \`Bun.env\` leak from a sibling test that fails on
\`main\`. \`bun check\` passes.
Addresses chatgpt-codex-connector review on #3439.
Threaded the resolved OpenAI completions compat into transformMessages so thinking-enabled request policy controls cross-API reasoning preservation.
Added coverage for Kimi/OpenCode replay where base compat is false but the thinking-enabled policy requires reasoning_content.
Fixes#3433
On a mid-session switch between an Anthropic-compatible 3p endpoint and an
OpenAI-compatible 3p endpoint on the same vendor (Z.AI Anthropic → Z.AI OpenAI,
Kimi Anthropic → Kimi OpenAI, custom \`models.yaml\` switches that cross API
types), \`transformMessages\` demoted every prior \`thinking\` block to plain
text on the cross-API path. The next request shipped the reasoning chain as
conversation text instead of structured \`reasoning_content\`, so the target
model lost the prior reasoning context and the source-side reasoning tokens
were paid for twice.
Two-half fix:
1. \`packages/ai/src/providers/transform-messages.ts\` — the cross-API thinking
path now preserves the prior reasoning as a native, signature-stripped
\`thinking\` block whenever the \`openai-completions\` target can re-emit it
on the wire (\`requiresReasoningContentForToolCalls\` or
\`thinkingFormat: "zai"\`). The source wire-format signature is always
stripped because it is bound to the source API (Anthropic crypto sig /
OpenAI Responses encrypted blob) and useless to the target.
2. \`packages/ai/src/providers/openai-completions.ts\` — the encoder now
surfaces preserved thinking blocks via \`reasoningContentField\` for
Z.AI-format hosts that don't strictly require the field, so a preserved
block with a stripped/opaque signature still ships as
\`reasoning_content\` on the wire.
Other cross-API targets (openai-responses encrypted blobs, google signed
thought parts, anthropic-from-foreign-source) keep the existing text-demotion
fallback so reasoning still survives as visible conversation context.
Tests:
- New \`packages/ai/test/issue-3434-repro.test.ts\` pins the Z.AI → Z.AI,
Anthropic 3p → DeepSeek, and Anthropic 3p → official-OpenAI-non-reasoning
wire shapes; all 4 tests fail on \`main\` and pass after the fix.
- \`bun test packages/ai\`: 2294 pass / 1 fail; the one failure
(\`proxy.test.ts > normalizes hyphenated provider ids to underscores\`) also
fails on \`main\` from a \`Bun.env.PI_PROXY_*\` leak in a sibling test and is
unrelated to this diff.
Fixes#3434
Threaded the caller's loaded skills through internal URL resolution so skill:// handlers do not depend on process-global skill state during tool execution.
Fixes#3436
Preserved cross-API thinking blocks for OpenAI-compatible reasoning targets so the completions encoder can write them to the configured reasoning_content field after signatures are stripped.
Added regression coverage for Anthropic-message history replayed through DeepSeek-compatible chat completions.
Fixes#3433
When the end-of-line cursor glyph lands past a wide trailing grapheme
(CJK comma ',', emoji, etc.), the inverse-video cursor extends past
'lineContentWidth' by the full overflow. Prior cursorInPadding logic
dropped exactly one space of right padding but always re-emitted '─'
before '╯', so 'paddingX=1' overflow=1 stayed 1 cell too wide and
'paddingX=2' overflow=2 stayed 2 cells too wide. In the user's
terminal the '╯' wrapped to a new row, looking like the editor box
collapsed onto two lines.
Replaced the boolean with the actual overflow cell count and recomputed
the right chrome budget (padding + optional '─' + corner) from
'paddingX + 1 - overflow', floored at 1 so the corner/vertical bar
survives. The else branch (non-last lines) follows the same budget for
symmetry.
Fixes#3431
- Added no-op setters to process.stdout properties in test geometry stubs.
- Prevented potential errors when code under test attempts to reassign stdout dimensions.
Remove `__getLegacyPiBundledRegistry` and `__synthesizeLegacyPiBundledSource`:
both reject outside compiled-binary mode, so no unit test can call them — they
were untested test seams. Real coverage runs through the pure
`__synthesizeLegacyPiBundledSourceWithRegistry` + `__getLegacyPiBundledRegistryGlobal`.
Also document why the synthesized virtual module must bridge back to the host
registry through `globalThis` (separate ES module, no shared closure scope,
live non-serializable exports) so the indirection isn't mistaken for cruft.
Serve bundled pi-* and TypeBox through an in-process virtual namespace
on Bun 1.3.14+ where `--compile` extras are unreachable via any
filesystem API (issue #3423).
Merge resolution:
- Reconciled `TYPEBOX_SHIM_PATH` with main's #3414 fall-through:
`__resolveTypeBoxShimPath(isCompiled, sourcePath, exists)` returns the
`omp-legacy-pi-bundled:` virtual specifier in compiled mode (no FS
probe) and the on-disk source path otherwise, dropping to null when
the shim file is missing so bare typebox imports fall through to native
resolution.
- Removed the now-dead `--compile` extras path: dropped
`LEGACY_COMPAT_BUILD_ENTRYPOINTS` usage from both build-binary.ts and
ci-release-build-binaries.ts and deleted scripts/binary-entrypoints.ts;
the bundler reaches every surface via legacy-pi-bundled-registry.ts.
- Deleted obsolete tests for the removed bunfs machinery
(legacy-pi-compat-entrypoints, legacy-pi-typebox-shim-validation) and
added regression coverage for __resolveTypeBoxShimPath.
- Dropped the binary-compiling smoke driver per maintainer request.
Verified: bun check clean; 77 extensibility tests pass; instrumented
compiled-binary run confirmed onLoad fires for all bundled specifiers.
- Add `usageProviderFor` helper to `AuthStorage` to distinguish between providers without usage endpoints and missing data.
- Implement account filtering in the usage CLI to exclude providers that do not support usage reporting unless explicitly requested.
- Improve CLI diagnostic messages when no usage data is retrieved by differentiating between missing credentials and unsupported providers.
Bun 1.3.14 stopped exposing `--compile` extras through every filesystem-style API: `fs.existsSync`, `Bun.file().exists()`, `Bun.resolveSync`, and `await import()` on `/$bunfs/...` or `file:///$bunfs/...` all fail; only `/$bunfs/root/<binary-name>` itself answers. The pre-existing legacy-pi rewrite emitted `file:///$bunfs/...` URLs that Bun then could not load, so every legacy extension that imported `@oh-my-pi/pi-*` or `@sinclair/typebox` failed on the `omp-darwin-arm64` release binary.
`legacy-pi-compat.ts` now keeps a JS-heap reference to every bundled pi-* surface in a lazy-loaded sibling `legacy-pi-bundled-registry.ts` and serves them through an `omp-legacy-pi-bundled:` virtual namespace whose `Bun.plugin().onLoad` synthesizes a re-export module — no bunfs path ever leaves the module in compiled mode. Dev / source-link / installed-package modes keep the historical `file://` rewrite (source files exist on disk). The matching `--compile` extras in `scripts/build-binary.ts` are gone; `BUNFS_PACKAGE_ROOT`, `bunfsPath`, `__computeBunfsPackageRoot`, and `__joinBunfsPath` are deleted as dead code. `scripts/smoke-3423.ts` compiles a tiny binary that loads a fixture extension end-to-end through the new path.
Fixes#3423
- Added id-based credential persistence and CAS-based invalidation to prevent cross-process race conditions.
- Categorized OAuth failure responses into transient and definitive types for more resilient refresh orchestration.
- Enabled stable credential lookups during refresh to ensure integrity during peer rotation or index shifts.
- Validated new persistence and error handling logic through comprehensive concurrency and failure-mode tests.
- Added listOAuthAccounts and getOAuthAccessAt to permit targeted credential retrieval without impacting sibling accounts.
- Implemented sameOAuthIdentity helper to ensure resolution consistency when manually selecting accounts.
- Added --list and --account CLI flags to token command for inspecting and choosing specific credentials.