@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
|
@@ -109,22 +109,25 @@ function appendDrafts(sessionManager, entries) {
|
|
|
109
109
|
}
|
|
110
110
|
function turnEndFacts(overrides = {}) {
|
|
111
111
|
return {
|
|
112
|
+
windowId: "pcw:test:window",
|
|
112
113
|
aborted: false,
|
|
113
114
|
overflow: false,
|
|
114
115
|
failed: false,
|
|
115
116
|
enabled: true,
|
|
116
117
|
queued: false,
|
|
117
118
|
automaticResetEnabled: true,
|
|
118
|
-
|
|
119
|
+
hardReserveDue: false,
|
|
119
120
|
...overrides,
|
|
120
121
|
};
|
|
121
122
|
}
|
|
122
123
|
function beforeSettleFacts(overrides = {}) {
|
|
123
124
|
return {
|
|
125
|
+
windowId: "pcw:test:window",
|
|
124
126
|
queued: false,
|
|
125
127
|
enabled: true,
|
|
126
128
|
automaticResetEnabled: true,
|
|
127
129
|
aborted: false,
|
|
130
|
+
failed: false,
|
|
128
131
|
...overrides,
|
|
129
132
|
};
|
|
130
133
|
}
|
|
@@ -156,14 +159,15 @@ test("public reset boundary drafts one marker, one boot, and one continuation af
|
|
|
156
159
|
assert.equal(markerDrafts.length, 1, "duplicate wipe requests in one turn dedupe");
|
|
157
160
|
assert.equal(bootDrafts.length, 1);
|
|
158
161
|
assert.equal(boundary.entries[0]?.type, "custom_message", "ordinary tool-batch entries precede the reset drafts");
|
|
159
|
-
assert.equal(boundary.entries[1]?.type, "
|
|
160
|
-
assert.equal(boundary.entries[2]?.type, "
|
|
162
|
+
assert.equal(boundary.entries[1]?.type, "compaction", "native retain-none checkpoint precedes the reset marker");
|
|
163
|
+
assert.equal(boundary.entries[2]?.type, "custom");
|
|
164
|
+
assert.equal(boundary.entries[3]?.type, "custom_message");
|
|
161
165
|
const windowId = markerDrafts[0].data.windowId;
|
|
162
166
|
assert.match(windowId, /^pcw:/);
|
|
163
167
|
assert.equal(bootDrafts[0].details.windowId, windowId);
|
|
164
168
|
const continuationDrafts = boundary.entries.filter((entry) => entry.type === "custom_message" && entry.customType === internal.CONTINUATION_TYPE);
|
|
165
169
|
assert.equal(continuationDrafts.length, 1, "the boundary persists exactly one reset message");
|
|
166
|
-
assert.equal(boundary.entries[
|
|
170
|
+
assert.equal(boundary.entries[4]?.type, "custom_message", "the continuation closes the ordered reset shape");
|
|
167
171
|
appendDrafts(h.sessionManager, boundary.entries);
|
|
168
172
|
const branch = h.sessionManager.getBranch();
|
|
169
173
|
assert.deepEqual(branch.filter((entry) => entry.type === "custom" && entry.customType === internal.RESET_MARKER_TYPE).map((entry) => entry.type === "custom" ? entry.data : undefined), [{ windowId }]);
|
|
@@ -189,12 +193,14 @@ test("off stops future automatic/manual reset requests while an existing marker
|
|
|
189
193
|
assert.equal(afterOff.entries.length, 0, "off does not create another reset");
|
|
190
194
|
await h.runCommand("pi-context", "on");
|
|
191
195
|
await h.runCommand("wipe-memory");
|
|
192
|
-
assert.equal(h.sent.length,
|
|
193
|
-
assert.equal(h.sent[0]?.triggerTurn,
|
|
194
|
-
assert.equal(h.sent[0]?.customType, internal.
|
|
195
|
-
|
|
196
|
+
assert.equal(h.sent.length, 1, "/wipe-memory sends one hidden close-out warning and starts a normal turn");
|
|
197
|
+
assert.equal(h.sent[0]?.triggerTurn, true);
|
|
198
|
+
assert.equal(h.sent[0]?.customType, internal.WARNING_TYPE);
|
|
199
|
+
const warning = h.sessionManager.getBranch().find((entry) => entry.type === "custom_message" && entry.customType === internal.WARNING_TYPE);
|
|
200
|
+
assert.ok(warning && warning.type === "custom_message");
|
|
201
|
+
assert.equal(warning.content, internal.WARNING_CONTENT);
|
|
196
202
|
const markers = h.sessionManager.getBranch().filter((entry) => entry.type === "custom" && entry.customType === internal.RESET_MARKER_TYPE);
|
|
197
|
-
assert.equal(markers.length,
|
|
203
|
+
assert.equal(markers.length, 1, "manual command waits for an agent boundary; it does not persist an immediate reset");
|
|
198
204
|
});
|
|
199
205
|
test("reset construction failure preserves incoming and budget drafts without continuation", async () => {
|
|
200
206
|
const sessionManager = Manager.inMemory("/private/tmp/pi-context-reset-failure-test");
|
|
@@ -220,13 +226,13 @@ test("reset construction failure preserves incoming and budget drafts without co
|
|
|
220
226
|
isEnabled: () => true,
|
|
221
227
|
budget: {
|
|
222
228
|
automaticResetEnabled: () => true,
|
|
223
|
-
|
|
229
|
+
hardReserveDue: () => false,
|
|
224
230
|
consumeTurnEnd: () => [budgetDraft],
|
|
225
231
|
clear: () => { },
|
|
226
232
|
},
|
|
227
233
|
buildReset: () => { throw new Error("synthetic reset construction failure"); },
|
|
228
234
|
});
|
|
229
|
-
lifecycle.request();
|
|
235
|
+
lifecycle.request(`pcw:${sessionManager.getSessionId().slice(0, 8)}:root`);
|
|
230
236
|
const incoming = { type: "custom_message", customType: "foreign/boundary", content: "foreign draft", display: false };
|
|
231
237
|
const results = [];
|
|
232
238
|
for (const handler of handlers.get("turn_end") ?? [])
|
|
@@ -237,6 +243,52 @@ test("reset construction failure preserves incoming and budget drafts without co
|
|
|
237
243
|
assert.equal(notices.at(-1)?.type, "warning");
|
|
238
244
|
assert.match(notices.at(-1)?.message ?? "", /could not build reset/);
|
|
239
245
|
});
|
|
246
|
+
test("a stale async reset is discarded after a lifecycle switch without staging a notice", async () => {
|
|
247
|
+
const sessionManager = Manager.inMemory("/private/tmp/pi-context-stale-reset-test");
|
|
248
|
+
const handlers = new Map();
|
|
249
|
+
const api = {
|
|
250
|
+
on(name, handler) {
|
|
251
|
+
const list = handlers.get(name) ?? [];
|
|
252
|
+
list.push(handler);
|
|
253
|
+
handlers.set(name, list);
|
|
254
|
+
return () => { };
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
const ctx = {
|
|
258
|
+
sessionManager,
|
|
259
|
+
model: undefined,
|
|
260
|
+
signal: undefined,
|
|
261
|
+
hasPendingMessages: () => false,
|
|
262
|
+
ui: { notify() { } },
|
|
263
|
+
};
|
|
264
|
+
let resolveBuild;
|
|
265
|
+
const pendingBuild = new Promise((resolve) => { resolveBuild = resolve; });
|
|
266
|
+
let readyCount = 0;
|
|
267
|
+
const lifecycle = registerResetLifecycle(api, {
|
|
268
|
+
isEnabled: () => true,
|
|
269
|
+
budget: {
|
|
270
|
+
automaticResetEnabled: () => true,
|
|
271
|
+
hardReserveDue: () => false,
|
|
272
|
+
consumeTurnEnd: () => [],
|
|
273
|
+
clear: () => { },
|
|
274
|
+
},
|
|
275
|
+
buildReset: () => pendingBuild,
|
|
276
|
+
onResetReady: () => { readyCount++; },
|
|
277
|
+
});
|
|
278
|
+
const windowId = `pcw:${sessionManager.getSessionId().slice(0, 8)}:root`;
|
|
279
|
+
lifecycle.request(windowId);
|
|
280
|
+
const incoming = { type: "custom_message", customType: "foreign/boundary", content: "preserve me", display: false };
|
|
281
|
+
const turnEnd = handlers.get("turn_end")?.[0];
|
|
282
|
+
const sessionTree = handlers.get("session_tree")?.[0];
|
|
283
|
+
assert.ok(turnEnd && sessionTree);
|
|
284
|
+
const pending = turnEnd(fakeBoundaryEvent([incoming]), ctx);
|
|
285
|
+
await sessionTree({}, ctx);
|
|
286
|
+
resolveBuild([{ type: "custom_message", customType: internal.BOOT_TYPE, content: "stale", display: false }]);
|
|
287
|
+
const result = resultEntries([await pending]);
|
|
288
|
+
assert.deepEqual(result.entries, [incoming], "a switched lifecycle keeps incoming drafts but discards stale reset drafts");
|
|
289
|
+
assert.equal(result.continue, false);
|
|
290
|
+
assert.equal(readyCount, 0, "stale reset work cannot stage a success notice");
|
|
291
|
+
});
|
|
240
292
|
test("a queued success clears an overflow failure before settle recovery can reset", async () => {
|
|
241
293
|
const sessionManager = Manager.inMemory("/private/tmp/pi-context-queued-overflow-test");
|
|
242
294
|
const handlers = new Map();
|
|
@@ -260,7 +312,7 @@ test("a queued success clears an overflow failure before settle recovery can res
|
|
|
260
312
|
isEnabled: () => true,
|
|
261
313
|
budget: {
|
|
262
314
|
automaticResetEnabled: () => true,
|
|
263
|
-
|
|
315
|
+
hardReserveDue: () => false,
|
|
264
316
|
consumeTurnEnd: () => [],
|
|
265
317
|
clear: () => { },
|
|
266
318
|
},
|
|
@@ -288,55 +340,63 @@ test("a queued success clears an overflow failure before settle recovery can res
|
|
|
288
340
|
assert.equal(await beforeSettle(settled, ctx), undefined, "the successful queued turn clears the stale recovery");
|
|
289
341
|
assert.equal(resetCount, 0);
|
|
290
342
|
});
|
|
291
|
-
test("reset-control:
|
|
292
|
-
const
|
|
293
|
-
const first = reduceResetControl(
|
|
294
|
-
assert.equal(first.effect, "
|
|
295
|
-
assert.deepEqual(first.state, {
|
|
296
|
-
const
|
|
297
|
-
assert.equal(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
assert.
|
|
301
|
-
|
|
343
|
+
test("reset-control: close-out phases deduplicate, span turns, and upgrade to a tool commit", () => {
|
|
344
|
+
const windowId = "pcw:test:window";
|
|
345
|
+
const first = reduceResetControl(initialResetControl(), { type: "close_out", windowId, source: "manual" });
|
|
346
|
+
assert.equal(first.effect, "close-out-armed");
|
|
347
|
+
assert.deepEqual(first.state.request, { phase: "close-out", windowId, source: "manual" });
|
|
348
|
+
const duplicate = reduceResetControl(first.state, { type: "close_out", windowId, source: "manual" });
|
|
349
|
+
assert.equal(duplicate.effect, "already-pending");
|
|
350
|
+
const noteTurn = reduceResetControl(first.state, { type: "turn_end", facts: turnEndFacts() });
|
|
351
|
+
assert.equal(noteTurn.effect, "none");
|
|
352
|
+
assert.deepEqual(noteTurn.state.request, first.state.request, "note/tool turns do not consume close-out");
|
|
353
|
+
const tool = reduceResetControl(noteTurn.state, { type: "tool_request", windowId });
|
|
354
|
+
assert.deepEqual(tool.state.request, { phase: "tool-requested", windowId });
|
|
355
|
+
const commit = reduceResetControl(tool.state, { type: "turn_end", facts: turnEndFacts() });
|
|
356
|
+
assert.equal(commit.effect, "commit-boundary");
|
|
357
|
+
assert.deepEqual(commit.state, initialResetControl());
|
|
302
358
|
});
|
|
303
|
-
test("reset-control:
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
assert.equal(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
assert.equal(
|
|
312
|
-
assert.deepEqual(
|
|
313
|
-
const
|
|
314
|
-
assert.equal(
|
|
315
|
-
|
|
359
|
+
test("reset-control: fallback is normal-stop only; hard reserve remains safety", () => {
|
|
360
|
+
const windowId = "pcw:test:window";
|
|
361
|
+
const manual = reduceResetControl(initialResetControl(), { type: "close_out", windowId, source: "manual" }).state;
|
|
362
|
+
const fallback = reduceResetControl(manual, { type: "before_settle", facts: beforeSettleFacts() });
|
|
363
|
+
assert.equal(fallback.effect, "commit-boundary", "manual close-out works even when automatic resets are disabled");
|
|
364
|
+
assert.deepEqual(fallback.state, initialResetControl());
|
|
365
|
+
const automatic = reduceResetControl(initialResetControl(), { type: "close_out", windowId, source: "automatic" }).state;
|
|
366
|
+
const disabledAuto = reduceResetControl(automatic, { type: "before_settle", facts: beforeSettleFacts({ automaticResetEnabled: false }) });
|
|
367
|
+
assert.equal(disabledAuto.effect, "none");
|
|
368
|
+
assert.deepEqual(disabledAuto.state, initialResetControl());
|
|
369
|
+
const safety = reduceResetControl(initialResetControl(), { type: "turn_end", facts: turnEndFacts({ hardReserveDue: true }) });
|
|
370
|
+
assert.equal(safety.effect, "commit-boundary");
|
|
371
|
+
for (const outcome of [{ failed: true }, { aborted: true }]) {
|
|
372
|
+
const notNormalStop = reduceResetControl(manual, { type: "before_settle", facts: beforeSettleFacts(outcome) });
|
|
373
|
+
assert.equal(notNormalStop.effect, "none");
|
|
374
|
+
assert.deepEqual(notNormalStop.state, initialResetControl());
|
|
375
|
+
}
|
|
376
|
+
assert.deepEqual(reduceResetControl(manual, { type: "settled" }).state, initialResetControl());
|
|
316
377
|
});
|
|
317
|
-
test("reset-control:
|
|
318
|
-
const
|
|
319
|
-
const
|
|
378
|
+
test("reset-control: abort and lifecycle transitions clear every pending phase", () => {
|
|
379
|
+
const windowId = "pcw:test:window";
|
|
380
|
+
const pending = reduceResetControl(initialResetControl(), { type: "close_out", windowId, source: "manual" }).state;
|
|
381
|
+
const aborted = reduceResetControl(pending, { type: "turn_end", facts: turnEndFacts({ aborted: true }) });
|
|
320
382
|
assert.equal(aborted.effect, "none");
|
|
321
|
-
assert.deepEqual(aborted.state, initialResetControl()
|
|
322
|
-
|
|
323
|
-
assert.deepEqual(
|
|
324
|
-
const cleared = reduceResetControl(both, { type: "clear" });
|
|
325
|
-
assert.deepEqual(cleared.state, initialResetControl());
|
|
383
|
+
assert.deepEqual(aborted.state, initialResetControl());
|
|
384
|
+
assert.deepEqual(reduceResetControl(pending, { type: "clear" }).state, initialResetControl());
|
|
385
|
+
assert.deepEqual(reduceResetControl(pending, { type: "settled" }).state, initialResetControl());
|
|
326
386
|
});
|
|
327
387
|
test("reset-control: overflow recovery is armed at turn_end and spent exactly once at settle", () => {
|
|
328
388
|
const armed = reduceResetControl(initialResetControl(), { type: "turn_end", facts: turnEndFacts({ overflow: true, failed: true }) });
|
|
329
|
-
assert.deepEqual(armed.state, { request: "none", overflow: "pending" });
|
|
389
|
+
assert.deepEqual(armed.state, { request: { phase: "none" }, overflow: "pending" });
|
|
330
390
|
const recovered = reduceResetControl(armed.state, { type: "before_settle", facts: beforeSettleFacts() });
|
|
331
391
|
assert.equal(recovered.effect, "recover-overflow", "the first settle commits the bounded recovery");
|
|
332
|
-
assert.deepEqual(recovered.state, { request: "none", overflow: "spent" });
|
|
392
|
+
assert.deepEqual(recovered.state, { request: { phase: "none" }, overflow: "spent" });
|
|
333
393
|
const repeated = reduceResetControl(recovered.state, { type: "before_settle", facts: beforeSettleFacts() });
|
|
334
394
|
assert.equal(repeated.effect, "none", "a spent recovery is never retried");
|
|
335
395
|
const rearmed = reduceResetControl(recovered.state, { type: "turn_end", facts: turnEndFacts({ overflow: true, failed: true }) });
|
|
336
|
-
assert.deepEqual(rearmed.state, { request: "none", overflow: "pending-spent" });
|
|
396
|
+
assert.deepEqual(rearmed.state, { request: { phase: "none" }, overflow: "pending-spent" });
|
|
337
397
|
const bounded = reduceResetControl(rearmed.state, { type: "before_settle", facts: beforeSettleFacts() });
|
|
338
398
|
assert.equal(bounded.effect, "none", "a second failure chain stays bounded to the spent attempt");
|
|
339
|
-
assert.deepEqual(bounded.state, { request: "none", overflow: "spent" });
|
|
399
|
+
assert.deepEqual(bounded.state, { request: { phase: "none" }, overflow: "spent" });
|
|
340
400
|
});
|
|
341
401
|
test("reset-control: a queued turn defers recovery and its success supersedes the failure", () => {
|
|
342
402
|
const armed = reduceResetControl(initialResetControl(), { type: "turn_end", facts: turnEndFacts({ overflow: true, failed: true }) }).state;
|
|
@@ -350,52 +410,40 @@ test("reset-control: disabled mode and explicit aborts disarm overflow without a
|
|
|
350
410
|
const armed = reduceResetControl(initialResetControl(), { type: "turn_end", facts: turnEndFacts({ overflow: true, failed: true }) }).state;
|
|
351
411
|
const disabled = reduceResetControl(armed, { type: "before_settle", facts: beforeSettleFacts({ enabled: false }) });
|
|
352
412
|
assert.equal(disabled.effect, "none");
|
|
353
|
-
assert.deepEqual(disabled.state, { request: "none", overflow: "idle" });
|
|
413
|
+
assert.deepEqual(disabled.state, { request: { phase: "none" }, overflow: "idle" });
|
|
354
414
|
const automaticOff = reduceResetControl(armed, { type: "before_settle", facts: beforeSettleFacts({ automaticResetEnabled: false }) });
|
|
355
415
|
assert.equal(automaticOff.effect, "none");
|
|
356
|
-
assert.deepEqual(automaticOff.state, { request: "none", overflow: "idle" });
|
|
416
|
+
assert.deepEqual(automaticOff.state, { request: { phase: "none" }, overflow: "idle" });
|
|
357
417
|
const aborted = reduceResetControl(armed, { type: "before_settle", facts: beforeSettleFacts({ aborted: true }) });
|
|
358
418
|
assert.equal(aborted.effect, "none");
|
|
359
|
-
assert.deepEqual(aborted.state, { request: "none", overflow: "idle" });
|
|
419
|
+
assert.deepEqual(aborted.state, { request: { phase: "none" }, overflow: "idle" });
|
|
360
420
|
});
|
|
361
|
-
test("reset-control:
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
};
|
|
373
|
-
return { facts, calls };
|
|
421
|
+
test("reset-control: lazy guards preserve overflow policy ordering", () => {
|
|
422
|
+
const calls = { queued: 0, automatic: 0, hardReserve: 0 };
|
|
423
|
+
const facts = {
|
|
424
|
+
windowId: "pcw:test:window",
|
|
425
|
+
aborted: false,
|
|
426
|
+
overflow: true,
|
|
427
|
+
failed: true,
|
|
428
|
+
enabled: true,
|
|
429
|
+
get queued() { calls.queued += 1; return false; },
|
|
430
|
+
get automaticResetEnabled() { calls.automatic += 1; return true; },
|
|
431
|
+
get hardReserveDue() { calls.hardReserve += 1; return true; },
|
|
374
432
|
};
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
assert.deepEqual(aborted.calls, { queued: 0, automatic: 0, threshold: 0 }, "an abort consults no policy guard");
|
|
378
|
-
const failed = tracked({ failed: true });
|
|
379
|
-
reduceResetControl(initialResetControl(), { type: "turn_end", facts: failed.facts });
|
|
380
|
-
assert.deepEqual(failed.calls, { queued: 0, automatic: 0, threshold: 0 }, "a failed turn consults no threshold guard");
|
|
381
|
-
const disabled = tracked({ enabled: false });
|
|
382
|
-
reduceResetControl(initialResetControl(), { type: "turn_end", facts: disabled.facts });
|
|
383
|
-
assert.deepEqual(disabled.calls, { queued: 0, automatic: 0, threshold: 0 }, "a disabled turn consults no threshold guard");
|
|
384
|
-
const overflow = tracked({ overflow: true, queuedResult: false });
|
|
385
|
-
reduceResetControl(initialResetControl(), { type: "turn_end", facts: overflow.facts });
|
|
386
|
-
assert.deepEqual(overflow.calls, { queued: 1, automatic: 1, threshold: 0 }, "an overflow turn consults only the overflow guards");
|
|
387
|
-
const complete = tracked({ thresholdResult: false });
|
|
388
|
-
reduceResetControl(initialResetControl(), { type: "turn_end", facts: complete.facts });
|
|
389
|
-
assert.deepEqual(complete.calls, { queued: 0, automatic: 0, threshold: 1 }, "a completed turn consults only the threshold guard");
|
|
433
|
+
reduceResetControl(initialResetControl(), { type: "turn_end", facts });
|
|
434
|
+
assert.deepEqual(calls, { queued: 1, automatic: 1, hardReserve: 0 }, "overflow checks never consult hard-reserve policy");
|
|
390
435
|
const settleCalls = { queued: 0, enabled: 0, automatic: 0 };
|
|
391
436
|
const settleFacts = {
|
|
437
|
+
windowId: "pcw:test:window",
|
|
392
438
|
get queued() { settleCalls.queued += 1; return true; },
|
|
393
439
|
get enabled() { settleCalls.enabled += 1; return true; },
|
|
394
440
|
get automaticResetEnabled() { settleCalls.automatic += 1; return true; },
|
|
395
441
|
aborted: false,
|
|
442
|
+
failed: false,
|
|
396
443
|
};
|
|
397
|
-
reduceResetControl({
|
|
398
|
-
|
|
444
|
+
const pendingOverflow = reduceResetControl(initialResetControl(), { type: "turn_end", facts: turnEndFacts({ overflow: true, failed: true }) }).state;
|
|
445
|
+
reduceResetControl(pendingOverflow, { type: "before_settle", facts: settleFacts });
|
|
446
|
+
assert.deepEqual(settleCalls, { queued: 1, enabled: 0, automatic: 0 }, "queued settle checks no further overflow policy");
|
|
399
447
|
});
|
|
400
448
|
test("a committed reset places incoming and budget drafts before marker -> boot -> continuation", async () => {
|
|
401
449
|
const sessionManager = Manager.inMemory("/private/tmp/pi-context-reset-ordering-test");
|
|
@@ -421,17 +469,18 @@ test("a committed reset places incoming and budget drafts before marker -> boot
|
|
|
421
469
|
isEnabled: () => true,
|
|
422
470
|
budget: {
|
|
423
471
|
automaticResetEnabled: () => true,
|
|
424
|
-
|
|
472
|
+
hardReserveDue: () => false,
|
|
425
473
|
consumeTurnEnd: () => [budgetDraft],
|
|
426
474
|
clear: () => { },
|
|
427
475
|
},
|
|
428
476
|
buildReset: () => [
|
|
477
|
+
{ type: "compaction", summary: "", firstKeptEntryId: null },
|
|
429
478
|
{ type: "custom", customType: internal.RESET_MARKER_TYPE, data: { windowId } },
|
|
430
479
|
{ type: "custom_message", customType: internal.BOOT_TYPE, content: "boot", display: false, details: { windowId } },
|
|
431
480
|
{ type: "custom_message", customType: internal.CONTINUATION_TYPE, content: "continuation", display: false },
|
|
432
481
|
],
|
|
433
482
|
});
|
|
434
|
-
lifecycle.request();
|
|
483
|
+
lifecycle.request(`pcw:${sessionManager.getSessionId().slice(0, 8)}:root`);
|
|
435
484
|
const incoming = { type: "custom_message", customType: "foreign/boundary", content: "incoming", display: false };
|
|
436
485
|
const results = [];
|
|
437
486
|
for (const handler of handlers.get("turn_end") ?? [])
|
|
@@ -439,12 +488,15 @@ test("a committed reset places incoming and budget drafts before marker -> boot
|
|
|
439
488
|
const result = resultEntries(results);
|
|
440
489
|
assert.equal(result.continue, true);
|
|
441
490
|
const customTypes = (entries) => entries.map((entry) => {
|
|
442
|
-
|
|
491
|
+
if (entry.type === "compaction")
|
|
492
|
+
return "native-compaction";
|
|
493
|
+
assert.ok(entry.type === "custom" || entry.type === "custom_message", "the boundary only carries expected reset drafts here");
|
|
443
494
|
return entry.customType;
|
|
444
495
|
});
|
|
445
496
|
assert.deepEqual(customTypes(result.entries), [
|
|
446
497
|
"foreign/boundary",
|
|
447
498
|
internal.GUIDANCE_TYPE,
|
|
499
|
+
"native-compaction",
|
|
448
500
|
internal.RESET_MARKER_TYPE,
|
|
449
501
|
internal.BOOT_TYPE,
|
|
450
502
|
internal.CONTINUATION_TYPE,
|
|
@@ -452,7 +504,12 @@ test("a committed reset places incoming and budget drafts before marker -> boot
|
|
|
452
504
|
appendDrafts(sessionManager, result.entries);
|
|
453
505
|
const branch = sessionManager.getBranch();
|
|
454
506
|
const markerIndex = branch.findIndex((entry) => entry.type === "custom" && entry.customType === internal.RESET_MARKER_TYPE);
|
|
455
|
-
assert.ok(markerIndex
|
|
507
|
+
assert.ok(markerIndex > 0);
|
|
508
|
+
assert.equal(branch[markerIndex - 1]?.type, "compaction", "the checkpoint is directly before the marker");
|
|
509
|
+
const checkpoint = branch[markerIndex - 1];
|
|
510
|
+
assert.ok(checkpoint?.type === "compaction");
|
|
511
|
+
assert.equal(checkpoint.summary, "");
|
|
512
|
+
assert.equal(checkpoint.firstKeptEntryId, checkpoint.id, "Pi materializes null as retain-none");
|
|
456
513
|
assert.deepEqual(customTypes(branch.slice(markerIndex)), [
|
|
457
514
|
internal.RESET_MARKER_TYPE,
|
|
458
515
|
internal.BOOT_TYPE,
|
package/docs/architecture.md
CHANGED
|
@@ -29,30 +29,30 @@ Dependencies flow from the composition root and tool adapters to projections and
|
|
|
29
29
|
|
|
30
30
|
## State and persistence
|
|
31
31
|
|
|
32
|
-
`
|
|
32
|
+
`reset-lifecycle.ts` is the sole `turn_end` composer: it accepts incoming drafts, drains budget-owned guidance/warning drafts, and appends a reset boundary only for an explicit tool request or the hard-reserve safety path. Manual and budget close-outs stay armed across note/tool turns and fall back from `agent_before_settle` only after a successful stop and queued work are drained. Repeated requests for the same window deduplicate. See [reset lifecycle](reset-lifecycle.md).
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
A new reset is a native empty-summary compaction checkpoint configured to retain none, followed by one `pi-context/reset-marker` custom entry with `{ windowId: string }`, a hidden `pi-context/boot` whose `details.windowId` matches, and a hidden continuation. The checkpoint is the canonical-history cut; the marker is the durable logical window identity. Raw session entries remain available to history tools. Legacy marker-only branches keep a narrow projection fallback. `context/context-window.ts` owns marker/checkpoint validation, active-branch scan, root/current IDs, and per-window message lookup; `history/history.ts` consumes those identity primitives while projecting entries. The scan never uses a global entry tail.
|
|
35
35
|
|
|
36
36
|
The runtime in `context/runtime.ts` performs final context projection by selecting the active boot through `details.windowId` and folding only the dropped system prefix through Pi's `getCurrentSystemMessage`. Later prompt patches and new messages stay in order. A missing boot aborts the hook with a safe head and notice rather than silently sending raw history. Startup/tree handling repairs only a genuinely incomplete marker tail: a missing boot with no later raw message, custom message, compaction, branch summary, or authoritative raw boot. If later work exists, boot creation is refused and `/wipe-memory` is the explicit recovery path; it does not parse or migrate the legacy reset-v2 protocol.
|
|
37
37
|
|
|
38
38
|
Boot and reminder deduplication inspect the current branch-local window. Reloading JSONL therefore does not duplicate messages, while navigation to a sibling branch cannot inherit another branch's window state. A fork/clone receives a new session ID while copying its selected path, so startup must also verify that a root boot's `details.windowId` matches the new `rootWindowId(sessionId)` before treating it as present.
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
Manual `/wipe-memory` first waits for idle, then records the same hidden warning used by budget-triggered close-out and starts an ordinary model turn. The agent can checkpoint notes through multiple turns; an explicit `wipe_memory` commits at `turn_end`, while a successful stop without the tool falls back at `agent_before_settle`. Abort/error never counts as successful close-out. The final checkpoint, marker, boot, and continuation are emitted as ordered session-boundary drafts. Pi's retain-none compaction entry persists the canonical cut; the marker/boot preserve pi-context's logical identity and protocol.
|
|
41
41
|
|
|
42
|
-
Boot is a fixed snapshot for its window, stored as an extension custom message and converted by Pi to a user-role model message. History's `developer` classification records extension authorship, not provider instruction priority. A system-role projection is technically possible through `context_with_system`, but would change the authority of the mixed protocol/MAP content and provider-specific prompt/cache behavior; it is not a prerequisite for persistence. Boot injection is silent, including startup, reload, and boot repair. Only an actual context reset produces `pi-context: memory cleared · <windowId>` through `ctx.ui.notify`, after
|
|
42
|
+
Boot is a fixed snapshot for its window, stored as an extension custom message and converted by Pi to a user-role model message. History's `developer` classification records extension authorship, not provider instruction priority. A system-role projection is technically possible through `context_with_system`, but would change the authority of the mixed protocol/MAP content and provider-specific prompt/cache behavior; it is not a prerequisite for persistence. Boot injection is silent, including startup, reload, and boot repair. Only an actual context reset produces `pi-context: memory cleared · <windowId>` through `ctx.ui.notify`, after the verified checkpoint, marker, boot, and continuation are present in the session. Committed resets are checked at the next turn start or final settlement. Uncommitted reset drafts never announce success. Boot and continuation messages retain `display: false`.
|
|
43
43
|
|
|
44
44
|
History reads reconstruct the selected session branch on demand without a cache, so branch navigation cannot expose history from a sibling.
|
|
45
45
|
|
|
46
|
-
The boot notes index in `notes/
|
|
46
|
+
The boot notes index in `pi/notes/snapshot.ts` is a closed snapshot: the current session, project, human, agent, and model homes are each loaded at most once while constructing a boot. `context/prompts.ts` then renders that explicit snapshot without reading the filesystem or consulting the clock. MAP bodies and pocket metadata are derived from the same snapshot, so a boot cannot mix two filesystem reads. Note storage and home traversal use `node:fs/promises`; same-file operations queue by absolute physical filename across store instances in this process, without promising symlink/case-alias or cross-process locking. Known persisted metadata is camelCase, and a known legacy snake_case key refuses use until the root-coordinated manual migration; startup does not migrate notes. A missing home (`ENOENT`) is normal. A real read failure omits only that home's index, preserves healthy homes, adds a model-facing `notes_list` recovery notice, and notifies the human once for that window. Boot/reset construction captures session, agent, and model identity before awaiting the snapshot, then checks lifecycle generation, active window, enabled state, and abort status before sending or returning artifacts; stale completions cannot commit into a switched or shut-down session. Note reads never mutate files or create fallback state.
|
|
47
47
|
|
|
48
48
|
`notes/session-replay.ts` accepts only supported operations, safe virtual paths, representable timestamps and results within the UTF-8 size limit. Invalid operations are ignored; they cannot replace a valid note. Notes remain in their filesystem-backed homes, unchanged by session branch navigation.
|
|
49
49
|
|
|
50
|
-
The `/wipe-memory` command
|
|
50
|
+
The `/wipe-memory` command captures the current window before waiting for idle, refuses a duplicate if the wait crossed into a new window, then records a hidden warning and starts a model close-out turn. While enabled, `/compact` is cancelled with an actionable `/wipe-memory` notice. Disabling pi-context stops new automatic resets, but an existing native checkpoint/marker still excludes earlier history and native compaction is still cancelled on that marked branch; a fresh root may use native Pi semantics. Threshold and warning accounting use active-window provider usage rather than pre-reset global usage. Budget policy and staged prompts are instance-owned, so concurrent sessions cannot share reserve/enablement or uncommitted drafts; default file-backed policy is cached per instance, while injected policy is resolved live on each decision and model/session transitions reset the diagnostic lifecycle.
|
|
51
51
|
|
|
52
52
|
## Evidence and limits
|
|
53
53
|
|
|
54
54
|
The retained integration tests use real SessionManager and SettingsManager instances. They check reset-window history retention, bounded history/notes reads with resumable cursors, notes-home isolation, and settings precedence. The suite is a representative regression set; the standalone coherence, pagination property, history, and threshold suites were removed during test reduction.
|
|
55
55
|
|
|
56
|
-
Scripted SDK tests execute the real Pi agent loop with no model
|
|
56
|
+
Scripted SDK tests execute the real Pi agent loop with no model/network request. They inspect provider contexts and durable entries for manual/budget close-out, multi-turn notes, explicit and normal-stop commits, abort/error cleanup, same-window command deduplication, queued steering/follow-up delivery, retain-none checkpoints across two resets and disk reopen, bounded overflow recovery, and settings authority. Smaller dream tests retain lock exclusion/ownership, jailed writes, CLI audit/report failure propagation, and read-only doctor behavior. Removing duplicate scenarios and edge-case matrices reduces coverage; the remaining tests do not establish every malformed-input case, branch interleaving, external-provider behavior, or filesystem failure mode.
|
|
57
57
|
|
|
58
|
-
Mixed tool batches finish before
|
|
58
|
+
Mixed tool batches finish before a direct tool reset boundary. Manual/budget close-outs let queued steering/follow-up work run once in the old window before fallback; direct tool resets let Pi schedule queued work across the new boundary. Scripted loop tests check that these messages are neither dropped nor replayed. Runtime overflow recovery is bounded to one reset/retry per failure chain, while ordinary retryable provider errors remain Pi-owned. When either the source or destination branch has a reset marker, `/tree` navigation still succeeds but its generated summary is replaced by an empty summary plus a notice; raw history and branch selection remain available. If neither branch has a marker, Pi's native tree summary is retained.
|