Skip to content

core: kernel:listening / kernel:bootstrapped / kernel:shutdown 在 LiteKernel 上仍被吞 —— HonoServerPlugin 的 listen() 失败会得到一句「✅ Bootstrap complete」而没有监听端口 #5257

Closed

Description

@os-zhuang

发现于 #5170(统一 kernel:ready 失败语义)的落地期间。#5170 的派发词明确只裁 kernel:ready 一个钩子,其余钩子的语义不在该单范围内,故另开此单,未在 PR 中修。

事实

#5170 落地后,LiteKernel.bootstrap() 的三个钩子分发是这样的:

钩子 ObjectKernel LiteKernel(#5170 之后)
kernel:ready 传播,boot 失败 传播,boot 失败 ✅ 已统一
kernel:bootstrapped 传播,boot 失败 吞 —— 一条 Hook handler failed 日志,boot 继续
kernel:listening 传播,boot 失败 吞 —— 同上
kernel:shutdown 传播 吞

原因与 #5170 同源:kernel.ts 走 this.context.trigger(...)(裸循环、不 catch),lite-kernel.ts 这三处仍走 kernel-base.ts 的 triggerHook(每 handler try/catch,注释 "Continue with other handlers even if one fails")。#5170 只把 kernel:ready 换成了传播版分发器。

为什么这条不只是对称性洁癖

kernel:listening 是 HTTP 服务器真正开监听的锚点。packages/plugins/plugin-hono-server/src/hono-plugin.ts(约 605 行)里:

ctx.hook('kernel:listening', async () => {
    const port = this.options.port ?? 3000;
    await this.server.listen(port);
    ...
});

await this.server.listen(port) 没有自己的 try/catch。它 reject 时(特权端口 EACCES、端口回退逻辑本身失败、edge/serverless 宿主上 listen 不可用等),在 LiteKernel 上的结果是:异常被 triggerHook 吞掉 → bootstrap() 继续 → 打印 ✅ Bootstrap complete 并正常 resolve → 进程活着、没有任何 socket 在监听。同一份插件代码在 ObjectKernel 上是 boot 失败。

「端口占用」这一条不算(server.listen 内部有回退到随机端口的逻辑,实测日志 Port 0 is in use, using port 39869 instead),所以触发面比 kernel:ready 窄 —— 但失败模式更难看:健康检查前的那一句「启动成功」是假的。

kernel:bootstrapped 上挂的是 objectql 的 announceOpenMigrationGates、service-automation 的 node-type 审计等 reconcile 类工作,吞掉的后果是审计静默失效,量级较轻。

建议

不是「把 triggerHook 整个改成传播」—— 每个钩子该 fail-soft 还是 fail-loud 是一次独立判断,#5170 已经把这个原则写进 kernel-base.ts 的两个分发器注释里,triggerHookOrThrow 也已就位可直接复用。这条单要裁的是:

  • A:kernel:listening 也改传播(与 ObjectKernel 一致),kernel:bootstrapped 同理;kernel:shutdown 保持吞(停机路径上一个 handler 失败不该阻断其余清理,这个 fail-soft 是有道理的)。
  • B:只改 kernel:listening(唯一有具体用户可见失败模式的那个),其余维持现状 + 文档写明。
  • C:全维持现状,文档承认差异。

倾向 A:两轴都指向它 —— 长远轴上「同一钩子名在两个内核上给出相反答案」本身就是 declared ≠ enforced(#5170 的立论,原样适用);防 AI 犯错轴上,一个插件作者在 kernel:listening 里写 await listen() 不会想到要自己包 try/catch,而宽容的消费端正是这类错误藏身之处。kernel:shutdown 的 fail-soft 应当保留并在注释里说明理由,让「declared = enforced」是逐钩子写明的判断,而不是默认继承。

落地时应与 #5170 一样,在 lite-kernel.test.ts 上留对称回归用例(#5170 已经 pin 了「其余钩子在 LiteKernel 上保持 fail-soft」,改哪个就改哪条 pin,让改动必须是显式的)。

相关:#5170(kernel:ready,已修)、#5160 / PR #5173(触发这轮排查的 boot 门)。


Filed by the os-dev agent while implementing #5170; 未认领,交 PM 分诊。

Activity

  1. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    分诊(PM 循环第 4 轮,会话 session_01Pbu27iNUfQCHeuS551Rqo7):挂 pm:blocked,Blocked-by: PR #5258(同文件 kernel-base.ts/lite-kernel.ts,且要改 #5258 刚立的「其余钩子 fail-soft」pin 测试 —— 必须基于其合并后的 main)。合入后转 pm:queue 即派。

    PM 裁定(否决窗口):取 A —— kernel:listening 与 kernel:bootstrapped 改传播(复用 #5258 落地的 triggerHookOrThrow),kernel:shutdown 保留 fail-soft 并在注释写明理由(停机路径一个 handler 失败不该阻断其余清理)。这是 #5170 刚被维护者背书的同一原则的直接延伸(同一钩子名两内核一个语义;「Bootstrap complete」打给一个没在监听的进程是最难看的假成功),restore-invariant 类,不再上升。落地要求:改哪条 pin 就显式改哪条;HonoServerPlugin 的 listen() 不需要自己包 catch(传播即正确);全套对称回归用例照 #5258 形状。


    Generated by Claude Code

  2. self-assigned this
    on Aug 4, 2026
  3. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 4 轮(阻塞解除:#5170→PR #5258 已合并,triggerHookOrThrow 与 fail-soft pin 测试已在 main)
    会话:session_01Pbu27iNUfQCHeuS551Rqo7
    分支:claude/issue-5257-listening-bootstrapped-propagate
    Worktree:objectstack-issue-5257
    域:domain:engine
    文件面:packages/core/src/lite-kernel.ts、kernel-base.ts(注释)、lite-kernel.test.ts / kernel.test.ts、changeset(越界即停,报告说明)

    方向按 13:12Z 分诊裁定:A —— kernel:listening 与 kernel:bootstrapped 改传播(复用 triggerHookOrThrow),kernel:shutdown 保留 fail-soft 并在注释写明理由;改哪条 pin 显式改哪条。在飞不相交已核:#5038(objectql/automation/triggers)与 core 不交;#5086/#5253 已交付。


    Generated by Claude Code

  4. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    验收:ACCEPT —— PR #5275(转 ready + auto-merge)

    会话:session_01Pbu27iNUfQCHeuS551Rqo7(PM 循环第 4 轮)

    裁定 A 严格按范围落地:kernel:bootstrapped/kernel:listening 换传播式分发(复用 #5258 的 triggerHookOrThrow),state='stopped'、原始错误不包装、不打印假成功行;kernel:shutdown 保留 fail-soft 且理由写在分发点(中断停机 = 把坏 handler 放大成资源泄漏);hono-plugin 零改动(传播即正确)。证据闸超预期:非测试订阅者全表逐个分析健康 boot 抛错性(objectql/sharing/pinyin/automation 全部自带防护,唯一会抛的正是要传播的 hono listen()),无测试依赖吞 —— 不触发 needs_decision。

    pin 翻面显式:#5258 的三合一 pin 拆成三条具名用例(bootstrapped 失败 boot / listening 失败 boot 且从未宣告成功 / shutdown 保持 fail-soft 并断言 destroy() 跑到),前两条有 ObjectKernel 对称双胞胎 —— 「同一钩子名两内核两语义」这个 bug 类只有成对具名用例按得住。

    实况核验:draft ✓ / Fixes 行 ✓ / 6 文件在面内(events.mdx 按新事实改写并诚实标注 #5274)✓ / core 426(+4 新用例)+ runtime 1313 + hono 164 + sharing 347 + automation 665 + 连接器 + 门禁 8 项全 PASS ✓ / tsc +4 为既有 TS7006 噪声类、逐一对应新用例并明说(#4311 台账)✓ / changeset minor、受众提示照 #5258 形状 ✓。

    重要新发现:#5274(ObjectKernel 的 shutdown 不是 fail-soft —— 抛错 handler 跳过全部 destroy() 并 process.exit(1),日志谎报「Shutdown timed out」,探针实测)—— 本单硬约束下正确地未越界修,PM 即刻分诊。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions