Skip to content

feat(explain): live progress and observable-state timeout diagnostic - #964

Merged
peyton-alt merged 13 commits into
mainfrom
feat/explain-stream-progress
Jul 23, 2026
Merged

feat(explain): live progress and observable-state timeout diagnostic#964
peyton-alt merged 13 commits into
mainfrom
feat/explain-stream-progress

Conversation

@peyton-alt

@peyton-alt peyton-alt commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

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):

  1. Live progress on TTY. Real-time phase events (sending request → first token / TTFT → writing tokens → done) instead of silent wait. In-place line updates on TTY; one line per event in CI/piped; ASCII glyphs under ACCESSIBLE=1.
  2. Observable-state timeout diagnostic. When --summary-timeout-seconds fires, 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:

  1. Automatic deadline removed. --generate no longer has a 5-minute default cap. Timeouts are opt-in only via --summary-timeout-seconds flag or summary_timeout_seconds setting (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:

Generating checkpoint summary... (transcript: 40.7 KB, provider: claude-code)
→ Sending request to provider...
→ Provider responded (TTFT 1.6s, 13k cached input tokens) -- generating...
→ Writing summary... (~Nk tokens)             ← updates in place
✓ Summary generated (7.6s, 420 output tokens)
✓ Summary generated for <checkpoint-id>

TTY, network down + --summary-timeout-seconds 10:

Generating checkpoint summary... (transcript: 40.7 KB, provider: claude-code)
→ Sending request to provider...
✗ Timed out after 10s: provider sent request but received no response
  cause    network/firewall, provider API degraded, or auth check stuck
  try      check connectivity to the provider, then retry

TTY, transcript too large + --summary-timeout-seconds 3:

→ Provider responded (TTFT 1.6s, 20.2k cached input tokens) -- generating...
→ Writing summary... (~24 tokens)
✗ Timed out after 3s: model responded but did not finish
  cause    transcript may be too large for the chosen cap, or model is slow
  try      raise --summary-timeout-seconds or pick a faster model

Non-TTY (| cat): same lines, one per event, no \r in-place updates, no ANSI styling (lipgloss auto-strips).

ACCESSIBLE=1: becomes ->, no ANSI escapes.

Architecture

  • agent.StreamingTextGenerator optional capability interface with AsStreamingTextGenerator detector. Defined alongside the existing TextGenerator interface. Claude Code is the first (currently only) implementer.
  • (*ClaudeCodeAgent).GenerateTextStreaming invokes claude --print --output-format stream-json --include-partial-messages --verbose, pipes stdout through a 4 MiB-buffered NDJSON scanner, and dispatches ProgressPhase callbacks (Connecting → FirstToken → Generating(×N) → Done). Falls back to plain GenerateText when stderr indicates the CLI rejected streaming flags (older Claude CLI builds).
  • summarize.TextGeneratorAdapter (the production summary-generator path used by all providers via resolveCheckpointSummaryProvider) gained streaming-capability detection and dispatches to GenerateTextStreaming when available. summarize.ClaudeGenerator got the same treatment for legacy callers.
  • summaryProgressWriter renders progress events using existing statusStyles (lipgloss). In-place \r updates 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.
  • summaryAttempt value 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.TextGenerationError wraps RunIsolatedTextGeneratorCLI failures with captured stderr + stdout byte count, so the diagnostic can surface real subprocess output for non-streaming providers.
  • providerCLIName maps types.AgentName to the user-facing CLI binary in diagnostic try: 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_seconds override. 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
  • Manual: TTY happy path — progress phases render with in-place updates, final ✓ with duration + token count
  • Manual: --summary-timeout-seconds 3 on a real Claude generation → "model responded but did not finish" with cause + try rows
  • Manual: Network offline + --summary-timeout-seconds 10 → "provider sent request but received no response" (phase-distinguished from "never sent its request")
  • Manual: | cat → one line per event, no \r, no ANSI
  • Manual: ACCESSIBLE=1-> instead of
  • Manual: 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 --generate execution/timeout behavior and adds a new streaming Claude CLI path; failures could affect summary generation UX and error reporting across providers.

Overview
entire explain --generate now reports live, phase-based progress during summary generation (TTY in-place updates, non-TTY line output, ASCII in ACCESSIBLE mode) and uses an observable-state summaryAttempt to produce more actionable timeout messages.

Adds optional agent.StreamingTextGenerator support (capability + detector) and implements Claude Code streaming via claude --output-format stream-json with 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.

Copilot AI review requested due to automatic review settings April 16, 2026 04:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-json with an NDJSON stream parser.
  • Thread streaming progress events through summarization and render them in explain via summaryProgressWriter (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.

Comment thread cmd/entire/cli/agent/claudecode/claude_test.go Outdated
Comment thread cmd/entire/cli/explain.go Outdated
Comment thread cmd/entire/cli/explain.go Outdated
Comment thread cmd/entire/cli/explain.go Outdated
Comment thread cmd/entire/cli/explain.go Outdated
Comment thread cmd/entire/cli/summarize/summarize_test.go Outdated
Comment thread cmd/entire/cli/agent/claudecode/response.go Outdated
Comment thread cmd/entire/cli/agent/claudecode/generate.go Outdated
Comment thread cmd/entire/cli/agent/claudecode/response.go Outdated
Comment thread cmd/entire/cli/explain.go Outdated
Comment thread cmd/entire/cli/explain.go Outdated
@peyton-alt peyton-alt changed the title feat: stream-driven progress and TTY-gated deadline for explain --generate feat: stream-driven progress for explain --generate Apr 16, 2026
@peyton-alt
peyton-alt force-pushed the feat/explain-stream-progress branch from 33a095c to 5e80233 Compare April 16, 2026 17:05
@peyton-alt
peyton-alt force-pushed the feat/explain-summary-observability branch from cd538a4 to 92449c4 Compare April 16, 2026 17:19
@peyton-alt
peyton-alt force-pushed the feat/explain-stream-progress branch 4 times, most recently from c6c0c5a to 9c500a7 Compare April 17, 2026 02:22
@peyton-alt
peyton-alt changed the base branch from feat/explain-summary-observability to main April 17, 2026 02:22
@peyton-alt
peyton-alt force-pushed the feat/explain-stream-progress branch from f660315 to 0b9993e Compare May 14, 2026 23:32
@peyton-alt peyton-alt changed the title feat: stream-driven progress for explain --generate feat(explain): live progress and observable-state timeout diagnostic May 14, 2026
@peyton-alt

Copy link
Copy Markdown
Contributor Author

@BugBot review

Comment thread cmd/entire/cli/explain.go Outdated
Comment thread cmd/entire/cli/explain.go
@peyton-alt
peyton-alt force-pushed the feat/explain-stream-progress branch from 58ea7e9 to 0b9993e Compare May 15, 2026 14:26
peyton-alt added a commit that referenced this pull request May 15, 2026
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>
peyton-alt added a commit that referenced this pull request May 15, 2026
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>
peyton-alt added a commit that referenced this pull request May 15, 2026
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>
@peyton-alt

Copy link
Copy Markdown
Contributor Author

@BugBot review

@peyton-alt
peyton-alt marked this pull request as ready for review May 15, 2026 15:54
@peyton-alt
peyton-alt requested a review from a team as a code owner May 15, 2026 15:54

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ 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.

Comment thread cmd/entire/cli/agent/claudecode/generate_streaming.go Outdated
Comment thread cmd/entire/cli/agent/claudecode/generate_streaming.go
peyton-alt added a commit that referenced this pull request May 15, 2026
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>
peyton-alt added a commit that referenced this pull request May 20, 2026
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>
peyton-alt added a commit that referenced this pull request May 20, 2026
…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>
peyton-alt added a commit that referenced this pull request May 20, 2026
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>
peyton-alt added a commit that referenced this pull request May 20, 2026
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>
@peyton-alt
peyton-alt force-pushed the feat/explain-stream-progress branch from 57a6498 to 3afe589 Compare May 20, 2026 20:08
peyton-alt added a commit that referenced this pull request May 20, 2026
…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>
peyton-alt added a commit that referenced this pull request May 20, 2026
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>
peyton-alt and others added 7 commits July 20, 2026 21:07
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>
@peyton-alt
peyton-alt force-pushed the feat/explain-stream-progress branch from 3afe589 to 9366ac7 Compare July 21, 2026 04:55
peyton-alt added a commit that referenced this pull request Jul 21, 2026
…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>
peyton-alt added a commit that referenced this pull request Jul 21, 2026
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>
peyton-alt and others added 6 commits July 21, 2026 13:21
…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>
@peyton-alt
peyton-alt merged commit 0bfed39 into main Jul 23, 2026
10 checks passed
@peyton-alt
peyton-alt deleted the feat/explain-stream-progress branch July 23, 2026 14:11
peyton-alt added a commit that referenced this pull request Jul 24, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

summary_timeout_seconds is ignored by checkpoint explain --generate

3 participants