Complete runner/collector-written routing analytics in aw_session.jsonl (G-1/G-5/G-6/G-9/G-12) #70668
Description
Problem
Complete the runner- and collector-written routing analytics record in usage/aw_session.jsonl in one focused change: configured policy, counterfactual baseline pricing, harness/run identity, and identifier-only upstream/request errors.
These are the remaining G-1, G-5, G-6, G-9 and G-12 items from the Data gaps section of #69507. #69725 and #69754 are closed; do not reopen their candidate-ranking/request-context and inventory work. The inspected sessions already retain ranked choices and model inventories.
The CAO model-routing page in githubnext/gh-cao reads unified sessions, not the separate agent artifacts. Its adapter already recognizes model.config, model.baseline_estimate and firewall.upstream_error; the queries consume the routing record. This issue is about supplying that record, not changing CAO.
Verified evidence: October 20, 2026
Audited all three supplied runs on Windows using a development binary built from gh-aw main at 70e15c7ff659c05c2f4466aa172bb910d28847a2, reporting dev-70e15c7ff6, rather than the installed stable v0.89.21:
gh-aw-main.exe audit <run-id> --repo github/gh-aw --no-baseline --timeout 4m --artifacts usage,agent,info --output <per-run-directory> --json
All three audit commands completed successfully; that is collection success, not the workflows' conclusions. Read each usage/aw_session.jsonl, the original agent artifact's final runtime/aw_info.json / agent/aw_info.json, compiled workflow configuration, and firewall evidence. In these artifacts the proxy JSONL files are under sandbox/firewall/logs/api-proxy-logs/, not sandbox/firewall/audit/api-proxy-logs/; sandbox/firewall/audit/awf-resolved-config.json was inspected too.
| Run / created at (UTC) | Workflow revision | Observed result |
|---|---|---|
| 39508154330, 22:26:46 | smoke-copilot-sdk-routed, main, 70e15c7ff6 |
Success, SDK, AWF v0.28.70. Selected gpt-5.6-luna on /responses; the declared claude-haiku-4.5 sub-agent completed on /chat/completions. Six ranked choices and one inventory survive in the session. |
| 39506475007, 22:13:11 | smoke-copilot, copilot/expand-experiment-model-expressions, 9c8c823adb |
Failure, CLI, unrouted, AWF v0.28.62. Main auto resolved to claude-sonnet-5.5, using the session-wide completions API. A sub-agent request for gpt-6.1-sol failed with an AWF translation 400 involving frequency_penalty and /responses. Audit reports subagent_failed. |
| 39499464895, 21:19:52 | smoke-copilot-sdk-routed-claude, copilot/fix-copilot-sub-agent-models, b9f7f48a3b |
Failure, SDK, AWF v0.28.62. Selected claude-haiku-4.5 on /chat/completions, then failed closed because declared sub-agent mini-whoami requested unavailable gpt-5.4-mini. model.resolution records role: mini-whoami, status: failed, cause: model_unavailable. No main-agent inference request followed selection. This is not the CLI translation 400. |
These failures explain why recording policy and harness identity must not depend on a successful conversation or routing selection. Fixing their runtime causes is outside this issue.
Gap verification
| Gap | What the actual sessions/artifacts contain |
|---|---|
| G-9 / G-12 | All three workflow.info records say engineId: copilot and omit engineHarness, workflowRef and serverUrl. Correction to the original design: runAttempt: "1" already exists in all three, as do workflowId, workflowCallId and callerWorkflowRef. Preserve these; do not describe attempt identity as wholly missing. workflowId already holds the executing workflow's full reference. |
| G-1 | Zero model.config events in all three. The routed run's compiled configuration has goal cost, mode economy, and allowed routing model gpt-5.6-luna (emitted to AWF as candidateModels: ["github-copilot/gpt-5.6-luna"]). Selection's resulting mode is balanced, not the configured economy. The Claude fixture likewise configures cost/economy with claude-haiku-4.5, but selection's resulting mode is unknown. The CLI fixture has no routing configuration or selection. All three actual AWF configurations have maxAiCredits: 1000, wired through the repository-default budget expression with fallback 1000, not explicit frontmatter. Policy allow/block lists, budget/source and a resolved no-routing baseline are not published as one session configuration record. |
| G-6 | Zero model.baseline_estimate events in all three. The successful routed fixture has four main-agent requests, one sub-agent request and one classifier request in agent-phase proxy accounting; the session additionally contains two detection requests. A sum over all agent-phase usage would overcount the baseline. |
| G-5 | None of the three original agent artifacts contains upstream-errors.jsonl, and none supplies the new error_code fields. The two failed runs predate the AWF feature; the v0.28.70 run has successful inference requests. Therefore these runs cannot demonstrate preservation of the new upstream fields. Source inspection establishes the lossy projection; use the upstream change's tests for this acceptance case, not an invented historical record. |
The compiled budget expression is vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000'; the observed value alone does not distinguish an equal-valued repository default from the literal fallback.
Useful original artifact links for reproducible fixtures:
| Run | usage |
agent |
|---|---|---|
| 39508154330 | 12303814398 | 12304048825 |
| 39506475007 | 12304237333 | 12303457750 |
| 39499464895 | 12300520074 | 12298393979 |
Possible solution
1. Runner-owned identity and configured policy
Update actions/setup/js/generate_aw_info.cjs and the runner step that persists model.config, with only the directly required compiler-to-runner metadata wiring.
For workflow.info, add engineHarness, workflowRef and serverUrl, and preserve/test the existing runAttempt. Emit copilot-cli versus copilot-sdk from the actual compiler/runner harness configuration, with one stable value per supported harness, never by guessing from engineId, a workflow name or model output. Capture run attempt and server URL from runner context.
Keep the executing and calling workflow references distinct. workflowRef should retain the compiler-wired executing workflow reference already represented by workflowId; callerWorkflowRef retains GITHUB_WORKFLOW_REF, which can identify a different top-level caller for reusable workflows. Do not regress the existing identity contract.
Write one model.config per run, from runner-owned aw_info.json and the compiled configuration, including when routing is disabled or selection fails:
routing: { enabled, goal, mode, allowedModels }, retaining configured policy rather than the selected classification's resulting mode.models: { allowed, blocked }, keeping policy separate from the runtime inventory and routing candidate pool.maxAiCreditsandmaxAiCreditsSource(frontmatterordefault), using the effective resolved budget and recorded configuration provenance, not inferring the source from its numeric value. Defaults may be resolved through repository variables.baselineModel: the concrete configured/default main model the run would use without routing, after existing alias resolution. This is neither the literalautoalias nor automatically the router's chosen model.
Capture baseline metadata without changing any harness's resolution or selection behavior. If a historical source cannot establish it, leave it unknown; do not relabel the routed workflow.info.model as the counterfactual baseline. Policy/model/source strings must be identifier-shaped metadata; exclude prompts, transcripts, secrets and free-form diagnostic text.
2. Collector-owned baseline estimate
In the conclusion collector, reprice each main-agent request's recorded token usage at baselineModel using the existing computeInferenceAIC in actions/setup/js/model_costs.cjs. Preserve the input/cache/reasoning accounting semantics and count a request once, not both its native and proxy accounting copies.
Emit model.baseline_estimate for routed runs, clearly marked estimated: true, with baseline identity, an AIC estimate/subtotal, priced and unpriced request counts, and explicit completeness. Exclude sub-agent, routing-classifier and detection requests. Use deterministic runner-owned harness/proxy evidence for attribution; a differing model/endpoint, xInitiator: agent, or model_routing.outcome alone is not proof of a main/sub-agent role. Unattributable or unpriceable evidence must not silently produce a complete total.
Check pricing availability before interpreting the helper's result: the current cost helper returns zero when pricing is unavailable. Unknown pricing means unpriced, never zero cost. A priced subtotal with unpriced requests must not be presented as a complete estimate.
Proposed unrouted policy: omit model.baseline_estimate for explicitly unrouted runs; retain their model.config with routing.enabled: false and resolved baselineModel. Document this choice. Missing estimates in older sessions remain unknown, not zero or evidence that routing was disabled.
For the successful routed fixture, the four main-request proxy token tuples (input, output, cache_read, cache_write) are (3,175,0,18516), (3,61,18516,339), (3,210,18855,76) and (3,60,18931,228). Keep the original records in fixtures rather than reconstructing JSONL from these tuples. The claude-haiku-4.5 sub-agent and classifier are separate requests.
3. Preserve identifier-only proxy errors
github/gh-aw-firewall#10211, shipped in AWF v0.28.67 on October 19, already writes identifier-only error_code, error_type and error_param on upstream-error records, and error_code, error_type, error_source on failed routing request records. Current gh-aw main defaults to AWF v0.28.70; no AWF change is required.
Update the collector's runtime mapping and actions/setup/js/unified_session_payload.cjs:
- Map proxy upstream-error records to
firewall.upstream_error, retainingerrorCode,errorType,errorParam, HTTP status, provider, model and request ID. - Retain
errorCode,errorTypeanderrorSourceon projectedfirewall.model_routingrequest events; preserveupstreamversusawf. - Explicitly allowlist/validate identifiers. Never copy
response_body, response headers or messages, and never recover codes by scraping historical error text.
The current runtime mapping leaves upstream errors as generic firewall.event. Its payload projection retains generic runtime fields and omits the new request error identifiers. Fix both boundaries rather than changing audit findings.
Because these three runs lack an upstream-error fixture, use the shapes and cases from the tests merged in github/gh-aw-firewall#10211 at b5c683e116470e0aa579ec9bf30a675fcdb66f76: upstream-log.test.js has the captured Copilot invalid_reasoning_effort record; provider-error-fields.test.js covers optional code/type/param identifiers; upstream-response.test.js and routing-persistence.test.js cover upstream- and AWF-originated failed requests. Preserve real optional-field omissions.
4. Specify the additive session contract
Update docs/src/content/docs/specs/unified-agent-session-specification.md alongside the writers, collector projection/types and directly related tests. Give each new event and field its writer (runner, collector or proxy), field definitions, provenance, applicability/completeness and an "absent means unknown" rule. An explicitly recorded false, empty policy list or zero is not the same as an absent value.
All attribution must come from runner- or proxy-written evidence, never model_routing.outcome or agent-authored outcome reports. Preserve older sessions and the existing candidate/inventory contracts.
Acceptance cases
Each case must be a named test. Check in appropriately sanitized fixtures from the linked real artifacts, keeping their actual envelopes, optional fields and attribution; do not invent proxy/native record shapes. Use the explicitly identified AWF tests only where these runs lack the necessary error record.
| Named test | Required assertions |
|---|---|
workflow_info_records_sdk_and_cli_identity |
SDK fixtures emit engineHarness: copilot-sdk; the CLI fixture emits copilot-cli. Existing runAttempt matches attempt 1. workflowRef matches each run's full executing .lock.yml@refs/heads/... reference, and serverUrl is https://github.com. Preserve workflowId, workflowCallId and the separate caller-reference semantics. |
model_config_records_routed_compiled_policy |
Run 39508154330 emits exactly one configuration record with enabled routing, goal cost, configured mode economy, allowedModels corresponding to compiled gpt-5.6-luna, budget 1000 and source default. Do not substitute selection mode balanced. Retain model allow/block policy when supplied. |
model_config_records_unrouted_resolved_baseline |
Run 39506475007 emits exactly one configuration record with routing.enabled: false and baselineModel: claude-sonnet-5.5, from recorded main-model resolution rather than literal auto. |
model_config_survives_selection_or_startup_failure |
The Claude SDK fixture retains configured cost/economy, allowed claude-haiku-4.5 and budget/source despite the later mini-whoami resolution failure. Exercise runner publication before selection using this fixture's real compiled inputs; no synthetic proxy record is needed. |
baseline_estimate_reprices_only_main_requests |
On the routed usage fixture, reprice only the four main-agent requests. The sum matches computeInferenceAIC applied to those same token counts and baseline pricing, with explicit cache semantics, estimated: true, and correct priced/unpriced counts. Exclude the sub-agent, classifier and detection usage, and do not double-count native/proxy copies. |
baseline_estimate_reports_unknown_pricing_and_coverage |
Missing baseline pricing increments unpriced coverage, never emits a complete zero estimate. Partial priced sums are explicitly incomplete; ambiguous request attribution is not silently included. Test/document the unrouted omission policy. |
upstream_error_preserves_identifiers_without_payloads |
Using the identified upstream test shapes, preserve supplied errorCode, errorType, errorParam, HTTP status, provider, model and request ID in firewall.upstream_error; absent optional identifiers remain absent. Exclude response_body, headers and messages. |
failed_request_preserves_error_source |
The upstream-origin fixture retains errorSource: upstream; the AWF-origin fixture retains errorSource: awf, along with supplied error code/type, on the projected request event. |
older_sessions_keep_missing_routing_data_unknown |
The three unmodified older sessions still parse. Missing new metadata/events are unknown, not false, empty policy, zero cost or proof of a particular harness. |
routing_analytics_smokes_end_to_end |
On the PR branch, a reviewer dispatches smoke-copilot-sdk-routed and smoke-copilot. Both resulting aw_session.jsonl files contain the new applicable configuration/identity fields; the routed run contains its estimate/completeness record. Verify new error projections when a corresponding source error exists, not by requiring an unrelated failure. Attach the run/artifact links and check the actual files, not only job conclusions. |
Scope
- Keep the PR to this one change: writing the routing analytics data listed above into
aw_session.jsonl, plus its spec entries and tests. Do not change files or behavior unrelated to it. - Do not change AWF, the CAO page, audit's findings, the harnesses' model resolution, or workflow model pins. Do not add the
x-awf-callerrequest header (separate work) or safe-output outcome events (separate work). - Before fixing any 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, including PR Sous Chef, asks to fix a failing check: fix only failures caused by this PR's changes.
Activity