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.
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
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
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
- Added no-op setters to process.stdout properties in test geometry stubs.
- Prevented potential errors when code under test attempts to reassign stdout dimensions.
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.
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
- Implemented SGR mouse event routing for dashboard interaction, including tab selection and pane scrolling.
- Added mouse-driven list manipulation in the extension viewer with selection highlighting, click toggling, and wheel navigation.
- Enabled fullscreen alternate-screen behavior and host terminal mouse tracking for the dashboard overlay.
- Integrated hit-testing and row selection logic into the extension list to support unified mouse and keyboard inputs.
- Extracted the legacy `--compile` entrypoint list to `scripts/binary-entrypoints.ts` and consumed it from both the release CI script and the local dev `build-binary.ts`, so the two cannot drift apart.
- `scripts/ci-release-build-binaries.ts` no longer ships release binaries without the typebox shim, legacy pi shims, and `@oh-my-pi/{agent,natives,tui,utils}` package barrels in bunfs. Commit dc5c93462f removed worker entrypoints and false-comment-claimed the legacy entrypoints were "still" listed, so every published `omp-<platform>-<arch>` since shipped without them and the resolver emitted bunfs URLs to missing files.
- Validated TYPEBOX_SHIM_PATH at module init via __resolveTypeBoxShimPath, mirroring __validateLegacyPiPackageRootOverrides (#2168). When the shim is absent the rewriter leaves bare typebox / @sinclair/typebox imports alone so Bun falls through to native node_modules resolution.
- Pinned both halves of the contract with tests: release+dev scripts must source from the shared constant, every shim path computed in legacy-pi-compat.ts must appear in it, and __resolveTypeBoxShimPath drops missing candidates.
Fixes#3414
Add a createAgentSession-based subagent regression for issue #3406. The test
runs a taskDepth=1 session against a loopback llama.cpp-style model so
provider.appendOnlyContext auto-enables through the SDK path used by task
subagents.
A real context extension rewrites the prior assistant turn on the second
subagent request. The captured provider contexts assert the first request's user
message object is reused in the second request, proving the append-only log kept
the stable prefix instead of clearing and re-rendering it for subagents.
Fixes#3406
Trailing empty assistant 'stop' arriving after a successful 'yield'
revived the already-yielded subagent. AgentSession.agent_end maintenance
compared #assistantEndedWithSuccessfulYield(msg) against the trailing
empty-stop message — not the yield-bearing one — so the empty-stop
recovery path appended a retry reminder and scheduled agent.continue().
Track a sticky #yieldTerminationPending flag set when the yield tool
finishes without error and cleared on the next #promptWithMessage. The
agent_end routing extends the existing successful-yield branch: when the
flag is set, or the current message ended with yield, short-circuit
empty-stop / unexpected-stop / compaction continuations for the rest of
the run, so a successful yield is terminal regardless of trailing stops.
Fixes#3389
GitHub Copilot's /models response advertises supports.vision = true for
Claude/GPT chat models on every host, but only the canonical personal
endpoint (https://api.githubcopilot.com) actually accepts image inputs;
the business (api.business.githubcopilot.com) and enterprise
(copilot-api.{domain}) hosts respond '400 vision is not supported'.
snapcompact then injected rasterized transcript frames after compaction
and permanently broke every business-Copilot session.
- Catalog discovery (githubCopilotModelManagerOptions.mapModel) now
forces input=['text'] whenever the resolved baseUrl is not the
canonical personal-Copilot host, so the upstream's vision flag is
honoured only where it actually works.
- mergeDynamicModel honours the dynamic input value (instead of
OR-upgrading with the bundled reference) when the merged baseUrl
differs from the bundled one, so a bundled spec pinned to the
personal host can no longer taint a business-resolved merge.
- snapcompact-inline's canSendImages helper short-circuits the
rasterizer for any github-copilot model whose baseUrl is non-personal,
catching stale cached specs that still advertise vision.
- Helper isPersonalGitHubCopilotBaseUrl exported from
pi-catalog/wire/github-copilot so catalog and coding-agent share one
canonical check.
Regression coverage in github-copilot-model-limits.test.ts (vision
endpoint policy + full merge) and snapcompact-inline.test.ts (#3387
business/enterprise case).
Fixes#3387
With collapseCompactedHistory the live display fell into the LLM compaction
branch, which skips the firstKeptEntryId..compaction turns whenever an OpenAI
remote-compaction replacementHistory payload is present. That payload feeds the
provider only and is not rendered, so a remotely-compacted session showed just
the summary plus post-compaction rows, hiding recent turns that were visible
before. Emit the kept SessionEntry rows in transcript mode regardless. Adds a
regression.
The append fast-path opened the session file for sentinel comparison and
recompute without a guard, so a file unlinked/rotated between #refresh's
statSync and the sentinel read threw out of the 250ms poll timer (no catch),
risking a TUI crash. Treat sentinel read/recompute failures as non-appendable
and fall back to the guarded full reload. Adds a fake-timer regression.
The "skips snapcompact entirely" case asserted .rejects.toThrow() on
session.compact(), but after the maxFrames<1 skip the manual /compact path
falls through to the LLM summarizer whose outcome is provider/network
dependent (resolves when a summary lands, rejects only without
credentials/network). In a sandbox it resolves and blows the 5s default
timeout. Tolerate either outcome and pin only the deterministic skip
contract (snapcompact.compact not invoked + the kept-history notice), with
a generous explicit timeout. Addresses chatgpt-codex P2 review on #3249.
renderUsageReports (command-controller) carried the #3268 dedup contract
with no regression test; the PR's added CLI test asserts the opposite
(per-limit CLI rendering shows the note twice). Export renderUsageReports
and add a real regression through it: two accounts sharing one window group
render a provider-wide UsageReport.note once and an identical per-limit note
once. Verified failing on the pre-fix flatMap form (0 and 2 occurrences) and
passing on head (1 and 1).