Repository navigation
fix(server): held queues record a reason and stale automatic holds expire - #917
Merged
Merged
Conversation
Held queued runs now record when and why they were held (stop, restart, provider_failure, inherited). A completed restart continuation releases the restart holds recorded before it finished, Stop replaces a restart hold, and a planned update's continuity keeps owning its threads. An hourly sweep, first run after startup recovery, cancels held automatic messages older than T3CODE_HELD_QUEUE_EXPIRY_HOURS (default 24) through queued-run.cancel. Human-typed messages never expire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Knip reported five exports that only the module itself uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 10, 2026
…261110 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hold reason and time were stored as queueHeldAt and queueHeldReason inside the upstream run payload, which the Jones database boundary forbids. They now live in jones_queue_holds (Jones migration 147), a sparse table keyed by upstream run ID with no upstream references. EventSink writes and deletes the rows in the same transaction as the hold or release events. It classifies them by event and command type, or by an explicit intent on the provider-failure write. Stop, restart release and the expiry sweep read rows by primary key. Run payloads, events and contracts return to their upstream shape. A missing table or row reads as a hold recorded before reasons existed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The receiving V2 synthetic fixture asserts the exact Jones migration ledger that the real migrator writes. Migration 147 (QueueHolds) is now part of that ledger, so the strict expectation lists it after 146. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mjones13
changed the base branch from
fix/orphaned-run-records-20261110
to
main
November 11, 2026 23:57
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mjones13
added a commit
that referenced
this pull request
Nov 12, 2026
Brings in #913 (orphaned run records), #917 (held queue expiry and the sparse Jones jones_queue_holds table at migration 147), #930 (install status reconcile), #931 (question line breaks) and #935 (signed Ubuntu APT retrieval during transport failures). #917 landed first and keeps Jones migration 147 (QueueHolds). The sync's unreleased Jones migrations move up by one, above the highest main ID: NativeExecutionProcessCustody 148, OrdinaryCheckoutProcessCustody 149, AuthSessionClientMetadata 150, WorkspaceRuntimeUses 151 and PeerLinksAdoption 152. These IDs were only ever on the unmerged sync branches (#850 and this successor), never on main. Their module files, ledger tests and the WorkspaceRuntimeUses basis literal in RuntimeUseInstallation follow the new IDs; no shipped schema or row changes. server.ts keeps the sync's managed-policy imports and adds main's HeldQueueExpiry (WorkMode and Scheduler were already imported). The boundary JSON is a structural three-way merge; the install-status lint and fmt commands keep the sync's sidebar path and gain main's finishPlan files. The server compile-cache anchor is re-pinned for server.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mjones13
added a commit
that referenced
this pull request
Nov 12, 2026
…overy reads Root-run tests now collect again, which exposes stale expectations: - 149 and 151 upgrade tests build their manifests with the new IDs. - Ledger and fixture expectations include 152 PeerLinksAdoption, which copies rows only and adds no schema object. - The RuntimeUseInstallation repair check reruns the manifest through 151 only, so a pending 152 cannot mask a schema repair. - The #917 restart-hold test mocks getMessagesById, which upstream's startup recovery now uses to read queued messages; the fixture has none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
queueHeldon a V2 run is a bare boolean with no reason or time. Restart holds never end: a restart continuation that finishes does not release the messages held by the restart. Only Resume, a selected send, or a recovered preparation failure clears a hold. On the owner's environment, 146 held runs piled up for weeks (historical count from 2026-11-10, not re-read). 145 of them were machine-made: monitor, background-task, MCP and peer notifications.Design
stop|restart|provider_failure|inherited) and hold time live injones_queue_holds(Jones migration 147,apps/server/src/jones/persistence/Migrations/147_JonesQueueHolds.ts).EventSink.applyStoredEvents(accepted-shared) callsacceptQueueHoldEventsonce per write, after projections, in the same SQLite transaction as the event append and receipt. There is no window between the event and the row, and a duplicate command writes nothing at its receipt.classifyQueueHoldEvents(apps/server/src/jones/orchestration/QueueHoldStore.ts) is pure and issues no SQL unless an event matches. It records:run.createdqueued behind held work →inherited;run.updatedunderprovider-runtime.reconcile→restart, which is startup recovery's one hold site;run.updatedunderthread.stoporrun.interrupt→stop;startNextQueuedRunpasses an explicitqueueHoldintent on its system write →provider_failure. The intent names only runs not already held, so an earlier hold keeps its reason and time.queueHeld: false), a held run leaving the queue (for example by cancel or expiry), andthread.deleteddelete rows by key.restartis re-emitted unchanged, and the hook replaces its row withstop. A continuation that completes after the user pressed Stop therefore releases nothing the user stopped.message:restart-continuation:,startNextQueuedRunfirst releases that thread's recordedrestartholds from no later than the continuation's completion, then drains as usual.PlannedUpdateContinuity) is left to that owner.apps/server/src/jones/orchestration/HeldQueueExpiry.ts). The sweep is registered with the serverScheduler, so it first runs after startup recovery and then at most hourly.(status, thread_id)recovery index and each message by primary key, then merges recorded holds by run ID.T3CODE_HELD_QUEUE_EXPIRY_HOURS(default 24), measured from the recorded hold time or elserequestedAt;queued-run.cancelpath with command IDcommand:held-queue-expiry:<runId>. The sweep logsjones.held-queue-expiry.cancelledwith run IDs, thread IDs, reasons and ages, and no message text.user, orcreatedByother thanuser;webormobile.QueueHold.test.tsbehaviour is kept.queued-run.cancel(dispatchQueuedRunCancel) writes events only on the cancelled run's own thread. It emits no events or effects on a sender's thread.Database boundary
An earlier head of this PR added
queueHeldAtandqueueHeldReasonto the upstreamOrchestrationV2Runpayload, against the Jones database boundary. This head removes them.packages/contracts/src/jones/queueHold.tsand its test are gone.ProviderRuntimeRecoveryService.tsandruntimeLayer.test.tsare back to their base form.Deployment note
The first sweep after an update cancels stale automatic held runs. Human-typed held messages stay held until resumed or cancelled by hand. Migration 147 only creates the empty table, and existing holds read as legacy.
Tests
QueueHoldReasons.test.tscovers:queueHeld*key appearing in projected run payloads ororchestration_events;restartwith an upstream-shaped payload;HeldQueueExpiry.test.tscovers:requestedAt;UpstreamIsolation.test.tsandMigrations.test.tsinclude migration 147, and upstream pruning and deletes are not blocked by ajones_queue_holdsrow.expected 'message:free' to equal 'message:first'. The candidate passes it.Checks run on
aa5cef5c2c(plus the fixture-ledger fix ina1f35a9997)Requalified on
60e4d1a2adafter mergingmain: 20 files, 454 tests passed. They cover thejones-held-queue-expiryandjones-queue-hold-isolationgroups,currentFixtures.test.ts, the migration-list tests and #928's steer tests. Typecheck, strict boundary, migration order andgit diff --checkalso pass.Local, Node 26.12.1, serial:
jones-held-queue-expirygroup (10 files)jones-queue-hold-isolationremainder,jones-orphaned-run-recordsremainder (#913, stacked top),115_…test.ts,NativeCreationRepositoryMigration.test.ts(16 files)vp lint <changed files>vp run --filter t3 typecheckknip --workspace apps/server --exportsnode scripts/jones/check-boundary.ts --strictnode scripts/jones/ci/jones-migration-order.cjs --baseline 3f3328a54c --candidate aa5cef5c2cgit diff --checkHead
a1f35a9997adds one line toapps/server/scripts/jones/currentFixtures.test.ts: its strictjones_sql_migrationsexpectation now lists{ id: 147, name: "QueueHolds" }. Onaa5cef5c2c, CITest Server 8failed only on that assertion, and the same 2 tests failed locally for the same reason. On the cleana1f35a9997commit the file passes (8 of 8). The harness requires a clean worktree bound to the commit.CI history: on
3a1bca5,Checkpassed. The advisoryServer parallel (linux)and(macos)jobs failed during dependency install at "Downloading Electron binary..." before any test ran. CI foraa5cef5c2cis on the checks tab.Overlap
763a4f538c4. This PR now targetsmainand includesmainat that commit, including fix: preserve inherited steers after prospective model saves #928 and fix(ci): recover signed Ubuntu dependencies during DNS outages #935, through merge60e4d1a2ad.QueueHolds.mainends at 146, so none of these is released. Per the allocation rule, whichever lands second moves its unreleased IDs abovemain's maximum and requalifies. The merge order is being coordinated before this PR merges.node scripts/jones/ci/jones-migration-order.cjsagainst freshmainimmediately before merging. On60e4d1a2adagainst763a4f538c4it accepts 1 appended identity.Generated by Claude Opus 5.5 in Claude Code; Jones database boundary repair by Claude Opus 5.5 (Jace Claude) from a Claude Fable 5.2 design review.
🤖 Generated with Claude Code