feat: add OMP (Oh My Pi) as a new session backend - #353
Conversation
The rebase hand-repair dropped the closing brace of .welcome-btn-pi:hover and .btn-toolbar.btn-run.mode-pi:hover before the inserted OMP rules. The browser CSS parser drops every rule after an unclosed block, so the deployed UI rendered as unstyled text bars (only ~456 of ~2583 rules applied). Verified clean via esbuild --minify (no css-syntax-error) and rebuilt dist.
Adds a Hide CLI buttons group to App Settings → Header & Panels (device-scoped, client-only like the other per-device header toggles). Each checked mode is hidden from the welcome screen, the Run dropdown, and the phone overview run picker, layered on top of CLI availability gating. - settings-ui: hiddenModes load/save (openAppSettings/saveAppSettings), isModeHidden() helper, welcome-button + header-settings re-apply - session-ui: _refreshRunModeAvailability() also skips hidden modes - mobile-overview: _buildMobileOverviewRunMenu() also skips hidden modes - tests: run-mode-ui + mobile-overview cover the hide list
The id-fix edit for the OpenCode chip accidentally replaced the Pi chip with a second OpenCode label, leaving both appSettingsHideOpenCode and appSettingsHideOpencode in the DOM (two OpenCode chips, no Pi). Rename the original chip to the JS-generated id and restore Pi.
…esolver - omp-cli-resolver.ts already uses createCliExecutableResolver; add dedicated test/omp-cli-resolver.test.ts mirroring pi's (version-probe accept/reject, negative-cache backoff, VITEST hermeticity gate) - dependency-registry omp entry now requires OMP_VERSION_REGEX match like pi, so codeman doctor and the run-mode resolver agree on what counts as installed - system-routes /api/omp/status surfaces version
Two prose lists in skills/codeman/ named some but not all external CLI modes after the omp-mode rebase, which is exactly the drift test/agent-skill-mode-lists.test.ts exists to catch: SKILL.md's no-hook-signals list was missing omp, and endpoints.md's version-probe sentence named pi/grok/omp as a bare 3-mode run with no matching class.
resumeHistorySession() never sent mode when recreating a session from a history/session-manager row, so the server default silently opened a plain Claude session for every non-claude row -- reproduced live: OMP rows spawned Claude sessions on click. Thread the row's mode through every call site (welcome list, session manager, mobile overview) and only send the Claude-specific resumeSessionId for claude rows. Codeman has no live PTY-reattach outside server boot, and it's moot for OMP anyway (exiting it kills the pane's only process), so route the non-claude relaunch through each CLI's own continue-most-recent flag instead of a context-free fresh start. OMP never got one: buildOmpCommand only implemented --model/--resume despite omp --help documenting -c/--continue. Added continueSession to OmpConfig end-to-end (type, schema, builder) mirroring the existing opencode/pi/grok/deepseek fields, and wired resumeHistorySession to use it. Verified live: told a real omp session a secret, exited it, closed the tab without killing tmux, relaunched with --continue in the same directory, and had it recall the secret.
…here respawnPane() -- the path used when a session's pane died (crash, idle respawn, or the user's own /exit) but the Codeman session object is still tracked -- never had ompConfig wired through at all, in either its options destructure or its inner buildSpawnCommand() call. This is a gap in the original OMP patch, distinct from the resumeHistorySession fix (which only covers a session that has been fully closed and shows up as a history row): reselecting a tab whose CLI process just exited goes through this path instead, and always launched a bare, contextless `omp` no matter what. Beyond the wiring, respawning a dead pane is semantically different from creating a brand-new session: the conversation is still "this session" to the user, so _buildRespawnPaneOptions() now defaults ompConfig to continueSession:true unless the session already carries an explicit resumeSessionId (which still wins in buildOmpCommand). Verified live: told a session a secret, exited OMP so the pane died (session and tmux both left alone), forced the exact dead-pane-respawn path, and the new process replied with the secret -- confirming `omp --continue` fired instead of a blank omp.
…ed-only sessions Every non-claude "Resume" click creates a brand-new Codeman session (there is no id to reattach to), but the old row was never cleaned up -- click resume on the same conversation a few times and the session list fills up with duplicate rows sharing one name. resumeHistorySession now retires the row it resumed from after the new one starts. That retirement needs DELETE to actually work on a row that was never live in the first place (the normal case for anything showing up in "Resume Conversation"): findSessionOrFail only checks the in-memory live-session map, so DELETE 404s on a persisted-only entry today. Give the route a fallback: when the id isn't live, look it up in persisted state instead and demote/remove it there (respecting the existing pinned-session protection). Verified live against a real persisted-only row via the API, and added route-test coverage for both the success and still-truly-unknown-id cases (which needed a demoteOrRemoveSession mock the route harness didn't have). Also includes an unrelated pre-existing prettier drift fix picked up by npm run format (omp-cli-resolver.ts, antigravity/opencode import wrapping in session-routes.ts).
Claude conversations survive "Kill Tmux & Claude" because Codeman reads them back independently from ~/.claude/projects, not from its own session bookkeeping. omp conversations had no equivalent: kill the Codeman session and the conversation vanished from Past Sessions entirely, even though omp itself never forgot it on disk. Adds omp-transcript.ts, a scanner over omp's own ~/.omp/agent/sessions/<mangled-cwd>/<uuid>.jsonl files (the same shape as Claude Code's own transcript scanner, but simpler -- these files are small enough to read whole instead of doing head/tail windows). Each file's own "session" header line carries the real cwd and session id directly, so unlike Claude's mangled-directory-name decoding this never has to guess. Wired into gatherUnifiedInputs() as a second history source alongside the Claude scan, and HistoryInput/ mergeUnifiedSessions() now carry an optional `mode` so a non-claude history-only row still gets a real mode badge. Also fixes the ambiguity behind the "continue picks the wrong conversation" report from this session's testing: omp mints its OWN session uuid, unrelated to Codeman's, so a live/persisted row and its own history-scan row would otherwise show up as two separate entries for the same conversation the moment the id gets resolved. Reuses the existing claudeSessionId alias field (mergeUnifiedSessions' fold-into- owner mechanism) to point at the resolved omp id, threading it through every place `_claudeSessionId` gets (re)computed -- the constructor, _resolvedOmpRespawnConfig, and a new _maybeCaptureOmpSessionId() that opportunistically resolves it the first time a brand-new omp session (one that has never gone through a respawn) goes idle. Also closes a THIRD instance of the "ompConfig never got wired in here" gap this session kept finding: restoreMuxSessions() in server.ts restores every sibling CLI's config from persisted state on boot except omp's, so a boot-recovered omp session always lost its resolved resume id and fell back to guessing again. Verified live end-to-end: told a session a secret, killed it fully (Kill Tmux equivalent, killMux=true -- the Codeman session AND its tmux pane both gone), and the conversation still showed up in the unified list as a history-sourced row with the real first prompt as its title and an omp mode badge, keyed by omp's own session id. Known remaining gap, not fixed here: the claudeSessionId alias doesn't yet resolve reliably on every boot-recovery path for a session that was never respawned while alive (e.g. a plain re-attach to a pane that was never dead) -- worth a follow-up, but doesn't affect the two things that matter most: the conversation surviving a kill, and continuation correctness once an id has been resolved (which happens on the very next respawn either way).
Two bugs compounded to break continuation pinning on every real OMP case (only /tmp-based manual testing happened to work by coincidence): 1. startInteractive() had a second, unconditional claudeSessionId assignment after the mux branch that clobbered its correctly resolved value back to the session's own id on every mux path. 2. mangleOmpWorkingDir() assumed omp mirrors Claude Code's directory naming (home prefix kept), but omp actually strips $HOME first. findLatestOmpSessionId() was silently returning null for every case under ~/codeman-cases/, so resumeSessionId never resolved for any real case dir - only /tmp paths (outside $HOME) worked, which is every dir this feature was previously tested against. Verified live: killed and relaunched the omp-verify server process mid-session (plain reattach, pane stayed alive) and confirmed claudeSessionId now resolves to the real omp transcript uuid instead of the Codeman session's own id. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…validation Follow-up from a full-branch review pass (Opus) of the omp-mode integration: - Add pinning tests for resolveOmpConfigForCreate() (session-routes.ts), exported to make it testable: the exact "resume this OMP row from history" pipeline that mangleOmpWorkingDir's earlier bug lived in had zero coverage despite being the resolver module's whole reason to exist. - Log a warning when findLatestOmpSessionId() finds nothing on disk and continuation silently degrades to omp's own ambiguous --continue, in both call sites (session create and respawn pinning) - previously silent, making the degradation invisible to anyone debugging it. - Require an absolute cwd before trusting a session file's working directory in omp-transcript.ts's parser, so a corrupted/malformed session file can't point a downstream resume at a relative or empty path. - Document (don't speculatively fix) an unverified symlinked-$HOME edge case in mangleOmpWorkingDir(): the review's suggested realpath() fix assumes omp itself resolves symlinks before mangling, which is unconfirmed - guessing wrong there would trade one silent mismatch for a different one. - Incidental: fixed unrelated pre-existing prettier drift in session-routes.ts (antigravity/opencode dynamic import line-wrapping) that was blocking the pre-commit formatting gate on this file. Confirmed as a non-issue: the model-name regex allowing "/" is intentional (provider/model ids like "crof/glm-5.2" were used successfully in live testing). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ials
OMP had full routing at the Docker layer (default pane command, schema) but
was never actually installed in docker/agent.Dockerfile, and had no
credential-isolation entry in docker-hosts.ts's CRED_STORES - a Docker-mode
OMP session would have failed with "omp: command not found", and even with
the binary present would have had no config/auth seeded, despite the README
already claiming OMP has "seamless auth, isolated credentials" in Docker.
- docker/agent.Dockerfile: install omp via its own installer (standalone
binary, same shape as grok/antigravity - not on npm). Verified against a
real --no-cache build: the installer actually targets ~/.local/bin, not
~/.omp/bin as the resolver's OMP_SEARCH_DIRS ordering would suggest -
confirmed omp/18.0.8 installs and runs correctly inside the image.
- src/docker-hosts.ts: add a .omp/agent CRED_STORES entry. Unlike every
sibling CLI in this family, sessions/ is SHARED (RW), not seeded: Codeman
reads ~/.omp/agent/sessions/**/*.jsonl host-side for history recovery and
--resume pinning (omp-transcript.ts, omp-session-resolver.ts), the same
reason codex's sessions/ is shared rather than seeded. Seeding it instead
would silently break the kill-survival feature for Docker cases. Only the
small config files (config.yml/mcp.json/models.yml/settings.yml) are
seeded; the SQLite caches and terminal-sessions/ stay container-local.
- test/docker-hosts.test.ts: pin the new CRED_STORES entry's behavior.
Found in passing (NOT fixed here, unrelated and pre-existing on master): the
agent image's DeepSeek (dsh) plugin-install step currently fails on a fresh
build ("pnpm not found on PATH"), confirmed via git diff against
origin/master that this line is untouched by this branch. Worth a separate
issue/PR.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
OMP was the one external CLI mode with no dedicated user-guide doc, unlike opencode/pi/grok/deepseek which each have one. Covers install, auth (omp owns its own entirely - no Codeman-side login flow or bypass switch), what Codeman wires up (OmpConfig), the exact-id pinning mechanism and the directory-mangling bug behind it, kill-survival via transcript scanning, terminal behavior, Docker/remote-SSH cases, and known gaps (no idle hook, mid-turn kill data loss, unverified symlinked-$HOME behavior). Cross-referenced from README.md's Multi-CLI doc list and docs/docker-cases.md's credential-seeding summary (which now also documents OMP's sessions/-is-shared exception to the seed-everything pattern the other CLIs use). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…onversation Found live 2026-08-27 by Tim: clicking Run OMP to start a brand-new session in a case directory with prior omp history launched --resume <old-id> instead of a clean `omp` invocation. Root cause: Session._resolvedOmpRespawnConfig() resolves-and-pins the newest on-disk omp conversation as a side effect on this._ompConfig. That is correct when reattaching to an ALREADY-TRACKED mux session (a dead-pane respawn, or a boot-recovery reattach - the constructor sets _muxSession from persisted state before startInteractive() ever runs there), but it ran unconditionally. startInteractive() computes `respawnPaneOptions: this._buildRespawnPaneOptions()` eagerly in the same object literal that builds `createSessionOptions.ompConfig: this._ompConfig`, so for a genuinely brand-new session (no muxSession in its create config, _muxSession still null) the resolve-and-pin side effect ran and poisoned this._ompConfig before that field was even read. Fix: gate the resolve-and-pin logic on `this._muxSession` already being set. A fresh session has no muxSession yet and now passes through untouched; a real reattach (muxSession present since construction) keeps resolving and pinning exactly as before. Verified live in production against the exact reported scenario (a fresh omp session in a case dir with 8+ hours of prior omp history) - confirmed both via the API (ompConfig stays empty, claudeSessionId equals the session's own id) and visually in the GUI. Regression test constructs a real Session + TmuxManager to exercise the actual private-method interaction directly, since no existing test called startInteractive() at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Pushed one more fix after opening this PR: 13a19f7. A fresh "Run OMP" click was silently resuming an old conversation instead of starting clean, in any working directory with prior OMP history — found live while testing, root-caused, fixed, and verified in production. Full details in the commit message; test suite still green (6319 passed). |
|
Thanks for this, and for the write-up. The core is in good shape: it follows the external-CLI pattern faithfully, the resolver reuses I read the diff against omp upstream too (the repo is 1. The newest-mtime pin breaks with two omp tabs in one case
This is the rule CLAUDE.md already carries for dsh: pair by the transcript's own header plus a launch window, never by newest mtime, because newest-mtime handed a fresh worker its predecessor's answer in the same case dir. You already parse that header in Related: the pin is a side effect of building respawn options, which is what bit you in 13a19f7. Resolving where the respawn is actually issued would remove that whole class of bug rather than guarding it. 2. The env-allowlist and clamp claims do not match omp's docs
None of this matters in single-user mode. It matters for a non-granted owner in multi-user mode, which is why pi and dsh both have explicit clamp branches. Please make the decision explicit instead of "nothing to gate": either add the config-root and broker keys to the clamp for omp, or write down why they are acceptable. Worth stating in the doc as well that omp's documented default 3. Three changes in one PRBesides the omp backend there are two riders:
I would rather take these as two or three PRs. If they stay together, the changeset has to describe all of it: right now it covers only the backend and lists the siblings as "Claude Code, OpenCode, Codex, Gemini, and Antigravity" with Pi, Grok and DeepSeek missing. That text lands verbatim in the release notes. 4. Smaller, all concrete
Things I would keep as they are: the version probe demanding Also: thanks for filing the |
Summary
Adds
omp(Oh My Pi, https://github.com/can1357/omp) as an eighth CLI backend,alongside Claude Code, OpenCode, Codex, Gemini, Antigravity, Pi, Grok and DeepSeek
Harness. It follows the existing external-CLI pattern exactly (own PTY, own tmux
session, own tab identity,
isExternalCliMode()), and gets full support acrossevery surface that pattern touches: schemas, spawn-command building, Docker cases,
remote-SSH cases, multi-user clamping, the UI run-mode picker, and skill docs.
Two things make this more than routine plumbing:
--continuealone is ambiguous the moment anyother omp conversation has touched the same working directory more recently.
omp-session-resolver.tsresolves and pins the real session id once, so everylater respawn uses
--resume <id>instead of re-guessing.omp-transcript.tsscans~/.omp/agent/sessions/**/*.jsonldirectly, so a conversation's history is recoverable in Past Sessions even when
both the Codeman session record and the underlying tmux pane are gone —
verified live against a full OS reboot, not just a killed pane.
Notable bugs found and fixed along the way
claudeSessionId(the field also used to alias anOMP session's own id) failed to resolve on a plain-reattach path. Root cause
was two compounding issues: an unconditional second assignment that clobbered
the correctly-resolved value, and a directory-mangling helper that assumed omp
mirrors Claude Code's naming convention (keeps the
$HOMEprefix) when ompactually strips
$HOMEfirst. This second bug meant continuation pinning wassilently a no-op for every real case directory under
$HOME— only/tmp-basedmanual testing had ever exercised the code path, and it happened to produce the
right answer there by coincidence.
docker/agent.Dockerfile, and had nocredential-isolation entry, despite the docs already claiming full Docker
support. Both fixed and verified against a real
--no-cacheimage build.OMP is also the one CLI in this family where
sessions/needs to be ashared mount rather than seeded, since Codeman reads it host-side for the
resume-pinning and history-recovery mechanisms above.
Testing
npm test)lint,typecheck,check:frontend-syntax,check:public-assets,check:lockfileall cleanTUI-exit +
tmux kill-session+ restart, and a full server/OS reboot —all recover correctly, including first-turn content
omp/18.0.8installs and runs inside a freshly builtagent image
Known limitations (documented in
docs/omp-integration.md)other external CLI
/exit) loses the conversation forreal; omp hasn't flushed its session file yet at that point. This isn't
something Codeman can compensate for from outside the process.
$HOMEis unverified