@zhushanwen/pi-base-tool-enhance 0.5.2 → 0.5.4

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.
@@ -8,14 +8,18 @@
8
8
  * ③ fork 后任务完成通知写进新 session,旧 session 文件的 register 成僵尸。
9
9
  *
10
10
  * 收尾写法(权威路径):直接 pi.appendEntry("pending:unregister", {id, reason,
11
- * status})——**不走 bus emit 作为权威**:appendEntry 不依赖 listener 存活,两个
12
- * extension 的加载/派发顺序(CLI --extension 顺序用户可控)无保障,顺序反转时
13
- * emit 无人接收、对账失效。差集消费方 goal 从持久化 entries 算差集
14
- * (agent-end.ts getEntries()),appendEntry 同步入账(pi dist 实证)对守卫直接
15
- * 生效,无不一致窗口。appendEntry 之外尽力补一次 emit(幂等兜底,纯日志性质:
16
- * pending-notifications 的 unregister listener 落盘前置判断 isPendingActive 对
17
- * getEntries() 现算——其内存 registry/rebuild 已随 ext-simplify-12 删除,emit
18
- * 到达时该 id 已注销即跳过,失败无害)。
11
+ * status})——**appendEntry 即唯一权威路径**:appendEntry 同步入账(pi dist 实证)
12
+ * 且不依赖 listener 存活,差集消费方 goal 从持久化 entries 算差集
13
+ * (agent-end.ts getEntries()),对守卫直接生效,无不一致窗口。(尽力补 emit 的
14
+ * 第二写路径已随 ext-simplify-13 删除:pending unregister listener 的落盘前置
15
+ * isPendingActive 对 entries 现算——其内存 registry/rebuild 已随 ext-simplify-12
16
+ * 删除——而对账 appendEntry 同步入账先于 emit 执行,emit 到达时该 id 必已注销
17
+ * = 恒 no-op 死路径。)
18
+ *
19
+ * 差集判据单点(ext-simplify-13):collectActivePendingIds(protocol
20
+ * pending-entries 模块,与 pending-notifications 守卫判据同源)+ bt- 前缀过滤;
21
+ * 本地差集副本已删除。task_id 前缀亦消费 protocol 契约常量
22
+ * BACKGROUND_TASK_ID_PREFIX(§2.3,区别于 subagent-workflow 的 bg-/run-)。
19
23
  *
20
24
  * 终态判据:registry state ∈ {exited, orphaned},或(state=running/killing 且
21
25
  * kill(pid,0) 判死:收殓/写盘失败遗留的 running 条目按事实终态处理)。
@@ -27,24 +31,26 @@
27
31
  * 语义覆盖孤儿终态由 runtime 异步写入的时序窗口。
28
32
  */
29
33
 
34
+ import {
35
+ BACKGROUND_TASK_ID_PREFIX,
36
+ collectActivePendingIds,
37
+ mapReasonToStatus,
38
+ } from "@taiji/extension-protocol";
39
+ import { isPidAlive } from "@taiji/extension-protocol/background-task";
40
+ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
30
41
  import { getLogger } from "@zhushanwen/pi-extension-logger";
31
42
 
32
- import { isPidAlive } from "../kill-tree.ts";
33
43
  import { toPendingReason } from "./notify.ts";
34
44
  import { getRegistryPath, readRegistry } from "./registry.ts";
35
45
  import { isActiveState, isTerminalState, type RegistryEntry } from "./types.ts";
36
46
 
37
47
  const logger = getLogger("base-tool-enhance");
38
48
 
39
- /** 本包 task_id 前缀(§2.3,区别于 subagent-workflow 的 bg-/run-)。 */
40
- export const BTE_TASK_ID_PREFIX = "bt-";
41
-
42
49
  /**
43
50
  * 对账依赖的最小 pi 面(结构兼容 ExtensionAPI 的子集;测试注入不造完整 pi)。
44
51
  */
45
52
  export interface ReconcilePi {
46
53
  appendEntry(customType: string, data?: unknown): void;
47
- events: { emit(channel: string, data: unknown): void };
48
54
  }
49
55
 
50
56
  /** 对账结果(日志 + 测试断言面)。 */
@@ -55,48 +61,9 @@ export interface ReconcileResult {
55
61
  skipped: string[];
56
62
  }
57
63
 
58
- /** entries 的最小可识别形状(duck-typed,与 pending-notifications state.ts EntryLike 同式)。 */
59
- interface EntryLike {
60
- customType?: string;
61
- data?: { id?: unknown } | null;
62
- }
63
-
64
- function readEntryId(raw: unknown): string | undefined {
65
- if (!raw || typeof raw !== "object") return undefined;
66
- const entry = raw as EntryLike;
67
- const id = entry.data?.id;
68
- return typeof id === "string" ? id : undefined;
69
- }
70
-
71
- /**
72
- * 差集:bt- 前缀 pending:register 且无对应 pending:unregister 的 id 集合。
73
- * 只认 bt- 前缀——workflow/subagent 的 register 不归本包对账(差集算法与
74
- * pending-notifications countActiveFromEntries 同构:unregister 全局抵消 + register
75
- * 去重,id 全局唯一前提)。
76
- */
77
- export function collectUnsettledTaskIds(entries: unknown[]): Set<string> {
78
- const unregistered = new Set<string>();
79
- for (const raw of entries) {
80
- if (!raw || typeof raw !== "object") continue;
81
- if ((raw as EntryLike).customType !== "pending:unregister") continue;
82
- const id = readEntryId(raw);
83
- if (id !== undefined && id.startsWith(BTE_TASK_ID_PREFIX)) unregistered.add(id);
84
- }
85
- const active = new Set<string>();
86
- for (const raw of entries) {
87
- if (!raw || typeof raw !== "object") continue;
88
- if ((raw as EntryLike).customType !== "pending:register") continue;
89
- const id = readEntryId(raw);
90
- if (id === undefined || !id.startsWith(BTE_TASK_ID_PREFIX)) continue;
91
- if (unregistered.has(id) || active.has(id)) continue;
92
- active.add(id);
93
- }
94
- return active;
95
- }
96
-
97
64
  /**
98
65
  * 对账主体(同步:readRegistry / kill(pid,0) / appendEntry 均同步,session_start
99
- * 链内毫秒级完成)。每个僵尸任务 appendEntry 一次 + 尽力 emit 一次。
66
+ * 链内毫秒级完成)。每个僵尸任务 appendEntry 一次(唯一权威写路径)。
100
67
  */
101
68
  export function reconcilePendingEntries(
102
69
  pi: ReconcilePi,
@@ -105,7 +72,7 @@ export function reconcilePendingEntries(
105
72
  entries: unknown[],
106
73
  ): ReconcileResult {
107
74
  const result: ReconcileResult = { reconciled: 0, skipped: [] };
108
- const unsettled = collectUnsettledTaskIds(entries);
75
+ const unsettled = collectActivePendingIds(entries, { idPrefix: BACKGROUND_TASK_ID_PREFIX });
109
76
  if (unsettled.size === 0) return result;
110
77
 
111
78
  const registry = readRegistry(getRegistryPath(dataDir, sessionId));
@@ -125,23 +92,20 @@ export function reconcilePendingEntries(
125
92
  }
126
93
  const pendingReason = settledPendingReason(entry);
127
94
  try {
128
- pi.appendEntry("pending:unregister", { id, reason: pendingReason, status: pendingReason });
95
+ // status 经 protocol mapReasonToStatus 单点映射(ext-simplify-17 D10):与
96
+ // pending-notifications unregister listener 同一函数——原 status === reason 的
97
+ // identity 假设在非 identity reason(budget_limited→failed 等)上会静默漂移
98
+ pi.appendEntry("pending:unregister", {
99
+ id,
100
+ reason: pendingReason,
101
+ status: mapReasonToStatus(pendingReason),
102
+ });
129
103
  } catch (err) {
130
104
  logger.warn("reconcile appendEntry failed; retry on next session_start", {
131
- detail: { id, err: err instanceof Error ? err.message : String(err) },
105
+ detail: { id, err: toErrorMessage(err) },
132
106
  });
133
107
  continue;
134
108
  }
135
- // 尽力补 emit(幂等兜底,纯日志性质:listener 落盘前置 isPendingActive 对
136
- // entries 现算,该 id 已注销即跳过——appendEntry 同步入账已无不一致窗口;
137
- // 失败无害)
138
- try {
139
- pi.events.emit("pending:unregister", { id, reason: pendingReason });
140
- } catch (err) {
141
- logger.debug("reconcile best-effort emit failed (harmless)", {
142
- detail: { id, err: err instanceof Error ? err.message : String(err) },
143
- });
144
- }
145
109
  result.reconciled++;
146
110
  }
147
111
  if (result.reconciled > 0) {
@@ -159,10 +123,10 @@ function isTerminalByRegistry(entry: RegistryEntry): boolean {
159
123
  }
160
124
 
161
125
  /**
162
- * 收尾 reason/status 映射:
126
+ * 收尾 reason 映射(reason→status 的第二跳在写点经 protocol mapReasonToStatus 单点):
163
127
  * - exited:按条目 reason/exitCode 走 toPendingReason(与 exit 边沿 emit 同一映射,
164
- * 两路径写出的 entry 语义一致);reason 缺失按 pending mapReasonToStatus 的
165
- * default=completed 语义处理(防御分支,正常路径 finalize 必写 reason)
128
+ * 两路径写出的 entry 语义一致);reason 缺失按 cancelled 处理(防御分支,正常路径
129
+ * finalize 必写 reason)
166
130
  * - orphaned / running+判死:cancelled(任务非自身成败地终止/消失)
167
131
  */
168
132
  function settledPendingReason(entry: RegistryEntry): "completed" | "failed" | "time_limited" | "cancelled" {
@@ -13,7 +13,8 @@
13
13
  * 可查、对账可补),不加同步握手。
14
14
  */
15
15
 
16
- import { isPidAlive } from "../kill-tree.ts";
16
+ import { isPidAlive } from "@taiji/extension-protocol/background-task";
17
+
17
18
  import { readTailSummary } from "./output-tail.ts";
18
19
  import { readRegistry, taskToRegistryEntry, writeRegistryEntry } from "./registry.ts";
19
20
  import { finalizeTask, getActiveTasks } from "./task-store.ts";
@@ -18,18 +18,19 @@
18
18
  * 挂起进程,pi 的 ignore 语义完全失效
19
19
  * - rpc/print 模式 Ctrl+C:pi 无 SIGINT listener(dist 全量 grep 唯一注册点在 TUI
20
20
  * 挂起态)→ 默认处置进程即死。探针实测:无 listener 的信号默认终止**不触发
21
- * exit 事件**,同步收殓在该路径本就不可达;孤儿由 xyz-agent runtime 收殓器按
21
+ * exit 事件**,同步收殓在该路径本就不可达;孤儿由 taiji runtime 收殓器按
22
22
  * 属主判定收殓(u-bte-remove 下沉后;与 SIGKILL 强杀同类,设计内兜底)
23
23
  * - cleanup-only listener 更不可取:注册即抑制默认终止,rpc 前台 Ctrl+C 后 pi 变
24
24
  * 成杀不死的僵尸进程
25
25
  *
26
- * 子进程死后 registry 目录由 xyz-agent runtime 启动期兜底扫描覆盖(收殓下沉)——
26
+ * 子进程死后 registry 目录由 taiji runtime 启动期兜底扫描覆盖(收殓下沉)——
27
27
  * 本单元不管。
28
28
  */
29
29
 
30
+ import { killProcessTree } from "@taiji/extension-protocol/background-task";
31
+ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
30
32
  import { getLogger } from "@zhushanwen/pi-extension-logger";
31
33
 
32
- import { killProcessTree } from "../kill-tree.ts";
33
34
  import { emitPendingUnregister } from "./notify.ts";
34
35
  import { stopPoller } from "./poller.ts";
35
36
  import { taskToRegistryEntry, writeRegistryEntry } from "./registry.ts";
@@ -76,11 +77,16 @@ export function reapBackgroundTasksNow(): void {
76
77
  stopPoller();
77
78
  for (const task of getActiveTasks()) {
78
79
  try {
79
- killProcessTree(task.pid);
80
+ // 回退路径诊断经 onFallback 注入 logger 适配(ext-simplify-13 D2)
81
+ killProcessTree(task.pid, (step, err) =>
82
+ logger.debug(step, {
83
+ detail: { pid: task.pid, err: toErrorMessage(err) },
84
+ }),
85
+ );
80
86
  } catch (err) {
81
87
  // 单条 kill 失败不阻断其余条目收殓;进程将退出,残余由 runtime 收殓兜底
82
88
  logger.warn("kill-tree failed during process-exit reap", {
83
- detail: { taskId: task.taskId, pid: task.pid, err: err instanceof Error ? err.message : String(err) },
89
+ detail: { taskId: task.taskId, pid: task.pid, err: toErrorMessage(err) },
84
90
  });
85
91
  }
86
92
  const finalized = finalizeTask(task.taskId, {
@@ -3,44 +3,47 @@
3
3
  *
4
4
  * 路径:<dataDir>/base-tool-enhance/<sessionId>/registry.json(dataDir = pi
5
5
  * getAgentDir() 同源路径,由调用方解析传入)。条目记 ownerPiPid——收殓侧属主
6
- * 判定依据(u-bte-remove 后 = xyz-agent runtime background-task-reaper),M2 只负责
6
+ * 判定依据(u-bte-remove 后 = taiji runtime background-task-reaper),M2 只负责
7
7
  * 写入。
8
8
  *
9
+ * 行为原语单点(ext-simplify-13):解析防御 / corrupt 隔离 / 原子写 / 序列化 /
10
+ * 终态 LRU 裁剪自 @taiji/extension-protocol background-task 子出口引入——此前
11
+ * 本地实现与 runtime 侧逐字同构、靠注释对齐;本模块只保留 bte 形态薄壳:Map 投影
12
+ * readRegistry 与锁内 RMW 写壳。
13
+ *
9
14
  * 写入协议:
10
- * - 原子写 temp+rename(tmp 名带 pid+随机段防并发碰撞,llm-shared saveConfig 范式)
15
+ * - 原子写 temp+rename、损坏读取防御(§3.6 重命名 .corrupt 保留现场 + 按空表
16
+ * 重建)、终态条目 LRU 上限 50 均由 protocol 原语承担;诊断日志(corrupt 隔离
17
+ * warn 等)经 onLog 注入本包 logger,落盘通道不变
11
18
  * - 锁内 RMW(@zhushanwen/pi-file-lock withFileLockSync)——同 sessionId 目录可能
12
19
  * 被桌面端 ephemeral 附着进程与发起进程并发写,跨进程互斥只依赖同一 lockfile
13
- * (runtime 收殓器写终态用 xyz-agent 统一锁,与该 lockfile 互斥)
20
+ * (runtime 收殓器写终态用 taiji 统一锁,与该 lockfile 互斥)
14
21
  * - 锁获取失败不降级无锁写:返回 {success:false},条目停留 running 由 runtime
15
22
  * 收殓兜底(§3.5「registry/entry 写不进则条目停留 running」)
16
- * - 损坏读取防御(§3.6):解析失败/形状非法 → 重命名 .corrupt 保留现场 + 按空表
17
- * 重建 + warn 日志
18
- * - 终态条目 LRU 上限 50(与单例表对称)
19
- *
20
- * 契约改引(u-bte-remove):版本常量 / LRU 上限 / 文件形状自
21
- * @xyz-agent/extension-protocol background-task.ts 契约引入(此前本地重复定义)。
22
23
  */
23
24
 
24
- import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
25
- import { dirname, join } from "node:path";
25
+ import { join } from "node:path";
26
26
 
27
+ // 契约常量(LRU 上限)走 index 出口(background-task.ts 契约面);行为原语走
28
+ // background-task 子出口(含 node 内建依赖,不进 index——renderer/core 零触达)
29
+ import { MAX_TERMINAL_REGISTRY_ENTRIES } from "@taiji/extension-protocol";
27
30
  import {
28
- BACKGROUND_TASK_REGISTRY_VERSION,
29
- type BackgroundTaskRegistryFile,
30
- MAX_TERMINAL_REGISTRY_ENTRIES,
31
- } from "@xyz-agent/extension-protocol";
31
+ atomicWriteRegistry,
32
+ readRegistry as readRegistryEntries,
33
+ serializeRegistryFile,
34
+ trimTerminalEntries,
35
+ type RegistryFileLogFn,
36
+ } from "@taiji/extension-protocol/background-task";
37
+ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
32
38
  import { getLogger } from "@zhushanwen/pi-extension-logger";
33
39
  import { withFileLockSync } from "@zhushanwen/pi-file-lock";
34
40
 
35
- import { isTerminalState, type BackgroundTask, type RegistryEntry } from "./types.ts";
41
+ import type { BackgroundTask, RegistryEntry } from "./types.ts";
36
42
 
37
43
  const logger = getLogger("base-tool-enhance");
38
44
 
39
- const JSON_INDENT = 2;
40
- // tmp 随机段参数(llm-shared uniqueTmpPath 同款:36 进制随机串,跳过 "0." 前缀)
41
- const TMP_RADIX = 36;
42
- const TMP_SLICE_START = 2;
43
- const TMP_SLICE_END = 10;
45
+ /** protocol registry 原语 → 本包 logger 适配(corrupt 隔离 warn 是排障生命线)。 */
46
+ const registryFileLog: RegistryFileLogFn = (level, event, detail) => logger[level](event, { detail });
44
47
 
45
48
  export function getBaseToolEnhanceDir(dataDir: string): string {
46
49
  return join(dataDir, "base-tool-enhance");
@@ -51,78 +54,13 @@ export function getRegistryPath(dataDir: string, sessionId: string): string {
51
54
  }
52
55
 
53
56
  /**
54
- * 最低限度形状校验:核心标识字段缺失即整条丢弃(不因单条脏数据报废全表)。
55
- */
56
- function isValidRegistryEntry(item: unknown): item is RegistryEntry {
57
- if (typeof item !== "object" || item === null) return false;
58
- const e = item as Record<string, unknown>;
59
- return (
60
- typeof e.taskId === "string" &&
61
- typeof e.pid === "number" &&
62
- typeof e.command === "string" &&
63
- typeof e.outputFile === "string" &&
64
- typeof e.startedAt === "number" &&
65
- typeof e.state === "string" &&
66
- typeof e.ownerPiPid === "number" &&
67
- typeof e.sessionId === "string"
68
- );
69
- }
70
-
71
- /** 校验并归一化 registry 文件内容;形状非法返回 undefined(走 corrupt 路径)。 */
72
- function parseRegistryContent(raw: string): BackgroundTaskRegistryFile | undefined {
73
- let parsed: unknown;
74
- try {
75
- parsed = JSON.parse(raw);
76
- } catch {
77
- return undefined;
78
- }
79
- if (typeof parsed !== "object" || parsed === null) return undefined;
80
- const { version, entries } = parsed as Record<string, unknown>;
81
- if (version !== BACKGROUND_TASK_REGISTRY_VERSION || !Array.isArray(entries)) return undefined;
82
- const valid: RegistryEntry[] = [];
83
- for (const item of entries) {
84
- if (isValidRegistryEntry(item)) valid.push(item);
85
- }
86
- return { version: BACKGROUND_TASK_REGISTRY_VERSION, entries: valid };
87
- }
88
-
89
- /** .corrupt 落点:固定名优先;已存在则带时间戳,不覆盖前一份现场。 */
90
- function corruptPathFor(registryPath: string): string {
91
- const base = `${registryPath}.corrupt`;
92
- return existsSync(base) ? `${base}-${Date.now()}` : base;
93
- }
94
-
95
- /**
96
- * 读取 registry 全量条目。文件不存在 / 读失败 / 解析失败均返回空表(工具面不因
97
- * registry 问题崩溃);解析失败时重命名 .corrupt 保留现场 + warn(§3.6)。
57
+ * 读取 registry 全量条目(Map 投影薄壳)。文件不存在 / 读失败 / 解析失败均返回
58
+ * 空表(工具面不因 registry 问题崩溃);解析失败由 protocol 原语重命名 .corrupt
59
+ * 保留现场 + warn(§3.6)。
98
60
  */
99
61
  export function readRegistry(registryPath: string): Map<string, RegistryEntry> {
100
- if (!existsSync(registryPath)) return new Map();
101
- let raw: string;
102
- try {
103
- raw = readFileSync(registryPath, "utf8");
104
- } catch (err) {
105
- logger.warn("registry read failed, treating as empty", {
106
- detail: { path: registryPath, err: err instanceof Error ? err.message : String(err) },
107
- });
108
- return new Map();
109
- }
110
- const parsed = parseRegistryContent(raw);
111
- if (parsed === undefined) {
112
- const corruptPath = corruptPathFor(registryPath);
113
- try {
114
- renameSync(registryPath, corruptPath);
115
- logger.warn("registry corrupted, renamed to preserve scene and rebuilt empty", {
116
- detail: { path: registryPath, corruptPath },
117
- });
118
- } catch (err) {
119
- logger.warn("registry corrupted and rename failed, rebuilding empty in place", {
120
- detail: { path: registryPath, err: err instanceof Error ? err.message : String(err) },
121
- });
122
- }
123
- return new Map();
124
- }
125
- return new Map(parsed.entries.map((e) => [e.taskId, e]));
62
+ const { entries } = readRegistryEntries(registryPath, registryFileLog);
63
+ return new Map(entries.map((e) => [e.taskId, e] as const));
126
64
  }
127
65
 
128
66
  /** BackgroundTask → RegistryEntry(剥离运行时字段 intent/timeoutTimer/child/registryPath)。 */
@@ -145,34 +83,10 @@ export function taskToRegistryEntry(task: BackgroundTask): RegistryEntry {
145
83
  };
146
84
  }
147
85
 
148
- function serializeRegistry(entries: RegistryEntry[]): string {
149
- const shape: BackgroundTaskRegistryFile = { version: BACKGROUND_TASK_REGISTRY_VERSION, entries };
150
- return `${JSON.stringify(shape, null, JSON_INDENT)}\n`;
151
- }
152
-
153
- /** 原子写:tmp(pid+随机段唯一化)+ rename(POSIX/Windows 均原子);失败清理 tmp。 */
154
- function atomicWriteRegistry(registryPath: string, content: string): void {
155
- mkdirSync(dirname(registryPath), { recursive: true });
156
- const tmpPath = `${registryPath}.tmp_${process.pid}_${Math.random().toString(TMP_RADIX).slice(TMP_SLICE_START, TMP_SLICE_END)}`;
157
- try {
158
- writeFileSync(tmpPath, content, "utf8");
159
- renameSync(tmpPath, registryPath);
160
- } catch (err) {
161
- try {
162
- if (existsSync(tmpPath)) unlinkSync(tmpPath);
163
- } catch (cleanupErr) {
164
- // tmp 清理失败不掩盖原错误,仅留诊断
165
- logger.warn("registry tmp cleanup failed", {
166
- detail: { tmpPath, err: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr) },
167
- });
168
- }
169
- throw err;
170
- }
171
- }
172
-
173
86
  /**
174
87
  * 写入/更新单条 registry 条目(锁内 RMW:读全量 → 合并同 id 覆盖 → 终态 LRU 50 →
175
- * 原子写)。任务登记(running)、killing intent、终态三条路径共用。
88
+ * 原子写;读 / 裁剪 / 序列化 / 原子写由 protocol 原语承担)。任务登记(running)、
89
+ * killing intent、终态三条路径共用。
176
90
  * 失败返回 {success:false}——调用方按「写不进则条目停留 running,runtime 收殓
177
91
  * 兜底」处理(M2 只 warn,不重试不阻断主流程)。
178
92
  */
@@ -181,21 +95,19 @@ export function writeRegistryEntry(
181
95
  entry: RegistryEntry,
182
96
  ): { success: boolean; error?: string } {
183
97
  const writeMerged = (): void => {
184
- const merged = readRegistry(registryPath);
98
+ const { entries } = readRegistryEntries(registryPath, registryFileLog);
99
+ const merged = new Map(entries.map((e) => [e.taskId, e] as const));
185
100
  merged.set(entry.taskId, entry);
186
- const all = [...merged.values()];
187
- const terminal = all
188
- .filter((e) => isTerminalState(e.state))
189
- .sort((a, b) => (a.endedAt ?? a.startedAt) - (b.endedAt ?? b.startedAt));
190
- const excess = terminal.length - MAX_TERMINAL_REGISTRY_ENTRIES;
191
- for (let i = 0; i < excess; i++) merged.delete(terminal[i].taskId);
192
- atomicWriteRegistry(registryPath, serializeRegistry([...merged.values()]));
101
+ const trimmed = trimTerminalEntries([...merged.values()], MAX_TERMINAL_REGISTRY_ENTRIES);
102
+ atomicWriteRegistry(registryPath, serializeRegistryFile(trimmed), registryFileLog);
193
103
  };
194
104
  try {
195
105
  withFileLockSync(registryPath, writeMerged);
196
106
  return { success: true };
197
107
  } catch (err) {
198
- const message = err instanceof Error ? err.message : String(err);
108
+ // 锁壳层写失败 warn 是本模块职责(protocol 原语不发该日志——写失败时的
109
+ // 「条目停留 running」降级决策在锁壳)
110
+ const message = toErrorMessage(err);
199
111
  logger.warn("registry write failed; entry stays as-is (runtime reaper will collect the orphan)", {
200
112
  detail: { path: registryPath, taskId: entry.taskId, err: message },
201
113
  });
@@ -5,6 +5,11 @@
5
5
  * <dataDir>/base-tool-enhance/<sessionId>/<task_id>.log(append fd,不占内存、
6
6
  * 无 pipe backpressure)+ child.unref()(pi 不等它)。abort/interrupt 不传播到
7
7
  * 后台任务(D15)——execute 立即返回,本模块不接触 signal。
8
+ *
9
+ * D6:本包不强加任务寿命上限(无 TTL/墙钟)——vitest watch、dev server 这类任务
10
+ * 的合法寿命就是「跑到用户叫停」,强加上限会把长任务误判死、完成通知链路随之失效。
11
+ * 寿命出口只有三个:自然退出 / 显式 timeout(LLM 传值或配置 backgroundTimeoutSeconds)
12
+ * / bash_kill。
8
13
  */
9
14
 
10
15
  import { spawn } from "node:child_process";
@@ -12,9 +17,10 @@ import { mkdirSync, openSync, closeSync, accessSync, constants } from "node:fs";
12
17
  import { dirname, join } from "node:path";
13
18
  import { randomBytes } from "node:crypto";
14
19
 
20
+ import { getProcessStartTimeSec, killProcessTree, pidStartMatchesRegistered } from "@taiji/extension-protocol/background-task";
21
+ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
15
22
  import { getLogger } from "@zhushanwen/pi-extension-logger";
16
23
 
17
- import { getProcessStartTimeSec, killProcessTree, pidStartMatchesRegistered } from "../kill-tree.ts";
18
24
  import { emitPendingRegister } from "./notify.ts";
19
25
  import { ensurePollerRunning } from "./poller.ts";
20
26
  import { getRegistryPath, taskToRegistryEntry, writeRegistryEntry } from "./registry.ts";
@@ -165,7 +171,7 @@ export function spawnBackgroundTask(opts: SpawnBackgroundOptions): SpawnBackgrou
165
171
  child.on("error", () => {});
166
172
  child.unref();
167
173
  } catch (err) {
168
- const message = err instanceof Error ? err.message : String(err);
174
+ const message = toErrorMessage(err);
169
175
  return { ok: false, error: `Failed to start background command: ${message}` };
170
176
  } finally {
171
177
  // 子进程已继承 fd 副本(含 spawn 同步失败路径:fd 未被子进程持有),父进程侧
@@ -176,7 +182,7 @@ export function spawnBackgroundTask(opts: SpawnBackgroundOptions): SpawnBackgrou
176
182
  } catch (err) {
177
183
  // 已关闭/不可关闭均不掩盖主流程,仅留诊断
178
184
  logger.debug("background spawn output fd close failed", {
179
- detail: { outputFile, err: err instanceof Error ? err.message : String(err) },
185
+ detail: { outputFile, err: toErrorMessage(err) },
180
186
  });
181
187
  }
182
188
  }
@@ -253,7 +259,12 @@ function armBackgroundTimeout(task: BackgroundTask, timeoutSec: number): void {
253
259
  // 收尾不受影响。
254
260
  return;
255
261
  }
256
- killProcessTree(task.pid);
262
+ // 回退路径诊断经 onFallback 注入 logger 适配(ext-simplify-13 D2)
263
+ killProcessTree(task.pid, (step, err) =>
264
+ logger.debug(step, {
265
+ detail: { pid: task.pid, err: toErrorMessage(err) },
266
+ }),
267
+ );
257
268
  const marked = markKillingIntent(task.taskId, "timeout");
258
269
  if (marked === undefined) return;
259
270
  writeRegistryEntry(marked.registryPath, taskToRegistryEntry(marked));
@@ -265,8 +276,9 @@ function armBackgroundTimeout(task: BackgroundTask, timeoutSec: number): void {
265
276
 
266
277
  /**
267
278
  * 到点 pid 身份校验:true = 登记时的原进程仍占用该 pid(可安全 kill-tree)。
268
- * 判据(精确比较 / startedAt 秒级降级 / 读不到保守 false)单点定义于 kill-tree.ts
269
- * 的 pidStartMatchesRegistered(原 reaper.ts 单点,收殓下沉后随删除平移)。
279
+ * 判据(精确比较 / startedAt 秒级降级 / 读不到保守 false)单点定义于
280
+ * extension-protocol 的 pidStartMatchesRegistered(原 reaper.ts 单点,跨端原语
281
+ * 归一后经 background-task 子出口消费——ext-simplify-13)。
270
282
  */
271
283
  function isRecordedPidStillOriginal(task: BackgroundTask): boolean {
272
284
  const actualStartSec = getProcessStartTimeSec(task.pid);
@@ -1,21 +1,17 @@
1
1
  /**
2
- * subagent 进程识别(D14 降级判据,探针 P5)。
2
+ * subagent 进程识别(bte background 降级判据——生命周期设计 D14 项,commit 8599abd0c)。
3
3
  *
4
- * subagent-core 的 buildChildEnv(packages/subagent-core/src/execution/engine/engines/pi/session-runner.ts)
5
- * 对每个子 agent 进程**无条件**注入身份贯穿 env(PI_SUBAGENT_ROOT_SESSION_ID / SELF_RECORD_ID /
6
- * DEPTH / ROOT_CWD,见其源码注释「无条件注入每个 subagent」)。任一存在 = 当前
7
- * pi 进程是 subagent 子进程 → 本扩展降级:background:true 被忽略走前台同步语义。
4
+ * 判据收敛于 ext-guards isSubagentProcess(`TAIJI_AGENT_SUBAGENT === "1"`——引擎
5
+ * spawn 链恒注入的统一标记,ext-simplify-17 D4 重锚定)。命中 = 当前 pi 进程是
6
+ * 引擎链上的真 subagent → 本扩展降级:background:true 被忽略走前台同步语义。
7
+ *
8
+ * [HISTORICAL] 旧判据查 PI_SUBAGENT_* 身份键两枚(任一存在即命中)——引擎协议化
9
+ * 后该键族无写入方,判据失效态(降级实际从未生效),20260914 真机探针双断言证实
10
+ * 后随 D4 重锚。现行事实与口径详见 ext-guards isSubagentProcess JSDoc、设计
11
+ * docs/architecture/ext-simplify-17-shared-extraction.md §3.1 D4、探针报告
12
+ * .tmp/dev-flow/ext-simplify-17-d4-probe.md。
8
13
  *
9
14
  * 为什么降级:子 agent 内后台化会破坏 workflow 结构化输出契约(预算耗尽时测试未
10
15
  * 回);且子进程死后其 registry 目录永远不会再有 session 启动,孤儿无人 reap。
11
16
  */
12
-
13
- /** subagent-core(session-runner)注入的身份贯穿 env 前缀(跨包契约,P5 探针确认)。 */
14
- const SUBAGENT_ENV_KEYS = [
15
- "PI_SUBAGENT_ROOT_SESSION_ID",
16
- "PI_SUBAGENT_SELF_RECORD_ID",
17
- ] as const;
18
-
19
- export function isSubagentProcess(): boolean {
20
- return SUBAGENT_ENV_KEYS.some((key) => process.env[key] !== undefined);
21
- }
17
+ export { isSubagentProcess } from "@zhushanwen/pi-ext-guards";
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * 条目唯一来源 = 本进程 execute 的 spawn(registerSpawnedTask 全仓唯一调用点 =
9
9
  * spawn-background.ts)。他进程 / 历史 session 的 running 条目**永不进表**——
10
- * 零恢复零接管,孤儿处置权统一归收殓侧(u-bte-remove 后 = xyz-agent runtime)
10
+ * 零恢复零接管,孤儿处置权统一归收殓侧(u-bte-remove 后 = taiji runtime)
11
11
  * 按属主裁决;终态条目可从 registry 读,仅供 bash_output 查历史。
12
12
  *
13
13
  * 终态条目 LRU 上限 MAX_TERMINAL_ENTRIES(与 registry 对称),淘汰后 bash_output
@@ -28,6 +28,8 @@
28
28
  * 未来新增写路径(如 maintenance 类)必须保持该顺序,违反即 intent 可丢失。
29
29
  */
30
30
 
31
+ import { trimTerminalEntries } from "@taiji/extension-protocol/background-task";
32
+
31
33
  import {
32
34
  isActiveState,
33
35
  isTerminalState,
@@ -120,14 +122,17 @@ export function finalizeTask(taskId: string, outcome: FinalizeOutcome): Backgrou
120
122
  return task;
121
123
  }
122
124
 
123
- /** 终态条目超上限时按 endedAt 升序淘汰最老(LRU;endedAt 缺失退 startedAt)。 */
125
+ /**
126
+ * 终态条目超上限时按 endedAt 升序淘汰最老(LRU;endedAt 缺失退 startedAt)。
127
+ * 裁剪纯函数单点在 protocol trimTerminalEntries(ext-simplify-13:与 registry 写侧、
128
+ * runtime registry-write 共用同一实现,本地上限经 MAX_TERMINAL_TASKS 传参)。
129
+ */
124
130
  function evictTerminalOverflow(): void {
125
- const terminal = getAllTasks()
126
- .filter((t) => isTerminalState(t.state))
127
- .sort((a, b) => (a.endedAt ?? a.startedAt) - (b.endedAt ?? b.startedAt));
128
- const excess = terminal.length - MAX_TERMINAL_TASKS;
129
- for (let i = 0; i < excess; i++) {
130
- taskTable.delete(terminal[i].taskId);
131
+ const kept = trimTerminalEntries(getAllTasks(), MAX_TERMINAL_TASKS);
132
+ if (kept.length === taskTable.size) return;
133
+ const keptIds = new Set(kept.map((t) => t.taskId));
134
+ for (const id of [...taskTable.keys()]) {
135
+ if (!keptIds.has(id)) taskTable.delete(id);
131
136
  }
132
137
  }
133
138
 
@@ -8,7 +8,7 @@
8
8
  * 条目记 ownerPiPid(M2 只负责写入)
9
9
  *
10
10
  * 契约改引(u-bte-remove):registry 持久化形状(RegistryEntry / 状态机 / reason
11
- * 枚举 / active·terminal 判定)与 @xyz-agent/extension-protocol background-task.ts
11
+ * 枚举 / active·terminal 判定)与 @taiji/extension-protocol background-task.ts
12
12
  * 契约逐字段一致(契约文档头声明基线即本文件 + registry.ts),本地副本已删除、
13
13
  * 此处 re-export 并保留包内短名——消费方 import 路径与字段语义零变化。运行时
14
14
  * 对象类型(BackgroundTask / KillingIntent)不在契约内(剥离非持久字段前的形状),
@@ -19,18 +19,18 @@ import type { ChildProcess } from "node:child_process";
19
19
  import type {
20
20
  BackgroundTaskEndReason as ContractEndReason,
21
21
  BackgroundTaskState as ContractState,
22
- } from "@xyz-agent/extension-protocol";
22
+ } from "@taiji/extension-protocol";
23
23
 
24
24
  /** 状态机与 reason 枚举 / 持久化条目 / active·terminal 判定:契约 SSOT re-export。 */
25
25
  export type {
26
26
  BackgroundTaskRegistryEntry as RegistryEntry,
27
27
  BackgroundTaskState,
28
28
  BackgroundTaskEndReason,
29
- } from "@xyz-agent/extension-protocol";
29
+ } from "@taiji/extension-protocol";
30
30
  export {
31
31
  isActiveBackgroundTaskState as isActiveState,
32
32
  isTerminalBackgroundTaskState as isTerminalState,
33
- } from "@xyz-agent/extension-protocol";
33
+ } from "@taiji/extension-protocol";
34
34
 
35
35
  /**
36
36
  * killing intent:bash_kill / 后台 timeout 已发令、轮询器 exit 边沿未确认的瞬态标记。