@zhushanwen/pi-goal 0.4.0 → 0.4.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-goal",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Codex-style /goal command for Pi — persistent goal-driven autonomous loop with evidence-based completion, token/time budgets, blocked detection, and steering templates.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -10,7 +10,7 @@
10
10
  * 用 fake pi + fake ctx(不 import Pi SDK)。handler 签名 (pi, session, ctx) → void/result。
11
11
  */
12
12
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
13
- import { describe, expect, it } from "vitest";
13
+ import { describe, expect, it, vi } from "vitest";
14
14
 
15
15
  import { handleAgentEnd } from "../adapters/event-handlers/agent-end";
16
16
  import { handleBeforeAgentStart } from "../adapters/event-handlers/before-agent-start";
@@ -336,4 +336,75 @@ describe("handleBeforeAgentStart", () => {
336
336
 
337
337
  expect(result).toBeUndefined();
338
338
  });
339
+
340
+ // ── pending-notifications(W2 跨扩展集成)───────────────────
341
+
342
+ it("有活跃 pending operations → 注入提示含 pending 数量", async () => {
343
+ const { pi } = makeFakePi();
344
+ // 创建带 pending:register entries 的 ctx
345
+ const entries = [
346
+ { customType: "pending:register", data: { id: "wf-1", type: "workflow", name: "test-wf" } },
347
+ { customType: "pending:register", data: { id: "bg-1", type: "subagent", name: "worker" } },
348
+ ];
349
+ const ctx = {
350
+ hasUI: true,
351
+ signal: { aborted: false } as AbortSignal,
352
+ getContextUsage: () => undefined,
353
+ ui: {
354
+ notify: vi.fn(),
355
+ setStatus: vi.fn(),
356
+ setWidget: vi.fn(),
357
+ theme: { fg: (_c: string, t: string) => t, bold: (t: string) => t },
358
+ },
359
+ sessionManager: { getEntries: () => entries, getBranch: () => undefined },
360
+ } as unknown as ExtensionContext;
361
+ const session = createGoalSession();
362
+ session.state = makeRunningState();
363
+
364
+ const result = await handleBeforeAgentStart(pi, session, ctx);
365
+
366
+ expect(result).toBeDefined();
367
+ expect(result!.message.customType).toBe("goal-context");
368
+ expect(result!.message.content).toContain("2 pending async operation(s)");
369
+ });
370
+
371
+ it("有 pending register 但已 unregister → 不注入提示", async () => {
372
+ const { pi } = makeFakePi();
373
+ const entries = [
374
+ { customType: "pending:register", data: { id: "wf-1", type: "workflow", name: "test-wf" } },
375
+ { customType: "pending:unregister", data: { id: "wf-1" } },
376
+ ];
377
+ const ctx = {
378
+ hasUI: true,
379
+ signal: { aborted: false } as AbortSignal,
380
+ getContextUsage: () => undefined,
381
+ ui: {
382
+ notify: vi.fn(),
383
+ setStatus: vi.fn(),
384
+ setWidget: vi.fn(),
385
+ theme: { fg: (_c: string, t: string) => t, bold: (t: string) => t },
386
+ },
387
+ sessionManager: { getEntries: () => entries, getBranch: () => undefined },
388
+ } as unknown as ExtensionContext;
389
+ const session = createGoalSession();
390
+ session.state = makeRunningState();
391
+
392
+ const result = await handleBeforeAgentStart(pi, session, ctx);
393
+
394
+ expect(result).toBeDefined();
395
+ expect(result!.message.customType).toBe("goal-context");
396
+ expect(result!.message.content).not.toContain("pending async operation");
397
+ });
398
+
399
+ it("无 pending entries → 正常注入(不含 pending 提示)", async () => {
400
+ const { pi } = makeFakePi();
401
+ const { ctx } = makeFakeCtx(); // 默认 getEntries: () => []
402
+ const session = createGoalSession();
403
+ session.state = makeRunningState();
404
+
405
+ const result = await handleBeforeAgentStart(pi, session, ctx);
406
+
407
+ expect(result).toBeDefined();
408
+ expect(result!.message.content).not.toContain("pending async operation");
409
+ });
339
410
  });
@@ -40,6 +40,14 @@ export async function handleBeforeAgentStart(
40
40
  ): Promise<BeforeAgentStartResult | undefined> {
41
41
  if (!session.state) return;
42
42
 
43
+ pi.appendEntry("goal:log", {
44
+ timestamp: Date.now(),
45
+ level: "debug",
46
+ component: "goal:before-agent-start",
47
+ message: "handleBeforeAgentStart invoked",
48
+ data: { status: session.state.status },
49
+ });
50
+
43
51
  // 终态处理
44
52
  if (isTerminalStatus(session.state.status)) {
45
53
  handleTerminalStateBeforeAgent(pi, session, ctx);
@@ -53,10 +61,44 @@ export async function handleBeforeAgentStart(
53
61
 
54
62
  // 正常 context injection。
55
63
  // 全解耦:planAvailable 恒 true(contextInjectionPrompt 恒定建议 plan mode,AI 自行决定)。
64
+ // pending-notifications:若有活跃的异步操作,注入等待提示。
65
+ // 读取 pending:register/unregister entry 算差集,并校验 expiresAt(TTL 过期的视为非活跃)。
66
+ // pending-notifications 的 rebuildFromEntries 在其 session_start 已 flush 跨 session/过期残留,
67
+ // 但同一 session 内超时未 unregister 的 entry(如崩溃恢复遗漏)需此处完补以防误报 active。
68
+ const now = Date.now();
69
+ const entries = ctx.sessionManager.getEntries();
70
+ const pendingUnregisters = new Set(
71
+ entries.filter((e) => e.customType === "pending:unregister").map((e) => (e.data as Record<string, unknown>)?.id),
72
+ );
73
+ const activePending = entries.filter((e) => {
74
+ if (e.customType !== "pending:register") return false;
75
+ const id = (e.data as Record<string, unknown>)?.id;
76
+ if (pendingUnregisters.has(id)) return false;
77
+ // TTL 过期视为非活跃(expiresAt 缺失或类型错误时宽容处理为活跃)
78
+ const expiresAt = (e.data as Record<string, unknown>)?.expiresAt;
79
+ if (typeof expiresAt === "number" && expiresAt <= now) return false;
80
+ return true;
81
+ });
82
+ const pendingHint = activePending.length > 0
83
+ ? `\nNote: There are ${activePending.length} pending async operation(s) running. Consider waiting for them to complete before starting new work.`
84
+ : "";
85
+
86
+ pi.appendEntry("goal:log", {
87
+ timestamp: Date.now(),
88
+ level: "debug",
89
+ component: "goal:before-agent-start",
90
+ message: "pending entries computed",
91
+ data: {
92
+ activePending: activePending.length,
93
+ injectHint: activePending.length > 0,
94
+ pendingIds: activePending.map((e) => (e.data as Record<string, unknown>)?.id),
95
+ },
96
+ });
97
+
56
98
  return {
57
99
  message: {
58
100
  customType: "goal-context",
59
- content: contextInjectionPrompt(session.state, session.state.timeUsedSeconds, true),
101
+ content: contextInjectionPrompt(session.state, session.state.timeUsedSeconds, true) + pendingHint,
60
102
  display: false,
61
103
  },
62
104
  };