Skip to content

Promote expbkmain to bkmain: startup-stall + write-amplification fixes and upstream nightly 20260807.1023 - #55

Merged
tusharbhardwaj-bk merged 60 commits into
bkmainfrom
expbkmain
Aug 7, 2026
Merged

Promote expbkmain to bkmain: startup-stall + write-amplification fixes and upstream nightly 20260807.1023#55
tusharbhardwaj-bk merged 60 commits into
bkmainfrom
expbkmain

Conversation

@tusharbhardwaj-bk

@tusharbhardwaj-bk tusharbhardwaj-bk commented Aug 7, 2026

Copy link
Copy Markdown

Promotes the verified staging branch to production. 59 commits, fast-forward (ahead_by: 59, behind_by: 0) — no conflicts possible.

bkt3.dev currently stalls ~161 s on every deploy and writes ~25.6 GB/day. This fixes both, and brings in upstream's reconnect work that targets the disconnect reports directly.

What's included

Our fixes (fork-owned):

Commit Fix
4318d24d0 Ownership backfill: fast-path skip, one-time repair marker, migration 1007 + idx_orchestration_events_event_type_stream, moved off the readiness path
675889ce3 SQLite pragmas: synchronous=NORMAL, wal_autocheckpoint=10000, cache_size=-65536, journal_size_limit
66e2f03d8 Trace sink drops fast sql.execute spans, retains slow ones
6ce5fe93e Stop tracking .mcp.json (spawned an xcodebuildmcp process tree per worktree)

Upstream v0.0.32-nightly.20260807.1023 (merge d47fc1af1, 56 commits):

Verified on expbkt3 (588eeefce)

Check Result
Startup readiness Started 14:22:08ready 14:22:12~4 s (bkt3: 161 s)
Backfill skip path No ownership backfill complete line — skip logs at debug, absence is the pass condition
Migration + index 1007 applied, idx_orchestration_events_event_type_stream present
Repair marker maintenance_markersownership.admin-reassignment @ 2026-08-06T19:16:52.074Z, not rewritten on later boots
Ownership integrity 0 ownerless threads, 0 ownerless projects
Checkpoint behaviour main DB 113.9 MB untouched for 20 min while WAL absorbs writes (bkt3: 1.96 GB rewritten every minute)
Trace filter 4 sql.execute of 22,823 spans, all ≥250 ms (961/438/403/369 ms) — both drop and retain halves proven
Upstream pagination migration 1008 + idx_projection_turns_thread_keyset present
Errors since boot 0
Writes 1.3 GB/day vs bkt3 25.6 GB/day

Expected effect on bkt3.dev

  1. Deploy outage ~161 s → ~4 s. The backfill was 97% of boot time; at ~11 restarts/48 h that recovers ~27 min of cumulative unavailability.
  2. Write amplification collapses — ends fsync-per-commit and constant re-checkpointing into the 1.96 GB database. This is the IOPS pressure behind the original 502 incident.
  3. Reconnect resilience (fix: prevent reconnect loops during server stalls pingdotgg/t3code#5561/fix(server): one disconnecting client no longer blocks every reconnect pingdotgg/t3code#5572) — an event-loop stall no longer kills the RPC connection, and one disconnecting client no longer blocks everyone else's reconnect.
  4. Smaller thread payloads (perf(server): stop shipping full MCP tool results in thread payloads pingdotgg/t3code#5482) and windowed thread loading (feat: paginate thread loading with user-anchored turn windows pingdotgg/t3code#5493).

Merge-time notes

  • The first boot after this deploys is the expensive one. It runs the one-time admin repair across 266 admin-owned threads and builds the new index (~6.4 s measured on a snapshot of the production DB). Expect one slower boot, then fast from then on.
  • Deploying restarts t3-bkmain.service and ends sessions hosted on bkt3. Merge from a non-bkt3 session or a human shell.
  • After it deploys, remove the trace stopgap so the diagnostics dashboard returns (safe now that the code-level filter is in):
    sudo rm /etc/systemd/system/t3-bkmain.service.d/60-trace.conf
    sudo systemctl daemon-reload
    
  • Nightly gate verified: merge-base(expbkmain, upstream/main) = 23f0a1ae3, which v0.0.32-nightly.20260807.1023 points at, so deploy-bkt3.yml's --points-at lookup resolves.

Caveats

  • Four commits (e7f535ed5, 88023e566, 04941d4de, 1985b66c0) landed on expbkmain after the currently deployed staging build, so they carry CI-green evidence but no runtime soak.
  • .mcp.json removal only affects worktrees that fast-forward past 6ce5fe93e; 41 existing worktrees still carry it and still spawn xcodebuildmcp.
  • Does not address the n8n/clickhouse/linear MCPs (they live in bk-docs / bk-docs-obs .codex/config.toml) or the provider process-tree termination leak.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

t3dotgg and others added 30 commits August 6, 2026 02:34
The version-skew banner is no longer an amber warning: it reads
"Server update available" with the raw versions (unreadable for
nightlies) moved to a tooltip. The in-flight rail (Download/Install/
Resume) becomes a single status row, "Downloading…" then
"Restarting…", since the wire installing stage is a sub-second
launcher handoff and "resuming" meant nothing to most people.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Measured on a live update: the server was back in ~9s but stayed
unreachable for ~96s, because releasing the tunnel on shutdown forces
the replacement tunnel's hostname route through 1-2 minutes of edge
propagation. An update handoff always brings a server right back
(new version or rollback), so the tunnel is never orphaned; skip the
release when the launcher state file records a pending update. The
next boot respawns the connector from the stored config against the
same tunnel and is reachable as soon as it connects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pdate

A pending update in the launcher state is not proof a replacement
server is coming: `t3 service uninstall` or `systemctl stop` during
the pending window also tears the server down, permanently. The
launcher now writes a stop marker before signalling its child on an
explicit stop and clears it on the next start; the shutdown tunnel
release keeps the tunnel only for pending updates without the marker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A tool.updated row is the in-flight snapshot of a call; once the call
completes, the tool.completed row carries the final state and both
clients fold every matching update into it. Shipping the updates buys
nothing: 47k such rows exist in one real database, and a single thread
carries 3,291 of them.

Filter them out of thread snapshots, mirroring the existing
context-window dedup. Matching is per turn and only against a LATER
completion, so a revert that discards the completing turn cannot leave
a call unrepresented, and a later update under the same identity (the
next call, still in flight) survives. Live events are untouched.

Rows are matched on the same identity the clients collapse by: an
explicit data.toolCallId when the adapter emits one, otherwise the
itemType/title/detail triple. No tool lifecycle row in the real db
carries a toolCallId, so the fallback does the work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… test

Bugbot flagged that clients collapse only adjacent lifecycle rows, so
dropping a superseded update separated by an interleaved parallel call
diverges from full-history rendering. Measured on a real database: 1.5%
of dropped rows (553/36,581), all pure in-flight state whose final result
the retained completion still shows, and zero dropped rows carry a
client-merged payload field their completion lacks (verified across all
49,515 update rows). Documents the tradeoff and adds a test pinning it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On bkt3.dev the startup ownership backfill took 146 s on every restart,
so each deploy disconnected everyone for over two minutes. All 334
threads and 8 projects already had an owner: the pass re-selected the 266
admin-owned rows forever because its WHERE clause treats "owned by the
admin" as work to redo, and each row drove correlated subqueries over a
258k-row event log with no index on event_type.

Three changes: a cheap guard that returns immediately when no row is
ownerless and the one-time admin repair is recorded; a durable marker so
that repair runs at most once instead of on every boot; and an index on
orchestration_events(event_type, stream_id). The pass also moves to
forkParked, alongside welcome.autobootstrap, so it can never hold
readiness again. Fail-soft semantics are unchanged.

Measured on a VACUUM INTO snapshot of the live database: the repair pass
goes from 55.8 s to 3 ms, and the new steady-state guard from 146 s to
under a millisecond.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sidebar meter took min() across every rate-limit window, so the short
rolling window (Claude five-hour, Codex primary) almost always won and the
bar reported a five-hour reading under a weekly-looking meter.

The headline is now the weekly window only. The rolling window gets its own
compact companion — `(40%, 68m)` — rendered beside the bar only once it drops
below 50% remaining, so it is visible exactly when it is the tighter
constraint and labelled with its own reset countdown. Providers reporting no
weekly quota keep the previous all-window reading instead of degrading to an
em dash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The server writes roughly 150 GB/day physically for about 80 MB/day of
logical data. Two SQLite defaults cause most of it: synchronous=FULL
fsyncs on every COMMIT, and wal_autocheckpoint=1000 copies the WAL back
into a 1.86 GB main database every ~4 MB, so every byte is written at
least twice.

synchronous=NORMAL cannot corrupt a WAL database — recovery replays the
WAL on open. The only exposure is losing the last few committed
transactions to a power cut or kernel panic; a clean process restart or
crash loses nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tracer emits one span per SQL statement, each carrying the full
query text. One observed window held 61,175 sql.execute spans, roughly
70-80% of all trace bytes, rotating a 10 MB file every 40 seconds.

Raising T3CODE_TRACE_MIN_LEVEL to Warn silences the file completely and
takes the in-app diagnostics dashboard down with it, since it reads the
same NDJSON. Instead the sink now takes a retain predicate, and the
server drops sql.execute spans that finished faster than
T3CODE_TRACE_SQL_SLOW_MS (default 250 ms). Slow statements and failures
are kept — a 97.9 s span is exactly how the ownership backfill was
found.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com>
Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com>
…tion (#54)

fix(server): unblock startup readiness and cut SQLite write amplification
The repo-root .mcp.json declared a single MCP server, xcodebuildmcp,
launched with `npx --yes xcodebuildmcp@2.6.2 mcp`. Because the file was
committed, every worktree checked it out and every session started in one
spawned its own xcodebuildmcp process tree — roughly 110-220 MB each, and
325 MB measured across two instances. There are ~39 t3code worktrees on
the shared dev host, and xcodebuildmcp drives Xcode and the iOS
Simulator, so it cannot do anything useful on a Linux server.

Removing the file from git and ignoring it stops that per-worktree cost.
Developers who want xcodebuildmcp locally can still create an untracked
.mcp.json; it just no longer ships to every checkout.

Committed with --no-verify: the pre-commit formatter rejects a changeset
with no formattable file (one deletion plus a .gitignore line).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The weekly meter's companion read `(40%, 68m)`, which does not say which
window it describes or how long that window is, and it printed raw minutes
past an hour.

It now reads `5h 40% · 1h 8m`: the window length comes from the provider's
reported duration, the countdown collapses to hours past 60 minutes, and the
same wording carries into the trigger's accessible summary. Providers that omit
a duration keep the unlabelled percentage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ut an origin remote (pingdotgg#5556)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…otes (pingdotgg#5547)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…k log (pingdotgg#5559)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ic error (pingdotgg#5557)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
t3dotgg and others added 24 commits August 6, 2026 21:32
… bottom (pingdotgg#5566)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…rovider update (pingdotgg#5570)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three changes to the experimental phase sidebar, all in fork-owned files.

Priority no longer tints a row. P0-P2 painted the whole row orange, which
competed with the routed row's own surface and left the list looking like
several selections at once. The row background now means exactly one thing --
routing -- and the P0..P4 badge carries priority alone. The badge fades from
full orange at P0 through 80/60/40% mixes to plain grey at P4, mixed in oklab
against a neutral of the same lightness so urgency reads as falling saturation
while the black label keeps identical contrast on every rung.

The routed row gets a stronger primary surface and ring to stay obvious now
that it is the only tinted row.

In-group ordering is strict. It folded in `attentionPriority` and
`isUnreadCompletion`, both of which flip the moment a row is opened, so simply
reading a thread reordered the group under the pointer. Ordering now reads
priority, the sort timestamp, then stable tiebreaks, and the direction
(most recent / oldest on top) plus the priority-first override are set per user
from the sidebar's filter popover and persisted with the filters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…otgg#5573)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…tgg#5558)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: maria-rcks <maria@kuuro.net>
Co-authored-by: Julius Marminge <jmarminge@gmail.com>
The weekly percentage carried no sense of how long it has to last: 67% means
something different on day one than on day six. The headline window's reset now
renders permanently beside it.

It prints a single rounded-up unit -- `5d`, `18h`, `47m` -- rather than the
rolling chip's compound form, so it never exceeds three characters in a sidebar
header that has no width to spare. The rolling chip keeps its existing
below-50% condition and its compound countdown, since it only appears when the
exact remaining time matters.

The countdown re-renders on unit boundaries instead of per minute: a week-long
window would otherwise schedule ~10k timers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…g#5579)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Merges upstream release commit 23f0a1a, the newest upstream nightly.
Targets the nightly rather than upstream main HEAD because the deploy
workflow's "Resolve upstream nightly version" step uses `git tag
--points-at` on the merge-base: the merge-base must land exactly on a
tagged nightly or the deploy gate exits 1.

Conflict resolution followed the fork rule — preserve T3-CUSTOM(expbkt3)
regions, take upstream elsewhere, follow upstream deletions unless the
fork depends on the removed code. 22 files conflicted (46 hunks).

Notable resolutions:
- observability.ts auto-merged; upstream's truncateTraceAttributes runs
  at record construction and the fork's retainSlowSqlSpans at sink push,
  so a slow SQL span is still emitted and its attributes are truncated.
- Migrations: upstream's 037_ProjectionTurnsKeysetIndex registers at
  fork id 1008 (legacy block already occupies 37); 1007 untouched.
- Upstream removed the plan right-panel surface and PlanSidebar.tsx;
  followed, keeping the fork's separate plannotator surface.
- ws.ts keeps the fork's durable execution-intent bootstrap rather than
  upstream's inline dispatchBootstrapTurnStart path.

Post-merge fixes so server, web and client-runtime typecheck cleanly:
ThreadPlanProgress wired into fork test layers, fork-required fields
added to upstream's new fixtures, and snooze helpers updated for their
new timestampFormat parameter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…l snapshot

Upstream PR pingdotgg#5561 rewrote shell-sync.test.ts and its new cached-snapshot
case asserts the surfaced snapshot equals the cached payload verbatim.
This fork's shell.ts normalizes every thread with an `execution` overlay
as it loads, so the surfaced object legitimately carries one field more
than the cache it came from and the upstream expectation cannot hold
here.

The normalization is fork-only and pre-existing (three call sites, both
before and after the merge; none upstream), and shell.ts took upstream's
rewrite in full, so the code is correct and the expectation is what
needed updating. Asserts the full shape rather than relaxing the match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Upstream v9 drops the "plan" right-panel surface (plans render inline in
the transcript), so a persisted panel whose only other surface was a plan
one now migrates to zero surfaces — and this fork's
withoutPersistedPlannotatorSurfaces then omits the record entirely. The
legacy-descriptor migration case asserted the plan surface survived.

Swaps the inert companion surface in these fixtures for "files", a kind
that still exists, so each case still asserts what it was written to
assert: descriptors removed, order preserved, nearest surface selected,
and the record omitted only when Plannotator was its sole surface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Upstream's windowed thread-detail pagination (pingdotgg#5493) arrived with its own
message query, listThreadMessageRowsByThreadWindow, which selects the
columns upstream knows about. It decodes rows with
ProjectionThreadMessageDbRowSchema, derived from this fork's
ProjectionThreadMessage — where sentByUserId is required (migration
034_ProjectionThreadMessageSender). Upstream has no such column, so every
decode through the bounded path failed with a missing-key schema issue
and the whole windowed thread-detail suite errored.

The non-windowed sibling listThreadMessageRowsByThread already selects
it; this brings the windowed variant in line. Marked so the next upstream
merge can see the column is fork-owned rather than dropping it again.

Note for future merges: a typecheck cannot catch this class — the SELECT
list is a template string, so a column omitted against a fork-extended
schema only surfaces at decode time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An agent that asks a question and then waits is invisible until you happen to
look at the sidebar. This adds an alert layer for the experimental sidebar:
a tone the moment a session needs input, and a second, distinct tone once
finished-but-unopened sessions pile up past a configurable threshold.

Everything lives in a new fork-owned `apps/web/src/notifications/` module with
three marked one-line seams upstream — the `__root.tsx` mount, one settings
path/label, one nav icon — and is gated on EXPERIMENTAL_CONTROL_CENTER_ENABLED
so bkmain can carry the code with the runner unmounted.

It is browser-local by construction. The pile-up signal is `hasUnseenCompletion`,
which reads `threadLastVisitedAtById` out of localStorage, so a server-side
preference would describe a number the server cannot compute. Preferences,
uploaded tones (IndexedDB), and the alert baseline are per-browser for the same
reason. Mattermost escalation deliberately stays in t3-linear-bridge, which
already polls every thread and owns delivery.

Four runtime rules carry the design, and are documented in the fork guide
because each one looks droppable and is not:

- Alerts fire on transitions, never on state. A client with no baseline adopts
  the snapshot silently, and a thread that appears already waiting counts as
  pre-existing — otherwise every reconnect becomes an alarm.
- Audio needs a user gesture, so the settings Test button doubles as the
  AudioContext unlock and the panel says when audio is still locked.
- Native notifications only fire when the tab is hidden; the visible tab already
  shows the row highlight.
- The pile-up alert is edge-triggered and re-arms only after the count drops.

Built-in tones are oscillator specs rather than audio files, keeping binary
assets out of a fork that merges from upstream weekly. Uploaded tones are
validated, capped at 2 MB, and any event pointing at a deleted tone falls back
to the default instead of silently muting.

Also carries the pending sidebar countdown spacing fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two changes that both hinge on how execution state reaches the client.

**Sessions stuck on "Running".** A finished agent kept rendering Running in the
sidebar and the chat while the server had already recorded activity=idle and
turn_state=completed. Execution is published over PubSub and never written to
the event log, so neither resume path can carry it: subscribeShell and
subscribeThread with an afterSequence replay domain events only, and
withShellExecutions runs solely on the full-snapshot fallback. Upstream survives
that because its shell reducer replaces the thread wholesale, so a stale overlay
lives until the next projection upsert. The fork preserves the overlay across
upserts (so a title edit stops wiping fresher execution), which turned a
one-second flicker into a permanent lie: a client that missed the terminal frame
— server restart, network blip, resumed tab — showed Running forever.

Both resume paths now emit the supervisor's current execution after catch-up,
reading in-memory state through the existing visibility filter. The reducer
additionally drops a preserved overlay when the upsert's own latestTurn reports
that overlay's turn finished; the contradiction travels in the same frame, so
the client no longer depends on a live frame it may never receive.

**Session titles.** Upstream titles a thread once, from its first prompt, so a
long session keeps a name describing what it started as. Every N user prompts
the turn-start path now dispatches an ordinary regenerateTitle meta update,
reusing the durable regeneration flow rather than renaming behind its back:
request ids, supersede checks, and interrupted-run recovery all still apply, and
a failure to dispatch is logged instead of touching the turn.

experimental.threadTitleMaintenance is on by default with a cadence of 3, is
editable in Experiments, and 0 disables it. A refresh does replace a title you
set by hand — nothing distinguishes a user title from a generated one — so the
setting says so and the cadence is the escape hatch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`vp check` and the commit hook's `vp fmt` disagreed on how to wrap the new
getSnapshot pipe, so CI rejected the formatting. No behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The resume stream now re-sends execution before the synchronization marker, so
the catch-up bound test asserted the wrong sequence. Updated rather than relaxed:
the frame's presence is the contract that stops a finished turn rendering as
Running forever.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Aug 7, 2026
Adds T3-CUSTOM(expbkt3) markers to the four fork edits this merge work
introduced into upstream-owned files, and drops the stale baseline entry
for PlanSidebar.tsx, which upstream deleted in pingdotgg#5558.

Uses BEGIN/END blocks rather than single-line markers: a single-line
marker added alongside its subject becomes part of the same diff hunk, so
it covers only itself and the check still reports the hunk as unmarked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tusharbhardwaj-bk
tusharbhardwaj-bk merged commit b6dda60 into bkmain Aug 7, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants