Skip to content

feat(backlog): persist full raw output when a headless triage/review call fails - #328

Open
tstapler wants to merge 1 commit into
mainfrom
worktree-agent-a70d7f75161a6543e
Open

feat(backlog): persist full raw output when a headless triage/review call fails#328
tstapler wants to merge 1 commit into
mainfrom
worktree-agent-a70d7f75161a6543e

Conversation

@tstapler

@tstapler tstapler commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Summary

Investigating stuck backlog item be676dab (an 8h52m headless triage session that produced no usable result) surfaced a real observability gap: there was no way to recover what the LLM actually returned.

  • ParseHeadlessTriageResult's parse-failure log line only includes a ~200-byte preview of the raw output.
  • The log file itself rotates out of ~/.stapler-squad/logs/ within roughly an hour — by the time anyone investigates a stuck item, both the full text and the log line describing it are usually gone.
  • Nothing durable/queryable recorded a call failure at all beyond that ephemeral log line. end_reason (classifyHeadlessCallError's bucket) already existed as a DB column but was never even surfaced over the RPC wire.
  • TriggerReReview's real call-error branch didn't create an ItemSession row at all — a failed re-review call was invisible even in the DB schema that already existed for triage.

Root cause

TriggerTriage's callErr/parseErr branches (server/services/backlog_service_triage.go) discarded the LLM's raw stdout entirely once they'd logged a preview of it.

Fix

Reuses this repo's existing scrollback/transcript-file precedent (session/review_transcript.go's WriteReviewTranscriptFile) rather than inventing a new storage mechanism:

  • session.WriteHeadlessFailureCapture (session/headless_failure_capture.go) writes the size-capped (256KB, tail-kept — the interesting content in a parse failure is usually near the end) raw output to a durable file under a new dir, config.HeadlessFailureCaptureDirOrDefault() (~/.stapler-squad/headless-failures/). Deliberately not inside the existing per-item triage-artifacts dir — readPlanFile feeds that directory's contents into later review/triage prompts, so writing raw failure text there would leak it into a future LLM's context.
  • New ItemSession.failure_capture_path column (ent schema + generated migration) references the file. A new, orthogonal UpdateItemSessionFailureCapture sets it alongside the existing end_reason column.
  • TriggerTriage wires this into both its callErr and parseErr branches.
  • TriggerReReview's callErr branch now also creates a best-effort audit ItemSession row (previously nothing was persisted there at all) with the same capture + a classified end_reason, before still returning the original RPC error.
  • classifyHeadlessCallError generalized to take an explicit call budget (was hardcoded to triageCallBudget) so TriggerReReview can share it with its own callTimeout.
  • end_reason and failure_capture_path added to the ItemSession proto message and threaded through to the frontend. BlockedNotice now renders the classified failure reason + capture path instead of an unexplained "No diagnostic data recorded." for a failed headless call — closing the loop from DB → RPC → UI for this one case, though there's no in-browser file-content viewer yet (see Follow-up below).

Separately investigated: zombie-reviewer durability

The brief also asked whether the zombie-reviewer distinction (list_workspace_peers' status: Active / lifecycle: gone, the bug PR #320 fixed) is persisted anywhere durable, or only visible transiently. Confirmed it already is: reconcileStuckReviewItems' zombie-session detection (session/backlog_lifecycle.go) writes a DB-backed BacklogStuckState row via markAbandonedReview, which survives a restart and is queryable via FindOpenStuckStates — not just an in-memory map or a log line. No gap found there; no change made.

Tests

  • session/headless_failure_capture_test.go — write/truncate-direction/no-op/durability behavior of the new capture helper.
  • server/services/backlog_service_triage_test.go — three new end-to-end regression tests driving the real TriggerTriage/TriggerReReview goroutines through a fake headless pool:
    • TestTriggerTriage_should_PersistFullRawOutputToDurableFile_When_HeadlessResultFailsToParse
    • TestTriggerTriage_should_PersistFailureCapture_When_HeadlessCallItselfErrors
    • TestTriggerReReview_should_PersistFailureCapture_When_HeadlessCallItselfErrors
  • web-app/src/components/backlog/detail/BlockedNotice.test.tsx — new describe block covering the endReason/failureCapturePath rendering, including that reviewVerdict.summary still wins when both happen to be present.

Verification

  • make build, make lint: clean.
  • go test ./session/... ./config/... ./server/services/...: all green.
  • Full make test / frontend jest hit two failures under parallel load, both confirmed pre-existing/unrelated (pass reliably 5/5 in isolation):

Follow-up (intentionally out of scope here)

Surfacing failureCapturePath in the UI currently shows the server-local path as text (useful for someone with server/SSH access) rather than fetching the file's content into the browser — that would need a new RPC (+ this repo's feature-registry + e2e-test overhead) to stream the capture file, which felt like its own PR rather than bundled into the core capture mechanism.

🤖 Generated with Claude Code

https://claude.ai/code/session_01W3683CH7Fs9zYR2yP3Dpba

…call fails

Investigating stuck triage item be676dab (an 8h52m headless triage session with
no usable result) showed there was no way to recover what the LLM actually
returned: ParseHeadlessTriageResult's parse-failure log line only includes a
~200-byte preview, and the log file itself rotates out of
~/.stapler-squad/logs/ within a few hours — by the time an operator
investigates, both the full output and the log line describing it are gone.
Nothing durable/queryable in the DB recorded a call failure's raw output
either; end_reason (classifyHeadlessCallError's bucket) already existed but
was never even surfaced over the wire.

Root cause: TriggerTriage's callErr and parseErr branches
(server/services/backlog_service_triage.go) discarded `raw` entirely on
failure, and TriggerReReview's real callErr branch didn't even create an
ItemSession row to record anything against.

Fix, reusing this repo's existing scrollback/transcript-file precedent
(session/review_transcript.go's WriteReviewTranscriptFile) rather than a new
storage mechanism:
- session.WriteHeadlessFailureCapture writes the size-capped (256KB, tail-kept)
  raw output to a durable file under a new config dir
  (~/.stapler-squad/headless-failures/, config.HeadlessFailureCaptureDirOrDefault),
  deliberately NOT inside the existing per-item triage-artifacts dir (which
  readPlanFile feeds into later review/triage prompts — writing there would
  leak raw failure text into future LLM context).
- New ItemSession.failure_capture_path column (ent schema + migration)
  references the file; a new orthogonal Update method sets it alongside the
  existing end_reason. TriggerTriage wires this into both its callErr and
  parseErr branches; TriggerReReview's callErr branch now also creates a
  best-effort audit ItemSession row (previously nothing was persisted there
  at all) with the same capture + classified end_reason.
- classifyHeadlessCallError generalized to take an explicit call budget
  (was hardcoded to triageCallBudget) so TriggerReReview can share it.
- end_reason and failure_capture_path added to the ItemSession proto message
  and threaded through to the frontend; BlockedNotice now renders the
  classified failure reason + capture path instead of an unexplained "No
  diagnostic data recorded." for a failed headless call.

Separately investigated (per the same brief) whether the zombie-reviewer
distinction (list_workspace_peers' status=Active/lifecycle=gone) that PR #320
fixed is persisted anywhere durable: confirmed it already is —
reconcileStuckReviewItems' zombie-session detection writes a DB-backed
BacklogStuckState row (survives restart, queryable via FindOpenStuckStates),
not just an in-memory/log signal. No gap there; no change made.

Regression tests: session/headless_failure_capture_test.go (write/truncate/
no-op behavior) plus three end-to-end tests exercising the real TriggerTriage/
TriggerReReview goroutines through a fake headless pool, asserting the full
raw output survives to a durable, DB-referenced file after both a parse
failure and a call error.

make build && make lint: clean. go test ./session/... ./config/...
./server/services/...: all green. Full `make test`/frontend jest hit two
failures under parallel load — the documented pre-existing session/tmux flake
(TestEnsureServerRunning_NoOp, PR #320 precedent) and an unrelated CAS-racer
test (TestReportDuplicate_ReportsDistinctMessage_WhenCASPreconditionFails);
both pass reliably in isolation (confirmed 5/5). Two frontend suites
(SessionDetail.embedded.test.tsx, BacklogEmptyState.test.tsx) also fail in
isolation on components this change never touches — pre-existing on this
branch, unrelated to this diff (3701 tests / 269 suites green otherwise).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3683CH7Fs9zYR2yP3Dpba
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

✅ Registry Validation

Registry Validation
===================

Building backend scanner...
Scanning backend features...
Wrote 117 feature files to /tmp/tmp.p3aPpx42al/backend
Wrote 15 feature files to /tmp/tmp.p3aPpx42al/backend
Wrote 45 feature files to /tmp/tmp.p3aPpx42al/backend
Wrote 8 feature files to /tmp/tmp.p3aPpx42al/backend
Wrote 12 feature files to /tmp/tmp.p3aPpx42al/backend

=== Backend Registry Diff ===
Committed: 181  Generated: 181  Divergence: 0.0%
⚠️  109 feature(s) missing // +api: marker (markerFound: false)

✅ Registry validation passed. Divergence: 0.0%

Test Coverage: 25/181 features have testIds (13.8%)

Divergence > 2% blocks merges. Coverage reporting is advisory only.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Go Benchmarks (Tier 1)

benchmarks/go/tier1-baseline.txt:98: missing iteration count
benchmarks/go/tier1-baseline.txt:198: missing iteration count
tier1-bench.txt:97: missing iteration count
tier1-bench.txt:195: missing iteration count
goos: linux
goarch: amd64
pkg: github.com/tstapler/stapler-squad/session
cpu: AMD EPYC 9V74 80-Core Processor                
                                            │ benchmarks/go/tier1-baseline.txt │
                                            │              sec/op              │
CircularBufferWrite_4KB-4                                          80.81n ± 0%
CircularBufferWrite_4KB_Allocs-4                                   80.46n ± 0%
CircularBufferGetRecent_4KB-4                                      503.1n ± 3%
CircularBufferGetAll-4                                             3.673µ ± 3%
GetTimeSinceLastMeaningfulOutput_HotPath-4                         70.47n ± 1%
GetTimeSinceLastMeaningfulOutput_ColdPath-4                        34.51n ± 0%
geomean                                                            175.5n

                                            │ benchmarks/go/tier1-baseline.txt │
                                            │               B/op               │
CircularBufferWrite_4KB-4                                         0.000 ± 0%
CircularBufferWrite_4KB_Allocs-4                                  0.000 ± 0%
CircularBufferGetRecent_4KB-4                                   4.000Ki ± 0%
CircularBufferGetAll-4                                          40.00Ki ± 0%
GetTimeSinceLastMeaningfulOutput_HotPath-4                        0.000 ± 0%
GetTimeSinceLastMeaningfulOutput_ColdPath-4                       0.000 ± 0%
geomean                                                                      ¹
¹ summaries must be >0 to compute geomean

                                            │ benchmarks/go/tier1-baseline.txt │
                                            │            allocs/op             │
CircularBufferWrite_4KB-4                                         0.000 ± 0%
CircularBufferWrite_4KB_Allocs-4                                  0.000 ± 0%
CircularBufferGetRecent_4KB-4                                     1.000 ± 0%
CircularBufferGetAll-4                                            1.000 ± 0%
GetTimeSinceLastMeaningfulOutput_HotPath-4                        0.000 ± 0%
GetTimeSinceLastMeaningfulOutput_ColdPath-4                       0.000 ± 0%
geomean                                                                      ¹
¹ summaries must be >0 to compute geomean

                              │ benchmarks/go/tier1-baseline.txt │
                              │               B/s                │
CircularBufferWrite_4KB-4                           47.21Gi ± 0%
CircularBufferGetRecent_4KB-4                       7.583Gi ± 3%
geomean                                             18.92Gi

cpu: Intel(R) Xeon(R) 6973P-C
                                            │ tier1-bench.txt │
                                            │     sec/op      │
CircularBufferWrite_4KB-4                         156.9n ± 8%
CircularBufferWrite_4KB_Allocs-4                  169.3n ± 8%
CircularBufferGetRecent_4KB-4                     510.7n ± 4%
CircularBufferGetAll-4                            3.689µ ± 3%
GetTimeSinceLastMeaningfulOutput_HotPath-4        43.28n ± 1%
GetTimeSinceLastMeaningfulOutput_ColdPath-4       22.31n ± 7%
geomean                                           190.8n

                                            │ tier1-bench.txt │
                                            │      B/op       │
CircularBufferWrite_4KB-4                        0.000 ± 0%
CircularBufferWrite_4KB_Allocs-4                 0.000 ± 0%
CircularBufferGetRecent_4KB-4                  4.000Ki ± 0%
CircularBufferGetAll-4                         40.00Ki ± 0%
GetTimeSinceLastMeaningfulOutput_HotPath-4       0.000 ± 0%
GetTimeSinceLastMeaningfulOutput_ColdPath-4      0.000 ± 0%
geomean                                                     ¹
¹ summaries must be >0 to compute geomean

                                            │ tier1-bench.txt │
                                            │    allocs/op    │
CircularBufferWrite_4KB-4                        0.000 ± 0%
CircularBufferWrite_4KB_Allocs-4                 0.000 ± 0%
CircularBufferGetRecent_4KB-4                    1.000 ± 0%
CircularBufferGetAll-4                           1.000 ± 0%
GetTimeSinceLastMeaningfulOutput_HotPath-4       0.000 ± 0%
GetTimeSinceLastMeaningfulOutput_ColdPath-4      0.000 ± 0%
geomean                                                     ¹
¹ summaries must be >0 to compute geomean

                              │ tier1-bench.txt │
                              │       B/s       │
CircularBufferWrite_4KB-4          24.32Gi ± 8%
CircularBufferGetRecent_4KB-4      7.470Gi ± 4%
geomean                            13.48Gi

pkg: github.com/tstapler/stapler-squad/session/detection/ratelimit
cpu: AMD EPYC 9V74 80-Core Processor                
                              │ benchmarks/go/tier1-baseline.txt │
                              │              sec/op              │
StripANSI_PlainText-4                                7.045n ± 3%
StripANSI_WithEscapes-4                              652.7n ± 1%
ProcessOutput_InactiveState-4                        6.635n ± 2%
geomean                                              31.25n

                              │ benchmarks/go/tier1-baseline.txt │
                              │               B/op               │
StripANSI_PlainText-4                               0.000 ± 0%
StripANSI_WithEscapes-4                             136.0 ± 0%
ProcessOutput_InactiveState-4                       0.000 ± 0%
geomean                                                        ¹
¹ summaries must be >0 to compute geomean

                              │ benchmarks/go/tier1-baseline.txt │
                              │            allocs/op             │
StripANSI_PlainText-4                               0.000 ± 0%
StripANSI_WithEscapes-4                             5.000 ± 0%
ProcessOutput_InactiveState-4                       0.000 ± 0%
geomean                                                        ¹
¹ summaries must be >0 to compute geomean

cpu: Intel(R) Xeon(R) 6973P-C
                              │ tier1-bench.txt │
                              │     sec/op      │
StripANSI_PlainText-4               3.649n ± 1%
StripANSI_WithEscapes-4             475.8n ± 5%
ProcessOutput_InactiveState-4       12.64n ± 0%
geomean                             28.00n

                              │ tier1-bench.txt │
                              │      B/op       │
StripANSI_PlainText-4              0.000 ± 0%
StripANSI_WithEscapes-4            136.0 ± 0%
ProcessOutput_InactiveState-4      0.000 ± 0%
geomean                                       ¹
¹ summaries must be >0 to compute geomean

                              │ tier1-bench.txt │
                              │    allocs/op    │
StripANSI_PlainText-4              0.000 ± 0%
StripANSI_WithEscapes-4            5.000 ± 0%
ProcessOutput_InactiveState-4      0.000 ± 0%
geomean                                       ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/tstapler/stapler-squad/session/queue
cpu: AMD EPYC 9V74 80-Core Processor                
                              │ benchmarks/go/tier1-baseline.txt │
                              │              sec/op              │
ReviewQueue_ConcurrentReads-4                       76.81n ± 18%
ReviewQueue_Add-4                                   495.8n ±  1%
geomean                                             195.2n

                              │ benchmarks/go/tier1-baseline.txt │
                              │               B/op               │
ReviewQueue_ConcurrentReads-4                       0.000 ± 0%
ReviewQueue_Add-4                                   640.0 ± 0%
geomean                                                        ¹
¹ summaries must be >0 to compute geomean

                              │ benchmarks/go/tier1-baseline.txt │
                              │            allocs/op             │
ReviewQueue_ConcurrentReads-4                       0.000 ± 0%
ReviewQueue_Add-4                                   4.000 ± 0%
geomean                                                        ¹
¹ summaries must be >0 to compute geomean

cpu: Intel(R) Xeon(R) 6973P-C
                              │ tier1-bench.txt │
                              │     sec/op      │
ReviewQueue_ConcurrentReads-4       138.5n ± 3%
ReviewQueue_Add-4                   355.6n ± 1%
geomean                             221.9n

                              │ tier1-bench.txt │
                              │      B/op       │
ReviewQueue_ConcurrentReads-4      0.000 ± 0%
ReviewQueue_Add-4                  640.0 ± 0%
geomean                                       ¹
¹ summaries must be >0 to compute geomean

                              │ tier1-bench.txt │
                              │    allocs/op    │
ReviewQueue_ConcurrentReads-4      0.000 ± 0%
ReviewQueue_Add-4                  4.000 ± 0%
geomean                                       ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/tstapler/stapler-squad/session/scrollback
cpu: AMD EPYC 9V74 80-Core Processor                
                                      │ benchmarks/go/tier1-baseline.txt │
                                      │              sec/op              │
CircularBuffer_ConcurrentReadWrite-4                         3.492µ ± 2%
CircularBuffer_BurstAppend-4                                 106.2µ ± 1%
CircularBuffer_GetLastN_LargeBuffer-4                        19.60µ ± 0%
CircularBuffer_GetRange_Sequential-4                         10.55µ ± 1%
CircularBufferAppend-4                                       103.7n ± 4%
CircularBufferGetLastN-4                                     2.286µ ± 1%
CircularBufferConcurrentAppend-4                             135.2n ± 1%
geomean                                                      3.051µ

                                      │ benchmarks/go/tier1-baseline.txt │
                                      │               B/op               │
CircularBuffer_ConcurrentReadWrite-4                        6.062Ki ± 0%
CircularBuffer_BurstAppend-4                                62.50Ki ± 0%
CircularBuffer_GetLastN_LargeBuffer-4                       56.00Ki ± 0%
CircularBuffer_GetRange_Sequential-4                        28.00Ki ± 0%
CircularBufferAppend-4                                        24.00 ± 0%
CircularBufferGetLastN-4                                    6.000Ki ± 0%
CircularBufferConcurrentAppend-4                              32.00 ± 0%
geomean                                                     3.077Ki

                                      │ benchmarks/go/tier1-baseline.txt │
                                      │            allocs/op             │
CircularBuffer_ConcurrentReadWrite-4                          2.000 ± 0%
CircularBuffer_BurstAppend-4                                 1.000k ± 0%
CircularBuffer_GetLastN_LargeBuffer-4                         1.000 ± 0%
CircularBuffer_GetRange_Sequential-4                          1.000 ± 0%
CircularBufferAppend-4                                        1.000 ± 0%
CircularBufferGetLastN-4                                      1.000 ± 0%
CircularBufferConcurrentAppend-4                              1.000 ± 0%
geomean                                                       2.962

                             │ benchmarks/go/tier1-baseline.txt │
                             │               B/s                │
CircularBuffer_BurstAppend-4                       574.5Mi ± 4%

cpu: Intel(R) Xeon(R) 6973P-C
                                      │ tier1-bench.txt │
                                      │     sec/op      │
CircularBuffer_ConcurrentReadWrite-4        2.804µ ± 2%
CircularBuffer_BurstAppend-4                88.60µ ± 4%
CircularBuffer_GetLastN_LargeBuffer-4       14.39µ ± 1%
CircularBuffer_GetRange_Sequential-4        9.446µ ± 1%
CircularBufferAppend-4                      86.90n ± 1%
CircularBufferGetLastN-4                    1.912µ ± 2%
CircularBufferConcurrentAppend-4            121.2n ± 1%
geomean                                     2.539µ

                                      │ tier1-bench.txt │
                                      │      B/op       │
CircularBuffer_ConcurrentReadWrite-4       6.062Ki ± 0%
CircularBuffer_BurstAppend-4               62.50Ki ± 0%
CircularBuffer_GetLastN_LargeBuffer-4      56.00Ki ± 0%
CircularBuffer_GetRange_Sequential-4       28.00Ki ± 0%
CircularBufferAppend-4                       24.00 ± 0%
CircularBufferGetLastN-4                   6.000Ki ± 0%
CircularBufferConcurrentAppend-4             32.00 ± 0%
geomean                                    3.077Ki

                                      │ tier1-bench.txt │
                                      │    allocs/op    │
CircularBuffer_ConcurrentReadWrite-4         2.000 ± 0%
CircularBuffer_BurstAppend-4                1.000k ± 0%
CircularBuffer_GetLastN_LargeBuffer-4        1.000 ± 0%
CircularBuffer_GetRange_Sequential-4         1.000 ± 0%
CircularBufferAppend-4                       1.000 ± 0%
CircularBufferGetLastN-4                     1.000 ± 0%
CircularBufferConcurrentAppend-4             1.000 ± 0%
geomean                                      2.962

                             │ tier1-bench.txt │
                             │       B/s       │
CircularBuffer_BurstAppend-4      688.9Mi ± 5%

pkg: github.com/tstapler/stapler-squad/session/tmux
cpu: AMD EPYC 9V74 80-Core Processor                
                             │ benchmarks/go/tier1-baseline.txt │
                             │              sec/op              │
StripANSICodes_PlainText-4                          7.418n ± 5%
StripANSICodes_WithEscapes-4                        619.2n ± 3%
IsBanner_PlainText-4                                468.1n ± 6%
geomean                                             129.1n

                             │ benchmarks/go/tier1-baseline.txt │
                             │               B/op               │
StripANSICodes_PlainText-4                         0.000 ± 0%
StripANSICodes_WithEscapes-4                       56.00 ± 0%
IsBanner_PlainText-4                               0.000 ± 0%
geomean                                                       ¹
¹ summaries must be >0 to compute geomean

                             │ benchmarks/go/tier1-baseline.txt │
                             │            allocs/op             │
StripANSICodes_PlainText-4                         0.000 ± 0%
StripANSICodes_WithEscapes-4                       4.000 ± 0%
IsBanner_PlainText-4                               0.000 ± 0%
geomean                                                       ¹
¹ summaries must be >0 to compute geomean

cpu: Intel(R) Xeon(R) 6973P-C
                             │ tier1-bench.txt │
                             │     sec/op      │
StripANSICodes_PlainText-4         3.644n ± 4%
StripANSICodes_WithEscapes-4       439.3n ± 2%
IsBanner_PlainText-4               304.2n ± 3%
geomean                            78.67n

                             │ tier1-bench.txt │
                             │      B/op       │
StripANSICodes_PlainText-4        0.000 ± 0%
StripANSICodes_WithEscapes-4      56.00 ± 0%
IsBanner_PlainText-4              0.000 ± 0%
geomean                                      ¹
¹ summaries must be >0 to compute geomean

                             │ tier1-bench.txt │
                             │    allocs/op    │
StripANSICodes_PlainText-4        0.000 ± 0%
StripANSICodes_WithEscapes-4      4.000 ± 0%
IsBanner_PlainText-4              0.000 ± 0%
geomean                                      ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/tstapler/stapler-squad/session/tokens
cpu: AMD EPYC 9V74 80-Core Processor                
                                   │ benchmarks/go/tier1-baseline.txt │
                                   │              sec/op              │
TokenParser_ProcessUserEntry-4                            5.379m ± 1%
DetectCommandsInText/NoSlash-4                            6.347n ± 4%
DetectCommandsInText/WithCommand-4                        1.488µ ± 2%
geomean                                                   3.704µ

                                   │ benchmarks/go/tier1-baseline.txt │
                                   │               B/op               │
TokenParser_ProcessUserEntry-4                         11.02Mi ± 0%
DetectCommandsInText/NoSlash-4                           0.000 ± 0%
DetectCommandsInText/WithCommand-4                       433.0 ± 0%
geomean                                                             ¹
¹ summaries must be >0 to compute geomean

                                   │ benchmarks/go/tier1-baseline.txt │
                                   │            allocs/op             │
TokenParser_ProcessUserEntry-4                           34.00 ± 0%
DetectCommandsInText/NoSlash-4                           0.000 ± 0%
DetectCommandsInText/WithCommand-4                       6.000 ± 0%
geomean                                                             ¹
¹ summaries must be >0 to compute geomean

cpu: Intel(R) Xeon(R) 6973P-C
                                   │ tier1-bench.txt │
                                   │     sec/op      │
TokenParser_ProcessUserEntry-4           3.309m ± 2%
DetectCommandsInText/NoSlash-4           3.450n ± 2%
DetectCommandsInText/WithCommand-4       1.139µ ± 1%
geomean                                  2.351µ

                                   │ tier1-bench.txt │
                                   │      B/op       │
TokenParser_ProcessUserEntry-4        11.02Mi ± 0%
DetectCommandsInText/NoSlash-4          0.000 ± 0%
DetectCommandsInText/WithCommand-4      433.0 ± 0%
geomean                                            ¹
¹ summaries must be >0 to compute geomean

                                   │ tier1-bench.txt │
                                   │    allocs/op    │
TokenParser_ProcessUserEntry-4          34.00 ± 0%
DetectCommandsInText/NoSlash-4          0.000 ± 0%
DetectCommandsInText/WithCommand-4      6.000 ± 0%
geomean                                            ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/tstapler/stapler-squad/session/unfinished
cpu: AMD EPYC 9V74 80-Core Processor                
                               │ benchmarks/go/tier1-baseline.txt │
                               │              sec/op              │
DiffShortstat/GitVCSReader-4                          3.331m ± 1%
DiffShortstat/GoGitVCSReader-4                        80.90n ± 0%
DiffShortstatCached-4                                 80.45n ± 0%
geomean                                               2.788µ

                               │ benchmarks/go/tier1-baseline.txt │
                               │               B/op               │
DiffShortstat/GitVCSReader-4                       62.58Ki ± 0%
DiffShortstat/GoGitVCSReader-4                       0.000 ± 0%
DiffShortstatCached-4                                0.000 ± 0%
geomean                                                         ¹
¹ summaries must be >0 to compute geomean

                               │ benchmarks/go/tier1-baseline.txt │
                               │            allocs/op             │
DiffShortstat/GitVCSReader-4                         360.0 ± 0%
DiffShortstat/GoGitVCSReader-4                       0.000 ± 0%
DiffShortstatCached-4                                0.000 ± 0%
geomean                                                         ¹
¹ summaries must be >0 to compute geomean

cpu: Intel(R) Xeon(R) 6973P-C
                               │ tier1-bench.txt │
                               │     sec/op      │
DiffShortstat/GitVCSReader-4         1.828m ± 3%
DiffShortstat/GoGitVCSReader-4       52.87n ± 1%
DiffShortstatCached-4                52.58n ± 2%
geomean                              1.719µ

                               │ tier1-bench.txt │
                               │      B/op       │
DiffShortstat/GitVCSReader-4      62.56Ki ± 0%
DiffShortstat/GoGitVCSReader-4      0.000 ± 0%
DiffShortstatCached-4               0.000 ± 0%
geomean                                        ¹
¹ summaries must be >0 to compute geomean

                               │ tier1-bench.txt │
                               │    allocs/op    │
DiffShortstat/GitVCSReader-4        360.0 ± 0%
DiffShortstat/GoGitVCSReader-4      0.000 ± 0%
DiffShortstatCached-4               0.000 ± 0%
geomean                                        ¹
¹ summaries must be >0 to compute geomean

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

E2E RPC Latency

list-sessions-ttfb-mean: 8ms (▲ slower +5.0%; baseline: 8ms)
list-sessions-total-mean: 13ms (▲ slower +18.7%; baseline: 11ms)

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Frontend Terminal Throughput

terminal-throughput-mean: 16 KB/s ▲ +18.5% (baseline: 14 KB/s)
terminal-throughput-p50: 16 KB/s ▲ +4.6% (baseline: 16 KB/s)

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

UX Analysis

Check Status Details
✅ Axe Core (WCAG 2.1 AA) success Critical/serious violations block merge
⚠️ Lighthouse Performance Score: unknown Warning if < 70 (non-blocking)
🤖 Claude UX Analysis Advisory See docs/qa/ for findings

Axe Core excludes terminal rendering areas (intentional design).
Lighthouse runs in desktop preset for this developer tool.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📊 Feature E2E Coverage

Feature coverage report unavailable

Run make e2e-report locally to view the full Allure report.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🎬 E2E Feature Demos

2 shard(s) recorded feature flows for this PR.

recordings shard 1
recordings shard 2

Demo preview opens directly in browser (single-file HTML). Raw WebM recordings in ZIP. Expires after 30 days.

@tstapler
tstapler marked this pull request as ready for review August 10, 2026 16:16
Copilot AI lite review requested due to automatic review settings August 10, 2026 16:16

Copilot AI 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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds durable capture + surfacing of full raw headless triage/review output on call/parse failures to close an observability gap (recoverable beyond log rotation), and threads the failure metadata through DB → RPC → UI.

Changes:

  • Persist size-capped raw headless output to a durable file and store its path on ItemSession (failure_capture_path) along with a surfaced end_reason.
  • Wire capture + persistence into TriggerTriage failure branches and add best-effort audit ItemSession creation on TriggerReReview call failure.
  • Expose the new fields to the frontend and render better failure detail in BlockedNotice with new tests.

Reviewed changes

Copilot reviewed 16 out of 25 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
web-app/src/lib/hooks/useBacklogService.ts Maps endReason + failureCapturePath from proto into LinkedSession.
web-app/src/gen/session/v1/backlog_pb.ts Regenerated TS proto types including new ItemSession fields.
web-app/src/components/backlog/detail/BlockedNotice.tsx Renders headless failure reason + capture path in the “missing diagnostic data” notice.
web-app/src/components/backlog/detail/BlockedNotice.test.tsx Adds UI tests for rendering endReason/failureCapturePath behavior + precedence.
session/storage_backlog.go Adds repository method to persist failure_capture_path.
session/storage.go Adds storage façade method for updating failure_capture_path.
session/repository.go Extends ItemSessionSummary to include FailureCapturePath.
session/headless_failure_capture.go Adds helper to write size-capped raw output to a durable “headless-failures” dir.
session/headless_failure_capture_test.go Adds unit tests for capture helper behavior (tail truncation, no-op, durability).
session/ent_repository_backlog.go Maps ent ItemSession.FailureCapturePathItemSessionSummary.
session/ent/schema/item_session.go Adds failure_capture_path column to ent schema.
server/services/backlog_service_triage.go Adds capture helper usage; persists capture path; generalizes error classifier signature; adds re-review audit session persist.
server/services/backlog_service_triage_test.go Adds regression tests covering triage parse failure capture, call error capture, and re-review call error capture.
server/services/backlog_service.go Threads end_reason and failure_capture_path into the proto mapping.
proto/session/v1/backlog.proto Adds end_reason and failure_capture_path to ItemSession.
config/config.go Adds HeadlessFailureCaptureDirOrDefault() for capture file location.
Files not reviewed (9)
  • gen/proto/go/session/v1/backlog.pb.go: Generated file
  • session/ent/itemsession.go: Generated file
  • session/ent/itemsession/itemsession.go: Generated file
  • session/ent/itemsession/where.go: Generated file
  • session/ent/itemsession_create.go: Generated file
  • session/ent/itemsession_update.go: Generated file
  • session/ent/migrate/schema.go: Generated file
  • session/ent/mutation.go: Generated file
  • session/ent/runtime.go: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +1893 to +1908
func (s *BacklogService) captureHeadlessFailure(sessionUUID, raw string) string {
if raw == "" {
return ""
}
dir, dirErr := s.cfg.HeadlessFailureCaptureDirOrDefault()
if dirErr != nil {
log.WarningLog.Printf("[captureHeadlessFailure] resolve capture dir: %v", dirErr)
return ""
}
path, writeErr := session.WriteHeadlessFailureCapture(dir, sessionUUID, raw, session.DefaultHeadlessFailureCaptureMaxBytes)
if writeErr != nil {
log.WarningLog.Printf("[captureHeadlessFailure] write capture file session=%s: %v", sessionUUID, writeErr)
return ""
}
return path
}
Comment on lines +2527 to +2536
errType := classifyHeadlessCallError(callErr, time.Since(callStart), callTimeout)
capturePath := s.captureHeadlessFailure(headlessReReviewUUIDPrefix+uuid.New().String(), reviewResult)
log.ErrorLog.Printf("[TriggerReReview] headless re-review call failed item=%s errType=%s capture=%s: %v", item.ID, errType, capturePath, callErr)
failCleanupCtx, failCleanupCancel := context.WithTimeout(context.Background(), 10*time.Second)
if failIS, failCreateErr := s.storage.CreateItemSession(failCleanupCtx, session.ItemSessionData{
ItemID: item.ID,
SessionUUID: headlessReReviewUUIDPrefix + uuid.New().String(),
SessionRole: session.SessionRoleReview,
AcSnapshot: session.AcCriteriaJSON(acSnapshotJSON),
}); failCreateErr != nil {
Comment on lines +70 to +75
absPath = filepath.Join(dir, headlessFailureCaptureFilePrefix+sessionUUID+".txt")
if writeErr := os.WriteFile(absPath, []byte(content), 0o644); writeErr != nil {
return "", fmt.Errorf("failed to write headless failure capture file: %w", writeErr)
}

return absPath, nil
Comment on lines +61 to +64
content := raw
if int64(len(content)) > maxBytes {
content = headlessFailureCaptureTruncationMarker + content[int64(len(content))-maxBytes:]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants