Skip to content

core(finding): ObjectKernel 不继承 ObjectKernelBase —— 钩子分发语义在两处各写一遍,#5170/#5257/#5274 都是同一条结构缝 #5282

Closed

Description

@os-zhuang

发现于 #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 上含义相反」:

目前把两侧钉在一起的,只有成对的手写测试(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 分诊。

相关:#5170 / PR #5258、#5257 / PR #5275、#5274 / PR #5281。

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    发现分诊轮:转 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

  2. claude commented on Aug 6, 2026

    @claude
    Contributor

    处置(2026-08-06):退回派发队列。

    按升级门槛复核:纯内部架构判断(ObjectKernel/LiteKernel 均为私有实现),无产品语义/公开契约分歧,无破坏性动作——不占决策箱。

    裁定方向(分诊建议采纳):B+C 组合——两个 kernel 的钩子分发抽成模块级共享函数(改一处两边生效);成对测试升格为门(每个 kernel:* 钩子在两个测试文件都要有具名 pin,漏配即红)。不做 A(747 行生产内核继承重构,收益不成比例)。量级 M。

    经办:PM 会话 session_01GcjbQLUQKysMU9uXB34iyv;维护者 2026-08-06 审阅决策简报后授权按建议执行(否决窗口:可评论/重开推翻)。


    Generated by Claude Code

  3. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    【裁决落地】维护者 2026-08-06 批复全舰队决策箱评估报告(批复「同意」),本单裁定:

    裁 B + C——钩子分发器抽为模块级自由函数(消掉手写镜像的两份实现)+ 成对测试升格为门(新增钩子不配对即红)。⛔ 不做 A(747 行生产内核继承重构,风险与收益不成比例)。

    依据:#5170/#5257/#5274 连续三单同一结构缝,B 消根因、C 兜 B 覆盖不到的双 hooks 存储残留——组合覆盖最全(分诊 08-05 同判)。

    流转:摘 needs-user-decision → pm:queue,归 engine-core 车道。

    评估与落地会话:session_01N3uGFF8teXbpgtbEJ1aYXu


    Generated by Claude Code

  4. self-assigned this
    on Aug 8, 2026
  5. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 1 extension (engine-core seat #6019, maintainer asked to accelerate)
    Session: session_01MwoubC3jL271FYt9rGXwxb
    Branch: claude/issue-5282-kernel-hook-dispatch-unify
    Worktree: objectstack-issue-5282
    Domain: domain:engine-core
    File 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 new scripts/check-kernel-hook-pairs.mjs wired 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/. ⛔ NOT packages/core/src/utils/** (#5586 in flight), ⛔ NOT packages/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:subagent shared container.


    Generated by Claude Code

  6. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    ContributorAuthor

    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 gate check:kernel-hook-pairs wired 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 mirrors context.trigger — is pinned by a test rather than left as folklore; the dormant kernel:shutdown two-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/core patch, no behaviour change documented in both directions). Now: ready + auto-merge via the queue; issue closes on merge.


    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