Skip to content
49 changes: 45 additions & 4 deletions .agents/skills/agent-release-gate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export AGENTA_API_KEY=... # project API key
uv run resources/qa_product.py --all --custom-slug <vault-slug> # every cell, every journey
uv run resources/qa_product.py --cell P1 # one cell
uv run resources/qa_product.py --cell C1 --only chat # one journey
uv run resources/qa_product.py --cell C3 --only records --only sessions # the sessions-storage journeys
uv run resources/qa_product.py --cell C3 --last-message-only # minimal-history differential run
```

Paths are relative to this skill's directory. The deployment's vault must hold the provider keys
Expand All @@ -55,13 +57,14 @@ cell — user MCP is Claude-only). Any `FAIL` blocks the release until triaged.
The runtime **fails open**: a component can break, get logged, and the turn still succeeds with a
normal-looking answer. A green turn is therefore not proof on its own. Before trusting a pass,
read `resources/LESSONS.md` — every trap there produced a green test that proved nothing. The two
that bite hardest: replay conversation history byte-faithfully (tool parts included) or every turn
silently goes cold, and re-run any prior blocker-level finding after a redeploy before believing it.
that bite hardest: a full-history client must replay conversation history byte-faithfully (tool
parts included) or every turn silently goes cold — a last-message-only client is exempt by design,
see LESSONS #1 — and re-run any prior blocker-level finding after a redeploy before believing it.

## Resources (read on demand)

- `resources/coverage.md` — the cells (harness × sandbox × auth) and journeys (chat, mount, tool,
approve, deny, commit, warm, mcp) with a one-line meaning for each.
approve, deny, commit, warm, mcp, records, sessions, followup) with a one-line meaning for each.
- `resources/LESSONS.md` — the traps. Read before writing or trusting any agent QA test.
- `resources/qa_product.py` — the gate driver (cells × journeys).
- `resources/qa_probe.py` — a one-turn wire probe: `uv run resources/qa_probe.py` confirms the
Expand All @@ -71,4 +74,42 @@ silently goes cold, and re-run any prior blocker-level finding after a redeploy
- `resources/seeds/` — representative green `results.json` files kept as regression-seed references.

Release-night findings and the full evidence history are archived in
`docs/design/agent-workflows/projects/qa/` (STATUS.md, findings.md, matrix.md).
`docs/design/agent-workflows/projects/qa/` (findings.md, matrix.md, README.md).

## Sessions rework (v0.106) addendum

The gate above predates the sessions-storage rework (`feat/sessions-storage-rework`, v0.106.x) and
does not yet exercise its flag-gated paths — see `resources/coverage.md` for the exact list of
what is not covered.

**The four flags.** Parsing is NOT uniform across them — check the literal value, not just
whether the variable is set:

- `AGENTA_SESSIONS_RECONSTRUCT` (runner) — rebuilds prior turns from the durable record log for a
minimal-history request. Accepts ONLY the literal string `"true"` (case-insensitive); `1`,
`yes`, `on` silently do nothing (`reconstruct-history.ts` `reconstructEnabled`).
- `AGENTA_RECORDS_DURABLE` (runner) — stronger retry + drop-counting on record persistence. Same
literal-`"true"`-only parsing (`sessions/persist.ts` `durableRecordsEnabled`).
- `NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY` (web) — sends only the trailing user message on a fresh
turn instead of full history; a HITL resume still sends full history. Broader truthy parsing
(`true`/`1`/`t`/`y`/`yes`/`on`/`enable`/`enabled`), unlike its runner counterpart. **Must be
flipped together with `AGENTA_SESSIONS_RECONSTRUCT`** — nothing enforces the pairing at runtime,
so web-on/runner-off silently loses all context on every cold turn (the client sends one
message; the runner has no reconstruction to fall back on).
- `AGENTA_RECORDS_SMART_TRUNCATION` (API) — preserves record structure instead of dropping an
oversized record body wholesale. Same broader truthy parsing as the web flag
(`api/oss/src/utils/env.py` `SessionsRecordsConfig`).

**Differential QA, not verdict QA.** A flags-on run and a flags-off run against the same stack each
report their own PASS/FAIL and tell you nothing about each other. The method that actually finds
defects here: run the gate **twice against ONE flags-on stack**, changing only the client's history
mode (full history vs. last-message-only), and diff the message arrays the runner hands the model,
turn by turn. The defects live in the diff — a dropped tool part, a duplicated turn, turns
reconstructed in the wrong order — not in either run's verdict.

**Release QA plan.** The concrete plan for this release (target stack, flag matrix, division of
labor) is `docs/design/agent-workflows/projects/qa/release-2026-07-sessions-storage-rework.md`.

**Caution.** A green flags-off run says nothing about the flags-on path, and vice versa — they
exercise different code (server-side history reconstruction, client-side history truncation). Run
both before shipping a release decision; never extrapolate one to the other.
46 changes: 33 additions & 13 deletions .agents/skills/agent-release-gate/resources/LESSONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,49 @@ your test client behave EXACTLY like the real frontend, or you are testing your

---

## 1. The test client must replay history byte-faithfully, or every turn silently goes COLD
## 1. The test client must replay history byte-faithfully — unless it deliberately sends minimal history instead

**The trap.** Our driver replayed each assistant turn as a text-only message
(`{role:"assistant", parts:[{type:"text",...}]}`), dropping the assistant's **tool parts**.

The runner fingerprints the conversation over **(ordered user texts, ordered deduped tool-call
ids, user-turn count)** — `session-pool.ts:226` `historyFingerprint`, and `:252`
`expectedNextHistoryFingerprint`, which folds in the tool-call ids the runner emitted last turn.
A replay with no tool-call ids therefore **cannot match** after any tool-using turn:
ids, user-turn count)** — `historyFingerprint` and `expectedNextHistoryFingerprint` in
`services/runner/src/engines/sandbox_agent/session-identity.ts:212` and `:238`, the second folding
in the tool-call ids the runner emitted last turn. A replay with no tool-call ids therefore
**cannot match** after any tool-using turn:

```
[keepalive] mismatch (history) key=…; evict + cold
```

**Why it poisons everything.** Every turn goes cold → a fresh harness process → the runner replays
a hand-rendered transcript instead of the harness's real context. So:
**Since the sessions rework, this only binds a full-history client.** When the request carries
minimal history — exactly one message, a fresh user turn, no approval envelope
(`carriesMinimalHistory`, same file, `:302`) — the keepalive check **skips the history-fingerprint
comparison entirely** (`server.ts:603` `clientAssertsHistory`) and, with
`AGENTA_SESSIONS_RECONSTRUCT=true`, the runner rebuilds prior turns from the durable record log
instead (`reconstruct-history.ts`). The client is no longer asserting the conversation at all, so
there is nothing to fingerprint-match against.

**Why a full-history driver still poisons everything if it gets this wrong.** Every turn goes cold
→ a fresh harness process → the runner replays a hand-rendered transcript instead of the harness's
real context. So:
- warm/cold numbers are meaningless (nothing was ever warm),
- **compaction never triggers** (the harness context never accumulates), so a long-context /
"loses information" test can pass while testing nothing at all.

**The rule.** Echo back the **full** assistant `UIMessage.parts` — text parts *and* `tool-<name>`
parts with `toolCallId`, `input`, `state`, `output` — exactly as the AI SDK does
(`web/packages/agenta-playground/src/state/execution/agentRequest.ts:401`). If your driver
synthesizes assistant turns, it is not testing the product.
**The rule.** In full-history mode (the default, and the only mode until
`NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY` is on): echo back the **full** assistant `UIMessage.parts`
— text parts *and* `tool-<name>` parts with `toolCallId`, `input`, `state`, `output` — exactly as
the AI SDK does (`web/packages/agenta-playground/src/state/execution/agentRequest.ts:402-413`). In
last-message-only mode, send exactly the trailing user message and nothing else — a driver that
sends "most of" the history (say, the last two turns) satisfies neither mode and mismatches either
way. If your driver synthesizes assistant turns without knowing which mode it is testing, it is not
testing the product.

**The tell.** `grep 'mismatch (history)'` in the runner log. If it fires on turns your client
believes are warm, your client is the bug.
believes are warm, your client is the bug — unless the client is deliberately minimal-history, in
which case the fingerprint check never runs and this grep is the wrong tell; grep `[reconstruct]`
instead.

## 2. Never assert on the model's prose. It will lie to you.

Expand Down Expand Up @@ -154,7 +170,9 @@ F-9 ("Claude harness never resumes its native session") was CONFIRMED across 72h
and triaged as a release blocker. A deployment repair landed later the same day, pulling in recent
upstream fixes. Nobody re-ran F-9 against the rebuilt stack before trusting it — until a decisive
cold-context experiment on 2026-07-14 showed native session resume now working 4/4 runs, downgrading
F-9 to a residual resilience concern (see STATUS.md).
F-9 to a residual resilience concern. (The original STATUS.md write-up of this downgrade did not
survive the later docs consolidation into `findings.md`/`matrix.md`; this paragraph is the
surviving record.)

**The trap.** A deployment under active repair invalidates earlier observations made against it.
Once the repair lands, the finding is stale, not necessarily wrong — but you don't know which
Expand Down Expand Up @@ -205,7 +223,9 @@ wire: a `tool-output-available` frame for a tool named `mcp__<server>__<tool>`.
1. `docker ps` — is anything restarting? If yes, wait.
2. Does the runner have its harness dirs (`/pi-agent`)? Is it root or not?
3. Drive the **product path** (`/services/agent/v0/invoke`), not the service `/invoke`.
4. Echo history **faithfully** (tool parts included), then confirm `hit-continue` in the log.
4. In full-history mode, echo history **faithfully** (tool parts included), then confirm
`hit-continue` in the log; in last-message-only mode, send just the trailing user message and
confirm `[reconstruct]` instead.
5. Assert on frames + side effects. Never on prose.
6. After every capability passes, grep the log for silent degradation.
7. Re-run anything that failed once before reporting it.
Expand Down
28 changes: 28 additions & 0 deletions .agents/skills/agent-release-gate/resources/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,37 @@ cell — keep them in sync if a cell changes.
| `commit` | Save an agent config as a new workflow revision, then fetch it back. | The changed parameter survives the round trip and the version bumps (v0 seed → v1; see LESSONS #14). Harness-agnostic — it drives the config REST API, not a turn. |
| `warm` | Run three turns, watch latency and the runner log. | Turns 2-3 are faster and the log confirms the session was genuinely **loaded**, not silently cold. |
| `mcp` | Deliver an MCP server in the agent config and call one of its tools. | A `tool-output-available` frame fires for an `mcp__*` tool. **Claude only** — Pi rejects user MCP, so this `SKIP`s on every Pi cell. Uses the public DeepWiki server by default; override with `--mcp-url`. |
| `records` | Force a tool call, then poll `POST /sessions/records/query` (ingestion is async, worker-drained off Redis). | Record types cover a user message, an assistant message, a `tool_call`, and a `tool_result`; `timestamp` is non-decreasing in returned order; the unguessable bash token appears inside a `tool_result` body; no record is the bare `{"_truncated": true}` legacy drop-in. Harness-agnostic, like `commit`. |
| `sessions` | REST lifecycle over `/api/sessions/*`: create (one cheap turn), list, archive, unarchive, rename (`PUT /sessions/streams/header`), delete. | Each step's effect on `POST /sessions/query` is exactly right: archived hides by default and shows with `include_archived`; unarchive restores it; rename shows in the next query; delete is a real hard delete — gone even with every include flag on. Harness-agnostic, like `commit`; cleans up on every path. |
| `followup` | After an approved resume settles, send ONE more normal user turn on the same session forcing a second tool call. | The followup gets a fresh wire `toolCallId` (never the gated call's) and its own durable `tool_call` record — no `record_id` is shared between the two calls. Probes an open defect prediction (2026-07-24 review: a fresh post-approval turn could silently collide/overwrite the approved call's record) that was never exercised before; live-verified clean in both full-history and `--last-message-only` modes on 2026-07-28. |

**`--last-message-only`** (a global flag, not a journey): mirrors the frontend's minimal-send switch
(`NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY` / `agentRequest.ts:401-415`) — a fresh user turn sends
only its trailing message instead of full history, while an approval resume still sends full
history, exactly like the browser. Every turn's exact `sent_messages` lands in `results.json` so a
full-history run and a `--last-message-only` run against the same stack can be diffed offline.

Triggers are deliberately **out of scope** for this gate.

## Not covered (sessions rework, as of 2026-07-28)

These feature areas shipped in `feat/sessions-storage-rework` and have no journey in
`qa_product.py` yet. Listed here so the gap is explicit rather than assumed away — flip a row to
covered once a journey lands, do not delete it silently.

| Feature | Status | Note |
|---|---|---|
| Durable-records readback | covered | `records` journey (J8): polls `POST /sessions/records/query`, asserts type coverage, timestamp order, real tool-result content, and no bare-truncated bodies. |
| Last-message-only client mode | covered | `--last-message-only` global flag mirrors `NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY`'s minimal-send condition and dumps each turn's exact `sent_messages` for an offline diff against a full-history run. |
| Sessions REST surface (query/archive/rename/delete/revive) | covered | `sessions` journey (J9): create → query → archive → unarchive → rename → delete, asserting each state transition's effect on `POST /sessions/query`. |
| Cold-replay approval resume | not covered | The paused-turn + resume transcript fold is UI-side; no wire-level journey exercises it. |
| Batch approvals | not covered | Approve-all/Deny-all with context peek is UI-side; the `approve`/`deny` journeys are single-gate only. |
| Warm Stop | not covered | Cooperative cancel that leaves the session resumable (sandbox destroyed) — no journey. |
| Steer | not covered | Deny + redirect, behind `NEXT_PUBLIC_AGENT_CHAT_STEER` — no journey. |

See `docs/design/agent-workflows/projects/qa/release-2026-07-sessions-storage-rework.md` for the
full flag-gated risk list this table is a slice of.

## Optional probes (`qa_longctx.py`)

Separate from the gate, these need live **Gmail and GitHub Composio connections** in the target
Expand Down
Loading
Loading