fix(core)!: kernel:bootstrapped / kernel:listening 抛错在 LiteKernel 上也失败 boot(#5257) - #5275
Merged
os-zhuang merged 1 commit intoAug 4, 2026
Merged
Conversation
… fails the boot on LiteKernel too (#5257) #5170 (PR #5258) unified `kernel:ready` across the two kernels and deliberately ruled that hook ONLY, pinning the rest as "still fail-soft on LiteKernel" so widening it would have to be a deliberate change. This is that change, for the two hooks that are still on the BOOT path. `kernel:bootstrapped` and `kernel:listening` now use the propagating dispatcher (`triggerHookOrThrow`) on LiteKernel, in the shape #5258 established: remaining handlers skipped, later boot hooks never fired, original error unwrapped, `state = 'stopped'`, and no "✅ Bootstrap complete". The failure this removes is the ugliest one available. HonoServerPlugin opens its socket inside a `kernel:listening` handler — `await server.listen(port)`, with no try/catch of its own, deliberately. When that rejected on LiteKernel (EACCES on a privileged port, a failure inside the port-fallback logic, an edge/serverless host where listen is unavailable) the throw was swallowed, `bootstrap()` resolved, and the process printed "✅ Bootstrap complete" with nothing listening — the health check after it was the first thing to notice, and it had already been told startup succeeded. The same plugin code on ObjectKernel failed the boot. Plain port-in-use never triggered it (server.listen falls back to a random port internally), which is why it stayed invisible. `kernel:shutdown` KEEPS fail-soft dispatch, now as an explicit per-hook judgement recorded at the dispatch site rather than an inherited default: on the teardown path the handlers queued behind a failing one — and the reverse-order destroy() pass after them — are what flush buffers and release resources. #5258's pin "keeps fail-soft dispatch for hooks other than kernel:ready" covered all three hooks in one assertion; it is split here into the three tests that are now true, per hook, so the flip is visible in the diff. Symmetric regressions added on BOTH kernels for bootstrapped and listening (incl. asserting the success line is never logged). Evidence: every non-test `kernel:bootstrapped` subscriber already guards itself (objectql's announceOpenMigrationGates catches internally — "an advisory must never be the reason a boot fails"; plugin-sharing wraps each of its five passes; plugin-pinyin-search wraps its backfill; service-automation's audit is in-memory reads behind an `if (!this.engine) return`). Suites: core 426, runtime 1313, client 222, http-conformance 46, service-automation 665, connector-{rest,mcp,slack,openapi} 79, plugin-hono-server 164, plugin-sharing 347, plugin-pinyin-search 14 — all pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 24 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 4, 2026 14:05
os-zhuang
enabled auto-merge
August 4, 2026 14:06
os-zhuang
deleted the
claude/issue-5257-listening-bootstrapped-propagate
branch
August 4, 2026 14:23
This was referenced Aug 4, 2026
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.
Fixes #5257
按 13:12Z 的 PM 裁定取 A,范围严格照单:两个 boot 路径钩子改传播,
kernel:shutdown保留 fail-soft 并把理由写在分发点。改了什么
packages/core/src/lite-kernel.ts——kernel:bootstrapped与kernel:listening从隔离式triggerHook换成传播式triggerHookOrThrow(#5170 / PR #5258 已落地的分发器,直接复用),并和kernel:ready一起收进同一个 try:一个 handler 抛错 ⇒ 该钩子后续 handler 不再跑、后面的 boot 钩子完全不触发、原始错误不包装地抛给调用方、state = 'stopped'、不打印「✅ Bootstrap complete」。这与ObjectKernel.bootstrap()的context.trigger(裸 await 循环)完全同形,#5258 为kernel:ready立的就是这个形状。kernel:shutdown保持triggerHook不变。分发点现在写着为什么:停机路径上排在失败 handler 后面的,正是其余订阅者的清理和逆序的destroy()—— 冲刷缓冲、关连接、放锁。中断它等于把一个坏 handler 放大成资源泄漏和未落盘的写。这是逐钩子写下来的判断,不是继承来的默认值,所以两个分发器是按钩子挑的,而不是整体换掉。packages/plugins/plugin-hono-server/**一个字没动 —— 传播本身就让它现有的代码是对的。为什么这条不只是对称性洁癖
HonoServerPlugin在kernel:listening里开监听套接字:await this.server.listen(port),故意没有自己的 try/catch。它 reject 时(特权端口 EACCES、端口回退逻辑本身失败、edge/serverless 宿主上 listen 根本不可用),在 LiteKernel 上异常被吞 →bootstrap()正常 resolve → 打印「✅ Bootstrap complete」→ 进程活着,没有任何 socket 在监听。同一份插件代码在 ObjectKernel 上是 boot 失败。第一个发现不对的是紧随其后的健康检查,而它已经被告知启动成功了。「端口占用」从来触发不了这条(
server.listen内部会回退到随机端口),这正是它一直没被发现的原因。kernel:bootstrapped上挂的是 reconcile / 审计类工作(objectql 的announceOpenMigrationGates、service-automation 的 node-type 与触发器绑定审计、plugin-sharing 的开机回填),吞掉是同一个谎的安静版本:审计静默失效。证据闸(枚举 + 跑套件)
非测试的
kernel:bootstrapped/kernel:listening订阅者全表,以及各自在健康 boot 下会不会抛:packages/objectql/src/plugin.ts:376announceOpenMigrationGates()自带try { … } catch {},注释原文「An advisory must never be the reason a boot fails」packages/plugins/plugin-sharing/src/sharing-plugin.ts:661packages/plugins/plugin-pinyin-search/src/pinyin-search-plugin.ts:82if (!engine) returnpackages/services/service-automation/src/plugin.ts:843if (!this.engine) return之后全是内存态读取(sealNodeTypeVocabulary/getTriggerBindingAudit/getFlowRuntimeStates)+ 日志packages/plugins/plugin-hono-server/src/hono-plugin.ts:605examples/app-showcase/src/security/{bind-position-sets,seed-approval-demo}.ts没有任何现存测试依赖这个吞:不需要 needs_decision。#5258 的爆炸半径集合原样重跑,外加三个直接订阅这两个钩子的插件包,全绿:
@objectstack/core@objectstack/runtime@objectstack/client@objectstack/service-automation@objectstack/http-conformanceconnector-{rest,mcp,slack,openapi}plugin-hono-serverplugin-sharingplugin-pinyin-search闸门:
check:type-check-coverage、check:doc-authoring、check:release-notes、check:adr-anchors、check:nul-bytes、check:startup-registry-verdict、check:durability-log-level、check:init-service-contract全部 PASS。@objectstack/core没有typecheckscript(#4311 的 DEBT 台账,冻结 91)。实测:main 基线tsc --noEmit= 91,本分支 = 95,+4 全是两个测试文件里既有的那一类 TS7006「Parameter 'ctx' implicitly has an 'any' type」噪声(根因是./types相对导入缺.js,由 #4311 统一处理),与新增用例数一一对应。台账不按数字设闸,故不改台账;在此明说以免日后重测时被当成新增漂移。pin 的显式翻面
#5258 留下的 pin 叫
keeps fail-soft dispatch for hooks other than kernel:ready (#5170),一条断言同时盖住 bootstrapped / listening / shutdown 三个钩子 —— 因为 #5170 的派发词只裁kernel:ready。本 PR 把它拆成三条现在为真的用例,逐钩子,让翻面在 diff 里看得见,而不是悄悄放宽:fails the boot when a kernel:bootstrapped handler throws (#5257)fails the boot — and never logs "Bootstrap complete" — when a kernel:listening handler throws (#5257)keeps fail-soft dispatch for kernel:shutdown — a failing handler must not block the remaining cleanup (#5257)(旧 pin 幸存的那一半,连同理由一起附到它唯一适用的那个钩子上;顺带断言后面的destroy()也确实跑到了)前两条在
kernel.test.ts里有对称的 ObjectKernel 双胞胎(照 #5258 的形状)。ObjectKernel 本来就是这个行为,加用例是因为:#5170/#5257 关掉的 bug 就是「一个钩子名在两个内核上是两个相反语义」,只有成对的用例才按得住 —— 任一内核回归都由一条具名用例接住,而不是靠另一条还绿着去推断。kernel:listening那条额外断言「Bootstrap complete从未被打印」:bootstrap()reject 只是契约的一半,另一半是出门时没有任何东西宣告成功。范围外发现(已单开,未认领)
kernel:shutdownhandler 会跳过所有插件destroy()并process.exit(1),日志还谎报「Shutdown timed out — forcing exit」。这就是本单要求「shutdown fail-soft 在两个内核上都 pin 住」时撞上的事实:ObjectKernel 那边不是 fail-soft,performShutdown()用的是传播式context.trigger,异常冒到只为超时写的 catch 里。实测(临时探针,vi.spyOn(process, 'exit')拦下退出,已删):reached = ["process.exit(1)"]—— 后一个 handler 和插件destroy()都没跑到。因此 shutdown 的 fail-soft pin 只在 LiteKernel 侧落地;在 ObjectKernel 侧写一条会process.exit的用例既不安全,改它也越界(本单硬约束:不得改这三个钩子以外的分发语义)。修法留给 core: ObjectKernel 上一个抛错的 kernel:shutdown handler 会跳过所有插件 destroy() 并 process.exit(1),日志还谎报「Shutdown timed out」 #5274 分诊。文档
content/docs/kernel/events.mdx—— #5258 加的那句诚实说明(「其余钩子两内核仍不同」)按新事实改写:三个 boot 路径钩子在两个内核上一致传播、且不会打印成功行;kernel:ready仍是放 boot 断言的地方(注册表到那时才填完);kernel:shutdown是刻意的例外并说明理由,同时诚实标注 ObjectKernel 停机路径尚未对齐(指向 #5274)。Changeset:
.changeset/kernel-boot-hook-failure-propagation.md,minor(与 #5258 同级),受众提示照 #5258 形状(vitest / serverless / edge 宿主中 bootstrapped/listening handler 会抛的那些),外加本单的具体标题句 ——listen()失败不会再换来一句假的「Bootstrap complete」。🤖 Generated with Claude Code
https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
Generated by Claude Code