Skip to content

test(qa): add the session-control cells to the agent release gate - #6518

Merged
mmabrouk merged 23 commits into
feat/session-controlfrom
qa/session-control-driver
Sep 4, 2026
Merged

test(qa): add the session-control cells to the agent release gate#6518
mmabrouk merged 23 commits into
feat/session-controlfrom
qa/session-control-driver

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Sep 3, 2026

Copy link
Copy Markdown
Member

The thirteen-cell driver that proved warm Stop on 2 and 3 September lived in an evidence folder outside the repo. This PR moves it into the release-gate skill as a standing check, with two new cells the QA audit asked for: repeated Stop, and Stop racing a natural completion.

What changes

  • .agents/skills/agent-release-gate/resources/session_control.py: fifteen cells. Seven run over HTTP alone against any deployment; six that need container or Postgres access sit behind an operator-hooks interface and skip by name without --project. Results land as PASS, FAIL, or SKIP with a one-line reason in a timestamped run folder, and --resume replays a prior results.json so a lost agent costs one cell.
  • path_triggers.py: the cells become mandatory for any change under the runner sessions and sandbox-agent code or the API sessions core, tasks, and routers.
  • SKILL.md: the one-line command, the flags, and the three environment variables by name.
  • Unit tests for the pure parts (test_session_control.py, 8 pass).

Runs tonight

Stack Harness Provider Result
integration (7f2ef31 + sweep fixes) Pi local stop-warm and repeat-stop pass; Stop 105 ms, two Stops 50 ms apart gave one terminal record
integration Claude Code local 5 of 7 pass; stop-approval fails because the builtin shell tool never asks; stop-after-finish ambiguous without hooks
integration Pi Daytona 6 of 7 pass, every cell in the same sandbox
Track A stack (#6503 + #6501 + fixes) Pi local, with hooks 9 of 13 pass; three failures were first-turn 504s on a cold stack, one is the missing #6500 admission

The stop-approval cell found a real defect on both providers: after a Stop cancels an approval, the next message takes the runner's approval-resume path and the new text never reaches the model. The fix is on PR #6496.

Added on 2026-09-04

  • Cells runner-gone (the runner is paused before the Stop, so delivery cannot reach it; the sweep must settle the command lost), runner-gone-late (the runner is killed right after the Stop is accepted), and concurrent-stops (five sessions stopped within one second). All three found real defects on their first run; the fixes are on PR fix(sessions): settle executions after runner or sandbox loss #6501 and PR feat(runner): preserve the warm sandbox after Stop #6496.
  • Every hook cell restores its container in a finally block, and the run ends with the runner running and healthy even when a cell throws (5ce3d7e982).
  • --client-shape full|last-message: last-message mirrors the desktop's request semantics from agentRequest.ts (send only the trailing user message; keep full history when the trailing turn carries a settled tool answer). The browser pass found a continuity defect the full shape hides, so continuity cells must run in both shapes (3c07ceb242).
  • Run folders carry the process id so concurrent runs never collide; the run records client_shape.
    Nineteen cells; 23 unit tests.
  • Every Stop-issuing cell now asserts settlement within 20 s: the command row reaches applied or obsolete (never left pending or claimed) and exactly one execution row carries a terminal outcome; the false pass seen on repeat-stop (command claimed forever, no execution row) fails now (d5bbdc3ee6). Per-harness stream timeouts: Pi 600 s, Codex and Claude Code 900 s (e33a1aa3ea). Nineteen cells; 49 unit tests.

Agent-generated, low weight. Not merged.

https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV

Update 2026-09-04 afternoon

The sandbox-gone cell killed the wrong process on the local provider: it matched the first sandbox-agent daemon under a shared mount key and hit the parked idle sandbox of another session, while the tested turn ran on and finished. Head a769ce9 makes the cell target the tested session's own sandbox port from the runner log, verify the pid's command line, refuse a guess as "wrong target", record port and pid in the evidence, and wait the design window (three liveness probe failures at 30 s plus slack, never shorter than the slow command). 63 driver tests pass.

Head f6bf53c adds the second half: the cell polls the runner log and the turn ledger for the tested session's own sandbox for up to 120 s (a cold acquire on the stack takes about 35 s), waits until the slow command runs, then kills only that daemon; the slow command lasts 300 s so it outlasts the acquire, the poll, and the 90 s probe window. 67 driver tests pass.

Head 74ad9f9 adds a third cell fix: runner-gone-late polls the runner health (up to 60 s at 2 s) after it restarts the runner and before the recovery Send, so the restart race can no longer read as a product failure. 69 driver tests pass.

Head 4fbfcb6 adds the fourth cell fix: runner-gone reads is_running from session_streams while the runner is still paused, after the Stop command settles lost, and records the paused-read and settle timestamps. The unpause and the following Send are a separate resumability check, because the returning runner starts a new turn that sets is_running true again. 71 driver tests pass.

Live results on the integration stack (head dfb94a0c85 and later): runner-gone-late passed on 74ad9f9, and sandbox-gone passed on f6bf53c (the tested session's own sandbox died mid-turn and the turn ended with a terminal record). runner-gone re-runs on 4fbfcb6.

Head 97b42fd adds the fifth cell fix: the shared settlement assertion now accepts a Stop that lands after a natural finish — command obsolete/not_running with zero execution rows — recording "stop landed after a natural finish", while keeping the strict one-row requirement when the outcome is stopped or lost. This is the shape stale-stop hit on a fast Claude Code turn in run 4. Because every Stop-issuing cell routes its settlement check through this one function, they all inherit it. 73 driver tests pass.

Port the durable-cancel spike's 13-cell driver (refresh_live.py) into the
release-gate skill as resources/session_control.py, per qa-audit-2026-09-03.md
section 4, so the standing regression check survives outside one evidence
folder. Matches the gate's env contract (AGENTA_BASE, AGENTA_ADMIN_KEY,
QA_OPENAI_API_KEY, no file fallback), moves the Docker/Postgres-only helpers
behind an OperatorHooks interface so six cells run over HTTP against any
deployment and the rest SKIP by name without --project, emits the gate's
PASS/FAIL/SKIP result shape into a timestamped ~/agenta-qa-evidence/ run
folder, and adds --resume so a lost agent costs one cell, not the run. Adds
two new cells (repeat-stop, stop-during-completion) from qa-audit section 3,
a path_triggers.py rule that makes the suite mandatory for session-code
changes, a SKILL.md section naming the command and the model-key locations,
and a pytest-and-standalone-runnable unit test for the pure parts (cell
registry, hooks skip path, resume, verdict shape, env resolution).

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
The live smoke run against the integration stack showed /sessions/{id}/cancel
returns 202 Accepted (a pending command plus a stopping execution), the
correct async-acceptance status. The verdict checks in stop-warm,
stop-approval, repeat-stop, and stop-during-completion hardcoded 200 and
FAILed every real Stop. Accept 200 or 202 in each.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
…ontrol.py

Add HARNESSES["claude"] (kind claude, model sonnet, provider anthropic, vault
connection) so the session-control cells can drive the Claude Code harness,
and stock an Anthropic provider key into the bootstrapped account's vault
the same way the OpenAI key is stocked, gated on --harness claude so a
pi_core/codex-only run does not need ANTHROPIC_API_KEY set. Also widen
wait_for_turn/wait_for_tool by a configurable SANDBOX_STARTUP_SLACK_S (25s)
when --sandbox daytona is selected, since a Daytona sandbox takes 10 to 20s
to start on top of local timings. Record the session's distinct sandbox ids
(via /sessions/turns/query, HTTP-only) in every HTTP-only cell's evidence as
sandbox_ids / warm_same_sandbox, so a resume that silently rebuilt the
sandbox is visible in the result instead of only in the recalled codeword.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
The Daytona smoke run FAILed stop-approval with only
"resume did not recall the codeword" and no reply text to check why, so a
driver replay bug (the reconstructed output-denied tool part) could not be
told apart from a genuine product miss. Add resume_text, resume_frames, and
resume_errors to the cell's evidence.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
Two invocations started in the same second (e.g. Claude Code and Daytona
smoke runs fired in parallel tonight) shared a run folder, since the
timestamp alone has 1-second resolution -- the second writer silently
overwrote the first one's results.json mid-run and one run's evidence was
lost until recovered from its redirected stdout log. Add the PID to the
folder name so concurrent invocations never collide.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agenta-documentation Ready Ready Preview Sep 4, 2026 4:23pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Team

Run ID: 5883e1ab-c024-4110-8e7a-12d5f7028779

📥 Commits

Reviewing files that changed from the base of the PR and between 97b42fd and e2a25fe.

📒 Files selected for processing (3)
  • .agents/skills/agent-release-gate/SKILL.md
  • .agents/skills/agent-release-gate/resources/qa_product.py
  • .agents/skills/agent-release-gate/resources/test_qa_product_concurrency.py

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Summary

Summary by CodeRabbit

  • Documentation

    • Added guidance for running session-control quality checks, including required commands, environment settings, resume support, result locations, and Daytona permissions.
    • Documented scenarios requiring session-control validation across session and sandbox-related changes.
  • Tests

    • Added an offline regression suite covering session recovery, pause/resume behavior, command handling, timeout logic, environment resolution, and sandbox targeting.
    • Expanded automated validation to include session-control results, mandatory checks, incomplete reports, and failure propagation in release gating.

Walkthrough

The release gate now documents and triggers standalone session-control regression cells for session-related paths. It validates recorded session-control results, reports their status, and includes network-free coverage for session-control behavior.

Changes

Session control release validation

Layer / File(s) Summary
Release gate documentation and path wiring
.agents/skills/agent-release-gate/SKILL.md, .agents/skills/agent-release-gate/resources/path_triggers.py
Documents session-control commands, result handling, environment requirements, and Daytona Secret permissions. Maps sandbox and session paths to SESSION_CONTROL.
Session-control result integration
.agents/skills/agent-release-gate/resources/qa_product.py, .agents/skills/agent-release-gate/resources/test_qa_product_concurrency.py
Adds --session-control-results, validates complete result files, reports recorded PASS or FAIL status, and returns exit code 1 for a recorded failure.
Standalone session-control validation
.agents/skills/agent-release-gate/resources/test_session_control.py
Tests cell registration, verdicts, runner lifecycle behavior, command settlement, sandbox controls, recovery timing, environment handling, message shaping, and pytest-free execution.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟡 Moderate · up to 97b42

This change adds session-control coverage to release paths, but its standalone result is not currently enforced by the release gate, so affected changes can proceed without a passing session-control outcome. The hook-requirement documentation is also inconsistent and can lead to incorrect execution expectations.

Sequence Diagram(s)

sequenceDiagram
  participant ChangedPaths
  participant qa_product
  participant session_control_results
  ChangedPaths->>qa_product: trigger session_control.py
  qa_product->>session_control_results: load results.json
  session_control_results-->>qa_product: return cell verdicts
  qa_product-->>ChangedPaths: return release-gate status
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.44% which is insufficient. The required threshold is 60.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 114 functions across 2 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the session-control driver, path triggers, tests, execution options, and recorded validation results.
Title check ✅ Passed The title clearly identifies the addition of session-control cells to the agent release gate.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 25.44% which is insufficient. The required threshold is 60.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 114 functions across 2 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch qa/session-control-driver

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…imeout

assistant_message() indexed turn["segments"] unconditionally. When the
driver's own wait for a turn times out (handle["out"] stays None,
observed when the runner is unhealthy after a restart), the cell passed
an empty {} dict in and the KeyError masked the real signal, which is a
driver-side timeout rather than a cell result.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
Add cell_runner_gone, ported from cell_runner_gone in refresh_live.py, as
cell "runner-gone" in the session-control driver's registry. It restarts
the runner right after a Stop is claimed, then checks that the sweep
settles the command as lost (not claimed) in session_commands, the
session_streams row reads is_running: false, and a Send sent after that
runs. Register it in CELLS and in the registry's stable-names unit test,
and update SKILL.md's cell count and Docker-needing cell list.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
…ncurrent-stops

Add OperatorHooks.ensure_runner_healthy(), implemented in DockerComposeHooks, that
unpauses, restarts, and health-checks the runner as needed. Extract the per-cell
execution in main() into run_cell(), which calls it in a finally block after every
needs_hooks cell, so a cell that raises before its own restore code runs (as
cell_stale_tail did tonight, leaving the runner paused) cannot strand the runner
for the next cell. Also wrap cell_stale_tail's pause/unpause and
cell_records_outage's stop/start Postgres in their own try/finally, so each cell
restores what it touched even on an exception in between.

Add cell "concurrent-stops": five sessions started at once with a long turn, Stop
sent to all five within about a second, each expected to return HTTP 202, settle
exactly one terminal record, and recall its own codeword on a warm resume.
HTTP-only, no hooks needed.

Add unit tests for run_cell's finally path (NullHooks skips the recovery call
without crashing; a stub hooks object confirms the recovery call fires when a
cell raises, and is skipped for a cell that does not need hooks) and add
"ensure_runner_healthy" to the NullHooks-raises coverage. Update SKILL.md's cell
count and lists.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
… both races

Split cell_runner_gone into two: the new cell_runner_gone pauses the runner
BEFORE sending the Stop, so the command can never be claimed or reported and
must settle lost off a deterministic sweep, with an explicit check for the
watchdog's execution_lost ending. cell_runner_gone_late keeps the old
restart-after-stop timing, which mostly loses that hard race because the
runner often reports the Stop's outcome before it actually dies.

Both races satisfy the same invariant: exactly one effective terminal
outcome, no command left pending or claimed, is_running false, and the next
Send succeeds. Factor that shared PASS rule into _judge_runner_gone(), used
by cell_runner_gone_late (cell_runner_gone keeps its own stricter assertion
since pausing first is meant to force the lost/execution_lost shape every
time). Both record which race landed on evidence["race"].

Register runner-gone-late in CELLS and the registry's stable-names test.
Update SKILL.md's cell count and Docker-needing cell list.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
Add nine unit tests against _judge_runner_gone with synthetic evidence dicts:
both accepted races (outcome lost -> "never-reported", outcome stopped ->
"outcome-reported-then-died") record the right evidence["race"] and PASS, and
each failure path (no terminal record, no command row, an unexpected command
state, the command still pending or claimed, is_running still true, the next
Send not running) FAILs without setting evidence["race"]. The cell-name
registry coverage for runner-gone and runner-gone-late already existed in
test_cell_names_are_stable_and_known.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
The desktop client sends only the trailing user message on each invoke
(web/packages/agenta-playground/src/state/execution/agentRequest.ts), while
this driver always replays the whole transcript, masking continuity bugs
that only show up under the desktop's actual request shape (found by a
browser QA pass: a codeword from an earlier completed turn was lost on
resume).

Add --client-shape full|last-message, default full so existing results stay
comparable. Under last-message, invoke() reshapes every outbound `messages`
list the way agentRequest.ts does: _has_answer()/_is_answer_part() mirror
its hasAnswer/isAnswerPart to strip answer-less assistant turns, then
_client_shape_messages() sends only the trailing message when it is a fresh
user turn. A resume whose trailing turn carries a settled HITL answer (not
a user turn) keeps the full history, matching agentRequest.ts's
`lastMessage?.role === "user"` guard, so the answer still binds to its tool
call. Record client_shape in results.json.

Add four unit tests against _client_shape_messages() covering: full is a
no-op, last-message produces exactly one message for a fresh user turn, a
HITL resume keeps full history, and an answer-less assistant turn is
stripped before the trailing-turn check.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
Both cells assumed a local sandbox: sandbox-gone found and killed the
sandbox-agent process via docker exec into the runner container, and
codex-child listed processes the same way. Neither works for --sandbox
daytona, where the sandbox is a remote machine docker exec cannot see
or touch (confirmed live: sandbox-gone reported "no process to kill"
and codex-child reported "never observed the child process").

Add DaytonaAwareHooks (subclasses DockerComposeHooks, reusing its
Postgres/runner-container hooks) that ends the sandbox and lists its
processes through the same Daytona REST API the runner itself uses:
DELETE /sandbox/{id} (what Sandbox.delete() calls) for kill_sandbox,
and GET /sandbox/{id}/toolbox-proxy-url + POST {proxy}/process/execute
with the exact `ps -eo pid=,ppid=,etimes=,args=` reap-exec.ts uses for
sandbox_procs. Every call is scoped to the one sandbox id the cell
observed for its own session (sandbox_ids(session_id)), never a list
or wildcard. Credentials come from AGENTA_RUNNER_DAYTONA_API_KEY/URL,
export only, never logged.

Pulled hook selection into select_hooks() so the provider switch is
unit-testable without a live stack; both cells keep their existing
PASS/FAIL rules. Not run against a live Daytona sandbox yet (the stack
is being redeployed with product fixes first) - the toolbox-proxy-url
shape was verified by reading the SDK/API-client source rather than
a live call, so treat the first Daytona run with these hooks as also
validating the hooks themselves.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
…-stop

A recalled codeword after a runner restart is not proof the native
harness session resumed: the runner can recover it by reconstructing
the conversation from persisted records (the [reconstruct] path) even
when the native session itself never hydrated, so recall alone is a
false pass for the thing this cell claims to test.

Require the recall AND one independent signal: the sandbox id after
the restart equals the one before (no rebuild happened), or the
runner log for the resume shows session/load ... loaded=true (a
genuine native hydrate). Neither present -> FAIL "native session not
resumed, recovered by transcript replay".

Also force this cell's resume onto --client-shape last-message (the
shape the desktop actually sends) regardless of the run's own
--client-shape, so a future change to this cell's message
construction can't quietly reintroduce a client-side replay that
papers over the same gap.

Pulled the PASS rule into a pure _judge_restart_after_stop(), mirroring
_judge_runner_gone, so it is unit-testable without a live stack. 6 new
tests.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
…runner-gone

The repeat-stop false pass (2026-09-04, session 190e9118): its
session_commands row was stuck `claimed` forever with zero
session_executions rows, yet every other assertion the cell made (one
terminal trace record, a warm resume) still passed. Only
_judge_runner_gone checked command settlement; every other
Stop-issuing cell only ever looked at the trace/tracing-record layer,
which can look healthy while the durable command/execution layer never
actually closed the Stop out.

Add assert_command_settled(): polls up to 20s for the session_commands
row to reach applied/obsolete (never left pending/claimed) and for
exactly one session_executions row to exist with a non-empty terminal
outcome. A hookless run (NullHooks) reads as settled=True so an
HTTP-only cell keeps running against any deployment. Extracted
_match_stop_command() (the "find the command row this Stop produced"
lookup) out of cell_runner_gone/cell_runner_gone_late so it is shared,
not duplicated a third time.

Wired into every Stop-issuing cell where a genuine in-flight execution
is expected to settle: stop-warm, stale-stop (the real bare-stop, not
the intentionally-refused stale one), stop-approval, post-stop-row,
codex-child, repeat-stop, concurrent-stops (checked per-session, in
parallel). Deliberately NOT wired into:
- stop-after-finish and stop-during-completion: both race a Stop
  against an ALREADY-naturally-finished turn, where "obsolete" with NO
  execution row is the documented healthy outcome (verified in
  Postgres: a naturally-completed turn with no Stop leaves zero
  session_executions rows) — the strict check would false-fail these.
- runner-gone / runner-gone-late: already have a stronger, more
  specific settlement check via _judge_runner_gone, on their own much
  longer --sweep-wait timeline; a rigid 20s check would false-fail a
  cell that is DESIGNED to take longer than that.
These four are flagged in the PR/report for a maintainer call rather
than silently guessed at.

13 new unit tests for assert_command_settled/_match_stop_command,
including the exact repeat-stop scenario reproduced with a stub hook
(stuck claimed, zero execution rows -> FAILS with a reason naming the
stuck state). 46/46 total pass.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
… 1.5x Pi

concurrent-stops hit an httpx.ReadTimeout on the Claude Code harness:
the SSE stream client used a flat 600s timeout regardless of harness,
and Claude Code/Codex (agentic CLIs behind an ACP bridge) can
legitimately take longer per turn than Pi under load. A driver
timeout that is too tight for a harness gets misread as a product
failure.

Add STREAM_TIMEOUT_S, a per-harness-kind table (cfg["harness"]["kind"],
the same key HARNESSES sets): Pi stays at 600s, Codex and Claude Code
get 900s (about 1.5x). invoke()'s httpx.Client now reads its timeout
from stream_timeout_s(cfg) instead of a hardcoded constant. Unknown or
missing harness kind falls back to the Pi budget.

This does not paper over the settlement bug the same run actually hit
(session 190e9118, and the concurrent-stops sessions from run
20260904-121933-225924: a command stuck `claimed` forever, tracked
separately) — assert_command_settled (previous commit) still catches
that within its own fixed 20s regardless of harness, well before this
timeout would ever matter. This change is about not confusing a
too-tight driver timeout with a product defect on a harness that is
simply, legitimately slower.

3 new unit tests. 49/49 total pass.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
…cell

The sandbox-gone cell killed the wrong process. On local it ran `ps | grep
"sandbox-agent server"` and killed EVERY match, but the keep-alive pool keeps
other sessions' parked daemons alive in the same runner container, and two
sessions can share one mount key. In the final matrix it killed a different,
idle session's parked sandbox; the tested turn ran on and completed normally,
so the cell recorded a false negative against a healthy product.

Map the tested session to its OWN sandbox instead:
- Derive the session's sandbox port from the runner log `prepare_workspace`
  line for that exact session id, cross-checked against the turn ledger's
  `local/<host>:<port>` id; a disagreement is refused as ambiguous.
- Resolve the pid listening on that port inside the runner container (`ss`,
  with a /proc/net/tcp + /proc/*/fd fallback for an image without `ss`), and
  assert the pid's cmdline is a sandbox-agent daemon before killing it.
- Refuse with WrongSandboxTarget (a `wrong target` cell failure) whenever the
  mapping cannot be made, so the driver never kills a guess. The port and pid
  are recorded in the cell evidence.
- Daytona already addressed its remote sandbox by id; that path now shares the
  same per-session entry point and refuses when no id was observed.

Derive the wait window from the runner's sandbox-liveness probe defaults
(three failures at the probe interval, plus slack) and never wait less than
the slow command, whose duration the cell prints. This stops a slow-but-healthy
turn from being misread as "still running".

Add unit tests for the port parsing, the ss pid parsing, the port-to-pid
mapping, and every wrong-target refusal, all with the container calls mocked.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
Run 1b refused correctly but for the wrong reason: it read the port once, right
after the turn started, while a cold acquire on the gate stack takes about 35 s
to write the session's `prepare_workspace` line (`acquire_total ms=34766`), so
the line was not there yet and the cell gave up.

Poll instead. `wait_for_sandbox_ready` polls the runner log and the turn ledger
for this session's own sandbox for up to 120 s, then the cell waits a few more
seconds so the slow command is running, then resolves the pid and kills. A
transient log/ledger disagreement during acquire is retried, not fatal; the
refusal is kept for the case where the line never appears within the wait. The
Daytona path polls for the remote sandbox id with the same budget.

Derive the slow command's duration from the parts it must outlast: the acquire
budget, the resolve poll window, and the probe's design window, plus margin
(300 s). The cell prints the command duration, the resolve timeout, and the
resolve time it actually took.

Add unit tests for the poll (the line appears on the third read), the timeout
refusal, the ledger fallback, and the command-duration invariant, all with the
clock and the container calls mocked.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
Run 2 of runner-gone-late failed as a race, not a product defect. The Stop
settled cleanly (command applied/stopped, is_running false, stopping_turn_id
null), but the recovery Send fired while the runner was taking SIGTERM from the
cell's own restart, so the API returned "All connection attempts failed" and
new_message_ran was false. The cell restarted the runner and then sent without
waiting for it to come back.

Poll the runner's health after the restart and before the Send. A new
`_recover_then_send` helper polls `runner_healthy()` (one Docker health check)
until healthy, bounded to 60 s at 2 s, and only then issues the Send; if the
runner never recovers it does not send a doomed request and the cell fails with
a clear reason. `wait_for_runner` now shares the same single-shot health check.

Add unit tests: the Send is not issued until the health poll returns healthy
(fail twice, then succeed, asserting call order), and no Send is attempted when
health never recovers. Both use a mocked clock.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
Run 2b of runner-gone failed as a race, not a product defect. The cell read
is_running after it unpaused the runner, and the returning runner started a new
turn on the same session that legitimately set is_running true (watchdog settled
the lost turn and cleared is_running at 14:03:51 while paused; unpause at
14:03:59; a new turn at 14:04:09 set is_running true; the driver read that true).

Measure "runner gone" while the runner is still gone. A new
`_measure_runner_gone_while_paused` helper pauses the runner, fires the Stop,
polls the durable rows until the Stop command settles obsolete/applied+lost (or
an execution row carries a terminal outcome), then reads is_running from
session_streams WHILE STILL PAUSED, and only then unpauses. The gone-and-stays
-gone verdict asserts settled-lost, a watchdog execution_lost ending, and
is_running false — all from the paused reads. The paused-read and settle
timestamps are recorded.

The unpause and the following Send are now only a restore step plus an OPTIONAL,
separately recorded resumability check (via the health-gated _recover_then_send);
they are no longer part of the runner-gone pass.

Add unit tests that the is_running read happens between pause and unpause (mocked
pause/unpause and DB reads), and that a no-settle window still unpauses and still
reads while paused. Both use a mocked clock.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
…d settlement check

Matrix run 4 (Claude Code local) failed stale-stop with "expected exactly one
session_executions row for the stopped session, saw 0". Post-hoc the shape was
correct: the valid Stop returned 202, but the fast Claude Code turn had already
finished naturally, the runner had nothing to cancel, session_commands settled
obsolete/not_running, and zero execution rows is right for that case.

Teach the shared assert_command_settled about it: when the Stop command settles
obsolete with outcome not_running, accept zero execution rows, set
natural_finish=True, and record "stop landed after a natural finish". Keep the
strict exactly-one-row requirement when the outcome is stopped or lost. Because
stale-stop and the other Stop-issuing cells route their settlement check through
this one function, they all inherit the correct behavior;
stop-after-finish and stop-during-completion already accepted this shape.

Add a unit test for the not_running shape and a companion test that a real
stopped outcome with zero rows still fails; keep the existing strict tests.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
@mmabrouk
mmabrouk marked this pull request as ready for review September 4, 2026 15:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Team

Run ID: 6f7a2183-5c0d-494a-9d0d-dd325d48b729

📥 Commits

Reviewing files that changed from the base of the PR and between ded2cc3 and 97b42fd.

📒 Files selected for processing (4)
  • .agents/skills/agent-release-gate/SKILL.md
  • .agents/skills/agent-release-gate/resources/path_triggers.py
  • .agents/skills/agent-release-gate/resources/session_control.py
  • .agents/skills/agent-release-gate/resources/test_session_control.py

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.

Comment thread .agents/skills/agent-release-gate/resources/path_triggers.py
Comment thread .agents/skills/agent-release-gate/SKILL.md Outdated
@mmabrouk mmabrouk added the lgtm This PR has been approved by a maintainer label Sep 4, 2026
Require a complete standalone session-control artifact whenever path rules make the driver mandatory. Carry any recorded failure into the product gate exit code.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Describe the eight hook-dependent cells separately from the stop-after-finish abort-log subcheck. Document how the standing gate consumes the standalone result.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Apply Ruff 0.15.12 formatting to the new release-result coverage.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
@mmabrouk
mmabrouk changed the base branch from main to feat/session-control September 4, 2026 16:22
@mmabrouk
mmabrouk merged commit dac7bdf into feat/session-control Sep 4, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant