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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **A lich worker that pauses on a background command is no longer reported as
finished.** A worker whose turn ended before it reported, usually waiting on a
command it left running, now comes back as a backgrounded agent, and its
report still arrives as a [lich] note.

## [0.15.0] - 2026-10-05

### Added
Expand Down
2 changes: 1 addition & 1 deletion docs/agent-cards.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Nothing runs after that: the delivery `lich open` printed decides the answer.
|-------------------------------|---------------------------------------------------------------------|
| `pending` | `async_launched` at once, plus a note on how the report comes back; the worker is counted in the status line |
| `answered` | `completed`: the report, then the checkout (and branch) it is on |
| `unanswered` | `completed`: the worker ended its turn without reporting, and where to look |
| `unanswered` | `async_launched`, as for `pending`: a turn that ended unanswered usually left a background command running, and the report still arrives as a [lich] note |
| `unread`, `undelivered` | denied: the task never reached the card |
| a status it does not know | denied, naming the card and its branch |
| the open failed | the native agent runs, with a toast saying why |
Expand Down
10 changes: 4 additions & 6 deletions hooks/agent-cards.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,11 @@ function completed(e, opened, branch, text, durationMs) {
*/
function answerFor(e, opened, branch, report, durationMs) {
switch (report.status) {
// A worker whose turn ended unanswered is usually still at it: it left a
// command running in the background and resumes when it finishes, and its
// report still arrives as a [lich] note.
case "pending":
case "unanswered":
return backgrounded(e, opened, branch)
case "answered": {
const where =
Expand All @@ -258,12 +262,6 @@ function answerFor(e, opened, branch, report, durationMs) {
`Reach that session with send_to_session or lich send, not SendMessage.`
return { result: completed(e, opened, branch, `${report.answer}\n\n${where}`, durationMs) }
}
case "unanswered": {
const text =
`"${opened.label}" ended its turn without reporting back through lich. What it did is on its card and ` +
`${branch ? `on branch ${branch}` : "in this checkout"}; a report it sends later arrives here as a [lich] note.`
return { result: completed(e, opened, branch, text, durationMs) }
}
case "unread":
case "undelivered":
return { deny: `the task never reached "${opened.label}" (${report.status}): open its card.` }
Expand Down
26 changes: 9 additions & 17 deletions tests/agent-cards.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -368,24 +368,16 @@ test('an isolated task still pending at the open says which branch the worker is
assert.deepEqual(answer, backgrounded())
})

test('a worker that ended its turn without reporting is a completed subagent saying so', async () => {
const { answer } = await delegate([exits(3, openedHere('unanswered'))])
assert.deepEqual(
answer,
completed(
`"${SHARED.label}" ended its turn without reporting back through lich. What it did is on its card and in ` +
`this checkout; a report it sends later arrives here as a [lich] note.`,
{ worker: SHARED },
),
)
// A worker whose turn ended before it reported is usually still working: it
// left a command running in the background and resumes when it finishes. Its
// report still arrives as a [lich] note, so the call is a backgrounded agent,
// never a finished one.
test('a worker that ended its turn without reporting comes back as a backgrounded agent', async () => {
const { answer, passed } = await delegate([exits(3, openedHere('unanswered'))])
assert.deepEqual(answer, backgrounded({ worker: SHARED }))
assert.deepEqual(passed, [])
const isolated = await delegate([onBranch('main'), exits(3, opened('unanswered'))], ISOLATED)
assert.deepEqual(
isolated.answer,
completed(
`"${BRANCH}" ended its turn without reporting back through lich. What it did is on its card and on branch ` +
`${BRANCH}; a report it sends later arrives here as a [lich] note.`,
),
)
assert.deepEqual(isolated.answer, backgrounded())
})

test('a task that never reached the worker is denied, never run natively', async () => {
Expand Down
Loading