@zhushanwen/subagent-core 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/{chunk-VURUAGMM.js → chunk-OL5BK4VN.js} +237 -40
  2. package/dist/{engine-discovery-scan-ocpM8FmI.d.cts → engine-discovery-scan-BXLA8y-z.d.cts} +17 -1
  3. package/dist/{engine-discovery-scan-ocpM8FmI.d.ts → engine-discovery-scan-BXLA8y-z.d.ts} +17 -1
  4. package/dist/execution/engine/engine-discovery-scan.cjs +150 -40
  5. package/dist/execution/engine/engine-discovery-scan.d.cts +1 -1
  6. package/dist/execution/engine/engine-discovery-scan.d.ts +1 -1
  7. package/dist/execution/engine/engine-discovery-scan.js +1 -1
  8. package/dist/index.cjs +278 -225
  9. package/dist/index.d.cts +24 -4
  10. package/dist/index.d.ts +24 -4
  11. package/dist/index.js +188 -304
  12. package/dist.bundle/index.cjs +279 -226
  13. package/package.json +5 -5
  14. package/src/__tests__/record-store-last-line.test.ts +12 -0
  15. package/src/core/logger.ts +1 -1
  16. package/src/execution/__tests__/inflight-production-wiring.test.ts +223 -0
  17. package/src/execution/__tests__/nested-visibility.test.ts +13 -1
  18. package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +55 -53
  19. package/src/execution/engine/__tests__/inflight-snapshot.test.ts +180 -0
  20. package/src/execution/engine/client/engine-client.ts +30 -1
  21. package/src/execution/engine/host/host-bridge.ts +7 -0
  22. package/src/execution/engine/host/spawned-children.ts +1 -1
  23. package/src/execution/engine/inflight-snapshot.ts +92 -0
  24. package/src/execution/engine/port.ts +18 -0
  25. package/src/execution/path-encoding.ts +6 -0
  26. package/src/execution/subagent-service.ts +25 -0
  27. package/src/execution/worktree-registry.ts +11 -7
  28. package/src/index.ts +11 -1
@@ -0,0 +1,180 @@
1
+ // inflight-snapshot.test.ts —— core→壳在途事件出口 + EnginePort.inFlightSnapshot? 缺省语义
2
+ //(u7a,设计权威源:docs/design/crash-forensics-and-watchdog.md §3.3 D5)。
3
+ //
4
+ // 三视角:
5
+ // ①使用者(壳层 reporter 视角)——setInFlightListener 注册后,core 状态迁移点
6
+ // (子进程注册/移除、idle timer arm)推来最新绝对计数快照;getInFlightSnapshot
7
+ // 随时可拉(初始上报数据源)。
8
+ // ②构建者——计数 = 双谓词过滤(hasLiveProcessHandle && !hasIdleTimer,与
9
+ // notify-host hasRunningBackground 同源):活句柄且无 armed idle timer 才算在途,
10
+ // Path A 保活(timer armed)不算——D5 防推迟恒真的核心裁决。
11
+ // ③观察者——EnginePort 可选成员向后兼容:pi 引擎不实现(undefined = 无在途),
12
+ // zcode 引擎实现(快照面在场);出口监听者抛错不反噬 core。
13
+
14
+ import { describe, expect, it, beforeEach, afterEach } from "vitest";
15
+ import { EventEmitter } from "node:events";
16
+ import type { ChildProcess } from "node:child_process";
17
+
18
+ import {
19
+ armIdleTimer,
20
+ disarmIdleTimer,
21
+ _resetLifecycleState,
22
+ } from "../../lifecycle-manager.ts";
23
+ import {
24
+ getInFlightSnapshot,
25
+ notifyInFlightChanged,
26
+ setInFlightListener,
27
+ type InFlightSnapshot,
28
+ } from "../inflight-snapshot.ts";
29
+ import type { EnginePort } from "../port.ts";
30
+ // W3 合并改写:inproc 引擎已删——句柄记账走 host/spawned-children 镜像(register +
31
+ // reset 测试面);「引擎无 inFlightSnapshot 可选成员」断言改桩(EnginePort 缺席形态)。
32
+ import {
33
+ killRecordChildWithEscalation,
34
+ registerSpawnedChildForRecord,
35
+ _resetCoreSpawnedChildrenMirrorForTest,
36
+ } from "../host/spawned-children.ts";
37
+ import { createHostBridge } from "../host/host-bridge.ts";
38
+
39
+ /** 最小 fake 子进程(EventEmitter + killed/pid 字段——hasLiveProcessHandle 消费面)。 */
40
+ function makeFakeChild(): ChildProcess & EventEmitter {
41
+ const child = new EventEmitter() as ChildProcess & EventEmitter;
42
+ (child as { pid?: number }).pid = 4242;
43
+ (child as { killed: boolean }).killed = false;
44
+ return child;
45
+ }
46
+
47
+ beforeEach(() => {
48
+ _resetLifecycleState();
49
+ setInFlightListener(null);
50
+ });
51
+
52
+ afterEach(() => {
53
+ // 清理本测试注册的句柄镜像(W3 后记账在 host/spawned-children,reset 即清;close 事件
54
+ // 摘除路径已由「close 摘除自动触发通知」用例覆盖),防跨用例泄漏。
55
+ _resetCoreSpawnedChildrenMirrorForTest();
56
+ _resetLifecycleState();
57
+ setInFlightListener(null);
58
+ });
59
+
60
+ describe("getInFlightSnapshot:双谓词绝对计数(D5 求值位置 = 状态所在进程)", () => {
61
+ it("空态 → 0(无任何 subagent 的 session 初始上报数据源)", () => {
62
+ expect(getInFlightSnapshot()).toEqual({ inFlight: 0 });
63
+ });
64
+
65
+ it("注册活句柄 → 1;arm idle timer(Path A 保活)→ 0;disarm → 1", () => {
66
+ const id = "sa-inflight-arm";
67
+ registerSpawnedChildForRecord(id, makeFakeChild());
68
+ expect(getInFlightSnapshot().inFlight).toBe(1);
69
+
70
+ armIdleTimer(id, () => undefined, 60_000);
71
+ expect(getInFlightSnapshot().inFlight).toBe(0);
72
+
73
+ disarmIdleTimer(id);
74
+ expect(getInFlightSnapshot().inFlight).toBe(1);
75
+ });
76
+
77
+ it("killed 句柄不算活(hasLiveProcessHandle 的 !killed 子句)", () => {
78
+ const id = "sa-inflight-killed";
79
+ const child = makeFakeChild();
80
+ registerSpawnedChildForRecord(id, child);
81
+ // W3 镜像形态:killed 判据由镜像置死位承载(注册后改 child 引用字段不进镜像——
82
+ // 生产置死路径 = killRecordChildWithEscalation 杀链记账面 / 引擎反向通道)。
83
+ killRecordChildWithEscalation(id, "test");
84
+ expect(getInFlightSnapshot().inFlight).toBe(0);
85
+ });
86
+
87
+ it("多 record 混合形态:在途/保活/垂死并存时按谓词逐一过滤", () => {
88
+ const running = makeFakeChild();
89
+ const keepAlive = makeFakeChild();
90
+ const dying = makeFakeChild();
91
+ (dying as { killed: boolean }).killed = true;
92
+ registerSpawnedChildForRecord("sa-inflight-run", running);
93
+ registerSpawnedChildForRecord("sa-inflight-keep", keepAlive);
94
+ registerSpawnedChildForRecord("sa-inflight-dying", dying);
95
+ armIdleTimer("sa-inflight-keep", () => undefined, 60_000);
96
+ expect(getInFlightSnapshot().inFlight).toBe(1);
97
+ });
98
+ });
99
+
100
+ describe("notifyInFlightChanged:迁移点 → 壳层监听者(同步 fire-and-forget)", () => {
101
+ it("监听者收到最新快照(迁移时刻求值,非注册时刻)", () => {
102
+ const seen: InFlightSnapshot[] = [];
103
+ setInFlightListener((s) => seen.push({ ...s }));
104
+
105
+ notifyInFlightChanged();
106
+ expect(seen).toEqual([{ inFlight: 0 }]);
107
+
108
+ registerSpawnedChildForRecord("sa-inflight-notify", makeFakeChild());
109
+ notifyInFlightChanged();
110
+ expect(seen[seen.length - 1]).toEqual({ inFlight: 1 });
111
+ });
112
+
113
+ it("监听者抛错不反噬 core(壳层故障不打断生命周期主链)", () => {
114
+ setInFlightListener(() => {
115
+ throw new Error("shell reporter bug");
116
+ });
117
+ expect(() => notifyInFlightChanged()).not.toThrow();
118
+ });
119
+
120
+ it("无监听者时 no-op;注册覆盖语义(后注册者收到,先注册者不再收)", () => {
121
+ expect(() => notifyInFlightChanged()).not.toThrow();
122
+
123
+ const first: InFlightSnapshot[] = [];
124
+ const second: InFlightSnapshot[] = [];
125
+ setInFlightListener((s) => first.push(s));
126
+ setInFlightListener((s) => second.push(s));
127
+ notifyInFlightChanged();
128
+ expect(first).toEqual([]);
129
+ expect(second).toEqual([{ inFlight: 0 }]);
130
+ });
131
+
132
+ it("host-bridge armIdleTimer / disarmIdleTimer 委托点自动触发通知(迁移点接线证据)", () => {
133
+ // W3 镜像形态:register 本身不再通知(子进程终止经引擎反向通道),core 侧唯一挂
134
+ // notifyInFlightChanged 的委托面 = host-bridge 的 arm/disarm(u7a 迁移点新宿主)。
135
+ const seen: InFlightSnapshot[] = [];
136
+ setInFlightListener((s) => seen.push(s));
137
+ registerSpawnedChildForRecord("sa-inflight-auto", makeFakeChild());
138
+
139
+ const bridge = createHostBridge({
140
+ service: {} as Parameters<typeof createHostBridge>[0]["service"],
141
+ onIdleTimeout: () => undefined,
142
+ });
143
+ bridge.armIdleTimer("sa-inflight-auto", 60_000);
144
+ expect(seen.at(-1)).toEqual({ inFlight: 0 });
145
+
146
+ bridge.disarmIdleTimer("sa-inflight-auto");
147
+ expect(seen.at(-1)).toEqual({ inFlight: 1 });
148
+ });
149
+ });
150
+
151
+ describe("EnginePort.inFlightSnapshot? 可选成员缺省语义(D5:pi 不实现,zcode 实现)", () => {
152
+ it("未实现该成员的引擎桩 → undefined(= 无引擎侧在途面,pi 形态由 extension 聚合上报覆盖)", () => {
153
+ // 经 EnginePort 接口面访问(可选成员缺席的正当形态);裸桩无此声明(W3 后 inproc
154
+ // pi 引擎已删,缺席形态以桩承载——zcode 真实现的断言移 zcode-subagent-cli 测试),
155
+ // 消费方必须走 port 类型——本身就是「缺席正当」契约的一部分。
156
+ const pi: EnginePort = { capabilities: () => ({ id: "pi-stub" }) } as unknown as EnginePort;
157
+ expect(pi.inFlightSnapshot).toBeUndefined();
158
+ });
159
+
160
+ it("实现该成员的引擎桩 → 同步快照;未初始化恒 0(空闲常驻≠在途——真实现断言在 zcode-subagent-cli 的 zcode-engine-inflight.test)", () => {
161
+ const engine: EnginePort = {
162
+ capabilities: () => ({ id: "zcode-stub" }),
163
+ inFlightSnapshot: () => ({ inFlight: 0 }),
164
+ } as unknown as EnginePort;
165
+ expect(typeof engine.inFlightSnapshot).toBe("function");
166
+ expect(engine.inFlightSnapshot?.()).toEqual({ inFlight: 0 });
167
+ });
168
+
169
+ it("未实现该成员的引擎对象仍满足 EnginePort(向后兼容,可选成员不强制)", () => {
170
+ // 结构化证据:EnginePort 的既有可选成员扩展先例(listModels/validateModel/dispose)
171
+ // 同款——缺席成员不破坏实现关系(裸桩承载缺席形态)。W3 后 inproc pi 引擎已删,
172
+ // id 不再由引擎身份推导断言(桩无 id 字面量),能力面 id 经 capabilities() 取。
173
+ const pi: EnginePort = { capabilities: () => ({ id: "pi-stub" }) } as unknown as EnginePort;
174
+ // EngineCapabilities 契约面无 id 成员(contract-types.ts)——运行期桩携带的 id 经
175
+ // 窄化读取断言(HEAD 既有类型红此处修复:pi.capabilities().id 直取 TS2339)。
176
+ expect((pi.capabilities() as unknown as { id: string }).id).toBe("pi-stub");
177
+ expect(typeof pi.capabilities).toBe("function");
178
+ expect(pi.inFlightSnapshot).toBeUndefined();
179
+ });
180
+ });
@@ -43,7 +43,7 @@ import {
43
43
  type RequestFrame,
44
44
  } from "@zhushanwen/subagent-engine-sdk";
45
45
 
46
- import { SpawnedChildrenMirror } from "./mirror.ts";
46
+ import { SpawnedChildrenMirror, type MirrorChangeEvent } from "./mirror.ts";
47
47
  import {
48
48
  removePidfile,
49
49
  registerEnginePidfile,
@@ -56,9 +56,35 @@ import {
56
56
  type EngineClientOptions,
57
57
  } from "./client-options.ts";
58
58
  import { killProcessTree, waitForChildExit } from "./reaper.ts";
59
+ // [u7a 生产补挂] 反向通道镜像 → core 侧镜像桥接:in-flight 计数与生命周期谓词读 core
60
+ // 全局镜像(recordId 键),而引擎侧子进程上报只落 EngineClient 镜像——本桥接实现
61
+ // spawned-children.ts 头注「数据源①:反向通道上报落本镜像」的既登记契约(W3 协议化
62
+ // 后 ctx.onChildSpawned 不被调用,该数据面悬空致计数恒 0),见 crash-forensics §7 v7。
63
+ import { coreSpawnedChildrenMirror } from "../host/spawned-children.ts";
64
+ import { notifyInFlightChanged } from "../inflight-snapshot.ts";
59
65
 
60
66
  const logger = getLogger("subagents");
61
67
 
68
+ /**
69
+ * 单条镜像事件 → core 镜像投影 + 在途推送(u7a 数据面桥接):置死/退出 → markKilled,
70
+ * running → register(同 recordId 重 spawn 覆盖,与引擎侧 Map 同语义)。投影后必推送
71
+ * ——镜像事件本身就是在途迁移点(子进程注册/移除),首轮无任何 arm/disarm 推送,
72
+ * 不在此推则首轮在途窗口对 D5 不可见;notify 同步 fire-and-forget,异常不外溢。
73
+ * killedAll 空表单发(无 pid)与迟到事件(条目已清)为 no-op。
74
+ */
75
+ function bridgeMirrorEventToCoreMirror(event: MirrorChangeEvent, mirror: SpawnedChildrenMirror): void {
76
+ if (event.recordId === undefined || event.pid === undefined) return;
77
+ const entry = mirror.getEntry(event.pid);
78
+ if (entry === undefined) return;
79
+ const core = coreSpawnedChildrenMirror();
80
+ if (entry.killed) {
81
+ core.markKilled(event.recordId);
82
+ } else {
83
+ core.register(event.recordId, { pid: entry.pid, killed: false });
84
+ }
85
+ notifyInFlightChanged();
86
+ }
87
+
62
88
  /** dispose 帧等待上界(设计 §3.6:dispose 上界 3s,超时即杀)。 */
63
89
  const DISPOSE_GRACE_MS = 3_000;
64
90
  /** SIGKILL 后收尸等待上限(kill-chain 同型有界兜底,reaper.waitForChildExit 消费)。 */
@@ -171,6 +197,9 @@ export class EngineClient {
171
197
  };
172
198
  this.mirror.onChange((event) => {
173
199
  opts.onMirrorChanged?.(event);
200
+ // [u7a 生产补挂] 反向通道镜像事件同步投影进 core 侧镜像 + 推送在途计数
201
+ //(数据面桥接,见 bridgeMirrorEventToCoreMirror 与文件头 u7a 注释)。
202
+ bridgeMirrorEventToCoreMirror(event, this.mirror);
174
203
  });
175
204
  }
176
205
 
@@ -16,6 +16,11 @@
16
16
  // chat-session.cancel 等价承接,偏差登记见 chat-domain impl-plan §5。
17
17
 
18
18
  import { armIdleTimer, disarmIdleTimer } from "../../lifecycle-manager.ts";
19
+ // u7a D5 在途记账迁移点(crash-forensics §3.3 D5):idle timer arm/disarm 是「保活 ↔
20
+ // 正在执行」翻转的全部迁移点(agent_settled arm / 新 turn disarm / 热路径 re-arm 均经
21
+ // 本委托),迁移后推最新绝对计数给壳层监听者(同步 fire-and-forget,不进投递 await 面;
22
+ // 环规避同 inflight-snapshot.ts 头注释——挂桥层而非 lifecycle-manager)。
23
+ import { notifyInFlightChanged } from "../inflight-snapshot.ts";
19
24
  import type { AgentResult as WorkflowAgentResult } from "../../../orchestration/models/types.ts";
20
25
  import type { StatusFilter } from "../../record-store.ts";
21
26
  import type { SubagentStream } from "../../stream-sink.ts";
@@ -142,9 +147,11 @@ export function createHostBridge(deps: HostBridgeDeps): HostBridge {
142
147
  reportRecordTransition: (record) => service.reportRecordTransition?.(record),
143
148
  armIdleTimer: (id, ms) => {
144
149
  armIdleTimer(id, () => deps.onIdleTimeout(id), ms);
150
+ notifyInFlightChanged();
145
151
  },
146
152
  disarmIdleTimer: (id) => {
147
153
  disarmIdleTimer(id);
154
+ notifyInFlightChanged();
148
155
  },
149
156
  };
150
157
  }
@@ -71,7 +71,7 @@ class CoreSpawnedChildrenMirror {
71
71
  }
72
72
 
73
73
  /** 快照(诊断/测试)。 */
74
- snapshot(): SpawnedChildMirrorEntry[] {
74
+ snapshot(): Array<SpawnedChildMirrorEntry & { recordId: string }> {
75
75
  return [...this.entries.entries()].map(([recordId, entry]) => ({ recordId, ...entry }));
76
76
  }
77
77
 
@@ -0,0 +1,92 @@
1
+ // src/execution/engine/inflight-snapshot.ts
2
+ //
3
+ // core→壳在途事件出口(u7a,设计权威源:docs/design/crash-forensics-and-watchdog.md
4
+ // §3.3 D5「在途判定谓词 + 求值位置」)。
5
+ //
6
+ // 在途状态(spawnedChildren Map / idleTimers Map)真实存在于本包(随 subagent-workflow
7
+ // extension 加载进 pi 进程),runtime 无法直查——D5 裁决为 extension 聚合上报:壳层
8
+ // (extensions/universal/subagent-workflow 的 host/inflight-reporter)经 select 通道
9
+ // 把绝对计数推给 runtime。本模块是这条通道的 core 侧出口:
10
+ // - 求值位置 = 状态所在的 pi 进程(本模块 getInFlightSnapshot——谓词与
11
+ // notify-host.ts hasRunningBackground 的双谓词过滤同源:hasLiveProcessHandle &&
12
+ // !hasIdleTimer,只把「正在执行」计为在途,Path A 保活(idle timer armed)不计);
13
+ // - 事件产生点 = 引擎域内的状态迁移点(session-runner 子进程注册/移除、idle timer
14
+ // arm/disarm),迁移后调 notifyInFlightChanged() 把最新快照推给壳层监听者;
15
+ // - core 闭包红线:本模块零 pi SDK import(只 import 本包 lifecycle 记账面),
16
+ // 壳层经包根 barrel 消费 setInFlightListener / getInFlightSnapshot(exports 面
17
+ // 收窄后壳侧生产消费必须走 barrel,无深路径豁免)。
18
+ //
19
+ // 回调注入形态(RunContext 回调先例):监听者由壳层注册,core 同步调用、绝不 await
20
+ // 返回值——满足 D5 接线约束①「上报不阻塞生命周期主链、不 await 进 agent_settled
21
+ // handler 链」(该链有时序保护约束 armIdleTimer 先于 notify)。
22
+
23
+ import { hasIdleTimer } from "../lifecycle-manager.ts";
24
+ import { hasLiveProcessHandle } from "../lifecycle-predicates.ts";
25
+ // 环声明:session-runner(迁移点)import 本模块的 notifyInFlightChanged,本模块经
26
+ // lifecycle-predicates → session-runner.getChildByRecord 读句柄记账 + 直接取
27
+ // spawnedChildren 键集——双向仅函数体内取值(ESM 活绑定,调用期解析),模块求值序
28
+ // 无依赖,环安全;esbuild bundle(builtin 打包)同规则成立。
29
+ // W3 合并改写:inproc 引擎已删,句柄记账唯一源 = engine/host/spawned-children 镜像
30
+ //(coreSpawnedChildrenMirror().snapshot() 返回 [{recordId, ...}],键集等价旧 Map.keys())。
31
+ import { coreSpawnedChildrenMirror } from "./host/spawned-children.ts";
32
+
33
+ /** 在途快照(与 EnginePort.inFlightSnapshot? 返回形状一致——runtime 侧统一消费形状)。 */
34
+ export interface InFlightSnapshot {
35
+ /** 当前非 idle 句柄数(绝对计数,非增量)。 */
36
+ inFlight: number;
37
+ }
38
+
39
+ /** 壳层注册的监听回调(同步、fire-and-forget;core 不 await 不重试)。 */
40
+ export type InFlightListener = (snapshot: InFlightSnapshot) => void;
41
+
42
+ /**
43
+ * 进程级单监听者(非 per-session 状态——在途计数本身是 pi 进程级模块状态
44
+ * (spawnedChildren/idleTimers 均模块级 Map),出口与之同域;壳层 reporter 持有
45
+ * 当前 session 上下文负责归属,与本出口无关)。后注册覆盖先注册(jiti 模块重载
46
+ * /多 factory 实例场景幂等),null 注销。
47
+ */
48
+ let listener: InFlightListener | null = null;
49
+
50
+ /** 壳层注册监听(extension factory 装配点调用;传 null 注销)。 */
51
+ export function setInFlightListener(next: InFlightListener | null): void {
52
+ listener = next;
53
+ }
54
+
55
+ /**
56
+ * 当前在途快照(纯读,无副作用):遍历活句柄记账(spawnedChildren 的 recordId 键集),
57
+ * 按双谓词过滤——活句柄(child 存在且未 killed)且无 armed idle timer。
58
+ *
59
+ * SSOT 复用:谓词取 lifecycle-predicates/lifecycle-manager 既有导出,与 notify-host
60
+ * hasRunningBackground 完全同源,不复制判定逻辑。活句柄记账里天然覆盖 pi 引擎进程内
61
+ * 与 relay 形态 child 两种宿主(relay spawn 经同一 runSpawn/spawnedChildren 记账——
62
+ * D5「extension 是两形态派生的共同属主」)。
63
+ */
64
+ export function getInFlightSnapshot(): InFlightSnapshot {
65
+ return { inFlight: countInFlight() };
66
+ }
67
+
68
+ /**
69
+ * 状态迁移点调用:把最新快照同步推给监听者。约束(D5 接线①):
70
+ * - 同步 void 语义——调用方(含 agent_settled handler 链内的 armIdleTimer 迁移点)
71
+ * 不 await 本函数的任何下游(监听者内部自行 fire-and-forget);
72
+ * - 监听者异常不反噬 core(壳层 bug 不得打断生命周期主链),catch 后照常返回。
73
+ */
74
+ export function notifyInFlightChanged(): void {
75
+ if (listener === null) return;
76
+ try {
77
+ listener(getInFlightSnapshot());
78
+ // eslint-disable-next-line taste/no-silent-catch -- 上报出口故障刻意静默(D5 约束①):壳层 bug 不得打断生命周期主链;绝对计数语义下后续事件自愈,丢失单帧无累积误差,记日志徒增 core logger 噪音面
79
+ } catch {
80
+ // 同上:静默是接受的。
81
+ }
82
+ }
83
+
84
+ /** 双谓词过滤的绝对计数(getInFlightSnapshot 与 notifyInFlightChanged 共用)。 */
85
+ function countInFlight(): number {
86
+ let count = 0;
87
+ for (const entry of coreSpawnedChildrenMirror().snapshot()) {
88
+ const recordId = entry.recordId;
89
+ if (hasLiveProcessHandle(recordId) && !hasIdleTimer(recordId)) count++;
90
+ }
91
+ return count;
92
+ }
@@ -12,6 +12,9 @@
12
12
  // 与 onPoolResolved 分立两个时点)。
13
13
  // - [u-h2 已实施 2026-09-05] EnginePort.validateModel?()——派发同步期 model 校验面。
14
14
  // 权威源:docs/design/timeout-audit-hygiene-batch.md §3.2 D2-2。
15
+ // - [u7a 已实施 2026-09-10] EnginePort.inFlightSnapshot?()——引擎在途只读快照面
16
+ // (滚动重启推迟谓词输入)。权威源:
17
+ // docs/design/crash-forensics-and-watchdog.md §3.3 D5。
15
18
  //
16
19
  // 四个能力面(D1):
17
20
  // run —— 主语义:一次性 fire-to-completion 任务执行;
@@ -254,4 +257,19 @@ export interface EnginePort {
254
257
  * Promise 前完成;grace→SIGKILL 升级序列属异步面(promise 段)。
255
258
  */
256
259
  dispose?(): Promise<void>;
260
+
261
+ /**
262
+ * [u7a D5] 可选面:引擎在途任务只读快照(滚动重启推迟谓词的引擎侧输入——
263
+ * 权威源:docs/design/crash-forensics-and-watchdog.md §3.3 D5「推迟判定源 =
264
+ * relay ∪ 引擎池在途 ∪ pi 侧 extension 聚合上报」)。同步纯读、无副作用。
265
+ *
266
+ * 返回 null = 引擎不提供快照;成员缺席(undefined,pi 引擎不实现)= 无引擎侧
267
+ * 在途面——pi 形态的在途由 subagent-workflow extension 聚合上报覆盖(EnginePort
268
+ * 之外的第 4 通道,两通道互不替代)。可选成员保持向后兼容(port.ts 既有扩展
269
+ * 先例:listModels / validateModel / dispose 全为可选成员)。
270
+ *
271
+ * zcode 实现语义(显式裁决):在途 = activeSessions 非空——poolKey 'shared' 的
272
+ * app-server 空闲常驻进程恒活,**禁止按进程存在判定**(会恒真、推迟常态化)。
273
+ */
274
+ inFlightSnapshot?(): { inFlight: number } | null;
257
275
  }
@@ -19,6 +19,12 @@ export function encodeCwd(cwd: string): string {
19
19
  /**
20
20
  * 获取 subagent session 持久化目录路径。
21
21
  *
22
+ * C-ext-21(跨包隐式契约):本函数产出的 `subagents/<enc>/sessions` 路径形态被
23
+ * extensions/universal/rename-session/src/llm.ts 的 isSubagentSession 嗅探
24
+ * (路径含 `<sep>subagents<sep>` 段 → 判为 subagent 会话并跳过重命名)——改本布局
25
+ * (移动/改名 subagents 段)必须同步该嗅探逻辑,反之亦然;约束登记见
26
+ * docs/constraints.json C-ext-21,设计依据 rename-session-three-modes.md §3.3 D7。
27
+ *
22
28
  * D-004: 用主 cwd 编码——保证同一主 cwd 下所有 subagent 的 session 文件
23
29
  * 存放在同一目录,便于 session-file-gc 统一清理。
24
30
  * [MF-3] worktree 模式下调用方必须传树根 cwd(ROOT mainCwd,经 PI_SUBAGENT_ROOT_CWD
@@ -78,6 +78,13 @@ import {
78
78
  killRecordChildWithEscalation,
79
79
  registerSpawnedChildForRecord,
80
80
  } from "./engine/host/spawned-children.ts";
81
+ // [u7a 生产补挂] D5 在途推送的迁移点出口:本模块的裸 arm/disarm 与镜像置死是
82
+ // 「保活 ↔ 正在执行」的全部生产翻转点,翻转后同步推最新绝对计数给壳层监听者
83
+ // (extension reporter → runtime 镜像)。挂点在调用方而非 lifecycle-manager——环规避
84
+ // 同 host-bridge.ts 头注(inflight-snapshot 反向读 lifecycle-manager 记账,lifecycle
85
+ // 层不得依赖 engine 域出口);createHostBridge 无生产装配点,本文件的直调点才是
86
+ // 生产迁移点(dev-0.9.17 合并后 u7a 推送链悬空的根因,见 crash-forensics impl-plan §7 v6)。
87
+ import { notifyInFlightChanged } from "./engine/inflight-snapshot.ts";
81
88
  // [u-t2a T2②/T2③ + W4 协议事件面] settled watchdog:chatMode 轮 settled 等待两段守护。
82
89
  // W3 起事件接线 = 协议事件流(arm 点 = 轮开始;refresh 源 = streamDelta/轮次生命周期
83
90
  // 帧;W4 三入口与旧原语逐一同构,见 settled-watchdog.ts 尾段)。
@@ -797,6 +804,9 @@ export class SubagentService {
797
804
  this.notifyHost.emitPendingUnregister(record.id, "closed");
798
805
  count++;
799
806
  }
807
+ // [u7a 生产补挂] 批量 dispose 收敛点推一次终态快照(绝对计数语义下循环内逐条推
808
+ // 与收敛后单推等价,单推省 N-1 次同步派发)。此刻镜像已由上方 kill/disarm 全量清零。
809
+ notifyInFlightChanged();
800
810
  return count;
801
811
  }
802
812
 
@@ -1355,6 +1365,8 @@ export class SubagentService {
1355
1365
  // 新 turn 开跑先 disarm idle timer(防 turn 期间误杀活进程——V2 决策 4,
1356
1366
  // 原 PiEngine.deliverPrompt 入口语义,编排侧承接)。
1357
1367
  disarmIdleTimer(record.id);
1368
+ // [u7a 生产补挂] disarm = 该 record 翻入「正在执行」,迁移后推最新在途计数(D5)。
1369
+ notifyInFlightChanged();
1358
1370
  const engine = this.resolveChatEnginePort();
1359
1371
  // recordId 键路由兜底注册(首轮已注册时幂等跳过——kickOffChatRound 注册面):
1360
1372
  // 续聊轮 delta/生命周期帧的分发目标。stream 缺省(首轮流已 dispose——widget 清除后
@@ -1557,6 +1569,9 @@ export class SubagentService {
1557
1569
  `terminating (LC-1 wedge recovery)`,
1558
1570
  );
1559
1571
  killRecordChildWithEscalation(record.id, "settled watchdog (hot path)");
1572
+ // [u7a 生产补挂] 镜像置死 = 在途 −1 的迁移点(本路径无相邻 disarm——轮中守护
1573
+ // 击杀,timer 早已 disarm),置死后推最新在途计数(D5)。
1574
+ notifyInFlightChanged();
1560
1575
  // [W3] 子进程在引擎进程内——终止经协议 interact cancel(SIGTERM → 引擎侧
1561
1576
  // settle 等待 → 杀链升级);镜像置死位由上方 killRecordChildWithEscalation 记账。
1562
1577
  this.terminateChatSession(record, "cancel", "settled watchdog (hot path)");
@@ -1717,6 +1732,8 @@ export class SubagentService {
1717
1732
  // [W3] 实际终止在引擎进程内(协议 interact close force → 引擎侧杀链);
1718
1733
  // killRecordChildWithEscalation 只做镜像置死记账。
1719
1734
  disarmIdleTimer(record.id);
1735
+ // [u7a 生产补挂] 终态化 disarm = 该 record 翻出「正在执行」,迁移后推最新在途计数。
1736
+ notifyInFlightChanged();
1720
1737
  disarmSettledWatchdog(record.id);
1721
1738
  disarmRoundFromProtocol(record.id);
1722
1739
  killRecordChildWithEscalation(record.id, "closeChatIdle");
@@ -1779,6 +1796,8 @@ export class SubagentService {
1779
1796
  // settled 等待窗口同步终结(disarm 幂等)。[W3] 实际终止经协议 interact close
1780
1797
  // force(引擎进程内的子进程),镜像置死位由 killRecordChildWithEscalation 记账。
1781
1798
  disarmIdleTimer(record.id);
1799
+ // [u7a 生产补挂] 终态化 disarm = 该 record 翻出「正在执行」,迁移后推最新在途计数。
1800
+ notifyInFlightChanged();
1782
1801
  disarmSettledWatchdog(record.id);
1783
1802
  disarmRoundFromProtocol(record.id);
1784
1803
  killRecordChildWithEscalation(record.id, "closeAfterRoundSettled");
@@ -2800,6 +2819,10 @@ export class SubagentService {
2800
2819
  bestEffort(fallbackErr, "armIdleTimer fallback (chat idle phase)", "error");
2801
2820
  }
2802
2821
  }
2822
+ // [u7a 生产补挂] arm = 该 record 翻入「保活」(Path A,不计在途),迁移后推最新
2823
+ // 在途计数(D5)。置于降级链之后:配置值/DEFAULT 两分支任一成功或双失败(状态
2824
+ // 未变)都统一推终态快照——绝对计数语义下重复推幂等无害。
2825
+ notifyInFlightChanged();
2803
2826
  }
2804
2827
 
2805
2828
  /** [W3] idle 帧锚点回填(冷续锚点 = 引擎侧会话滚动/compaction 后的最新定位)。 */
@@ -2994,6 +3017,8 @@ export class SubagentService {
2994
3017
  // cancel 分级另行承载,两路幂等)。
2995
3018
  killRecordChildWithEscalation(record.id, "cancelBackground");
2996
3019
  disarmIdleTimer(record.id);
3020
+ // [u7a 生产补挂] cancel 的 kill+disarm = 在途/保活双态翻出,迁移后推最新在途计数。
3021
+ notifyInFlightChanged();
2997
3022
  disarmSettledWatchdog(record.id);
2998
3023
  disarmRoundFromProtocol(record.id);
2999
3024
  this.unregisterChatRoundRoute(record.id);
@@ -38,10 +38,12 @@ export const SPAWN_GRACE_MS = 60_000;
38
38
  const JSON_INDENT = 2;
39
39
 
40
40
  /**
41
- * 锁参数:逐项对齐 extensions/shared/file-lock/src/file-lock.ts 的 withFileLock
42
- * 包装默认值(stale 30s / retries 10)。两侧参数漂移会破坏「同一把
43
- * <worktrees.json>.lock」的跨进程互斥语义(协议登记 data-source-registry.md §6),
44
- * 与旧包装共存/替换期间尤其如此。
41
+ * 锁参数:对齐三方同一磁盘协议的既有默认值——runtime
42
+ * packages/runtime/src/utils/file-lock.ts withFileLockAsync、pi auth-storage
43
+ * 范本、本文件 proper-lockfile 调用(retry 库参数:stale 30s / retries 10 /
44
+ * factor 2 / 100ms~10s randomize)。互斥由 lockfile 路径(<worktrees.json>.lock)
45
+ * + mkdir 原子协议保证;默认值对齐锚定的是各写方夺取时机(stale)与失败速度
46
+ * (重试参数)行为一致(协议登记 data-source-registry.md §6)。
45
47
  */
46
48
  const LOCK_STALE_MS = 30_000;
47
49
  const LOCK_RETRIES = 10;
@@ -184,9 +186,11 @@ export class WorktreeRegistry {
184
186
 
185
187
  /**
186
188
  * proper-lockfile 直用的跨进程锁(取代已删除的共享 file-lock 包装,抽包去依赖)。
187
- * 锁协议逐项对齐 extensions/shared/file-lock/src/file-lock.ts 的 withFileLock:
188
- * - lockfile 路径 = <目标文件>.lock(proper-lockfile 默认,与包装/runtime 侧
189
- * 同一路径才互斥)
189
+ * 锁协议对齐现存三方同协议实现——runtime 侧 packages/runtime/src/utils/file-lock.ts
190
+ * withFileLockAsync(范本 pi FileAuthStorageBackend,参数对齐 proper-lockfile
191
+ * 内部 retry 库)与 extension 侧 @zhushanwen/pi-file-lock:
192
+ * - lockfile 路径 = <目标文件>.lock(proper-lockfile 默认,与 runtime 侧/
193
+ * extension 侧同一路径才互斥)
190
194
  * - realpath:false —— 目标文件不存在也可锁(realpath 默认 true 时 ENOENT)
191
195
  * - stale 30s:持锁进程崩溃后锁可被夺取
192
196
  * - async retries 指数退避:10 次 / factor 2 / 100ms~10s / randomize,耗尽抛
package/src/index.ts CHANGED
@@ -24,7 +24,7 @@
24
24
  // 0.4.0 = 首个公开发布的收敛收口面(0.3.0 为 2026-08-30 裁决的跳号占位,永不单独
25
25
  // 发布;+ minor changeset 收口面落本号);与 package.json version 的一致性由
26
26
  // src/__tests__/smoke.test.ts 动态守护,改版本须两处同步。
27
- export const CORE_PACKAGE_VERSION = "0.7.0";
27
+ export const CORE_PACKAGE_VERSION = "0.8.0";
28
28
 
29
29
  // ── 宿主端口接线面(core/)────────────────────────────────────
30
30
  // HostServices:dataRoot / log / discoveryRoots 端口 + configureCore 注入;
@@ -123,6 +123,16 @@ export {
123
123
  type D8CompatZcodeEngineDeps,
124
124
  } from "./execution/engine/d8-compat.ts";
125
125
 
126
+ // 在途事件出口(u7a,D5):壳层(subagent-workflow host/inflight-reporter)注册
127
+ // 监听 + 求值绝对计数快照——core→壳回调出口的 barrel 消费面(exports 面收窄后
128
+ // 壳侧生产消费必须经 barrel,无深路径豁免)。
129
+ export {
130
+ setInFlightListener,
131
+ getInFlightSnapshot,
132
+ type InFlightSnapshot,
133
+ type InFlightListener,
134
+ } from "./execution/engine/inflight-snapshot.ts";
135
+ // W3 后内核宿主 = engine/host(host-bridge arm/disarm 委托点为在途迁移点),模块本体不经 engines/pi。
126
136
  // maxTurnsToWatchdogMs 为 maxTurns→watchdog 毫秒换算(U3/U4 / D7,floor 语义
127
137
  // 文档化——两宿主预算一致性 S2 的函数级锚点;[W3] 定义收敛在 pi-host-binding,
128
138
  // 原 inproc session-runner(已删) 定义随删件消亡);killRecordChildWithEscalation 为