@zhushanwen/pi-system-prompt-trace 0.1.2 → 0.1.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 CHANGED
@@ -35,14 +35,16 @@ appendEntry("xyz:system-prompt", {
35
35
 
36
36
  存在任一 hash 基线(见下)时,首个 turn 写 `resume`(无论 session_start reason)。
37
37
 
38
- ## 跨重启 hash 基线三路径(优先级从高到低)
38
+ ## 跨重启 hash 基线四路径(优先级从高到低,权威实现见 `src/trace.ts` onSessionStart)
39
39
 
40
- 1. **进程内 resume**:`session_before_switch.targetSessionFile` 直读目标 session 文件,取最后一条留痕 entry 的
41
- hash/version/fullText。switch 会 teardown 并重建 extension runtime,该基线经模块级 stash 跨 runtime 传递。
42
- 2. **app 重启直 spawn resume**:`<agentDir>/system-prompt-trace-baseline.json` 自持久化小文件(sessionId
43
- hash/version),原子写入,保留最近 64 个 session。
44
- 3. **兜底**:两路都读不到且 reason=resume 必写一条(宁可多写不可漏记)。
40
+ 1. **进程内 resume(stash)**:`session_before_switch.targetSessionFile` 直读目标 session 文件,取最后一条留痕
41
+ entry 的 hash/version/fullText。switch 会 teardown 并重建 extension runtime,该基线经模块级 stash 跨 runtime 传递。
42
+ 2. **fork**:`session_start.previousSessionFile` 直读源 session 文件(暂定语义,待 P2 探针实测定)。
43
+ 3. **自持久化小文件**:`<agentDir>/system-prompt-trace-baseline.json`(sessionId → hash/version),原子写入,
44
+ 保留最近 64 个 session。app 重启直 spawn resume / reload 等没有 switch 事件的链路由此恢复基线。
45
+ 4. **兜底必写**:三路都 miss(无任何基线)时,首个 turn 无论 session_start reason 必写一条——
46
+ startup/new → `initial`,resume/fork/reload → `resume`(宁可多写不可漏记)。
45
47
 
46
48
  ## 注册状态
47
49
 
48
- 本包尚未注册进 `packages/shared/src/mandatory-extensions.json`(后续单元处理;feature tier,可禁不可卸)。
50
+ 已注册进 `packages/shared/src/mandatory-extensions.json`(feature tier,可禁不可卸)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-system-prompt-trace",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "System prompt trace extension: appends xyz:system-prompt entries to the session JSONL whenever the effective system prompt is established or changes",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -26,14 +26,14 @@
26
26
  ]
27
27
  },
28
28
  "peerDependencies": {
29
- "@earendil-works/pi-coding-agent": "^0.84.1"
29
+ "@earendil-works/pi-coding-agent": "^0.84.4"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@vitest/coverage-v8": "^4.1.9",
33
33
  "vitest": "^4.1.8"
34
34
  },
35
35
  "dependencies": {
36
- "@zhushanwen/pi-extension-logger": "0.3.0"
36
+ "@zhushanwen/pi-extension-logger": "0.4.0"
37
37
  },
38
38
  "scripts": {
39
39
  "typecheck": "npx tsc --noEmit",
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * A12 hash 基线跨重启恢复(spec:.cw-specs/trace-ext.json;设计 D2 复审 N2 / plan §2.1)。
3
3
  *
4
- * 三路径(优先级从高到低):
4
+ * 四路径(优先级从高到低,口径同 trace.ts onSessionStart):
5
5
  * 1. 进程内 resume:session_before_switch.targetSessionFile 直读目标文件取上一版 hash
6
6
  * (switch 重建 extension runtime,基线经模块级 stash 传递——用「新闭包 + 共享 stash」模拟)
7
- * 2. app 重启直 spawn resumeagentDir 自持久化小文件(此链路无 switch 事件、reason=startup)
8
- * 3. 兜底:两路都读不到 resume 必写一条(宁可多写不可漏记)
7
+ * 2. forkpreviousSessionFile 直读源 session 文件(暂定语义,本文件未覆盖)
8
+ * 3. app 重启直 spawn resume:agentDir 自持久化小文件(此链路无 switch 事件、reason=startup)
9
+ * 4. 兜底:三路都读不到 → 任意 reason 必写一条(startup/new→initial,resume/fork/reload→resume)
9
10
  *
10
11
  * 用真实临时目录 + 真实 fs 函数(非 mock 投影);appendEntry 同步模拟 pi appendCustomEntry
11
12
  * 的落盘形状(session-manager.ts)。
@@ -157,7 +158,7 @@ describe("A12 hash 基线跨重启恢复", () => {
157
158
  expect(h.entries[1]?.parentVersionDiffSummary).toContain("+1 -0 lines");
158
159
  });
159
160
 
160
- it("路径 2(app 重启直 spawn):自持久化小文件命中且 hash 未变 → 不写(reason=startup、无 switch 事件)", () => {
161
+ it("路径 3(app 重启直 spawn):自持久化小文件命中且 hash 未变 → 不写(reason=startup、无 switch 事件)", () => {
161
162
  const h = makeHarness(P1);
162
163
  h.logic.onSessionStart("startup", undefined, h.ctx);
163
164
  h.logic.onTurnStart(h.ctx); // v1 initial + 基线小文件已写
@@ -174,7 +175,7 @@ describe("A12 hash 基线跨重启恢复", () => {
174
175
  expect(h.entries).toHaveLength(1);
175
176
  });
176
177
 
177
- it("路径 2(app 重启直 spawn):基线命中但 prompt 已变 → 写 resume v2(无 parent 全文,diff 摘要缺省)", () => {
178
+ it("路径 3(app 重启直 spawn):基线命中但 prompt 已变 → 写 resume v2(无 parent 全文,diff 摘要缺省)", () => {
178
179
  const h = makeHarness(P1);
179
180
  h.logic.onSessionStart("startup", undefined, h.ctx);
180
181
  h.logic.onTurnStart(h.ctx); // v1
@@ -189,7 +190,7 @@ describe("A12 hash 基线跨重启恢复", () => {
189
190
  expect(h.entries[1]?.parentVersionDiffSummary).toBeUndefined();
190
191
  });
191
192
 
192
- it("路径 3(兜底):两路基线都读不到且 reason=resume → 必写一条", () => {
193
+ it("路径 4(兜底):三路基线都读不到且 reason=resume → 必写一条", () => {
193
194
  const h = makeHarness(P1);
194
195
  h.logic.onSessionStart("resume", undefined, h.ctx); // 无 stash、无小文件
195
196
  h.logic.onTurnStart(h.ctx);
@@ -6,6 +6,8 @@
6
6
  * 从而验证 wiring 把基线小文件真的落在 getAgentDir() 下
7
7
  * - pi 用 Proxy 假体:捕获 on 注册的 handler 与 appendEntry 落点;handler 以 SDK 双参契约
8
8
  * (event, ctx) 驱动;ctx 只需 index.ts 实际消费的字段(getSystemPrompt + sessionManager.getSessionId)
9
+ * - @zhushanwen/pi-extension-logger mock 三导出(getLogger / createLogger / setPiHandle),
10
+ * setPiHandle 捕获 factory 注入(D3 接线契约),logger 方法收集供吞错断言
9
11
  * - switchStash 是模块级单例:beforeEach vi.resetModules 隔离用例;同 it 内二次 dynamic import
10
12
  * 模拟「switch 重建 extension runtime + 同进程模块缓存延续」的真实链路
11
13
  */
@@ -25,13 +27,14 @@ import type { SystemPromptTraceEntryData } from "../types.js";
25
27
  const P1 = "wiring prompt\nline-1";
26
28
  const P2 = "wiring prompt\nline-1\nline-2-added";
27
29
 
28
- const { loggerMock } = vi.hoisted(() => {
30
+ const { loggerMock, setPiHandleMock } = vi.hoisted(() => {
29
31
  const mock = { debug: vi.fn(), warn: vi.fn(), error: vi.fn() };
30
- return { loggerMock: mock };
32
+ return { loggerMock: mock, setPiHandleMock: vi.fn() };
31
33
  })
32
34
  vi.mock("@zhushanwen/pi-extension-logger", () => ({
33
35
  getLogger: () => loggerMock,
34
36
  createLogger: () => loggerMock,
37
+ setPiHandle: setPiHandleMock,
35
38
  }))
36
39
 
37
40
  const agentDirRef = vi.hoisted(() => ({ current: "" }));
@@ -138,6 +141,18 @@ describe("index.ts wiring SDK 契约", () => {
138
141
  ]);
139
142
  });
140
143
 
144
+ it("factory 调用 setPiHandle(pi) 注入日志通道(缺注入时 logger.error 的 appendEntry 通道是 no-op,生产完全静默)", async () => {
145
+ const ext = await loadExtension();
146
+ const h = createWiringHarness();
147
+ // hoisted mock 不随 resetModules 清调用历史,先清再驱动,隔离其他用例的累计调用
148
+ setPiHandleMock.mockClear();
149
+ ext(h.pi);
150
+ expect(setPiHandleMock).toHaveBeenCalledTimes(1);
151
+ // pi 是 Proxy 假体,深度相等比较不可靠,用引用相等断言
152
+ const injected = setPiHandleMock.mock.calls[0]?.[0];
153
+ expect(injected).toBe(h.pi);
154
+ });
155
+
141
156
  it("startup(无 previousSessionFile)→ 首 turn 写 initial v1(appendEntry 形状 + 基线落 getAgentDir);prompt 变化写 change v2 带 diff 摘要", async () => {
142
157
  const ext = await loadExtension();
143
158
  const h = createWiringHarness();
package/src/baseline.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  /**
2
- * 跨重启基线的文件系统侧实现(设计 D2 三路径中的路径 1/2 的读与路径 2 的写)。
2
+ * 跨重启基线的文件系统侧实现(四路径口径,见 trace.ts onSessionStart 解析优先级:
3
+ * 路径 1 stash 直读 / 路径 2 fork prev 直读的读,与路径 3 自持久化小文件的读和写)。
3
4
  *
4
5
  * - readLastPromptFromSessionFile:直读 session JSONL(进程内 resume 的 targetSessionFile、
5
6
  * fork 暂定的 previousSessionFile),倒序找最后一条 xyz:system-prompt 留痕 entry。
6
- * - readPersistedBaseline / writePersistedBaseline:dataDir 自持久化小文件
7
- * (app 重启直 spawn resume 时唯一可用的基线来源),原子写入。
7
+ * - readPersistedBaseline / writePersistedBaseline:agentDir 自持久化小文件
8
+ * (app 重启直 spawn resume / reload 等无 switch 事件链路的唯一基线来源),原子写入。
8
9
  *
9
10
  * 所有函数不抛错:读失败返回 null、写失败 logger.error 后静默——基线丢失的代价只是
10
11
  * 下次 resume 多写一条留痕(设计 D2 已接受),不允许影响 agent 主流程。
@@ -148,8 +149,9 @@ export function writePersistedBaseline(
148
149
  // 条目;对齐 quota-providers atomicWriteJson);清理失败不掩盖原错误
149
150
  try {
150
151
  if (existsSync(tmpPath)) unlinkSync(tmpPath);
151
- } catch {
152
+ } catch (cleanupErr) {
152
153
  // 清理失败仅残留一个小文件,原错误优先上抛
154
+ void cleanupErr;
153
155
  }
154
156
  throw err;
155
157
  }
package/src/index.ts CHANGED
@@ -10,6 +10,8 @@ import { join } from "node:path";
10
10
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
11
11
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
12
12
 
13
+ import { setPiHandle } from "@zhushanwen/pi-extension-logger";
14
+
13
15
  import {
14
16
  BASELINE_FILENAME,
15
17
  readLastPromptFromSessionFile,
@@ -53,6 +55,12 @@ function toTraceContext(pi: ExtensionAPI, ctx: ExtensionContext): TraceContext {
53
55
  }
54
56
 
55
57
  export default function systemPromptTraceExtension(pi: ExtensionAPI): void {
58
+ // 日志通道注入(extension-logger 两阶段初始化:工厂拿 pi → setPiHandle)。缺此注入时
59
+ // trace/baseline 中 logger.error 的 appendEntry 通道是 no-op,生产默认完全静默——
60
+ // 此前全包从未注入(文本-实现漂移审查 D3 补接线,恢复注释声称的持久化语义)。
61
+ // 测试环境无真实 globalPi 消费方时调用本身安全(存引用,不触发 appendEntry)。
62
+ setPiHandle(pi);
63
+
56
64
  const baselineFilePath = join(getAgentDir(), BASELINE_FILENAME);
57
65
 
58
66
  const env: TraceEnv = {
package/src/trace.ts CHANGED
@@ -84,7 +84,7 @@ export function createSystemPromptTrace(env: TraceEnv, stash: SwitchStash): Syst
84
84
  data.parentVersionDiffSummary = summarizePromptDiff(parentFullText, text);
85
85
  }
86
86
  ctx.appendEntry(SYSTEM_PROMPT_CUSTOM_TYPE, data);
87
- // 落盘成功后才刷新自持久化基线(app 重启直 spawn resume 的唯一基线来源,设计 D2 路径 2
87
+ // 落盘成功后才刷新自持久化基线(app 重启直 spawn resume / reload 的唯一基线来源,设计 D2 路径 3
88
88
  env.writePersistedBaseline(ctx.getSessionId(), hash, version);
89
89
  };
90
90
 
@@ -92,7 +92,7 @@ export function createSystemPromptTrace(env: TraceEnv, stash: SwitchStash): Syst
92
92
  onSessionStart(reason, previousSessionFile, ctx) {
93
93
  sessionStartReason = normalizeSessionStartReason(reason);
94
94
  current = null;
95
- // 基线解析(设计 D2 跨重启三路径,优先级从高到低):
95
+ // 基线解析(设计 D2 跨重启四路径,优先级从高到低):
96
96
  // 1. session_before_switch 直读目标文件(进程内 resume;stash 为模块级单例,跨 runtime 传递)
97
97
  // 2. fork 的 previousSessionFile 直读【暂定语义,待 P2 实测定】
98
98
  // 3. agentDir 自持久化小文件(app 重启直 spawn resume / reload——这两种链路没有 switch 事件)
package/src/types.ts CHANGED
@@ -35,7 +35,7 @@ export interface PromptBaseline {
35
35
  version: number;
36
36
  /** 从 session 文件留痕 entry 直读时有值(可生成 diff 摘要);自持久化小文件只有 hash+version。 */
37
37
  fullText?: string;
38
- /** 基线来源(三路径,见 trace.ts onSessionStart 的解析优先级)。 */
38
+ /** 基线来源(四路径口径,见 trace.ts onSessionStart 的解析优先级)。 */
39
39
  source: "target-file" | "previous-session-file" | "persisted";
40
40
  }
41
41