Skip to content

Commit d2a46d9

Browse files
committed
docs(plan): approvals incident fixes — root cause, Zed comparison, and execution plan
1 parent f5d8535 commit d2a46d9

9 files changed

Lines changed: 1660 additions & 0 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Concurrent approvals incident fixes
2+
3+
This workspace plans the fixes for the concurrent human-approval failure observed in live
4+
session `db58551b` on 2026-07-19, plus the session-turns counter bug found in the same logs.
5+
The incident and its seven defects are documented and log-verified in
6+
`docs/design/agent-workflows/scratch/debug-concurrent-approvals-db58551b.md`; the design
7+
principles we adopt from Zed's handling of the same problem are in
8+
`docs/design/agent-workflows/scratch/zed-acp-approvals-comparison.md`; the counter bug is in
9+
`docs/design/agent-workflows/scratch/debug-session-turns-append-500.md`.
10+
11+
## Reading order
12+
13+
1. `context.md` explains why this work exists, what it must achieve, and what is
14+
deliberately out of scope.
15+
2. `research.md` holds the code-verified findings (R1 through R8) that the plan is built
16+
on, with file and line evidence for every claim, and a prominent list of open risks.
17+
3. `plan.md` is the implementation plan: five ordered steps, each with exact files,
18+
behavioral contracts, acceptance criteria, and test commands. Each step is independently
19+
landable. The implementer is expected to work from this file without having read the
20+
incident conversation.
21+
4. `qa.md` is the live QA script that reproduces the incident shape against the dev stack
22+
and states the expected correct behavior at each point.
23+
5. `status.md` tracks where the project stands.
24+
25+
## Glossary
26+
27+
Every domain term used in this workspace, one line each.
28+
29+
- **Runner**: the Node/TypeScript sidecar under `services/runner/` that drives a coding-agent
30+
harness inside a sandbox and streams events to the web UI.
31+
- **Harness**: the agent program that implements the model-and-tool loop, such as Pi
32+
(`pi_core`) or Claude Code (`claude`). The runner talks to it over ACP, the Agent Client
33+
Protocol.
34+
- **Gate**: a human-approval request. When the harness wants to run a permission-gated tool,
35+
the runner shows the user an approval card and waits for allow or deny.
36+
- **Park**: ending the browser-facing turn while an unanswered gate keeps the live harness
37+
process waiting. The answer arrives in a later request.
38+
- **Warm resume**: continuing a parked session in place. The runner checks the live process
39+
out of the keepalive pool and answers the original permission request by its id, so the
40+
approved tool runs with its original arguments.
41+
- **Keepalive pool**: the runner's bounded collection of live parked sessions, each with a
42+
time-to-live limit after which it is destroyed.
43+
- **Records**: the durable per-session event stream. The runner posts every agent event to
44+
the API's record-ingest endpoint; the frontend rebuilds a conversation from these rows
45+
(that rebuild is called hydration).
46+
- **Interactions**: the durable table of human-in-the-loop requests. Each gate creates one
47+
interaction row with a lifecycle status (pending, responded, resolved, cancelled).
48+
- **Turns**: the append-only `session_turns` table. One row per completed conversation turn,
49+
carrying the harness's native session id so a restarted runner can resume the conversation
50+
natively instead of replaying text.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Context: why this work exists
2+
3+
## The incident in three sentences
4+
5+
During live QA of two parallel permission-gated shell commands (session `db58551b`,
6+
2026-07-19), both commands executed exactly once and only after their approvals, but the
7+
system misreported both, flipped an already-approved card back to "waiting", and then went
8+
silent because the user's final approval was never sent to the runner. The root causes are
9+
that the durable session record stores every approval request but never any approval answer,
10+
that the post-pause cleanup invents tool results (a false "not executed" error for an
11+
approved running call, and a false success for a call that never started), and that the
12+
frontend only dispatches approvals once every visible card looks settled, a condition that
13+
can never hold after a state rebuild. A seventh, unrelated defect found in the same logs
14+
makes the new `session_turns` ingestion fail with HTTP 500 on every warm turn, because the
15+
turn index is computed once per environment acquire instead of once per turn.
16+
17+
## Goals
18+
19+
1. Fix the session-turns counter so `turn_index` is a true conversation-turn counter, and
20+
make the API answer a duplicate append with 409 Conflict instead of 500.
21+
2. Stop the pause cleanup from inventing tool results: an approved, executing call keeps its
22+
real result, and a never-started call is recorded as deferred, never as success.
23+
3. Persist the answer half of every gate (a new `interaction_response` record event plus the
24+
allow/deny verdict in the interaction row's existing `resolution` field), and make
25+
frontend hydration overlay answers onto requests, so a rebuilt conversation shows
26+
answered cards as answered.
27+
4. Dispatch each approval per card the moment the user answers it, and make the runner
28+
accept a resume that answers only part of the parked gates while remaining parked on the
29+
rest.
30+
5. Add a regression test that reproduces the exact incident shape, then verify with live QA
31+
on the dev stack.
32+
33+
## Non-goals
34+
35+
Two related fixes are deliberately out of scope here.
36+
37+
- **Real turn cancellation** (the Zed-style cancel-then-restart when new user text arrives
38+
while gates are parked, which fixes defect 6 of the incident report) goes to Arda after
39+
JP's sessions PRs merge, because it depends on the session-streams control plane those PRs
40+
introduce and is partly a product decision.
41+
- **Audit-hardening of record ids** (scoping stable record ids by turn so contradictory
42+
results append instead of overwriting, defect 5) is queued separately, because the answer
43+
persistence in this project already fixes the user-facing rebuild problem and the id
44+
rescope touches the record identity contract end to end.
45+
46+
## Constraints already decided
47+
48+
- Work lands in this order, on these lanes: the counter fix on the existing rebase lanes for
49+
PR #5376 (runner, branch `sessions-rebase/runner`) and PR #5375 (backend, branch
50+
`sessions-rebase/backend`), with a wipe of the wrongly numbered dev rows and an
51+
explanatory note for JP; everything else on the PR #5382 lane (branch
52+
`plan/concurrent-approvals`).
53+
- `turn_index` is confirmed to be a true conversation-turn counter, not an acquire counter.
54+
The implementation must carry a code comment stating this invariant, and the PR body must
55+
explain what was done and what was assumed.
56+
- The implementation will be done by an agent that has not read the incident conversation,
57+
so every step in `plan.md` is specified with exact files, behavioral contracts, and
58+
acceptance criteria.

0 commit comments

Comments
 (0)