Repository navigation
Description
发现于 #5274(PR #5281)的实现阶段。观察类:今天没有用户会踩到(#5274 合入后四个生命周期钩子在两个 kernel 上语义已经一致),记录的是让这一致性得以维持的机制有多脆。
事实
packages/core/src/kernel.ts:56 是 export class ObjectKernel { —— 不继承任何基类。packages/core/src/kernel-base.ts:30 的 export abstract class ObjectKernelBase 只有一个子类:lite-kernel.ts:23 的 LiteKernel。
后果:钩子分发在仓库里存在两套独立实现,没有共享代码路径。
| ObjectKernel | LiteKernel | |
|---|---|---|
| 钩子存储 | kernel.ts 自己的私有 hooks map |
kernel-base.ts 的 hooks map |
| 隔离式分发 | kernel.ts 的私有 triggerShutdownHookIsolating()(#5274 新增) |
ObjectKernelBase.triggerHook() |
| 传播式分发 | context.trigger(裸 await 循环) |
ObjectKernelBase.triggerHookOrThrow() |
两侧的隔离式分发现在逐字打同一行日志(Hook handler failed: kernel:shutdown),但那是手工镜像的结果,不是共享实现。#5274 的实现里之所以没有直接复用 triggerHook,原因就是它够不着:protected 方法挂在一个 ObjectKernel 并不继承的基类上。
为什么值得记一笔
连续三个单都长在这条缝上,而且每次都是同一个形状 ——「同一个钩子名在两个 kernel 上含义相反」:
- core: kernel:ready 钩子抛错在 ObjectKernel 上失败 boot、在 LiteKernel 上被吞成一条 error 日志 —— 同一钩子两套失败语义 #5170 / PR fix(core)!: kernel:ready 抛错在两个内核上一律失败 boot(#5170) #5258 ——
kernel:ready:LiteKernel 吞掉抛错,ObjectKernel 让 boot 失败; - core: kernel:listening / kernel:bootstrapped / kernel:shutdown 在 LiteKernel 上仍被吞 —— HonoServerPlugin 的 listen() 失败会得到一句「✅ Bootstrap complete」而没有监听端口 #5257 / PR fix(core)!: kernel:bootstrapped / kernel:listening 抛错在 LiteKernel 上也失败 boot(#5257) #5275 ——
kernel:bootstrapped与kernel:listening:同上;最难看的一例是HonoServerPlugin在kernel:listening里await server.listen(port),LiteKernel 上 listen 失败被吞,进程打印「✅ Bootstrap complete」却没有在监听; - core: ObjectKernel 上一个抛错的 kernel:shutdown handler 会跳过所有插件 destroy() 并 process.exit(1),日志还谎报「Shutdown timed out」 #5274 / PR fix(core): 隔离 kernel:shutdown 分发,并让「超时」只报真超时 (#5274) #5281 ——
kernel:shutdown:方向相反,ObjectKernel 的传播式分发让一个坏 handler 跳过全部destroy()并process.exit(1)。
目前把两侧钉在一起的,只有成对的手写测试(kernel.test.ts 与 lite-kernel.test.ts 各一份)。这是约定,不是机制:明天新增第五个生命周期钩子,不会自动获得任何配对,也没有门会因为漏配而失败。
一处仍然存在的休眠不一致(#5274 之后)
在 ObjectKernel 上,kernel:shutdown 现在有两条语义不同的分发路径:
- 内核自己在
performShutdown()里的分发 —— 隔离式(失败记日志,其余继续); - 插件手工调用
ctx.trigger('kernel:shutdown')—— 仍是context.trigger的裸 await 循环,传播式。
LiteKernel 那边 context.trigger(kernel-base.ts:123)同样是裸循环,所以两个 kernel 在这一点上形状相同 —— 但都与各自内核自身的停机分发不一致。今天仓库里没有任何地方手工 trigger kernel:shutdown,所以这是休眠的,不是在冒烟的火。
不预判方向
至少三种走法,代价差很多,需要一次显式裁定而不是顺手改:
- A 让
ObjectKernel继承ObjectKernelBase—— 最彻底,但ObjectKernel是 747 行、自带plugins/services/hooks/state/logger全套字段的生产内核,重构面和回归风险都不小; - B 把两个分发器抽成模块级自由函数(接收 handlers 数组 + logger),两边各自调用 —— 只统一分发这一件事,改动面小,但不解决「两套 hooks 存储」;
- C 什么都不做,把「成对测试」升格为一道门(例如检查每个
kernel:*钩子在两个测试文件里都有具名 pin)—— 不动运行时,只让漏配可见。
倾向 B 或 C,但这属于架构裁定,留给 PM 分诊。
Activity
os-zhuang commentedon Aug 5, 2026
发现分诊轮:转
needs-user-decision(摘finding,domain:engine沿用)。理由:正文明说「属架构裁定,需要一次显式裁定而不是顺手改」,且证据面已完整 —— 同一结构缝连续吃掉三个单(#5170/#5257/#5274),维持一致性的机制只剩手写成对测试。待裁决三选一:A ObjectKernel 继承 ObjectKernelBase(最彻底,747 行生产内核重构风险大)/ B 两个分发器抽成模块级自由函数(只统一分发,改动面小,不解决双 hooks 存储)/ C 把成对测试升格为门(不动运行时,漏配可见)。发单人倾向 B 或 C;从「第五个生命周期钩子不会自动获得配对」的复发模式看,B+C 组合(小重构 + 门)覆盖最全。裁决后转
pm:queue。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
claude commentedon Aug 6, 2026
处置(2026-08-06):退回派发队列。
按升级门槛复核:纯内部架构判断(ObjectKernel/LiteKernel 均为私有实现),无产品语义/公开契约分歧,无破坏性动作——不占决策箱。
裁定方向(分诊建议采纳):B+C 组合——两个 kernel 的钩子分发抽成模块级共享函数(改一处两边生效);成对测试升格为门(每个
kernel:*钩子在两个测试文件都要有具名 pin,漏配即红)。不做 A(747 行生产内核继承重构,收益不成比例)。量级 M。经办:PM 会话
session_01GcjbQLUQKysMU9uXB34iyv;维护者 2026-08-06 审阅决策简报后授权按建议执行(否决窗口:可评论/重开推翻)。Generated by Claude Code
os-zhuang commentedon Aug 6, 2026
【裁决落地】维护者 2026-08-06 批复全舰队决策箱评估报告(批复「同意」),本单裁定:
裁 B + C——钩子分发器抽为模块级自由函数(消掉手写镜像的两份实现)+ 成对测试升格为门(新增钩子不配对即红)。⛔ 不做 A(747 行生产内核继承重构,风险与收益不成比例)。
依据:#5170/#5257/#5274 连续三单同一结构缝,B 消根因、C 兜 B 覆盖不到的双 hooks 存储残留——组合覆盖最全(分诊 08-05 同判)。
流转:摘
needs-user-decision→pm:queue,归 engine-core 车道。评估与落地会话:
session_01N3uGFF8teXbpgtbEJ1aYXuGenerated by Claude Code
os-zhuang commentedon Aug 8, 2026
Claim: PM loop round 1 extension (engine-core seat #6019, maintainer asked to accelerate)
Session:
session_01MwoubC3jL271FYt9rGXwxbBranch:
claude/issue-5282-kernel-hook-dispatch-unifyWorktree:
objectstack-issue-5282Domain:
domain:engine-coreFile surface:
packages/core/src/kernel.ts,kernel-base.ts,lite-kernel.ts, one NEW internal dispatcher module (not exported from the core barrel),kernel.test.ts/lite-kernel.test.ts, plus the ruling's gate half: a newscripts/check-kernel-hook-pairs.mjswired into.github/workflows/lint.yml(gate file surface sanctioned by the fleet-approved B+C ruling on this card; flagged to devx by this comment)..changeset/. ⛔ NOTpackages/core/src/utils/**(#5586 in flight), ⛔ NOTpackages/core/src/index.ts(barrel untouched).Serial constraints cleared: kernel files' last functional changes are #5258/#5275/#5281 (all MERGED, the three predecessors this card cites); no open PR touches
packages/core. Same-package overlap with in-flight #5586 is file-disjoint by pinned non-touch lists on both dispatches (deviation noted there too).Ruling being executed (2026-08-06, maintainer-approved): B+C — shared module-level dispatch functions + paired-pin gate; ⛔ option A (inheritance refactor) rejected. Per-path dispatch semantics preserved exactly (isolating stays isolating, propagating stays propagating; the dormant ctx.trigger inconsistency stays dormant and gets pinned, not flipped).
Container judgment: size M ⇒
mode:subagentshared container.Generated by Claude Code
os-zhuang commentedon Aug 8, 2026
ACCEPT (engine-core seat #6019, session
session_01MwoubC3jL271FYt9rGXwxb). PR #6672 reviewed against the diff and fresh CI, not the report: the B+C ruling is implemented exactly — one implementation per dispatch flavour in a new internal module (core barrel untouched,utils/**untouched, hooks maps deliberately still separate, no inheritance refactor), and the paired-pin gatecheck:kernel-hook-pairswired into the ESLint job, measured against main's corpus before being pinned (4 hooks, zero false positives).Highlights that made this a clean accept: the premise table re-anchored every 08-04 line number; the PM's mechanism assumption was falsified in the small (the propagating flavour has two sub-shapes differing by the
Triggering hook:trace) and the faithful reproduction — an optional logger whose absence mirrorscontext.trigger— is pinned by a test rather than left as folklore; the dormantkernel:shutdowntwo-path asymmetry is pinned, not flipped; four reverse verifications ran with directions written first and all matched, including the deliberate demonstration that the gate guards pairing while the tests guard sharing (neither substitutes for the other); TEST_DEBT held at exactly its ledgered 98 with the four draft-introduced errors fixed at source.CI: 25/25 concluded, ESLint (carrying the gate family incl. the new gate) and TypeScript Type Check both
success; changeset present (@objectstack/corepatch, no behaviour change documented in both directions). Now: ready + auto-merge via the queue; issue closes on merge.Generated by Claude Code