Files
Codeman/docs/omp-integration.md
Codeman maintainer 02b0e27898 fix: merge-time follow-ups for #400, #401, #362 and #388
Each item is from the pre-merge review of the PR it names, applied on master
rather than by pushing to a contributor branch.

#400 (response viewer, shenlvkang-collab)
- The brief view opened at `scrollTop = 0`, right when it was a single card
  holding the last row. Now that it renders the whole turn, the top is the
  turn's first narration line and the answer can be screens below it, while
  loadFullContext already scrolls to the bottom of the same turn. A multi-row
  turn now opens at its newest text; a single card still opens at the top.

#401 (loopback links as web tabs, shenlvkang-collab)
- Drop `*.localhost` from the auto-route set. Every other member is an address
  literal that can only mean this box; a `*.localhost` DNS name is not one, and
  a resolver with a search domain retries `evil.localhost` as
  `evil.localhost.<search domain>`. The link source is agent-written terminal
  output, so that set is the whole confinement on a tap that makes Codeman
  fetch a URL server-side and persist it. The page-side test stays broader
  (`isOnBoxHostname`), where a false positive only declines to proxy.
- A link to the origin root navigated nothing: the path was flattened to '',
  which openWebview reads as "no deep link", leaving an open frame where it was.
- `this.webviews` being set does not mean it is loaded. initWebviews() assigns a
  truthy empty map and only then awaits the list, so a tap during page load
  found nothing to reuse and POSTed a duplicate record. Join the in-flight
  refresh instead.
- One dashboard per dev server rather than per host spelling, which is what the
  method's own comment already promised.
- Toast on the auto-create: it writes webviews.json, broadcasts over SSE and
  adds a Run-dropdown row on every signed-in device, with a new tab as its only
  previous signal.

#362 (remote omp continuation, timkjr)
- Accept the allowlisted `mode === 'omp'` arm as-is; a blanket registry render
  would hand deepseek a locally-resolved --profile and bypass claude's own
  overlay. A registry-declared switch is the follow-up if a third mode needs it.
- Revert the whole-file Prettier reformat of docs/remote-sessions.md (docs/ is
  hand-formatted and outside `npm run format`), keeping only the two new
  sections.
- Correct three stale passages: architecture-invariants' `exec claude
  --dangerously-skip-permissions`, the `exec <cli>` paragraph (claude and omp
  now have their own arms, and the claude pane's PID is the login shell), and
  omp-integration's `-c 'omp'`. RemoteCommandMode gains deepseek and omp.
- Add the missing `_maybeCaptureOmpSessionId` remote-guard test; the sibling
  guard in `_pinOmpRespawnId` had one and this path runs earlier, on the first
  idle turn.

#388 (keyCode 229 recovery, aakhter)
- Gate notifyCanonicalData on shouldSuppressTerminalQueryResponse and
  isTerminalFocusOrMouseReport. onData also carries the DA/DSR/CPR/OSC replies
  xterm answers during Ink redraws and its SGR mouse and focus reports; any of
  those landing between the keydown and the candidate's resolution was read as
  "xterm spoke for this keystroke", standing the recovery down and leaving the
  character dropped, worst on a busy agent pane. Reached through
  window.CodemanTerminalInput: the predicates live in a module IIFE that closes
  long before this call site, so bare references would throw into the
  surrounding try/catch and stop the notify from ever running.

Every fix has a test that fails without it (verified by reverting each).
Full gate green on the combined tree: 358 files, 6849 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 05:15:41 +02:00

10 KiB
Raw Permalink Blame History

OMP (Oh My Pi) sessions

Codeman can drive OMP (omp, Oh My Pi) as a session backend, alongside Claude Code, OpenCode, Codex, Gemini, Antigravity, Pi, Grok and DeepSeek Harness. omp is the ninth CLI backend (tenth SessionMode, counting shell): its own PTY, its own tmux session, its own tab identity. It is not a location overlay like Docker or remote-SSH cases, and it is not a web tab.

Install

curl -fsSL https://omp.sh/install | sh

The installer places the binary in ~/.local/bin (verified against a real --no-cache Docker build — see docker/agent.Dockerfile; an earlier guess of ~/.omp/bin was wrong). Codeman resolves the binary via the server PATH and then the usual install locations (~/.local/bin first, then ~/.omp/bin, /usr/local/bin, ~/.bun/bin, ~/.npm-global/bin, ~/bin).

omp is a short name, so like pi and grok the resolver does not trust a PATH hit on its own: it runs omp --version and requires omp/<semver>-shaped output (e.g. omp/18.0.8) before accepting a candidate. Check what it resolved:

curl -s localhost:3000/api/omp/status | jq
# { "available": true, "path": "/home/you/.local/bin", "version": "18.0.8" }

Authenticate

OMP owns its own auth and provider configuration entirely in ~/.omp — there is no Codeman-side login flow, API key field, or bypass switch to configure. Run omp directly once outside Codeman to complete whatever onboarding the CLI itself asks for; every session started through Codeman afterward inherits that config.

What Codeman wires up

OmpConfig (per session, persisted in state.json, round-trips through respawn):

Field Flag Notes
model --model <v> Regex-validated ([a-zA-Z0-9._-/]+); provider/model forms like crof/glm-5.2 pass
continueSession --continue omp's own "most recent conversation in this directory" heuristic
resumeSessionId --resume <id> Ids only, id-regexed; wins over --continue when both are present

Every value is regex-validated and dropped (not escaped) if it fails, because the result is interpolated into the pane's spawn command.

omp reads its own model routing and hooks from ~/.omp, so no trust or permission flags are needed — unlike every sibling CLI in this family, there is no bypass-permissions equivalent to wire up, so buildOmpCommand() only ever passes --model/--resume/--continue. ⚠️ That does NOT mean omp is unrestricted: its documented default tools.approvalMode is yolo, so an omp pane auto-approves exec with no flag from Codeman — the CLI's own config, not Codeman, is what would need to change that.

Env overrides: the OMP_* prefix is allowlisted, and per omp's own docs/environment-variables.md it is not the narrow surface it looks like. omp reads roughly 40 provider keys from the environment (ANTHROPIC_API_KEY, OPENAI_API_KEY, XAI_API_KEY, HF_TOKEN, ...) — pi's 34-key problem in the same shape — which is why none of those get a dedicated allowlist entry; a session authenticates from ~/.omp config or the server process's own env instead, like pi. omp's own documented knobs are mostly PI_*, not OMP_* (PI_CONFIG_DIR, PI_CODING_AGENT_DIR, PI_CODING_AGENT_SESSION_DIR, PI_SUBPROCESS_CMD, PI_SHELL_PREFIX, OMP_PROFILE/PI_PROFILE), and PI_* is already allowlisted globally because pi mode needs it — so an omp session today already accepts all of those. The first three also move the tree omp-session-resolver.ts and omp-transcript.ts hardcode (resolveOmpHome() assumes ~/.omp unconditionally), so pinning and history quietly stop working under a redirected config root; this is a known gap, not fixed here.

The OMP_ prefix itself brings in OMP_AUTH_BROKER_URL / OMP_AUTH_BROKER_TOKEN, where omp resolves credentials from — the same shape DEEPSEEK_BASE_URL is dropped for in clampEnvOverridesForOwner() (session-routes.ts), so both are clamped there for a non-granted owner in multi-user mode. None of this matters in single-user mode.

Exact-id pinning: why --resume, not just --continue

--continue alone is ambiguous the moment any other omp conversation has touched the same working directory more recently — it just picks the newest session file on disk, silently. That happens routinely: a closed-then-resumed Codeman row plus a still-running duplicate, two Codeman sessions pointed at the same case, or a plain reattach after a server restart.

src/utils/omp-session-resolver.ts resolves and pins the exact conversation id once (findLatestOmpSessionId() reads ~/.omp/agent/sessions/<mangled-workingDir>/, the newest .jsonl file's embedded uuid), then every later respawn reuses that pinned id via --resume instead of re-guessing with --continue.

⚠️ The directory mangling is NOT a straight / → - replace. Unlike Claude Code's ~/.claude/projects/* convention (which keeps the full path, e.g. -home-user-codeman-cases-foo), omp strips the $HOME prefix FIRST and only then dash-replaces (/home/user/codeman-cases/foo → -codeman-cases-foo; a path outside $HOME, like /tmp/..., is dash-replaced as-is with no stripping). Getting this wrong doesn't error — findLatestOmpSessionId() just silently returns null for every case under $HOME (virtually all real Codeman cases), so pinning quietly degrades to omp's own ambiguous --continue. This was found and fixed 2026-08-27 after months of testing had only ever exercised /tmp-based working directories, where the bug's wrong output happened to coincidentally match the right one.

Surviving a full session kill

src/omp-transcript.ts scans ~/.omp/agent/sessions/**/*.jsonl directly — a second, independent history source alongside Codeman's own state. This means an OMP conversation's history (working directory, first/last prompt, size) is recoverable in the Past Sessions list even when both the Codeman session record and the underlying tmux pane are gone — verified live against a full OS reboot, not just a "Kill Tmux" button click.

Terminal behavior

OMP renders inside tmux like every external CLI (narrow scrollback strip — alt-screen toggles only, not the full Claude/Codex/Gemini strip). It stays out of the alt-screen-strip list and lands on the 'buffer' local-echo policy via the _updateLocalEchoState fallthrough, same as grok and pi.

Docker cases

The agent image installs omp in its own Dockerfile step (not npm; omp's installer targets $HOME/.local/bin with no --dir override, the same shape as grok's installer). Rebuild with the mandatory --no-cache:

node scripts/build-agent-image.mjs --no-cache

⚠️ --resume pinning does not currently reach an in-container omp process. Docker panes are built from defaultDockerCommandForMode, which never sees ompConfig — appendResumeFlag()'s case 'omp' keys off the top-level resumeSessionId field, which nothing populates for omp today. Host-side history recovery still works (the shared sessions/ mount below), but a respawned in-container omp pane falls back to its own ambiguous --continue, not a pinned id. Flagged in upstream review, not yet fixed.

Credentials are mostly seeded, but sessions/ is the one exception in this CLI family: ~/.omp/agent/{config.yml,mcp.json,models.yml,settings.yml} are seeded (read-only mount, copied into the container's own ~/.omp/agent once), so an in-container omp never writes refreshed config back to the host and docker commit exports stay secret-free. But ~/.omp/agent/sessions/ is shared (RW), not seeded — the same treatment as codex's sessions/, and for the identical reason: Codeman reads it host-side (omp-transcript.ts, omp-session-resolver.ts) for history recovery and --resume pinning. Seeding it instead of sharing it would make an in-container OMP conversation invisible to Codeman's own history/resume logic, silently breaking Docker support for the kill-survival feature above. The rest of ~/.omp/agent (agent.db/history.db/models.db SQLite caches, terminal-sessions/, blobs/, cache/) stays container-local and is neither shared nor seeded.

Remote SSH cases

omp mode is routed through an interactive login shell (exec "$SHELL" -i -l -c 'omp'), because sshd's remote-command PATH does not include ~/.local/bin. Per-session config and envOverrides do not cross ssh and are rejected rather than silently ignored; use the per-host command override instead.

⚠️ A respawn or reattach of a remote omp session runs omp --continue, not a bare omp, so it lands back in the same conversation. It is deliberately --continue rather than the exact --resume <id> the local and docker paths pin: omp-session-resolver.ts only ever reads THIS host's ~/.omp/agent/sessions/, and a remote conversation's session file lives on the remote host under the remote user's home, so resolving locally would pin a stranger's id. See Respawn / reattach continuation.

Known gaps

  • No idle/completion hook. Idle detection falls back to output-stabilization like every other external CLI. If omp ever ships a hooks system, a Codeman hook POSTing to /api/hook-event would be the highest-value follow-up.
  • Killing a pane mid-turn loses the conversation for real. tmux kill-session before an in-TUI /exit beats omp's own session-file flush — confirmed by direct testing (kill after a clean /exit resumes correctly; kill without /exit first does not). This is not something Codeman can compensate for from outside the process; it would need an upstream omp fix (e.g. flush-on-SIGTERM).
  • Unverified: $HOME as a symlink. The directory-mangling fix above compares against the literal homedir() string, not a realpath()-resolved one. Whether omp itself canonicalizes symlinks before mangling is unconfirmed — this has not been tested against a symlinked-home setup.
  • Ralph, respawn heuristics, token/CLI-info parsing and the ❯ readiness probe are off for omp, as for every external CLI.