Skip to content

feat: run the lich plugin in oh-my-pi, and resume its sessions - #258

Merged
omartelo merged 1 commit into
mainfrom
feat/omp-plugin
Aug 13, 2026
Merged

omartelo merged 1 commit into
mainfrom
feat/omp-plugin

Conversation

@omartelo

Copy link
Copy Markdown
Collaborator

lich's half of oh-my-pi support. The plugin's half is omartelo/lich-plugin#18 — this needs a release carrying omp/lich.js before an install can succeed, since the install fetches that path at the latest tag.

What

The plugin installs (internal/agentplugin/omp.go). omp has no plugin CLI, so lich writes the released extension into omp's own extensions/ directory — the file being there is the install, and the marker line above it is the only record of the version, since omp keeps no plugin state to ask. Settings › lich plugin lists it beside the others; no UI change was needed, the row comes from Status().

The MCP server is registered. omp takes no --mcp-config flag, and the only place it reads a server from is an mcpServers document beside the extensions, so the install merges a lich entry into mcp.json. Every other server and every key lich knows nothing about survive the round trip. A document lich cannot parse aborts the install before anything is written, rather than being replaced — overwriting one lich failed to understand would delete servers it cannot see.

omp sessions resume. omp -r <id> reopens a conversation, and omp files one JSONL per session — so ResumeAvailable has something to prove one still exists, which is exactly what opencode and Crush lack (both keep conversations in SQLite). Cards that ran omp now get the same "continue where it left off" prompt Claude Code and Codex cards get.

The contract carries omp. docs/hooks/ gains its column in every mapping table and its case in the session-start fixtures.

On the "blocker" the plugin side reported

The plugin PR reports that provider: "omp" would be rejected with a 400 until omp is registered. It would not: the endpoint validates against providers.Known (internal/terminal/transport.go), and omp has been in providers.Registry since it was added. What was missing was only a fixture case naming it — docs/hooks/fixtures/session-start.jsonl is a sample of cases, not the provider list (claude has no case of its own either). The case added here is now exercised by the real parser in TestHookFixturesMatchParsers, which is the proof.

Measured, not guessed

Everything provider-specific was read off the binary on this machine (omp v17.3.0):

  • -r, --resume=<value> — resume by id.
  • omp config path → ~/.omp/agent; with OMP_PROFILE=work → ~/.omp/profiles/work/agent; with PI_CODING_AGENT_DIR=/tmp/x → /tmp/x; with both, the profile wins. That precedence is pinned by a test, because getting it backwards means the install lands where omp is not reading and every restored card silently starts fresh.

Known gaps, both the harness's own

  • No waiting. omp declares tool_approval_requested but no real run was observed emitting it, and a report wired to an event name that never fires is one that silently never arrives. So a session waiting on permission shows a spinner rather than a bell — Settings says so (OMP_APPROVAL_HINT), following the precedent of the Crush hint, because a missing bell reads as a broken install.
  • No idle, for opencode's reason: the extension runs in-process, so what would report the CLI leaving dies with it.

Test plan

  • gofmt -l . clean, go vet ./... clean
  • go test ./internal/... green (1344), -race on agentplugin and terminal
  • Cross-compile loop for linux/darwin/windows: go build + go vet clean on each
  • biome check, tsc --noEmit, vitest run (875) and vite build green
  • New coverage: install writes the extension with its marker and registers the server; a pre-existing mcp.json keeps its own servers and its unknown keys; an unparseable one is refused with nothing written; a 404 release leaves neither file behind; ompAgentDir precedence; resumeArgs and ResumeAvailable for omp, including that omp never matches a Codex rollout
  • Not exercised against a real install — that needs the plugin release above. The chain is covered to the bytes on disk, which is where lich's half ends.

Four things, all oh-my-pi.

**The plugin installs.** omp has no plugin CLI, so lich writes the released
extension into omp's own `extensions/` directory, exactly as it writes
opencode's module — the file being there is the install, and the marker
line above it is the only record of the version, since omp keeps no
plugin state to ask. Settings › lich plugin offers it beside the others.

**The MCP server is registered.** omp takes no `--mcp-config` flag, and
the only place it reads a server from is an `mcpServers` document beside
the extensions, so the install merges a `lich` entry into it. Every other
server and every key lich knows nothing about survive the round trip; a
document lich cannot parse aborts the install rather than being replaced,
before anything else is written. What it cannot preserve is the user's
formatting — JSON has no comment to hide a marker in and no way to
append, which is why Crush got a block in `crushrc` and omp gets a
rewrite.

**omp sessions resume.** `omp -r <id>` reopens a conversation, and omp
files one JSONL per session, so `ResumeAvailable` has something to prove
one still exists — which is exactly what opencode and Crush lack. The
directory is resolved the way `omp config path` was measured to resolve
it: `OMP_PROFILE` moves the whole thing and beats `PI_CODING_AGENT_DIR`.

**The contract carries omp.** `docs/hooks/` gains its column in every
mapping table and its case in the session-start fixtures — the payload
was already accepted (`omp` has been in `providers.Registry` since it was
added), but a contract that never named it left the plugin side guessing.

Two gaps are the harness's own, and Settings says the first out loud:
omp declares a tool-approval event that no real run was observed emitting,
so a session waiting on permission shows a spinner rather than a bell; and
`idle` dies with the in-process extension that would report it, as
opencode's does.
@omartelo

Copy link
Copy Markdown
Collaborator Author

Validated end to end against the real release (lich-plugin v0.10.0), on Linux, in an isolated omp agent directory (PI_CODING_AGENT_DIR) — the user's own omp config was never touched.

1. The install, run through this branch's own code path (Service.Install("omp"), real network, real release):

  • extensions/lich.js written, 7232 bytes, first line // lich-plugin v0.10.0 — installed by lich, replace with the file from omartelo/lich-plugin
  • installedVersion("omp") reads back 0.10.0
  • a pre-existing mcp.json carrying {"mcpServers":{"notes":…},"other":1} came out with lich merged in and both notes and other intact:
{
  "mcpServers": {
    "lich": { "args": ["mcp"], "command": "…/lich" },
    "notes": { "command": "notes-server" }
  },
  "other": 1
}

2. omp loads what lich wrote, and reports. Running omp against a stub listener on the coordinates lich injects — no --hook flag, purely the autodiscovery of extensions/:

/session-start?token=…  {"session_id":"probe-session","provider_session_id":"019ffbbe-3f1b-7000-90dd-0184132e5d80","provider":"omp"}
/hook?token=…           {"session_id":"probe-session","state":"busy"}
/hook?token=…           {"session_id":"probe-session","state":"done"}

The turn itself died on a deliberately invalid API key, and the reports still arrived — start and end of turn do not depend on the model call.

3. Resume, against the transcript that run actually wrote. ResumeAvailable("omp", <that id>) → true, a made-up id → false, and the glob resolved the real file (sessions/-tmp/2026-08-13T15-29-37-051Z_<id>.jsonl). Then the invocation itself: omp -r <id> re-entered the same conversation — its session-start reported the identical provider_session_id rather than a new one, which is the whole contract resumeArgs rides on.

One thing worth knowing for the manual check: omp installs into ~/.cache/.bun/bin, which is on PATH only in a login shell. lich resolves its environment through $SHELL -lic env at boot (ResolveShellEnv), so Available is true in the app — but a bare non-interactive shell will not find omp, which is what makes the Settings row say "CLI not installed" if you go looking from one.

Probes were disposable and are gone; nothing in the diff changed since the checks above.

@omartelo
omartelo merged commit 28350ad into main Aug 13, 2026
4 checks passed
@omartelo
omartelo deleted the feat/omp-plugin branch August 13, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant