@astrosheep/pi-context 0.26.0 → 0.26.1

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.
@@ -1,11 +1,9 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { internal } from "../src/index.js";
4
- import { appendText, call, commitTurnEndBoundary, context, installExtensionTestEnvironment, makeExtension, manager, noticesOf, resultJson, runContextHook, runHandlers, sentOf, settingsFixture, } from "./helpers/extension.js";
5
- const testEnvironment = installExtensionTestEnvironment("pi-context-integration");
6
- test.beforeEach(() => testEnvironment.beforeEach());
7
- test.afterEach(() => testEnvironment.afterEach());
8
- test.after(() => testEnvironment.dispose());
4
+ import { appendText, call, commitTurnEndBoundary, context, makeExtension, manager, noticesOf, resultJson, runContextHook, runHandlers, sentOf, settingsFixture, } from "./helpers/extension.js";
5
+ import { installExtensionTestHooks } from "./helpers/extension-test-environment.js";
6
+ const testEnvironment = installExtensionTestHooks("pi-context-integration");
9
7
  test("low-budget guidance and warning persist at turn_end, once per active window", async () => {
10
8
  const sessionManager = manager();
11
9
  const captured = makeExtension(sessionManager);
@@ -24,6 +22,23 @@ test("low-budget guidance and warning persist at turn_end, once per active windo
24
22
  assert.equal(warningBoundary.entries.filter((entry) => entry.type === "custom_message" && entry.customType === internal.WARNING_TYPE).length, 1);
25
23
  assert.equal(sessionManager.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === internal.WARNING_TYPE).length, 1);
26
24
  });
25
+ test("early guidance stays silent and the committed final warning notifies once", async () => {
26
+ const sm = manager();
27
+ const captured = makeExtension(sm);
28
+ const early = context(sm, undefined, { tokens: 165_000, percent: 82.5, contextWindow: 200_000 });
29
+ assert.equal(await runContextHook(captured, early), undefined, "early guidance is persisted at turn_end, not injected immediately");
30
+ await commitTurnEndBoundary(captured, sm, early);
31
+ await runHandlers(captured, "agent_settled", {}, early);
32
+ assert.equal(noticesOf(early).filter((notice) => notice.message.includes("Context running low") || notice.message.includes("Context almost full")).length, 0, "early model guidance has no UI toast");
33
+ assert.equal(sm.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === internal.GUIDANCE_TYPE).length, 1, "early guidance remains durable for the model");
34
+ const final = context(sm, undefined, { tokens: 181_000, percent: 90.5, contextWindow: 200_000 });
35
+ const warning = await runContextHook(captured, final);
36
+ assert.equal(warning?.messages[0]?.customType, internal.WARNING_TYPE);
37
+ await commitTurnEndBoundary(captured, sm, final);
38
+ await runHandlers(captured, "turn_start", {}, final);
39
+ await runHandlers(captured, "agent_settled", {}, final);
40
+ assert.equal(noticesOf(final).filter((notice) => notice.message === "pi-context: Context almost full; close out the current memory window.").length, 1, "only the committed final warning notifies, exactly once");
41
+ });
27
42
  test("the visible countdown ends at the warning line, clamps at zero, and preserves unknown usage", async () => {
28
43
  const fixture = settingsFixture({
29
44
  reserveTokens: 16_384,
@@ -0,0 +1 @@
1
+ export declare function installExtensionTestHooks(prefix: string): import("./extension.js").ExtensionTestEnvironment;
@@ -0,0 +1,9 @@
1
+ import test from "node:test";
2
+ import { installExtensionTestEnvironment } from "./extension.js";
3
+ export function installExtensionTestHooks(prefix) {
4
+ const environment = installExtensionTestEnvironment(prefix);
5
+ test.beforeEach(() => environment.beforeEach());
6
+ test.afterEach(() => environment.afterEach());
7
+ test.after(() => environment.dispose());
8
+ return environment;
9
+ }
@@ -88,19 +88,8 @@ export type ReadWindow = {
88
88
  };
89
89
  /** Decode either raw read without including its shared metadata block in the payload. */
90
90
  export declare function resultRead(result: AgentToolResult<unknown>): ReadWindow;
91
- /**
92
- * Assert a value is a local-time ISO 8601 string with an explicit numeric offset (never "Z")
93
- * and that Date.parse restores the stored epoch milliseconds. No time zone is assumed.
94
- */
95
- export declare function assertLocalIso(value: unknown, epochMs: number, message: string): void;
96
- /** Assert the text contains a well-formed local ISO timestamp and return it, without pinning surrounding wording. */
97
- export declare function assertIsoTimestamp(text: string, message: string): string;
98
- /** Assert `actual` is a middle-truncation of `original`: same head, same tail, strictly fewer characters. */
99
- export declare function assertTruncationOf(original: string, actual: string): void;
100
91
  export declare function runManualCompact(captured: Captured, ctx: ExtensionContext): Promise<CompactionHookResult>;
101
92
  export declare function runHandlers(captured: Captured, name: string, event: unknown, ctx: ExtensionContext): Promise<void>;
102
- export declare function runHandlersAsync(captured: Captured, name: string, event: unknown, ctx: ExtensionContext): Promise<unknown[]>;
103
- export declare function completeRequestedCompaction(ctx: ExtensionContext): void;
104
93
  export declare function runCommand(captured: Captured, name: string, args: string, ctx: ExtensionContext): Promise<Notice[]>;
105
94
  export type ContextHookResult = {
106
95
  messages: unknown[];
@@ -166,41 +166,12 @@ export function sentOf(captured, customType) {
166
166
  export async function call(captured, name, params, ctx) {
167
167
  const tool = captured.tools.get(name);
168
168
  assert.ok(tool, `registered ${name}`);
169
- // Most pre-redesign coverage names session notes by their bare address. Keep these old
170
- // fixture call sites readable while routing the direct tool invocation through its new
171
- // address-shaped input; contract-specific tests below pass address themselves.
172
- const noteCall = name === "notes_write" || name === "notes_edit" || name === "notes_read";
173
- if (noteCall && "path" in params && !("address" in params)) {
174
- const { path, scope, ...rest } = params;
175
- assert.equal(typeof path, "string", "legacy note fixture path is a string");
176
- const address = scope === "project" ? `@project/${path}` : scope === "human" ? `@human/${path}` : path;
177
- return tool.execute("call-1", { ...rest, address }, new AbortController().signal, () => { }, ctx);
178
- }
179
- if ((name === "notes_list" || name === "notes_search") && params.scope === "human") {
180
- const { scope: _scope, pattern, ...rest } = params;
181
- return tool.execute("call-1", { ...rest, pattern: `@human/${typeof pattern === "string" ? pattern : "**"}` }, new AbortController().signal, () => { }, ctx);
182
- }
183
- if ((name === "notes_list" || name === "notes_search") && params.scope === "session") {
184
- const { scope: _scope, pattern, ...rest } = params;
185
- return tool.execute("call-1", { ...rest, pattern: typeof pattern === "string" ? pattern : "*.md" }, new AbortController().signal, () => { }, ctx);
186
- }
187
169
  return tool.execute("call-1", params, new AbortController().signal, () => { }, ctx);
188
170
  }
189
171
  export function resultJson(result) {
190
172
  const text = result.content[0];
191
173
  assert.ok(text && text.type === "text", "tool result carries text");
192
- const value = JSON.parse(text.text);
193
- const suffix = (address) => address.startsWith("@project/") ? address.slice("@project/".length) : address.startsWith("@human/") ? address.slice("@human/".length) : address;
194
- const legacyPath = (row) => {
195
- if (typeof row.address === "string" && row.path === undefined)
196
- Object.defineProperty(row, "path", { value: suffix(row.address), enumerable: false });
197
- };
198
- legacyPath(value);
199
- if (Array.isArray(value.files))
200
- for (const file of value.files)
201
- if (file && typeof file === "object")
202
- legacyPath(file);
203
- return value;
174
+ return JSON.parse(text.text);
204
175
  }
205
176
  /** Assert the delivered wire text fits the tool-output budget, header included for raw reads. */
206
177
  export function assertWithinBudget(result, message) {
@@ -223,32 +194,6 @@ export function resultRead(result) {
223
194
  assert.equal(Array.from(content).length, end - offset_chars, "READ WINDOW range matches the delivered payload");
224
195
  return { header, content, offset_chars, total_chars, next_offset_chars, details: (result.details ?? {}) };
225
196
  }
226
- /**
227
- * Assert a value is a local-time ISO 8601 string with an explicit numeric offset (never "Z")
228
- * and that Date.parse restores the stored epoch milliseconds. No time zone is assumed.
229
- */
230
- export function assertLocalIso(value, epochMs, message) {
231
- assert.equal(typeof value, "string", message);
232
- assert.match(value, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/, message);
233
- assert.equal(Date.parse(value), epochMs, `${message}: Date.parse restores the stored epoch ms`);
234
- }
235
- /** Assert the text contains a well-formed local ISO timestamp and return it, without pinning surrounding wording. */
236
- export function assertIsoTimestamp(text, message) {
237
- const match = text.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}/);
238
- assert.ok(match, message);
239
- assert.equal(Number.isNaN(Date.parse(match[0])), false, `${message}: timestamp parses`);
240
- return match[0];
241
- }
242
- /** Assert `actual` is a middle-truncation of `original`: same head, same tail, strictly fewer characters. */
243
- export function assertTruncationOf(original, actual) {
244
- const match = actual.match(/^([\s\S]*)…\[truncated \d+ chars\]…([\s\S]*)$/);
245
- assert.ok(match, "truncated value carries the middle-truncation marker");
246
- const head = match[1];
247
- const tail = match[2];
248
- assert.ok(original.startsWith(head), "truncation keeps the original head");
249
- assert.ok(original.endsWith(tail), "truncation keeps the original tail");
250
- assert.ok(head.length + tail.length < original.length, "truncation actually removes characters");
251
- }
252
197
  export async function runManualCompact(captured, ctx) {
253
198
  const handler = captured.handlers.get("session_before_compact")?.[0];
254
199
  assert.ok(handler, "session_before_compact handler registered");
@@ -282,25 +227,6 @@ export async function runHandlers(captured, name, event, ctx) {
282
227
  ctx.isIdle = isIdle;
283
228
  }
284
229
  }
285
- export async function runHandlersAsync(captured, name, event, ctx) {
286
- const results = [];
287
- for (const handler of captured.handlers.get(name) ?? [])
288
- results.push(await handler(event, ctx));
289
- return results;
290
- }
291
- export function completeRequestedCompaction(ctx) {
292
- const requests = ctx.compactionRequests;
293
- const options = requests.shift();
294
- assert.ok(options?.onComplete, "a reset request has a completion callback");
295
- const isIdle = ctx.isIdle;
296
- ctx.isIdle = () => true;
297
- try {
298
- options.onComplete({});
299
- }
300
- finally {
301
- ctx.isIdle = isIdle;
302
- }
303
- }
304
230
  export async function runCommand(captured, name, args, ctx) {
305
231
  const command = captured.commands.get(name);
306
232
  assert.ok(command, `${name} command registered`);
@@ -2,11 +2,9 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { historyFromSession } from "../src/index.js";
4
4
  import { TOOL_OUTPUT_MAX_BYTES } from "../src/tool-output.js";
5
- import { appendText, assertWithinBudget, call, context, installExtensionTestEnvironment, makeExtension, manager, objectSchema, resultJson, resultRead, } from "./helpers/extension.js";
6
- const testEnvironment = installExtensionTestEnvironment("pi-context-integration");
7
- test.beforeEach(() => testEnvironment.beforeEach());
8
- test.afterEach(() => testEnvironment.afterEach());
9
- test.after(() => testEnvironment.dispose());
5
+ import { appendText, assertWithinBudget, call, context, makeExtension, manager, objectSchema, resultJson, resultRead, } from "./helpers/extension.js";
6
+ import { installExtensionTestHooks } from "./helpers/extension-test-environment.js";
7
+ const testEnvironment = installExtensionTestHooks("pi-context-integration");
10
8
  test("schemas cover the History/Notes actions plus reset controls", () => {
11
9
  const captured = makeExtension(manager());
12
10
  for (const name of [
@@ -94,7 +92,7 @@ test("paged tool outputs stay bounded and cursors reconstruct history and notes"
94
92
  }
95
93
  assert.equal(readParts.join(""), historyText);
96
94
  for (let index = 0; index < 100; index++) {
97
- await call(captured, "notes_write", { path: `page-${"x".repeat(120)}-${index}.md`, content: Array.from({ length: 1000 }, (_, line) => `needle ${line} ${"z".repeat(30)}`).join("\n") }, ctx);
95
+ await call(captured, "notes_write", { address: `page-${"x".repeat(120)}-${index}.md`, content: Array.from({ length: 1000 }, (_, line) => `needle ${line} ${"z".repeat(30)}`).join("\n") }, ctx);
98
96
  }
99
97
  const listPages = [];
100
98
  let listOffset = 0;
@@ -102,7 +100,7 @@ test("paged tool outputs stay bounded and cursors reconstruct history and notes"
102
100
  while (listNext !== null) {
103
101
  const result = resultJson(await call(captured, "notes_list", { max_results: 300, cursor: listOffset }, ctx));
104
102
  assert.ok(Buffer.byteLength(JSON.stringify(result), "utf8") <= TOOL_OUTPUT_MAX_BYTES);
105
- listPages.push(...result.files.map((file) => file.path));
103
+ listPages.push(...result.files.map((file) => file.address));
106
104
  listNext = result.next_cursor;
107
105
  if (listNext !== null)
108
106
  listOffset = listNext;
@@ -127,7 +125,7 @@ test("paged tool outputs stay bounded and cursors reconstruct history and notes"
127
125
  let noteOffset = 0;
128
126
  let noteNext = 0;
129
127
  while (noteNext !== null) {
130
- const raw = await call(captured, "notes_read", { path: `page-${"x".repeat(120)}-0.md`, offset_chars: noteOffset }, ctx);
128
+ const raw = await call(captured, "notes_read", { address: `page-${"x".repeat(120)}-0.md`, offset_chars: noteOffset }, ctx);
131
129
  assertWithinBudget(raw, `notes_read page at ${noteOffset}`);
132
130
  const result = resultRead(raw);
133
131
  // The window is a plain prefix of the file, so the pages join by plain concatenation.
@@ -172,7 +170,7 @@ test("a page cap limits the page, not the enumerable set: cursors stay truthful
172
170
  assert.equal(searchTail.next_cursor, null);
173
171
  // notes_search: max_files caps the page, not the matched files.
174
172
  for (let index = 0; index < 7; index++)
175
- await call(captured, "notes_write", { path: `needle-${index}.md`, content: "needle" }, ctx);
173
+ await call(captured, "notes_write", { address: `needle-${index}.md`, content: "needle" }, ctx);
176
174
  const notes = async (params) => resultJson(await call(captured, "notes_search", params, ctx));
177
175
  const notesFirst = await notes({ query: "needle", max_files: 3 });
178
176
  assert.equal(notesFirst.files.length, 3);
@@ -200,18 +198,18 @@ test("multi-query search: OR semantics, dedupe, and bare-string backward compati
200
198
  assert.deepEqual(await historyIds({ query: ["alpha"] }), [bothId, alphaId], "history: a one-element array searches that literal");
201
199
  assert.deepEqual(await historyIds({ query: "alpha" }), orIds.filter((id) => id !== betaId), "history: a bare string still behaves exactly as before");
202
200
  assert.deepEqual(await historyIds({ query: "alpha" }), await historyIds({ query: ["alpha"] }), "history: bare string equals the single-element list");
203
- await call(captured, "notes_write", { path: "both.md", content: "alpha beta\nunrelated" }, ctx);
204
- await call(captured, "notes_write", { path: "alpha.md", content: "alpha only" }, ctx);
205
- await call(captured, "notes_write", { path: "beta.md", content: "beta only" }, ctx);
206
- await call(captured, "notes_write", { path: "gamma.md", content: "gamma only" }, ctx);
201
+ await call(captured, "notes_write", { address: "both.md", content: "alpha beta\nunrelated" }, ctx);
202
+ await call(captured, "notes_write", { address: "alpha.md", content: "alpha only" }, ctx);
203
+ await call(captured, "notes_write", { address: "beta.md", content: "beta only" }, ctx);
204
+ await call(captured, "notes_write", { address: "gamma.md", content: "gamma only" }, ctx);
207
205
  const notesSearch = async (params) => resultJson(await call(captured, "notes_search", params, ctx)).files;
208
206
  const orFiles = await notesSearch({ query: ["alpha", "beta"] });
209
- assert.deepEqual(orFiles.map((file) => file.path), ["alpha.md", "beta.md", "both.md"], "notes: a file matching any query is returned once, path-ordered");
210
- assert.equal(orFiles.find((file) => file.path === "both.md")?.matches.length, 1, "notes: one line containing both queries is reported once");
211
- assert.deepEqual((await notesSearch({ query: ["alpha"] })).map((file) => file.path), ["alpha.md", "both.md"], "notes: a one-element array searches that literal");
212
- assert.deepEqual((await notesSearch({ query: "alpha" })).map((file) => file.path), ["alpha.md", "both.md"], "notes: a bare string still behaves exactly as before");
213
- assert.deepEqual((await notesSearch({ query: "alpha" })).map((file) => file.path), (await notesSearch({ query: ["alpha"] })).map((file) => file.path), "notes: bare string equals the single-element list");
214
- assert.deepEqual((await notesSearch({ query: ["gamma"] })).map((file) => file.path), ["gamma.md"]);
207
+ assert.deepEqual(orFiles.map((file) => file.address), ["alpha.md", "beta.md", "both.md"], "notes: a file matching any query is returned once, address-ordered");
208
+ assert.equal(orFiles.find((file) => file.address === "both.md")?.matches.length, 1, "notes: one line containing both queries is reported once");
209
+ assert.deepEqual((await notesSearch({ query: ["alpha"] })).map((file) => file.address), ["alpha.md", "both.md"], "notes: a one-element array searches that literal");
210
+ assert.deepEqual((await notesSearch({ query: "alpha" })).map((file) => file.address), ["alpha.md", "both.md"], "notes: a bare string still behaves exactly as before");
211
+ assert.deepEqual((await notesSearch({ query: "alpha" })).map((file) => file.address), (await notesSearch({ query: ["alpha"] })).map((file) => file.address), "notes: bare string equals the single-element list");
212
+ assert.deepEqual((await notesSearch({ query: ["gamma"] })).map((file) => file.address), ["gamma.md"]);
215
213
  // An empty array is an argument error, not a silently empty result set.
216
214
  await assert.rejects(() => call(captured, "history_search", { query: [] }, ctx), /non-empty array of strings/, "history: empty query array is refused");
217
215
  await assert.rejects(() => call(captured, "notes_search", { query: [] }, ctx), /non-empty array of strings/, "notes: empty query array is refused");
@@ -91,6 +91,23 @@ test("stores snapshot explicit identity and do not leak homes across instances",
91
91
  assert.equal((await notes.read("@agents/visitor/hello"))?.body, "visiting");
92
92
  await assert.rejects(() => notes.write("@agents/visitor/hello", "overwrite"), (error) => error instanceof NoteError && error.code === "invalid_scope");
93
93
  });
94
+ test("list and search share scoped pattern scans and search offsets count Unicode code points", async (t) => {
95
+ const { notes } = fixture(t);
96
+ await notes.write("shared.md", "😀 needle in session");
97
+ await notes.write("@project/shared.md", "needle in project");
98
+ await notes.write("@human/shared.md", "needle in human");
99
+ await notes.write("@project/elsewhere.md", "unrelated");
100
+ const pattern = "**/shared.md";
101
+ const listed = await notes.list({ pattern });
102
+ const searched = await notes.search(["needle"], { pattern });
103
+ const listedAddresses = listed.map((row) => row.address).sort();
104
+ const searchedAddresses = searched.map((row) => row.address).sort();
105
+ assert.deepEqual(searchedAddresses, ["@human/shared.md", "@project/shared.md", "shared.md"]);
106
+ assert.deepEqual(searchedAddresses, listedAddresses, "list and search traverse the same filtered homes and files");
107
+ const sessionMatch = searched.find((row) => row.address === "shared.md").matches[0];
108
+ const serialized = (await notes.read("shared.md")).text;
109
+ assert.ok(Array.from(serialized).slice(sessionMatch.offsetChars).join("").startsWith("needle"), "the absolute offset counts the emoji as one code point");
110
+ });
94
111
  test("invalid addressing and failed edits leave stored bytes untouched without poisoning the queue", async (t) => {
95
112
  const { home, context, notes } = fixture(t);
96
113
  assert.throws(() => createNotesStore({ ...context, sessionId: "../escape" }));
@@ -7,11 +7,9 @@ import { renderBootBlock } from "../src/context/prompts.js";
7
7
  import { localIso } from "../src/notes/frontmatter.js";
8
8
  import { listNotes, physicalPath, scopeDir } from "./helpers/notes.js";
9
9
  import { TOOL_OUTPUT_MAX_BYTES } from "../src/tool-output.js";
10
- import { assertWithinBudget, call, context, explicitBoot, installExtensionTestEnvironment, makeExtension, manager, resultJson, resultRead, } from "./helpers/extension.js";
11
- const testEnvironment = installExtensionTestEnvironment("pi-context-integration");
12
- test.beforeEach(() => testEnvironment.beforeEach());
13
- test.afterEach(() => testEnvironment.afterEach());
14
- test.after(() => testEnvironment.dispose());
10
+ import { assertWithinBudget, call, context, explicitBoot, makeExtension, manager, resultJson, resultRead, } from "./helpers/extension.js";
11
+ import { installExtensionTestHooks } from "./helpers/extension-test-environment.js";
12
+ const testEnvironment = installExtensionTestHooks("pi-context-integration");
15
13
  test("notes_list is most-recently-updated first across merged scopes", async () => {
16
14
  const session = manager();
17
15
  const captured = makeExtension(session);
@@ -37,47 +35,47 @@ test("notes are real files that persist across sessions and round-trip Unicode",
37
35
  const original = manager();
38
36
  const captured = makeExtension(original);
39
37
  const ctx = context(original);
40
- await call(captured, "notes_write", { path: "checkpoint/进度.md", content: "第一行\nneedle Café", scope: "human" }, ctx);
38
+ await call(captured, "notes_write", { address: "@human/checkpoint/进度.md", content: "第一行\nneedle Café" }, ctx);
41
39
  // A brand-new session over the same physical root sees the human note: nothing is replayed
42
40
  // from session entries, the file itself is the durable artifact.
43
41
  const restored = manager();
44
42
  const restoredCaptured = makeExtension(restored);
45
43
  const restoredCtx = context(restored);
46
- const rawRead = await call(restoredCaptured, "notes_read", { path: "checkpoint/进度.md", scope: "human", offset_chars: -4 }, restoredCtx);
44
+ const rawRead = await call(restoredCaptured, "notes_read", { address: "@human/checkpoint/进度.md", offset_chars: -4 }, restoredCtx);
47
45
  const read = resultRead(rawRead);
48
46
  assert.equal(read.details.address, "@human/checkpoint/进度.md");
49
47
  assert.equal(read.content, "Café", "a negative offset reads the body tail in one call");
50
- const searched = resultJson(await call(restoredCaptured, "notes_search", { query: "Café", scope: "human" }, restoredCtx));
48
+ const searched = resultJson(await call(restoredCaptured, "notes_search", { pattern: "@human/**", query: "Café" }, restoredCtx));
51
49
  assert.equal(searched.files[0]?.matches[0]?.line, 2);
52
- const listedFiles = resultJson(await call(restoredCaptured, "notes_list", { pattern: "checkpoint/**", scope: "human" }, restoredCtx));
50
+ const listedFiles = resultJson(await call(restoredCaptured, "notes_list", { pattern: "@human/checkpoint/**" }, restoredCtx));
53
51
  assert.equal(listedFiles.files.length, 1, "glob ** crosses into the checkpoint directory");
54
- assert.equal(listedFiles.files[0]?.path, "checkpoint/进度.md");
52
+ assert.equal(listedFiles.files[0]?.address, "@human/checkpoint/进度.md");
55
53
  // A single-segment * never crosses `/`, so a nested-only store matches nothing at the root.
56
- const rootOnly = resultJson(await call(restoredCaptured, "notes_list", { pattern: "*", scope: "human" }, restoredCtx));
54
+ const rootOnly = resultJson(await call(restoredCaptured, "notes_list", { pattern: "@human/*" }, restoredCtx));
57
55
  assert.equal(rootOnly.files.length, 0, "glob * stays within one segment");
58
56
  assert.equal(searched.files[0]?.updated_at, listedFiles.files[0]?.updated_at);
59
- await assert.rejects(() => call(captured, "notes_write", { path: "../escape", content: "x" }, ctx), /unsupported component/);
57
+ await assert.rejects(() => call(captured, "notes_write", { address: "../escape", content: "x" }, ctx), /unsupported component/);
60
58
  });
61
59
  test("stale lifecycle: writes and metadata-only edits close and revive a note", async () => {
62
60
  const sm = manager();
63
61
  const captured = makeExtension(sm);
64
62
  const ctx = context(sm);
65
- await call(captured, "notes_write", { path: "journal.md", content: "log line" }, ctx);
63
+ await call(captured, "notes_write", { address: "journal.md", content: "log line" }, ctx);
66
64
  // metadata-only: content unchanged, flag set, applied 0
67
- const markOnly = resultJson(await call(captured, "notes_edit", { path: "journal.md", stale: true }, ctx));
65
+ const markOnly = resultJson(await call(captured, "notes_edit", { address: "journal.md", stale: true }, ctx));
68
66
  assert.equal(markOnly.applied, 0);
69
67
  assert.equal((await listNotes(ctx, { scope: "session" }))[0]?.meta.stale, true);
70
- assert.equal(resultRead(await call(captured, "notes_read", { path: "journal.md" }, ctx)).content.endsWith("log line"), true, "mark-only leaves content unchanged");
68
+ assert.equal(resultRead(await call(captured, "notes_read", { address: "journal.md" }, ctx)).content.endsWith("log line"), true, "mark-only leaves content unchanged");
71
69
  // explicit revive
72
- const revived = resultJson(await call(captured, "notes_edit", { path: "journal.md", stale: false }, ctx));
70
+ const revived = resultJson(await call(captured, "notes_edit", { address: "journal.md", stale: false }, ctx));
73
71
  assert.equal((await listNotes(ctx, { scope: "session" }))[0]?.meta.stale, false, "stale:false revives");
74
72
  // write+stale closure then plain write revival
75
- await call(captured, "notes_write", { path: "journal.md", content: "final", stale: true }, ctx);
73
+ await call(captured, "notes_write", { address: "journal.md", content: "final", stale: true }, ctx);
76
74
  assert.equal((await listNotes(ctx, { scope: "session" }))[0]?.meta.stale, true);
77
- await call(captured, "notes_write", { path: "journal.md", content: "reopened" }, ctx);
75
+ await call(captured, "notes_write", { address: "journal.md", content: "reopened" }, ctx);
78
76
  assert.equal((await listNotes(ctx, { scope: "session" }))[0]?.meta.stale, false, "writing without stale revives");
79
77
  // metadata-only on a missing path is the typed not-found arm
80
- const missing = resultJson(await call(captured, "notes_edit", { path: "missing.md", stale: true }, ctx));
78
+ const missing = resultJson(await call(captured, "notes_edit", { address: "missing.md", stale: true }, ctx));
81
79
  assert.equal(missing.error, "note not found");
82
80
  });
83
81
  test("the filesystem notes loader treats an absent home as empty but surfaces a real directory read failure", async () => {
@@ -181,8 +179,8 @@ test("an over-budget note is delivered as a prefix and resumed by next_offset_ch
181
179
  const ctx = context(session);
182
180
  const huge = `H${"x".repeat(TOOL_OUTPUT_MAX_BYTES * 2)}`;
183
181
  const text = `${huge}\ntail line`;
184
- await call(captured, "notes_write", { path: "huge.md", content: text }, ctx);
185
- const rawFirst = await call(captured, "notes_read", { path: "huge.md" }, ctx);
182
+ await call(captured, "notes_write", { address: "huge.md", content: text }, ctx);
183
+ const rawFirst = await call(captured, "notes_read", { address: "huge.md" }, ctx);
186
184
  assertWithinBudget(rawFirst, "single oversized note");
187
185
  const first = resultRead(rawFirst);
188
186
  assert.ok(first.content.length > 0, "the page is not empty");
@@ -196,7 +194,7 @@ test("an over-budget note is delivered as a prefix and resumed by next_offset_ch
196
194
  const parts = [first.content];
197
195
  let offset = first.next_offset_chars;
198
196
  while (offset !== null) {
199
- const rawChunk = await call(captured, "notes_read", { path: "huge.md", offset_chars: offset }, ctx);
197
+ const rawChunk = await call(captured, "notes_read", { address: "huge.md", offset_chars: offset }, ctx);
200
198
  assertWithinBudget(rawChunk, `huge note chunk at ${offset}`);
201
199
  const chunk = resultRead(rawChunk);
202
200
  assert.equal(chunk.offset_chars, offset, "the response echoes the resolved absolute offset");
@@ -205,7 +203,7 @@ test("an over-budget note is delivered as a prefix and resumed by next_offset_ch
205
203
  }
206
204
  assert.ok(parts.join("").endsWith(text), "the cursors reconstruct the body exactly");
207
205
  // A success carries structured details; an error stays a JSON envelope with no details.
208
- const missingResult = await call(captured, "notes_read", { path: "no-such.md" }, ctx);
206
+ const missingResult = await call(captured, "notes_read", { address: "no-such.md" }, ctx);
209
207
  const missing = resultJson(missingResult);
210
208
  assert.deepEqual(Object.keys(missing).sort(), ["address", "error"], "the read error carries exactly error and address");
211
209
  assert.equal(missing.error, "note not found");
@@ -217,8 +215,8 @@ test("an over-budget note search match is a named prefix with an honest line add
217
215
  const ctx = context(session);
218
216
  // The query sits behind a prefix, so its address is a real body-absolute offset, not line 1.
219
217
  const hugeLine = `${'p'.repeat(500)}needle ${"y".repeat(TOOL_OUTPUT_MAX_BYTES * 2)}`;
220
- await call(captured, "notes_write", { path: "a.md", content: "needle small" }, ctx);
221
- await call(captured, "notes_write", { path: "search.md", content: hugeLine }, ctx);
218
+ await call(captured, "notes_write", { address: "a.md", content: "needle small" }, ctx);
219
+ await call(captured, "notes_write", { address: "search.md", content: hugeLine }, ctx);
222
220
  const pages = [];
223
221
  let cursor = 0;
224
222
  let next = 0;
@@ -230,7 +228,7 @@ test("an over-budget note search match is a named prefix with an honest line add
230
228
  if (next !== null)
231
229
  cursor = next;
232
230
  }
233
- assert.deepEqual(pages.map((file) => file.path), ["a.md", "search.md"], "pagination reaches the oversized file instead of looping");
231
+ assert.deepEqual(pages.map((file) => file.address), ["a.md", "search.md"], "pagination reaches the oversized file instead of looping");
234
232
  const oversized = pages[1];
235
233
  assert.equal(oversized.matches_total, 1, "the file's full match count is named even though the line was cut");
236
234
  assert.equal(oversized.matches.length, 1);
@@ -239,13 +237,13 @@ test("an over-budget note search match is a named prefix with an honest line add
239
237
  assert.ok(hugeLine.startsWith(match.text), "the match text is a plain prefix of the line");
240
238
  assert.equal(match.text.includes("…"), false, "no marker is appended to the match text");
241
239
  assert.equal(match.line, 1, "the informational line number survives");
242
- const atMatch = resultRead(await call(captured, "notes_read", { path: "search.md", offset_chars: match.offset_chars }, ctx));
240
+ const atMatch = resultRead(await call(captured, "notes_read", { address: "search.md", offset_chars: match.offset_chars }, ctx));
243
241
  assert.ok(atMatch.content.startsWith("needle"), "the search offset starts a read at the matched substring");
244
242
  // The body is reconstructible by following notes_read's cursor from the start of the file.
245
243
  const parts = [];
246
244
  let offset = 0;
247
245
  while (offset !== null) {
248
- const rawChunk = await call(captured, "notes_read", { path: "search.md", offset_chars: offset }, ctx);
246
+ const rawChunk = await call(captured, "notes_read", { address: "search.md", offset_chars: offset }, ctx);
249
247
  assertWithinBudget(rawChunk, `search.md chunk at ${offset}`);
250
248
  const chunk = resultRead(rawChunk);
251
249
  assert.equal(chunk.offset_chars, offset, "the read echoes the resolved address");
@@ -258,10 +256,10 @@ test("notes_search scopes by glob pattern; a non-matching pattern is an empty pa
258
256
  const session = manager();
259
257
  const captured = makeExtension(session);
260
258
  const ctx = context(session);
261
- await call(captured, "notes_write", { path: "deep/nested/a.md", content: "needle here" }, ctx);
262
- await call(captured, "notes_write", { path: "top.md", content: "needle there" }, ctx);
259
+ await call(captured, "notes_write", { address: "deep/nested/a.md", content: "needle here" }, ctx);
260
+ await call(captured, "notes_write", { address: "top.md", content: "needle there" }, ctx);
263
261
  const scoped = resultJson(await call(captured, "notes_search", { query: "needle", pattern: "deep/**" }, ctx));
264
- assert.deepEqual(scoped.files.map((file) => file.path), ["deep/nested/a.md"], "a glob scopes the search to the subtree");
262
+ assert.deepEqual(scoped.files.map((file) => file.address), ["deep/nested/a.md"], "a glob scopes the search to the subtree");
265
263
  const none = resultJson(await call(captured, "notes_search", { query: "needle", pattern: "absent/**" }, ctx));
266
264
  assert.equal(none.error, undefined, "a non-matching pattern is not an error");
267
265
  assert.deepEqual(none.files, [], "a non-matching pattern is an empty page");