Skip to content

chore(main): release 1.38.0#156

Merged
tstapler merged 1 commit into
mainfrom
release-please--branches--main
Jul 17, 2026
Merged

chore(main): release 1.38.0#156
tstapler merged 1 commit into
mainfrom
release-please--branches--main

Conversation

@github-actions

@github-actions github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

🤖 I have created a release beep boop

1.38.0 (2026-07-17)

Features

  • backlog: add pipeline-mode selector, management UI, and what-ran surface (54a34cc)
  • backlog: add PipelineMode CRUD RPCs with structural validation (edcf2f2)
  • backlog: add PipelineMode data model and PipelineEngine seam (37daaed)
  • backlog: durable stuck-item visibility on /unfinished (a0ebca8)
  • backlog: grant reviewer bounded codebase read access on empty-diff reviews (#155) (9697b86)
  • backlog: opt-in auto-spawn-session toggle + fix a partial-update clobbering bug (b28ace2)
  • backlog: standing detector for orphaned triage sessions (60c8a2a)
  • unfinished: control gogitstore's mmap index via feature flags (9da5dfa)
  • unfinished: gogitstore production hardening — eviction, refcounting, mmap index (97251a5)

Bug Fixes

  • actor: route MCP server URL backfill through SetMCPServerURL (c1c9e5e)
  • auto-regenerate ent ORM code on build (self-heal missing packages) (81fa981)
  • backlog: auto-transition re-review PASS verdict to done; close bucket-2 manual-gates audit (4d1501a)
  • backlog: AutoReopenForPRFix skips status churn when a fix is already active (f8f788a)
  • backlog: distinguish real lookup failures from expected not-linked case (d13755d)
  • backlog: notify on silent auto-merge fallback; dedup stuck-resolve idiom (47bbe05)
  • backlog: notify operator when post-triage persistence fails (11570a2)
  • backlog: reconcile review-session-spawn refactor with pipeline-mode wiring (7042dc8)
  • backlog: surface status-transition failure in autonomous-complete notification (5a809a6)
  • backlog: tombstone dead work sessions blocking respawn (af426f2)
  • backlog: unrecognized session role still notifies the operator (3295389)
  • ci: bump release build Go version to match go.mod (7cde747)
  • ci: fetch the tmux submodule in every checkout that needs it (8c30912)
  • ci: make release builds structurally independent of tag-push recursion (03ec5ab)
  • extract full tmux release tarball, not just configure (c9a2528)
  • git: stop deleting worktree branches on cleanup/reset (a1e8efd)
  • lint: clear golangci-lint findings blocking CI (3657cb0)
  • re-register third_party/tmux as a real git submodule gitlink (7058d92)
  • remove broken Bazel/rules_foreign_cc tmux build path (c4adbbf)
  • simplify build-tmux Makefile target and stamp prerequisite (5e69576)
  • test: derive MCP tool count from registration, not a hardcoded literal (e2302f0)
  • test: eliminate two more tmux integration test flakes (1f91b1b)
  • test: retry transient git-gc failures in gogitstore test fixtures (b78b6c8)
  • test: stimulate PTY output in TestStreamTerminal_SendsRawOutput to eliminate a real flake (7ff0ac9)
  • test: update MCP tool count assertion to 26 (6a00974)
  • tmux: recover EnsureServerRunning from a transient check-race (0d1272b)
  • unfinished: harden gogitstore mmap index for production readiness (cfa0121)
  • unfinished: memory-based cache eviction and event-driven scanning (c998fa9)
  • unfinished: shrink and share go-git's per-repo object cache (ed6d85a)

This PR was generated with Release Please. See documentation.

@github-actions
github-actions Bot force-pushed the release-please--branches--main branch 2 times, most recently from 900a196 to a2d3c3c Compare July 17, 2026 02:25
@github-actions
github-actions Bot force-pushed the release-please--branches--main branch from a2d3c3c to d10a4f2 Compare July 17, 2026 03:37
@tstapler
tstapler merged commit 53e1964 into main Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor Author

🤖 Created releases:

🌻

tstapler added a commit that referenced this pull request Jul 26, 2026
…tor gap (#163)

* fix(terminal): correctly scan OSC/DCS escape sequences to stop render artifacts

stripANSIBytes and sanitizeUTF8Bytes treated any ASCII letter as the end
of an escape sequence. That's only true for CSI (ESC[...letter); OSC
(ESC]...BEL or ESC\) and DCS/PM/APC/SOS (ESC{P,^,_,X}...ESC\ or 0x9C)
terminate differently, and their payloads (window titles, hyperlink
URLs, shell-integration marks) almost always contain a letter before
the real terminator. Claude Code's newer renderer emits more of these
OSC sequences, so their payload tails were leaking through as literal
text in the web terminal and throwing off cursor-column math.

Add a shared scanEscapeSequence helper (mirrors the correct boundary
logic already used by pkg/analytics/escape_code_parser.go) and rewire
both duplicated stripANSIBytes definitions plus sanitizeUTF8Bytes to
consume whole sequences atomically instead of stopping at the first
letter.

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

* fix(terminal): widen CSI final-byte range to 0x40-0x7E, cap OSC/DCS scan size

Code review on PR #156 found that the new scanCSI only accepted A-Z/a-z
as CSI terminators, missing real ECMA-48-valid final bytes like '@'
(0x40, Insert Character) and '~' (0x7E, used by many real xterm
sequences e.g. function/navigation keys). Confirmed empirically:
stripANSIBytes("\x1b[5@Hello") leaked "@hello" instead of "Hello" —
the exact bug class this PR exists to eliminate, just for a different
final byte. Widened to the full 0x40-0x7E range and aligned the
malformed-CSI fallback with pkg/analytics' semantics (give up and
consume only the ESC, rather than swallowing partially-scanned params).

Also:
- Widened pkg/analytics/escape_code_parser.go's parseCSI terminator
  range to match (it was cited as the reference implementation for
  this fix but had the same narrower gap for '~' and other non-letter
  finals in 0x5B-0x60/0x7B-0x7E).
- Added a size cap (mirroring escape_code_parser.go's existing 65536
  bound) to scanUntilTerminator so an unterminated/adversarial OSC or
  DCS payload can't force an unbounded scan.
- Pre-size the bytes.Buffer in stripANSIBytes/sanitizeUTF8Bytes with
  Grow(len(b)) to avoid reallocation growth in this hot path.
- Removed the now-vestigial (*StateGenerator).stripANSIBytes wrapper
  method now that its only caller can use the shared free function
  directly.
- Added regression tests for all of the above, including a mid-buffer
  (start > 0) case and the new size-cap behavior.

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

* refactor: remove dead MOSH-style terminal state-sync infrastructure

Investigating the terminal-artifacts report (PR #156) surfaced that
server/terminal (the package that PR fixed) has zero callers anywhere
in the repo — it's pre-integration code for a "state" streaming mode
that was designed but never wired into the live rendering path. Tracing
further found this same abandoned effort spans three separate, never-
fully-connected implementations, plus the client-side code built to
consume them:

- server/terminal (StateGenerator/DeltaGenerator) — zero callers.
- server/ssp (Mosh-inspired Coordinator) + session/framebuffer (its
  diff engine) — zero callers.
- session/terminal_state.go (regex-based TerminalState) — only called
  from SessionService.StreamTerminal, which browsers never reach: the
  WebSocket bridge (connectrpc_websocket.go) intercepts the same RPC
  procedure and reimplements it with plain raw-byte passthrough.
- server/compression (LZMA) — only exercised by transport_e2e_test.go,
  itself a spec-as-test for streaming modes the server never actually
  implements (its own docstring says so).
- Frontend: StateApplicator/DeltaApplicator/EchoOverlay/lzma.ts and the
  predictive-echo/SSP-negotiation logic in useTerminalFlowControl.ts —
  all gated behind `sspNegotiated`, which never becomes true since the
  server never sends an SSPNegotiation message. Confirmed sendInput
  (the working path) is always used in practice, not sendInputWithEcho.
- The streaming-mode selector (config field, proto field, dropdown UI)
  that drove all of the above: the value was accepted and logged but
  never branched on by the code that actually sends output.

Simplified SessionService.StreamTerminal to plain raw output (removing
its session.TerminalState dependency) rather than deleting it outright,
since the RPC interface still requires an implementation for any future
non-browser Connect client. Also widened the same CSI final-byte gap
(missing '@'/'~' etc.) in pkg/analytics/escape_code_parser.go that
PR #156 fixed in server/terminal, since it was cited as that fix's
reference implementation and had the identical bug.

Left ADR-002 (which documented the abandoned "state" mode decision for
external sessions) in place with its status marked Superseded, rather
than deleting it, to preserve the historical record.

Verification: go build/vet, full Go test suite, npx tsc --noEmit, and
the full Jest suite (2776 tests) all pass. go mod tidy dropped the now-
unused github.com/ulikunitz/xz dependency.

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

* fix(terminal): resolve CSI terminator gap and code-review follow-ups

BUG-025: widen the CSI final-byte character class from [a-zA-Z] to
[@-~] (0x40-0x7E per ECMA-48) everywhere it was copy-pasted narrow,
so sequences terminating on '@' or '~' no longer leak raw escape
bytes past rate-limit/activity detection and tmux banner filtering.

Consolidates the fix into one shared, tested module per language
(pkg/ansi for Go, web-app/src/lib/terminal/stripAnsi.ts for
TypeScript) instead of five independently duplicated regexes, per
code-review findings. Also reserves and deletes the orphaned MOSH
oneof message types left behind in events.proto by the earlier
dead-code removal.

Adding real end-to-end test coverage for StreamTerminal's simplified
output path surfaced two pre-existing concurrency bugs under -race,
now fixed:
- the output goroutine could race Connect's own stream-close write
  since nothing joined it before the handler returned
- session.Instance.Started() read the started field without its
  lock while start() wrote it outside the lock

Also adds a test pinning the WebSocket-vs-general-handler routing
precedence for the StreamTerminal path, previously only documented
in a comment.

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

* fix(terminal): close residual races flagged by PR #163 code review

A /github:pr-ship code-review gate on PR #163 (4 parallel agents:
testing, code quality, architecture, security) found that both
concurrency fixes from the previous commit were real but incomplete:

- StreamTerminal's output goroutine was bounded-and-abandoned on
  timeout rather than actually interrupted, so it could still call
  stream.Send() after the handler returned if the PTY read blocked
  past the 2s bound. Fixed by dup'ing the PTY fd so this goroutine
  gets its own independent *os.File/poll.FD, safe to set a real
  250ms read deadline on without racing the instance's other PTY
  consumers (response stream, command executor) that share the
  original fd.

- session.Instance.started was fixed at exactly the two sites this
  session's new test exercised, leaving ~30 other unguarded read/write
  sites across the package equally racy. Converted the field to
  atomic.Bool everywhere instead, which is race-free by construction
  rather than by per-site lock-discipline audit.

Also: added a deterministic unit test for waitWithTimeout's timeout
branch (previously only indirectly exercised), and exported
ansi.IsCSIFinalByte as a byte-range twin to CSIFinalByteClass so
pkg/analytics/escape_code_parser.go's manual byte-scanning CSI check
(a 6th independent reimplementation the prior consolidation missed)
now derives from the same shared source of truth.

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

* fix(session): close remaining actor/i.mu synchronization gaps post-merge

Follow-up to the main-merge commit: these edits were made while chasing
-race failures during conflict resolution but landed after that commit's
git add, so they weren't included in it.

- Instance.Preview, InstanceStatusManager.GetStatus: read Status via
  Snapshot() instead of a bare/RLock-guarded field read, matching the
  pattern already used by GetStatus/IsActive/etc. Both were caught by
  go test ./... -race (transitionToLocked writes i.Status outside i.mu).
- pkg/ansi: tighten csiRegex/csiFinalByteMin/csiFinalByteMax to unexported
  now that no caller outside the package needs them directly.
- instance_fork_test.go, instance_workspace_test.go: fix two `started:`
  struct-literal fields / bare assignments that go vet flagged after the
  bool -> atomic.Bool conversion.
- instance_timestamp_test.go: wrap the test instance in a LiveInstance so
  TestInstance_TimestampConcurrency's concurrent UpdateTerminalTimestamps
  calls are actually actor-serialized, instead of silently taking the
  no-actor synchronous fallback (send()'s documented behavior when
  liveInstance is nil) with zero cross-goroutine synchronization.
- instance_test.go: TestFromInstanceDataWithMissingWorktree now uses
  ForceStatus instead of a bare `instance.Status = Paused` write, so the
  Paused() check after it doesn't read a stale cached Snapshot().
- instance_serialization.go: ToInstanceData now builds its snapshot via
  sendSyncErr instead of reading the cached Snapshot(). Storage.UpdateInstance
  and SaveInstancesSync mutate Instance fields (Tags, Category) directly and
  expect the next ToInstanceData() call to see it; Snapshot()'s cache only
  refreshes when an actor command republishes it, so those callers were
  reading stale data (TestStorage_UpdateInstance, TestStorage_SaveInstancesSync).
  sendSyncErr gets a fresh buildSnapshot(s.inst) while still serializing
  against any concurrently-running actor commands.
- instance_approval.go: simplify snap.ReviewState.TimeSince* to snap.TimeSince*
  (staticcheck QF1008 — ReviewState is embedded in InstanceSnapshot).
- session_service_stream_terminal_test.go: wire session.NewRegistry +
  SetRegistry, matching server/dependencies.go's production wiring, so
  CreateSession actually wraps the instance in a LiveInstance during the test.

go test ./... -race: 0 data races (verified across multiple full-suite runs).

* fix(server): make PTY fd duplication cross-platform for windows/amd64 build

syscall.Dup (used by StreamTerminal to give its reader goroutine an
independent PTY fd) doesn't exist on Windows, breaking the release build
matrix (windows/amd64) added by .github/workflows/build.yml — the failure
first surfaced on PR #163's post-merge CI run, caused fail-fast cancellation
of the whole Build matrix.

Move the dup behind a dupPTYFile helper, split by build tag like the
existing execSyscall precedent (exec_unix.go/exec_windows.go): syscall.Dup
on Unix, windows.DuplicateHandle (golang.org/x/sys/windows, already a
direct dependency) on Windows. Verified go build ./... for
darwin/{amd64,arm64}, linux/{amd64,arm64}, and windows/amd64.

Also investigated two other CI failures from the same run:
- TestActorNoLeak/TestActorStopIdempotent (goleak "unexpected goroutines"
  from TestMain's own background helpers) — could not reproduce locally
  (isolated run or full `go test ./session -race`), consistent with a
  CI-timing-sensitive flake in this newly-merged upstream test, not
  something introduced here.
- A TmuxSession.ptmx data race (GetPTY's unguarded read racing
  closePTYAndAttachCmd's unguarded close/reconnect) reproduced locally
  under -race -count=15 on TestStreamTerminal_SendsRawOutput. Pre-existing
  in session/tmux (t.ptmx is read/written without synchronization in ~15
  call sites in tmux.go); fixing it properly needs a TmuxSession-level
  mutex refactor, out of scope for this merge-conflict PR. Flagged as a
  known follow-up, not fixed here.

* fix(session): stop TestActorNoLeak/TestActorStopIdempotent flaking on unrelated leaks

Reproduced the CI-only failure (2/2 remote runs) locally by constraining
GOMAXPROCS=2, which changes goroutine scheduling enough to surface it — a
plain -race/-count run on this fast dev machine never hit it.

Two contributing issues, both fixed:

1. TestApprove_FromHibernated_Succeeds and TestApprove_AllSourceStatuses's
   Hibernated->Active case call Approve(), which (via transitionToLocked)
   dispatches resumeFromHibernationLocked's real Start()/StartController()/
   StartSessionDriver() work in a background goroutine. StartSessionDriver's
   ticker loop has no external cancellation and runs for driverTotalTimeout
   (tens of minutes) — these tests never stop it, so it leaks for the rest
   of the test binary's life. Fixed by pre-setting driverRunning (the same
   CAS guard TestStartSessionDriver_Idempotent already relies on) so
   StartSessionDriver is a no-op; the tests aren't exercising driver
   dispatch, just the state transition.

2. The deeper issue: TestActorNoLeak/TestActorStopIdempotent used a bare
   process-wide goleak.VerifyNone(), which asserts "no goroutine is alive
   anywhere in the process" rather than these tests' actual claim ("Stop()
   joins the goroutine THIS test started"). In the large session package's
   shared test binary, ANY earlier, unrelated test that leaves its own
   background goroutine running (a ResponseStream poller, an exec.Cmd wait,
   etc.) trips this on unrelated grounds, nondeterministically depending on
   run order and scheduling — confirmed locally: after fixing #1, a -count=8
   stress run still surfaced two more pre-existing unrelated leaks
   (MangleCorrelator.StartEviction, os.Process.pidWait) before either test
   assertion even reached the actor-specific logic. Auditing and fixing
   every leak source across this large, pre-existing test package is a
   separate, much larger effort - fixed the actual fragility instead:
   goleak.IgnoreCurrent() baselines the goroutine set before the actor work,
   so pre-existing goroutines from other tests are excluded and only a
   genuine actor-goroutine leak fails the test.

Verified: GOMAXPROCS=2 go test ./session -race -v -count=8 — 0 failures
(was reproducing on every run before this fix).

* fix(scanner): register 3 missing backlog GitHub RPCs in methodToID

tools/scanner's TestMethodToIDCompleteness failed in CI (Run scanner unit
tests step of PR #163's Test job): SearchGitHubRepos, ListGitHubIssues, and
ImportGitHubIssue exist in backlog.proto (and have real, if currently
unimplemented/stubbed, server handlers - see the separate
TestSearchGitHubRepos_*/TestListGitHubIssues_* skips) but were never added
to proto_scanner.go's methodToID feature-registry map. Landed via the same
upstream fork-sync merge as the actor-pattern refactor; nothing in this PR
added these RPCs, just the first CI run to reach this check since the merge.

Ran make registry-generate to regenerate the per-feature JSON files this
map drives (docs/registry/features/backend/backlog/{search-github-repos,
list-github-issues,import-github-issue}.json), which also cleaned up a
stale duplicate (top-level DeleteBacklogItem.json, superseded by the
already-committed backlog/delete-item.json from PR #159's merge - unrelated
to this fix, just picked up by the same regenerate pass). registry-diff
divergence back to 0.0%.

* fix(scanner): keep GitHub RPC feature IDs stable to avoid a registry-diff false positive

Follow-up to 94ce8333f: mapping SearchGitHubRepos/ListGitHubIssues/
ImportGitHubIssue to new kebab-case backlog:* IDs relocated their registry
files (docs/registry/features/backend/backlog/*.json), which two things
then treated as brand-new, untested RPCs:

- The "Check new RPCs have tests" CI gate diffs registry files against
  origin/main by path; a relocated file is indistinguishable from a
  genuinely new one there.
- SearchGitHubRepos and ListGitHubIssues already have real, committed tests
  (server/services/backlog_github_rpc_test.go) and tested:true/testIds on
  origin/main under their old top-level path/id — relocating them would
  have discarded that (loadExisting only preserves tested/testIds/
  lastModified when the output *path* is unchanged).

Fixed by mapping these three to their own method name (matching
ScanProto's pre-existing fallback when methodToID has no entry), so the
generated id/path is byte-identical to what's already committed. Restored
SearchGitHubRepos.json/ListGitHubIssues.json/ImportGitHubIssue.json from
origin/main and re-ran make registry-generate: markerFound correctly
recalculates to false (no // +api: marker exists — these RPCs are still
unimplemented stubs, that part hasn't changed), while tested/testIds/
lastModified are preserved via loadExisting's existing-file logic.
ImportGitHubIssue.json (tested:false on both branches, no marker either
side) comes out byte-identical to origin/main; the other two only change
markerFound (true, which was stale, to false) and lastModified, with
tested:true and their real testIds untouched - so neither trips the "new
RPC has no test" per-file check (tested is true, and ImportGitHubIssue
doesn't show up as changed in the origin/main diff at all).

registry-diff: 0.0% divergence. cd tools/scanner && go test ./...: pass.

* chore(tests): untrack tests/e2e/node_modules and fix flaky accessibility waits

node_modules under tests/e2e was accidentally committed since it wasn't
covered by .gitignore. Also swap networkidle waits in accessibility.spec.ts
for element-based waits, since this app polls continuously and network
activity never settles for the required window (matches alias.spec.ts).

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

* fix: include tests/e2e/node_modules/ in .gitignore (missed in prior commit)

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

* fix: bump otel past CVE-2026-41178, fix PreviewFullHistory race, refresh stale docs

- security: bump go.opentelemetry.io/otel, /sdk, /trace from v1.43.0 to v1.44.0.
  v1.43.0 falls inside CVE-2026-41178's affected range (baggage-header-parsing DoS);
  the app registers the baggage propagator globally and otelhttp wraps the network-
  exposed HTTP server as the outermost layer, ahead of auth middleware. go mod tidy
  confirmed otelhttp v0.67.0 and otlptracegrpc v1.39.0 remain compatible with the
  bumped otel core, so those were left untouched; golang.org/x/sys picked up a
  transitive bump to v0.45.0. go mod tidy also dropped an unused gvisor.dev/gvisor
  indirect require (checklocks is installed via `go install ...@latest` in the
  Makefile, not consumed as a module dependency).

- fix(session): PreviewFullHistory() read i.Status directly, unsynchronized with
  transitionToLocked's writes (which happen inside the actor's own serialization,
  not under i.mu) - the same hazard already fixed on Preview() a few lines above.
  Extracted the shared check into a new previewBlocked() helper used by both
  Preview() and PreviewFullHistory() so the two can't diverge again.

- docs(session): updated ReviewState's file-header and UpdateTimestamps doc
  comments, which still described the old "protected by Instance.mu" discipline.
  UpdateTerminalTimestamps (the sole call path) now runs inside the actor's
  i.send() closure instead of holding i.mu; the comments now describe that.

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

---------

Co-authored-by: Tyler Stapler <tystapler@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant