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

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.
@@ -27,9 +27,10 @@
27
27
  * 本单元不管。
28
28
  */
29
29
 
30
+ import { killProcessTree } from "@xyz-agent/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, {
@@ -6,41 +6,44 @@
6
6
  * 判定依据(u-bte-remove 后 = xyz-agent runtime background-task-reaper),M2 只负责
7
7
  * 写入。
8
8
  *
9
+ * 行为原语单点(ext-simplify-13):解析防御 / corrupt 隔离 / 原子写 / 序列化 /
10
+ * 终态 LRU 裁剪自 @xyz-agent/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
20
  * (runtime 收殓器写终态用 xyz-agent 统一锁,与该 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 "@xyz-agent/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 "@xyz-agent/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 "@xyz-agent/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(`XYZ_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";
@@ -28,6 +28,8 @@
28
28
  * 未来新增写路径(如 maintenance 类)必须保持该顺序,违反即 intent 可丢失。
29
29
  */
30
30
 
31
+ import { trimTerminalEntries } from "@xyz-agent/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
 
@@ -18,9 +18,12 @@
18
18
 
19
19
  import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
20
20
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
21
+ import { getProcessStartTimeSec, isPidAlive, killProcessTree } from "@xyz-agent/extension-protocol/background-task";
21
22
  import { Type } from "typebox";
22
23
 
23
- import { getProcessStartTimeSec, isPidAlive, killProcessTree } from "./kill-tree.ts";
24
+ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
25
+ import { getLogger } from "@zhushanwen/pi-extension-logger";
26
+
24
27
  import { ensurePollerRunning } from "./background/poller.ts";
25
28
  import { getRegistryPath, readRegistry, taskToRegistryEntry, writeRegistryEntry } from "./background/registry.ts";
26
29
  import { getAllTasks, markKillingIntent } from "./background/task-store.ts";
@@ -38,6 +41,8 @@ const BASH_KILL_DESCRIPTION = [
38
41
  /** JSON 输出缩进(registry.ts 同款)。 */
39
42
  const JSON_INDENT = 2;
40
43
 
44
+ const logger = getLogger("base-tool-enhance");
45
+
41
46
  function textResult(text: string): AgentToolResult<unknown> {
42
47
  return { content: [{ type: "text", text }], details: undefined };
43
48
  }
@@ -77,11 +82,15 @@ export function createBashKillToolDefinition() {
77
82
  }
78
83
  // registry-only 活跃条目 = 他进程任务(本进程活跃任务必在单例表)——跨进程
79
84
  // 不可 kill:处置权归发起进程;属主若已死,孤儿由 xyz-agent runtime 在
80
- // session 销毁时或启动期兜底扫描时收殓(属主判定,u-bte-remove 下沉)
85
+ // session 销毁时或启动期兜底扫描时收殓(属主判定,u-bte-remove 下沉;
86
+ // 纯 CLI 独立安装无 runtime 收殓兜底,孤儿自然退出或手动杀——E11 已
87
+ // 接受边界,hint 按形态分流,不写死 runtime 承诺)
81
88
  return killedFalse(
82
89
  "cross-process running task owned by another pi process",
83
90
  "the task is managed by the pi process that started it (bash_kill from that session); " +
84
- "if that process is gone, the owning xyz-agent runtime will collect the orphan when its session ends or on app restart",
91
+ "if that process is gone, the orphan keeps running until it exits on its own; " +
92
+ "inside the xyz-agent runtime it is collected when the owner's session ends or on app restart; " +
93
+ "standalone pi: kill the process group manually (kill -- -<pgid>; the registered task pid is the pgid) if it lingers",
85
94
  );
86
95
  }
87
96
  if (isTerminalState(fromStore.state)) {
@@ -125,7 +134,13 @@ export function createBashKillToolDefinition() {
125
134
  if (marked !== undefined) {
126
135
  writeRegistryEntry(marked.registryPath, taskToRegistryEntry(marked));
127
136
  }
128
- killProcessTree(fromStore.pid);
137
+ // 回退路径诊断经 onFallback 注入 logger 适配(ext-simplify-13 D2:进程原语
138
+ // 零日志依赖,协议侧 step 标识符 + 本包落盘通道)
139
+ killProcessTree(fromStore.pid, (step, err) =>
140
+ logger.debug(step, {
141
+ detail: { pid: fromStore.pid, err: toErrorMessage(err) },
142
+ }),
143
+ );
129
144
  // 轮询器确保在跑:边沿收尾(写终态)依赖它
130
145
  ensurePollerRunning();
131
146
  return textResult(
@@ -14,9 +14,10 @@
14
14
 
15
15
  import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
16
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
17
+ import { readOutputTail } from "@xyz-agent/extension-protocol/background-task";
17
18
  import { Type } from "typebox";
18
19
 
19
- import { readOutputTail } from "./background/output-tail.ts";
20
+ import { TAIL_MAX_BYTES, TAIL_MAX_LINES } from "./background/output-tail.ts";
20
21
  import { getRegistryPath, readRegistry } from "./background/registry.ts";
21
22
  import { truncateCommand } from "./background/spawn-background.ts";
22
23
  import { getAllTasks } from "./background/task-store.ts";
@@ -88,7 +89,10 @@ function findTask(
88
89
 
89
90
  /** 详情视图:tail 输出(丢失时 "<lost>",§3.6)+ 终态字段按存在性展开。 */
90
91
  function taskDetailText(task: BackgroundTask | RegistryEntry): string {
91
- const tail = readOutputTail(task.outputFile);
92
+ // protocol 原语返回 {text, truncated};50KB/2000 行口径(本包产品决策)在此传参。
93
+ // 返回 JSON 的 `output` 字段名是 LLM 可见契约(工具 schema/描述零变化),仅在
94
+ // 此处从原语的 text 投影,ext-simplify-13 D2 有意内部变化
95
+ const tail = readOutputTail(task.outputFile, { maxBytes: TAIL_MAX_BYTES, maxLines: TAIL_MAX_LINES });
92
96
  const detail = {
93
97
  task_id: task.taskId,
94
98
  state: task.state,
@@ -96,7 +100,7 @@ function taskDetailText(task: BackgroundTask | RegistryEntry): string {
96
100
  ...(task.reason !== undefined ? { reason: task.reason } : {}),
97
101
  startedAt: task.startedAt,
98
102
  ...(task.durationMs !== undefined ? { durationMs: task.durationMs } : {}),
99
- output: tail?.output ?? "<lost>",
103
+ output: tail?.text ?? "<lost>",
100
104
  truncated: tail?.truncated ?? false,
101
105
  outputFile: task.outputFile,
102
106
  };
package/src/bash-tool.ts CHANGED
@@ -148,6 +148,9 @@ export function createBashOverrideToolDefinition() {
148
148
  // (label/promptSnippet/promptGuidelines/renderCall/renderResult 等)全部随
149
149
  // delegate 透传,cwd 重建 delegate 时静态闭包引用不变(render 闭包与 cwd 无关)
150
150
  ...initial,
151
+ // D2(禁止改名):桌面侧六处按工具 key 'bash' 硬编码(truncate-tool-output、
152
+ // useToolMeta、block-icon、pi-preset BUILTIN_TOOLS 与 deniedTools、permission
153
+ // extractCommand)——改名后这些消费面全部失配,审批退化为逐次弹窗、渲染错位
151
154
  name: initial.name, // "bash":同名覆盖内置工具(agent-session _refreshToolRegistry 后注册者胜)
152
155
  description: ENHANCED_BASH_DESCRIPTION,
153
156
  parameters: enhancedBashSchema,
package/src/config.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { getConfigPath as getLlmSharedConfigPath, loadConfig } from "@zhushanwen/pi-llm-shared";
14
+ import { isRecord, toErrorMessage } from "@zhushanwen/pi-ext-guards";
14
15
  import { getLogger } from "@zhushanwen/pi-extension-logger";
15
16
 
16
17
  import { DEFAULT_MAX_CONCURRENT_BACKGROUND } from "./background/spawn-background.ts";
@@ -51,10 +52,6 @@ export function getConfigFilePath(): string {
51
52
  return getLlmSharedConfigPath(CONFIG_PKG);
52
53
  }
53
54
 
54
- function isPlainObject(value: unknown): value is Record<string, unknown> {
55
- return typeof value === "object" && value !== null && !Array.isArray(value);
56
- }
57
-
58
55
  function describeValue(value: unknown): string {
59
56
  return typeof value === "string" ? JSON.stringify(value) : String(value);
60
57
  }
@@ -109,7 +106,7 @@ function normalizeForceBackgroundPatterns(raw: unknown): string[] {
109
106
  } catch (err) {
110
107
  logger.warn(
111
108
  `Config key 'forceBackgroundPatterns[${index}]' is not a valid regex (${item}), dropped: ` +
112
- `${err instanceof Error ? err.message : String(err)} (${getLlmSharedConfigPath(CONFIG_PKG)}).`,
109
+ `${toErrorMessage(err)} (${getLlmSharedConfigPath(CONFIG_PKG)}).`,
113
110
  );
114
111
  return;
115
112
  }
@@ -138,7 +135,7 @@ function normalizeMaxConcurrentBackground(raw: unknown): number {
138
135
  * 问题在这里就地消化)。未知键忽略(前向兼容)。
139
136
  */
140
137
  export function normalizeBaseToolEnhanceConfig(raw: unknown): BaseToolEnhanceConfig {
141
- if (!isPlainObject(raw)) {
138
+ if (!isRecord(raw)) {
142
139
  if (raw !== undefined && raw !== null) {
143
140
  logger.warn(
144
141
  `Config root is not a JSON object (${describeValue(raw)}), using all defaults ` +
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ import type {
20
20
  SessionStartEvent,
21
21
  } from "@earendil-works/pi-coding-agent";
22
22
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
23
+ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
23
24
  import { getLogger } from "@zhushanwen/pi-extension-logger";
24
25
 
25
26
  import { createBashOutputToolDefinition } from "./bash-output-tool.ts";
@@ -90,7 +91,7 @@ function runSessionStartMaintenance(
90
91
  } catch (err) {
91
92
  // 对账失败无害:僵尸 register 停留差集,下一 session_start 幂等重查
92
93
  logger.warn("session_start pending reconcile failed; zombies retried next session start", {
93
- detail: { err: err instanceof Error ? err.message : String(err) },
94
+ detail: { err: toErrorMessage(err) },
94
95
  });
95
96
  }
96
97
  }
@@ -4,8 +4,9 @@
4
4
  * 迁移约定(与原实现逐字段一致,M1 验收点):
5
5
  * - 事件名 = "tool_execution_end"(pi 0.84.1 实装无 "tool_error" 事件,工具报错以
6
6
  * ToolExecutionEndEvent.isError=true 表达——以 dist types.d.ts 为准)
7
- * - customType = "unified-hooks:tool-error"(保持原值,等价迁移不断链;unified-hooks
8
- * 整包废弃后该 entry 由本包继续产出,M6 摘除旧包时消费方无感)
7
+ * - customType = "unified-hooks:tool-error"(保持原值:无代码消费方;保持旧名是
8
+ * 历史 entry 可查询性承诺(01 号设计 P-protocol / README 承接表),废弃包名残留
9
+ * 为已接受代价——ext-simplify-13 D4)
9
10
  * - entry 形态 = { timestamp, toolName, toolCallId, errorText },errorText 取不到时 null
10
11
  *
11
12
  * 原实现的设计决策一并继承:不调 ctx.ui.notify——tool error 已在对话流里