@astrosheep/pi-context 0.20.0 → 0.22.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 +22 -1
- package/dist/src/budget.js +10 -8
- package/dist/src/dream/cli.js +108 -24
- package/dist/src/dream/gates.js +13 -8
- package/dist/src/dream/git.js +71 -0
- package/dist/src/dream/lock.js +78 -37
- package/dist/src/dream/runner.js +90 -21
- package/dist/src/history-tools.js +5 -5
- package/dist/src/history.js +11 -6
- package/dist/src/index.js +14 -15
- package/dist/src/notes/address.js +31 -0
- package/dist/src/{memory → notes}/frontmatter.js +7 -5
- package/dist/src/{notes.js → notes/model.js} +1 -1
- package/dist/src/{memory → notes}/paths.js +7 -3
- package/dist/src/{memory → notes}/store.js +47 -74
- package/dist/src/notes/tools.js +153 -0
- package/dist/src/prompts.js +38 -29
- package/dist/src/protocol.js +9 -4
- package/dist/src/thresholds.js +33 -3
- package/dist/src/tool-output.js +4 -1
- package/dist/src/warning.js +3 -3
- package/dist/test/agent-loop.test.js +6 -4
- package/dist/test/coherence.test.js +5 -1
- package/dist/test/dream.test.js +419 -35
- package/dist/test/history.test.js +6 -1
- package/dist/test/integration.test.js +107 -47
- package/dist/test/{memory.test.js → notes.test.js} +154 -50
- package/dist/test/pagination.property.test.js +1 -1
- package/package.json +5 -5
- package/playbook.md +30 -3
- package/src/budget.ts +11 -9
- package/src/dream/cli.ts +95 -17
- package/src/dream/gates.ts +14 -7
- package/src/dream/git.ts +73 -0
- package/src/dream/lock.ts +67 -24
- package/src/dream/runner.ts +87 -20
- package/src/history-tools.ts +5 -5
- package/src/history.ts +12 -7
- package/src/index.ts +13 -14
- package/src/notes/address.ts +33 -0
- package/src/{memory → notes}/frontmatter.ts +7 -5
- package/src/{notes.ts → notes/model.ts} +2 -2
- package/src/{memory → notes}/paths.ts +8 -3
- package/src/{memory → notes}/store.ts +49 -79
- package/src/notes/tools.ts +132 -0
- package/src/prompts.ts +39 -29
- package/src/protocol.ts +9 -4
- package/src/thresholds.ts +38 -6
- package/src/tool-output.ts +4 -1
- package/src/warning.ts +3 -3
- package/dist/src/dream/apply.js +0 -87
- package/dist/src/dream/manifest.js +0 -16
- package/dist/src/memory/tools.js +0 -175
- package/src/dream/apply.ts +0 -47
- package/src/dream/manifest.ts +0 -21
- package/src/memory/tools.ts +0 -175
|
@@ -5,9 +5,10 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import { AgentSession, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import piContext, { historyFromSession, internal } from "../src/index.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { bootBlock } from "../src/prompts.js";
|
|
9
|
+
import { localIso } from "../src/notes/model.js";
|
|
10
|
+
import { physicalPath } from "../src/notes/paths.js";
|
|
11
|
+
import { listNotes } from "../src/notes/store.js";
|
|
11
12
|
import { middleTruncate, page, TOOL_OUTPUT_MAX_BYTES } from "../src/tool-output.js";
|
|
12
13
|
// Settings fixtures live in temp directories. PI_CODING_AGENT_DIR is redirected for the
|
|
13
14
|
// whole test process so the extension's SettingsManager.create(ctx.cwd, undefined, ...)
|
|
@@ -109,12 +110,41 @@ function noticesOf(ctx) {
|
|
|
109
110
|
export async function call(captured, name, params, ctx) {
|
|
110
111
|
const tool = captured.tools.get(name);
|
|
111
112
|
assert.ok(tool, `registered ${name}`);
|
|
113
|
+
// Most pre-redesign coverage names session notes by their bare address. Keep these old
|
|
114
|
+
// fixture call sites readable while routing the direct tool invocation through its new
|
|
115
|
+
// address-shaped input; contract-specific tests below pass address themselves.
|
|
116
|
+
const noteCall = name === "notes_write" || name === "notes_edit" || name === "notes_read";
|
|
117
|
+
if (noteCall && "path" in params && !("address" in params)) {
|
|
118
|
+
const { path, scope, ...rest } = params;
|
|
119
|
+
assert.equal(typeof path, "string", "legacy note fixture path is a string");
|
|
120
|
+
const address = scope === "project" ? `@project/${path}` : scope === "personal" ? `@personal/${path}` : path;
|
|
121
|
+
return tool.execute("call-1", { ...rest, address }, new AbortController().signal, () => { }, ctx);
|
|
122
|
+
}
|
|
123
|
+
if ((name === "notes_list" || name === "notes_search") && params.scope === "personal") {
|
|
124
|
+
const { scope: _scope, pattern, ...rest } = params;
|
|
125
|
+
return tool.execute("call-1", { ...rest, pattern: `@personal/${typeof pattern === "string" ? pattern : "**"}` }, new AbortController().signal, () => { }, ctx);
|
|
126
|
+
}
|
|
127
|
+
if ((name === "notes_list" || name === "notes_search") && params.scope === "session") {
|
|
128
|
+
const { scope: _scope, pattern, ...rest } = params;
|
|
129
|
+
return tool.execute("call-1", { ...rest, pattern: typeof pattern === "string" ? pattern : "*.md" }, new AbortController().signal, () => { }, ctx);
|
|
130
|
+
}
|
|
112
131
|
return tool.execute("call-1", params, new AbortController().signal, () => { }, ctx);
|
|
113
132
|
}
|
|
114
133
|
export function resultJson(result) {
|
|
115
134
|
const text = result.content[0];
|
|
116
135
|
assert.ok(text && text.type === "text", "tool result carries text");
|
|
117
|
-
|
|
136
|
+
const value = JSON.parse(text.text);
|
|
137
|
+
const suffix = (address) => address.startsWith("@project/") ? address.slice("@project/".length) : address.startsWith("@personal/") ? address.slice("@personal/".length) : address;
|
|
138
|
+
const legacyPath = (row) => {
|
|
139
|
+
if (typeof row.address === "string" && row.path === undefined)
|
|
140
|
+
Object.defineProperty(row, "path", { value: suffix(row.address), enumerable: false });
|
|
141
|
+
};
|
|
142
|
+
legacyPath(value);
|
|
143
|
+
if (Array.isArray(value.files))
|
|
144
|
+
for (const file of value.files)
|
|
145
|
+
if (file && typeof file === "object")
|
|
146
|
+
legacyPath(file);
|
|
147
|
+
return value;
|
|
118
148
|
}
|
|
119
149
|
/** Assert the delivered wire text fits the tool-output budget, header included for raw reads. */
|
|
120
150
|
export function assertWithinBudget(result, message) {
|
|
@@ -247,11 +277,13 @@ test("schemas cover the History/Notes actions plus reset controls", () => {
|
|
|
247
277
|
// The write surface requires its body; the edit surface requires its anchors.
|
|
248
278
|
const writeSchema = captured.tools.get("notes_write")?.parameters;
|
|
249
279
|
assert.ok(writeSchema?.properties?.content, "notes_write exposes content");
|
|
250
|
-
assert.ok(writeSchema?.properties?.
|
|
251
|
-
assert.
|
|
280
|
+
assert.ok(writeSchema?.properties?.address, "notes_write exposes address");
|
|
281
|
+
assert.equal(writeSchema?.properties?.scope, undefined, "notes_write has no scope parameter");
|
|
282
|
+
assert.deepEqual([...(writeSchema?.required ?? [])].sort(), ["address", "content"], "notes_write requires address and content");
|
|
252
283
|
const editSchema = captured.tools.get("notes_edit")?.parameters;
|
|
253
284
|
assert.ok(editSchema?.properties?.edits, "notes_edit exposes edits");
|
|
254
|
-
assert.
|
|
285
|
+
assert.equal(editSchema?.properties?.scope, undefined, "notes_edit has no scope parameter");
|
|
286
|
+
assert.deepEqual([...(editSchema?.required ?? [])].sort(), ["address"], "notes_edit requires only address; edits are optional for metadata-only updates");
|
|
255
287
|
// The history ordering switch is documented as newest-first by default.
|
|
256
288
|
for (const name of ["history_windows", "history_list", "history_search"]) {
|
|
257
289
|
const schema = captured.tools.get(name)?.parameters;
|
|
@@ -270,7 +302,12 @@ test("schemas cover the History/Notes actions plus reset controls", () => {
|
|
|
270
302
|
assert.equal(schema?.properties?.limit_chars?.maximum, 50000, `${name} caps limit_chars at 50000`);
|
|
271
303
|
}
|
|
272
304
|
const noteReadSchema = captured.tools.get("notes_read")?.parameters;
|
|
273
|
-
assert.deepEqual(Object.keys(noteReadSchema?.properties ?? {}).sort(), ["
|
|
305
|
+
assert.deepEqual(Object.keys(noteReadSchema?.properties ?? {}).sort(), ["address", "limit_chars", "offset_chars"], "notes_read exposes exactly address and character-window params");
|
|
306
|
+
for (const name of ["notes_write", "notes_edit", "notes_read", "notes_list", "notes_search"]) {
|
|
307
|
+
const schema = captured.tools.get(name)?.parameters;
|
|
308
|
+
assert.equal(schema?.properties?.scope, undefined, `${name} has no scope property`);
|
|
309
|
+
assert.equal(schema?.additionalProperties, false, `${name} rejects scope as an additional property`);
|
|
310
|
+
}
|
|
274
311
|
assert.equal(/start_|stop_line|total_lines/.test(captured.tools.get("notes_read")?.description ?? ""), false, "notes_read prose carries no line surface");
|
|
275
312
|
});
|
|
276
313
|
test("notes_list is most-recently-updated first across merged scopes", async () => {
|
|
@@ -286,36 +323,36 @@ test("notes_list is most-recently-updated first across merged scopes", async ()
|
|
|
286
323
|
put("session", "b.md", base + 10);
|
|
287
324
|
put("session", "a.md", base + 10);
|
|
288
325
|
put("project", "c.md", base + 5);
|
|
289
|
-
put("
|
|
326
|
+
put("personal", "e.md", base + 20);
|
|
290
327
|
const files = async (params) => resultJson(await call(captured, "notes_list", params, ctx)).files;
|
|
291
|
-
assert.deepEqual((await files({})).map((file) => file.
|
|
328
|
+
assert.deepEqual((await files({})).map((file) => file.address), ["@personal/e.md", "a.md", "b.md", "@project/c.md"], "updated_at descending with address ascending as the tiebreak");
|
|
292
329
|
// A same-path pair in two scopes keeps both rows; equal timestamps tie-break by scope name.
|
|
293
|
-
put("
|
|
294
|
-
assert.deepEqual((await files({})).filter((file) => file.
|
|
295
|
-
assert.deepEqual((await files({
|
|
330
|
+
put("personal", "a.md", base + 10);
|
|
331
|
+
assert.deepEqual((await files({})).filter((file) => file.address.endsWith("a.md")).map((file) => file.scope), ["personal", "session"], "equal timestamps tie-break by full address");
|
|
332
|
+
assert.deepEqual((await files({ pattern: "*.md" })).map((file) => file.address), ["a.md", "b.md"], "a bare pattern narrows to the session home");
|
|
296
333
|
});
|
|
297
334
|
test("notes are real files that persist across sessions and round-trip Unicode", async () => {
|
|
298
335
|
const original = manager();
|
|
299
336
|
const captured = makeExtension(original);
|
|
300
337
|
const ctx = context(original);
|
|
301
|
-
await call(captured, "notes_write", { path: "checkpoint/进度.md", content: "第一行\nneedle Café", scope: "
|
|
302
|
-
// A brand-new session over the same physical root sees the
|
|
338
|
+
await call(captured, "notes_write", { path: "checkpoint/进度.md", content: "第一行\nneedle Café", scope: "personal" }, ctx);
|
|
339
|
+
// A brand-new session over the same physical root sees the personal note: nothing is replayed
|
|
303
340
|
// from session entries, the file itself is the durable artifact.
|
|
304
341
|
const restored = manager();
|
|
305
342
|
const restoredCaptured = makeExtension(restored);
|
|
306
343
|
const restoredCtx = context(restored);
|
|
307
|
-
const rawRead = await call(restoredCaptured, "notes_read", { path: "checkpoint/进度.md", scope: "
|
|
344
|
+
const rawRead = await call(restoredCaptured, "notes_read", { path: "checkpoint/进度.md", scope: "personal", offset_chars: -4 }, restoredCtx);
|
|
308
345
|
const read = resultRead(rawRead);
|
|
309
|
-
assert.equal(read.details.
|
|
346
|
+
assert.equal(read.details.address, "@personal/checkpoint/进度.md");
|
|
310
347
|
assert.equal(read.content, "Café", "a negative offset reads the body tail in one call");
|
|
311
|
-
assert.equal(read.details.scope, "
|
|
312
|
-
const searched = resultJson(await call(restoredCaptured, "notes_search", { query: "Café", scope: "
|
|
348
|
+
assert.equal(read.details.scope, "personal");
|
|
349
|
+
const searched = resultJson(await call(restoredCaptured, "notes_search", { query: "Café", scope: "personal" }, restoredCtx));
|
|
313
350
|
assert.equal(searched.files[0]?.matches[0]?.line, 2);
|
|
314
|
-
const listedFiles = resultJson(await call(restoredCaptured, "notes_list", { pattern: "checkpoint/**", scope: "
|
|
351
|
+
const listedFiles = resultJson(await call(restoredCaptured, "notes_list", { pattern: "checkpoint/**", scope: "personal" }, restoredCtx));
|
|
315
352
|
assert.equal(listedFiles.files.length, 1, "glob ** crosses into the checkpoint directory");
|
|
316
353
|
assert.equal(listedFiles.files[0]?.path, "checkpoint/进度.md");
|
|
317
354
|
// A single-segment * never crosses `/`, so a nested-only store matches nothing at the root.
|
|
318
|
-
const rootOnly = resultJson(await call(restoredCaptured, "notes_list", { pattern: "*", scope: "
|
|
355
|
+
const rootOnly = resultJson(await call(restoredCaptured, "notes_list", { pattern: "*", scope: "personal" }, restoredCtx));
|
|
319
356
|
assert.equal(rootOnly.files.length, 0, "glob * stays within one segment");
|
|
320
357
|
assert.equal(searched.files[0]?.created_at, listedFiles.files[0]?.created_at, "note tools agree on the timestamp format");
|
|
321
358
|
assert.equal(searched.files[0]?.updated_at, listedFiles.files[0]?.updated_at);
|
|
@@ -343,6 +380,17 @@ test("stale lifecycle: writes and metadata-only edits close and revive a note",
|
|
|
343
380
|
const missing = resultJson(await call(captured, "notes_edit", { path: "missing.md", stale: true }, ctx));
|
|
344
381
|
assert.equal(missing.error, "note not found");
|
|
345
382
|
});
|
|
383
|
+
test("notes tools stay usable while a dream holds the lock", async () => {
|
|
384
|
+
// A live dream lock is not a general lock: the awake notes tools never consult it.
|
|
385
|
+
writeFileSync(join(process.env.PI_NOTES_HOME, ".dream.lock"), String(process.pid));
|
|
386
|
+
const sm = manager();
|
|
387
|
+
const captured = makeExtension(sm);
|
|
388
|
+
const ctx = context(sm);
|
|
389
|
+
const written = resultJson(await call(captured, "notes_write", { path: "during-dream.md", content: "awake" }, ctx));
|
|
390
|
+
assert.equal(written.address, "during-dream.md");
|
|
391
|
+
const edited = resultJson(await call(captured, "notes_edit", { path: "during-dream.md", edits: [{ oldText: "awake", newText: "still awake" }] }, ctx));
|
|
392
|
+
assert.equal(edited.applied, 1, "notes_edit still applies while a dream lock is held");
|
|
393
|
+
});
|
|
346
394
|
test("the boot notes index excludes stale notes while list, read, and search still see them", async () => {
|
|
347
395
|
const sm = manager();
|
|
348
396
|
const captured = makeExtension(sm);
|
|
@@ -354,7 +402,7 @@ test("the boot notes index excludes stale notes while list, read, and search sti
|
|
|
354
402
|
const text = typeof boot?.message.content === "string" ? boot.message.content : "";
|
|
355
403
|
assert.ok(text.includes("fresh.md"), "the fresh note is indexed");
|
|
356
404
|
assert.equal(text.includes("old.md"), false, "the stale note leaves the boot index");
|
|
357
|
-
assert.equal(text.includes("stale content"), false, "the stale
|
|
405
|
+
assert.equal(text.includes("stale content"), false, "the stale note's body is absent from boot");
|
|
358
406
|
const listed = resultJson(await call(captured, "notes_list", {}, ctx));
|
|
359
407
|
assert.equal(listed.files.find((file) => file.path === "old.md")?.stale, true, "list carries the stale flag");
|
|
360
408
|
assert.equal(listed.files.find((file) => file.path === "fresh.md")?.stale, false);
|
|
@@ -372,9 +420,30 @@ test("the boot notes index omits itself when every note is stale", async () => {
|
|
|
372
420
|
runHandlers(captured, "session_start", {}, ctx);
|
|
373
421
|
const text = typeof captured.sent[0]?.message.content === "string" ? captured.sent[0].message.content : "";
|
|
374
422
|
assert.equal(text.includes("done.md"), false, "no stale note is indexed");
|
|
375
|
-
assert.equal(text.includes("finished"), false, "
|
|
423
|
+
assert.equal(text.includes("finished"), false, "the stale note's body is absent from boot");
|
|
376
424
|
assert.ok(text.includes(internal.CONTEXT_WINDOW_PROTOCOL_OPEN_TAG), "the rest of the boot block still renders");
|
|
377
425
|
});
|
|
426
|
+
test("the boot block gives awake agents the notes-home file layout", () => {
|
|
427
|
+
const session = manager();
|
|
428
|
+
const rendered = bootBlock(context(session), "pcw:test:root", undefined, false);
|
|
429
|
+
assert.equal(rendered.includes(process.env.PI_NOTES_HOME ?? ""), false, "the absolute notes home is never exposed");
|
|
430
|
+
assert.match(rendered, /bare <vpath>.*@project\/<vpath>.*@personal\/<vpath>/);
|
|
431
|
+
assert.match(rendered, /there is no cross-home fallback/);
|
|
432
|
+
assert.match(rendered, /Any other note is a plain file — use the file tools/);
|
|
433
|
+
});
|
|
434
|
+
test("the boot block keeps fresh personal and project maps resident, never a session map", async () => {
|
|
435
|
+
const session = manager();
|
|
436
|
+
const captured = makeExtension(session);
|
|
437
|
+
const ctx = context(session);
|
|
438
|
+
await call(captured, "notes_write", { address: "MAP.md", content: "MAP: session" }, ctx);
|
|
439
|
+
await call(captured, "notes_write", { address: "@project/MAP.md", content: "MAP: project" }, ctx);
|
|
440
|
+
await call(captured, "notes_write", { address: "@personal/MAP.md", content: "MAP: personal" }, ctx);
|
|
441
|
+
const rendered = bootBlock(ctx, "pcw:test:root", undefined, false);
|
|
442
|
+
assert.ok(rendered.includes("MAP: personal"));
|
|
443
|
+
assert.ok(rendered.includes("MAP: project"));
|
|
444
|
+
assert.equal(rendered.includes("MAP: session"), false);
|
|
445
|
+
assert.ok(rendered.indexOf("MAP: personal") < rendered.indexOf("MAP: project"), "personal map precedes project map");
|
|
446
|
+
});
|
|
378
447
|
test("paged tool outputs stay bounded and cursors reconstruct history and notes", async () => {
|
|
379
448
|
const session = manager();
|
|
380
449
|
const captured = makeExtension(session);
|
|
@@ -636,8 +705,8 @@ test("an over-budget note is delivered as a prefix and resumed by next_offset_ch
|
|
|
636
705
|
assert.ok(first.content.length > 0, "the page is not empty");
|
|
637
706
|
assert.equal(first.content.includes("…"), false, "the payload is a plain prefix with no marker");
|
|
638
707
|
assert.ok(first.content.startsWith("---\n"), "the frontmatter is delivered first");
|
|
639
|
-
assert.equal(first.header, `[huge.md · chars 0-${first.next_offset_chars} of ${first.total_chars} · continue at offset_chars=${first.next_offset_chars} · session · created ${String(first.details.created_at)} · updated ${String(first.details.updated_at)}]`, "the raw header names the
|
|
640
|
-
assert.deepEqual(Object.keys(first.details).sort(), ["created_at", "limit_chars", "next_offset_chars", "offset_chars", "
|
|
708
|
+
assert.equal(first.header, `[huge.md · chars 0-${first.next_offset_chars} of ${first.total_chars} · continue at offset_chars=${first.next_offset_chars} · session · created ${String(first.details.created_at)} · updated ${String(first.details.updated_at)}]`, "the raw header names the address, delivered range, resume cursor, scope and timestamps");
|
|
709
|
+
assert.deepEqual(Object.keys(first.details).sort(), ["address", "created_at", "limit_chars", "next_offset_chars", "offset_chars", "scope", "total_chars", "updated_at"], "notes_read details carries exactly the slim window metadata plus scope");
|
|
641
710
|
assert.equal("content" in first.details, false, "details never duplicates the payload");
|
|
642
711
|
assert.equal(first.offset_chars, 0, "the default window starts at the resolved offset 0");
|
|
643
712
|
// Following the cursor reconstructs frontmatter + body by plain concatenation.
|
|
@@ -655,7 +724,7 @@ test("an over-budget note is delivered as a prefix and resumed by next_offset_ch
|
|
|
655
724
|
// A success carries structured details; an error stays a JSON envelope with no details.
|
|
656
725
|
const missingResult = await call(captured, "notes_read", { path: "no-such.md" }, ctx);
|
|
657
726
|
const missing = resultJson(missingResult);
|
|
658
|
-
assert.deepEqual(Object.keys(missing).sort(), ["
|
|
727
|
+
assert.deepEqual(Object.keys(missing).sort(), ["address", "error"], "the read error carries exactly error and address");
|
|
659
728
|
assert.equal(missing.error, "note not found");
|
|
660
729
|
assert.equal(missingResult.details, undefined, "a JSON error carries no details metadata");
|
|
661
730
|
});
|
|
@@ -937,11 +1006,10 @@ test("custom reset boundary removes old provider context but history remains sea
|
|
|
937
1006
|
assert.equal(found.items[0]?.item_id, oldUserId);
|
|
938
1007
|
assert.ok(sessionManager.getEntry(compactionId));
|
|
939
1008
|
});
|
|
940
|
-
test("the boot notes
|
|
1009
|
+
test("the boot notes index shows one metadata line per note and never a body", async () => {
|
|
941
1010
|
const sessionManager = manager();
|
|
942
1011
|
const captured = makeExtension(sessionManager);
|
|
943
1012
|
const ctx = context(sessionManager);
|
|
944
|
-
// Unique Unicode code points so an overlap introduced by a naive head+tail concat is detectable.
|
|
945
1013
|
const longText = Array.from({ length: 400 }, (_, index) => String.fromCharCode(0x4e00 + index)).join("");
|
|
946
1014
|
const shortText = "short-first\nshort-second";
|
|
947
1015
|
await call(captured, "notes_write", { path: "long.md", content: longText }, ctx);
|
|
@@ -950,19 +1018,11 @@ test("the boot notes preview keeps short notes whole and long notes head-to-tail
|
|
|
950
1018
|
const boot = captured.sent[0];
|
|
951
1019
|
const text = typeof boot?.message.content === "string" ? boot.message.content : "";
|
|
952
1020
|
assert.ok(text.includes("long.md") && text.includes("short.md"), "both notes are indexed");
|
|
953
|
-
//
|
|
954
|
-
assert.
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
const tail = chars.slice(chars.length - 240).join("");
|
|
959
|
-
const previewLine = text.split("\n").find((line) => line.startsWith(" ") && line.includes("…"));
|
|
960
|
-
assert.ok(previewLine, "long note carries an ellipsis preview line");
|
|
961
|
-
const preview = Array.from(previewLine.slice(2));
|
|
962
|
-
assert.ok(previewLine.includes(head), "long preview keeps the head");
|
|
963
|
-
assert.ok(previewLine.includes(tail), "long preview keeps the tail");
|
|
964
|
-
assert.equal(preview.length, 321, "head 80 + one separator + tail 240, nothing duplicated");
|
|
965
|
-
assert.equal(previewLine.includes(longText), false, "long note is truncated, not shown whole");
|
|
1021
|
+
// Each note is exactly one metadata line: address, line count, byte count, timestamp.
|
|
1022
|
+
assert.match(text, /^- long\.md \(1 lines, \d+ UTF-8 bytes, updated [^)]+\)$/m, "the long note is a single metadata line");
|
|
1023
|
+
assert.match(text, /^- short\.md \(2 lines, \d+ UTF-8 bytes, updated [^)]+\)$/m, "the short note is a single metadata line");
|
|
1024
|
+
assert.equal(text.includes(longText), false, "the long note's body never reaches boot");
|
|
1025
|
+
assert.equal(text.includes(shortText), false, "the short note's body never reaches boot");
|
|
966
1026
|
});
|
|
967
1027
|
test("the boot block is persisted at the root and baked into every reset summary", async () => {
|
|
968
1028
|
const sessionManager = manager();
|
|
@@ -986,8 +1046,7 @@ test("the boot block is persisted at the root and baked into every reset summary
|
|
|
986
1046
|
assert.ok(rootText.includes("decisions.md"));
|
|
987
1047
|
const decisionsMeta = listNotes(ctx, { scope: "session" }).find((row) => row.path === "decisions.md")?.meta;
|
|
988
1048
|
assert.ok(decisionsMeta);
|
|
989
|
-
|
|
990
|
-
assert.equal(Date.parse(bootUpdated), decisionsMeta.updated_at, "boot note timestamp restores the persisted updatedAt");
|
|
1049
|
+
assert.match(rootText, /updated \d+s ago\)/, "boot note metadata carries a relative update time");
|
|
991
1050
|
assert.ok(rootText.includes(internal.CONTEXT_WINDOW_PROTOCOL_OPEN_TAG));
|
|
992
1051
|
// Reset: the boot block IS the compaction summary; no separate boot/hint is persisted.
|
|
993
1052
|
await call(captured, "new_context", {}, ctx);
|
|
@@ -1001,8 +1060,8 @@ test("the boot block is persisted at the root and baked into every reset summary
|
|
|
1001
1060
|
assert.equal(before.compaction.summary.startsWith(internal.CONTEXT_WINDOW_OPEN_TAG), false, "a reset line precedes the identity block");
|
|
1002
1061
|
assert.match(before.compaction.summary, new RegExp(`Current context window id: ${details.windowId}`));
|
|
1003
1062
|
assert.ok(before.compaction.summary.includes("decisions.md"));
|
|
1004
|
-
|
|
1005
|
-
assert.equal(
|
|
1063
|
+
assert.match(before.compaction.summary, /updated \d+s ago\)/, "reset summary carries a relative update time");
|
|
1064
|
+
assert.equal(listNotes(ctx, { scope: "session" }).find((row) => row.path === "decisions.md")?.meta.updated_at, decisionsMeta.updated_at, "rendering relative time preserves the stored timestamp");
|
|
1006
1065
|
assert.ok(before.compaction.summary.includes(internal.CONTEXT_WINDOW_PROTOCOL_OPEN_TAG));
|
|
1007
1066
|
const windows = historyFromSession(ctx);
|
|
1008
1067
|
assert.ok(before.compaction.summary.includes(`Previous context window id: ${windows[windows.length - 1]?.windowId}`));
|
|
@@ -1584,7 +1643,8 @@ test("malformed frontmatter timestamps degrade to a finite fallback without pois
|
|
|
1584
1643
|
assert.ok(Number.isFinite(rows[0].meta.updated_at), "a malformed timestamp degrades to a finite fallback");
|
|
1585
1644
|
runHandlers(extension, "session_start", {}, ctx);
|
|
1586
1645
|
const rendered = JSON.stringify(extension.sent);
|
|
1587
|
-
assert.ok(rendered.includes("
|
|
1646
|
+
assert.ok(rendered.includes("good.md"), "the valid note still renders as a metadata line");
|
|
1647
|
+
assert.equal(rendered.includes("keep me"), false, "note bodies stay out of the boot block");
|
|
1588
1648
|
assert.equal(rendered.includes("NaN"), false, "no malformed timestamp leaks into the boot block");
|
|
1589
1649
|
});
|
|
1590
1650
|
test("JSONL reload retains once-per-window boot and reminder without runtime memory", () => {
|
|
@@ -1663,7 +1723,7 @@ test("argument footguns die loudly and tool-run metadata surfaces (A1/A2/A3/B4/B
|
|
|
1663
1723
|
assert.match(notePastEnd.error ?? "", /past the end/, "notes: past-end offset is a named error");
|
|
1664
1724
|
assert.equal(notePastEnd.offset_chars, noteTotal + 1, "notes: the error echoes the offending offset");
|
|
1665
1725
|
assert.equal(notePastEnd.total_chars, noteTotal, "notes: the error names the real length");
|
|
1666
|
-
assert.equal(notePastEnd.
|
|
1726
|
+
assert.equal(notePastEnd.address, "a.md", "notes: the error echoes the address");
|
|
1667
1727
|
const noteEnd = resultRead(await call(captured, "notes_read", { path: "a.md", offset_chars: noteTotal }, ctx));
|
|
1668
1728
|
assert.equal(noteEnd.content, "", "notes: offset == total is the legal empty end-read");
|
|
1669
1729
|
assert.equal(noteEnd.next_offset_chars, null, "notes: the end-read terminates");
|