@astrosheep/pi-context 0.25.1 → 0.26.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/README.md +88 -7
- package/dist/build-info.json +2 -2
- package/dist/extension.js +616 -370
- package/dist/src/context/boot.d.ts +24 -0
- package/dist/src/context/boot.js +33 -24
- package/dist/src/context/budget.d.ts +9 -0
- package/dist/src/context/budget.js +19 -15
- package/dist/src/context/context-window.d.ts +41 -0
- package/dist/src/context/context-window.js +16 -1
- package/dist/src/context/prompts.d.ts +20 -0
- package/dist/src/context/prompts.js +1 -1
- package/dist/src/context/reset-artifacts.d.ts +26 -0
- package/dist/src/context/reset-artifacts.js +18 -17
- package/dist/src/context/reset-lifecycle.d.ts +89 -0
- package/dist/src/context/reset-lifecycle.js +103 -75
- package/dist/src/context/runtime.d.ts +3 -0
- package/dist/src/context/runtime.js +53 -21
- package/dist/src/context/thresholds.d.ts +33 -0
- package/dist/src/context/thresholds.js +1 -1
- package/dist/src/dream/cli.d.ts +10 -0
- package/dist/src/dream/cli.js +1 -1
- package/dist/src/dream/doctor.d.ts +2 -0
- package/dist/src/dream/doctor.js +6 -2
- package/dist/src/dream/gates.d.ts +10 -0
- package/dist/src/dream/git.d.ts +21 -0
- package/dist/src/dream/lock.d.ts +31 -0
- package/dist/src/dream/runner.d.ts +30 -0
- package/dist/src/dream/settings.d.ts +16 -0
- package/dist/src/history/history-tools.d.ts +2 -0
- package/dist/src/history/history.d.ts +57 -0
- package/dist/src/index.d.ts +39 -0
- package/dist/src/index.js +4 -4
- package/dist/src/notes/address.d.ts +26 -0
- package/dist/src/notes/address.js +8 -14
- package/dist/src/notes/constants.d.ts +3 -0
- package/dist/src/notes/constants.js +3 -0
- package/dist/src/notes/context.d.ts +10 -0
- package/dist/src/notes/context.js +33 -0
- package/dist/src/notes/frontmatter.d.ts +46 -0
- package/dist/src/notes/frontmatter.js +10 -5
- package/dist/src/notes/index.d.ts +4 -0
- package/dist/src/notes/index.js +2 -0
- package/dist/src/notes/paths.d.ts +21 -0
- package/dist/src/notes/paths.js +72 -76
- package/dist/src/notes/store.d.ts +94 -0
- package/dist/src/notes/store.js +298 -242
- package/dist/src/pi/notes/adapter.d.ts +12 -0
- package/dist/src/pi/notes/adapter.js +39 -0
- package/dist/src/pi/notes/session-replay.d.ts +16 -0
- package/dist/src/{notes → pi/notes}/session-replay.js +2 -2
- package/dist/src/pi/notes/snapshot.d.ts +33 -0
- package/dist/src/{notes/notes-snapshot.js → pi/notes/snapshot.js} +11 -3
- package/dist/src/pi/notes/tools.d.ts +2 -0
- package/dist/src/{notes → pi/notes}/tools.js +24 -21
- package/dist/src/protocol.d.ts +41 -0
- package/dist/src/protocol.js +4 -6
- package/dist/src/session-reader.d.ts +5 -0
- package/dist/src/settings.d.ts +6 -0
- package/dist/src/tool-output.d.ts +101 -0
- package/dist/src/tool-schema.d.ts +17 -0
- package/dist/test/agent-loop.test.d.ts +1 -0
- package/dist/test/agent-loop.test.js +318 -19
- package/dist/test/boot.integration.test.d.ts +1 -0
- package/dist/test/boot.integration.test.js +55 -29
- package/dist/test/budget-settings.integration.test.d.ts +1 -0
- package/dist/test/budget-settings.integration.test.js +8 -7
- package/dist/test/doctor.test.d.ts +1 -0
- package/dist/test/doctor.test.js +10 -2
- package/dist/test/dream-skill.test.d.ts +1 -0
- package/dist/test/dream-skill.test.js +69 -0
- package/dist/test/dream.test.d.ts +1 -0
- package/dist/test/helpers/extension.d.ts +115 -0
- package/dist/test/helpers/extension.js +6 -6
- package/dist/test/helpers/notes.d.ts +6 -0
- package/dist/test/helpers/notes.js +13 -0
- package/dist/test/history.integration.test.d.ts +1 -0
- package/dist/test/notes-library.test.d.ts +1 -0
- package/dist/test/notes-library.test.js +111 -0
- package/dist/test/notes.integration.test.d.ts +1 -0
- package/dist/test/notes.integration.test.js +22 -24
- package/dist/test/notes.test.d.ts +1 -0
- package/dist/test/notes.test.js +137 -7
- package/dist/test/reset-lifecycle.test.d.ts +1 -0
- package/dist/test/reset-lifecycle.test.js +142 -85
- package/docs/architecture.md +8 -8
- package/docs/reset-lifecycle.md +63 -79
- package/package.json +35 -2
- package/playbook.md +33 -32
- package/skills/dream/SKILL.md +12 -0
- package/src/context/boot.ts +44 -25
- package/src/context/budget.ts +25 -17
- package/src/context/context-window.ts +16 -1
- package/src/context/prompts.ts +2 -2
- package/src/context/reset-artifacts.ts +26 -24
- package/src/context/reset-lifecycle.ts +117 -111
- package/src/context/runtime.ts +50 -22
- package/src/context/thresholds.ts +1 -1
- package/src/dream/cli.ts +1 -1
- package/src/dream/doctor.ts +5 -2
- package/src/index.ts +4 -4
- package/src/notes/address.ts +9 -15
- package/src/notes/constants.ts +3 -0
- package/src/notes/context.ts +40 -0
- package/src/notes/frontmatter.ts +18 -12
- package/src/notes/index.ts +22 -0
- package/src/notes/paths.ts +64 -78
- package/src/notes/store.ts +308 -244
- package/src/pi/notes/adapter.ts +44 -0
- package/src/{notes → pi/notes}/session-replay.ts +3 -3
- package/src/{notes/notes-snapshot.ts → pi/notes/snapshot.ts} +13 -4
- package/src/{notes → pi/notes}/tools.ts +25 -23
- package/src/protocol.ts +5 -6
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
2
3
|
import test from "node:test";
|
|
3
4
|
import { historyFromSession, internal } from "../src/index.js";
|
|
4
|
-
import { listNotes } from "
|
|
5
|
-
import { CONTINUATION_TYPE } from "../src/protocol.js";
|
|
5
|
+
import { listNotes } from "./helpers/notes.js";
|
|
6
|
+
import { CONTINUATION_TYPE, WARNING_TYPE } from "../src/protocol.js";
|
|
6
7
|
import { appendText, call, commitTurnEndBoundary, context, installExtensionTestEnvironment, makeExtension, manager, noticesOf, resultJson, resultRead, runContextHook, runContextWithSystemHook, runHandlers, runManualCompact, runCommand, sentOf, } from "./helpers/extension.js";
|
|
7
8
|
const testEnvironment = installExtensionTestEnvironment("pi-context-integration");
|
|
8
9
|
test.beforeEach(() => testEnvironment.beforeEach());
|
|
@@ -21,7 +22,12 @@ test("custom reset marker removes old provider context while history remains sea
|
|
|
21
22
|
const branch = sessionManager.getBranch();
|
|
22
23
|
const marker = branch.find((entry) => entry.type === "custom" && entry.customType === internal.RESET_MARKER_TYPE);
|
|
23
24
|
assert.ok(marker && marker.type === "custom");
|
|
24
|
-
|
|
25
|
+
const checkpoint = branch.find((entry) => entry.type === "compaction");
|
|
26
|
+
assert.ok(checkpoint && checkpoint.type === "compaction", "reset persists a native retain-none checkpoint");
|
|
27
|
+
assert.equal(checkpoint.parentId, toolResultId, "checkpoint follows the completed tool result");
|
|
28
|
+
assert.equal(checkpoint.summary, "");
|
|
29
|
+
assert.equal(checkpoint.firstKeptEntryId, checkpoint.id);
|
|
30
|
+
assert.equal(marker.parentId, checkpoint.id, "marker follows the native checkpoint");
|
|
25
31
|
assert.deepEqual(Object.keys(marker.data), ["windowId"]);
|
|
26
32
|
const projected = await runContextWithSystemHook(captured, ctx, sessionManager.buildSessionContext().messages);
|
|
27
33
|
const providerText = JSON.stringify(projected?.messages ?? []);
|
|
@@ -46,14 +52,14 @@ test("the root boot and reset boot carry durable window identity", async () => {
|
|
|
46
52
|
appendText(sessionManager, "assistant", "working");
|
|
47
53
|
await call(captured, "notes_write", { path: "decisions.md", content: "use terra" }, ctx);
|
|
48
54
|
// Root window: session_start persists the boot block without triggering a turn.
|
|
49
|
-
runHandlers(captured, "session_start", { reason: "startup" }, ctx);
|
|
55
|
+
await runHandlers(captured, "session_start", { reason: "startup" }, ctx);
|
|
50
56
|
assert.equal(captured.sent.length, 1);
|
|
51
57
|
const rootBoot = captured.sent[0];
|
|
52
58
|
assert.equal(rootBoot?.message.customType, internal.BOOT_TYPE);
|
|
53
59
|
assert.equal(rootBoot?.message.display, false, "boot block stays out of the TUI");
|
|
54
60
|
assert.equal(rootBoot?.options?.triggerTurn, false);
|
|
55
61
|
assert.equal(noticesOf(ctx).length, 0, "initial boot is silent");
|
|
56
|
-
runHandlers(captured, "session_start", { reason: "reload" }, ctx);
|
|
62
|
+
await runHandlers(captured, "session_start", { reason: "reload" }, ctx);
|
|
57
63
|
assert.equal(noticesOf(ctx).length, 0, "reload is silent");
|
|
58
64
|
assert.deepEqual(rootBoot?.message.details, { windowId: `pcw:${sessionManager.getSessionId().slice(0, 8)}:root` });
|
|
59
65
|
const rootText = typeof rootBoot?.message.content === "string" ? rootBoot.message.content : "";
|
|
@@ -63,7 +69,7 @@ test("the root boot and reset boot carry durable window identity", async () => {
|
|
|
63
69
|
assert.match(rootText, new RegExp(`First context window id: pcw:${sessionManager.getSessionId().slice(0, 8)}:root`));
|
|
64
70
|
assert.match(rootText, new RegExp(`Current context window id: pcw:${sessionManager.getSessionId().slice(0, 8)}:root`));
|
|
65
71
|
assert.ok(rootText.includes("decisions.md"));
|
|
66
|
-
const decisionsMeta = listNotes(ctx, { scope: "session" }).find((row) => row.path === "decisions.md")?.meta;
|
|
72
|
+
const decisionsMeta = (await listNotes(ctx, { scope: "session" })).find((row) => row.path === "decisions.md")?.meta;
|
|
67
73
|
assert.ok(decisionsMeta);
|
|
68
74
|
assert.match(rootText, /updated \d+s ago\)/, "boot note metadata carries a relative update time");
|
|
69
75
|
assert.ok(rootText.includes(internal.CONTEXT_WINDOW_PROTOCOL_OPEN_TAG));
|
|
@@ -72,8 +78,8 @@ test("the root boot and reset boot carry durable window identity", async () => {
|
|
|
72
78
|
assert.equal(noticesOf(ctx).length, 0, "requesting a reset does not announce success");
|
|
73
79
|
const boundary = await commitTurnEndBoundary(captured, sessionManager, ctx);
|
|
74
80
|
assert.equal(boundary.continue, true);
|
|
75
|
-
runHandlers(captured, "turn_start", {}, ctx);
|
|
76
|
-
runHandlers(captured, "agent_settled", {}, ctx);
|
|
81
|
+
await runHandlers(captured, "turn_start", {}, ctx);
|
|
82
|
+
await runHandlers(captured, "agent_settled", {}, ctx);
|
|
77
83
|
assert.equal(noticesOf(ctx).filter((notice) => notice.message.includes("memory cleared")).length, 1, "the committed reset is announced once");
|
|
78
84
|
const entries = sessionManager.getBranch();
|
|
79
85
|
const marker = entries.find((entry) => entry.type === "custom" && entry.customType === internal.RESET_MARKER_TYPE);
|
|
@@ -85,7 +91,28 @@ test("the root boot and reset boot carry durable window identity", async () => {
|
|
|
85
91
|
const continuation = entries.find((entry) => entry.type === "custom_message" && entry.customType === CONTINUATION_TYPE);
|
|
86
92
|
assert.ok(continuation && continuation.type === "custom_message" && continuation.display === false, "the resumed run is represented by one hidden continuation");
|
|
87
93
|
});
|
|
88
|
-
test("
|
|
94
|
+
test("an aborted async boot repair emits no boot, continuation, or incomplete-notes notice", async () => {
|
|
95
|
+
const notesHome = process.env.PI_NOTES_HOME;
|
|
96
|
+
assert.ok(notesHome);
|
|
97
|
+
rmSync(notesHome, { recursive: true, force: true });
|
|
98
|
+
writeFileSync(notesHome, "not a directory");
|
|
99
|
+
const sessionManager = manager();
|
|
100
|
+
const windowId = "pcw:aborted-boot-repair";
|
|
101
|
+
sessionManager.appendCustomEntry(internal.RESET_MARKER_TYPE, { windowId });
|
|
102
|
+
const captured = makeExtension(sessionManager);
|
|
103
|
+
const ctx = context(sessionManager);
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
ctx.signal = controller.signal;
|
|
106
|
+
const sessionStart = captured.handlers.get("session_start")?.[0];
|
|
107
|
+
assert.ok(sessionStart);
|
|
108
|
+
const pending = sessionStart({ reason: "startup" }, ctx);
|
|
109
|
+
controller.abort();
|
|
110
|
+
await pending;
|
|
111
|
+
assert.equal(captured.sent.length, 0, "aborted snapshot work cannot append the reset boot or continuation");
|
|
112
|
+
assert.equal(noticesOf(ctx).some((notice) => notice.message.includes("notes index incomplete")), false, "aborted snapshot work cannot notify about missing homes");
|
|
113
|
+
assert.equal(sessionManager.getBranch().some((entry) => entry.type === "custom_message" && [internal.BOOT_TYPE, CONTINUATION_TYPE].includes(entry.customType)), false);
|
|
114
|
+
});
|
|
115
|
+
test("a marker tail with only metadata repairs its missing boot and continuation without moving the boundary", async () => {
|
|
89
116
|
const sessionManager = manager(true);
|
|
90
117
|
const windowId = "pcw:metadata-tail";
|
|
91
118
|
const markerId = sessionManager.appendCustomEntry(internal.RESET_MARKER_TYPE, { windowId });
|
|
@@ -93,7 +120,7 @@ test("a marker tail with only metadata repairs its missing boot and continuation
|
|
|
93
120
|
sessionManager.appendThinkingLevelChange("low");
|
|
94
121
|
const captured = makeExtension(sessionManager);
|
|
95
122
|
const ctx = context(sessionManager);
|
|
96
|
-
runHandlers(captured, "session_start", { reason: "startup" }, ctx);
|
|
123
|
+
await runHandlers(captured, "session_start", { reason: "startup" }, ctx);
|
|
97
124
|
const branch = sessionManager.getBranch();
|
|
98
125
|
const markerIndex = branch.findIndex((entry) => entry.id === markerId);
|
|
99
126
|
const bootEntries = branch.filter((entry) => entry.type === "custom_message" && entry.customType === internal.BOOT_TYPE && entry.details && typeof entry.details === "object" && entry.details.windowId === windowId);
|
|
@@ -108,13 +135,13 @@ test("a marker tail with only metadata repairs its missing boot and continuation
|
|
|
108
135
|
assert.equal(captured.sent[1]?.message.customType, CONTINUATION_TYPE);
|
|
109
136
|
assert.equal(noticesOf(ctx).length, 0, "repairing a reset tail is not a new reset");
|
|
110
137
|
});
|
|
111
|
-
test("a bare marker tail repairs the full ordered reset shape", () => {
|
|
138
|
+
test("a bare marker tail repairs the full ordered reset shape", async () => {
|
|
112
139
|
const sessionManager = manager(true);
|
|
113
140
|
const windowId = "pcw:bare-marker";
|
|
114
141
|
const markerId = sessionManager.appendCustomEntry(internal.RESET_MARKER_TYPE, { windowId });
|
|
115
142
|
const captured = makeExtension(sessionManager);
|
|
116
143
|
const ctx = context(sessionManager);
|
|
117
|
-
runHandlers(captured, "session_start", { reason: "startup" }, ctx);
|
|
144
|
+
await runHandlers(captured, "session_start", { reason: "startup" }, ctx);
|
|
118
145
|
assert.equal(captured.sent.length, 2, "a bare marker emits a boot and a continuation");
|
|
119
146
|
const branch = sessionManager.getBranch();
|
|
120
147
|
const markerIndex = branch.findIndex((entry) => entry.id === markerId);
|
|
@@ -123,7 +150,7 @@ test("a bare marker tail repairs the full ordered reset shape", () => {
|
|
|
123
150
|
assert.ok(bootIndex > markerIndex, "the repaired boot follows its marker");
|
|
124
151
|
assert.ok(continuationIndex > bootIndex, "the repaired continuation follows its boot");
|
|
125
152
|
});
|
|
126
|
-
test("a marker tail that already carries a boot repairs only its missing continuation, once", () => {
|
|
153
|
+
test("a marker tail that already carries a boot repairs only its missing continuation, once", async () => {
|
|
127
154
|
const sessionManager = manager(true);
|
|
128
155
|
const windowId = "pcw:missing-continuation";
|
|
129
156
|
const markerId = sessionManager.appendCustomEntry(internal.RESET_MARKER_TYPE, { windowId });
|
|
@@ -131,23 +158,23 @@ test("a marker tail that already carries a boot repairs only its missing continu
|
|
|
131
158
|
sessionManager.appendCustomMessageEntry(internal.BOOT_TYPE, "already-persisted boot", false, { windowId });
|
|
132
159
|
const captured = makeExtension(sessionManager);
|
|
133
160
|
const ctx = context(sessionManager);
|
|
134
|
-
runHandlers(captured, "session_start", { reason: "startup" }, ctx);
|
|
161
|
+
await runHandlers(captured, "session_start", { reason: "startup" }, ctx);
|
|
135
162
|
assert.equal(captured.sent.length, 1, "only the missing continuation is emitted");
|
|
136
163
|
assert.equal(captured.sent[0]?.message.customType, CONTINUATION_TYPE);
|
|
137
164
|
assert.equal(sessionManager.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === internal.BOOT_TYPE).length, 1, "the existing boot is never duplicated");
|
|
138
165
|
assert.ok(sessionManager.getBranch().findIndex((entry) => entry.id === markerId) >= 0);
|
|
139
|
-
runHandlers(captured, "session_start", { reason: "reload" }, ctx);
|
|
166
|
+
await runHandlers(captured, "session_start", { reason: "reload" }, ctx);
|
|
140
167
|
assert.equal(captured.sent.length, 1, "a complete reset tail is idempotent");
|
|
141
168
|
assert.equal(sessionManager.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === CONTINUATION_TYPE).length, 1);
|
|
142
169
|
});
|
|
143
|
-
test("a marker tail followed by real conversation is never repaired", () => {
|
|
170
|
+
test("a marker tail followed by real conversation is never repaired", async () => {
|
|
144
171
|
const sessionManager = manager(true);
|
|
145
172
|
const windowId = "pcw:unsafe-tail";
|
|
146
173
|
sessionManager.appendCustomEntry(internal.RESET_MARKER_TYPE, { windowId });
|
|
147
174
|
appendText(sessionManager, "user", "post-marker work that must not be pushed behind a late boot");
|
|
148
175
|
const captured = makeExtension(sessionManager);
|
|
149
176
|
const ctx = context(sessionManager);
|
|
150
|
-
runHandlers(captured, "session_start", { reason: "startup" }, ctx);
|
|
177
|
+
await runHandlers(captured, "session_start", { reason: "startup" }, ctx);
|
|
151
178
|
assert.equal(captured.sent.length, 0, "an unsafe marker tail emits nothing");
|
|
152
179
|
assert.equal(sessionManager.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === internal.BOOT_TYPE).length, 0, "no boot is appended after real work");
|
|
153
180
|
assert.equal(sessionManager.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === CONTINUATION_TYPE).length, 0, "no continuation is appended after real work");
|
|
@@ -180,37 +207,36 @@ test("wipe_memory uses one turn boundary and never calls ctx.compact", async ()
|
|
|
180
207
|
assert.equal(sessionManager.getBranch().filter((entry) => entry.type === "custom" && entry.customType === internal.RESET_MARKER_TYPE).length, 1);
|
|
181
208
|
assert.equal(sessionManager.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === internal.BOOT_TYPE && entry.details && typeof entry.details === "object" && "windowId" in entry.details).length, 1);
|
|
182
209
|
});
|
|
183
|
-
test("pi-context command toggles future work, /wipe-memory
|
|
210
|
+
test("pi-context command toggles future work, /wipe-memory starts close-out, and /compact remains disabled", async () => {
|
|
184
211
|
const sessionManager = manager();
|
|
185
212
|
appendText(sessionManager, "user", "hello");
|
|
186
213
|
const captured = makeExtension(sessionManager);
|
|
187
214
|
const low = context(sessionManager, undefined, { tokens: 170_000, contextWindow: 200_000, percent: 85 });
|
|
188
215
|
// On by default: session_start persists the root boot block.
|
|
189
|
-
runHandlers(captured, "session_start", { reason: "startup" }, low);
|
|
216
|
+
await runHandlers(captured, "session_start", { reason: "startup" }, low);
|
|
190
217
|
assert.equal(captured.sent.length, 1);
|
|
191
218
|
assert.equal(captured.sent[0]?.message.customType, internal.BOOT_TYPE);
|
|
192
219
|
let notices = await runCommand(captured, "pi-context", "off", low);
|
|
193
220
|
assert.match(notices[0]?.message ?? "", /off/);
|
|
194
221
|
assert.equal(await runContextHook(captured, low), undefined, "no guidance while off");
|
|
195
222
|
assert.equal(sentOf(captured, internal.GUIDANCE_TYPE).length, 0, "no guidance persisted while off");
|
|
196
|
-
runHandlers(captured, "session_start", { reason: "startup" }, low);
|
|
223
|
+
await runHandlers(captured, "session_start", { reason: "startup" }, low);
|
|
197
224
|
assert.equal(captured.sent.length, 1, "no new boot block is persisted while off");
|
|
198
225
|
const offResult = resultJson(await call(captured, "wipe_memory", {}, low));
|
|
199
226
|
assert.match(offResult.error ?? "", /off/, "wipe_memory refuses while off");
|
|
200
227
|
notices = await runCommand(captured, "pi-context", "on", low);
|
|
201
228
|
assert.match(notices[0]?.message ?? "", /on/);
|
|
202
229
|
notices = await runCommand(captured, "wipe-memory", "", low);
|
|
203
|
-
assert.equal(notices.length,
|
|
204
|
-
assert.
|
|
205
|
-
assert.equal(captured.sent
|
|
206
|
-
assert.equal(captured.sent[1]?.
|
|
207
|
-
assert.equal(captured.sent[1]?.message.
|
|
208
|
-
assert.equal(
|
|
209
|
-
assert.equal(sessionManager.getBranch().filter((entry) => entry.type === "custom" && entry.customType === internal.RESET_MARKER_TYPE).length, 1);
|
|
230
|
+
assert.equal(notices.length, 0, "the command begins a model close-out rather than announcing a reset request");
|
|
231
|
+
assert.equal(captured.sent.length, 2, "the command persists one hidden warning after the startup boot");
|
|
232
|
+
assert.equal(captured.sent[1]?.options?.triggerTurn, true, "the shared warning starts an ordinary agent turn");
|
|
233
|
+
assert.equal(captured.sent[1]?.message.customType, WARNING_TYPE);
|
|
234
|
+
assert.equal(captured.sent[1]?.message.display, false);
|
|
235
|
+
assert.equal(sessionManager.getBranch().filter((entry) => entry.type === "custom" && entry.customType === internal.RESET_MARKER_TYPE).length, 0, "no reset is claimed until close-out completes");
|
|
210
236
|
const markerContext = await runManualCompact(captured, low);
|
|
211
|
-
assert.deepEqual(markerContext, { cancel: true }, "/compact is canceled while
|
|
237
|
+
assert.deepEqual(markerContext, { cancel: true }, "/compact is canceled while context windows are enabled");
|
|
212
238
|
const projected = await runContextWithSystemHook(captured, low, sessionManager.buildSessionContext().messages);
|
|
213
|
-
assert.equal(JSON.stringify(projected?.messages ?? []).includes("hello"),
|
|
239
|
+
assert.equal(JSON.stringify(projected?.messages ?? []).includes("hello"), true, "the old window remains active until the close-out commits");
|
|
214
240
|
notices = await runCommand(captured, "pi-context", "maybe", low);
|
|
215
241
|
assert.equal(notices[0]?.type, "error", "unknown argument rejected");
|
|
216
242
|
// Bare command reports current state without changing it.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -31,7 +31,7 @@ test("the visible countdown ends at the warning line, clamps at zero, and preser
|
|
|
31
31
|
});
|
|
32
32
|
const sm = manager();
|
|
33
33
|
const captured = makeExtension(sm);
|
|
34
|
-
runHandlers(captured, "session_tree", {}, context(sm, undefined, undefined, true, fixture.cwd, true));
|
|
34
|
+
await runHandlers(captured, "session_tree", {}, context(sm, undefined, undefined, true, fixture.cwd, true));
|
|
35
35
|
const readBudget = async (tokens, trusted = true) => {
|
|
36
36
|
const ctx = context(sm, undefined, { tokens, contextWindow: 200_000, percent: tokens === null ? null : tokens / 2000 }, true, fixture.cwd, trusted);
|
|
37
37
|
return resultJson(await call(captured, "get_context_remaining", {}, ctx)).remaining_tokens;
|
|
@@ -44,7 +44,7 @@ test("the visible countdown ends at the warning line, clamps at zero, and preser
|
|
|
44
44
|
const absent = context(sm, undefined, undefined, true, fixture.cwd);
|
|
45
45
|
assert.equal(resultJson(await call(captured, "get_context_remaining", {}, absent)).remaining_tokens, null);
|
|
46
46
|
const untrusted = context(sm, undefined, { tokens: 72_563, contextWindow: 200_000, percent: 36.2815 }, true, fixture.cwd, false);
|
|
47
|
-
runHandlers(captured, "session_start", {}, untrusted);
|
|
47
|
+
await runHandlers(captured, "session_start", {}, untrusted);
|
|
48
48
|
assert.equal(await readBudget(72_563, false), 98_765, "session start reloads the global reserve when the project is untrusted");
|
|
49
49
|
});
|
|
50
50
|
test("absent pi-context key or margins reproduce the default reminder threshold at Pi's default reserve", async () => {
|
|
@@ -59,7 +59,7 @@ test("absent pi-context key or margins reproduce the default reminder threshold
|
|
|
59
59
|
const captured = makeExtension(sm);
|
|
60
60
|
// Thresholds are resolved once per session and cached; branch navigation clears the
|
|
61
61
|
// cache without emitting a boot block, so the next read uses this fixture.
|
|
62
|
-
runHandlers(captured, "session_tree", {}, context(sm, undefined, undefined, true, fixture.cwd));
|
|
62
|
+
await runHandlers(captured, "session_tree", {}, context(sm, undefined, undefined, true, fixture.cwd));
|
|
63
63
|
const window = 200_000;
|
|
64
64
|
const at = (remaining) => context(sm, undefined, { tokens: window - remaining, percent: 0, contextWindow: window }, true, fixture.cwd);
|
|
65
65
|
const first = at(40_961);
|
|
@@ -81,7 +81,7 @@ test("project pi-context reminder margin and reserve override global per key", a
|
|
|
81
81
|
// Project reserve wins: reminder = 50000 + 40000 (project margin).
|
|
82
82
|
const sm = manager();
|
|
83
83
|
const captured = makeExtension(sm);
|
|
84
|
-
runHandlers(captured, "session_tree", {}, context(sm, undefined, undefined, true, fixture.cwd));
|
|
84
|
+
await runHandlers(captured, "session_tree", {}, context(sm, undefined, undefined, true, fixture.cwd));
|
|
85
85
|
const window = 300_000;
|
|
86
86
|
const at = (remaining) => context(sm, undefined, { tokens: window - remaining, percent: 0, contextWindow: window }, true, fixture.cwd);
|
|
87
87
|
assert.equal(await runContextHook(captured, at(90_001)), undefined, "nothing injected above the project-derived reminder");
|
|
@@ -96,12 +96,13 @@ test("an invalid reminder margin degrades to its default with one warning and ne
|
|
|
96
96
|
const sm = manager();
|
|
97
97
|
const captured = makeExtension(sm);
|
|
98
98
|
const ctx = context(sm, undefined, undefined, true, fixture.cwd);
|
|
99
|
-
assert.
|
|
99
|
+
await assert.doesNotReject(() => runHandlers(captured, "session_start", { reason: "startup" }, ctx));
|
|
100
100
|
const notices = noticesOf(ctx).filter((notice) => notice.type === "warning");
|
|
101
101
|
assert.equal(notices.length, 1, "one warning for the offending key");
|
|
102
102
|
assert.equal(notices[0]?.type, "warning");
|
|
103
103
|
assert.match(notices[0]?.message ?? "", /reminderMarginTokens/);
|
|
104
|
-
assert.match(notices[0]?.message ?? "", /
|
|
104
|
+
assert.match(notices[0]?.message ?? "", /default reminder margin/);
|
|
105
|
+
assert.equal(/\d+\s*k?\s*(remaining|tokens)/i.test(notices[0]?.message ?? ""), false, "UI notices do not expose token counts");
|
|
105
106
|
const window = 200_000;
|
|
106
107
|
const at = (remaining) => context(sm, undefined, { tokens: window - remaining, percent: 0, contextWindow: window }, true, fixture.cwd);
|
|
107
108
|
// The degraded reminder is Pi's default reserve + default margin = 40960.
|
|
@@ -121,6 +122,6 @@ test("the warning supersedes the early reminder when usage jumps across both thr
|
|
|
121
122
|
assert.deepEqual(warningResult?.messages.map((message) => message.customType), [internal.WARNING_TYPE]);
|
|
122
123
|
await commitTurnEndBoundary(captured, sm, ctx);
|
|
123
124
|
const reloaded = makeExtension(sm);
|
|
124
|
-
runHandlers(reloaded, "context", {}, ctx);
|
|
125
|
+
await runHandlers(reloaded, "context", {}, ctx);
|
|
125
126
|
assert.equal(reloaded.sent.length, 0, "persisted warning also suppresses a late reminder after reload");
|
|
126
127
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/test/doctor.test.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { main } from "../src/dream/cli.js";
|
|
3
3
|
import { doctor } from "../src/dream/doctor.js";
|
|
4
|
-
import { existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
-
const note = "---\norigin: self\nstatus: active\nstale: false\
|
|
8
|
+
const note = "---\norigin: self\nstatus: active\nstale: false\ncreatedAt: 2026-01-01T00:00:00Z\nupdatedAt: 2026-01-01T00:00:00Z\nlastAccessed: 2026-01-01T00:00:00Z\naccessCount: 0\n---\n\n";
|
|
9
|
+
test("doctor identifies legacy metadata as requiring manual migration", (t) => {
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), "dream-doctor-legacy-"));
|
|
11
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
12
|
+
mkdirSync(join(root, "human"));
|
|
13
|
+
writeFileSync(join(root, "human/legacy.md"), note.replace("createdAt", "created_at"));
|
|
14
|
+
const issues = doctor(root);
|
|
15
|
+
assert.ok(issues.some((issue) => issue.includes("legacy metadata key created_at; manually migrate to createdAt")));
|
|
16
|
+
});
|
|
9
17
|
test("doctor validates without repairing files or running the dreamer", async () => {
|
|
10
18
|
const root = mkdtempSync(join(tmpdir(), "dream-doctor-test-"));
|
|
11
19
|
mkdirSync(join(root, "human"));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { loadSkillsFromDir } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { loadPlaybook } from "../src/dream/runner.js";
|
|
10
|
+
function packageRoot() {
|
|
11
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
while (!existsSync(join(dir, "package.json"))) {
|
|
13
|
+
const parent = dirname(dir);
|
|
14
|
+
if (parent === dir)
|
|
15
|
+
throw new Error("could not locate package root");
|
|
16
|
+
dir = parent;
|
|
17
|
+
}
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
function discoverDreamSkill(skillsDir) {
|
|
21
|
+
const result = loadSkillsFromDir({ dir: skillsDir, source: "path" });
|
|
22
|
+
assert.deepEqual(result.diagnostics, []);
|
|
23
|
+
const skill = result.skills.find((candidate) => candidate.name === "dream");
|
|
24
|
+
assert.ok(skill, "Pi's public skill loader discovers dream");
|
|
25
|
+
assert.ok(skill.description.length > 0);
|
|
26
|
+
return skill;
|
|
27
|
+
}
|
|
28
|
+
test("dream skill is publicly discoverable and resolves the CLI's shared playbook", () => {
|
|
29
|
+
const root = packageRoot();
|
|
30
|
+
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
31
|
+
assert.deepEqual(manifest.pi.skills, ["./skills"]);
|
|
32
|
+
assert.deepEqual(manifest.pi.extensions, ["./dist/extension.js"]);
|
|
33
|
+
assert.equal(manifest.bin.dream, "dist/src/dream/cli.js");
|
|
34
|
+
assert.ok(manifest.files.includes("skills"));
|
|
35
|
+
const skill = discoverDreamSkill(join(root, "skills"));
|
|
36
|
+
const skillPlaybook = resolve(skill.baseDir, "../../playbook.md");
|
|
37
|
+
const packagePlaybook = resolve(root, "playbook.md");
|
|
38
|
+
assert.equal(skillPlaybook, packagePlaybook);
|
|
39
|
+
const playbook = readFileSync(packagePlaybook, "utf8");
|
|
40
|
+
assert.equal(loadPlaybook(skillPlaybook), playbook);
|
|
41
|
+
});
|
|
42
|
+
test("npm tarball keeps the skill's relative playbook path portable after extraction", () => {
|
|
43
|
+
const root = packageRoot();
|
|
44
|
+
const fixture = mkdtempSync(join(tmpdir(), "dream-skill-pack-"));
|
|
45
|
+
try {
|
|
46
|
+
const packed = JSON.parse(execFileSync("npm", ["pack", "--offline", "--ignore-scripts", "--json", "--pack-destination", fixture], {
|
|
47
|
+
cwd: root,
|
|
48
|
+
encoding: "utf8",
|
|
49
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
50
|
+
}))[0];
|
|
51
|
+
const packedPaths = packed.files.map((file) => file.path);
|
|
52
|
+
assert.ok(packedPaths.includes("skills/dream/SKILL.md"));
|
|
53
|
+
assert.ok(packedPaths.includes("playbook.md"));
|
|
54
|
+
assert.ok(packedPaths.includes("dist/extension.js"));
|
|
55
|
+
assert.ok(packedPaths.includes("dist/src/dream/cli.js"));
|
|
56
|
+
const extracted = join(fixture, "extracted");
|
|
57
|
+
execFileSync("mkdir", ["-p", extracted]);
|
|
58
|
+
execFileSync("tar", ["-xzf", join(fixture, packed.filename), "-C", extracted]);
|
|
59
|
+
const installedRoot = join(extracted, "package");
|
|
60
|
+
const installedSkill = discoverDreamSkill(join(installedRoot, "skills"));
|
|
61
|
+
const referencedPlaybook = resolve(installedSkill.baseDir, "../../playbook.md");
|
|
62
|
+
assert.equal(referencedPlaybook, resolve(installedRoot, "playbook.md"));
|
|
63
|
+
assert.equal(loadPlaybook(referencedPlaybook), readFileSync(referencedPlaybook, "utf8"));
|
|
64
|
+
assert.equal(loadPlaybook(referencedPlaybook), readFileSync(join(root, "playbook.md"), "utf8"));
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
rmSync(fixture, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { type ContextUsage, type ExtensionAPI, type ExtensionContext, type RegisteredCommand, SessionManager, SettingsManager, type SessionBoundaryDraft, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
export type ExtensionTestEnvironment = {
|
|
4
|
+
readonly cwd: string;
|
|
5
|
+
readonly agentDir: string;
|
|
6
|
+
beforeEach(): void;
|
|
7
|
+
afterEach(): void;
|
|
8
|
+
newNotesRoot(): string;
|
|
9
|
+
dispose(): void;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Install the process-level settings and notes roots needed by one test file.
|
|
13
|
+
* The caller owns the lifecycle hooks; this helper never registers tests or hooks.
|
|
14
|
+
*/
|
|
15
|
+
export declare function installExtensionTestEnvironment(prefix?: string): ExtensionTestEnvironment;
|
|
16
|
+
export type Notice = {
|
|
17
|
+
message: string;
|
|
18
|
+
type?: "info" | "warning" | "error";
|
|
19
|
+
};
|
|
20
|
+
export declare function writeJson(path: string, value: unknown): void;
|
|
21
|
+
export type SettingsFixture = {
|
|
22
|
+
cwd: string;
|
|
23
|
+
agentDir: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Materialize global (agentDir/settings.json) and project (cwd/.pi/settings.json)
|
|
27
|
+
* settings in temp directories, then read them back through the same public
|
|
28
|
+
* SettingsManager.create the extension uses. Never touches the real ~/.pi.
|
|
29
|
+
*/
|
|
30
|
+
export declare function settingsFixture(options?: {
|
|
31
|
+
global?: Record<string, unknown>;
|
|
32
|
+
project?: Record<string, unknown>;
|
|
33
|
+
reserveTokens?: number;
|
|
34
|
+
}): SettingsFixture;
|
|
35
|
+
export type EventHandler = (event: never, ctx: ExtensionContext) => unknown;
|
|
36
|
+
type SendMessageArg = Parameters<ExtensionAPI["sendMessage"]>[0];
|
|
37
|
+
export type SentMessage = {
|
|
38
|
+
message: SendMessageArg;
|
|
39
|
+
options: {
|
|
40
|
+
triggerTurn?: boolean;
|
|
41
|
+
} | undefined;
|
|
42
|
+
};
|
|
43
|
+
export type Captured = {
|
|
44
|
+
tools: Map<string, ToolDefinition>;
|
|
45
|
+
handlers: Map<string, EventHandler[]>;
|
|
46
|
+
commands: Map<string, CommandOptions>;
|
|
47
|
+
sent: SentMessage[];
|
|
48
|
+
contextMessages: unknown[];
|
|
49
|
+
flags: string[];
|
|
50
|
+
};
|
|
51
|
+
export type CommandOptions = Omit<RegisteredCommand, "name" | "sourceInfo">;
|
|
52
|
+
export type CompactionHookResult = {
|
|
53
|
+
cancel: true;
|
|
54
|
+
} | {
|
|
55
|
+
compaction: {
|
|
56
|
+
summary: string;
|
|
57
|
+
firstKeptEntryId: string | null;
|
|
58
|
+
tokensBefore: number;
|
|
59
|
+
details?: unknown;
|
|
60
|
+
};
|
|
61
|
+
} | undefined;
|
|
62
|
+
/** TypeBox's TSchema does not expose `type`/`required` statically; read them structurally. */
|
|
63
|
+
export declare function objectSchema(tool: ToolDefinition | undefined): {
|
|
64
|
+
type?: string;
|
|
65
|
+
required?: string[];
|
|
66
|
+
} | undefined;
|
|
67
|
+
export declare function manager(persisted?: boolean): SessionManager;
|
|
68
|
+
export declare function makeExtension(sessionManager: SessionManager, settingsManager?: SettingsManager): Captured;
|
|
69
|
+
export declare function explicitBoot(ctx: ExtensionContext, currentWindowId: string, previousWindowId: string | undefined): Promise<string>;
|
|
70
|
+
export declare function context(sessionManager: SessionManager, compact?: ExtensionContext["compact"], usage?: ContextUsage, idle?: boolean, cwd?: string, projectTrusted?: boolean, model?: string | {
|
|
71
|
+
provider: string;
|
|
72
|
+
id: string;
|
|
73
|
+
}): ExtensionContext;
|
|
74
|
+
export declare function noticesOf(ctx: ExtensionContext): Notice[];
|
|
75
|
+
export declare function sentOf(captured: Captured, customType: string): SentMessage[];
|
|
76
|
+
export declare function call(captured: Captured, name: string, params: Record<string, unknown>, ctx: ExtensionContext): Promise<AgentToolResult<unknown>>;
|
|
77
|
+
export declare function resultJson<T>(result: AgentToolResult<unknown>): T;
|
|
78
|
+
/** Assert the delivered wire text fits the tool-output budget, header included for raw reads. */
|
|
79
|
+
export declare function assertWithinBudget(result: AgentToolResult<unknown>, message: string): void;
|
|
80
|
+
/** Decoded raw read response using the shared READ WINDOW grammar. */
|
|
81
|
+
export type ReadWindow = {
|
|
82
|
+
header: string;
|
|
83
|
+
content: string;
|
|
84
|
+
offset_chars: number;
|
|
85
|
+
total_chars: number;
|
|
86
|
+
next_offset_chars: number | null;
|
|
87
|
+
details: Record<string, unknown>;
|
|
88
|
+
};
|
|
89
|
+
/** Decode either raw read without including its shared metadata block in the payload. */
|
|
90
|
+
export declare function resultRead(result: AgentToolResult<unknown>): ReadWindow;
|
|
91
|
+
/**
|
|
92
|
+
* Assert a value is a local-time ISO 8601 string with an explicit numeric offset (never "Z")
|
|
93
|
+
* and that Date.parse restores the stored epoch milliseconds. No time zone is assumed.
|
|
94
|
+
*/
|
|
95
|
+
export declare function assertLocalIso(value: unknown, epochMs: number, message: string): void;
|
|
96
|
+
/** Assert the text contains a well-formed local ISO timestamp and return it, without pinning surrounding wording. */
|
|
97
|
+
export declare function assertIsoTimestamp(text: string, message: string): string;
|
|
98
|
+
/** Assert `actual` is a middle-truncation of `original`: same head, same tail, strictly fewer characters. */
|
|
99
|
+
export declare function assertTruncationOf(original: string, actual: string): void;
|
|
100
|
+
export declare function runManualCompact(captured: Captured, ctx: ExtensionContext): Promise<CompactionHookResult>;
|
|
101
|
+
export declare function runHandlers(captured: Captured, name: string, event: unknown, ctx: ExtensionContext): Promise<void>;
|
|
102
|
+
export declare function runHandlersAsync(captured: Captured, name: string, event: unknown, ctx: ExtensionContext): Promise<unknown[]>;
|
|
103
|
+
export declare function completeRequestedCompaction(ctx: ExtensionContext): void;
|
|
104
|
+
export declare function runCommand(captured: Captured, name: string, args: string, ctx: ExtensionContext): Promise<Notice[]>;
|
|
105
|
+
export type ContextHookResult = {
|
|
106
|
+
messages: unknown[];
|
|
107
|
+
} | undefined;
|
|
108
|
+
export declare function runContextHook(captured: Captured, ctx: ExtensionContext, eventOverride?: Record<string, unknown>): Promise<ContextHookResult>;
|
|
109
|
+
export declare function runContextWithSystemHook(captured: Captured, ctx: ExtensionContext, messages: unknown[]): Promise<ContextHookResult>;
|
|
110
|
+
export declare function commitTurnEndBoundary(captured: Captured, sessionManager: SessionManager, ctx: ExtensionContext): Promise<{
|
|
111
|
+
entries: SessionBoundaryDraft[];
|
|
112
|
+
continue: boolean;
|
|
113
|
+
}>;
|
|
114
|
+
export declare function appendText(sessionManager: SessionManager, role: "user" | "assistant" | "toolResult", text: string, toolName?: string): string;
|
|
115
|
+
export {};
|
|
@@ -4,9 +4,9 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import piContext, { createPiContext } from "../../src/index.js";
|
|
7
|
-
import { loadNotesSnapshot } from "../../src/notes/
|
|
7
|
+
import { loadNotesSnapshot } from "../../src/pi/notes/snapshot.js";
|
|
8
8
|
import { renderBootBlock } from "../../src/context/prompts.js";
|
|
9
|
-
import { agentSlug, modelSlug } from "../../src/notes/
|
|
9
|
+
import { agentSlug, modelSlug } from "../../src/pi/notes/adapter.js";
|
|
10
10
|
import { rootWindowId } from "../../src/context/context-window.js";
|
|
11
11
|
import { TOOL_OUTPUT_MAX_BYTES } from "../../src/tool-output.js";
|
|
12
12
|
let defaultCwd = "/private/tmp/pi-context-test-cwd";
|
|
@@ -130,14 +130,14 @@ export function makeExtension(sessionManager, settingsManager) {
|
|
|
130
130
|
(settingsManager ? createPiContext({ settingsManager }) : piContext)(api);
|
|
131
131
|
return captured;
|
|
132
132
|
}
|
|
133
|
-
export function explicitBoot(ctx, currentWindowId, previousWindowId) {
|
|
133
|
+
export async function explicitBoot(ctx, currentWindowId, previousWindowId) {
|
|
134
134
|
return renderBootBlock({
|
|
135
135
|
agentName: agentSlug(ctx),
|
|
136
136
|
modelName: modelSlug(ctx),
|
|
137
137
|
firstWindowId: rootWindowId(ctx.sessionManager.getSessionId()),
|
|
138
138
|
currentWindowId,
|
|
139
139
|
previousWindowId,
|
|
140
|
-
notes: loadNotesSnapshot(ctx),
|
|
140
|
+
notes: await loadNotesSnapshot(ctx),
|
|
141
141
|
});
|
|
142
142
|
}
|
|
143
143
|
export function context(sessionManager, compact, usage, idle = true, cwd = defaultCwd, projectTrusted = true, model) {
|
|
@@ -270,13 +270,13 @@ export async function runManualCompact(captured, ctx) {
|
|
|
270
270
|
};
|
|
271
271
|
return (await handler(event, ctx));
|
|
272
272
|
}
|
|
273
|
-
export function runHandlers(captured, name, event, ctx) {
|
|
273
|
+
export async function runHandlers(captured, name, event, ctx) {
|
|
274
274
|
const isIdle = ctx.isIdle;
|
|
275
275
|
if (name === "agent_settled")
|
|
276
276
|
ctx.isIdle = () => true;
|
|
277
277
|
try {
|
|
278
278
|
for (const handler of captured.handlers.get(name) ?? [])
|
|
279
|
-
handler(event, ctx);
|
|
279
|
+
await handler(event, ctx);
|
|
280
280
|
}
|
|
281
281
|
finally {
|
|
282
282
|
ctx.isIdle = isIdle;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type NoteRow, type NotesQuery, type Scope } from "../../src/notes/index.js";
|
|
3
|
+
/** Test-only conveniences for fixtures that still model notes through a live Pi context. */
|
|
4
|
+
export declare function physicalPath(scope: Scope, path: string, ctx: ExtensionContext, who?: string): string;
|
|
5
|
+
export declare function scopeDir(scope: Scope, ctx: ExtensionContext, who?: string): string;
|
|
6
|
+
export declare function listNotes(ctx: ExtensionContext, query?: NotesQuery): Promise<NoteRow[]>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createNotesStore } from "../../src/notes/index.js";
|
|
2
|
+
import { notesContextFromPi } from "../../src/pi/notes/adapter.js";
|
|
3
|
+
import { physicalPath as corePhysicalPath, scopeDir as coreScopeDir } from "../../src/notes/paths.js";
|
|
4
|
+
/** Test-only conveniences for fixtures that still model notes through a live Pi context. */
|
|
5
|
+
export function physicalPath(scope, path, ctx, who) {
|
|
6
|
+
return corePhysicalPath(scope, path, notesContextFromPi(ctx), who);
|
|
7
|
+
}
|
|
8
|
+
export function scopeDir(scope, ctx, who) {
|
|
9
|
+
return coreScopeDir(scope, notesContextFromPi(ctx), who);
|
|
10
|
+
}
|
|
11
|
+
export function listNotes(ctx, query = {}) {
|
|
12
|
+
return createNotesStore(notesContextFromPi(ctx)).list(query);
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|