Skip to content

feat: let lich drive a Claude Code session through a mod - #28

Merged
omartelo merged 6 commits into
mainfrom
feat/mod-control
Oct 3, 2026
Merged

omartelo merged 6 commits into
mainfrom
feat/mod-control

Conversation

@omartelo

@omartelo omartelo commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

Client of lich's mod-control contract (lichdotdev/lich#645, shipped in lich v0.57.0), and the v0.14.0 release: fixtures read at lich v0.57.0 (the temporary pin is gone), every manifest and plugin_version bumped, CHANGELOG cut. node --test tests/*.test.mjs: 220 pass.

The mod-control contract is served by lichdotdev/lich#645 and is in no lich
release, so tests/lich-ref has nothing to read it at. refresh-fixtures.sh
reads its two files at that pull request's head commit instead, and CI's
drift check covers them like the rest; once a release ships the contract,
lich-ref moves there and the pinned block goes.

contract.mjs maps /mod/acks to mod-control.jsonl and models each of its
rejected cases, so the generic fixture tests cover it too.
hooks/mod-control.js is a Claude Code mod, named under `modules` in the
same hooks/hooks.json as the scripts, which keep firing beside it. In an
interactive session inside lich it long-polls GET /mod/commands, applies
prompt, abort, model, effort and compact in order, and acks each one on
POST /mod/acks, awaiting every ack before the next command so an abort's
ack never ends the turn a later prompt opened. Polling runs apart from the
commands, so a prompt waiting for the session to go idle does not let
lich's 30 second attach window lapse. A network error or a 5xx backs off
1s doubling to 10s, a 404 stops polling, and an effort outside the five
levels is refused up front rather than failing at the next request.

Claude Code 2.1.280 and later run it; older releases ignore or skip the
module and keep every report. A `claude -p` never polls: a parked poll
held its run open and inherited the parent session's variables.
Every command ran on one serial chain, and a prompt settles only once the
session is idle and its turn starts. An abort, model, effort or compact
that arrived while a lich prompt waited on a busy session was held behind
it: the abort the user clicked never reached the running turn, and once
that turn ended on its own it cancelled the prompt's new turn instead.

A prompt is now submitted in its place on the chain and acked when its
turn starts, without holding the commands after it. Every other command
still waits for its ack, so an abort's ack lands before the next prompt
runs.
@omartelo
omartelo merged commit 011a77b into main Oct 3, 2026
1 check passed
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