@zhushanwen/pi-base-tool-enhance 0.5.4 → 0.6.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.
- package/package.json +7 -7
- package/src/__tests__/background-lifecycle.test.ts +9 -3
- package/src/__tests__/bash-tool.test.ts +7 -0
- package/src/__tests__/maintenance-once.test.ts +2 -3
- package/src/__tests__/notify.test.ts +82 -2
- package/src/__tests__/pending-reconcile.test.ts +1 -1
- package/src/__tests__/registry.test.ts +1 -1
- package/src/background/notify.ts +61 -9
- package/src/background/output-tail.ts +1 -1
- package/src/background/pending-reconcile.ts +2 -2
- package/src/background/poller.ts +1 -1
- package/src/background/process-exit-guard.ts +1 -1
- package/src/background/registry.ts +3 -3
- package/src/background/spawn-background.ts +1 -1
- package/src/background/task-store.ts +1 -1
- package/src/background/types.ts +5 -5
- package/src/bash-kill-tool.ts +1 -1
- package/src/bash-output-tool.ts +2 -1
- package/src/bash-tool.ts +9 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-base-tool-enhance",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Base tool enhancement - override builtin bash tool: foreground delegates to pi official factory, incremental background mode (design decisions documented in source comments)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
"skills/"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@
|
|
32
|
-
"@zhushanwen/pi-ext-guards": "0.4.
|
|
33
|
-
"@zhushanwen/pi-extension-logger": "0.6.
|
|
34
|
-
"@zhushanwen/pi-file-lock": "0.4.
|
|
35
|
-
"@zhushanwen/pi-llm-shared": "0.8.
|
|
31
|
+
"@zhushanwen/extension-protocol": "0.12.0",
|
|
32
|
+
"@zhushanwen/pi-ext-guards": "0.4.1",
|
|
33
|
+
"@zhushanwen/pi-extension-logger": "0.6.1",
|
|
34
|
+
"@zhushanwen/pi-file-lock": "0.4.1",
|
|
35
|
+
"@zhushanwen/pi-llm-shared": "0.8.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@vitest/coverage-v8": "^4.1.9",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
43
43
|
"typebox": "*",
|
|
44
|
-
"@zhushanwen/pi-pending-notifications": "0.7.
|
|
44
|
+
"@zhushanwen/pi-pending-notifications": "0.7.5"
|
|
45
45
|
},
|
|
46
46
|
"peerDependenciesMeta": {
|
|
47
47
|
"@zhushanwen/pi-pending-notifications": {
|
|
@@ -20,9 +20,9 @@ vi.mock("@earendil-works/pi-coding-agent", () => ({
|
|
|
20
20
|
// 进程原语(ext-simplify-13 后单点在 protocol 子出口)转发真实杀 + 记录调用
|
|
21
21
|
//(timeout 分支断言 killProcessTree 被调)
|
|
22
22
|
const { killTreeCalls } = vi.hoisted(() => ({ killTreeCalls: [] as number[] }));
|
|
23
|
-
vi.mock("@
|
|
23
|
+
vi.mock("@zhushanwen/extension-protocol/background-task", async (importOriginal) => {
|
|
24
24
|
const orig = await importOriginal<
|
|
25
|
-
typeof import("@
|
|
25
|
+
typeof import("@zhushanwen/extension-protocol/background-task")
|
|
26
26
|
>();
|
|
27
27
|
return {
|
|
28
28
|
...orig,
|
|
@@ -33,7 +33,7 @@ vi.mock("@taiji/extension-protocol/background-task", async (importOriginal) => {
|
|
|
33
33
|
};
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
import { isPidAlive } from "@
|
|
36
|
+
import { isPidAlive } from "@zhushanwen/extension-protocol/background-task";
|
|
37
37
|
|
|
38
38
|
import { createBashKillToolDefinition } from "../bash-kill-tool.ts";
|
|
39
39
|
import { createBashOutputToolDefinition } from "../bash-output-tool.ts";
|
|
@@ -237,6 +237,12 @@ describe("real spawn lifecycle (poll edge finalization)", () => {
|
|
|
237
237
|
});
|
|
238
238
|
|
|
239
239
|
describe("bash_output tool", () => {
|
|
240
|
+
it("description pins the auto-notify contract (on-demand checks only, DO NOT bash sleep)", () => {
|
|
241
|
+
// 与 bash description / 启动回执同一契约话术(统一 "DO NOT bash sleep")——删改必须红灯
|
|
242
|
+
expect(bashOutput.description).toContain("completion is auto-notified");
|
|
243
|
+
expect(bashOutput.description).toContain("DO NOT bash sleep");
|
|
244
|
+
});
|
|
245
|
+
|
|
240
246
|
it("list merges store + registry terminal entries, store wins on same id", async () => {
|
|
241
247
|
// 单例表:一个 running 真任务
|
|
242
248
|
const spawned = spawnBg("sleep 5");
|
|
@@ -139,6 +139,10 @@ describe("createBashOverrideToolDefinition", () => {
|
|
|
139
139
|
// 白名单自动转后台 + timeout 显式值被尊重(除白名单强转后台例外)
|
|
140
140
|
expect(tool.description).toContain("whitelist");
|
|
141
141
|
expect(tool.description).toContain("timeout in seconds");
|
|
142
|
+
// 自动通知契约("DO NOT bash sleep" 统一话术是 background-bash-notify 主线主张——
|
|
143
|
+
// 实测大量 agent bash sleep 空等后加的硬契约,删改必须红灯)
|
|
144
|
+
expect(tool.description).toContain("Completion is auto-notified");
|
|
145
|
+
expect(tool.description).toContain("DO NOT bash sleep");
|
|
142
146
|
});
|
|
143
147
|
|
|
144
148
|
it("passes through official promptSnippet/promptGuidelines (system-prompt parity)", () => {
|
|
@@ -281,6 +285,9 @@ describe("background branch routing (M2)", () => {
|
|
|
281
285
|
expect(text).toContain("pid: 12345");
|
|
282
286
|
expect(text).toContain("Output file:");
|
|
283
287
|
expect(text).toContain('bash_output {task_id:"bt-1724589012-a3f7"}');
|
|
288
|
+
// 启动回执同钉自动通知契约(与 description 同一话术,startBackgroundAndReply 单点拼装)
|
|
289
|
+
expect(text).toContain("Completion is auto-notified");
|
|
290
|
+
expect(text).toContain("DO NOT bash sleep");
|
|
284
291
|
});
|
|
285
292
|
|
|
286
293
|
it("background spawn failure surfaces the error", async () => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// src/__tests__/maintenance-once.test.ts —— 收殓下沉后的 session_start 维护链
|
|
2
|
-
// 语义(实施单元 u-bte-remove
|
|
3
|
-
//
|
|
4
|
-
// 「批 2 后 reap 类操作不再执行」):
|
|
2
|
+
// 语义(实施单元 u-bte-remove:extension 删 session_start reaper,批 2 后
|
|
3
|
+
// reap 类操作不再执行):
|
|
5
4
|
// - reconcilePendingEntries 是 session 级豁免类,每 session_start 派发都执行
|
|
6
5
|
// (startup/resume/new 多派发 ×N——含 factory 二调 handler 累积的真实形态)
|
|
7
6
|
// - 触发面消失:reaper.ts 模块文件已删除(文件级断言,见首条用例)+ 维护链不再触发
|
|
@@ -8,10 +8,11 @@ import { join } from "node:path";
|
|
|
8
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
10
|
|
|
11
|
-
import { killProcessTree } from "@
|
|
11
|
+
import { killProcessTree } from "@zhushanwen/extension-protocol/background-task";
|
|
12
12
|
import {
|
|
13
13
|
BACKGROUND_BASH_CUSTOM_TYPE,
|
|
14
14
|
buildNotificationContent,
|
|
15
|
+
buildNotifyDetails,
|
|
15
16
|
emitPendingRegister,
|
|
16
17
|
handleTaskExit,
|
|
17
18
|
refreshPiReference,
|
|
@@ -46,6 +47,12 @@ function attach(pi: MockPi): void {
|
|
|
46
47
|
refreshPiReference(pi as unknown as ExtensionAPI);
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/** 取第一条 sendMessage 的首参数对象(D1 details 断言入口)。 */
|
|
51
|
+
function sentMessage(pi: MockPi): { content: string; details: unknown } {
|
|
52
|
+
const [message] = pi.sendMessage.mock.calls[0] as [{ content: string; details: unknown }, unknown];
|
|
53
|
+
return message;
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
function sleep(ms: number): Promise<void> {
|
|
50
57
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
51
58
|
}
|
|
@@ -187,7 +194,7 @@ describe("exit-edge notification (⑧⑨, poll edge wiring)", () => {
|
|
|
187
194
|
});
|
|
188
195
|
expect(pi.sendMessage).toHaveBeenCalledTimes(1);
|
|
189
196
|
const [message, options] = pi.sendMessage.mock.calls[0] as [
|
|
190
|
-
{ customType: string; content: string; display: boolean },
|
|
197
|
+
{ customType: string; content: string; display: boolean; details: Record<string, unknown> },
|
|
191
198
|
{ deliverAs: string; triggerTurn: boolean },
|
|
192
199
|
];
|
|
193
200
|
expect(message.customType).toBe(BACKGROUND_BASH_CUSTOM_TYPE);
|
|
@@ -196,6 +203,15 @@ describe("exit-edge notification (⑧⑨, poll edge wiring)", () => {
|
|
|
196
203
|
expect(message.content).toContain("exit 0");
|
|
197
204
|
expect(message.content).toContain("done"); // tail 摘要
|
|
198
205
|
expect(options).toEqual({ deliverAs: "steer", triggerTurn: true });
|
|
206
|
+
// U1(D1):真实轮询边沿投递的 details 与终态条目逐字段一致(非 fixture 常量)
|
|
207
|
+
const finalizedEntry = getTask(task.taskId);
|
|
208
|
+
expect(message.details).toMatchObject({
|
|
209
|
+
taskId: task.taskId,
|
|
210
|
+
command: "sleep 0.3 && echo done",
|
|
211
|
+
endReason: "natural",
|
|
212
|
+
exitCode: 0,
|
|
213
|
+
});
|
|
214
|
+
expect((message.details as { durationMs: number }).durationMs).toBe(finalizedEntry?.durationMs);
|
|
199
215
|
});
|
|
200
216
|
|
|
201
217
|
it("killed: emits unregister reason 'cancelled' but does NOT sendMessage (single-point rule)", async () => {
|
|
@@ -285,6 +301,70 @@ describe("notification content (§3.1 sample)", () => {
|
|
|
285
301
|
});
|
|
286
302
|
});
|
|
287
303
|
|
|
304
|
+
describe("notification details (D1 schema: taskId/command/durationMs/endReason/exitCode)", () => {
|
|
305
|
+
it("natural: details mirror the terminal task fields; content stays byte-identical (LLM payload)", () => {
|
|
306
|
+
const pi = createMockPi();
|
|
307
|
+
attach(pi);
|
|
308
|
+
const task = finalizedTask();
|
|
309
|
+
handleTaskExit(task);
|
|
310
|
+
|
|
311
|
+
const message = sentMessage(pi);
|
|
312
|
+
expect(message.details).toEqual({
|
|
313
|
+
taskId: task.taskId,
|
|
314
|
+
command: task.command,
|
|
315
|
+
durationMs: task.durationMs,
|
|
316
|
+
endReason: "natural",
|
|
317
|
+
exitCode: task.exitCode,
|
|
318
|
+
});
|
|
319
|
+
// 键级断言:D1 恰五字段,防字段悄悄混入(register emit 同款范式)
|
|
320
|
+
expect(Object.keys(message.details as Record<string, unknown>).sort()).toEqual([
|
|
321
|
+
"command",
|
|
322
|
+
"durationMs",
|
|
323
|
+
"endReason",
|
|
324
|
+
"exitCode",
|
|
325
|
+
"taskId",
|
|
326
|
+
]);
|
|
327
|
+
// content 是 triggerTurn:true 给 LLM 接力的载荷:挂 details 不得改动一个字节
|
|
328
|
+
expect(message.content).toBe(
|
|
329
|
+
"[background-bash] bt-1700000000-test01 finished (exit 0, 3m12s): pnpm test\n" +
|
|
330
|
+
"Last lines: Tests: 42 passed\n" +
|
|
331
|
+
'Full output: /tmp/out/bt-1700000000-test01.log; use bash_output {task_id:"bt-1700000000-test01"} for details.',
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("natural nonzero exit: exitCode passes through unchanged, endReason stays 'natural'", () => {
|
|
336
|
+
const pi = createMockPi();
|
|
337
|
+
attach(pi);
|
|
338
|
+
handleTaskExit(finalizedTask({ exitCode: 3, durationMs: 5000 }));
|
|
339
|
+
|
|
340
|
+
expect(sentMessage(pi).details).toEqual({
|
|
341
|
+
taskId: "bt-1700000000-test01",
|
|
342
|
+
command: "pnpm test",
|
|
343
|
+
durationMs: 5000,
|
|
344
|
+
endReason: "natural",
|
|
345
|
+
exitCode: 3,
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("timeout: endReason 'timeout' + exitCode null (D1 可空字段的 timeout 形态)", () => {
|
|
350
|
+
const pi = createMockPi();
|
|
351
|
+
attach(pi);
|
|
352
|
+
handleTaskExit(finalizedTask({ reason: "timeout", exitCode: null, durationMs: 15_000 }));
|
|
353
|
+
|
|
354
|
+
expect(sentMessage(pi).details).toEqual({
|
|
355
|
+
taskId: "bt-1700000000-test01",
|
|
356
|
+
command: "pnpm test",
|
|
357
|
+
durationMs: 15_000,
|
|
358
|
+
endReason: "timeout",
|
|
359
|
+
exitCode: null,
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("durationMs 缺省归 0:必需字段恒为 number(消费侧解析器才不判 null)", () => {
|
|
364
|
+
expect(buildNotifyDetails(finalizedTask({ durationMs: undefined })).durationMs).toBe(0);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
288
368
|
describe("D17: pi reference refresh (session replacement takeover)", () => {
|
|
289
369
|
it("second load wins: notification goes through the NEW reference, old one untouched", () => {
|
|
290
370
|
const piA = createMockPi();
|
|
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
|
|
6
6
|
import { afterEach, describe, expect, it } from "vitest";
|
|
7
7
|
|
|
8
|
-
import { MAX_TERMINAL_REGISTRY_ENTRIES } from "@
|
|
8
|
+
import { MAX_TERMINAL_REGISTRY_ENTRIES } from "@zhushanwen/extension-protocol";
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
getRegistryPath,
|
package/src/background/notify.ts
CHANGED
|
@@ -48,6 +48,16 @@ export const BACKGROUND_BASH_CUSTOM_TYPE = "background-bash";
|
|
|
48
48
|
*/
|
|
49
49
|
const PENDING_NAME_LIMIT = 80;
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* 命令文案截断(pending register 的 name 与完成通知 head 行共用同一口径):
|
|
53
|
+
* 超 PENDING_NAME_LIMIT 取前 80 字符 + 省略号。
|
|
54
|
+
*/
|
|
55
|
+
function truncateCommand(command: string): string {
|
|
56
|
+
return command.length > PENDING_NAME_LIMIT
|
|
57
|
+
? `${command.slice(0, PENDING_NAME_LIMIT)}…`
|
|
58
|
+
: command;
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
/** 模块级「当前 pi 引用」(D17 核心可变状态,见文件头)。 */
|
|
52
62
|
let currentPi: ExtensionAPI | undefined;
|
|
53
63
|
|
|
@@ -100,10 +110,7 @@ export function toPendingReason(
|
|
|
100
110
|
export function emitPendingRegister(task: BackgroundTask): void {
|
|
101
111
|
const pi = currentPi;
|
|
102
112
|
if (pi === undefined) return;
|
|
103
|
-
const name =
|
|
104
|
-
task.command.length > PENDING_NAME_LIMIT
|
|
105
|
-
? `${task.command.slice(0, PENDING_NAME_LIMIT)}…`
|
|
106
|
-
: task.command;
|
|
113
|
+
const name = truncateCommand(task.command);
|
|
107
114
|
try {
|
|
108
115
|
pi.events.emit("pending:register", { id: task.taskId, type: "bash", name });
|
|
109
116
|
} catch (err) {
|
|
@@ -153,7 +160,12 @@ function sendTaskFinishedMessage(task: BackgroundTask): void {
|
|
|
153
160
|
if (pi === undefined) return;
|
|
154
161
|
try {
|
|
155
162
|
pi.sendMessage(
|
|
156
|
-
{
|
|
163
|
+
{
|
|
164
|
+
customType: BACKGROUND_BASH_CUSTOM_TYPE,
|
|
165
|
+
content: buildNotificationContent(task),
|
|
166
|
+
display: true,
|
|
167
|
+
details: buildNotifyDetails(task),
|
|
168
|
+
},
|
|
157
169
|
{ deliverAs: "steer", triggerTurn: true },
|
|
158
170
|
);
|
|
159
171
|
} catch (err) {
|
|
@@ -179,10 +191,7 @@ function sendTaskFinishedMessage(task: BackgroundTask): void {
|
|
|
179
191
|
*/
|
|
180
192
|
export function buildNotificationContent(task: BackgroundTask): string {
|
|
181
193
|
const duration = formatDurationMs(task.durationMs ?? 0);
|
|
182
|
-
const command =
|
|
183
|
-
task.command.length > PENDING_NAME_LIMIT
|
|
184
|
-
? `${task.command.slice(0, PENDING_NAME_LIMIT)}…`
|
|
185
|
-
: task.command;
|
|
194
|
+
const command = truncateCommand(task.command);
|
|
186
195
|
let head: string;
|
|
187
196
|
if (task.reason === "timeout") {
|
|
188
197
|
head = `[background-bash] ${task.taskId} timed out (${duration}): ${command}`;
|
|
@@ -199,6 +208,49 @@ export function buildNotificationContent(task: BackgroundTask): string {
|
|
|
199
208
|
return lines.join("\n");
|
|
200
209
|
}
|
|
201
210
|
|
|
211
|
+
/**
|
|
212
|
+
* sendMessage details 载荷(设计 §3.3 D1 schema)。
|
|
213
|
+
*
|
|
214
|
+
* 为什么另立 details 而非让上层解析 content:content 是本模块内部模板字符串、写给
|
|
215
|
+
* LLM 接力的日志行,格式漂移即静默打破解析;details 经既有透传管道(event-adapter
|
|
216
|
+
* → registry → reducer → Message.details)结构化到达渲染层,content 原文对 LLM 逐
|
|
217
|
+
* 字节不变。
|
|
218
|
+
*
|
|
219
|
+
* 字面量镜像:消费侧解析器 `packages/shared/src/message.ts` 的
|
|
220
|
+
* `parseBackgroundBashDetails`(本包是独立 npm 包,不 import taiji 内部 shared)——
|
|
221
|
+
* 字段名、可空性、枚举值三处必须逐字对齐,改动须同步另一侧。
|
|
222
|
+
*
|
|
223
|
+
* 枚举收敛:`endReason` 只落 natural | timeout——killed 在 handleTaskExit 提前返回、
|
|
224
|
+
* process-exit 走收殓路径(只 emit 不发消息、根本不进本函数),两者均不可达(D1
|
|
225
|
+
* 源码核实),不为不可达形态造死值。
|
|
226
|
+
*/
|
|
227
|
+
export interface BackgroundBashDetails {
|
|
228
|
+
taskId: string;
|
|
229
|
+
command: string;
|
|
230
|
+
/** 存活时长(毫秒),与 content 中的耗时同源同口径。 */
|
|
231
|
+
durationMs: number;
|
|
232
|
+
endReason: "natural" | "timeout";
|
|
233
|
+
/** 无退出码时为 null(timeout / 被信号终止语义,D1 可空字段)。 */
|
|
234
|
+
exitCode: number | null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 完成通知 details(D1):终态字段与 buildNotificationContent 同源——durationMs
|
|
239
|
+
* 同用 `?? 0` 归一(终态条目必已物化,缺省仅防御)。
|
|
240
|
+
*
|
|
241
|
+
* command 取原始终态值不截断:截断(PENDING_NAME_LIMIT)是 content 行宽约束,结构化
|
|
242
|
+
* 消费方的行宽由渲染层自己决定(旧数据无 details 时仍走 content 原文兜底)。
|
|
243
|
+
*/
|
|
244
|
+
export function buildNotifyDetails(task: BackgroundTask): BackgroundBashDetails {
|
|
245
|
+
return {
|
|
246
|
+
taskId: task.taskId,
|
|
247
|
+
command: task.command,
|
|
248
|
+
durationMs: task.durationMs ?? 0,
|
|
249
|
+
endReason: task.reason === "timeout" ? "timeout" : "natural",
|
|
250
|
+
exitCode: task.exitCode ?? null,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
202
254
|
/** 耗时换算常量(毫秒/秒/分/时 + 时分两位补零宽度)。 */
|
|
203
255
|
const MS_PER_SECOND = 1000;
|
|
204
256
|
const SECONDS_PER_MINUTE = 60;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - readTailSummary:exit 边沿的 tail 摘要(存条目 / M3 通知用)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { readOutputTail as readOutputTailPrimitive } from "@
|
|
11
|
+
import { readOutputTail as readOutputTailPrimitive } from "@zhushanwen/extension-protocol/background-task";
|
|
12
12
|
|
|
13
13
|
/** pi 内置 bash 同款截断上限(last 2000 lines / 50KB = 51200 bytes,先到为准)。 */
|
|
14
14
|
export const TAIL_MAX_LINES = 2000;
|
|
@@ -35,8 +35,8 @@ import {
|
|
|
35
35
|
BACKGROUND_TASK_ID_PREFIX,
|
|
36
36
|
collectActivePendingIds,
|
|
37
37
|
mapReasonToStatus,
|
|
38
|
-
} from "@
|
|
39
|
-
import { isPidAlive } from "@
|
|
38
|
+
} from "@zhushanwen/extension-protocol";
|
|
39
|
+
import { isPidAlive } from "@zhushanwen/extension-protocol/background-task";
|
|
40
40
|
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
41
41
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
42
42
|
|
package/src/background/poller.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* 可查、对账可补),不加同步握手。
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { isPidAlive } from "@
|
|
16
|
+
import { isPidAlive } from "@zhushanwen/extension-protocol/background-task";
|
|
17
17
|
|
|
18
18
|
import { readTailSummary } from "./output-tail.ts";
|
|
19
19
|
import { readRegistry, taskToRegistryEntry, writeRegistryEntry } from "./registry.ts";
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* 本单元不管。
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import { killProcessTree } from "@
|
|
30
|
+
import { killProcessTree } from "@zhushanwen/extension-protocol/background-task";
|
|
31
31
|
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
32
32
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
33
33
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 写入。
|
|
8
8
|
*
|
|
9
9
|
* 行为原语单点(ext-simplify-13):解析防御 / corrupt 隔离 / 原子写 / 序列化 /
|
|
10
|
-
* 终态 LRU 裁剪自 @
|
|
10
|
+
* 终态 LRU 裁剪自 @zhushanwen/extension-protocol background-task 子出口引入——此前
|
|
11
11
|
* 本地实现与 runtime 侧逐字同构、靠注释对齐;本模块只保留 bte 形态薄壳:Map 投影
|
|
12
12
|
* readRegistry 与锁内 RMW 写壳。
|
|
13
13
|
*
|
|
@@ -26,14 +26,14 @@ import { join } from "node:path";
|
|
|
26
26
|
|
|
27
27
|
// 契约常量(LRU 上限)走 index 出口(background-task.ts 契约面);行为原语走
|
|
28
28
|
// background-task 子出口(含 node 内建依赖,不进 index——renderer/core 零触达)
|
|
29
|
-
import { MAX_TERMINAL_REGISTRY_ENTRIES } from "@
|
|
29
|
+
import { MAX_TERMINAL_REGISTRY_ENTRIES } from "@zhushanwen/extension-protocol";
|
|
30
30
|
import {
|
|
31
31
|
atomicWriteRegistry,
|
|
32
32
|
readRegistry as readRegistryEntries,
|
|
33
33
|
serializeRegistryFile,
|
|
34
34
|
trimTerminalEntries,
|
|
35
35
|
type RegistryFileLogFn,
|
|
36
|
-
} from "@
|
|
36
|
+
} from "@zhushanwen/extension-protocol/background-task";
|
|
37
37
|
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
38
38
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
39
39
|
import { withFileLockSync } from "@zhushanwen/pi-file-lock";
|
|
@@ -17,7 +17,7 @@ import { mkdirSync, openSync, closeSync, accessSync, constants } from "node:fs";
|
|
|
17
17
|
import { dirname, join } from "node:path";
|
|
18
18
|
import { randomBytes } from "node:crypto";
|
|
19
19
|
|
|
20
|
-
import { getProcessStartTimeSec, killProcessTree, pidStartMatchesRegistered } from "@
|
|
20
|
+
import { getProcessStartTimeSec, killProcessTree, pidStartMatchesRegistered } from "@zhushanwen/extension-protocol/background-task";
|
|
21
21
|
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
22
22
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
23
23
|
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* 未来新增写路径(如 maintenance 类)必须保持该顺序,违反即 intent 可丢失。
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import { trimTerminalEntries } from "@
|
|
31
|
+
import { trimTerminalEntries } from "@zhushanwen/extension-protocol/background-task";
|
|
32
32
|
|
|
33
33
|
import {
|
|
34
34
|
isActiveState,
|
package/src/background/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* background
|
|
2
|
+
* background 任务核心数据模型。
|
|
3
3
|
*
|
|
4
4
|
* 两层存储分工:
|
|
5
5
|
* - 单例任务表(task-store.ts,模块级 Map)= 运行时权威;条目唯一来源 = 本进程
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 条目记 ownerPiPid(M2 只负责写入)
|
|
9
9
|
*
|
|
10
10
|
* 契约改引(u-bte-remove):registry 持久化形状(RegistryEntry / 状态机 / reason
|
|
11
|
-
* 枚举 / active·terminal 判定)与 @
|
|
11
|
+
* 枚举 / active·terminal 判定)与 @zhushanwen/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 "@zhushanwen/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 "@zhushanwen/extension-protocol";
|
|
30
30
|
export {
|
|
31
31
|
isActiveBackgroundTaskState as isActiveState,
|
|
32
32
|
isTerminalBackgroundTaskState as isTerminalState,
|
|
33
|
-
} from "@
|
|
33
|
+
} from "@zhushanwen/extension-protocol";
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* killing intent:bash_kill / 后台 timeout 已发令、轮询器 exit 边沿未确认的瞬态标记。
|
package/src/bash-kill-tool.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
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 "@
|
|
21
|
+
import { getProcessStartTimeSec, isPidAlive, killProcessTree } from "@zhushanwen/extension-protocol/background-task";
|
|
22
22
|
import { Type } from "typebox";
|
|
23
23
|
|
|
24
24
|
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
package/src/bash-output-tool.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
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 "@
|
|
17
|
+
import { readOutputTail } from "@zhushanwen/extension-protocol/background-task";
|
|
18
18
|
import { Type } from "typebox";
|
|
19
19
|
|
|
20
20
|
import { TAIL_MAX_BYTES, TAIL_MAX_LINES } from "./background/output-tail.ts";
|
|
@@ -32,6 +32,7 @@ const JSON_INDENT = 2;
|
|
|
32
32
|
|
|
33
33
|
const BASH_OUTPUT_DESCRIPTION = [
|
|
34
34
|
"Fetch output and status of a background bash task started with bash {background:true}.",
|
|
35
|
+
"On-demand checks only: completion is auto-notified (steer wakes your next turn), so DO NOT bash sleep or call this tool in a wait loop.",
|
|
35
36
|
"Provide task_id to get task detail: state (running|killing|exited|orphaned), exitCode, reason, duration and tail output (last 2000 lines / 50KB).",
|
|
36
37
|
'reason "killed" means the task was terminated by the user from the UI — do not restart the command unless explicitly asked.',
|
|
37
38
|
'reason "orphaned" means the owner process died before the task finished — the task is no longer supervised.',
|
package/src/bash-tool.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* bash 工具同名 override
|
|
2
|
+
* bash 工具同名 override 定义。
|
|
3
3
|
*
|
|
4
4
|
* 前台行为 100% 委托 pi 官方工厂 createBashToolDefinition——override 后工具的全部
|
|
5
5
|
* 行为归本包负责,前台语义必须跟随 pi 版本升级而不是自研复刻(自研 spawn 会与
|
|
@@ -54,6 +54,9 @@ const enhancedBashSchema = Type.Object({
|
|
|
54
54
|
* 1. background:true 语义(立即返回 task_id,bash_output 查询 / bash_kill 终止)
|
|
55
55
|
* 2. 白名单命中命令自动转后台(即使未要求 background)
|
|
56
56
|
* 3. timeout 单位秒、显式填写会被尊重(唯一例外:白名单强转后台时忽略,D13)
|
|
57
|
+
* 4. 自动通知契约(统一话术 "DO NOT bash sleep"):任务完成 steer 自动唤醒新
|
|
58
|
+
* turn(notify.ts),LLM 文案必须传达「不要 sleep 空等」——实测大量 agent
|
|
59
|
+
* 启动后台任务后跑 bash sleep 轮询空等,机制在而契约不在所致
|
|
57
60
|
*
|
|
58
61
|
* bash_output / bash_kill 工具由 M2 单元注册,description 先行提及(D2/§3.5 要求)。
|
|
59
62
|
*/
|
|
@@ -62,10 +65,11 @@ const ENHANCED_BASH_DESCRIPTION = [
|
|
|
62
65
|
"Output is truncated to last 2000 lines or 50KB (whichever is hit first). If truncated, full output is saved to a temp file.",
|
|
63
66
|
"Optionally provide a timeout in seconds; an explicit timeout is respected, except when a whitelisted long-running command is force-routed to background. If no timeout is given, a configured default timeout applies when set.",
|
|
64
67
|
"",
|
|
65
|
-
"Background mode: set background: true to start the command without waiting for it. The tool returns immediately with a task_id, the pid, and an output file path
|
|
66
|
-
"
|
|
68
|
+
"Background mode: set background: true to start the command without waiting for it. The tool returns immediately with a task_id, the pid, and an output file path.",
|
|
69
|
+
"Completion is auto-notified: when the task finishes, a steer message with its output tail wakes your next turn — DO NOT bash sleep (e.g. run `sleep 30` in another bash call) or busy-wait while it runs. Do useful non-overlapping work, otherwise just stop and let the notification arrive.",
|
|
70
|
+
"bash_output {task_id} is for on-demand progress checks only (omit task_id to list known background tasks); never use it as a wait loop. Terminate a task with bash_kill {task_id}.",
|
|
67
71
|
"",
|
|
68
|
-
"Commands matching the force-background whitelist (test suites, dev servers, watch jobs and similar long-running commands) are automatically routed to background even when background was not requested;
|
|
72
|
+
"Commands matching the force-background whitelist (test suites, dev servers, watch jobs and similar long-running commands) are automatically routed to background even when background was not requested; the result carries a task_id and completion auto-notifies — the same DO NOT bash sleep rule applies.",
|
|
69
73
|
].join("\n");
|
|
70
74
|
|
|
71
75
|
/**
|
|
@@ -101,7 +105,7 @@ function startBackgroundAndReply(
|
|
|
101
105
|
`task_id: ${task.taskId} pid: ${task.pid}`,
|
|
102
106
|
`Output file: ${task.outputFile}`,
|
|
103
107
|
...extraNotes,
|
|
104
|
-
`
|
|
108
|
+
`Completion is auto-notified — DO NOT bash sleep while waiting. On-demand check: bash_output {task_id:"${task.taskId}"} (or omit task_id to list all tasks); terminate with bash_kill {task_id:"${task.taskId}"}.`,
|
|
105
109
|
].join("\n"),
|
|
106
110
|
},
|
|
107
111
|
],
|