@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.
- package/README.md +2 -2
- package/package.json +7 -6
- package/skills/base-tool-enhance-ext-config/SKILL.md +3 -3
- package/src/__tests__/background-lifecycle.test.ts +11 -7
- package/src/__tests__/index.test.ts +3 -5
- package/src/__tests__/maintenance-once.test.ts +2 -2
- package/src/__tests__/notify.test.ts +1 -1
- package/src/__tests__/pending-reconcile.test.ts +98 -33
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/subagent-guard.test.ts +29 -0
- package/src/background/notify.ts +4 -3
- package/src/background/output-tail.ts +9 -65
- package/src/background/pending-reconcile.ts +33 -69
- package/src/background/poller.ts +2 -1
- package/src/background/process-exit-guard.ts +11 -5
- package/src/background/registry.ts +38 -126
- package/src/background/spawn-background.ts +18 -6
- package/src/background/subagent-guard.ts +11 -15
- package/src/background/task-store.ts +13 -8
- package/src/background/types.ts +4 -4
- package/src/bash-kill-tool.ts +21 -6
- package/src/bash-output-tool.ts +7 -3
- package/src/bash-tool.ts +3 -0
- package/src/config.ts +3 -6
- package/src/index.ts +4 -3
- package/src/tool-error-audit.ts +3 -2
- package/src/__tests__/kill-tree.test.ts +0 -161
- package/src/kill-tree.ts +0 -151
|
@@ -8,14 +8,18 @@
|
|
|
8
8
|
* ③ fork 后任务完成通知写进新 session,旧 session 文件的 register 成僵尸。
|
|
9
9
|
*
|
|
10
10
|
* 收尾写法(权威路径):直接 pi.appendEntry("pending:unregister", {id, reason,
|
|
11
|
-
* status})
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
|
126
|
+
* 收尾 reason 映射(reason→status 的第二跳在写点经 protocol mapReasonToStatus 单点):
|
|
163
127
|
* - exited:按条目 reason/exitCode 走 toPendingReason(与 exit 边沿 emit 同一映射,
|
|
164
|
-
* 两路径写出的 entry 语义一致);reason 缺失按
|
|
165
|
-
*
|
|
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" {
|
package/src/background/poller.ts
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* 可查、对账可补),不加同步握手。
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { isPidAlive } from "
|
|
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 事件**,同步收殓在该路径本就不可达;孤儿由
|
|
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 目录由
|
|
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
|
-
|
|
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:
|
|
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 后 =
|
|
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
|
|
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 收殓器写终态用
|
|
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 {
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 {
|
|
41
|
+
import type { BackgroundTask, RegistryEntry } from "./types.ts";
|
|
36
42
|
|
|
37
43
|
const logger = getLogger("base-tool-enhance");
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
187
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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)单点定义于
|
|
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
|
|
2
|
+
* subagent 进程识别(bte background 降级判据——生命周期设计 D14 项,commit 8599abd0c)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 后 =
|
|
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
|
-
/**
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
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
|
|
package/src/background/types.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 条目记 ownerPiPid(M2 只负责写入)
|
|
9
9
|
*
|
|
10
10
|
* 契约改引(u-bte-remove):registry 持久化形状(RegistryEntry / 状态机 / reason
|
|
11
|
-
* 枚举 / active·terminal 判定)与 @
|
|
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 "@
|
|
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 "@
|
|
29
|
+
} from "@taiji/extension-protocol";
|
|
30
30
|
export {
|
|
31
31
|
isActiveBackgroundTaskState as isActiveState,
|
|
32
32
|
isTerminalBackgroundTaskState as isTerminalState,
|
|
33
|
-
} from "@
|
|
33
|
+
} from "@taiji/extension-protocol";
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* killing intent:bash_kill / 后台 timeout 已发令、轮询器 exit 边沿未确认的瞬态标记。
|