Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) | — |
Expand Down
7 changes: 5 additions & 2 deletions docs/session-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <provider>` | send the CLI's `session_id` | `SessionStart` | `SessionStart` | `PreInvocation` | `session.created` | `session_start` | `PreToolUse` |
| `report-session-start.sh <provider>` | 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": <session_id/conversationId from the hook payload on stdin>, "provider":
Expand Down Expand Up @@ -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.

Expand Down
17 changes: 10 additions & 7 deletions omp/lich.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 17 additions & 0 deletions tests/omp.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
Loading