Repository navigation
fix(server): promote shell reader worker, subagent archive and stuck-subagent fixes to bkt3 - #335
Conversation
…atabases (pingdotgg#19226) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Alex Southwell <saphid@gmail.com> (cherry picked from commit 8659fcf) Conflict resolution: ShellStream.ts keeps the fork side. This base has no loadShellSnapshotParts (upstream pingdotgg#17141, 1f31906, is not on stage), so the reader wrap for the shell snapshot routes lands in the follow-up fork commit at the routes' own transactions in http.ts and ws.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cherry-picked reader worker (upstream pingdotgg#19226) only moves reads marked NodeSqliteClient.readOnly. Upstream marks its shell snapshot routes inside loadShellSnapshotParts, which stage does not have, so this marks the fork's equivalents and the fork-only shell readers that froze bkt3: - http.orchestration.loadShellSnapshot (GET /api/orchestration/shell, the Linear bridge) and ws.orchestrationV2.loadShellSnapshot (shell subscription): the whole read transaction, enrichment after it. - ws getArchivedShellSnapshot: the whole read transaction. - ws.orchestrationV2.projectShellItems: the per-thread shell read. Events are published after their commit, so the reader already sees them. - Fork ProjectionSnapshotQuery shell() and threadShell(): only the ProjectionStore read, not the provider name lookup. Covers threadUsage.get, claudeAccounts.setThreadMode (access guard and owner lookup) and the other compatibility callers. Every marked read is a pure read of committed state. Inside an open transaction readOnly is ignored and the read stays on the writer. On the reader a deferred BEGIN does not wait on another process's write lock, where the writer's BEGIN IMMEDIATE blocks the event loop for up to busy_timeout (the 5 s setThreadMode stall). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fork marker check treats a line as upstream's only when it matches upstream main. Upstream changed the reader worker after pingdotgg#19226 (pingdotgg#19305, pingdotgg#19324, pingdotgg#19417, pingdotgg#19425, pingdotgg#19559), so the cherry-picked lines differ from upstream main even though they are upstream's code. - nodeSqliteClient.ts: BEGIN/END markers around the three cherry-picked regions, saying the next upstream merge takes upstream's side. The code stays byte-identical to 8659fcf. - ThreadPullRequestService.ts: drop the two comment lines that upstream itself removed in pingdotgg#19559, so the wrap matches upstream main exactly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A review of the reader-worker port found that the fork-only wraps of projectShellItems' per-event getThreadShell and ProjectionSnapshotQuery's threadShell() put frequent small reads on the single reader worker, where each waits behind multi-second full snapshots. After a restart that can fill the 1,000-event live buffers and make shell streams resubscribe. Upstream keeps these reads on the writer; only full snapshots move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ingdotgg#18443) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: SAPHID <saphid@users.noreply.github.com> (cherry picked from commit 62c74a9) Conflict resolution: ThreadLaunchService.ts keeps the fork's cleanup condition (`cancelled || !workspaceRecorded`, terminal close inside it). Stage has no `runReleased` flag, so only the rename fiber handle and its interrupt are ported. The test applies unchanged. Why now: "cancels tracked setup before provider work is released" failed on this branch because the background rename's thread.metadata.update (worktreePath: worktreeCwd) landed after the cancel's update to null. The race was already in the code; the reader-worker client adds a few effect steps per SQL statement, which moved the fiber interleaving. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rchive Archiving or deleting a thread never archived the provider-native subagent threads (Claude Task/Agent children and the like) mirrored under it. Every client hides those threads, but they stayed in every active shell read: on bkt3 on 2026-10-19, 318 of 1,144 active threads were finished subagents of an archived parent. A fork-owned reactor (SubagentArchiveCascade.expbkt3.ts) now: - archives a parent's provider-native subagent children on thread.archived and thread.deleted, and unarchives them on thread.unarchived; - once per start, after activation, archives every provider-native subagent whose parent is archived, deleted or missing, and logs the count. delegate_task children, forks and children with an unfinished run are left alone. Candidates come from one index-backed SQL read; commands go out four at a time. server.ts gains one marked import and one marked layer line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every archive also starts a history export that runs git in the worktree, so
archiving bkt3's ~318 orphaned subagents at once would burst ~1,300-1,600 git
processes right after a deploy.
The startup pass now queues one orphan per pause (default 2 seconds, set with
`make({ startupPause })`), so the archives and their exports spread over ~11
minutes. A separate fiber does the pacing and feeds the same worker that runs
live cascades. Live cascades therefore run between orphans and are not held
for the whole pass, and nothing races them on the same child. Each orphan is
re-checked when its turn comes and skipped if its parent came back. The pass
logs progress once per 50 orphans, then one summary line.
Tests pass a zero pause, and a new test advances TestClock to show one archive
per pause.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…live cascades The "unfinished run" guard could never fire: a provider-native subagent thread has no run (0 of 777 on bkt3). Its liveness lives in its parent's subagent record (orchestration_v2_projection_subagents, one per child thread). An archive now skips a child whose record is pending, running or waiting, using the child_thread_id index. When a provider-native record stops being live, its thread is re-checked and follows an archived parent, so archiving a parent mid-turn no longer leaves its subagents behind until the next restart. Unarchive no longer applies the guard. Live cascades are now paced too: a live archive or unarchive waits until the previous live move is `cascadePause` old (default 500 ms, an option to `make`). A bulk clean-out of parents with 40-60 subagents each therefore no longer bursts history exports. Startup orphans still come one per `startupPause` from the pacing fiber, and they interleave with live work in the shared worker. Each child is re-read just before it moves. Tests seed a live subagent record instead of a run. New tests cover a subagent that finishes after its parent was archived, and a cascade paced on the TestClock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…agents fix(server): provider subagent threads follow their parent into the archive
…-worker fix(server): shell reads run on a SQLite reader worker and no longer freeze the server
…t stops their session The Claude account service stops a thread's provider session between turns (rebalance, mode change, hard limit). Its idle check looked only at the parent turn, so it also stopped sessions whose background subagents were still working, and the session release recorded nothing for them: the subagent rows, the parent's subagent cards and the child's root turn stayed running until the parent got another message (84 minutes on 2026-10-20). - A released provider session now settles the provider-native subagents it leaves open (cancelled), for every release reason except server shutdown, which startup recovery already handles. A cheap subagent-row check first keeps idle releases from paying for the full recovery read. - Rebalance and mode-change restarts now wait for the session's background work, re-checked on the 15 s status poll and capped at 60 minutes. Access-revoked and hard-limit stops are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use Effect.catchTags (t3code/prefer-catch-tags), give the test store's getThreadRecords the projection's subagents, and cover an idle release with no open subagent skipping the full recovery read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tart waits - Skip a subagent whose provider thread the recovery read did not load: it may still run in another shared session. - A completePendingRestart fiber that lost the race no longer leaves a stale background-wait start behind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-session-stop fix(server): subagents no longer stay Running after an account restart stops their session
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged as bk-agent-01, pinned to the approved head
|
Post-deploy check on bkt3bkt3 runs
The #330 startup pass archived 165 of 168 orphaned subagent threads between 13:42 and 13:47 UTC. No session was released with open subagents yet, so the new |
24-hour check on bkt3 (2026-10-20 13:41 → 2026-10-21 14:15 UTC): the result does not hold
Cause of the freezes. The trace gaps on the main thread match full shell reads of 2.1–2.2 s that have no parent span (for example, 14:04:57.7, 14:09:27.4, 14:13:27.4 UTC). Cause of the 9 stuck cards. The |
What this promotes
This PR promotes three
stagefixes to bkt3 production. The files are the same asstageat3dc90ea742. The merge ofbkmain(#324) into this branch changes no file.How I tested
stage.gpt-6.1-solsessions ran 18 turns and 4 subagents.3dc90ea742since 13:29:30 UTC.git fetchofhermes-agentand a keybinding conflict).ffa176f511) cover the new paths.runningwith no update.Merge effect
Merging this PR deploys bkt3 and restarts
t3-nextbkt3.service. The restart stops every live session on bkt3. Merge at a quiet time.🤖 Generated with Claude Code