fix: IJAI-3744 keep a session running while its background tasks are open (#1399)

## Summary

A Claude Code session can start a background shell command or a
background sub-agent, and then end its turn. When a task ends, Claude
Code wakes the session for a short turn. The session index reported
`idle` at each turn end, so a client saw one completion for each task.

Now the index finds the open background tasks in the transcript. A turn
end with an open task stays `running`, also after 10 minutes. The
session gets one `idle`, when the last task ends. The rules are the same
as in the native Claude provider of AIR (IJAI-3744).

## Rules

- A task opens with a tool result that has `backgroundTaskId`, or
`isAsync` with an `agentId`.
- A task closes with its `<task-notification>` (a user record or a queue
`remove`), or with a `TaskStop` result.
- The wait ends when no live process holds the session, or when the
registry status is `idle`.
- A task can start before the tail window. A later scan applies the tail
on top of the earlier task set. When it cannot, the index reads the
whole transcript, but only for a finished turn of a live session.

## Not in this PR

- A background task that a sub-agent starts. Its start record is only in
the transcript of the sub-agent.
- A session that this connection runs. Its state comes from the SDK.

## Tests

- `background-tasks.test.ts`: the record orders of the native provider
tests, in a small transcript and in one longer than the tail window. The
tests also cover a task before the tail window, the end of the wait, and
the count of whole-file reads.
- A real check with Claude Code 2.1.306 in a terminal. With two
background shells or two background sub-agents, `main` gave three `idle`
and this change gave one.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ilya Kirillov
2026-10-21 15:27:16 +02:00
committed by GitHub
parent 4d378f93f3
commit e9f986b8a9
10 changed files with 1012 additions and 77 deletions

View File

@@ -1370,6 +1370,7 @@ Each row carries `_meta.jetbrains.air` with flat row fields of the RFDs. `archiv
- a session this connection runs: the SDK `session_state_changed` state; `error` instead of `idle` when its last turn failed (an error result, `is_error` or an `error_*` subtype, that was not a cancel). A session whose query failed here (not a cancel) is reported as any other session, but `error` until its transcript shows a prompt or a turn end after the failure, unless a busy or waiting interactive CLI holds it;
- a session no live Claude Code process holds: `idle`, or `error` when its last turn ended with an API error (an assistant record with `isApiErrorMessage: true`, also when the CLI's turn-end records follow it, with no prompt after it);
- a session that an interactive CLI holds (not an `sdk-*` entrypoint) whose registry status is newer than the transcript: `busy` and `shell` are `running`, `waiting` is `requires_action`, `idle` is `idle`, or `error` after such an API error;
- any other held session whose last turn ended with an open background task: `running`, also after 10 minutes, until the registry status is `idle`. Claude Code wakes the session again when the task ends, so the session gets one `idle` at the last turn end, not one for each task. A background shell command (`toolUseResult.backgroundTaskId`) or an async sub-agent (`isAsync` with an `agentId`) opens a task. Its `<task-notification>` with a `<status>`, as a user record or a `queue-operation` `remove`, or the result of a `TaskStop` call closes it. A notification without a `<status>`, such as the check-in of a quiet shell or a `Monitor` event, keeps it open. Sidechain records open no task. The tail gives the tasks on top of those of an earlier scan when the file only grew within the tail; else the whole transcript is read, and only for such a held session. A background task that a sub-agent starts is not seen: it is only in the transcript of the sub-agent;
- any other held session: `idle` after a finished turn (an assistant `end_turn`, a user interrupt, or the CLI turn-end records), `error` after a turn that ended with an API error, `running` for an unfinished turn written in the last 10 minutes, and no state otherwise.
- The live processes come from `<config>/sessions/<pid>.json`, with the liveness rules of Claude Desktop: `kill(pid, 0)` (or `EPERM`), a matching `procStart`, no foreign `pidDomain`, and records older than 24 hours only with a matching `procStart`.
The adapter never reads the `.key` files there and never changes the directory.