The extension shutdown context action installed by
ExtensionUiController.initializeHookRunner was an empty stub, so
ctx.shutdown() in interactive mode silently did nothing while extensions
fell back to process.exit(0), bypassing session flush and terminal
restore. Flip InteractiveModeContext.shutdownRequested so the main
loop's existing checkShutdownRequested() drives the graceful path.
Fixes#1020.
- Switched issue and PR search handlers to `gh api /search/issues` with `is:issue`/`is:pr` queries.
- Added REST search response models and mapped issue/code/commit/repo payloads to normalized results.
- Updated code, commit, and repo search parsing to read `{items}` envelopes and convert snake_case fields.
- Fixed merged-PR output state by deriving it from `pull_request.merged_at` in test fixtures.
Ollama Cloud has documented 503 windows under load that hard-failed the
turn. Adds bounded backoff (2s/5s/10s, ~17s worst case) for /api/chat
and /api/tags, wired through a shared abortableSleep so the retry
respects abort signals. Local Ollama uses a different code path and is
untouched.
Closes#1025
Slash skill invocations bypassed the Enter / Ctrl+Enter contract that
every other slash command honors. Now Enter steers, Ctrl+Enter queues
a follow-up, sharing one #invokeSkillCommand helper between the editor
submit handler and handleFollowUp. Compaction short-circuit ordering
preserved.
Closes#1033
Two omp processes refreshing the same Anthropic OAuth token would
clobber each other — the loser persisted a stale refresh token and
disabled the credential on the next call. Re-reads the row by id,
compares the refresh token to the in-memory snapshot, and reloads +
retries on rotation. Recursion is bounded since the rotated row's
expiry is in the future.
Closes#1034
# Conflicts:
# packages/ai/CHANGELOG.md
PR #998's branch was rebased on stale main and its CHANGELOG conflict
resolution dropped the immutable [14.9.8] and parts of [14.9.7]
released sections. Restore them and keep only the new credential_disabled
bullet PR #998 actually contributes under [Unreleased]/Added.
Converts AuthStorage from a single-subscriber to multi-subscriber model
so the SDK and extensions can both observe credential changes without
clobbering each other, and emits a new credential_disabled event when
storage permanently rejects a credential. Lets extensions prompt
re-auth instead of silently failing the next call.
Reconciliation + mismatch rejection covered for both the SDK and
runSubprocess paths.
Closes#998
Mirrors the existing InternalUrlRouter pre-pass from search/ast-edit/
ast-grep so the find tool resolves session-scoped scheme URLs (local,
skill, agent, memory, etc.) to their backing filesystem paths before
fast-grep runs. Without this, the model passing literal scheme URLs as
paths to find hit ENOENT.
Glob patterns mixed with scheme URLs and URLs without a backing file
raise explicit ToolError instead of silently failing.
Closes#1026
Adds an rpc-ui mode that shuttles tool-UI context over the existing RPC
channel, sharing one RpcExtensionUIContext between the tool store and
the extension runner so extension_ui_response routing stays correct.
Forces PI_NO_PTY=1 in this mode to avoid PTY bash crashes.
Closes#994
Ollama Cloud rejects the thinking field in assistant history messages
(live streaming is fine), breaking multi-turn conversations after the
first reply. Strips it in convertMessages on the cloud path only;
local Ollama uses openai-responses and is unaffected.
Closes#1000
Rewrites the final top-level expression statement to a runtime hook so
its value surfaces without an explicit return/display, while preserving
top-level binding lifetime across cells. Promise-valued and thenable
finals are awaited exactly once; import-only cells stay silent.
Closes#1024
Marks AgentToolResult.isError (top-level and details.isError) on the JS
eval programmatic tool bridge so MCP/tool error results are visible to
JS callers and status events.
Closes#1023
Adds `pi.on("credential_disabled", handler)` so extensions can react to
soft-disabled credentials (e.g. OAuth invalid_grant) without regex-matching
`agent_end` errorMessages.
`AuthStorage.onCredentialDisabled(listener)` returns an unsubscribe function;
multiple listeners fire for every event with per-listener exception isolation
and FIFO buffer-and-replay (cap 32) when none are attached. The constructor
option from #991 stays as sugar for an immediate permanent subscription.
`createAgentSession()` subscribes the per-session extension runner to
`modelRegistry.authStorage` immediately after resolution and unsubscribes on
dispose / startup failure. Events are forwarded via
`ExtensionRunner.emitCredentialDisabled(event)`, which buffers (cap 32,
drop-oldest) until `runner.initialize(...)` runs in the mode controller so
extension handlers see real UI/runtime context, not the constructor no-op
defaults.
Supersedes #997. Builds on #991.
Co-Authored-By: omp <noreply@oh-my-pi.dev>
rpc-ui sets hasUI=true which causes the bash tool to take the
runInteractiveBashPty path when pty=true. RpcExtensionUIContext.custom()
is a stub returning undefined, so result.cancelled dereferences undefined
and throws. PTY bash requires a live TUI overlay; rpc-ui only provides
dialog-style UI. Set PI_NO_PTY for rpc-ui so the usePty guard in
bash.ts stays false.
Adds a new `rpc-ui` mode that extends the existing headless RPC mode with
interactive tool support (ask tool, extension UI dialogs, etc.).
In plain `rpc` mode the session has `hasUI=false` and no UI context is
wired, so interactive tools are disabled. `rpc-ui` mode sets `hasUI=true`
and wires a single shared `RpcExtensionUIContext` instance into both the
tool context store and the extension runner. Both consumers share the same
`pendingExtensionRequests` map and output closure, so `extension_ui_response`
messages received on stdin are routed to the correct waiting promise
regardless of which code path (tool or extension) created the request.
Changes:
- `args.ts`: add `rpc-ui` to the `Mode` union and the parse guard
- `launch.ts`: expose `rpc-ui` in the OCLIF flag definition and help text
- `main.ts`: propagate `rpc-ui` through all RPC-mode guard conditions and
pass `setToolUIContext` to `runRpcMode` when the mode is `rpc-ui`
- `rpc-mode.ts`: accept optional `setToolUIContext` callback; create one
shared `RpcExtensionUIContext` instance and pass it to both the tool
context store and the extension runner
Ollama Cloud intermittently returns HTTP 503 (capacity exhausted).
Add fetchChatWithRetry in providers/ollama.ts and fetchWithRetry in
provider-models/ollama.ts — both retry up to 3 times with 2s/5s/10s
backoffs before surfacing the error.
Makes `/skill:<name> [args]` work identically under both submission
keybindings, mirroring how free text is already routed during streaming:
- `/skill:foo` + Enter, streaming -> steer queue (interrupt)
- `/skill:foo` + Ctrl+Enter, streaming -> followUp queue
- `/skill:foo` + Enter, idle -> idle prompt
- `/skill:foo` + Ctrl+Enter, idle -> idle prompt (was: literal text)
A single private helper `#invokeSkillCommand(text, streamingBehavior)`
on `InputController` handles the dispatch; the Enter submit handler
calls it with "steer", and `handleFollowUp` calls it with "followUp"
after the compaction short-circuit so a skill typed during compaction
rides the same `queueCompactionMessage` queue as free text.
Behavior deltas vs upstream/main:
- Enter on `/skill:foo` during streaming now steers (was: queued as
followUp). Users who relied on the followUp default can press
Ctrl+Enter -- the same key they already use for free-text follow-ups.
- Ctrl+Enter on `/skill:foo` is new capability; previously the
literal string `/skill:foo ...` was sent as plain followUp text and
the skill was never invoked.
Op: extend
Anthropic rotates refresh tokens on every use, so two omp processes
sharing a single agent.db on the same Anthropic OAuth credential
deterministically race when both notice the access token has expired:
the winner refreshes successfully and persists the new tokens in place
via updateAuthCredential; the loser's in-flight refresh hits the server
with the previous (now consumed) refresh token and gets the standard
'invalid_grant: Refresh token not found or invalid' 400.
#tryOAuthCredential treated that response as a definitive failure and
unconditionally soft-deleted the row at the credential's index, which
in this race actually deletes the row that the peer just rotated. The
user observes 'anthropic OAuth disappeared, must /login again' even
though a valid, freshly-rotated token is sitting in the row's data
column behind the disabled_cause flag.
Before flagging the row disabled, re-read it from disk and compare the
persisted refresh token against the one we just tried. If they differ,
the peer's rotation succeeded between our snapshot and our request, so
reload the in-memory cache and retry getApiKey against the fresh
credential instead of disabling. Genuine 'invalid_grant' on a credential
that no peer touched still soft-deletes, preserving the legitimate
disable path.
Add a regression test that seeds a row, simulates the peer's in-place
rotation, mocks getOAuthApiKey to mirror Anthropic's invalid_grant
contract, and asserts the row survives the race with its rotated
refresh token. A companion negative test confirms that an isolated
refresh failure (no peer rotation) still disables.
Ollama cloud rejects requests (HTTP 400) when assistant history messages
contain the `thinking` field. The field is valid as output from the
model (streamed thinking blocks), but the cloud API does not accept it
as an input in conversation history.
Root-cause trace: a kimi-k2.6 response containing both `thinking` and
`tool_calls` was stored in history. The following request echoed it
back, triggering a 400. Requests containing only `tool_calls` (no
`thinking`) succeeded, isolating the `thinking` field as the cause.
Strip the field from converted assistant messages when the provider is
`ollama-cloud`. Local Ollama instances (provider `ollama`) are unaffected
and continue to receive `thinking` in history for models that support it.
The asset-emission assertion was the false positive flagged in
tab-supervisor.ts: the new pattern intentionally does not bundle the
worker as an asset of the supervisor — it is added as a separate
`--compile` entrypoint in build-binary.ts, and runtime is covered by
`omp --smoke-test` (sync worker).
Replaces it with two cheap static checks that defend the two halves
that actually make the worker survive `bun build --compile`:
1. tab-supervisor.ts branches on `isCompiledBinary()` and uses the
exact `--root`-relative literal at the `new Worker(...)` site
that Bun's `--compile` analyzer can discover, while keeping the
`new URL("./tab-worker-entry.ts", import.meta.url)` branch for
dev/source spawns.
2. scripts/build-binary.ts lists the same worker as an explicit
additional `--compile` entrypoint so Bun emits it into bunfs.
[Unreleased] → Added entries for both the new "Approve and compact
context" ExitPlanMode selector choice and the underlying typed
`CompactionCancelledError` + `CompactionOutcome` plumbing.
Op: extend
- Extend the existing selector-list assertion to include the new
"Approve and compact context" entry in the expected five-choice array.
- Add three new cases that mock `handleCompactCommand` at the
CompactionOutcome boundary (the contract `#approvePlan` consumes):
- "ok" → asserts compactSpy called with the planning-specific
custom instruction rendered from `plan-mode-compact-instructions.md`
(matched by content: "Preparing to execute the approved plan" +
the final plan file path); plan-approved synthetic prompt dispatched
(discriminated by the `{ synthetic: true }` option flag);
`markPlanReferenceSent` called once.
- "cancelled" → no synthetic dispatch; `showWarning` surfaces the
deferred-dispatch message; `setPlanReferencePath` IS called with the
final path so the session knows the plan was approved; but
`markPlanReferenceSent` is NOT called so the next operator turn
re-injects the reference via #buildPlanReferenceMessage. This last
assertion is the load-bearing regression guard for the asymmetric
cancel/fail contract.
- "failed" → plan-approved synthetic prompt IS dispatched
(best-effort); markPlanReferenceSent fires.
Per AGENTS.md the tests use `vi.spyOn` + `vi.restoreAllMocks()` in
the existing afterEach; no `mock.module()`.
Op: extend
A fifth ExitPlanMode approval choice — sits between the existing
"Approve and execute" (purge session) and "Approve and keep context"
(full transcript). Runs `handleCompactCommand` against the plan-mode
transcript with a planning-specific custom instruction rendered from
`plan-mode-compact-instructions.md`, then dispatches the plan-approved
synthetic prompt so it lands as the first entry in the freshly-
summarized transcript — giving execution a fresh cache anchor with
the rationale carried over.
Cancel/fail contract:
- ok → bookkeeping runs, plan-approved synthetic prompt dispatched.
- cancelled → bookkeeping runs (tools restored, plan reference path
recorded), warning surfaced, dispatch skipped.
`markPlanReferenceSent` is intentionally deferred past
the cancel guard so `AgentSession.#buildPlanReferenceMessage`
re-injects the plan on the operator's next prompt() call.
If we marked it sent on cancel, the executor's first turn
would have no plan context.
- failed → bookkeeping runs, error already surfaced by executeCompaction,
dispatch proceeds best-effort. Approval intent stands.
Cancel vs. fail is discriminated via `instanceof CompactionCancelledError`
at the session/compaction error boundary (introduced in the previous
commit), so any abort source — operator Esc, extension hook, programmatic
abort — classifies uniformly without input-modality or message-string
coupling.
Op: extend
Introduce `CompactionCancelledError` and `CompactionOutcome` ("ok" |
"cancelled" | "failed") so callers can discriminate user-driven aborts
from generic failures via `instanceof`, instead of inspecting error
messages or `AbortError`-name strings.
`AgentSession.compact()`'s two abort-rejection sites now throw the
typed sentinel; the model-call wrapper normalizes AbortError-shaped
rejections to the sentinel only when the compaction's abort signal
is actually set, preserving every other exception unchanged so real
compaction bugs are not silently relabeled as cancellations.
`CommandController.executeCompaction` and `handleCompactCommand`
return `Promise<CompactionOutcome>`; the catch classifies via
`instanceof CompactionCancelledError`. Existing callers (`/compact`,
loop runner, auto-compact) ignore the return value — non-breaking.
Op: extend
- Replaced `with { type: "file" }` worker imports with `isCompiledBinary()` hybrid: literal string for `--compile` static analysis, `new URL(import.meta.url)` for dev portability.
- Added worker entrypoints as explicit `--compile` args in `build-binary.ts` so Bun emits them into bunfs.
- Added `smokeTestSyncWorker` and `omp --smoke-test` to catch silent worker-load failures in compiled binaries (fixes#1011, #1027).
- Added `isCompiledBinary()` utility to `@oh-my-pi/pi-utils` detecting bunfs path markers.
- Updated hashline section parsing to accept headers with any leading `@` characters, normalizing them to the path before validation.
- Updated the hashline grammar and fallback errors to use canonical `@@ PATH` section headers.
- Added coverage for mixed `@@` and `@@@` headers across multiple file sections in hashline parsing tests.
- Added untracked worktree baseline capture via `untrackedPatch` and synthetic tree diffing.
- Added fallback-aware backend ordering by collecting host candidates and retrying alternates on unavailable PAL.
- Hardened overlay mount lifecycle by removing stale overlays and deleting upper/work dirs after unmount.
- Refined ZFS clone deletion to validate ownership and remove dataset+origin only when checks pass.
- Updated ProjFS integration to use extended-info callbacks and symlink metadata reads.
- Updated rcopy path handling to absolutize paths, and added `writeTree` plus combined shell timeout checks.
- Added a workspace dependency catalog in `Cargo.toml` for internal and external crates.
- Replaced explicit dependency specs and pinned versions with workspace references in crate manifests.
- Removed inline dependency overrides such as feature flags, renames, and package metadata.
- Consolidated platform-specific target deps (`libc`, `winreg`, `windows-sys`, `parking_lot`) to workspace entries.
- Added unified isolation primitives (BackendKind, ProbeResult, IsoError) and resolve fallback selection logic.
- Added Diff, FileChange, and ChangeKind with default_diff choosing git mode or filesystem walk based on repository state.
- Added APFS/btrfs/zfs/reflink/overlayfs/projfs/rcopy/block-clone backends with platform-aware start, stop, and probe.
- Mapped backend operations to canonicalized paths, recursive clone helpers, rollback cleanup, and unavailable error mapping.
- Added unified native exports isoBackend/isoProbe/isoResolve/isoStart/isoStop/isoDiff and removed projfsOverlay APIs.
- Replaced task isolation resolver flow with ensureIsolation/cleanupIsolation and migrated mode handling to auto plus legacy-mode aliases.