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