- Added compaction.asyncEnabled (Async Compaction, default on): when
context enters the pre-threshold band [threshold - lead, threshold)
with lead = clamp(threshold * 0.125, 8192, 32000), maintenance
speculatively summarizes in the background off a branch snapshot
(first configured LLM-backed method: remote, handoff, or soft) using
a side session id isolated from the live turn. Crossing the threshold
splices the armed result in instantly instead of blocking on a
summarization round-trip. Armed results are invalidated by branch
changes, reset boundaries, model switches that strand provider-native
replay payloads, and context growth past keepRecentTokens (which
re-speculates); extensions registering session_before_compact keep
exact blocking semantics (speculation disabled).
- Reworked handoff to commit in place: /handoff and the auto handoff
method now write the generated document as a regular compaction entry
on the current session (summary = document + <files> tag, cut from
prepareCompaction) instead of starting a new session. SessionHandoff
shrank to a document generator; session_before_switch/session_switch
no longer fire with reason "handoff"; mid-turn maintenance no longer
suppresses the handoff preference; overflow recovery can apply an
armed handoff result.
- Extracted the shared auto-compaction commit tail
(#commitAutoCompactionResult / #commitCompactionEntry) used by the
blocking production path, the armed speculative apply, manual
compaction, and manual handoff.
- Status line pulses the auto-compact icon while a speculation runs and
holds it in accent once a result is armed.
- Exported remotePreserveReusable from pi-agent-core/compaction for
apply-time validation of speculative remote results.
Replayed idle transcripts in bounded message and time chunks, painting cleared scrollback between macrotasks so terminal input remains responsive during restore and tree navigation. Kept streaming rebuilds atomic and migrated every rebuild caller to await completion.
Fixes#8133
The #7904 fix stopped masking provider errors as "Handoff cancelled", but
an empty or whitespace-only generation still fell through: whitespace-only
text passed the `!handoffText` guard and produced a bogus handoff, while
empty text returned undefined which the interactive /handoff caller mapped
to "Handoff cancelled" with no detail and no log entry.
Treat empty/whitespace-only output as a real failure: a user-initiated
handoff throws "Handoff generation produced no content" (surfaced as
"Handoff failed: ...") and logs it; auto-handoff keeps returning undefined
so maintenance falls back to context-full compaction. Also log genuine
handoff failures in the command controller so they persist for debugging.
Fixes#7993
The handoff catch in session-handoff.ts and the /handoff handler in
command-controller.ts mapped any error named AbortError to "Handoff
cancelled" regardless of whether the handoff signal was actually
aborted. Providers throw name-AbortError errors on non-user conditions
(stalls, idle timeouts, nested resolution failures), so a genuine
generation failure surfaced as a user cancellation and hid the cause.
Only report "Handoff cancelled" when handoffSignal.aborted is set;
re-throw the real error otherwise. The controller now trusts the
normalized "Handoff cancelled" message and drops its own AbortError
check so re-thrown provider failures render as "Handoff failed: ...".
Fixes#7903
The TUI's CommandController special-cased backend.id === "off" for
/memory stats|diagnose, but the ACP/RPC slash-command handler in
builtin-registry.ts still fell back to the generic "not available for
the off backend" template — non-TUI users with memory.backend=off saw
the self-contradictory wording this PR was meant to remove.
Extract the shared fallback into memoryStatsUnavailableMessage()
(memory-backend/messages.ts) and use it from both CommandController
and the ACP builtin-registry handler, so the two surfaces can't drift
again.
Addresses review comment:
https://github.com/can1357/oh-my-pi/pull/7251#discussion_r3695383090
/memory stats and /memory diagnose fall back to a generic
'Memory <action> is not available for the <backend.id> backend.'
message whenever the active backend's stats/diagnose hook is
undefined. For every real backend (hindsight, mnemopi, local) this
reads fine, but the off backend isn't a backend a user picked among
several stats-capable options - it's the no-op state memory falls
back to by default - so the same template renders as 'Memory stats
is not available for the off backend.', which reads as an odd,
almost self-contradictory warning.
Special-case backend.id === "off" with wording that matches the
phrasing offBackend.status() already uses elsewhere ('Memory backend
is off.'), and add a unit test covering both the off-backend wording
and the unchanged generic fallback for a real backend (local) that
simply has no stats hook.
The regression test only asserts on a mocked showWarning call and
never renders Markdown, so it doesn't need a real theme instance;
drop the global dark-theme setup/teardown to avoid leaving the
process-wide theme singleton mutated for later suites in the same
Bun process.
/usage, /session, /advisor status, /jobs, /changelog, /context, and
/memory view mounted their finalized panel immediately via ctx.present()
instead of ctx.presentCommandOutput(), the streaming-deferral path added in
#5427 for /tools and /mcp. When invoked mid-turn, the panel landed above a
still-growing live block and the append-only scrollback contract recommitted
it lower down, so it appeared twice in native scrollback.
Route all six large command panels through presentCommandOutput() so they
defer until agent_end, matching /tools and /mcp.
Fixes#6767
/usage and omp usage now report Synthetic (synthetic.new) quota state
via GET /v2/quotas (free, does not consume quota): the rolling 5-hour
request limit with per-tick regeneration rate, and the weekly credit
quota in USD. Applies to API-key credentials for the synthetic
provider; every payload section is parsed defensively since only
subscription is documented.
The TUI /usage matrix sorted each quota window's account columns
independently by used fraction, so the positional `account N` labels
denoted different credentials on each row. An account exhausted on one
window but light on another (e.g. a Kimi Code account's 5h limit)
rendered its exhausted bar under a sibling that still had quota, making
the section `% free` and capacity numbers look wrong.
Order account columns once per provider (worst-first) and hold that
order stable across every window row.
Fixes#6067
Preserved settled user and assistant component instances during compaction-only transcript rebuilds so warmed Markdown and layout caches remain valid.
Covered both manual and automatic compaction paths with focused regression tests.
Fixes#6033
Brings the per-advisor toggle, status-line glyphs, quota display, and the
failing-advisor stall/abort fix (f4c8143) onto main's rewritten advisor
runtime. Conflict reconciliation kept main's architecture (fingerprint
prefix reconciliation, host-level onTurnError recovery + fallback chains,
terminal-failure classification) and ported the branch semantics onto it:
- #failing latch: waitForCatchup resolves immediately while an advisor is
mid-failure; parked waiters wake the moment a turn fails, before any
async hook or retry sleep.
- Turn-end render containment: a formatter bug restores the cursor/prefix/
dedup snapshot and never propagates into the primary's turn-end callback
(per-advisor try/catch boundary in AgentSession).
- Quota pause: when host recovery declines a usage-limit failure, the
runtime latches quotaExhausted, requeues the batch, and notifies —
cleared only by an explicit reset.
- Hard halt after a permanent rejection or three backlog-drop cycles.
- #recoverAdvisorTurn also marks usage limits for structural errors thrown
before any assistant turn is recorded.
Removed the 24-column cap from account cells so wide terminals can show full disambiguating labels.
Kept usage bars independently capped and added regression coverage for same-email organization accounts.
Fixes#5701
The /usage show "in use by this session:" marker took only the bare
email from OAuthAccountIdentity, so two same-email Anthropic credentials
in different orgs were indistinguishable. Route the label through a
shared formatActiveAccountLabel that suffixes the active org, matching
the account list and login-success surfaces.
Fixes#5691
- Added the `xd://` virtual device protocol (`internal-urls/xd-protocol.ts`, `tools/xdev.ts`): tools declaring `loadMode: "discoverable"` are unmounted from the request tools array and driven via `read xd://` (list/docs+schema) and `write xd://<tool>` (execute), gated by the `tools.xdev` setting (default on) and inlined into the system prompt.
- Merged the `irc`, `job`, and `launch` tools into a single `hub` tool (`tools/hub/`, `async/job-manager.ts`): messaging keeps `send`/`inbox`/`list`, job control maps to `wait`/`cancel`/`jobs`, process supervision keeps `start`/`logs`/`stop`/`restart`/`describe` with `ps`, and the unified `wait` races background jobs against peer messages; SDK `IrcTool`/`JobTool`/`LaunchTool` are replaced by `HubTool`.
- Removed the hidden `resolve` tool in favor of the `xd://resolve`/`xd://reject`/`xd://propose` resolution devices, auto-including `write` whenever a deferrable tool or plan mode is present.
- Removed the BM25 tool-discovery system: the `search_tool_bm25` tool, the `tool-discovery` module, the `tools.discoveryMode`/`mcp.discoveryMode`/`mcp.discoveryDefaultServers`/`tools.essentialOverride` settings, per-tool MCP selection, and the `mcp_tool_selection` message type.
- Unified tool presentation on `ToolLoadMode` (`essential`|`discoverable`), replacing the custom-tool `xdev?: boolean` opt-out; custom, extension, MCP, RPC host, image-generation, and TTS tools now default to `discoverable`, and added a `satisfies` predicate to `SoftToolRequirement`.
- Removed the standalone `ssh` command tool and `ssh/ssh-executor` (the `ssh://` read/write/search protocol stays), and made `--tools` address hidden built-ins.
- Updated collab-web to render `xd://` dispatches and `hub` op families, dropped the `search_tool_bm25`/`ssh`/`report-finding` renderers, refreshed tool docs and prompts, and migrated the affected tests and changelogs.
Vibe worker roster lived only in a process-local Map, so a resumed parent
session started with an empty registry and vibe_send failed with
"Unknown vibe session". Persist a versioned, parent-scoped lifecycle
journal (spawn/turn/tombstone events), rehydrate validated idle workers
through the persisted-subagent reviver on resume, and gate the flow with
generation/CAS protection so stale finalizers cannot clobber a
replacement worker. Killed transcripts stay readable but non-revivable;
mode-exit commits tombstones atomically with the mode change and rolls
back cleanly on storage failure.
Ported from @mastertyko's fork branch fix/vibe-session-persistence.
Fixes#5303
- Introduced conditional scrollback clearing during UI renders when transcript compaction is enabled.
- Updated `CommandController` and `EventController` to respect the `display.collapseCompacted` setting.
- Configured `SelectorController` to trigger a chat rebuild and UI reset when the compaction setting changes.
- Updated `InteractiveMode` to dynamically toggle between collapsed and full inline history based on user settings.
One Anthropic account email can hold multiple organizations (a Team seat
plus a personal Max plan), each with its own org-scoped OAuth token and
independent 5h/7d limit pools. Credentials were deduped by bare email, so
logging in with the second subscription silently replaced the first, and
usage reports from the two pools merged into one row with mixed numbers.
- capture organization uuid/name at login (token exchange response, with
a claude_cli/bootstrap fallback); token refreshes never rewrite it
- key anthropic credential identity as email + org; a legacy email-keyed
row is claimed in place by the first org-scoped login with the same
email, and org-less credentials never clobber org-scoped rows
- partition usage-report dedupe and the per-credential usage cache by
org so the two subscriptions' limit pools stay distinct for rotation
- show the organization in omp usage (redaction-safe) and name the
stored account/org in the login success message
- Treated missing or invalid changelog markers as first install and persisted the current version without replaying historical notes.
- Shared bounded changelog rendering between startup and recent changelog views, with a 64 KiB startup cap and full-history hint on truncation.
- Added marker, truncation, recent/full rendering, and PTY startup regression coverage.
Fixes#5135
- Add 'enabled' field to AdvisorConfig (default true, persisted in WATCHDOG.yml)
- Filter disabled advisors in #resolveAdvisorRuntimeDescriptors, keep in status map
- Classify quota/rate-limit errors separately from transient server errors
- Auto-pause advisor on quota exhaustion, auto-resume after 5min cooldown
- Add AdvisorRuntimeStatus enum (running/paused/no_model/quota_exhausted/error)
- Render per-advisor status dots in status line: ●○✕ with truncation to 4+ '+'
- Include disabled/no-model advisors in PerAdvisorStat with status field
- Add notifyQuotaExhausted host callback distinct from notifyFailure
- Tests: config round-trip for enabled field, quota classification, overloaded path
Stopped new-session and session-switch UI paths from detaching active loader/render components without running their disposal hooks.
Added container and loader coverage for disposing children before destructive transcript/status replacement.
Fixes#4686