Skip to content

fix(backlog): auto-respawn triage for items orphaned in idea - #169

Closed
tstapler wants to merge 5 commits into
mainfrom
worktree-agent-aa4517075c8f84c7f
Closed

fix(backlog): auto-respawn triage for items orphaned in idea#169
tstapler wants to merge 5 commits into
mainfrom
worktree-agent-aa4517075c8f84c7f

Conversation

@tstapler

Copy link
Copy Markdown
Owner

Summary

  • reconcileOrphanedTriageItems previously only detected and notified when an idea-status item's triage session died mid-run — its own doc comment falsely claimed the item "self-heals once it leaves idea," but nothing ever moved it out of idea.
  • Adds a TriageRespawner seam (mirrors PRFixSpawner) and BacklogService.AutoRetriggerTriage, which re-triggers triage via the existing TriggerTriage entry point once the item is confirmed orphaned (2h+ stale, per the existing staleness gate), with an active-session re-check and a maxAutoReworkIterations cap to prevent runaway retriggers.
  • Corrects the misleading doc comment on reconcileOrphanedTriageItems to describe what the code now actually does.

Context

Found during a systematic autonomy-gap audit (docs/tasks/backlog-feature-improvement.md, row 8): idea-status items whose triage session crashed, was killed, or never completed after a server restart were flagged and notified but never actually retried — they sat stuck at idea forever until a human noticed and manually re-triggered triage.

Changes

  • session/backlog_lifecycle.go: new TriageRespawner interface + SetTriageRespawner/getTriageRespawner; reconcileOrphanedTriageItems now dispatches an async respawn (bounded by the existing reviewSem limiter) once past the existing 2h staleness threshold, in addition to the existing notify; doc comment corrected.
  • server/services/backlog_service_triage.go: new AutoRetriggerTriage (implements TriageRespawner) and hasFreshTriageSession helper — status guard, "might still be running" re-check (headless triage has no liveness signal, so elapsed time since session creation is the proxy), rework-cap guard reusing maxAutoReworkIterations/notifyReworkCapHit, then delegates to the existing TriggerTriage RPC handler.
  • server/dependencies.go: wires backlogLifecycleListener.SetTriageRespawner(backlogSvc).
  • Tests: TestReconcileOrphanedTriageItems_AutoRetriggersTriage_When_SessionStale, TestReconcileOrphanedTriageItems_NoRespawn_WhenNoTriageRespawnerConfigured (session package); TestAutoRetriggerTriage_ReworkCapHit_LeavesInIdeaAndNotifies, TestAutoRetriggerTriage_FreshTriageSession_SkipsWithoutDoubleSpawn, TestAutoRetriggerTriage_NoActiveSession_TriggersTriage (server/services package).

Impact

  • Scope: backlog reconciliation loop only (idea-status items with a dead triage session). No API/proto changes.
  • Breaking Changes: none.
  • Performance: negligible — respawn dispatch is async and bounded by the existing semaphore; the actual headless triage call is bounded by its own existing triageSem (max 8 concurrent).
  • Dependencies: none new.

Testing

  • go test ./session/... — full suite passes
  • go test ./server/services/... — full suite passes
  • golangci-lint run --enable=nilnil,staticcheck,ineffassign,govet ./session/... ./server/... — 0 issues
  • Manual verification: not performed (reconciler runs on a 60s tick in production; behavior verified via unit tests simulating the stale-session/cap/fresh-session scenarios)

Reviewer Notes

  • Focus areas: the hasFreshTriageSession staleness re-check (no liveness signal exists for headless triage sessions, unlike work/review sessions) and the choice to reuse l.reviewSem for the dispatch limiter (matches the precedent set by the sibling abandoned-review-respawn fix, PR fix(backlog): auto-respawn review for items stuck abandoned in review #168, currently open/unmerged).
  • Known limitations: no separate post-detection grace period before respawn — the existing 2h staleness threshold itself is the gate, matching reconcileStaleWorkSessions' established pattern in this file.
  • Follow-up tasks: none identified.

tstapler and others added 2 commits July 17, 2026 16:52
reconcileOrphanedTriageItems (session/backlog_lifecycle.go) previously only
wrote a stuck row and notified an operator once a triage session's crash/kill/
restart went undetected past the 2h staleness threshold — its own doc comment
claimed the item "self-heals once it leaves idea," but nothing ever moved it
out of idea, so it sat there forever until a human noticed and manually
re-triggered triage (docs/tasks/backlog-feature-improvement.md, row 8).

Adds a TriageRespawner seam (mirrors the existing PRFixSpawner pattern) and
wires BacklogService.AutoRetriggerTriage into it via the existing TriggerTriage
entry point, following the same shape as AutoReopenForPRFix/
AutoReopenAfterFailedReview: status guard, an elapsed-time "might still be
running" check (headless triage has no liveness signal, so age is the only
proxy — mirrors reconcileOrphanedTriageItems' own staleness gate), and a cap
on triage-session count (reusing maxAutoReworkIterations/notifyReworkCapHit)
to stop a persistently-crashing item from re-triggering forever. Dispatched
async under the existing reviewSem limiter so TriggerTriage's dispatch never
blocks the synchronous reconcile sweep. Fires on the same tick the 2h
staleness threshold is crossed — no separate grace period, matching
reconcileStaleWorkSessions' established pattern (the staleness check itself is
the "don't act on first detection" gate).

Doc comment on reconcileOrphanedTriageItems corrected to describe what the
code now actually does, rather than the previously-false self-heal claim.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…075c8f84c7f

# Conflicts:
#	server/dependencies.go
#	server/services/backlog_service_triage.go
#	session/backlog_lifecycle.go
#	session/backlog_lifecycle_test.go
@github-actions

Copy link
Copy Markdown
Contributor

✅ Registry Validation

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

Building backend scanner...
Scanning backend features...
Wrote 116 feature files to /tmp/tmp.7Qc91Ys1OD/backend
Wrote 14 feature files to /tmp/tmp.7Qc91Ys1OD/backend
Wrote 38 feature files to /tmp/tmp.7Qc91Ys1OD/backend
Wrote 6 feature files to /tmp/tmp.7Qc91Ys1OD/backend
Wrote 10 feature files to /tmp/tmp.7Qc91Ys1OD/backend

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

✅ Registry validation passed. Divergence: 0.0%

Test Coverage: 10/172 features have testIds (5.8%)

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

@github-actions

github-actions Bot commented Jul 18, 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:98: missing iteration count
tier1-bench.txt:199: missing iteration count
goos: linux
goarch: amd64
pkg: github.com/tstapler/stapler-squad/session
cpu: AMD EPYC 7763 64-Core Processor                
                                            │ benchmarks/go/tier1-baseline.txt │
                                            │              sec/op              │
CircularBufferWrite_4KB-4                                          80.34n ± 2%
CircularBufferWrite_4KB_Allocs-4                                   81.50n ± 1%
CircularBufferGetRecent_4KB-4                                      489.8n ± 1%
CircularBufferGetAll-4                                             3.909µ ± 8%
GetTimeSinceLastMeaningfulOutput_HotPath-4                         65.54n ± 0%
GetTimeSinceLastMeaningfulOutput_ColdPath-4                        33.09n ± 0%
geomean                                                            173.4n

                                            │ 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.48Gi ± 2%
CircularBufferGetRecent_4KB-4                       7.788Gi ± 1%
geomean                                             19.23Gi

cpu: AMD EPYC 9V74 80-Core Processor                
                                            │ tier1-bench.txt │
                                            │     sec/op      │
CircularBufferWrite_4KB-4                         62.11n ± 0%
CircularBufferWrite_4KB_Allocs-4                  62.22n ± 2%
CircularBufferGetRecent_4KB-4                     440.4n ± 5%
CircularBufferGetAll-4                            3.128µ ± 3%
GetTimeSinceLastMeaningfulOutput_HotPath-4        54.44n ± 0%
GetTimeSinceLastMeaningfulOutput_ColdPath-4       26.78n ± 0%
geomean                                           140.7n

                                            │ 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          61.42Gi ± 2%
CircularBufferGetRecent_4KB-4      8.663Gi ± 5%
geomean                            23.07Gi

pkg: github.com/tstapler/stapler-squad/session/detection/ratelimit
cpu: AMD EPYC 7763 64-Core Processor                
                              │ benchmarks/go/tier1-baseline.txt │
                              │              sec/op              │
StripANSI_PlainText-4                                6.873n ± 0%
StripANSI_WithEscapes-4                              746.7n ± 0%
ProcessOutput_InactiveState-4                        6.314n ± 1%
geomean                                              31.88n

                              │ 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: AMD EPYC 9V74 80-Core Processor                
                              │ tier1-bench.txt │
                              │     sec/op      │
StripANSI_PlainText-4               5.461n ± 2%
StripANSI_WithEscapes-4             509.5n ± 3%
ProcessOutput_InactiveState-4       5.188n ± 0%
geomean                             24.35n

                              │ 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 7763 64-Core Processor                
                              │ benchmarks/go/tier1-baseline.txt │
                              │              sec/op              │
ReviewQueue_ConcurrentReads-4                        83.19n ± 6%
ReviewQueue_Add-4                                    506.0n ± 1%
geomean                                              205.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: AMD EPYC 9V74 80-Core Processor                
                              │ tier1-bench.txt │
                              │     sec/op      │
ReviewQueue_ConcurrentReads-4      69.02n ± 11%
ReviewQueue_Add-4                  400.9n ±  2%
geomean                            166.3n

                              │ 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 7763 64-Core Processor                
                                      │ benchmarks/go/tier1-baseline.txt │
                                      │              sec/op              │
CircularBuffer_ConcurrentReadWrite-4                         3.974µ ± 2%
CircularBuffer_BurstAppend-4                                 102.4µ ± 1%
CircularBuffer_GetLastN_LargeBuffer-4                        19.93µ ± 0%
CircularBuffer_GetRange_Sequential-4                         10.92µ ± 1%
CircularBufferAppend-4                                       97.99n ± 0%
CircularBufferGetLastN-4                                     2.260µ ± 2%
CircularBufferConcurrentAppend-4                             127.1n ± 1%
geomean                                                      3.057µ

                                      │ 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                       596.2Mi ± 1%

cpu: AMD EPYC 9V74 80-Core Processor                
                                      │ tier1-bench.txt │
                                      │     sec/op      │
CircularBuffer_ConcurrentReadWrite-4        2.761µ ± 1%
CircularBuffer_BurstAppend-4                83.69µ ± 3%
CircularBuffer_GetLastN_LargeBuffer-4       15.10µ ± 2%
CircularBuffer_GetRange_Sequential-4        8.134µ ± 1%
CircularBufferAppend-4                      79.73n ± 1%
CircularBufferGetLastN-4                    1.806µ ± 1%
CircularBufferConcurrentAppend-4            103.2n ± 0%
geomean                                     2.372µ

                                      │ 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      729.3Mi ± 3%

pkg: github.com/tstapler/stapler-squad/session/tmux
cpu: AMD EPYC 7763 64-Core Processor                
                             │ benchmarks/go/tier1-baseline.txt │
                             │              sec/op              │
StripANSICodes_PlainText-4                          6.877n ± 5%
StripANSICodes_WithEscapes-4                        694.1n ± 0%
IsBanner_PlainText-4                                484.9n ± 0%
geomean                                             132.3n

                             │ 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: AMD EPYC 9V74 80-Core Processor                
                             │ tier1-bench.txt │
                             │     sec/op      │
StripANSICodes_PlainText-4         5.745n ± 0%
StripANSICodes_WithEscapes-4       473.8n ± 0%
IsBanner_PlainText-4               359.7n ± 1%
geomean                            99.29n

                             │ 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 7763 64-Core Processor                
                                   │ benchmarks/go/tier1-baseline.txt │
                                   │              sec/op              │
TokenParser_ProcessUserEntry-4                            5.352m ± 1%
DetectCommandsInText/NoSlash-4                            7.497n ± 0%
DetectCommandsInText/WithCommand-4                        1.659µ ± 1%
geomean                                                   4.053µ

                                   │ 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: AMD EPYC 9V74 80-Core Processor                
                                   │ tier1-bench.txt │
                                   │     sec/op      │
TokenParser_ProcessUserEntry-4           4.280m ± 1%
DetectCommandsInText/NoSlash-4           5.184n ± 5%
DetectCommandsInText/WithCommand-4       1.155µ ± 1%
geomean                                  2.948µ

                                   │ 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 7763 64-Core Processor                
                               │ benchmarks/go/tier1-baseline.txt │
                               │              sec/op              │
DiffShortstat/GitVCSReader-4                          3.176m ± 1%
DiffShortstat/GoGitVCSReader-4                        76.92n ± 0%
DiffShortstatCached-4                                 75.55n ± 1%
geomean                                               2.643µ

                               │ benchmarks/go/tier1-baseline.txt │
                               │               B/op               │
DiffShortstat/GitVCSReader-4                       56.57Ki ± 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: AMD EPYC 9V74 80-Core Processor                
                               │ tier1-bench.txt │
                               │     sec/op      │
DiffShortstat/GitVCSReader-4         2.681m ± 1%
DiffShortstat/GoGitVCSReader-4       62.70n ± 0%
DiffShortstatCached-4                62.93n ± 0%
geomean                              2.195µ

                               │ tier1-bench.txt │
                               │      B/op       │
DiffShortstat/GitVCSReader-4      56.58Ki ± 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 Jul 18, 2026

Copy link
Copy Markdown
Contributor

E2E RPC Latency

list-sessions-ttfb-mean: 5ms (▼ faster -34.1%; baseline: 7ms)
list-sessions-total-mean: 6ms (▼ faster -54.1%; baseline: 13ms)

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Frontend Terminal Throughput

terminal-throughput-mean: 15 KB/s ▼ -0.7% (baseline: 16 KB/s)
terminal-throughput-p50: 16 KB/s ▲ +1.9% (baseline: 16 KB/s)

@github-actions

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 Jul 18, 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.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Registry Validation

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

Building backend scanner...
Scanning backend features...
Wrote 116 feature files to /tmp/tmp.esOoWomICi/backend
Wrote 14 feature files to /tmp/tmp.esOoWomICi/backend
Wrote 38 feature files to /tmp/tmp.esOoWomICi/backend
Wrote 6 feature files to /tmp/tmp.esOoWomICi/backend
Wrote 10 feature files to /tmp/tmp.esOoWomICi/backend

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

✅ Registry validation passed. Divergence: 0.0%

Test Coverage: 10/172 features have testIds (5.8%)

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

These match .gitignore rules already but were tracked from before the
rule was added, so gitignore had no effect and automation kept
committing per-worktree state to them. git rm --cached only; local
copies are untouched.

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

Copy link
Copy Markdown
Contributor

✅ Registry Validation

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

Building backend scanner...
Scanning backend features...
Wrote 116 feature files to /tmp/tmp.QtUc0B7l5l/backend
Wrote 14 feature files to /tmp/tmp.QtUc0B7l5l/backend
Wrote 38 feature files to /tmp/tmp.QtUc0B7l5l/backend
Wrote 6 feature files to /tmp/tmp.QtUc0B7l5l/backend
Wrote 10 feature files to /tmp/tmp.QtUc0B7l5l/backend

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

✅ Registry validation passed. Divergence: 0.0%

Test Coverage: 10/172 features have testIds (5.8%)

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

tstapler added a commit that referenced this pull request Jul 20, 2026
…kstop (#195)

.backlog-context.md and .claude/commands/backlog/*.md are written into every
backlog work-session worktree, then git-excluded via $GIT_DIR/info/exclude.
That only stops NEW files from being staged — it does nothing once a file is
already tracked, which is why this repo's own history has ~20 manual
"chore(backlog): untrack backlog context/command files" commits, and why two
currently-open PRs (#169, #170) both carry an incidental deletion of a stale,
committed .backlog-context.md from an unrelated item's context.

WriteSlashCommands and WriteBacklogContextFile now call
selfHealWorktreeScaffolding before writing: it walks the git index (via
go-git, not a subshell) for any entry matching backlogExcludePatterns and
untracks it (git-rm-cached semantics — working tree file is left alone), so
a branch that ever got one of these files committed self-heals on the very
next spawn/reattach/reopen instead of requiring a manual untrack commit.

Also adds a CI backstop (.github/workflows/backlog-scaffolding-guard.yml)
that fails any PR whose diff adds/modifies a backlogExcludePatterns file, as
a second, independent layer for this repo. This only protects stapler-squad's
own repo — it cannot reach into the arbitrary target repos backlog items
point at, which is why the self-heal above (not CI) is the primary fix.

On the "scoped by session/backlog item so it's not reused poorly" half of
the request: chose to harden the existing worktree-relative storage rather
than relocate it to a session/item-scoped path outside the worktree
(~/.stapler-squad/backlog-context/<item>/...). Both WriteBacklogContextFile
and WriteSlashCommands already do a full atomic overwrite on every
spawn/reattach/reopen, so no session ever reads content from a different
item/session — the actual "reused poorly" risk was exclusively the
already-tracked git pollution this fix closes. Relocating storage is a
larger, real architecture change (touches taskProtocolBlock's literal
`.backlog-context.md` references, needs a session UUID that doesn't exist
yet at the pre-spawn write callsite, and increases collision surface with
other agents' concurrent work in these same files) and is left as a
follow-up rather than folded into this bug fix.

CleanupSlashCommands/CleanupBacklogContextFile remain uncalled from any
production teardown path, and that's intentional, not an oversight:
shipViaAgentOrFallback relies on ship.md still existing after a work session
exits review to re-invoke /backlog/ship as a one-shot call. Wiring cleanup
into a review-exit teardown would delete ship.md out from under that path.
Documented this directly in both functions' doc comments so it isn't
"fixed" again by mistake.

Regression tests: a worktree with .backlog-context.md (or
.claude/commands/backlog/status.md) already committed gets it untracked on
the next Write call with fresh on-disk content; respawning the same
worktree path for a different item never leaks the prior item's content;
the self-heal helper no-ops cleanly on a non-git directory and leaves
unrelated tracked files alone.


Claude-Session: https://claude.ai/code/session_01BxNAMeGteuzNyN46Q4zAn1

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
tstapler added a commit that referenced this pull request Jul 25, 2026
…on (#184)

* fix(ui): restore checkpoint/fork UI lost in benchmarks merge regression

The benchmarks PR (#17, 9479a1e) carried an older SessionCard.tsx and
silently overwrote 213 lines of checkpoint/fork UI introduced by the
session-resumption PR (#18, 38d40fd) merged 4 hours earlier.

Restored three files from 38d40fd:
- SessionCard.tsx: CheckpointProto import, 3 props, 10 useState vars,
  handlers (create/submit/cancel/fork), checkpoint + fork dialogs,
  📍 Checkpoint and 🍴 Fork action buttons
- useSessionService.ts: createCheckpoint, listCheckpoints, forkSession
  hooks + CheckpointProto import + interface declarations
- page.tsx: destructure + pass checkpoint/fork props to SessionCard

TypeScript compiles clean (tsc --noEmit).

* feat(checkpoints): wire HistoryLinker startup, shutdown capture, and pre-switch checkpoint

Task A - HistoryLinker startup pipeline:
- Add NewHistoryLinkerFromRealInspector() production constructor with fsnotify
  watcher on ~/.claude/projects/ for fast JSONL detection
- Add HistoryLinker.Instances() snapshot getter for live session set
- Wire HistoryLinker.Start() in server.go; wire Add/RemoveInstance into
  ExternalDiscovery callbacks so discovered sessions are tracked

Task B - Shutdown capture (cold restore CWD):
- Add GetPaneCurrentPath() to TmuxSession (tmux display-message #{pane_current_path})
- Add CaptureCurrentState() to Instance with guards (not started, paused, dead tmux)
- Register shutdown hook in server.go: captures pane CWDs then SaveInstances with
  4-second deadline to keep within SIGTERM kill window
- Shutdown hook uses HistoryLinker.Instances() (live set) instead of startup snapshot
  so externally-discovered sessions are also captured

Task C - Auto-checkpoint before workspace switch:
- Create "pre-switch: <target>" checkpoint in SwitchWorkspace before switching;
  non-fatal if it fails so the switch always proceeds

Tests (8 new):
- GetPaneCurrentPath: trim behavior + error wrapping
- CaptureCurrentState: not-started, paused, dead-tmux guard branches
- NewHistoryLinkerFromRealInspector: smoke test (non-nil constructor)
- HistoryLinker.Instances(): snapshot correctness + independence from internal state

Code quality fixes:
- defer stateMutex.Unlock() in CaptureCurrentState
- os.UserHomeDir() error now logged instead of silently producing /.claude/projects

* fix(web): add missing checkpoint props to SessionListProps

SessionList was passing onCreateCheckpoint, onListCheckpoints, and
onForkFromCheckpoint to SessionCard but these were absent from the
SessionListProps interface and function signature, causing a TypeScript
compile error in CI.

* test: add missing happy-path and integration tests for checkpoints

- TestInstance_CaptureCurrentState_UpdatesWorkingDir: verifies WorkingDir
  is populated from the tmux pane path on graceful shutdown (Story 1.2.1)
- TestHistoryLinker_LinksUUIDToSession: verifies the scan loop propagates
  a conversation UUID to an instance via correlateSession (Story 1.1.4)
- TestHistoryLinker_IdempotentRelink: verifies correlateSession returns
  early when HasClaudeSession() is already true
- Add 5 missing CSS module classes for the fork-from-checkpoint dialog
  (.forkEmptyMessage, .forkCheckpointList, .forkCheckpointItem,
   .forkCheckpointLabel, .forkGitSha)

* docs(terminal-visibility-resync): SDD phases 1-4 (requirements, research, plan, validation)

Requirements derived from backlog item 7728f6df-268a-4578-9066-c300ff69269b,
grounded against the actual codebase rather than the (stale) issue text --
the referenced StateApplicator/useTerminalSnapshot primitives don't exist;
the real fix wires up requestFullResync/markResyncComplete/markPaneResponseReceived
(already implemented in useTerminalFlowControl.ts but never exposed/called)
and fixes useDebouncedCallback's useState-backed timer bug.

Plan went through two rounds of architecture + adversarial review (both
BLOCKED -> CONCERNS after fixes: Promise<void> return types, session-switch
cleanup, connect()-in-flight guard, hook extraction to useVisibilityResync.ts)
plus a Product/UX/Engineering triad review (UX gap fixed by adding the
2s-intermediate-banner story the plan's own Pattern Decisions table had
already promised but never implemented).

No source code changed yet -- planning artifacts only.

* feat(terminal): expose resync primitives from useTerminalStream

Add requestFullResync, markResyncComplete, and markPaneResponseReceived
to TerminalStreamResult so TerminalOutput can trigger/track a full pane
resync without new prop plumbing. Also widen connect/disconnect's
declared return types from () => void to Promise<void>-returning,
matching their existing async implementations — a pre-existing type
bug that Phase 2's stall watchdog needs corrected to type-check its
disconnect().then(() => connect()) chain.

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

* fix(hooks): useDebouncedCallback root-cause double-fire bug

Store the debounce timer id in a useRef instead of useState so two
calls landing in the same JS tick (e.g. visibilitychange + focus
firing back-to-back) don't read a stale timer id from a batched
setState update and let both timers fire. Wrap the returned callback
in useCallback keyed on [callback, delay] so it's referentially
stable across re-renders, making it safe to use in dependency arrays
and as an event listener reference.

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

* test(terminal): AC7 integration proof for full-resync repaint via TerminalStreamManager.write

Proves, against a real (unmocked) @xterm/xterm Terminal and real
TerminalStreamManager, that writing the server's clearAndHome-prefixed
resync payload produces a genuine full repaint over stale/corrupted prior
content, verified by reading back the actual rendered buffer rather than
asserting on a mock call count.

Adds a cross-reference comment on connectrpc_websocket.go's
ansiSnapshotPrefix constant pointing at the new test's duplicated
clearAndHome fixture, since the two have no compile-time link.

* feat(terminal): visibility/focus resync via useVisibilityResync hook

Implements Epic 2.1 of terminal-visibility-resync: a standalone
useVisibilityResync hook that debounces document visibilitychange/window
focus events (300ms) and, while connected, triggers exactly one
requestFullResync(true) per debounce window to repaint a render-desynced
xterm buffer after a backgrounded tab regains focus (AC1). While
disconnected (and not mid-handshake), it falls back to a direct connect()
+ setShowReconnectButton(true) (AC4). A 4000ms stall watchdog force-clears
pending resync state and runs disconnect().then(connect) if no output
arrives (AC5), and a 2000ms intermediate timer surfaces the existing
reconnecting-banner UI for slow resyncs. The hook never touches focus
(AC3) and tears down session-scoped pending state on sessionId change,
including a guard against a debounced call that fires mid-flight against
a session it was no longer scheduled for.

Wired into TerminalOutput.tsx via a small hook call plus a
notifyResyncOutputReceived() call in handleOutput (routed through a
ref-mirror since the callback isn't available until after
useTerminalStream/useVisibilityResync are both called, avoiding a TDZ
reference-before-declaration issue in the plan's literal sketch).
XtermTerminal.tsx, useTerminalStream.ts's NEXT_PUBLIC_RECONNECT_V2
listener, and handleManualReconnect's body are all unmodified (AC6,
verified via scoped git diff).

Also updates the mocked useTerminalStream() return objects across six
existing TerminalOutput test files to include the three
already-committed-but-previously-unmocked fields
(requestFullResync/markResyncComplete/markPaneResponseReceived), which
were undefined and caused the new hook's session-cleanup effect to throw
on unmount.

19 new tests in useVisibilityResync.test.ts cover AC1, AC3, AC4, AC5, the
Story 2.1.8 banner behavior, and the Story 2.1.6 session-switch cleanup
(including the mid-debounce-switch guard).

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

* refactor(terminal): address sdd:6-verify Layer 1/2 findings for visibility resync

Move notifyResyncOutputReceivedRef sync into a useEffect instead of the
render body (both the idiom and architecture review agents independently
flagged the mid-render ref write as inconsistent with this file's own
ref-mirror convention). Remove the redundant unmount-only cleanup effect
in useVisibilityResync, superseded by the sessionId-keyed effect that
already performs a strict superset of the same cleanup.

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

* chore: restore backlog scaffolding files accidentally deleted by prior commit

The prior refactor commit's `git add <2 files>` picked up 10 unrelated
pre-existing staged deletions (.backlog-context.md,
.claude/commands/backlog/*.md) that were already in the index before this
session touched anything. Restoring them to their pre-commit tracked
content; unrelated to the terminal-visibility-resync feature.

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

* chore: revert erroneous restoration of stale cross-item backlog scaffolding

The prior "restore" commit brought back done-0/1/2.md, fail-0/1/2.md,
review.md, status.md, and .backlog-context.md at their old committed
content, which referenced a different, unrelated backlog item
(9292b871-ab19-4c7b-8df5-7e7ce90895f2) rather than the item this branch
is actually for (7728f6df-268a-4578-9066-c300ff69269b, confirmed via
every get_backlog_item/report_progress call this session). Re-removing
them restores the pre-session state where the harness had already staged
their deletion in favor of the current item's regenerated (untracked)
versions.

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

* chore: add current-item backlog command scaffolding (done/fail 3-7, ship)

Harness-generated command files for backlog item 7728f6df, matching its
current 8-criteria checklist (done-0..2/fail-0..2/help/review/status were
already tracked from the correct-item regeneration in a prior commit).

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

* fix(ci): extract full tmux release tarball tree, not just configure

configure.ac declares AC_CONFIG_AUX_DIR(etc), so ./configure needs
etc/install-sh (and other etc/ aux scripts) from the tmux release
tarball. The script only extracted the top-level configure file,
so configure failed with "cannot find install-sh, install.sh, or
shtool in etc" — breaking the Test CI job on every PR (confirmed
failing on main since at least 2026-07-07, unrelated to this branch).

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

* fix(test): raise CI timeout for hook-URL integration tests to 30s

TestServer_should_WriteRealPortIntoSessionHooksAndMCPURL_When_StartedWithPortZeroThenSessionCreated
and TestServer_should_WriteUnchangedHookURL_When_StartedOnExplicitPort pass in
under 1s locally, but were flaking on the "Test" CI job (tmux session startup +
async hook injection missing the old 15s deadline under -race + parallel-suite
load). Bumping both to 30s to absorb that contention; unrelated to this PR's
terminal visibility-resync change.

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

* revert: restore backlog scaffolding files swept into prior commit unintentionally

These 11 files (.claude/commands/backlog/{done,fail}-3..7.md, ship.md) were
already staged for deletion in the working tree before this session started
and got included in the previous commit alongside the actual test fix. They
are unrelated to this PR's scope — restoring to avoid touching harness
scaffolding this session didn't intend to change.

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

* fix(session): route ForceStatus through actor mailbox to fix data race

go test -race caught SessionService.CreateSession's async goroutine
calling Instance.ForceStatus() (which took i.mu directly) racing with
runActor's own unguarded buildSnapshot rebuild after each actor command
(session/actor.go). The actor model relies on single-goroutine
confinement instead of i.mu, so a direct-lock writer from outside the
actor is invisible to it.

Route ForceStatus through the existing sendCtx actor-command helper so
the write is serialized with the actor's command loop when the
instance is actor-owned, matching the pattern already used by Epic 4+
mutators. Falls back to running in place when there's no LiveInstance
(bare *Instance in tests).

Fixes CI failure in TestServer_should_WriteRealPortIntoSessionHooksAndMCPURL_When_StartedWithPortZeroThenSessionCreated
blocking PR #184 — root cause identified via git log --all --grep=race,
which found this exact fix (bdd621e) already authored and merged into
an unrelated, still-open cross-repo sync PR (#169) that hasn't reached
this branch yet. Cherry-picked rather than re-derived.

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

* test(terminal): AC7 wire-to-repaint integration proof via useTerminalStream

Closes the gap TerminalStreamManager.resync.test.ts left open: that test
calls manager.write() directly and never proves a resync payload reaches
TerminalStreamManager via the real wire-message pipeline. This test drives
the real (non-mocked) useTerminalStream message loop with a scripted
CurrentPaneRequest-style resync `output` message, feeding a real
TerminalStreamManager backed by a real @xterm/xterm Terminal — the same
onOutput -> manager.write() wiring TerminalOutput.tsx uses in production.

AC7's wording names a "StateApplicator" class with resetSequence()/
applyState() methods that was never built (confirmed via grep) — the
implementation instead reuses the pre-existing resize-resync path
(requestFullResync -> CurrentPaneRequest -> clearAndHome-prefixed output ->
TerminalStreamManager.write()). Documents that mapping in the test file so
future review isn't blocked on a class name mismatch.

* revert: restore backlog scaffolding files unintentionally swept into AC7 test commit

Following files were staged as deletions from before this session started
(pre-existing index state, unrelated to the AC7 integration test change) and
got bundled into the previous commit by an overly broad `git add`. Same
unintentional-sweep pattern already reverted once in this repo's history
(99369870); restoring rather than repeating it.

* chore: commit current-item backlog scaffolding snapshot

Harness-generated per-session files (.backlog-context.md, .claude/commands/backlog/*)
required by request_review's clean-worktree check; content is just command
scaffolding and a snapshot of this backlog item's current state.

* fix(terminal): address code-review findings on visibility resync

Three issues found by /code:review's parallel testing/code-quality/
architecture pass over PR #184:

- TerminalOutput.tsx handleOutput only signalled a pending resync's
  completion on the direct-write path; output queued during a concurrent
  RESIZING state never notified useVisibilityResync, so a resync whose
  response happened to land mid-resize looked stalled and triggered the
  4s watchdog's spurious disconnect+reconnect. Move the notify call
  before the RESIZING early return so it fires unconditionally. Added a
  regression test (TerminalOutputBug.test.tsx) that fails on the old
  code and passes on the fix.

- useTerminalStream.ts's pre-existing Story 3.1.3 visibilitychange
  listener (gated behind NEXT_PUBLIC_RECONNECT_V2, off by default) and
  useVisibilityResync's new disconnected-branch fallback both call
  connect() independently on the same event with different debounce
  windows; connect() only guarded against "already connected," not
  "already connecting," so both could race and orphan the first
  in-flight stream. Added an isConnectingRef guard.

- session/instance_state.go's ForceStatus (from an unrelated, already-
  bundled data-race fix) routed through sendCtx with context.Background()
  — an error-recovery path with no timeout would hang its caller
  indefinitely if the actor mailbox were ever wedged. Added a 5s timeout
  with an error log on expiry.

Deferred (documented, not blocking): dual ownership of
showReconnectBanner between TerminalOutput.tsx's isConnected-driven
effect and useVisibilityResync's own timer is a real design smell but
requires a larger return-value refactor across an already-tested API;
a regression test for the ForceStatus actor-mailbox race itself; an
unrelated pre-existing checksum gap in scripts/build-tmux.sh's tarball
fallback path.

* chore: untrack backlog scaffolding files, add to .gitignore

.backlog-context.md and .claude/commands/backlog/* are session-scoped
files the backlog harness injects into a worktree to drive an agent
session — not source files. They ended up committed to this branch by
mistake across several earlier sessions. The personal fork (tstapler/
stapler-squad) already gitignores both paths; origin was missing the
same entries.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
@tstapler
tstapler force-pushed the worktree-agent-aa4517075c8f84c7f branch from b921835 to 6d23463 Compare July 26, 2026 18:06
tstapler added a commit that referenced this pull request Jul 26, 2026
…on (#184)

* fix(ui): restore checkpoint/fork UI lost in benchmarks merge regression

The benchmarks PR (#17, acbc44e) carried an older SessionCard.tsx and
silently overwrote 213 lines of checkpoint/fork UI introduced by the
session-resumption PR (#18, fc5598c) merged 4 hours earlier.

Restored three files from fc5598c:
- SessionCard.tsx: CheckpointProto import, 3 props, 10 useState vars,
  handlers (create/submit/cancel/fork), checkpoint + fork dialogs,
  📍 Checkpoint and 🍴 Fork action buttons
- useSessionService.ts: createCheckpoint, listCheckpoints, forkSession
  hooks + CheckpointProto import + interface declarations
- page.tsx: destructure + pass checkpoint/fork props to SessionCard

TypeScript compiles clean (tsc --noEmit).

* feat(checkpoints): wire HistoryLinker startup, shutdown capture, and pre-switch checkpoint

Task A - HistoryLinker startup pipeline:
- Add NewHistoryLinkerFromRealInspector() production constructor with fsnotify
  watcher on ~/.claude/projects/ for fast JSONL detection
- Add HistoryLinker.Instances() snapshot getter for live session set
- Wire HistoryLinker.Start() in server.go; wire Add/RemoveInstance into
  ExternalDiscovery callbacks so discovered sessions are tracked

Task B - Shutdown capture (cold restore CWD):
- Add GetPaneCurrentPath() to TmuxSession (tmux display-message #{pane_current_path})
- Add CaptureCurrentState() to Instance with guards (not started, paused, dead tmux)
- Register shutdown hook in server.go: captures pane CWDs then SaveInstances with
  4-second deadline to keep within SIGTERM kill window
- Shutdown hook uses HistoryLinker.Instances() (live set) instead of startup snapshot
  so externally-discovered sessions are also captured

Task C - Auto-checkpoint before workspace switch:
- Create "pre-switch: <target>" checkpoint in SwitchWorkspace before switching;
  non-fatal if it fails so the switch always proceeds

Tests (8 new):
- GetPaneCurrentPath: trim behavior + error wrapping
- CaptureCurrentState: not-started, paused, dead-tmux guard branches
- NewHistoryLinkerFromRealInspector: smoke test (non-nil constructor)
- HistoryLinker.Instances(): snapshot correctness + independence from internal state

Code quality fixes:
- defer stateMutex.Unlock() in CaptureCurrentState
- os.UserHomeDir() error now logged instead of silently producing /.claude/projects

* fix(web): add missing checkpoint props to SessionListProps

SessionList was passing onCreateCheckpoint, onListCheckpoints, and
onForkFromCheckpoint to SessionCard but these were absent from the
SessionListProps interface and function signature, causing a TypeScript
compile error in CI.

* test: add missing happy-path and integration tests for checkpoints

- TestInstance_CaptureCurrentState_UpdatesWorkingDir: verifies WorkingDir
  is populated from the tmux pane path on graceful shutdown (Story 1.2.1)
- TestHistoryLinker_LinksUUIDToSession: verifies the scan loop propagates
  a conversation UUID to an instance via correlateSession (Story 1.1.4)
- TestHistoryLinker_IdempotentRelink: verifies correlateSession returns
  early when HasClaudeSession() is already true
- Add 5 missing CSS module classes for the fork-from-checkpoint dialog
  (.forkEmptyMessage, .forkCheckpointList, .forkCheckpointItem,
   .forkCheckpointLabel, .forkGitSha)

* docs(terminal-visibility-resync): SDD phases 1-4 (requirements, research, plan, validation)

Requirements derived from backlog item 7728f6df-268a-4578-9066-c300ff69269b,
grounded against the actual codebase rather than the (stale) issue text --
the referenced StateApplicator/useTerminalSnapshot primitives don't exist;
the real fix wires up requestFullResync/markResyncComplete/markPaneResponseReceived
(already implemented in useTerminalFlowControl.ts but never exposed/called)
and fixes useDebouncedCallback's useState-backed timer bug.

Plan went through two rounds of architecture + adversarial review (both
BLOCKED -> CONCERNS after fixes: Promise<void> return types, session-switch
cleanup, connect()-in-flight guard, hook extraction to useVisibilityResync.ts)
plus a Product/UX/Engineering triad review (UX gap fixed by adding the
2s-intermediate-banner story the plan's own Pattern Decisions table had
already promised but never implemented).

No source code changed yet -- planning artifacts only.

* feat(terminal): expose resync primitives from useTerminalStream

Add requestFullResync, markResyncComplete, and markPaneResponseReceived
to TerminalStreamResult so TerminalOutput can trigger/track a full pane
resync without new prop plumbing. Also widen connect/disconnect's
declared return types from () => void to Promise<void>-returning,
matching their existing async implementations — a pre-existing type
bug that Phase 2's stall watchdog needs corrected to type-check its
disconnect().then(() => connect()) chain.

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

* fix(hooks): useDebouncedCallback root-cause double-fire bug

Store the debounce timer id in a useRef instead of useState so two
calls landing in the same JS tick (e.g. visibilitychange + focus
firing back-to-back) don't read a stale timer id from a batched
setState update and let both timers fire. Wrap the returned callback
in useCallback keyed on [callback, delay] so it's referentially
stable across re-renders, making it safe to use in dependency arrays
and as an event listener reference.

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

* test(terminal): AC7 integration proof for full-resync repaint via TerminalStreamManager.write

Proves, against a real (unmocked) @xterm/xterm Terminal and real
TerminalStreamManager, that writing the server's clearAndHome-prefixed
resync payload produces a genuine full repaint over stale/corrupted prior
content, verified by reading back the actual rendered buffer rather than
asserting on a mock call count.

Adds a cross-reference comment on connectrpc_websocket.go's
ansiSnapshotPrefix constant pointing at the new test's duplicated
clearAndHome fixture, since the two have no compile-time link.

* feat(terminal): visibility/focus resync via useVisibilityResync hook

Implements Epic 2.1 of terminal-visibility-resync: a standalone
useVisibilityResync hook that debounces document visibilitychange/window
focus events (300ms) and, while connected, triggers exactly one
requestFullResync(true) per debounce window to repaint a render-desynced
xterm buffer after a backgrounded tab regains focus (AC1). While
disconnected (and not mid-handshake), it falls back to a direct connect()
+ setShowReconnectButton(true) (AC4). A 4000ms stall watchdog force-clears
pending resync state and runs disconnect().then(connect) if no output
arrives (AC5), and a 2000ms intermediate timer surfaces the existing
reconnecting-banner UI for slow resyncs. The hook never touches focus
(AC3) and tears down session-scoped pending state on sessionId change,
including a guard against a debounced call that fires mid-flight against
a session it was no longer scheduled for.

Wired into TerminalOutput.tsx via a small hook call plus a
notifyResyncOutputReceived() call in handleOutput (routed through a
ref-mirror since the callback isn't available until after
useTerminalStream/useVisibilityResync are both called, avoiding a TDZ
reference-before-declaration issue in the plan's literal sketch).
XtermTerminal.tsx, useTerminalStream.ts's NEXT_PUBLIC_RECONNECT_V2
listener, and handleManualReconnect's body are all unmodified (AC6,
verified via scoped git diff).

Also updates the mocked useTerminalStream() return objects across six
existing TerminalOutput test files to include the three
already-committed-but-previously-unmocked fields
(requestFullResync/markResyncComplete/markPaneResponseReceived), which
were undefined and caused the new hook's session-cleanup effect to throw
on unmount.

19 new tests in useVisibilityResync.test.ts cover AC1, AC3, AC4, AC5, the
Story 2.1.8 banner behavior, and the Story 2.1.6 session-switch cleanup
(including the mid-debounce-switch guard).

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

* refactor(terminal): address sdd:6-verify Layer 1/2 findings for visibility resync

Move notifyResyncOutputReceivedRef sync into a useEffect instead of the
render body (both the idiom and architecture review agents independently
flagged the mid-render ref write as inconsistent with this file's own
ref-mirror convention). Remove the redundant unmount-only cleanup effect
in useVisibilityResync, superseded by the sessionId-keyed effect that
already performs a strict superset of the same cleanup.

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

* chore: restore backlog scaffolding files accidentally deleted by prior commit

The prior refactor commit's `git add <2 files>` picked up 10 unrelated
pre-existing staged deletions (.backlog-context.md,
.claude/commands/backlog/*.md) that were already in the index before this
session touched anything. Restoring them to their pre-commit tracked
content; unrelated to the terminal-visibility-resync feature.

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

* chore: revert erroneous restoration of stale cross-item backlog scaffolding

The prior "restore" commit brought back done-0/1/2.md, fail-0/1/2.md,
review.md, status.md, and .backlog-context.md at their old committed
content, which referenced a different, unrelated backlog item
(9292b871-ab19-4c7b-8df5-7e7ce90895f2) rather than the item this branch
is actually for (7728f6df-268a-4578-9066-c300ff69269b, confirmed via
every get_backlog_item/report_progress call this session). Re-removing
them restores the pre-session state where the harness had already staged
their deletion in favor of the current item's regenerated (untracked)
versions.

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

* chore: add current-item backlog command scaffolding (done/fail 3-7, ship)

Harness-generated command files for backlog item 7728f6df, matching its
current 8-criteria checklist (done-0..2/fail-0..2/help/review/status were
already tracked from the correct-item regeneration in a prior commit).

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

* fix(ci): extract full tmux release tarball tree, not just configure

configure.ac declares AC_CONFIG_AUX_DIR(etc), so ./configure needs
etc/install-sh (and other etc/ aux scripts) from the tmux release
tarball. The script only extracted the top-level configure file,
so configure failed with "cannot find install-sh, install.sh, or
shtool in etc" — breaking the Test CI job on every PR (confirmed
failing on main since at least 2026-07-07, unrelated to this branch).

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

* fix(test): raise CI timeout for hook-URL integration tests to 30s

TestServer_should_WriteRealPortIntoSessionHooksAndMCPURL_When_StartedWithPortZeroThenSessionCreated
and TestServer_should_WriteUnchangedHookURL_When_StartedOnExplicitPort pass in
under 1s locally, but were flaking on the "Test" CI job (tmux session startup +
async hook injection missing the old 15s deadline under -race + parallel-suite
load). Bumping both to 30s to absorb that contention; unrelated to this PR's
terminal visibility-resync change.

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

* revert: restore backlog scaffolding files swept into prior commit unintentionally

These 11 files (.claude/commands/backlog/{done,fail}-3..7.md, ship.md) were
already staged for deletion in the working tree before this session started
and got included in the previous commit alongside the actual test fix. They
are unrelated to this PR's scope — restoring to avoid touching harness
scaffolding this session didn't intend to change.

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

* fix(session): route ForceStatus through actor mailbox to fix data race

go test -race caught SessionService.CreateSession's async goroutine
calling Instance.ForceStatus() (which took i.mu directly) racing with
runActor's own unguarded buildSnapshot rebuild after each actor command
(session/actor.go). The actor model relies on single-goroutine
confinement instead of i.mu, so a direct-lock writer from outside the
actor is invisible to it.

Route ForceStatus through the existing sendCtx actor-command helper so
the write is serialized with the actor's command loop when the
instance is actor-owned, matching the pattern already used by Epic 4+
mutators. Falls back to running in place when there's no LiveInstance
(bare *Instance in tests).

Fixes CI failure in TestServer_should_WriteRealPortIntoSessionHooksAndMCPURL_When_StartedWithPortZeroThenSessionCreated
blocking PR #184 — root cause identified via git log --all --grep=race,
which found this exact fix (0b635b2) already authored and merged into
an unrelated, still-open cross-repo sync PR (#169) that hasn't reached
this branch yet. Cherry-picked rather than re-derived.

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

* test(terminal): AC7 wire-to-repaint integration proof via useTerminalStream

Closes the gap TerminalStreamManager.resync.test.ts left open: that test
calls manager.write() directly and never proves a resync payload reaches
TerminalStreamManager via the real wire-message pipeline. This test drives
the real (non-mocked) useTerminalStream message loop with a scripted
CurrentPaneRequest-style resync `output` message, feeding a real
TerminalStreamManager backed by a real @xterm/xterm Terminal — the same
onOutput -> manager.write() wiring TerminalOutput.tsx uses in production.

AC7's wording names a "StateApplicator" class with resetSequence()/
applyState() methods that was never built (confirmed via grep) — the
implementation instead reuses the pre-existing resize-resync path
(requestFullResync -> CurrentPaneRequest -> clearAndHome-prefixed output ->
TerminalStreamManager.write()). Documents that mapping in the test file so
future review isn't blocked on a class name mismatch.

* revert: restore backlog scaffolding files unintentionally swept into AC7 test commit

Following files were staged as deletions from before this session started
(pre-existing index state, unrelated to the AC7 integration test change) and
got bundled into the previous commit by an overly broad `git add`. Same
unintentional-sweep pattern already reverted once in this repo's history
(99369870); restoring rather than repeating it.

* chore: commit current-item backlog scaffolding snapshot

Harness-generated per-session files (.backlog-context.md, .claude/commands/backlog/*)
required by request_review's clean-worktree check; content is just command
scaffolding and a snapshot of this backlog item's current state.

* fix(terminal): address code-review findings on visibility resync

Three issues found by /code:review's parallel testing/code-quality/
architecture pass over PR #184:

- TerminalOutput.tsx handleOutput only signalled a pending resync's
  completion on the direct-write path; output queued during a concurrent
  RESIZING state never notified useVisibilityResync, so a resync whose
  response happened to land mid-resize looked stalled and triggered the
  4s watchdog's spurious disconnect+reconnect. Move the notify call
  before the RESIZING early return so it fires unconditionally. Added a
  regression test (TerminalOutputBug.test.tsx) that fails on the old
  code and passes on the fix.

- useTerminalStream.ts's pre-existing Story 3.1.3 visibilitychange
  listener (gated behind NEXT_PUBLIC_RECONNECT_V2, off by default) and
  useVisibilityResync's new disconnected-branch fallback both call
  connect() independently on the same event with different debounce
  windows; connect() only guarded against "already connected," not
  "already connecting," so both could race and orphan the first
  in-flight stream. Added an isConnectingRef guard.

- session/instance_state.go's ForceStatus (from an unrelated, already-
  bundled data-race fix) routed through sendCtx with context.Background()
  — an error-recovery path with no timeout would hang its caller
  indefinitely if the actor mailbox were ever wedged. Added a 5s timeout
  with an error log on expiry.

Deferred (documented, not blocking): dual ownership of
showReconnectBanner between TerminalOutput.tsx's isConnected-driven
effect and useVisibilityResync's own timer is a real design smell but
requires a larger return-value refactor across an already-tested API;
a regression test for the ForceStatus actor-mailbox race itself; an
unrelated pre-existing checksum gap in scripts/build-tmux.sh's tarball
fallback path.

* chore: untrack backlog scaffolding files, add to .gitignore

.backlog-context.md and .claude/commands/backlog/* are session-scoped
files the backlog harness injects into a worktree to drive an agent
session — not source files. They ended up committed to this branch by
mistake across several earlier sessions. The personal fork (tstapler/
stapler-squad) already gitignores both paths; origin was missing the
same entries.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
@tstapler

Copy link
Copy Markdown
Owner Author

Closing as superseded — this is the same feature (orphaned-triage auto-respawn wired into the remediation/backoff framework) that just shipped as #274, with the same core files (server/services/backlog_service_triage.go, session/backlog_lifecycle.go, session/backlog_lifecycle_stuck_test.go). #274 is merged to main; this older attempt from 07-17 predates it and is redundant now.

@tstapler tstapler closed this Jul 27, 2026
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.

1 participant