@astrosheep/pi-context 0.25.2 → 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 +613 -367
- 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 +16 -12
- 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 +309 -10
- 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 +19 -11
- 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
|
@@ -6,7 +6,7 @@ import test from "node:test";
|
|
|
6
6
|
import { createAssistantMessageEventStream, getCurrentSystemMessage } from "@earendil-works/pi-ai";
|
|
7
7
|
import { createAgentSession, DefaultResourceLoader, ModelRuntime, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import piContext, { createPiContext } from "../src/index.js";
|
|
9
|
-
import { BOOT_TYPE, CONTEXT_WINDOW_OPEN_TAG, CONTINUATION, CONTINUATION_TYPE, GUIDANCE_OPEN_TAG, GUIDANCE_TYPE, RESET_MARKER_TYPE, WARNING_TYPE } from "../src/protocol.js";
|
|
9
|
+
import { BOOT_TYPE, CONTEXT_WINDOW_OPEN_TAG, CONTINUATION, CONTINUATION_TYPE, GUIDANCE_OPEN_TAG, GUIDANCE_TYPE, RESET_MARKER_TYPE, WARNING_CONTENT, WARNING_PROMPT, WARNING_TYPE } from "../src/protocol.js";
|
|
10
10
|
async function openFixture(options) {
|
|
11
11
|
const dir = options.cwd ?? mkdtempSync(join(tmpdir(), "pi-context-agent-loop-"));
|
|
12
12
|
const ownsDir = options.cwd === undefined;
|
|
@@ -76,7 +76,7 @@ async function openFixture(options) {
|
|
|
76
76
|
}],
|
|
77
77
|
});
|
|
78
78
|
await loader.reload();
|
|
79
|
-
const sessionManager = SessionManager.inMemory(dir);
|
|
79
|
+
const sessionManager = options.sessionManager ?? (options.sessionDir ? SessionManager.create(dir, options.sessionDir) : SessionManager.inMemory(dir));
|
|
80
80
|
options.seed?.(sessionManager);
|
|
81
81
|
const created = await createAgentSession({
|
|
82
82
|
cwd: dir,
|
|
@@ -90,7 +90,7 @@ async function openFixture(options) {
|
|
|
90
90
|
});
|
|
91
91
|
session = created.session;
|
|
92
92
|
session.subscribe((event) => events.push(event));
|
|
93
|
-
session.agent.streamFunction = (_model, context, streamOptions) => {
|
|
93
|
+
session.agent.streamFunction = async (_model, context, streamOptions) => {
|
|
94
94
|
const aborted = streamOptions?.signal?.aborted === true;
|
|
95
95
|
streamContexts.push(context);
|
|
96
96
|
streamSignals.push(aborted);
|
|
@@ -113,7 +113,7 @@ async function openFixture(options) {
|
|
|
113
113
|
}
|
|
114
114
|
requests.push(context);
|
|
115
115
|
const request = requests.length;
|
|
116
|
-
const message = options.script(request, context);
|
|
116
|
+
const message = await options.script(request, context);
|
|
117
117
|
const stream = createAssistantMessageEventStream();
|
|
118
118
|
stream.push({ type: "done", reason: (message.stopReason === "error" || message.stopReason === "aborted" ? "stop" : message.stopReason), message });
|
|
119
119
|
stream.end();
|
|
@@ -268,13 +268,181 @@ test("real AgentSession: explicit tiny-session wipe ignores keepRecentTokens and
|
|
|
268
268
|
const boot = fixture.sessionManager.getBranch().find((entry) => entry.type === "custom_message" && entry.customType === BOOT_TYPE && entry.details && typeof entry.details === "object" && entry.details.windowId === windowId);
|
|
269
269
|
assert.ok(boot && boot.type === "custom_message", "the reset boot is persisted after the marker");
|
|
270
270
|
assert.equal(boot.details.windowId, windowId);
|
|
271
|
-
|
|
271
|
+
const checkpoint = fixture.sessionManager.getBranch().find((entry) => entry.type === "compaction");
|
|
272
|
+
assert.ok(checkpoint && checkpoint.type === "compaction", "explicit wipe commits a native checkpoint without a summary model call");
|
|
273
|
+
assert.equal(checkpoint.summary, "");
|
|
274
|
+
assert.equal(checkpoint.firstKeptEntryId, checkpoint.id, "the checkpoint retains none of the preceding context");
|
|
272
275
|
assert.ok(JSON.stringify(fixture.sessionManager.getBranch()).includes("OLD_CONTEXT_SENTINEL"), "raw history remains readable");
|
|
273
276
|
}
|
|
274
277
|
finally {
|
|
275
278
|
fixture.close();
|
|
276
279
|
}
|
|
277
280
|
});
|
|
281
|
+
test("real AgentSession: manual close-out spans note turns and commits its checkpoint after notes", { timeout: 20000 }, async () => {
|
|
282
|
+
let fixture;
|
|
283
|
+
fixture = await openFixture({
|
|
284
|
+
compactionEnabled: false,
|
|
285
|
+
script: (request, context) => {
|
|
286
|
+
if (request === 1)
|
|
287
|
+
return assistant(fixture, [{ type: "text", text: "before manual close-out" }]);
|
|
288
|
+
if (request === 2) {
|
|
289
|
+
assert.ok(text(context).includes(WARNING_PROMPT), "manual /wipe-memory sends the shared close-out warning");
|
|
290
|
+
return assistant(fixture, [{ type: "toolCall", id: "manual-note-a", name: "notes_write", arguments: { address: "manual-a.md", content: "MANUAL_NOTE_ALPHA" } }], "toolUse");
|
|
291
|
+
}
|
|
292
|
+
if (request === 3)
|
|
293
|
+
return assistant(fixture, [{ type: "toolCall", id: "manual-note-b", name: "notes_write", arguments: { address: "manual-b.md", content: "MANUAL_NOTE_BETA" } }], "toolUse");
|
|
294
|
+
if (request === 4)
|
|
295
|
+
return assistant(fixture, [{ type: "toolCall", id: "manual-wipe", name: "wipe_memory", arguments: {} }], "toolUse");
|
|
296
|
+
assertFreshRequest(fixture, request - 1, "MANUAL_CLOSEOUT_OLD_SENTINEL");
|
|
297
|
+
return assistant(fixture, [{ type: "text", text: "new window resumed" }]);
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
try {
|
|
301
|
+
await fixture.session.prompt("MANUAL_CLOSEOUT_OLD_SENTINEL");
|
|
302
|
+
await fixture.session.waitForIdle();
|
|
303
|
+
await fixture.session.prompt("/wipe-memory");
|
|
304
|
+
await fixture.session.waitForIdle();
|
|
305
|
+
assert.equal(fixture.requests.length, 5, "two note turns, explicit wipe, and one fresh continuation run");
|
|
306
|
+
assert.equal(resetMarkers(fixture).length, 1);
|
|
307
|
+
const branch = fixture.sessionManager.getBranch();
|
|
308
|
+
const warningIndex = branch.findIndex((entry) => entry.type === "custom_message" && entry.customType === WARNING_TYPE);
|
|
309
|
+
const checkpointIndex = branch.findIndex((entry) => entry.type === "compaction");
|
|
310
|
+
const markerIndex = branch.findIndex((entry) => entry.type === "custom" && entry.customType === RESET_MARKER_TYPE);
|
|
311
|
+
assert.ok(warningIndex >= 0 && warningIndex < checkpointIndex, "the warning is durable before the checkpoint");
|
|
312
|
+
const warning = branch[warningIndex];
|
|
313
|
+
assert.ok(warning?.type === "custom_message");
|
|
314
|
+
assert.equal(warning.content, WARNING_CONTENT, "manual and budget close-out persist the same hidden warning");
|
|
315
|
+
assert.equal(branch[markerIndex - 1]?.id, branch[checkpointIndex]?.id, "native retain-none checkpoint immediately precedes the marker");
|
|
316
|
+
assert.ok(branch.slice(0, checkpointIndex).filter((entry) => entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolName === "notes_write").length >= 2, "both note writes finish before the checkpoint");
|
|
317
|
+
const marker = resetMarkers(fixture)[0];
|
|
318
|
+
assert.equal(marker.type, "custom");
|
|
319
|
+
const windowId = marker.data.windowId;
|
|
320
|
+
const boot = branch.find((entry) => entry.type === "custom_message" && entry.customType === BOOT_TYPE && entry.details?.windowId === windowId);
|
|
321
|
+
assert.ok(boot && boot.type === "custom_message");
|
|
322
|
+
assert.ok(typeof boot.content === "string" && boot.content.includes("manual-a.md") && boot.content.includes("manual-b.md"), "the fresh boot indexes both completed notes");
|
|
323
|
+
assert.equal(boot.display, false);
|
|
324
|
+
const canonical = fixture.sessionManager.buildSessionProjection().messages;
|
|
325
|
+
assert.equal(JSON.stringify(canonical).includes("MANUAL_CLOSEOUT_OLD_SENTINEL"), false, "canonical context excludes the prior window");
|
|
326
|
+
assert.ok(canonical.some((message) => message.role === "compactionSummary"), "the empty native summary wrapper remains canonical");
|
|
327
|
+
assert.ok(JSON.stringify(branch).includes("MANUAL_CLOSEOUT_OLD_SENTINEL"), "raw history remains intact");
|
|
328
|
+
}
|
|
329
|
+
finally {
|
|
330
|
+
fixture.close();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
test("real AgentSession: repeated manual commands share one pending window and normal-stop fallback", { timeout: 20000 }, async () => {
|
|
334
|
+
let fixture;
|
|
335
|
+
let announceStarted;
|
|
336
|
+
let releaseResponse;
|
|
337
|
+
const started = new Promise((resolve) => { announceStarted = resolve; });
|
|
338
|
+
const gate = new Promise((resolve) => { releaseResponse = resolve; });
|
|
339
|
+
fixture = await openFixture({
|
|
340
|
+
compactionEnabled: false,
|
|
341
|
+
script: async (request) => {
|
|
342
|
+
if (request === 1) {
|
|
343
|
+
announceStarted();
|
|
344
|
+
await gate;
|
|
345
|
+
}
|
|
346
|
+
return assistant(fixture, [{ type: "text", text: "close-out complete" }]);
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
try {
|
|
350
|
+
const first = fixture.session.prompt("/wipe-memory");
|
|
351
|
+
await started;
|
|
352
|
+
const second = fixture.session.prompt("/wipe-memory");
|
|
353
|
+
releaseResponse();
|
|
354
|
+
await Promise.all([first, second]);
|
|
355
|
+
await fixture.session.waitForIdle();
|
|
356
|
+
assert.equal(fixture.requests.length, 2, "duplicate command waits do not schedule a second fresh-window request");
|
|
357
|
+
assert.equal(fixture.sessionManager.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === WARNING_TYPE).length, 1, "the same pending-window warning is deduplicated");
|
|
358
|
+
assert.equal(resetMarkers(fixture).length, 1, "normal stop commits exactly one reset");
|
|
359
|
+
assert.equal(fixture.sessionManager.getBranch().filter((entry) => entry.type === "compaction").length, 1);
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
releaseResponse();
|
|
363
|
+
fixture.close();
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
test("real AgentSession: user abort, synthetic aborted response, and generic error never fall back", { timeout: 25000 }, async () => {
|
|
367
|
+
for (const mode of ["ctx-abort", "synthetic-abort", "generic-error"]) {
|
|
368
|
+
let fixture;
|
|
369
|
+
let didAbort = false;
|
|
370
|
+
fixture = await openFixture({
|
|
371
|
+
compactionEnabled: false,
|
|
372
|
+
hook: mode === "ctx-abort" ? (pi) => pi.on("context", (_event, ctx) => {
|
|
373
|
+
if (!didAbort) {
|
|
374
|
+
didAbort = true;
|
|
375
|
+
ctx.abort();
|
|
376
|
+
}
|
|
377
|
+
}) : undefined,
|
|
378
|
+
script: (request) => {
|
|
379
|
+
if (request === 1 && mode === "synthetic-abort")
|
|
380
|
+
return assistant(fixture, [{ type: "text", text: "synthetic aborted" }], "aborted");
|
|
381
|
+
if (request === 1 && mode === "generic-error")
|
|
382
|
+
return assistant(fixture, [{ type: "text", text: "request failed" }], "error", { errorMessage: "synthetic generic provider failure" });
|
|
383
|
+
return assistant(fixture, [{ type: "text", text: "later user turn" }]);
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
try {
|
|
387
|
+
await fixture.session.prompt("/wipe-memory");
|
|
388
|
+
await fixture.session.waitForIdle();
|
|
389
|
+
assert.equal(resetMarkers(fixture).length, 0, `${mode} does not force a reset`);
|
|
390
|
+
assert.equal(fixture.sessionManager.getBranch().filter((entry) => entry.type === "compaction").length, 0);
|
|
391
|
+
assert.equal(fixture.notices.some((notice) => notice.includes("memory cleared")), false);
|
|
392
|
+
if (mode === "ctx-abort")
|
|
393
|
+
assert.equal(fixture.streamSignals.at(-1), true, "this case aborts the real Pi operation signal");
|
|
394
|
+
else
|
|
395
|
+
assert.equal(fixture.streamSignals.at(-1), false, "synthetic abort/error is not a user abort signal");
|
|
396
|
+
await fixture.session.prompt("AFTER_UNSUCCESSFUL_CLOSEOUT");
|
|
397
|
+
await fixture.session.waitForIdle();
|
|
398
|
+
assert.equal(resetMarkers(fixture).length, 0, "no pending close-out leaks into a later prompt");
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
fixture.close();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
test("real AgentSession: steering and follow-up queued during fallback reach the old window once", { timeout: 25000 }, async () => {
|
|
406
|
+
for (const delivery of ["steer", "followUp"]) {
|
|
407
|
+
let fixture;
|
|
408
|
+
let announceStarted;
|
|
409
|
+
let releaseResponse;
|
|
410
|
+
const started = new Promise((resolve) => { announceStarted = resolve; });
|
|
411
|
+
const gate = new Promise((resolve) => { releaseResponse = resolve; });
|
|
412
|
+
fixture = await openFixture({
|
|
413
|
+
compactionEnabled: false,
|
|
414
|
+
script: async (request, context) => {
|
|
415
|
+
if (request === 1) {
|
|
416
|
+
assert.ok(text(context).includes(WARNING_PROMPT));
|
|
417
|
+
announceStarted();
|
|
418
|
+
await gate;
|
|
419
|
+
}
|
|
420
|
+
if (request === 2)
|
|
421
|
+
assert.equal(text(context).split("QUEUED_DURING_FALLBACK").length - 1, 1, `${delivery} is delivered once before fallback`);
|
|
422
|
+
return assistant(fixture, [{ type: "text", text: `response ${request}` }]);
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
try {
|
|
426
|
+
const command = fixture.session.prompt("/wipe-memory");
|
|
427
|
+
await started;
|
|
428
|
+
if (delivery === "steer")
|
|
429
|
+
fixture.session.steer("QUEUED_DURING_FALLBACK");
|
|
430
|
+
else
|
|
431
|
+
fixture.session.followUp("QUEUED_DURING_FALLBACK");
|
|
432
|
+
releaseResponse();
|
|
433
|
+
await command;
|
|
434
|
+
await fixture.session.waitForIdle();
|
|
435
|
+
assert.equal(fixture.requests.length, 3, `${delivery} runs once before one fresh continuation`);
|
|
436
|
+
assert.equal(resetMarkers(fixture).length, 1, `${delivery} does not duplicate the fallback reset`);
|
|
437
|
+
assert.equal(fixture.sessionManager.getBranch().filter((entry) => entry.type === "message" && JSON.stringify(entry.message).includes("QUEUED_DURING_FALLBACK")).length, 1);
|
|
438
|
+
assertFreshRequest(fixture, 2, "QUEUED_DURING_FALLBACK");
|
|
439
|
+
}
|
|
440
|
+
finally {
|
|
441
|
+
releaseResponse();
|
|
442
|
+
fixture.close();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
});
|
|
278
446
|
test("real AgentSession: a reset survives all notes-home read failures with an incomplete boot", { timeout: 15000 }, async () => {
|
|
279
447
|
let fixture;
|
|
280
448
|
fixture = await openFixture({
|
|
@@ -390,6 +558,66 @@ test("real AgentSession: successive resets and a mixed tool batch cut only after
|
|
|
390
558
|
}
|
|
391
559
|
}
|
|
392
560
|
});
|
|
561
|
+
test("real AgentSession: two retain-none checkpoints survive session-file reopen", { timeout: 20000 }, async () => {
|
|
562
|
+
const sessionDir = mkdtempSync(join(tmpdir(), "pi-context-persisted-session-"));
|
|
563
|
+
let fixture;
|
|
564
|
+
let sessionFile = "";
|
|
565
|
+
let sessionId = "";
|
|
566
|
+
fixture = await openFixture({
|
|
567
|
+
compactionEnabled: false,
|
|
568
|
+
sessionDir,
|
|
569
|
+
script: (request) => request < 3
|
|
570
|
+
? assistant(fixture, [{ type: "toolCall", id: `persisted-wipe-${request}`, name: "wipe_memory", arguments: {} }], "toolUse")
|
|
571
|
+
: assistant(fixture, [{ type: "text", text: "second reset window complete" }]),
|
|
572
|
+
});
|
|
573
|
+
try {
|
|
574
|
+
await fixture.session.prompt("PERSISTED_OLD_WINDOW_SENTINEL");
|
|
575
|
+
await fixture.session.waitForIdle();
|
|
576
|
+
assert.equal(fixture.requests.length, 3, "two explicit requests and one final continuation, with no summary-model request");
|
|
577
|
+
assert.equal(resetMarkers(fixture).length, 2);
|
|
578
|
+
assertFreshRequest(fixture, 2, "PERSISTED_OLD_WINDOW_SENTINEL");
|
|
579
|
+
const branch = fixture.sessionManager.getBranch();
|
|
580
|
+
const checkpoints = branch.filter((entry) => entry.type === "compaction");
|
|
581
|
+
assert.equal(checkpoints.length, 2);
|
|
582
|
+
const markers = resetMarkers(fixture);
|
|
583
|
+
assert.equal(markers[0]?.parentId, checkpoints[0]?.id);
|
|
584
|
+
assert.equal(markers[1]?.parentId, checkpoints[1]?.id);
|
|
585
|
+
assert.equal(checkpoints[0]?.type === "compaction" ? checkpoints[0].firstKeptEntryId : undefined, checkpoints[0]?.id);
|
|
586
|
+
assert.equal(checkpoints[1]?.type === "compaction" ? checkpoints[1].firstKeptEntryId : undefined, checkpoints[1]?.id);
|
|
587
|
+
sessionFile = fixture.sessionManager.getSessionFile() ?? "";
|
|
588
|
+
sessionId = fixture.sessionManager.getSessionId();
|
|
589
|
+
assert.ok(sessionFile && existsSync(sessionFile), "the SDK persisted the real session file");
|
|
590
|
+
}
|
|
591
|
+
finally {
|
|
592
|
+
fixture.close();
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
const reopened = SessionManager.open(sessionFile, sessionDir);
|
|
596
|
+
assert.equal(reopened.getSessionId(), sessionId);
|
|
597
|
+
const branch = reopened.getBranch();
|
|
598
|
+
assert.equal(branch.filter((entry) => entry.type === "compaction").length, 2);
|
|
599
|
+
assert.equal(branch.filter((entry) => entry.type === "custom" && entry.customType === RESET_MARKER_TYPE).length, 2);
|
|
600
|
+
assert.ok(JSON.stringify(branch).includes("PERSISTED_OLD_WINDOW_SENTINEL"), "raw history survives disk reopen");
|
|
601
|
+
const canonical = reopened.buildSessionProjection().messages;
|
|
602
|
+
assert.equal(JSON.stringify(canonical).includes("PERSISTED_OLD_WINDOW_SENTINEL"), false, "disk-loaded canonical context agrees with the fresh request");
|
|
603
|
+
assert.ok(canonical.some((message) => message.role === "compactionSummary"), "the accepted empty-summary wrapper survives reload");
|
|
604
|
+
const system = getCurrentSystemMessage(canonical);
|
|
605
|
+
assert.ok(JSON.stringify(system).includes("Use the tools as requested."), "the complete system prompt survives the checkpoint");
|
|
606
|
+
assert.ok(system?.toolsAdded?.some((tool) => tool.name === "wipe_memory"), "the active tool loadout survives reopen");
|
|
607
|
+
const reopenedMarkers = branch.filter((entry) => entry.type === "custom" && entry.customType === RESET_MARKER_TYPE);
|
|
608
|
+
const latestMarker = reopenedMarkers.at(-1);
|
|
609
|
+
const previousMarker = reopenedMarkers[0];
|
|
610
|
+
assert.ok(latestMarker?.type === "custom" && previousMarker?.type === "custom");
|
|
611
|
+
const latestWindowId = latestMarker.data.windowId;
|
|
612
|
+
const previousWindowId = previousMarker.data.windowId;
|
|
613
|
+
const latestBoot = branch.find((entry) => entry.type === "custom_message" && entry.customType === BOOT_TYPE && entry.details?.windowId === latestWindowId);
|
|
614
|
+
assert.ok(latestBoot && latestBoot.type === "custom_message" && typeof latestBoot.content === "string");
|
|
615
|
+
assert.ok(latestBoot.content.includes(`Previous context window id: ${previousWindowId}`));
|
|
616
|
+
}
|
|
617
|
+
finally {
|
|
618
|
+
rmSync(sessionDir, { recursive: true, force: true });
|
|
619
|
+
}
|
|
620
|
+
});
|
|
393
621
|
test("real AgentSession: steering and follow-up are delivered exactly once in the fresh window", async () => {
|
|
394
622
|
for (const deliverAs of ["steer", "followUp"]) {
|
|
395
623
|
let fixture;
|
|
@@ -531,6 +759,7 @@ test("real AgentSession: overflow and recoverable length reset and retry once; r
|
|
|
531
759
|
await fixture.session.prompt(`RECOVER_${failure}_SENTINEL`);
|
|
532
760
|
await fixture.session.waitForIdle();
|
|
533
761
|
assert.equal(resetMarkers(fixture).length, 1, `${failure} performs one reset`);
|
|
762
|
+
assert.equal(fixture.sessionManager.getBranch().filter((entry) => entry.type === "compaction").length, 1, `${failure} commits one native reset checkpoint`);
|
|
534
763
|
assert.equal(fixture.requests.length, 2, `${failure} resumes once`);
|
|
535
764
|
assertFreshRequest(fixture, 1, `RECOVER_${failure}_SENTINEL`);
|
|
536
765
|
}
|
|
@@ -546,8 +775,8 @@ test("real AgentSession: overflow and recoverable length reset and retry once; r
|
|
|
546
775
|
await repeated.session.prompt("REPEATED_OVERFLOW_SENTINEL");
|
|
547
776
|
await repeated.session.waitForIdle();
|
|
548
777
|
assert.equal(resetMarkers(repeated).length, 1, "repeated overflow does not create repeated windows");
|
|
778
|
+
assert.equal(repeated.sessionManager.getBranch().filter((entry) => entry.type === "compaction").length, 1, "bounded overflow recovery commits one retain-none checkpoint");
|
|
549
779
|
assert.equal(repeated.requests.length, 2, "repeated overflow stops after one recovery request");
|
|
550
|
-
assert.ok(repeated.events.some((event) => event.type === "compaction_end" && typeof event.errorMessage === "string"));
|
|
551
780
|
}
|
|
552
781
|
finally {
|
|
553
782
|
repeated.close();
|
|
@@ -709,7 +938,7 @@ test("real AgentSession: injected settings managers own live policy and ignore c
|
|
|
709
938
|
await first.session.waitForIdle();
|
|
710
939
|
assert.equal(resetMarkers(first).length, 1, "live enablement and a lower reserve apply on the next turn");
|
|
711
940
|
firstHighUsage.next = true;
|
|
712
|
-
firstManager.applyOverrides({ compaction: { reserveTokens:
|
|
941
|
+
firstManager.applyOverrides({ compaction: { reserveTokens: 80_000 } });
|
|
713
942
|
await first.session.prompt("INJECTED_LIVE_HIGH_RESERVE");
|
|
714
943
|
await first.session.waitForIdle();
|
|
715
944
|
assert.equal(resetMarkers(first).length, 2, "a live reserve increase drives the next reset decision");
|
|
@@ -781,14 +1010,83 @@ test("real AgentSession: injected model overrides select the active model's rese
|
|
|
781
1010
|
rmSync(notesRoot, { recursive: true, force: true });
|
|
782
1011
|
}
|
|
783
1012
|
});
|
|
1013
|
+
test("real AgentSession: persisted budget warning re-arms after a later abort/error", { timeout: 30000 }, async () => {
|
|
1014
|
+
for (const mode of ["ctx-abort", "generic-error"]) {
|
|
1015
|
+
let fixture;
|
|
1016
|
+
let warningTurnStarted;
|
|
1017
|
+
let releaseWarningTurn;
|
|
1018
|
+
let didAbort = false;
|
|
1019
|
+
const warningStarted = new Promise((resolve) => { warningTurnStarted = resolve; });
|
|
1020
|
+
const warningGate = new Promise((resolve) => { releaseWarningTurn = resolve; });
|
|
1021
|
+
fixture = await openFixture({
|
|
1022
|
+
compactionEnabled: true,
|
|
1023
|
+
hook: mode === "ctx-abort" ? (pi, _getSession, requests) => pi.on("context", (_event, ctx) => {
|
|
1024
|
+
if (!didAbort && requests.length === 3) {
|
|
1025
|
+
didAbort = true;
|
|
1026
|
+
ctx.abort();
|
|
1027
|
+
}
|
|
1028
|
+
}) : undefined,
|
|
1029
|
+
script: async (request, context) => {
|
|
1030
|
+
if (request === 1)
|
|
1031
|
+
return assistant(fixture, [{ type: "toolCall", id: "rearm-probe-1", name: "get_context_remaining", arguments: {} }], "toolUse", { usage: usage(50_000) });
|
|
1032
|
+
if (request === 2)
|
|
1033
|
+
return assistant(fixture, [{ type: "toolCall", id: "rearm-probe-2", name: "get_context_remaining", arguments: {} }], "toolUse", { usage: usage(60_000) });
|
|
1034
|
+
if (request === 3) {
|
|
1035
|
+
assert.ok(text(context).includes(WARNING_PROMPT), "the low-budget request carries the close-out warning");
|
|
1036
|
+
warningTurnStarted();
|
|
1037
|
+
await warningGate;
|
|
1038
|
+
return assistant(fixture, [{ type: "toolCall", id: "rearm-note", name: "notes_write", arguments: { address: "warning-rearm.md", content: "DURABLE_WARNING_CHECKPOINT" } }], "toolUse", { usage: usage(60_000) });
|
|
1039
|
+
}
|
|
1040
|
+
if (request === 4 && mode === "generic-error") {
|
|
1041
|
+
assert.equal(text(context).split(WARNING_PROMPT).length - 1, 1, "the durable warning is not injected a second time");
|
|
1042
|
+
assert.ok(text(context).includes("REARM_QUEUED_FOLLOWUP"));
|
|
1043
|
+
return assistant(fixture, [{ type: "text", text: "ordinary provider failure" }], "error", { errorMessage: "synthetic non-overflow failure", usage: usage(60_000) });
|
|
1044
|
+
}
|
|
1045
|
+
return assistant(fixture, [{ type: "text", text: `normal response ${request}` }], "stop", { usage: usage(60_000) });
|
|
1046
|
+
},
|
|
1047
|
+
});
|
|
1048
|
+
try {
|
|
1049
|
+
const initialTurn = fixture.session.prompt(`REARM_${mode}_INITIAL`);
|
|
1050
|
+
await warningStarted;
|
|
1051
|
+
fixture.session.steer("REARM_QUEUED_FOLLOWUP");
|
|
1052
|
+
releaseWarningTurn();
|
|
1053
|
+
await initialTurn;
|
|
1054
|
+
await fixture.session.waitForIdle();
|
|
1055
|
+
const afterFailure = fixture.sessionManager.getBranch();
|
|
1056
|
+
assert.equal(afterFailure.filter((entry) => entry.type === "custom_message" && entry.customType === WARNING_TYPE).length, 1, "the warning was persisted exactly once before the failed follow-up");
|
|
1057
|
+
assert.ok(existsSync(join(fixture.notesRoot, "pi", "session", fixture.sessionManager.getSessionId(), "warning-rearm.md")), "the preceding note/tool turn completed");
|
|
1058
|
+
assert.equal(resetMarkers(fixture).length, 0, `${mode} itself must not reset`);
|
|
1059
|
+
assert.equal(afterFailure.filter((entry) => entry.type === "compaction").length, 0);
|
|
1060
|
+
const failedContext = fixture.streamContexts[3];
|
|
1061
|
+
assert.ok(failedContext);
|
|
1062
|
+
assert.equal(text(failedContext).split(WARNING_PROMPT).length - 1, 1, "the persisted warning appears once, without duplicate injection");
|
|
1063
|
+
assert.equal(text(failedContext).split("REARM_QUEUED_FOLLOWUP").length - 1, 1, "queued follow-up reaches the failed request once");
|
|
1064
|
+
assert.equal(fixture.streamSignals.at(-1), mode === "ctx-abort", "the abort case cancels Pi's real signal; the error case does not");
|
|
1065
|
+
await fixture.session.prompt("REARM_AFTER_FAILED_TURN");
|
|
1066
|
+
await fixture.session.waitForIdle();
|
|
1067
|
+
assert.equal(resetMarkers(fixture).length, 1, "the next low-budget normal stop re-arms and commits one reset");
|
|
1068
|
+
assert.equal(fixture.sessionManager.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === WARNING_TYPE).length, 1, "re-arming does not duplicate the durable warning");
|
|
1069
|
+
const checkpoint = fixture.sessionManager.getBranch().find((entry) => entry.type === "compaction");
|
|
1070
|
+
assert.ok(checkpoint?.type === "compaction");
|
|
1071
|
+
assert.equal(checkpoint.firstKeptEntryId, checkpoint.id, "the retry closes with one retain-none checkpoint");
|
|
1072
|
+
assertFreshRequest(fixture, fixture.requests.length - 1, "REARM_AFTER_FAILED_TURN");
|
|
1073
|
+
}
|
|
1074
|
+
finally {
|
|
1075
|
+
releaseWarningTurn();
|
|
1076
|
+
fixture.close();
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
784
1080
|
test("real AgentSession: warning precedes a durable checkpoint, including failed writes and an ignored warning", { timeout: 20000 }, async () => {
|
|
785
1081
|
for (const scenario of ["checkpoint", "write-error", "ignored-warning"]) {
|
|
786
1082
|
let fixture;
|
|
787
1083
|
fixture = await openFixture({
|
|
788
1084
|
compactionEnabled: true,
|
|
789
1085
|
script: (request, context) => {
|
|
790
|
-
if (request === 3)
|
|
791
|
-
assert.ok(text(context).includes(GUIDANCE_OPEN_TAG), "the
|
|
1086
|
+
if (request === 3) {
|
|
1087
|
+
assert.ok(text(context).includes(GUIDANCE_OPEN_TAG), "the warning remains in its hidden guidance wrapper");
|
|
1088
|
+
assert.ok(text(context).includes(WARNING_PROMPT), "the shared close-out warning reaches the provider before checkpoint choice");
|
|
1089
|
+
}
|
|
792
1090
|
if (request === 1)
|
|
793
1091
|
return assistant(fixture, [{ type: "toolCall", id: "budget-probe-1", name: "get_context_remaining", arguments: {} }], "toolUse", { usage: usage(50_000) });
|
|
794
1092
|
if (request === 2)
|
|
@@ -821,8 +1119,9 @@ test("real AgentSession: warning precedes a durable checkpoint, including failed
|
|
|
821
1119
|
assert.ok(branch.some((entry) => entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolName === "notes_write" && entry.message.isError), "the failed write is a durable tool error");
|
|
822
1120
|
}
|
|
823
1121
|
else {
|
|
824
|
-
assert.equal(resetMarkers(fixture).length,
|
|
1122
|
+
assert.equal(resetMarkers(fixture).length, 1, "normal stop falls back to the same reset even when the warning was ignored");
|
|
825
1123
|
assert.equal(existsSync(noteFile), false);
|
|
1124
|
+
assertFreshRequest(fixture, fixture.requests.length - 1, `WARNING_${scenario.toUpperCase()}_SENTINEL`);
|
|
826
1125
|
}
|
|
827
1126
|
}
|
|
828
1127
|
finally {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 {};
|