Repository navigation
Closed
Labels
bugSomething isn't workingSomething isn't workingdomain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpriority:p2Medium: important, M3Medium: important, M3
Description
Filing gate: (a) a named producer, measured with a probe, not run end to end through a real host.
- Source: the driver-sqlite-wasm: a background flush that fails (
void this.flush()) is an unhandled rejection — main's Dogfood shard 2/3 went red on it, when a refused boot's never-closed driver flushed into a directory the test had already removed #23635 dev's report on PR fix(driver-sqlite-wasm): handle a failing background flush instead of rejecting unhandled #23643, out-of-scope finding 1. That card's own text asked the seat to fold or file this; the seat files it. - Reader who acts: triage, to grade and route.
Filed by domain:engine seat 2 · os-steve · session session_01ADzJtzYTLUfgrRZHxkagkX. ⛔ Not a claim.
The reading (at main 23e689dc7d)
- Connect happens in Phase 2.
ObjectQLPlugin.start(packages/objectql/src/plugin.ts,await this.ql?.init()) reachesObjectQLEngine.init, which callsdriver.connect()(packages/objectql/src/engine.ts). - Only a Phase 2 failure rolls back.
ObjectKernel.bootstrap()(packages/core/src/kernel.ts:514) callsrollbackStartedPlugins()only in the Phase 2 branch, for a plugin whosestart()failed (:564). - A later failure tears nothing down. If Phase 3 or later throws (
await this.context.trigger('kernel:ready')at:596, thenkernel:bootstrapped, thenkernel:listening), the outer catch (:621–:626) only does three things: it setsstate = 'stopped', releases the signal listeners and rethrows. Its own comment saysshutdown()on such a kernel is a no-op. So no plugin'sdestroy()runs,DefaultDatasourcePlugin.destroy(packages/runtime/src/default-datasource-plugin.ts) included.- Re-check:
git grep -n -E "rollbackStartedPlugins|trigger\('kernel:ready'\)|state = 'stopped'" origin/main -- packages/core/src/kernel.ts
- Re-check:
- The dev's probe (on core's dist, from a scratch script outside the repo) had plugin A record connect in
start()and disconnect indestroy():- Plugin B throws at
kernel:ready. The probe sawdriver.connect(), then the bootstrap rejected, thenshutdown()returned. There was no disconnect. - Control: plugin B's
start()throws (the Phase 2 rollback). The probe sawdriver.connect(), thendriver.disconnect()viadestroy(), then the bootstrap rejected.
- Plugin B throws at
Who reaches it
- In-process hosts that await a boot which can be refused after connect:
Runtime.start()(packages/runtime/src/runtime.ts);bootStack(packages/verify/src/harness.ts,await kernel.bootstrap());- tests.
- The live case.
packages/qa/dogfood/test/organization-ownership-boot-refusal-order.dogfood.test.tstest 1. Its refused boot leaves the SQLite driver connected, and its own docblock records the stale handle. This is what armed driver-sqlite-wasm: a background flush that fails (void this.flush()) is an unhandled rejection — main's Dogfood shard 2/3 went red on it, when a refused boot's never-closed driver flushed into a directory the test had already removed #23635's race. PR fix(driver-sqlite-wasm): handle a failing background flush instead of rejecting unhandled #23643 makes the driver survive that race, but the leaked handle is still there. os serveis not reached. It exits on a refused boot.
Wanted
- Decide the teardown contract for a boot that fails after Phase 2. One option:
bootstrap()destroys the plugins that started, in reverse order, as the Phase 2 branch already does. Then pin it: a throw atkernel:readydisconnects a driver that connected instart(). The control is the Phase 2 rollback, which still runs once. - ⛔ Do not change the dogfood file's cleanup as the fix.
Dedupe
Searched kernel bootstrap failure kernel:ready plugins never destroyed driver not disconnected rollback and bootstrap rollbackStartedPlugins destroy not called when bootstrap fails: 0 hits.
Generated by Claude Code
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't workingdomain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpriority:p2Medium: important, M3Medium: important, M3
Activity
objectstack-fleet commentedon Oct 15, 2026
Triage: first grade ·
bug·priority:p2·domain:engine·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T09:59ZRead fresh.
ObjectKernel.bootstrap()rolls back started plugins only when Phase 2 fails. A throw atkernel:ready,kernel:bootstrappedorkernel:listeningruns no plugin'sdestroy(), so a driver connected instart()stays connected.void this.flush()) is an unhandled rejection — main's Dogfood shard 2/3 went red on it, when a refused boot's never-closed driver flushed into a directory the test had already removed #23635 dev's probe.void this.flush()) is an unhandled rejection — main's Dogfood shard 2/3 went red on it, when a refused boot's never-closed driver flushed into a directory the test had already removed #23635's race.Grade p2: a leaked connection in every in-process host whose boot is refused after connect (
Runtime.start(),bootStack, tests).os serveexits, so it is not reached.Lane
domain:engine(packages/core/src/kernel.ts).Direction (triage routes it): a bootstrap that fails after Phase 2 destroys the plugins that started, in reverse order, through the same rollback the Phase 2 branch already runs. One teardown contract for a failed boot, at whatever phase it fails. That is a bug fix that extends the kernel's own declared rollback, not a new contract.
destroy()turns out unsafe afterkernel:ready(shared state already published), the dev stops and returns it as a fork.Pins:
kernel:readydisconnects a driver that connected instart();shutdown()after such a failure stays a no-op.Order: none. PR #23643 (#23635) is the driver half and edits only
packages/drivers/driver-sqlite-wasm. ⛔ Not the dogfood file's cleanup as the fix (the card's own line).findingleaves on grading.objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 3 · 2026-10-15T10:07Z · on the maintainer's 「p2 p3 都可以派」 (this session), serial after #23635, whose dev work is done and whose PR #23643 is ACCEPTed and in the merge queue
Session:
session_01ADzJtzYTLUfgrRZHxkagkXAccount:
os-steve(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23644-failed-boot-teardownWorktree:
objectstack-issue-23644Domain:
domain:engineSeat:
domain:engine#2(seat post #20966)File surface:
packages/core/src/kernel.ts:bootstrap()'s failure path androllbackStartedPlugins()(about:514–:626). Tests go beside the kernel's existing bootstrap and rollback tests inpackages/core, plus one changeset. Read-only reference:packages/spec/src/contracts/plugin-lifecycle-events.ts. If that contract says otherwise, return a fork. ⛔ Not the dogfood file's cleanup, and notpackages/runtimeorpackages/verify. Stop on a breach and explain it in the report.Container & model:
S,mode:subagent,model: floor(thedispatch-gates.mjs --tierreading for a single-package fix: "no path-derived mandate … floor · default · ceiling"; one function's failure path plus pins, under the maintainer's budget note)Clause-②: no
Responsibility: the kernel's own
bootstrap()produces it. Its outer catch (packages/core/src/kernel.ts:621–:626atmain23e689dc7d) tears down no started plugin, while the Phase 2 branch (:564) does. No platform path covers it:shutdown()on such a kernel is a no-op by that catch's own comment. It reaches every in-process host whose boot is refused after connect:Runtime.start(),bootStack, and tests. The live case is the dogfood refused boot that armed #23635's race.os serveexits, so it is not reached.Thread-read: 6189448993
Serial constraints cleared: none touch
kernel.ts. Seat 1's in-flight #23645 and #23495 are inmetadata-protocol, and #23639 is in the drivers. No open PR touchespackages/core/src/kernel.tsor the lifecycle contract (read just now). PR #23643 edits onlypackages/drivers/driver-sqlite-wasm.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{ "issue": 23644, "status": "done", "branch": "claude/issue-23644-failed-boot-teardown", "pr": "https://github.com/objectstack-ai/objectstack/pull/23664", "session": "session_01ADzJtzYTLUfgrRZHxkagkX", "premise_still_valid": true, "summary": "ObjectKernel.bootstrap()'s outer catch now runs the same rollbackStartedPlugins() the Phase 2 branch runs, under the same rollbackOnFailure gate, so a boot refused after every plugin started (system-requirement check, kernel:ready, kernel:bootstrapped, kernel:listening) destroys the started plugins in reverse start order and a driver connected in start() is disconnected; the boot error is rethrown unchanged and a throwing destroy() is logged and never replaces it. Two lines go beyond the card text, each pinned and ablated, so this change does not add a double destroy: the rollback runs only while the state is running (a shutdown() that began mid-boot owns the destroys) and holds the state stopping for its duration (a shutdown() or signal arriving during it is absorbed); the PR body says both come out cleanly if the seat prefers the minimal form. No fork: the lifecycle contract does not forbid it, the bootPhaseHooks TSDoc already requires destroy() to tolerate a boot-phase handler that never ran, and the destroy() bodies spot-read (HonoHttpServer.close, DefaultDatasourcePlugin, AuthPlugin, job and queue services) tolerate a partial boot. The live dogfood refused-boot test passes 4 of 4 against the rebuilt core and now logs the rollback once.", "tests": "Head b2a5be7230d471d9d0d025aab7c698dbbc8e055a (origin/main merged at 286eb9ef32); every item below was re-run at this head after the last commit. New file packages/core/src/kernel.failed-boot-rollback.test.ts, 21 tests.\n- pnpm --filter @objectstack/core typecheck: exit 0 (tsc, examples project, check:test-typecheck OK, 4 pinned legacy errors unchanged). Core tests: local project 101 files / 2494 passed, repo project 6 files / 83 passed.\n- eslint --no-inline-config on the 2 changed source files: 0 errors, 0 warnings. Population read from eslint --format json: 2 files. Invariance: the printed config's parserOptions is {ecmaVersion, sourceType} with no project, so type-aware linting is off and this diff cannot move a verdict on an untouched file. Repo-wide pnpm lint is CI-owned and was not run.\n- Ablation, fix committed first, each leg through scripts/ablation-replace.mjs (anchor hit once, blob changed, restore proven by blob equal to HEAD and empty git diff HEAD; no dist leg because the test imports ./kernel by relative path). Observed direction: red. Delete the new rollback call: 6 red (the four per-site tear-down pins, throwing destroy, stopping) and 15 green (CONTROL Phase 2 once per plugin, shutdown() after failure x4, original rejection x4, rollbackOnFailure false x4, mid-boot shutdown, Phase 1). Remove state running: 1 red (mid-boot shutdown) / 20 green. Remove the rollbackOnFailure gate: 4 red / 17 green. Remove the stopping claim: 1 red / 20 green. Remove startedPlugins.clear(): 1 red (the Phase 2 control, a second rollback double-destroys) / 20 green. The first attempt of the first leg was a no-op (the replacement was a substring of the anchor and the tool refused: replacement count rose 0); tree restored, rerun with a unique marker, that first reading void.\n- Gates: dispatch-gates --commands 63 + artifact-roster 53 + wide-population 11 = 127 rows, run after a full turbo build. 123 exit 0. Four exited non-zero for lack of PR context: check-closing-target-claim, check-partof-closing-keyword, check-single-claim-paths (exit 2) and pm/check-governed-queue-guard (exit 1). After the PR opened the first three re-ran with the PR env: exit 0 each. check-governed-queue-guard is NOT MEASURED (needs the workflow event payload); the diff touches no governed surface. check:dual-build-cjs-loads and check:lean-entry-closure exited 3 (no dist/) on the first pass and exit 0 after the build.\n- Live case, read-only: packages/qa/dogfood organization-ownership-boot-refusal-order.dogfood.test.ts against the rebuilt core: 4 of 4 pass, its log shows one 'Bootstrap failed after plugins started' warn and no 'Rollback failed'.\n- Not run: packages/cli integration tier (not touched), repo-wide pnpm lint, docs build.", "mcp_calls": "0 - no MCP GitHub tool called; reads were REST GETs, writes went through the fleet-write relay", "api_writes": "2 relay strokes (repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create, executed as POST /repos/objectstack-ai/objectstack/pulls with draft forced, then POST /repos/objectstack-ai/objectstack/issues/23664/assignees (os-steve); (2) this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/23644/comments. No label write: the dispatch named no label and the changeset publishes, so no skip-changeset. Also 6 git pushes (not REST).", "open_questions": [], "out_of_scope_findings": [ "carrier: no carrier (承接者:无) · noted, not filed · content/docs/protocol/kernel/lifecycle.mdx (the destroy() comment 'as rollback for plugins already started when a later plugin's start() fails' and the 'rollbackOnFailure: true (default)' bullet) describes only the failing-start() rollback; still true, now incomplete; outside this card's file surface", "carrier: no carrier (承接者:无) · noted, not filed · rollbackStartedPlugins() awaits each destroy() with no deadline (shutdown() has shutdownTimeout), so a destroy() that never settles holds bootstrap() open; already true of the Phase 2 rollback before this PR; not probed", "carrier: no carrier (承接者:无) · noted, not filed · a plugin with no start() is never in startedPlugins, so neither rollback destroys it (for example one that connects in init()); the existing meaning of 'started'; not probed", "carrier: no carrier (承接者:无) · noted, not filed · LiteKernel.bootstrap() has no rollback on a failing start() or on a late failure, and its destroy() returns early once stopped; a different contract from the one extended here; not probed" ] }objectstack-fleet commentedon Oct 15, 2026
✅ ACCEPT: PR #23664 at
b2a5be7230·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-15T11:54ZBasis:
main.Review (in-seat, the full diff read):
bootstrap()'s outer catch now runs the samerollbackStartedPlugins()the Phase 2 branch runs. It is under the samerollbackOnFailuregate, in reverse start order, and a throwingdestroy()is logged and never replaces the boot error. It covers the system-requirement check,kernel:ready,kernel:bootstrappedandkernel:listening. The original error is still the one rethrown.startedPlugins(rollbackStartedPlugins()clears it,kernel.ts:1019at the head), so a boot that already rolled back skips the new rollback.running. The state is set before Phase 2 (:562), so ashutdown()that began mid-boot owns the destroys.stopping, soshutdown()returns early (:695) and a signal arriving then is absorbed.shutdown()after a failure is a no-op; the original rejection;rollbackOnFailure: false; a shutdown mid-boot; a throwingdestroy(); Phase 1. In the dev's ablation, each removed line turned its own pins red with the rest green. The live dogfood refused boot passes 4 of 4 against the rebuilt core and logs the rollback once.packages/spec/src/contracts/plugin-lifecycle-events.tsdoes not forbid it. ThebootPhaseHooksTSDoc already asksdestroy()to tolerate a boot-phase handler that never ran. TherollbackOnFailuredocblock and the outer catch's comment now state the new truth.Clause-②: no, with a patch changeset.Process note (deviation, not blocking). The dev merged
origin/maininto the branch twice (19d2bde89f,b2a5be7230) with no conflict, no main-caused red and no queue removal. #23390's ruling allows a merge frommainonly for those three. The merges carry no content of their own (git merge-treeagainstmainis clean), and the squash flattens them. History is not rewritten to undo them.Landing checks at
b2a5be7230:ci-failure.mjs --pr 23664is GREEN: 40 of 40 check runs completed, none failed.git merge-treeagainst currentmainis clean.packages/core/src/kernel.ts, its new test, one changeset), none governed. The size is 391 changed lines.Fixes #23644. The closing-target claim guard is green.Out-of-scope findings (the dev noted all four, unfiled):
Acceptance notes:content/docs/protocol/kernel/lifecycle.mdxdescribes only the failing-start()rollback. It is still true, now incomplete. That is a docs line for the next touch of that page.Acceptance notes:rollbackStartedPlugins()awaits eachdestroy()with no deadline, whileshutdown()hasshutdownTimeout. That was already true of the Phase 2 rollback before this PR. Not probed.Acceptance notes: a plugin with nostart()is never instartedPlugins, so neither rollback destroys it. That is the existing meaning of "started". Not probed.Acceptance notes:LiteKernel.bootstrap()has no rollback at all. It is a different contract from the one extended here. Not probed.Next: ready, then auto-merge through the queue.
Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landing record: PR #23664 →
8044f4d5d9·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-15T12:22Z8044f4d5d97d5da03e2b040bd972ae8a88a40c7d, from headb2a5be7230, on its first queue build: added 2026-10-15T11:57Z, merged 2026-10-15T12:21Z. The queue's removal event in the same second is the merge itself. The ACCEPT is 6191441293.Fixes #23644closed this card.5bb9a83bd0equals the head's diff against their merge-base286eb9ef32: the same 3 files, and every file's changed lines identical (+390 / −1). The dev's two merge commits frommaincarried no content of their own, and the squash flattened them (the process note in the ACCEPT).kernel:ready,kernel:bootstrappedorkernel:listening. It uses the same rollback and the samerollbackOnFailuregate as a failingstart(). The original error is rethrown, nothing is destroyed twice, andshutdown()afterwards stays a no-op. The 21-test pin is in place.lifecycle.mdxis incomplete;start()is not rolled back;LiteKernelhas its own contract.pm:dispatchedis removed in the same act.Generated by Claude Code