Symptom
A gpt-6-astra turn that keeps streaming at about 2.1 tokens per second never fails, never retries, and never walks a fallback chain. The session looks healthy (working line only shows elapsed time) while remaining effectively unusable.
Why
Every in-progress stream watchdog is a silence detector:
- Stream-start (
retry.provider.streamStartTimeoutMs, default 300s) applies only until the first event.
- Inter-event idle (
retry.provider.timeoutMs / httpIdleTimeoutMs, default 300s) resets on every event, including thinking deltas.
- There is no total-turn budget on the agent loop.
- WebSocket liveness and HTTP body idle likewise reset on any bytes.
- Fallback runs only after a retryable assistant error. A trickle produces none.
Compaction already documents the missing class ("a stream that keeps trickling events never trips the idle watchdog") and bounds it with a wall-clock budget. That bound was not applied to the main turn, and a wall-clock total-turn cap would be the wrong shape anyway (tool-using turns are long).
The suite currently pins the opposite contract: packages/agent/test/agent-loop-stream-start-timeout.test.ts (lets slow-but-alive streams finish despite gaps above the start bound).
Live tok/s is not shown in the working line or footer. The builtin TPS extension posts TPS N tok/s. … only on agent_end.
Shipped retry.fallbackChains is empty (no implicit/wildcard lane). Category builtins pin ultrabrain/deep to a single Astra model in config; resolve-time chains can add gpt-5.6-sol only when that id is in the live registry, and only after an error. architect is a single-rung pin with no second model.
Expected
- After the first stream event, the working line shows live tok/s (
Working (12s • 42 tok/s • Esc to interrupt)).
- If streamed text+thinking stays under 8 tok/s for a 20s window (5s grace after first event, tool time excluded), abort with
Provider stream throughput degraded: … tok/s over 20s (floor 8 tok/s).
- Do not spend the same-model retry budget. Fall back immediately when a chain candidate exists; if none, end the turn with that error and the existing "No fallback chain configured — set one with /fallback." copy.
- A control stream at tens of tok/s is unaffected. Silence is still start/idle, not this path.
Acceptance tests (failing first)
packages/agent/test/agent-loop-throughput-watchdog.test.ts: 2 tok/s mock for >25s → stopReason === "error" and throughput wording; 40 tok/s control → stop.
packages/coding-agent/test/suite/retry-fallback-throughput-degraded.test.ts: one primary call, then retry_fallback_applied; empty chain → no second model, error still names tok/s.
Symptom
A gpt-6-astra turn that keeps streaming at about 2.1 tokens per second never fails, never retries, and never walks a fallback chain. The session looks healthy (working line only shows elapsed time) while remaining effectively unusable.
Why
Every in-progress stream watchdog is a silence detector:
retry.provider.streamStartTimeoutMs, default 300s) applies only until the first event.retry.provider.timeoutMs/httpIdleTimeoutMs, default 300s) resets on every event, including thinking deltas.Compaction already documents the missing class ("a stream that keeps trickling events never trips the idle watchdog") and bounds it with a wall-clock budget. That bound was not applied to the main turn, and a wall-clock total-turn cap would be the wrong shape anyway (tool-using turns are long).
The suite currently pins the opposite contract:
packages/agent/test/agent-loop-stream-start-timeout.test.ts(lets slow-but-alive streams finish despite gaps above the start bound).Live tok/s is not shown in the working line or footer. The builtin TPS extension posts
TPS N tok/s. …only onagent_end.Shipped
retry.fallbackChainsis empty (no implicit/wildcard lane). Category builtins pin ultrabrain/deep to a single Astra model inconfig; resolve-time chains can add gpt-5.6-sol only when that id is in the live registry, and only after an error. architect is a single-rung pin with no second model.Expected
Working (12s • 42 tok/s • Esc to interrupt)).Provider stream throughput degraded: … tok/s over 20s (floor 8 tok/s).Acceptance tests (failing first)
packages/agent/test/agent-loop-throughput-watchdog.test.ts: 2 tok/s mock for >25s →stopReason === "error"and throughput wording; 40 tok/s control →stop.packages/coding-agent/test/suite/retry-fallback-throughput-degraded.test.ts: one primary call, thenretry_fallback_applied; empty chain → no second model, error still names tok/s.