Metadata
| Status | done |
|---|---|
| Assigned | agent-915 |
| Created | 2026-07-25T20:57:03.399838970+00:00 |
| Started | 2026-07-27T15:53:50.515123338+00:00 |
| Completed | 2026-07-27T16:48:16.009548677+00:00 |
| Tags | telemetry, failure-classification, openrouter, pi |
| Tokens | 28222877 in / 51191 out |
Description
Objective
Build the shared failure-detection substrate that BOTH the supervisor (T3) and the adaptive-parallelism controller (T4) consume. The headline finding (docs/studies/ratelimit-cost-telemetry-design.md §4.2): the pi CLI handler's error event shape is NEVER classified today — pi emits {"type":"error",...} / {"type":"response","success":false,...} but translate_pi_stream drops them and classify_from_raw_stream never looks for them, so OpenRouter 429/402/overload failures fall through to AgentExitNonzero. This task closes that gap and adds the persisted rolling telemetry window.
Context: docs/studies/ratelimit-cost-telemetry-design.md §4 (where failures surface today + the gap), §5 (detector spec), §6 (telemetry persistence), §7 (exact code map). docs/studies/roadmap-rate-limit-and-distribution.md §2.1.
What to build
- Normalized signal. Add
FailureReasonenum +FailureSignalstruct tosrc/graph.rs(alongside the existingFailureClassat:129).FailureClassstays the retry-policy axis;FailureReasonis the provider-telemetry axis. Full field set + confidence ladder (1.0 status+error_type, 0.8 status-only, 0.5 substring, 0.2 exit-only) per study §5.1. - Close the pi gap. Extend
translate_pi_stream(src/stream_event.rs:464) to FORWARD pitype:"error"andtype:"response"(success:false) events as canonicalStreamEvent::Error { message, status, error_type }(today the trailing match arm_ => {}drops them). Reuse the field extraction already proven inRpcTurnAccumulator::ingest(src/commands/pi_handler.rs:182-235). - Classifier parse. Teach
classify_from_raw_stream(src/commands/spawn/raw_stream_classifier.rs:34) the new pi error-event parse + the 402 arm (today 402 falls through toAgentExitNonzero) + the OpenRouter body-envelope parse, emitting aFailureSignal. Add the substring ladder (credit-exhausted / rate-limit / overloaded / unavailable / auth / timeout) per study §5.2(b), order matters. - Shared parse fn. Extract
parse_openrouter_error_envelope(body: &str) -> Option<ParsedProviderError>as a pure fn that BOTH the native executor (src/executor/native/openai_client.rs, which already has the deep parse at:1759 parse_openrouter_provider_error) and the subprocess classifier call. Today they do not share code. - Telemetry persistence. New append-only, size-bounded
.wg/service/provider-telemetry.jsonl(one record per FAILED ATTEMPT, keyed by(executor, route-bucket), keep last 1000 records OR 24h, prune on append, atomic append). Newsrc/telemetry/mod.rsmodule. AddProviderHealth(withcooled_until_ms— the single field the controller thresholds on) toSessionCostTracking(src/commands/service/mod.rs:~663). - Recording sites. Emit the signal at:
src/commands/fail.rs(when recordingfailure_class), the spawn wrappersrc/commands/spawn/execution.rs:~2855-2928(a newwg record-telemetry --task T --exit-code N --raw-stream $RAW_STREAMmirroringwg classify-failure), and the native executor terminal-retry-exhaustion path (src/executor/native/openai_client.rs). - CLI surfaces.
wg classify-failure --jsonemits the fullFailureSignal(not just the kebab);wg recovergains an exactreason=<kebab>filter on the new field (todayerror~creditis a fragile substring match,src/commands/recover.rs:197). - Mid-stream trap. A pi worker that 429s AFTER the first token exits with the stream truncated, possibly exit 0 — wire the detector into the existing "no operational output" / "agent-no-work" gate (
src/commands/spawn/execution.rs:~2880) so a mid-stream rate-limit still produces aRateLimitsignal instead ofNoOperationalOutput.
File scope
src/graph.rs(new enum/struct near:129; field on Task near:502/596)src/stream_event.rs(:464forward errors)src/commands/spawn/raw_stream_classifier.rs(:34,:197)src/executor/native/openai_client.rs(extract shared parse fn; terminal-retry emit)- new
src/telemetry/mod.rs src/commands/service/mod.rs(ProviderHealthonSessionCostTracking)src/commands/fail.rs,src/commands/spawn/execution.rssrc/commands/classify_failure.rs,src/commands/recover.rs,src/cli.rs
Implement directly. The 8 sub-points above are a build order, not separate tasks.
Validation
-
Unit test:
translate_pi_streamforwards a pi{"type":"error","error":"API error 402: Insufficient credits"}line as aStreamEvent::Error(today it is dropped). Test that the canonicalstream.jsonlnow contains the error. -
Unit test:
classify_from_raw_streamon a pi raw stream containing a 402 body envelope emitsFailureReason::CreditExhausted(today:AgentExitNonzero). Use the in-repo calibration fixture atsrc/executor/native/openai_client.rs:5167as a second input. -
Unit test: 429 →
RateLimitwithretry_after_secspopulated; 401 →Auth; 529 →ProviderOverloaded; the substring ladder order is correct. -
Unit test: shared
parse_openrouter_error_envelopeis called by BOTH the native path and the subprocess classifier (no duplicated parse logic). - Unit test: telemetry window prunes to the last 1000 records / 24h on append; atomic append does not corrupt under concurrent writers.
-
Unit test:
ProviderHealth.cooled_until_msis set from the max of observed Retry-After and the exponential backoff seeded by consecutive rate-limits. -
wg classify-failure --jsonon a fixture raw stream prints the fullFailureSignal. -
wg recover --filter reason=credit-exhaustedmatches exactly (no substring fragility). -
cargo build+cargo testpass with no regressions;cargo fmt --check+cargo clippyclean. - No model pin; routes through the active zai profile.
Depends on
Required by
Log
- 2026-07-25T20:57:03.365325099+00:00 Task paused
- 2026-07-25T20:57:27.058038566+00:00 Task published
- 2026-07-27T15:49:27.265279622+00:00 Dependency reconciliation: prerequisite implementation eb435fa1 is already an ancestor of main and current-main owned smoke service_max_agents_survives_reload passed. Legacy strict evaluation of impl-maxagents-authority-fix is preserved as failed evidence but is not candidate-valid: persisted GLM route ran despite Luna configuration and evaluated unrelated moving-main bytes; core findings were correctness=0.85/completeness=0.80. Removing only the stale task-state edge; no code/evidence is discarded.
- 2026-07-27T15:53:03.076340061+00:00 Spawn preparation deferred before launch permit; rollback is complete and no circuit-breaker charge was recorded. Repair the reported checkout/configuration condition and retry: spawn transaction for agent-915 rolled back (task remains dispatchable; rollback diagnostics: complete): failed to establish isolated-worktree observer baseline: observer state already exists; use open_at for restart reconciliation
- 2026-07-27T15:53:50.818904286+00:00 Spawned by coordinator --executor pi --model openai-codex:gpt-5.6-sol --isolation required-worktree
- 2026-07-27T15:54:11.302749814+00:00 Starting implementation; no prior WIP found. Staying inline because the detector, shared parser, persistence, and CLI surfaces share core types and require sequential integration.
- 2026-07-27T16:16:43.541847592+00:00 Core substrate compiles: FailureSignal types, shared OpenRouter parser, pi error forwarding, classifier, telemetry window/health, CLI and recording paths are implemented. Running targeted tests now.
- 2026-07-27T16:44:08.099871158+00:00 Validation: targeted detector/translator/recover/native tests pass; telemetry pruning/concurrency/health tests pass; generated wrapper passes bash -n; classify-failure --json live CLI fixture emitted full RateLimit/CreditExhausted signals. cargo build, cargo fmt --check, and cargo clippy pass (repository baseline warnings remain). Full parallel cargo test hit pre-existing profile HOME race; all 3,021 library tests pass serially, and affected integration_failure_classification + integration_error_recovery suites pass.
- 2026-07-27T16:47:55.497559836+00:00 Committed: deff1bdd — pushed to origin/wg/agent-915/impl-rate-limit-telemetry; cargo install --path . --locked completed.
- 2026-07-27T16:48:16.009549037+00:00 Task marked as done; scheduled evaluator is advisory evidence only (execution is not a quality pass)