From e8b8a78dd1c0416f84ad686d69733c9e3bcc90a7 Mon Sep 17 00:00:00 2001 From: omartelo Date: Sat, 17 Oct 2026 14:53:51 -0300 Subject: [PATCH] fix(omp): report the conversation a /resume switches to A /resume inside omp moves the session onto another conversation without a second session_start: omp emits session_switch instead (reason "resume", measured on 18.8.9 with a logger extension), with the session manager already on the new id. The client now reports session-start on it too, so lich holds the conversation the card runs and can warn when another card runs it as well. --- CHANGELOG.md | 8 ++++++++ docs/providers.md | 2 +- docs/session-start.md | 7 +++++-- omp/lich.js | 17 ++++++++++------- tests/omp.test.mjs | 17 +++++++++++++++++ 5 files changed, 41 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15223fb..bed3fd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **An omp session that resumes another conversation tells lich which.** A + `/resume` inside omp left lich holding the conversation the card opened + with, so a restart resumed the old one and lich could not warn when the + conversation was already open in another card. The switch is now reported + as it happens. + ## [0.20.4] - 2026-10-16 ### Fixed diff --git a/docs/providers.md b/docs/providers.md index 897b511..fe43cf2 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -74,7 +74,7 @@ swallows its own errors and never awaits a report. | Report | Claude Code | Codex | Antigravity | opencode | omp | Crush | |---------------------|------------------------------|------------------------------|------------------------------|---------------------------|--------------------------|-----------------------------| -| session id | `SessionStart` | `SessionStart` | `PreInvocation` | `session.created` | `session_start` | `PreToolUse` | +| session id | `SessionStart` | `SessionStart` | `PreInvocation` | `session.created` | `session_start`, `session_switch` | `PreToolUse` | | `busy` | `UserPromptSubmit`, `PostToolUse` | `UserPromptSubmit`, `PostToolUse` | `PreInvocation` | `session.status` (`busy`) | `input`, `turn_start` | — | | `busy` + tool | `PreToolUse` | `PreToolUse` | `PreToolUse` | `tool.execute.before` | `tool_call` | — | | `waiting` + reason | `Notification` | `PermissionRequest` | — (not measured) | any `*.asked` | — (see below) | — | diff --git a/docs/session-start.md b/docs/session-start.md index 22b9b47..0817a25 100644 --- a/docs/session-start.md +++ b/docs/session-start.md @@ -10,7 +10,7 @@ the *provider* session id — the field is provider-agnostic. | script | action | Claude Code hook | Codex hook | Antigravity hook | opencode event | omp event | Crush hook | |------------------------------------|---------------------------------|------------------|----------------|------------------|-------------------|-----------------|--------------| -| `report-session-start.sh ` | send the CLI's `session_id` | `SessionStart` | `SessionStart` | `PreInvocation` | `session.created` | `session_start` | `PreToolUse` | +| `report-session-start.sh ` | send the CLI's `session_id` | `SessionStart` | `SessionStart` | `PreInvocation` | `session.created` | `session_start`, `session_switch` | `PreToolUse` | `POST /session-start` with `{"session_id": $LICH_SESSION_ID, "provider_session_id": , "provider": @@ -41,7 +41,10 @@ The module harnesses report from the earliest place each offers. opencode raises `session.created` before the first turn, as early as `SessionStart`; omp's `session_start` fires at the same point, and the id itself is read off `ctx.sessionManager.getSessionId()` rather than off the event, which carries -nothing but its type. **Crush only has `PreToolUse`**, so its id rides the first +nothing but its type. A `/resume` inside omp switches conversations with no +second `session_start`: omp emits `session_switch` (reason `resume`, measured on +18.8.9), by which point the manager already answers with the new id, so that +event reports too. **Crush only has `PreToolUse`**, so its id rides the first tool call and a conversation that answers without a tool never sends one — late, not wrong. diff --git a/omp/lich.js b/omp/lich.js index 4872cc6..44ba666 100644 --- a/omp/lich.js +++ b/omp/lich.js @@ -115,13 +115,16 @@ export default function lichPlugin(pi) { // The session id lich resumes from. It is on the manager rather than in the // environment: omp exports nothing about the running session to its own // process env, so `ctx` is the only source. - pi.on( - "session_start", - safely((_event, ctx) => { - const id = sessionIdOf(ctx) - if (id !== undefined) report("session-start", { provider_session_id: id, provider: "omp" }) - }), - ) + const reportStart = safely((_event, ctx) => { + const id = sessionIdOf(ctx) + if (id !== undefined) report("session-start", { provider_session_id: id, provider: "omp" }) + }) + pi.on("session_start", reportStart) + // A `/resume` moves the session onto another conversation with no second + // `session_start`: omp emits `session_switch` instead, its manager already + // on the new id (measured on 18.8.9). Reported like a start, so lich holds + // the conversation the card runs now. + pi.on("session_switch", reportStart) // The user has sent something: the turn starts here in an interactive // session, which is the only kind lich spawns. diff --git a/tests/omp.test.mjs b/tests/omp.test.mjs index 1abc1e2..37cebf9 100644 --- a/tests/omp.test.mjs +++ b/tests/omp.test.mjs @@ -130,6 +130,23 @@ test('session_start with no id yet reports nothing', async () => { assert.equal(requests.length, 0, `posted an empty provider session id: ${requests[0]?.raw}`) }) +// A `/resume` inside omp moves the session onto another conversation without +// a second `session_start`: omp 18.8.9 emits `session_before_switch` and then +// `session_switch` (reason "resume"), measured with a logger extension, and +// by the second one the manager already answers with the new id. lich keeps +// the id of the conversation running in the card, and warns when another card +// runs it too, so the switch is reported like a start. +test('session_switch reports the conversation switched to', async () => { + const switched = '0000000b-0000-7000-9000-00000000000b' + const requests = await reportsFor(({ emit }) => + emit('session_switch', { type: 'session_switch', reason: 'resume' }, ctxWith({ id: switched })), + ) + assert.equal(requests.length, 1) + const { body } = assertSessionStart(requests[0]) + assert.equal(body.provider, 'omp') + assert.equal(body.provider_session_id, switched) +}) + test('input reports busy', async () => { const requests = await reportsFor(({ emit }) => emit('input', { type: 'input', text: 'ship it', source: 'interactive' }, ctxWith()),