Routed direct custom-message conversion through the collab steering transform so side requests and compaction see the same enveloped user turn as primary requests.
Extended the regression test to exercise convertToLlm without transformContext.
Converted user-attributed collab prompt frames to prioritized user messages only on the model-facing path, preserving guest details in persisted transcript frames.
Added regression coverage for the provider role, steering envelope, and retained guest attribution.
Fixes#7288
The move fence intentionally defers a flushSync landing in the rename window; full synchronous durability there is incompatible with orphan-avoidance for a rename-based, Windows-safe move. Document the caveat honestly instead of implying parity with the in-place rewrite path.
Refs #7270
The move fence bumped #diskEpoch, which no-oped any disk task already queued at the prior epoch (e.g. a header-only ensureOnDisk materializing rewrite), losing explicitly materialized ACP/draft sessions. Gate the append hot path on #sessionFileRelocating instead of a fresh epoch, so prior disk work still drains.
Fixes#7270
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.
- 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.