A fenced append followed by a Ctrl+C flushSync in the post-rename, pre-repoint window rewrote the full body to the old path, recreating the orphan. Sync rewrites now defer while the session file is relocating.
Fixes#7270
Rebuilt advisor runtimes with the rediscovered context files so advisor turns stop evaluating against stale AGENTS.md instructions after /reload-plugins.
Fixes#7258
Threaded the session's disabledExtensions into context-file rediscovery so a concurrently-created session's global settings cannot toggle another session's context entries.
Fixes#7258
Rediscovered context files from the active session cwd whenever plugin prompt sources refresh, while preserving explicitly preloaded SDK context.
Covered edited and disabled context files in the current system prompt.
Fixes#7258
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.
A parked revive holds the same AgentRef while constructing a new session.
If the Hub tombstoned that ref before revive completed, the reviver could
still attach its session and set the terminal ref back to idle because
identity alone remained unchanged.
- Made aborted registry refs terminal: reject revival claims, late session
attachment, and status transitions out of aborted.
- Made lifecycle revival accept only an untouched detached parked ref or the
exact running session already claimed by createAgentSession; dispose and
reject every terminal/stale result.
- Added a delayed-revival regression test and claim-before-kill CAS checks.
Fixes#7250
Preserving aborted refs on dispose exposed a latent invariant break: the
executor's hard-abort path (finalizeSubagentLifecycle) set status `aborted`
and disposed the session without detaching it. With the ref now retained, it
kept a dangling pointer to the disposed session, and ensureLive returns any
non-null ref.session before its revivability check — so hub focus / transcript
chat could route into a dead session.
- finalizeSubagentLifecycle: detach the session before disposing on the
terminal hard-abort path, upholding the AgentRef invariant (session === null
when aborted).
- release(tombstone): detach before dispose too (capture the live session
first), same invariant.
- unregisterUnlessParked: preserve `aborted` refs only when already detached;
an aborted ref still holding a live session is a bug and is unregistered
rather than kept reachable.
- Regression test now asserts ensureLive rejects a tombstoned id as terminal.
Fixes#7250
A live-session hub kill did not stick: release(tombstone) awaited the
wrapped session dispose first, and createAgentSession's unregisterUnlessParked
removed any non-parked ref, so the subsequent detach/setStatus no-oped and the
ref was gone — leaving the reopen resurrection for idle/running agents.
- release(tombstone) now marks the ref `aborted` BEFORE disposing, so the
dispose guard preserves it; the session is detached afterward.
- unregisterUnlessParked now also spares terminal `aborted` refs (matching
the documented "hard-killed, terminal" retention and finalizeSubagentLifecycle).
- Regression test now uses a session stub that mirrors the real wrapped
dispose (unregister unless parked/aborted), so it fails if the tombstone is
set after dispose.
Fixes#7250
The Agent Hub kill path called AgentLifecycleManager.release(), which
disposes the session and then unregisters the ref while leaving the
on-disk <id>.jsonl intact. On the next hub open, registerPersistedSubagents
rescans the transcript tree and its `if (!registry.get(id))` guard cannot
distinguish an explicit kill from a normally-parked agent, so it re-adopts
the killed id as a fresh `parked` row.
Add a `tombstone` option to release() that mirrors finalizeSubagentLifecycle's
genuine-kill path: dispose and detach the session but keep the ref registered
as terminal `aborted` instead of removing it. The kept-registered id makes the
rescan guard skip it, and the transcript stays on disk (still reachable via
history://<id>, per #5261). The hub kill button now passes tombstone: true.
Fixes#7250
When a skill name exists both in a default discovery path (e.g.
~/.claude/skills/<name>) and in an explicitly configured
skills.customDirectories entry, the default-path copy loaded first and the
custom-directory skill was silently dropped on the name collision. As a
result skill://<name> resolved to the default path and reported "File not
found" when the user's skill lived only in the custom directory.
Custom-directory skills now override same-named default-path skills (the
user's explicit configuration is the higher-priority source); duplicates
within customDirectories keep first-wins.
These four paths bypassed DirResolver's XDG-aware rootSubdir/agentSubdir
hooks, resolving directly against getConfigRootDir()/getAgentDir() and
ignoring XDG state/data layout. Add XDG-aware path helpers in dirs.ts
and route all four through them:
- secret-placeholder.key → $XDG_STATE_HOME/omp/ (state, agent flattened)
- marketplaces.json → $XDG_DATA_HOME/omp/ (data)
- run/daemons/<hash>/ → $XDG_STATE_HOME/omp/run/ (state)
- run/provider-inflight/ → $XDG_STATE_HOME/omp/run/ (state)
omp config init-xdg migrates secret-placeholder.key and marketplaces.json
from their legacy locations; run/ is ephemeral and rebuilds on restart.
- Added `ensureSharedBrowser` and shared browser acquisition to manage project-shared broker-owned Chromium instances.
- Implemented concurrent duplicate daemon start prevention and single-flight `pendingOpens` deduplication.
- Updated browser handle disposal to disconnect from shared daemons rather than closing them.
- Updated browser documentation and launch specifications to support shared and local headless runs.
Settings.init opens agent.db/stats.db before setInteractiveHost ran, so
interactive hosts received the 1000ms headless busy timeout on those
databases (issue #2421 class). Declare the flag before settings load and
add [Unreleased] changelog entries per repo conventions.
waitForManagedBashJob raced job completion against a bare
Bun.sleep(thresholdMs), which cannot be cancelled. When completion,
abort, or steering won the race, the losing Bun.sleep timer stayed
scheduled and ref'd, keeping Bun's event loop alive until the threshold
expired — delaying SDK/headless shutdown and accumulating timers under
fast command rates.
Replace the Bun.sleep with a Promise.withResolvers settled by a
cancellable setTimeout, and route every outcome (including the former
no-signal early return) through one try/finally that clears the timer
and removes the abort/steer listeners.
Add a child-process regression test that runs the real auto-background
path for a fast command against a 30s threshold and asserts the process
exits promptly instead of being held for the full threshold.
Fixes#7235
- Reused the agent's explicit or inherited cache identity for ephemeral side turns.
- Forwarded the same effective key through manual and automatic native compaction.
- Added regression coverage for all three secondary request paths.
Fixes#7218
Headless hosts (print/RPC/ACP/eval/SDK) run the agent loop on the same
thread as bun:sqlite, so a lock-contention busy-wait of the interactive
5s timeout freezes the protocol loop for seconds at a time with no
liveness signal. Use a 1s busy_timeout for session-critical databases
(agent.db, history.db, stats.db) when the host is not interactive, and
let the existing asynchronous open/retry paths recover from contention.
The live AskDialog trusted question.question while its render helpers
(replaceTabs, renderQuestionTitle, questionTabLabel) assume a string. A
question reaching AskDialogComponent without a string question field threw
an uncaught TypeError that escaped the TUI render loop and killed the
session. The transcript renderer already normalizes the same malformed
data via normalizeRenderQuestions; the live path did not.
Normalize the questions array at dialog entry (new normalizeDialogQuestions),
coercing question/id/label to strings and options to a well-formed array,
matching the transcript path.
Fixes#7211
Track entry into tool.execute separately from tool event emission. Never-started skips retain SyntheticToolResultDetails with executed:false; in-flight aborts now use distinct interrupted metadata with execution:started so consumers do not assume no partial work occurred.
Keep both interrupt states neutral in the TUI and cover the agent metadata boundary plus rendering behavior.
Fixes#7199
A tool call aborted mid-batch to service queued steering/peer input emits a synthetic placeholder result with isError:true so the model retries it. The TUI keyed all error styling (red ✘, red frame/text) off that flag, so a normal steering skip rendered identically to a real tool failure.
Mark the skip placeholder with the existing SyntheticToolResultDetails discriminator (source: "interrupt_skipped", executed:false) and render benign skips through the neutral generic card (info glyph, dim text, neutral background), bypassing any bespoke error frame. Genuine failures keep their error styling.
Fixes#7199
- Reused the live Codex provider session for WebSocket-first V2 compaction.
- Fell back to SSE V2 on WebSocket transport failure before the existing V1 fallback.
- Propagated the configured WebSocket preference through manual, automatic, and advisor compaction paths.
- Added transport reuse and fallback regression coverage.
Fixes#7198
The Codex SSE `type:"error"` branch read only top-level `code`/`message`,
so backend rejections emitted under a nested `error` or `response.error`
object collapsed to `Codex error (): Unknown error`, hiding the cause
(e.g. a regional/model-snapshot rejection). `response.failed` similarly
dropped the error code.
Add a shared `extractCodexSseError` that reads top-level, nested `error`,
and `response.error` envelopes, and wire both error paths through it so
the backend code and message survive in `SearchProviderError`. The
existing `web_search_call` requirement is untouched.
Fixes#7200
The option only exists in OMP's own config format, so the OMP-owned discovery
providers are the only ones that parse it. Say so in the schema description, the
MCPServerConfigBase doc, and the changelog, and name the config paths where
setting it actually takes effect, so nobody expects a server imported from
another tool's config to honor it.
OMP plugins ship their own .mcp.json, and that provider whitelists fields into
the canonical MCPServer shape the same way the other loaders do, so a plugin
bundling an integer-only server could set requestIdFormat and still have it
dropped before it reached a transport.
Extract the value parsing into parseRequestIdFormat() next to parseBoolean() in
discovery/helpers.ts and use it from all three OMP-owned loaders (native,
standalone mcp.json, omp-plugins), replacing the two hand-rolled copies.
Vendor providers (claude, claude-plugins, cursor, vscode, gemini, opencode,
windsurf) are deliberately untouched: they translate another tool's own server
list, where an OMP-specific key has no meaning.
Discovery collapses differently-named MCP entries that point at the same
command/args/env/cwd (or url/headers) into one connection, keeping whichever
loaded first and dropping the rest as aliases. requestIdFormat changes the bytes
sent on the wire, same as auth/oauth which the comparator already covers, so a
higher-priority alias lacking the field could shadow a lower-priority entry that
set it, silently falling back to string ids for a server that needs numbers.
Add requestIdFormat to isSameMCPConnection, normalized against its own default so
an explicit "string" is not treated as a different connection than leaving it
unset. timeout stays excluded, since it is a client-side knob that never touches
the wire.
The option was only present on the transport-facing `MCPServerConfig`, so a
value written in `.omp/mcp.json` or a standalone `.mcp.json` never reached the
transports: discovery normalizes config into the canonical `MCPServer` shape and
`convertToLegacyConfig()` rebuilds the transport config from it, and neither step
knew about the field. Setting `"number"` in the documented config path silently
kept the snowflake-string default, which is the hang the option exists to avoid.
Wire it through the same four places `timeout` already uses: the canonical
`MCPServer` shape, the two OMP-native loaders (with validate-and-warn on an
unrecognized value), and the legacy conversion. Foreign-format providers are
untouched, since the key is OMP-specific.
Also point the `MCPServerConfigBase` doc comment at `RequestIdAllocator` rather
than a helper name that never existed.
Apple's `xcrun mcpbridge` decodes JSON-RPC `id` as an integer only. OMP
mints collision-resistant snowflake strings, so the bridge logs
`mcpbridge.DecodeError Code=1`, never replies, and every request hangs
until it times out (#7053). JSON-RPC 2.0 permits String and Number ids
equally, so both shapes are legal and the string default stays.
Add `requestIdFormat: "string" | "number"` to the shared server config
and honor it in all three transports through one allocator. The string
default is unchanged, so this is inert unless a server opts in.
Verified against Xcode 26.3's bridge: with `"number"`, `initialize`
succeeds and `tools/list` returns all 21 tools; with the default, the
same request times out.
- Update hashline block resolution formatting to correctly incorporate anchor lines within operation labels.
- Fix and update test assertions and mock contexts across coding agent tests.
- Replaced the reactive weekly-only auto-redeem predicate with a pool-wide
planner: an expiry-salvage sweep piggybacks on the 5-minute usage
heartbeat and spends any account's reset that would otherwise expire
within codexResets.salvageHorizonHours, and the blocked-turn path scans
all stored accounts with eligibility built from the exact exhausted
5h/weekly windows (openai/codex#28525), unblocking at the latest reset
among them.
- Made the live 429's parsed unblock timestamp authoritative for the
active account (pre-block snapshots survive cache invalidation via
in-flight adoption and last-good fallback), synthesizing the candidate
when no usable report exists, and overlaying live credit counts from
the dedicated credits route since a stale /wham/usage zero is never
corrected upstream.
- Treated nothing_to_reset, credit_list_failed, and thrown consumes as
non-terminal: the episode key is released and deferred 30 minutes
instead of burying a banked credit; redeemResetCredit now spends the
soonest-expiring credit.
- Added planner unit fixtures plus integration regressions driving the
real triggers end to end, with an injectable per-session coordinator
seam and a sweep settlement handle.
- resolveOwnerScopedSessionKey's getOwners in the Python and JS executors
only read live sessions, so a subagent reset issued while the shared
kernel was still starting resolved to the base key, awaited the
parent's startup, and shut its brand-new kernel down.
- Python and JS starting sessions are now owner-bearing records like
Ruby/Julia's: owners attach synchronously before startup resolves,
getOwners and per-owner disposal consult them, and the final
sessions.set is identity-guarded so a disposed starting record cannot
resurrect its kernel.
- Regression: deferred PythonKernel.start proves a concurrent subagent
reset forks immediately and never reaps the parent's starting kernel
(fails with the previous getOwners).
- Subagents inherit the parent's eval session id, so a child's
reset: true destroyed the co-owned kernel and every sibling's
interpreter state mid-session.
- resolveOwnerScopedSessionKey now routes a reset from a non-exclusive
owner onto a deterministic per-owner fork key: the requester gets a
fresh private kernel, co-owners keep the shared one, and the fork
stays sticky for that owner until its teardown reaps it.
- Applied across Python, JavaScript, Ruby, and Julia executors; JS
contexts gained an owner registry plus disposeVmContextsByOwner,
wired into EvalRunner.disposeKernels and SDK session teardown.
- Covered by pure key-resolution contracts and an end-to-end JS test:
co-owner reset forks, shared state survives, fork is sticky, and
per-owner dispose reaps only the fork.