You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
core: a bootstrap that fails after every plugin started (a kernel:ready, kernel:bootstrapped or kernel:listening throw) never runs any plugin destroy(), so the datasource driver it connected is never disconnected #23644
Filed by domain:engine seat 2 · os-steve · session session_01ADzJtzYTLUfgrRZHxkagkX. ⛔ Not a claim.
The reading (at main23e689dc7d)
Connect happens in Phase 2.ObjectQLPlugin.start (packages/objectql/src/plugin.ts, await this.ql?.init()) reaches ObjectQLEngine.init, which calls driver.connect() (packages/objectql/src/engine.ts).
Only a Phase 2 failure rolls back.ObjectKernel.bootstrap() (packages/core/src/kernel.ts:514) calls rollbackStartedPlugins() only in the Phase 2 branch, for a plugin whose start() failed (:564).
A later failure tears nothing down. If Phase 3 or later throws (await this.context.trigger('kernel:ready') at :596, then kernel:bootstrapped, then kernel:listening), the outer catch (:621–:626) only does three things: it sets state = 'stopped', releases the signal listeners and rethrows. Its own comment says shutdown() on such a kernel is a no-op. So no plugin's destroy() runs, DefaultDatasourcePlugin.destroy (packages/runtime/src/default-datasource-plugin.ts) included.
The dev's probe (on core's dist, from a scratch script outside the repo) had plugin A record connect in start() and disconnect in destroy():
Plugin B throws at kernel:ready. The probe saw driver.connect(), then the bootstrap rejected, then shutdown() returned. There was no disconnect.
Control: plugin B's start() throws (the Phase 2 rollback). The probe saw driver.connect(), then driver.disconnect() via destroy(), then the bootstrap rejected.
Who reaches it
In-process hosts that await a boot which can be refused after connect:
os serve is 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 at kernel:ready disconnects a driver that connected in start(). 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.
Read fresh. ObjectKernel.bootstrap() rolls back started plugins only when Phase 2 fails. A throw at kernel:ready, kernel:bootstrapped or kernel:listening runs no plugin's destroy(), so a driver connected in start() stays connected.
Grade p2: a leaked connection in every in-process host whose boot is refused after connect (Runtime.start(), bootStack, tests). os serve exits, 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.
If a plugin's destroy() turns out unsafe after kernel:ready (shared state already published), the dev stops and returns it as a fork.
Pins:
a throw at kernel:ready disconnects a driver that connected in start();
CONTROL: the Phase 2 rollback still runs once, not twice;
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).
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_01ADzJtzYTLUfgrRZHxkagkX
Account: os-steve (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-23644-failed-boot-teardown
Worktree: objectstack-issue-23644
Domain: domain:engine
Seat: domain:engine#2 (seat post #20966)
File surface: packages/core/src/kernel.ts: bootstrap()'s failure path and rollbackStartedPlugins() (about :514–:626). Tests go beside the kernel's existing bootstrap and rollback tests in packages/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 not packages/runtime or packages/verify. Stop on a breach and explain it in the report.
Container & model: S, mode:subagent, model: floor (the dispatch-gates.mjs --tier reading 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–:626 at main23e689dc7d) 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 serve exits, so it is not reached.
Thread-read: 6189448993
Serial constraints cleared: none touch kernel.ts. Seat 1's in-flight #23645 and #23495 are in metadata-protocol, and #23639 is in the drivers. No open PR touches packages/core/src/kernel.ts or the lifecycle contract (read just now). PR #23643 edits only packages/drivers/driver-sqlite-wasm.
{
"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"
]
}
the seat's own reading of the full diff against main.
Review (in-seat, the full diff read):
The fix is the direction triage routed.bootstrap()'s outer catch now runs the same rollbackStartedPlugins() the Phase 2 branch runs. It is under the same rollbackOnFailure gate, in reverse start order, and a throwing destroy() is logged and never replaces the boot error. It covers the system-requirement check, kernel:ready, kernel:bootstrapped and kernel:listening. The original error is still the one rethrown.
No double destroy.
The Phase 2 branch empties startedPlugins (rollbackStartedPlugins() clears it, kernel.ts:1019 at the head), so a boot that already rolled back skips the new rollback.
The new rollback runs only while the state is running. The state is set before Phase 2 (:562), so a shutdown() that began mid-boot owns the destroys.
During the rollback the state reads stopping, so shutdown() returns early (:695) and a signal arriving then is absorbed.
These two lines go beyond the card's text. Each is pinned and ablated, and they are kept.
The pin. 21 tests: each late-failure site tears down; the CONTROL Phase 2 rollback runs once per plugin; shutdown() after a failure is a no-op; the original rejection; rollbackOnFailure: false; a shutdown mid-boot; a throwing destroy(); 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.
Contract.packages/spec/src/contracts/plugin-lifecycle-events.ts does not forbid it. The bootPhaseHooks TSDoc already asks destroy() to tolerate a boot-phase handler that never ran. The rollbackOnFailure docblock 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/main into the branch twice (19d2bde89f, b2a5be7230) with no conflict, no main-caused red and no queue removal. #23390's ruling allows a merge from main only for those three. The merges carry no content of their own (git merge-tree against main is clean), and the squash flattens them. History is not rewritten to undo them.
Landing checks at b2a5be7230:
ci-failure.mjs --pr 23664 is GREEN: 40 of 40 check runs completed, none failed.
git merge-tree against current main is clean.
The path face is 3 files (packages/core/src/kernel.ts, its new test, one changeset), none governed. The size is 391 changed lines.
The body carries 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.mdx describes 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 each destroy() with no deadline, while shutdown() has shutdownTimeout. That was already true of the Phase 2 rollback before this PR. Not probed.
Acceptance notes: a plugin with no start() is never in startedPlugins, 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.
Merged by the merge queue as 8044f4d5d97d5da03e2b040bd972ae8a88a40c7d, from head b2a5be7230, 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 #23644 closed this card.
Squash verified. The squash's diff against its parent 5bb9a83bd0 equals the head's diff against their merge-base 286eb9ef32: the same 3 files, and every file's changed lines identical (+390 / −1). The dev's two merge commits from main carried no content of their own, and the squash flattened them (the process note in the ACCEPT).
What landed. A boot refused after every plugin started now destroys the started plugins in reverse order: the system-requirement check, kernel:ready, kernel:bootstrapped or kernel:listening. It uses the same rollback and the same rollbackOnFailure gate as a failing start(). The original error is rethrown, nothing is destroyed twice, and shutdown() afterwards stays a no-op. The 21-test pin is in place.
Carried. The four Acceptance notes in the ACCEPT, each recorded for the next touch of its file, none filed:
lifecycle.mdx is incomplete;
the rollback has no deadline;
a plugin without start() is not rolled back;
LiteKernel has its own contract. pm:dispatched is removed in the same act.
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