feat(explain): live progress and observable-state timeout diagnostic - #964
Conversation
There was a problem hiding this comment.
Pull request overview
This PR upgrades entire explain --generate to use Claude CLI streaming output for incremental progress reporting, and introduces a TTY-gated deadline strategy (no deadline on interactive TTY by default; idle-based safety deadline in non-TTY).
Changes:
- Add a streaming text-generation capability (
StreamingTextGenerator) and implement it for the Claude Code agent using--output-format stream-jsonwith an NDJSON stream parser. - Thread streaming progress events through summarization and render them in
explainviasummaryProgressWriter(TTY in-place updates vs. line-per-event for non-TTY/accessible mode). - Replace the prior fixed wall-clock timeout behavior with a resolver-driven deadline policy plus an idle watchdog for non-TTY default behavior.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| cmd/entire/cli/explain.go | Adds progress rendering, deadline resolution, idle watchdog, and transcript-size preflight message for explain --generate. |
| cmd/entire/cli/explain_test.go | Updates and adds tests for progress writer, idle watchdog, and deadline policy behavior. |
| cmd/entire/cli/summarize/summarize.go | Extends GenerateFromTranscript with an optional progress callback and threads it into Claude generation. |
| cmd/entire/cli/summarize/summarize_test.go | Updates call sites for new signature and adds tests for progress callback threading. |
| cmd/entire/cli/summarize/claude.go | Prefers streaming generation when available and forwards progress callback into the agent. |
| cmd/entire/cli/summarize/claude_test.go | Adds test coverage for streaming preference/fallback in ClaudeGenerator. |
| cmd/entire/cli/strategy/manual_commit_condensation.go | Updates summarization call to match new GenerateFromTranscript signature. |
| cmd/entire/cli/agent/agent.go | Introduces progress phase/types and the StreamingTextGenerator interface. |
| cmd/entire/cli/agent/capabilities.go | Adds capability detection helper AsStreamingTextGenerator and declared-cap field. |
| cmd/entire/cli/agent/capabilities_test.go | Adds tests validating AsStreamingTextGenerator behavior. |
| cmd/entire/cli/agent/claudecode/generate.go | Switches Claude Code generation to streaming NDJSON parsing and emits progress callbacks. |
| cmd/entire/cli/agent/claudecode/response.go | Adds NDJSON stream event types and streamClaudeResponse parser. |
| cmd/entire/cli/agent/claudecode/response_test.go | Replaces legacy response parsing tests with stream parser tests using fixtures. |
| cmd/entire/cli/agent/claudecode/claude_test.go | Adds streaming-generation tests and delegation test for GenerateText. |
| cmd/entire/cli/agent/claudecode/testdata/stream_success.jsonl | Adds real-world success fixture for stream-json NDJSON. |
| cmd/entire/cli/agent/claudecode/testdata/stream_error_404.jsonl | Adds error fixture (is_error:true, 404) for stream-json NDJSON. |
33a095c to
5e80233
Compare
cd538a4 to
92449c4
Compare
c6c0c5a to
9c500a7
Compare
f660315 to
0b9993e
Compare
|
@BugBot review |
58ea7e9 to
0b9993e
Compare
Three tests in manifest_test.go hardcoded `started := time.Date(2026, 5, 8, ...)` as the session.State.StartedAt anchor. session.StateStore.Load auto-deletes sessions whose StartedAt is older than StaleSessionThreshold (7 days) and returns nil, so the hardcoded date silently rots: tests pass while the calendar is inside the 7-day window, then fail forever once it crosses. CI on PR #964 caught this — same SHA passed yesterday (6 days after the hardcoded date) and failed today (7 days after). Unrelated to the streaming/diagnostic work in this PR; the manifest_test.go file isn't touched by any other commit on this branch. Switch all three tests to `time.Now().UTC().Add(-time.Hour)` so the session is always one hour old at test time. Still exercises the 5-second jitter check inside matchReviewSessionState; stays well inside the staleness window. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two findings from Bugbot's 2026-05-14 review: 1. **Timeout check discarded typed errors when deadline races** (explain.go:1209). The fallback clause `(cancel != nil && errors.Is(runCtx.Err(), context.DeadlineExceeded))` could fire when `err` was a typed envelope failure (e.g. *agent.TextGenerationError carrying HTTP 404) and ctx happened to be done concurrently. That clause converted the envelope error into a generic "timed out" message, losing the typed signal. The streaming generator already gives envelope errors precedence over ctx.Err() (see claudecode.GenerateTextStreaming), so by the time err reaches this branch, context.DeadlineExceeded only appears in err's chain when the timeout truly was the cause. Drop the OR fallback. 2. **Redundant provider-binary mapping** (explain.go:1319). `providerCLIName` duplicated the (claude-code, codex, copilot-cli, cursor, gemini) → binary mapping already in `summaryProviderBinaries`. Two parallel maps would drift silently when a new provider is added. Export `agent.SummaryCLIBinaryName` as the single source of truth; `providerCLIName` collapses to a one-line delegate. Drops the FactoryAIDroid case (Factory is not summary-capable; the diagnostic code path never sees it). `IsSummaryCLIAvailable` is updated to call SummaryCLIBinaryName for consistency. Net: -34 lines in explain.go, +8 lines in agent/text_generator_cli.go, no behavioral change for the happy path; the envelope-error precedence is now correctly preserved when ctx is cancelled mid-call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three tests in manifest_test.go hardcoded `started := time.Date(2026, 5, 8, ...)` as the session.State.StartedAt anchor. session.StateStore.Load auto-deletes sessions whose StartedAt is older than StaleSessionThreshold (7 days) and returns nil, so the hardcoded date silently rots: tests pass while the calendar is inside the 7-day window, then fail forever once it crosses. CI on PR #964 caught this — same SHA passed yesterday (6 days after the hardcoded date) and failed today (7 days after). Unrelated to the streaming/diagnostic work in this PR; the manifest_test.go file isn't touched by any other commit on this branch. Switch all three tests to `time.Now().UTC().Add(-time.Hour)` so the session is always one hour old at test time. Still exercises the 5-second jitter check inside matchReviewSessionState; stays well inside the staleness window. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@BugBot review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 252e320. Configure here.
Three tests in manifest_test.go hardcoded `started := time.Date(2026, 5, 8, ...)` as the session.State.StartedAt anchor. session.StateStore.Load auto-deletes sessions whose StartedAt is older than StaleSessionThreshold (7 days) and returns nil, so the hardcoded date silently rots: tests pass while the calendar is inside the 7-day window, then fail forever once it crosses. CI on PR #964 caught this — same SHA passed yesterday (6 days after the hardcoded date) and failed today (7 days after). Unrelated to the streaming/diagnostic work in this PR; the manifest_test.go file isn't touched by any other commit on this branch. Switch all three tests to `time.Now().UTC().Add(-time.Hour)` so the session is always one hour old at test time. Still exercises the 5-second jitter check inside matchReviewSessionState; stays well inside the staleness window. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two findings from Bugbot's 2026-05-14 review: 1. **Timeout check discarded typed errors when deadline races** (explain.go:1209). The fallback clause `(cancel != nil && errors.Is(runCtx.Err(), context.DeadlineExceeded))` could fire when `err` was a typed envelope failure (e.g. *agent.TextGenerationError carrying HTTP 404) and ctx happened to be done concurrently. That clause converted the envelope error into a generic "timed out" message, losing the typed signal. The streaming generator already gives envelope errors precedence over ctx.Err() (see claudecode.GenerateTextStreaming), so by the time err reaches this branch, context.DeadlineExceeded only appears in err's chain when the timeout truly was the cause. Drop the OR fallback. 2. **Redundant provider-binary mapping** (explain.go:1319). `providerCLIName` duplicated the (claude-code, codex, copilot-cli, cursor, gemini) → binary mapping already in `summaryProviderBinaries`. Two parallel maps would drift silently when a new provider is added. Export `agent.SummaryCLIBinaryName` as the single source of truth; `providerCLIName` collapses to a one-line delegate. Drops the FactoryAIDroid case (Factory is not summary-capable; the diagnostic code path never sees it). `IsSummaryCLIAvailable` is updated to call SummaryCLIBinaryName for consistency. Net: -34 lines in explain.go, +8 lines in agent/text_generator_cli.go, no behavioral change for the happy path; the envelope-error precedence is now correctly preserved when ctx is cancelled mid-call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…egradation Foundation for non-Claude streaming summary generation. Mirrors the ReviewerTemplate pattern (review/types/template.go, from PR #1192): - StreamingGeneratorTemplate owns subprocess lifecycle (Start, scanner, drain, Wait, stderr capture, fallback detection, error wrapping into *TextGenerationError). Per-agent code only provides BuildCmd (argv) and Parser (stdout -> progress events). - testutil.FakeStreamCmd extracted from claudecode/generate_streaming_test.go so all 5 streaming agents (claudecode + 4 upcoming) reuse one fake. Drops the in-line itoa reinvention (carry-forward from PR #964 review) in favor of strconv.Itoa. - summaryProgressWriter renders PhaseFirstToken with graceful degradation: omits the (TTFT, cached input tokens) clause when both fields are zero; omits one or the other when only one is present. Lets non-Claude agents (some of which emit neither field) render cleanly under the same template. No agent uses the new template yet — wiring lands in subsequent commits (one per agent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PhaseGenerating dispatches used to send OutputTokens=0, leaving the progress writer stuck at `→ Writing summary... (~0 tokens)` until the final assistant.message event landed. Estimate a running count from accumulated deltaContent character length (totalChars/4) so the counter ticks during the stream, matching Claude's pattern from PR #964. The authoritative output_tokens from assistant.message events still overrides on PhaseDone, so the final summary line is unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two findings from Bugbot's 2026-05-14 review: 1. **Timeout check discarded typed errors when deadline races** (explain.go:1209). The fallback clause `(cancel != nil && errors.Is(runCtx.Err(), context.DeadlineExceeded))` could fire when `err` was a typed envelope failure (e.g. *agent.TextGenerationError carrying HTTP 404) and ctx happened to be done concurrently. That clause converted the envelope error into a generic "timed out" message, losing the typed signal. The streaming generator already gives envelope errors precedence over ctx.Err() (see claudecode.GenerateTextStreaming), so by the time err reaches this branch, context.DeadlineExceeded only appears in err's chain when the timeout truly was the cause. Drop the OR fallback. 2. **Redundant provider-binary mapping** (explain.go:1319). `providerCLIName` duplicated the (claude-code, codex, copilot-cli, cursor, gemini) → binary mapping already in `summaryProviderBinaries`. Two parallel maps would drift silently when a new provider is added. Export `agent.SummaryCLIBinaryName` as the single source of truth; `providerCLIName` collapses to a one-line delegate. Drops the FactoryAIDroid case (Factory is not summary-capable; the diagnostic code path never sees it). `IsSummaryCLIAvailable` is updated to call SummaryCLIBinaryName for consistency. Net: -34 lines in explain.go, +8 lines in agent/text_generator_cli.go, no behavioral change for the happy path; the envelope-error precedence is now correctly preserved when ctx is cancelled mid-call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
57a6498 to
3afe589
Compare
…egradation Foundation for non-Claude streaming summary generation. Mirrors the ReviewerTemplate pattern (review/types/template.go, from PR #1192): - StreamingGeneratorTemplate owns subprocess lifecycle (Start, scanner, drain, Wait, stderr capture, fallback detection, error wrapping into *TextGenerationError). Per-agent code only provides BuildCmd (argv) and Parser (stdout -> progress events). - testutil.FakeStreamCmd extracted from claudecode/generate_streaming_test.go so all 5 streaming agents (claudecode + 4 upcoming) reuse one fake. Drops the in-line itoa reinvention (carry-forward from PR #964 review) in favor of strconv.Itoa. - summaryProgressWriter renders PhaseFirstToken with graceful degradation: omits the (TTFT, cached input tokens) clause when both fields are zero; omits one or the other when only one is present. Lets non-Claude agents (some of which emit neither field) render cleanly under the same template. No agent uses the new template yet — wiring lands in subsequent commits (one per agent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PhaseGenerating dispatches used to send OutputTokens=0, leaving the progress writer stuck at `→ Writing summary... (~0 tokens)` until the final assistant.message event landed. Estimate a running count from accumulated deltaContent character length (totalChars/4) so the counter ticks during the stream, matching Claude's pattern from PR #964. The authoritative output_tokens from assistant.message events still overrides on PhaseDone, so the final summary line is unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the mechanism for live progress events from the summary generation subprocess to the explain layer. - `agent`: StreamingTextGenerator capability interface, ProgressPhase constants (Connecting, FirstToken, Generating, Done), GenerationProgress struct, ProgressFn type, AsStreamingTextGenerator detector. - `claudecode`: stream-json NDJSON parser; `*ClaudeCodeAgent.GenerateTextStreaming` invokes `claude --print --output-format stream-json --include-partial-messages --verbose` and dispatches per-phase progress callbacks. Falls back to plain GenerateText on old CLIs that reject the streaming flags. - `summarize`: GenerateFromTranscript accepts an agent.ProgressFn parameter. ClaudeGenerator and TextGeneratorAdapter both check AsStreamingTextGenerator on their underlying agent and prefer streaming when supported. The Generator interface is unchanged. Foundation only; no UI behavior change yet — both production callers pass nil progress at this point. The progress UI and timeout diagnostic land in subsequent commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
resolveSummaryTimeout now returns 0 (no deadline) when neither the --summary-timeout-seconds flag nor the summary_timeout_seconds setting is set. generateCheckpointAISummary skips context.WithTimeout entirely in that case — the provider call inherits the parent ctx unchanged. The 5-minute package default from #1204 is removed. Users who relied on it can restore the cap with one command: entire configure --summarize-timeout-seconds 300 This sets the stage for the streaming-aware timeout diagnostic in the next commit: with no automatic deadline, the only timeout the diagnostic ever explains is one the user explicitly asked for. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two paired user-facing improvements to `entire explain --generate`: 1. Live progress on TTY. summaryProgressWriter renders agent.GenerationProgress events from the streaming generator as provider-neutral phase lines: "Sending request to provider...", "Provider responded (TTFT Xs, Nk cached input tokens)", "Writing summary... (~Nk tokens)", "Summary generated (Ds, N output tokens)". In-place line updates on TTY; one line per event in non-TTY/CI. ACCESSIBLE=1 swaps the styled glyphs (→ / ✓) for ASCII (-> / *) and suppresses ANSI escapes. 2. Observable-state timeout diagnostic. When --summary-timeout-seconds fires, the error message is built from captured subprocess state, not a generic three-cause list. Streaming providers (Claude) get phase- specific attribution: "never sent its request" / "received no response" / "responded but did not finish". Non-streaming providers (Codex/ Cursor/Copilot/Gemini) surface captured stderr and distinguish "produced no output" from "was generating output when killed". The three-cause shrug message from PR #876 is removed. Adds summaryAttempt as the diagnostic state value type (separate from the UI writer), providerCLIName for binary-name lookup, and *agent.TextGenerationError for the non-streaming provider stderr-capture path through RunIsolatedTextGeneratorCLI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two findings from Bugbot's 2026-05-14 review: 1. **Timeout check discarded typed errors when deadline races** (explain.go:1209). The fallback clause `(cancel != nil && errors.Is(runCtx.Err(), context.DeadlineExceeded))` could fire when `err` was a typed envelope failure (e.g. *agent.TextGenerationError carrying HTTP 404) and ctx happened to be done concurrently. That clause converted the envelope error into a generic "timed out" message, losing the typed signal. The streaming generator already gives envelope errors precedence over ctx.Err() (see claudecode.GenerateTextStreaming), so by the time err reaches this branch, context.DeadlineExceeded only appears in err's chain when the timeout truly was the cause. Drop the OR fallback. 2. **Redundant provider-binary mapping** (explain.go:1319). `providerCLIName` duplicated the (claude-code, codex, copilot-cli, cursor, gemini) → binary mapping already in `summaryProviderBinaries`. Two parallel maps would drift silently when a new provider is added. Export `agent.SummaryCLIBinaryName` as the single source of truth; `providerCLIName` collapses to a one-line delegate. Drops the FactoryAIDroid case (Factory is not summary-capable; the diagnostic code path never sees it). `IsSummaryCLIAvailable` is updated to call SummaryCLIBinaryName for consistency. Net: -34 lines in explain.go, +8 lines in agent/text_generator_cli.go, no behavioral change for the happy path; the envelope-error precedence is now correctly preserved when ctx is cancelled mid-call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two findings from Cursor Bugbot's review of commit 252e320: 1. **Streaming envelope errors lose typed ClaudeError classification** (Medium, generate_streaming.go:137). `envelopeErrorMessage` returned a plain `errors.New(msg)` containing the HTTP status as a substring. Since `AsStreamingTextGenerator` routes Claude through the streaming path by default, structured API errors (auth, rate-limit, config, model-not-found) became opaque to `formatCheckpointSummaryError`'s `errors.As(*ClaudeError)` switch — users lost the actionable remediation hints ("Run `claude login` and retry" etc.). Route the final envelope through `classifyEnvelopeError` like the non-streaming path does. `exitCode=0` because envelope errors arrive on stdout while the CLI itself exits successfully. 2. **Per-delta integer division truncates token estimate to zero** (Low, generate_streaming.go:164). `outputTokensEstimate += len(text) / 4` floors each individual delta. With Claude streaming single-character or single-token deltas (the common case for `--include-partial-messages`), every increment was 0 and the UI stayed at "~0 tokens" until a chunky delta arrived. Accumulate raw character count and compute the estimate from the running total instead, so 4 single-char deltas correctly render as ~1 token rather than 0. Test update: `TestGenerateTextStreaming_EnvelopeErrorSurfaced` previously asserted `Contains(err.Error(), "404")` against the plain-string format. With the typed-error return, switch to `errors.As(err, &claudeErr)` and assert `APIStatus == 404` plus `Kind == ClaudeErrorConfig` — this validates the contract the explain layer actually depends on, not the formatter output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three generateTranscriptSummary fake fixtures (TestExplain_V1Generate, TestExplain_V2Generate, TestExplain_V2GeneratePreferred) were added on main during the lifetime of this branch using the pre-streaming signature without `agent.ProgressFn`. They merged textually clean during the rebase onto origin/main but produced a semantic mismatch — the function-pointer type changed when this branch's stream-driven summarization commit (a3a44c9) added the progress parameter. Add `_ agent.ProgressFn,` after `_ summarize.Generator,` in each fixture so it matches the rest of the test file's fakes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3afe589 to
9366ac7
Compare
…egradation Foundation for non-Claude streaming summary generation. Mirrors the ReviewerTemplate pattern (review/types/template.go, from PR #1192): - StreamingGeneratorTemplate owns subprocess lifecycle (Start, scanner, drain, Wait, stderr capture, fallback detection, error wrapping into *TextGenerationError). Per-agent code only provides BuildCmd (argv) and Parser (stdout -> progress events). - testutil.FakeStreamCmd extracted from claudecode/generate_streaming_test.go so all 5 streaming agents (claudecode + 4 upcoming) reuse one fake. Drops the in-line itoa reinvention (carry-forward from PR #964 review) in favor of strconv.Itoa. - summaryProgressWriter renders PhaseFirstToken with graceful degradation: omits the (TTFT, cached input tokens) clause when both fields are zero; omits one or the other when only one is present. Lets non-Claude agents (some of which emit neither field) render cleanly under the same template. No agent uses the new template yet — wiring lands in subsequent commits (one per agent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PhaseGenerating dispatches used to send OutputTokens=0, leaving the progress writer stuck at `→ Writing summary... (~0 tokens)` until the final assistant.message event landed. Estimate a running count from accumulated deltaContent character length (totalChars/4) so the counter ticks during the stream, matching Claude's pattern from PR #964. The authoritative output_tokens from assistant.message events still overrides on PhaseDone, so the final summary line is unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…treaming path Three streaming-path fixes from PR review: - Inject the user's apiKeyHelper via a 0600 --settings file exactly like GenerateText does. The streaming argv passed --setting-sources "" without the re-injection, silently breaking API-billing auth on every streaming call (including per-commit condensation summaries). - Wrap ctx-sentinel returns in *agent.TextGenerationError carrying captured stderr and the stdout byte count, so the explain timeout diagnostic gets real evidence for the default provider instead of fabricating a cause. - Let a fully decoded success survive a context-caused kill that lands between the result envelope and cmd.Wait (signal termination while ctx is done). A CLI exiting non-zero on its own remains authoritative, matching the existing comment's intent. Adds a hang mode to the fake stream subprocess so context-kill behavior is deterministically testable, asserts the previously unchecked token/TTFT fields in the streaming success test, and strengthens the re-aritied RunIsolatedTextGeneratorCLI tests to assert the captured-output returns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PhaseConnecting comes from a version-dependent status event, so an older claude CLI can reach FirstToken/Generating without ever reporting Connecting. Checking the earliest missing phase diagnosed that as 'provider never sent its request' while tokens were visibly streaming. Check from the furthest phase reached backwards instead, and add an explicit branch for the deadline firing after PhaseDone so that case no longer falls through to 'provider produced no output'. Also fixes a stale docstring (the 5-minute package default no longer exists) and drops a leftover staged-implementation comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bution
- pi: wrap generation failures in *agent.TextGenerationError like the other
summary providers, so a Pi timeout diagnostic can distinguish 'produced
no output' from 'was generating when killed' and surface stderr.
- explain: set attempt.streaming eagerly from the provider's declared
streaming capability instead of waiting for the first progress event. A
streaming provider that stalls before emitting anything now gets the
streaming diagnostic ('provider never sent its request') rather than the
non-streaming 'produced no output' guess. Streaming diagnostic branches
now also render the captured stderr row so the eager attribution does
not trade away evidence.
Resolves the two open findings on trail 164.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TestGenerateTextStreaming_CtxKillMidStreamCarriesEvidence kills the child as soon as a progress event (stdout) is observed. With stdout written first, a loaded runner could deliver the kill in the gap before the child's stderr write, leaving captured stderr empty (flaked on CI test-core). Writing stderr first makes the assertion deterministic: observing any stdout implies the stderr write already completed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Text path The old-CLI streaming fallback lands in GenerateText, whose ctx-cancel branches returned bare sentinels — the only remaining text-generation path without *agent.TextGenerationError wrapping. A fallback timeout therefore reached the explain diagnostic with no captured stderr or stdout byte count. Wrap the sentinel like the streaming path and every other provider; errors.Is routing is unchanged. Resolves the remaining open finding on trail 164. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ut exists A streaming attempt that recorded no phases but did observe stdout means streaming degenerated — e.g. the old-CLI flag-rejection fallback ran GenerateText and produced output that never became progress events. Route that case to the evidence-based branches (which read the stdout byte count and captured stderr) instead of asserting the provider never sent its request. Resolves the open finding on trail 164. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconciles this PR's classification surface with #964, which merged 2026-07-23 (28a39a3) and rewrote the same 11 files. The PR description's queue note is inverted by that: it assumed #964 would adopt TextGenError, but #964's carrier landed first and is now the incumbent. The two error types are complementary, not competing, so they compose rather than one replacing the other: - *agent.TextGenError answers "what kind of failure?" (Kind, Provider, APIStatus) and drives the user-facing label and remediation. - *agent.TextGenerationError answers "what did the subprocess emit before it died?" (Stderr, StdoutBytes) and drives #964's timeout diagnostic. That evidence is the ONLY signal on the ctx path, where classification is meaningless. HandleTextGenResult and claudecode.GenerateText now wrap the classified error (or ctx sentinel) in *TextGenerationError, so errors.As recovers the classification, errors.Is recovers the sentinel, and errors.As recovers the evidence — all from one error. Returning either type bare silently regresses the other, which is why TestHandleTextGenResult_DeadlineCarriesPartialOutput (ported from main's deleted helper test) now asserts all three at once. Conflict resolutions of note: - claudecode/generate.go resolved toward MAIN's body. This PR's version predates the apiKeyHelper auth injection (writeAuthSettingsFile + --settings <0600 file>, never argv) that #964 added after finding it silently dropped API-billing auth. Taking this PR's side would have re-introduced that outage and lost the argv-exposure mitigation. Kept main's buildGenerateArgs(model, settingsPath) and layered classification on top. - generate_streaming.go had NO conflict marker but referenced the deleted classifyEnvelopeError — the silent half of the collision. Extracted classifyEnvelopeFields so both Claude paths share one implementation and cannot drift, which is what main's comment already said should hold. - formatCheckpointSummaryError adopts main's (err, *summaryAttempt) signature; the typed-error branch delegates to renderTextGenError. - generateCheckpointAISummary adopts main's 2-return signature with the timeout/progress/attempt tail. - Dropped RunIsolatedTextGeneratorCLI and its tests; the stdout-fallback behavior its deleted test pinned is now covered in HandleTextGenResult. - Dropped formatMessageSuffix/formatClaudeErrorSuffix (obsolete with ClaudeError); kept main's providerCLIName and timeoutDiagnostic. Verified: mise run test — 8452 tests, 0 failures. Both features' tests pass in one tree (#964's TestTimeoutDiagnostic_* alongside this PR's TestRenderTextGenError_*), and main's apiKeyHelper tests pass against the live GenerateText call path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01KYAZ33V93JY670BJYK46THFZ

https://entire.io/gh/entireio/cli/trails/164
Summary
Two paired user-facing improvements to
entire explain --generate, building on PR #1204 (which made the 30s timeout configurable but kept a 5-minute default and didn't change the silent-wait UX):ACCESSIBLE=1.--summary-timeout-secondsfires, the error message reflects what the subprocess was actually doing. Streaming providers (Claude) get phase-specific attribution; non-streaming providers (Codex/Cursor/Copilot/Gemini) surface captured stderr and distinguish "produced no output" from "was generating output when killed". The three-cause shrug from PR Add timeout to explain summary generation #876 is removed.Plus one policy change that enables both:
--generateno longer has a 5-minute default cap. Timeouts are opt-in only via--summary-timeout-secondsflag orsummary_timeout_secondssetting (both surfaces from Honor summary_timeout_seconds in explain --generate; raise default to 5m #1204 preserved). With no automatic cap, every diagnostic users see is for a timeout they consciously requested.What users see now
TTY, normal run:
TTY, network down +
--summary-timeout-seconds 10:TTY, transcript too large +
--summary-timeout-seconds 3:Non-TTY (
| cat): same lines, one per event, no\rin-place updates, no ANSI styling (lipgloss auto-strips).ACCESSIBLE=1:→becomes->, no ANSI escapes.Architecture
agent.StreamingTextGeneratoroptional capability interface withAsStreamingTextGeneratordetector. Defined alongside the existingTextGeneratorinterface. Claude Code is the first (currently only) implementer.(*ClaudeCodeAgent).GenerateTextStreaminginvokesclaude --print --output-format stream-json --include-partial-messages --verbose, pipes stdout through a 4 MiB-buffered NDJSON scanner, and dispatchesProgressPhasecallbacks (Connecting → FirstToken → Generating(×N) → Done). Falls back to plainGenerateTextwhen stderr indicates the CLI rejected streaming flags (older Claude CLI builds).summarize.TextGeneratorAdapter(the production summary-generator path used by all providers viaresolveCheckpointSummaryProvider) gained streaming-capability detection and dispatches toGenerateTextStreamingwhen available.summarize.ClaudeGeneratorgot the same treatment for legacy callers.summaryProgressWriterrenders progress events using existingstatusStyles(lipgloss). In-place\rupdates on TTY; one line per event otherwise; ASCII glyphs in accessible mode. Throttled to 500ms-OR-25%-jump in non-TTY mode to avoid spam.summaryAttemptvalue type captures observable state during a run (phases reached, captured stderr, stdout byte count) so the diagnostic path can attribute timeouts without depending on the UI struct.*agent.TextGenerationErrorwrapsRunIsolatedTextGeneratorCLIfailures with captured stderr + stdout byte count, so the diagnostic can surface real subprocess output for non-streaming providers.providerCLINamemapstypes.AgentNameto the user-facing CLI binary in diagnostictry:rows (run \claude` directly to confirm it works`).What changed from the original draft design (in case you remember the old PR)
The original draft (commits before this rebase) had a TTY-gated deadline strategy with an idle-based 5min watchdog + 30min hard wall-clock cap, plus an opt-in
summary_timeout_secondsoverride. This redesign drops the automatic deadline entirely — timeouts are pure opt-in via the flag/setting from #1204. Rationale: the bug behind issue #1198 was that the auto-default truncated legitimate generations. Making the deadline opt-in fixes the root cause and removes a layer of mechanism (no idle watchdog, no separate wall cap, no goroutine).Also intentionally NOT included: a stderr substring classifier for non-streaming providers (would have been "chunk 8" in the original plan). Decision: ship verbatim stderr passthrough rather than speculative phrase-matching without real CLI fixtures. The raw stderr is always surfaced in the diagnostic rows.
Test plan
mise run check(fmt + lint + unit + integration + E2E Vogon canary) passes locally--summary-timeout-seconds 3on a real Claude generation → "model responded but did not finish" with cause + try rows--summary-timeout-seconds 10→ "provider sent request but received no response" (phase-distinguished from "never sent its request")| cat→ one line per event, no\r, no ANSIACCESSIBLE=1→->instead of→entire explain --help | grep summary-timeout→ "no automatic deadline applies"Closes #1198 (issue was already closed at #1204 time; this delivers the diagnostic improvement that #1204 deferred).
Note
Medium Risk
Touches
explain --generateexecution/timeout behavior and adds a new streaming Claude CLI path; failures could affect summary generation UX and error reporting across providers.Overview
entire explain --generatenow reports live, phase-based progress during summary generation (TTY in-place updates, non-TTY line output, ASCII inACCESSIBLEmode) and uses an observable-statesummaryAttemptto produce more actionable timeout messages.Adds optional
agent.StreamingTextGeneratorsupport (capability + detector) and implements Claude Code streaming viaclaude --output-format stream-jsonwith NDJSON parsing, progress callbacks (connecting/TTFT/generating/done), and fallback to non-streaming on older CLIs.Removes the implicit 5-minute default deadline for
--generate(timeouts are now opt-in via flag/setting), and enhances non-streaming provider diagnostics by wrapping CLI errors with captured stderr and stdout byte counts (TextGenerationError), plus a shared provider→binary lookup (SummaryCLIBinaryName).Reviewed by Cursor Bugbot for commit 252e320. Configure here.