SIGN IN SIGN UP

fix(acp): stop an async task from outliving the session that owns it

An AIR task row is scoped to one session and settles only when the
adapter publishes its terminal frame. Four ways it could stop settling:

A fork attaches to a new session id on the same process, and the old
session's frames stop routing to this connection. The rows stayed,
permanently `running` — the strip showed work that could never finish,
and a live row exempts the connection from idle reaping, so the agent
CLI was pinned for good. Both apply_event and the frontend reducer now
drop the table when the session id changes; an empty snapshot table
cannot do it for them, since empty reads as "nothing to say".

A snapshot fetched before a fork can land after it. Gate the merge on
the snapshot describing the session we are on, the same identity-guard
shape as the connection check one level up.

A stale-by-seq snapshot could walk a finished task back to `running`:
the rows carry no revision, so replace-by-id is only sound while the
snapshot is the fresher of the two, and the live terminal event is
already below the applied watermark and never replayed. The stale branch
now only adopts ids the client does not have — the add-only rule the
failure-record merge gets from its revision counter.

Anything left over gets the wall-clock bound the transcript watcher
already had, refreshed by any delta so a task that keeps reporting keeps
its exemption.

Also: the frontend idle sweep only knew about the watcher's accounting,
so a workflow or monitor task — which announces itself on this channel
and nowhere else — could be disconnected mid-run; count only a spawn as
turn output, since a progress tick from an earlier turn's task was
silencing the empty-turn diagnosis for prompts the agent answered with
nothing; and require `session/update` before claiming a frame ahead of
the typed pipeline.
X
xintaofei committed
df8afe20340930a295a0e11d48d8689aa86eba06
Parent: b59230f