@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 +9 -7
- package/package.json +3 -3
- package/src/__tests__/baseline.a12.test.ts +7 -6
- package/src/__tests__/index-wiring.test.ts +17 -2
- package/src/baseline.ts +6 -4
- package/src/index.ts +8 -0
- package/src/trace.ts +2 -2
- package/src/types.ts +1 -1
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
|
|
41
|
-
hash/version/fullText。switch 会 teardown 并重建 extension runtime,该基线经模块级 stash 跨 runtime 传递。
|
|
42
|
-
2. **
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
8
|
-
* 3.
|
|
7
|
+
* 2. fork:previousSessionFile 直读源 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("路径
|
|
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("路径
|
|
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("路径
|
|
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
|
-
*
|
|
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:
|
|
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 路径
|
|
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
|
-
/**
|
|
38
|
+
/** 基线来源(四路径口径,见 trace.ts onSessionStart 的解析优先级)。 */
|
|
39
39
|
source: "target-file" | "previous-session-file" | "persisted";
|
|
40
40
|
}
|
|
41
41
|
|