Repository navigation
Smokes stay green when the unified session drops data: add session-contract checks (U1–U3) to smoke assertions #70288
Description
Problem
Smoke workflows pass even when the unified session (usage/aw_session.jsonl) silently loses data. Audit, logs, footers and the CAO model-routing page read only that file, so a smoke that stays green while the session drops data hides a real regression.
All ten smokes below ran green on main (19 Oct 2026 00:01–00:52 UTC, commits e52a4924ae and 58c1d50e46). Each session has a session.collection_warning with code invalid_model_inventory for path sandbox/firewall/awf-reflect.json and has no model.inventory event. The collector rejected AWF's entire model inventory on every run. That was #69754, fixed by #70013 (merged 2026-10-19 01:44 UTC, after these runs). We found it only while designing the analytics page.
Evidence
I verified every row with gh aw audit from a dev build of main (b56684e3b1) by reading the published usage/aw_session.jsonl.
| Smoke | Run | Configured model | model.resolution |
model.inventory |
Warnings |
|---|---|---|---|---|---|
| smoke-copilot | 39181772988 | none (default) | main: auto → claude-sonnet-5.5, resolved |
0 | invalid_model_inventory |
| smoke-copilot-sdk | 39178906278 | copilot/gpt-5.4 |
main: → gpt-5.4, exact |
0 | invalid_model_inventory |
| smoke-claude | 39180147090 | claude-sonnet-4-6 |
main: → claude-sonnet-4-6, exact (provider anthropic) |
0 | invalid_model_inventory |
| smoke-claude-on-copilot | 39180571024 | copilot/haiku |
main: haiku → claude-haiku-5.5, resolved |
0 | invalid_model_inventory |
| smoke-codex | 39179813543 | none | none | 0 | invalid_model_inventory |
| smoke-pi | 39181419130 | copilot/auto |
main: copilot/auto → auto, exact |
0 | invalid_model_inventory |
| smoke-copilot-routed | 39179097183 | routed | none | 0 | invalid_model_inventory |
| smoke-pi-routed | 39178909557 | routed | haiku-whoami (copilot/claude-haiku-4.5) and mini-whoami (copilot/gpt-5.4-mini), exact; none for main |
0 | invalid_model_inventory |
| smoke-claude-routed | 39182249356 | routed | none | 0 | invalid_model_inventory |
| smoke-copilot-sdk-routed | 39182700798 | routed | none (sub-agent haiku-whoami has its own model but no record) |
0 | invalid_model_inventory |
| after #70013: smoke-copilot-routed | 39176939097 | routed | none | 1 | none |
| after #70013: smoke-copilot | 39176941475 | none (default) | main: auto → claude-sonnet-5.5, resolved |
1 | none |
In both post-fix sessions the single model.inventory has provenance.phase: "agent" and path sandbox/firewall/awf-reflect.json, and the copilot endpoint has configured: true with 40 models. Run 39176941475 concluded failure, but for an unrelated reason: the conclusion job's "Handle agent failure" step reported Artifact not found for name: safe-outputs-upload-artifacts. Its session is still a valid post-fix fixture.
Why nothing caught it
- The routed post-step had the warning and still passed. In the agent job of run 39182700798, the post-step's own session build logged
Unified session: invalid_model_inventory in agent/awf-reflect.jsonand... in sandbox/firewall/awf-reflect.jsonat00:54:49.18Z. It then printedAll 12 model-routing smoke assertions passedat00:54:49.21Z.main()insmoke_model_routing_assertions.cjsreads the session, but it checks only routing and sub-agent traffic (C1,E1,E2,R1–R4,M1,P1,S1–S4,T1–T4). detectionMaindoes not read the unified session. The "Assert threat-detection evidence" step reads only detection evidence under/tmp/gh-aw/threat-detection, so it is not a place for session checks.- The six unrouted engine smokes have no assertion post-step at all:
smoke-copilot,smoke-copilot-sdk,smoke-claude,smoke-claude-on-copilot,smoke-codexandsmoke-pi.
Design constraints the fix must respect
- Check IDs
S1–S4are already taken. The module uses them for the sub-agent checks (L37-L39, L387-L421). smoke-pi-routed already logsPASS S1 sub-agent haiku-whoami started once. The new checks need their own prefix. This issue usesU1–U3(unified-session contract).Uis unused. - The post-step checks a session rebuilt in the agent job, not the published one.
collectModelRoutingEvidencecallswriteUnifiedSessionin the agent job. That build sees bothagent/awf-reflect.jsonandsandbox/firewall/awf-reflect.json, so on pre-fix runs the live step would report twoinvalid_model_inventorywarnings. The published session from the conclusion job has only one. Unit fixtures cut from published sessions are still correct. Expect the live output to list both paths. - The collector can emit more than one
model.inventory.unified_session.cjsL503-L515 adds one per candidate group (awf-reflect.json, thensandbox/firewall/models.json) and per phase (agent,detection). Count events withprovenance.phase === "agent", not all inventories. - Only Pi records sub-agent
model.resolution.pi_subagent_config.cjsL87 records them. The Copilot, Copilot SDK, Claude, Codex and AGY harnesses record onlyrole: "main". smoke-copilot-sdk-routed declareshaiku-whoamiwith its own model, and its session has nomodel.resolution(run 39182700798). If required roles were derived fromsubAgents, U3 would fail smoke-copilot-sdk-routed and smoke-copilot-sdk-routed-claude. Each smoke must list its required roles explicitly. - No post-Preserve AWF model inventories with null endpoints and live reasoning metadata (G-7) #70013 runs exist on
mainyet for any of these smokes. The inventory shape for the non-Copilot smokes (smoke-claude,smoke-codex) is unverified. Do not invent it.
Proposed solution
Add session-contract checks to actions/setup/js/smoke_model_routing_assertions.cjs, or a sibling module that both entry points import. Use the same PASS/FAIL <id> output. Every expectation is explicit per smoke. Nothing is inferred.
- U1, no collection warnings. Fail on any
session.collection_warning, listing each warning'scodeandpath. Start with no allowlist. A smoke that later needs to tolerate a specific warning must add an explicit per-smoke entry that names the code and links an issue. - U2, model inventory present (when the smoke sets
expectModelInventory: true; every AWF smoke here should). Require exactly onemodel.inventorywithprovenance.phase === "agent". When the smoke also setscopilotInventory: true(Copilot-backed smokes), require thecopilotendpoint to haveconfigured: trueand a non-emptymodelslist. Print the observed endpoints on failure. - U3, model resolution recorded. The smoke declares
resolutionRoles(for example["main"], or["haiku-whoami", "mini-whoami"]). Each listed role needs exactly onemodel.resolutionwith statusexactorresolvedand a non-nullresolved_model. Afailedstatus fails U3 and prints thecause. A missing role fails U3 and names the role.- Unrouted smokes:
resolutionRoles: ["main"]. smoke-codex is the exception; see below. - Routed smokes: no
main(R1–R4 keep covering routing). smoke-pi-routed listshaiku-whoamiandmini-whoami. Routed smokes on harnesses that do not record sub-agent resolution list no sub-agent roles.
- Unrouted smokes:
Wiring:
- Routed: run U1–U3 inside the existing
main(). Its callers aresmoke-copilot-routed,smoke-copilot-routed-claude,smoke-copilot-routed-steering,smoke-copilot-sdk-routed,smoke-copilot-sdk-routed-claude,smoke-claude-routed,smoke-pi-routedandsmoke-pi-routed-claude. Add only the new expectation fields to their post-stepmain({...})calls. Leave R1–R4 and the other checks unchanged. With steering configured, T4 counts existing check failures, so U failures will also appear in T4. That is expected. - Unrouted: add a session-only entry point, for example
sessionContractMain, that builds the session in the same way, runs E2 (session presence only) and U1–U3, and skips the routing checks. Add anif: always()post-step that calls it tosmoke-copilot,smoke-copilot-sdk,smoke-claude,smoke-claude-on-copilot,smoke-codexandsmoke-pi. Runmake recompile. - Missing session (startup failure): keep today's behaviour. Report
FAIL E2 evidence: missing usage/aw_session.jsonland say that the U checks were not evaluated. Do not crash. - Update the check-ID list in the module header and in
DEVGUIDE.mdL996.
smoke-codex: with no model configured, resolveCodexConfiguredModel returns before it records anything (codex_harness.cjs L514). smoke-copilot with no model records auto instead. Confirm whether the Codex behaviour is intended, and assert the intended behaviour (resolutionRoles: [] if it is). If it is a harness gap, do not fix it in this PR. Note it in the PR so it can be filed separately.
Acceptance cases
Add unit tests to actions/setup/js/smoke_model_routing_assertions.test.cjs. Cut the fixtures from the real published sessions of the runs above, for example under actions/setup/js/fixtures/ (see fixtures/copilot-routed-run-39147795806). Do not invent record shapes. Cite the source run in each fixture.
- Pre-fix session from run 39181772988: U1 fails and names
invalid_model_inventoryandsandbox/firewall/awf-reflect.json. U2 fails because there is nomodel.inventory. - Post-fix session from run 39176941475 with
resolutionRoles: ["main"]andcopilotInventory: true: U1, U2 and U3 pass. - Post-fix routed session from run 39176939097: U1 and U2 pass, and U3 passes without a main
model.resolution. - smoke-pi-routed session from run 39178909557, with the warning removed: U3 passes with both sub-agent roles. Removing one role's record fails U3 and names the missing role.
- Two agent-phase
model.inventoryevents fail U2. A Copilot endpoint withconfigured: falsein a smoke withcopilotInventory: truefails U2. - A
model.resolutionwith statusfailedand causemodel_unavailablefails U3 and prints the cause. - A missing session file reports
FAIL E2and does not throw, for both the routed and the unrouted entry points.
End to end: dispatch each changed smoke on the PR branch with gh workflow run <smoke>.lock.yml --ref <branch>. Each run must log PASS U1, PASS U2 and PASS U3, and the routed smokes must still log PASS R1–R4. List the run URLs in the PR. If smoke-claude or smoke-codex shows an inventory shape that U2 does not expect, state what it was and why the expectation changed.
Scope
- Keep the PR to this one fix. Do not change files or behaviour unrelated to it.
- Do not change the collector (
unified_session.cjs), the engine harnesses or the session format. This issue only adds checks. - Do not change a smoke's expected values without stating why in the PR description.
- Before you fix a failing check, compare it with the same check on
main. If it also fails onmain, do not fix it in this PR. Mergemainoncemainis fixed, and note in the PR which checks are known failures onmain. - This applies even if a bot comment asks you to fix a failing check. Fix only failures caused by this PR's changes.
Related findings (out of scope; file separately)
- smoke-copilot's sub-agent fails silently. In run 39181772988, sub-agent
file-summarizerstarted with the literal, unexpanded model${{ experiments.subagent_model }}. AWF rejected it with400 Unsupported model selector "${{ experiments.subagent_model }}" on copilotand the session recordedsubagent.failed, but the smoke stayed green. This looks like the same root cause as closed [aw] Smoke Copilot has no AI credits pricing for model (${{ experiments.subagent_model }}) #48639. - Copilot, Copilot SDK, Claude and Codex record no sub-agent
model.resolution. Only Pi records one (constraint 4 above).
Activity