@astrosheep/pi-context 0.26.0 → 0.26.2

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.
Files changed (37) hide show
  1. package/README.md +2 -2
  2. package/dist/build-info.json +2 -2
  3. package/dist/extension.js +69 -64
  4. package/dist/src/context/budget.js +37 -18
  5. package/dist/src/context/prompts.js +3 -3
  6. package/dist/src/dream/doctor.js +0 -4
  7. package/dist/src/notes/frontmatter.d.ts +1 -2
  8. package/dist/src/notes/frontmatter.js +1 -6
  9. package/dist/src/notes/store.js +24 -40
  10. package/dist/src/protocol.d.ts +4 -4
  11. package/dist/src/protocol.js +4 -4
  12. package/dist/src/text-match.d.ts +5 -0
  13. package/dist/src/text-match.js +15 -0
  14. package/dist/src/tool-output.d.ts +1 -5
  15. package/dist/src/tool-output.js +1 -15
  16. package/dist/test/agent-loop.test.js +78 -10
  17. package/dist/test/boot.integration.test.js +5 -7
  18. package/dist/test/budget-settings.integration.test.js +20 -5
  19. package/dist/test/doctor.test.js +4 -5
  20. package/dist/test/helpers/extension-test-environment.d.ts +1 -0
  21. package/dist/test/helpers/extension-test-environment.js +9 -0
  22. package/dist/test/helpers/extension.d.ts +0 -11
  23. package/dist/test/helpers/extension.js +1 -75
  24. package/dist/test/history.integration.test.js +17 -19
  25. package/dist/test/notes-library.test.js +17 -0
  26. package/dist/test/notes.integration.test.js +47 -31
  27. package/dist/test/notes.test.js +51 -44
  28. package/docs/architecture.md +1 -1
  29. package/package.json +1 -1
  30. package/src/context/budget.ts +35 -18
  31. package/src/context/prompts.ts +3 -3
  32. package/src/dream/doctor.ts +0 -3
  33. package/src/notes/frontmatter.ts +1 -5
  34. package/src/notes/store.ts +24 -37
  35. package/src/protocol.ts +4 -4
  36. package/src/text-match.ts +13 -0
  37. package/src/tool-output.ts +2 -14
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import type { Dirent } from "node:fs";
3
3
  import { mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
4
  import { dirname, join, resolve } from "node:path";
5
+ import { earliestMatchOffsetChars } from "../text-match.js";
5
6
  import { assertAddress, assertGlobPattern, addressFor, globToRegExp } from "./address.js";
6
7
  import { snapshotNotesContext, type NotesContext } from "./context.js";
7
8
  import { MAX_NOTE_BYTES, MAX_NOTE_PATH_BYTES } from "./constants.js";
@@ -179,17 +180,6 @@ function matchLineNumbers(body: string, needle: string): number[] {
179
180
  return lines;
180
181
  }
181
182
 
182
- /** Code-point offset of the earliest query occurrence, matching the serialized read text. */
183
- function earliestMatchOffsetChars(text: string, queries: string[]): number {
184
- let earliest = -1;
185
- for (const query of queries) {
186
- const index = text.indexOf(query);
187
- if (index < 0) continue;
188
- if (earliest < 0 || index < earliest) earliest = index;
189
- }
190
- return earliest <= 0 ? 0 : Array.from(text.slice(0, earliest)).length;
191
- }
192
-
193
183
  /** Every mutation uses a tmp file renamed into place in the same directory. */
194
184
  async function atomicWrite(path: string, content: string): Promise<void> {
195
185
  await mkdir(dirname(path), { recursive: true });
@@ -348,11 +338,10 @@ export function createNotesStore(input: NotesContext): NotesStore {
348
338
  });
349
339
  }
350
340
 
351
- async function list(options: NotesQuery = {}): Promise<NoteRow[]> {
352
- const stableOptions = { ...options } as NotesQuery;
353
- const matcher = matcherFor(normalizePattern(stableOptions.pattern, context));
354
- const rows: NoteRow[] = [];
355
- for (const home of await homesFor(context, stableOptions)) {
341
+ async function* scan(options: NotesQuery): AsyncGenerator<Omit<NoteRow, "sizeBytes">> {
342
+ const matcher = matcherFor(normalizePattern(options.pattern, context));
343
+ const homes = await homesFor(context, options);
344
+ for (const home of homes) {
356
345
  const scope = home.scope;
357
346
  const root = scopeDir(scope, context, home.who);
358
347
  for (const path of await walkMarkdown(root)) {
@@ -362,9 +351,17 @@ export function createNotesStore(input: NotesContext): NotesStore {
362
351
  const raw = await withPathQueue(fullPath, () => readFile(fullPath, "utf8"));
363
352
  const { meta, body } = parseNote(raw);
364
353
  meta.scope = scope;
365
- rows.push({ address, scope, path, meta, body, sizeBytes: Buffer.byteLength(body, "utf8") });
354
+ yield { address, scope, path, meta, body };
366
355
  }
367
356
  }
357
+ }
358
+
359
+ async function list(options: NotesQuery = {}): Promise<NoteRow[]> {
360
+ const stableOptions = { ...options } as NotesQuery;
361
+ const rows: NoteRow[] = [];
362
+ for await (const row of scan(stableOptions)) {
363
+ rows.push({ ...row, sizeBytes: Buffer.byteLength(row.body, "utf8") });
364
+ }
368
365
  rows.sort((a, b) => b.meta.updatedAt - a.meta.updatedAt || a.address.localeCompare(b.address));
369
366
  return rows;
370
367
  }
@@ -372,29 +369,19 @@ export function createNotesStore(input: NotesContext): NotesStore {
372
369
  async function search(queries: string[], options: NotesQuery = {}): Promise<NoteSearchRow[]> {
373
370
  const stableQueries = [...queries];
374
371
  const stableOptions = { ...options } as NotesQuery;
375
- const matcher = matcherFor(normalizePattern(stableOptions.pattern, context));
376
372
  const rows: NoteSearchRow[] = [];
377
- for (const home of await homesFor(context, stableOptions)) {
378
- const scope = home.scope;
379
- const root = scopeDir(scope, context, home.who);
380
- for (const path of await walkMarkdown(root)) {
381
- const address = addressFor(context, scope, path, home.who);
382
- if (matcher && !matcher.test(address)) continue;
383
- const fullPath = join(root, path);
384
- const raw = await withPathQueue(fullPath, () => readFile(fullPath, "utf8"));
385
- const { meta, body } = parseNote(raw);
386
- meta.scope = scope;
387
- const serializedBodyOffset = Array.from(serializeNote(accessedMeta(meta, scope, Date.now()), "")).length;
388
- let baseChars = 0;
389
- const matches: NoteMatch[] = [];
390
- for (const [index, line] of body.split("\n").entries()) {
391
- if (stableQueries.some((query) => line.includes(query))) {
392
- matches.push({ line: index + 1, text: line, offsetChars: serializedBodyOffset + baseChars + earliestMatchOffsetChars(line, stableQueries) });
393
- }
394
- baseChars += Array.from(line).length + 1;
373
+ for await (const note of scan(stableOptions)) {
374
+ const { address, path, scope, meta, body } = note;
375
+ const serializedBodyOffset = Array.from(serializeNote(accessedMeta(meta, scope, Date.now()), "")).length;
376
+ let baseChars = 0;
377
+ const matches: NoteMatch[] = [];
378
+ for (const [index, line] of body.split("\n").entries()) {
379
+ if (stableQueries.some((query) => line.includes(query))) {
380
+ matches.push({ line: index + 1, text: line, offsetChars: serializedBodyOffset + baseChars + earliestMatchOffsetChars(line, stableQueries) });
395
381
  }
396
- if (matches.length > 0) rows.push({ address, path, scope, meta, matches });
382
+ baseChars += Array.from(line).length + 1;
397
383
  }
384
+ if (matches.length > 0) rows.push({ address, path, scope, meta, matches });
398
385
  }
399
386
  rows.sort((a, b) => a.address.localeCompare(b.address));
400
387
  return rows;
package/src/protocol.ts CHANGED
@@ -6,10 +6,10 @@ export const RESET_MARKER_TYPE = "pi-context/reset-marker";
6
6
  export const CONTINUATION_TYPE = "pi-context/continuation";
7
7
  export { MAX_NOTE_BYTES, MAX_NOTE_PATH_BYTES } from "./notes/constants.js";
8
8
  export const POCKET_SESSION_LIMIT = 5;
9
- export const POCKET_PROJECT_LIMIT = 2;
10
- export const POCKET_HUMAN_LIMIT = 2;
11
- export const POCKET_AGENT_LIMIT = 1;
12
- export const POCKET_MODEL_LIMIT = 1;
9
+ export const POCKET_PROJECT_LIMIT = 5;
10
+ export const POCKET_HUMAN_LIMIT = 5;
11
+ export const POCKET_AGENT_LIMIT = 5;
12
+ export const POCKET_MODEL_LIMIT = 3;
13
13
  export const CONTEXT_WINDOW_OPEN_TAG = "<context_window>";
14
14
  export const CONTEXT_WINDOW_CLOSE_TAG = "</context_window>";
15
15
  export const CONTEXT_WINDOW_PROTOCOL_OPEN_TAG = "<context_window_protocol>";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Code-point offset of the earliest occurrence of any of `queries` in `text`, or 0 when
3
+ * none occurs. Shared by the two search tools so a match address is computed identically.
4
+ */
5
+ export function earliestMatchOffsetChars(text: string, queries: string[]): number {
6
+ let earliest = -1;
7
+ for (const query of queries) {
8
+ const index = text.indexOf(query);
9
+ if (index < 0) continue;
10
+ if (earliest < 0 || index < earliest) earliest = index;
11
+ }
12
+ return earliest <= 0 ? 0 : Array.from(text.slice(0, earliest)).length;
13
+ }
@@ -1,3 +1,5 @@
1
+ export { earliestMatchOffsetChars } from "./text-match.js";
2
+
1
3
  export const TOOL_OUTPUT_MAX_BYTES = 32 * 1024;
2
4
  export const DEFAULT_READ_WINDOW_CHARS = 12000;
3
5
  export const MAX_READ_WINDOW_CHARS = 50000;
@@ -125,20 +127,6 @@ export function readWindowBlock(identity: ReadonlyArray<readonly [string, string
125
127
  return `--- READ WINDOW ---\n${fields}\nchars: [${window.offset_chars},${end}) of ${window.total_chars}\nnext_offset_chars: ${next}\n`;
126
128
  }
127
129
 
128
- /**
129
- * Code-point offset of the earliest occurrence of any of `queries` in `text`, or 0 when
130
- * none occurs. Shared by the two search tools so a match address is computed identically.
131
- */
132
- export function earliestMatchOffsetChars(text: string, queries: string[]): number {
133
- let earliest = -1;
134
- for (const query of queries) {
135
- const index = text.indexOf(query);
136
- if (index < 0) continue;
137
- if (earliest < 0 || index < earliest) earliest = index;
138
- }
139
- return earliest <= 0 ? 0 : Array.from(text.slice(0, earliest)).length;
140
- }
141
-
142
130
  /** Shrink a single page item to fit; only invoked when that item alone exceeds the budget. */
143
131
  export type ItemTruncator<T> = (item: T, fits: (candidate: T) => boolean) => T;
144
132