@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.
Files changed (112) hide show
  1. package/README.md +88 -7
  2. package/dist/build-info.json +2 -2
  3. package/dist/extension.js +613 -367
  4. package/dist/src/context/boot.d.ts +24 -0
  5. package/dist/src/context/boot.js +33 -24
  6. package/dist/src/context/budget.d.ts +9 -0
  7. package/dist/src/context/budget.js +16 -12
  8. package/dist/src/context/context-window.d.ts +41 -0
  9. package/dist/src/context/context-window.js +16 -1
  10. package/dist/src/context/prompts.d.ts +20 -0
  11. package/dist/src/context/prompts.js +1 -1
  12. package/dist/src/context/reset-artifacts.d.ts +26 -0
  13. package/dist/src/context/reset-artifacts.js +18 -17
  14. package/dist/src/context/reset-lifecycle.d.ts +89 -0
  15. package/dist/src/context/reset-lifecycle.js +103 -75
  16. package/dist/src/context/runtime.d.ts +3 -0
  17. package/dist/src/context/runtime.js +53 -21
  18. package/dist/src/context/thresholds.d.ts +33 -0
  19. package/dist/src/context/thresholds.js +1 -1
  20. package/dist/src/dream/cli.d.ts +10 -0
  21. package/dist/src/dream/cli.js +1 -1
  22. package/dist/src/dream/doctor.d.ts +2 -0
  23. package/dist/src/dream/doctor.js +6 -2
  24. package/dist/src/dream/gates.d.ts +10 -0
  25. package/dist/src/dream/git.d.ts +21 -0
  26. package/dist/src/dream/lock.d.ts +31 -0
  27. package/dist/src/dream/runner.d.ts +30 -0
  28. package/dist/src/dream/settings.d.ts +16 -0
  29. package/dist/src/history/history-tools.d.ts +2 -0
  30. package/dist/src/history/history.d.ts +57 -0
  31. package/dist/src/index.d.ts +39 -0
  32. package/dist/src/index.js +4 -4
  33. package/dist/src/notes/address.d.ts +26 -0
  34. package/dist/src/notes/address.js +8 -14
  35. package/dist/src/notes/constants.d.ts +3 -0
  36. package/dist/src/notes/constants.js +3 -0
  37. package/dist/src/notes/context.d.ts +10 -0
  38. package/dist/src/notes/context.js +33 -0
  39. package/dist/src/notes/frontmatter.d.ts +46 -0
  40. package/dist/src/notes/frontmatter.js +10 -5
  41. package/dist/src/notes/index.d.ts +4 -0
  42. package/dist/src/notes/index.js +2 -0
  43. package/dist/src/notes/paths.d.ts +21 -0
  44. package/dist/src/notes/paths.js +72 -76
  45. package/dist/src/notes/store.d.ts +94 -0
  46. package/dist/src/notes/store.js +298 -242
  47. package/dist/src/pi/notes/adapter.d.ts +12 -0
  48. package/dist/src/pi/notes/adapter.js +39 -0
  49. package/dist/src/pi/notes/session-replay.d.ts +16 -0
  50. package/dist/src/{notes → pi/notes}/session-replay.js +2 -2
  51. package/dist/src/pi/notes/snapshot.d.ts +33 -0
  52. package/dist/src/{notes/notes-snapshot.js → pi/notes/snapshot.js} +11 -3
  53. package/dist/src/pi/notes/tools.d.ts +2 -0
  54. package/dist/src/{notes → pi/notes}/tools.js +24 -21
  55. package/dist/src/protocol.d.ts +41 -0
  56. package/dist/src/protocol.js +4 -6
  57. package/dist/src/session-reader.d.ts +5 -0
  58. package/dist/src/settings.d.ts +6 -0
  59. package/dist/src/tool-output.d.ts +101 -0
  60. package/dist/src/tool-schema.d.ts +17 -0
  61. package/dist/test/agent-loop.test.d.ts +1 -0
  62. package/dist/test/agent-loop.test.js +309 -10
  63. package/dist/test/boot.integration.test.d.ts +1 -0
  64. package/dist/test/boot.integration.test.js +55 -29
  65. package/dist/test/budget-settings.integration.test.d.ts +1 -0
  66. package/dist/test/budget-settings.integration.test.js +8 -7
  67. package/dist/test/doctor.test.d.ts +1 -0
  68. package/dist/test/doctor.test.js +10 -2
  69. package/dist/test/dream-skill.test.d.ts +1 -0
  70. package/dist/test/dream-skill.test.js +69 -0
  71. package/dist/test/dream.test.d.ts +1 -0
  72. package/dist/test/helpers/extension.d.ts +115 -0
  73. package/dist/test/helpers/extension.js +6 -6
  74. package/dist/test/helpers/notes.d.ts +6 -0
  75. package/dist/test/helpers/notes.js +13 -0
  76. package/dist/test/history.integration.test.d.ts +1 -0
  77. package/dist/test/notes-library.test.d.ts +1 -0
  78. package/dist/test/notes-library.test.js +111 -0
  79. package/dist/test/notes.integration.test.d.ts +1 -0
  80. package/dist/test/notes.integration.test.js +22 -24
  81. package/dist/test/notes.test.d.ts +1 -0
  82. package/dist/test/notes.test.js +137 -7
  83. package/dist/test/reset-lifecycle.test.d.ts +1 -0
  84. package/dist/test/reset-lifecycle.test.js +142 -85
  85. package/docs/architecture.md +8 -8
  86. package/docs/reset-lifecycle.md +63 -79
  87. package/package.json +35 -2
  88. package/playbook.md +33 -32
  89. package/skills/dream/SKILL.md +12 -0
  90. package/src/context/boot.ts +44 -25
  91. package/src/context/budget.ts +19 -11
  92. package/src/context/context-window.ts +16 -1
  93. package/src/context/prompts.ts +2 -2
  94. package/src/context/reset-artifacts.ts +26 -24
  95. package/src/context/reset-lifecycle.ts +117 -111
  96. package/src/context/runtime.ts +50 -22
  97. package/src/context/thresholds.ts +1 -1
  98. package/src/dream/cli.ts +1 -1
  99. package/src/dream/doctor.ts +5 -2
  100. package/src/index.ts +4 -4
  101. package/src/notes/address.ts +9 -15
  102. package/src/notes/constants.ts +3 -0
  103. package/src/notes/context.ts +40 -0
  104. package/src/notes/frontmatter.ts +18 -12
  105. package/src/notes/index.ts +22 -0
  106. package/src/notes/paths.ts +64 -78
  107. package/src/notes/store.ts +308 -244
  108. package/src/pi/notes/adapter.ts +44 -0
  109. package/src/{notes → pi/notes}/session-replay.ts +3 -3
  110. package/src/{notes/notes-snapshot.ts → pi/notes/snapshot.ts} +13 -4
  111. package/src/{notes → pi/notes}/tools.ts +25 -23
  112. package/src/protocol.ts +5 -6
package/dist/extension.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // <define:__PI_CONTEXT_BUILD__>
2
- var define_PI_CONTEXT_BUILD_default = { version: "0.25.2", sourceHash: "e240c1946d0f2b6d7b1d83ed658c348c7daed5fce5a374844215f1eebddcb25f" };
2
+ var define_PI_CONTEXT_BUILD_default = { version: "0.26.0", sourceHash: "48dcba30fd66f729c046d9ec89214e69ada06592df8d464f3b8cc70562dd5174" };
3
3
 
4
4
  // src/index.ts
5
5
  import { VERSION as VERSION2 } from "@earendil-works/pi-coding-agent";
@@ -235,6 +235,10 @@ function estimateToolsTokens(tools) {
235
235
  // src/context/context-window.ts
236
236
  import { convertToLlm } from "@earendil-works/pi-coding-agent";
237
237
 
238
+ // src/notes/constants.ts
239
+ var MAX_NOTE_BYTES = 1e6;
240
+ var MAX_NOTE_PATH_BYTES = 512;
241
+
238
242
  // src/protocol.ts
239
243
  var NOTE_TYPE = "pi-context/note";
240
244
  var BOOT_TYPE = "pi-context/boot";
@@ -242,13 +246,11 @@ var GUIDANCE_TYPE = "pi-context/guidance";
242
246
  var WARNING_TYPE = "pi-context/warning";
243
247
  var RESET_MARKER_TYPE = "pi-context/reset-marker";
244
248
  var CONTINUATION_TYPE = "pi-context/continuation";
245
- var MAX_NOTE_BYTES = 1e6;
246
249
  var POCKET_SESSION_LIMIT = 5;
247
250
  var POCKET_PROJECT_LIMIT = 2;
248
251
  var POCKET_HUMAN_LIMIT = 2;
249
252
  var POCKET_AGENT_LIMIT = 1;
250
253
  var POCKET_MODEL_LIMIT = 1;
251
- var MAX_NOTE_PATH_BYTES = 512;
252
254
  var CONTEXT_WINDOW_OPEN_TAG = "<context_window>";
253
255
  var CONTEXT_WINDOW_CLOSE_TAG = "</context_window>";
254
256
  var CONTEXT_WINDOW_PROTOCOL_OPEN_TAG = "<context_window_protocol>";
@@ -275,7 +277,10 @@ Session notes belong to this trip \u2014 the goal, the progress, the loose ends.
275
277
  @self notes are yours \u2014 your voice, your lessons, your gripes \u2014 for the next run of whoever you are. Other agents read yours by explicit address and never write them; you read theirs the same way. A note only its author would ever need belongs here, not in @human.
276
278
  @model notes capture the substrate \u2014 how the current model actually behaves: context honesty, tool quirks, fallback patterns. @model resolves live, so what you learn on one model is filed under that model even when a fallback moves you mid-window.
277
279
  ${CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG}`;
278
- var WARNING_PROMPT = "Your memory is about to be erased. Write the note. NOW. If it already exists, revise it with notes_edit (or rewrite it whole): the goal, decisions, progress, learnings, next steps, the skills you still need, the window ID and item ID of every relevant user request still being solved, and important actions/tool calls for future reference. Do not continue any task. Then call wipe_memory IMMEDIATELY \u2014 anything not in the note dies with the window.";
280
+ var WARNING_PROMPT = "Your memory is about to be erased. Stop the current task and write the note NOW. If it already exists, revise it with notes_edit (or rewrite it whole): the goal, decisions, progress, learnings, next steps, the skills you still need, the window ID and item ID of every relevant user request still being solved, and important actions/tool calls for future reference. Use as many note/tool turns as needed to finish the checkpoint. When it is ready, call wipe_memory to reset immediately; otherwise finish normally and the extension will reset at your normal stop. Anything not in the note dies with the window.";
281
+ var WARNING_CONTENT = `${GUIDANCE_OPEN_TAG}
282
+ ${WARNING_PROMPT}
283
+ ${GUIDANCE_CLOSE_TAG}`;
279
284
 
280
285
  // src/context/context-window.ts
281
286
  function isWindowMarker(entry) {
@@ -289,8 +294,15 @@ function currentReset(ctx) {
289
294
  }
290
295
  return void 0;
291
296
  }
292
- function rootWindowId(sessionId2) {
293
- return `pcw:${sessionId2.slice(0, 8)}:root`;
297
+ function isCheckpointBackedReset(ctx, marker = currentReset(ctx)) {
298
+ if (!marker?.parentId) return false;
299
+ const branch = ctx.sessionManager.getBranch();
300
+ const markerIndex = branch.findIndex((entry) => entry.id === marker.id);
301
+ const checkpoint = branch[markerIndex - 1];
302
+ return markerIndex > 0 && checkpoint?.id === marker.parentId && checkpoint.type === "compaction" && checkpoint.summary === "" && checkpoint.firstKeptEntryId === checkpoint.id;
303
+ }
304
+ function rootWindowId(sessionId) {
305
+ return `pcw:${sessionId.slice(0, 8)}:root`;
294
306
  }
295
307
  function hasWindowMessage(ctx, customType) {
296
308
  const branch = ctx.sessionManager.getBranch();
@@ -343,7 +355,8 @@ function windowUsage(ctx) {
343
355
  if (!contextWindow) return void 0;
344
356
  const windowId = reset.data.windowId;
345
357
  try {
346
- const messages = projectWindow(ctx.sessionManager.buildSessionProjection().messages, windowId);
358
+ const canonicalMessages = ctx.sessionManager.buildSessionProjection().messages;
359
+ const messages = isCheckpointBackedReset(ctx, reset) ? canonicalMessages : projectWindow(canonicalMessages, windowId);
347
360
  const { tokens } = estimateContextTokens(convertToLlm(messages));
348
361
  return { tokens, contextWindow, percent: tokens / contextWindow * 100 };
349
362
  } catch {
@@ -405,8 +418,8 @@ function toolCallItems(windowId, entry, message) {
405
418
  return items;
406
419
  }
407
420
  function historyFromSession(ctx) {
408
- const sessionId2 = ctx.sessionManager.getSessionId();
409
- let window = { windowId: rootWindowId(sessionId2), items: [] };
421
+ const sessionId = ctx.sessionManager.getSessionId();
422
+ let window = { windowId: rootWindowId(sessionId), items: [] };
410
423
  const windows = [window];
411
424
  for (const entry of ctx.sessionManager.getBranch()) {
412
425
  if (isWindowMarker(entry)) {
@@ -570,16 +583,17 @@ function registerHistoryTools(pi) {
570
583
  }));
571
584
  }
572
585
 
573
- // src/notes/tools.ts
586
+ // src/pi/notes/tools.ts
574
587
  import { Type as Type3 } from "@earendil-works/pi-ai";
575
- import { defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
588
+ import { defineTool as defineTool2, generateDiffString } from "@earendil-works/pi-coding-agent";
576
589
 
577
590
  // src/notes/frontmatter.ts
578
591
  var SCOPES = ["session", "project", "human", "agent", "model"];
579
592
  var ORIGINS = ["user", "self", "external"];
580
593
  var STATUSES = ["active", "superseded", "pending", "archived"];
581
- var TIMESTAMP_KEYS = ["created_at", "updated_at", "last_accessed"];
582
- var KNOWN_KEYS = ["origin", "status", "stale", "created_at", "updated_at", "last_accessed", "access_count", "source_window", "supersedes", "recurrence_count", "recurrence_windows"];
594
+ var TIMESTAMP_KEYS = ["createdAt", "updatedAt", "lastAccessed"];
595
+ var LEGACY_KNOWN_KEYS = ["created_at", "updated_at", "last_accessed", "access_count", "source_window", "recurrence_count", "recurrence_windows"];
596
+ var KNOWN_KEYS = ["origin", "status", "stale", "createdAt", "updatedAt", "lastAccessed", "accessCount", "sourceWindow", "supersedes", "recurrenceCount", "recurrenceWindows"];
583
597
  var pad2 = (value) => String(value).padStart(2, "0");
584
598
  function localIso(epochMs) {
585
599
  const date = new Date(epochMs);
@@ -628,7 +642,7 @@ function parseFrontmatter(raw) {
628
642
  }
629
643
  }
630
644
  if (close === -1) return { fields: {}, body: raw };
631
- const fields = {};
645
+ const fields = /* @__PURE__ */ Object.create(null);
632
646
  for (let index = 1; index < close; index++) {
633
647
  const line = lines[index];
634
648
  const match = /^([A-Za-z_][A-Za-z0-9_-]*):(.*)$/.exec(line);
@@ -652,13 +666,15 @@ function parseFrontmatter(raw) {
652
666
  }
653
667
  function parseNote(raw, now = Date.now()) {
654
668
  const { fields, body } = parseFrontmatter(raw);
669
+ const legacyKeys = LEGACY_KNOWN_KEYS.filter((key) => Object.hasOwn(fields, key));
670
+ if (legacyKeys.length > 0) throw new Error(`legacy note metadata ${legacyKeys.join(", ")} requires manual migration to camelCase before this note can be used`);
655
671
  const meta = { ...fields };
656
672
  meta.scope = isScope(meta.scope) ? meta.scope : "session";
657
673
  meta.origin = isOrigin(meta.origin) ? meta.origin : "self";
658
674
  meta.status = isStatus(meta.status) ? meta.status : "active";
659
675
  meta.stale = meta.stale === true;
660
676
  for (const key of TIMESTAMP_KEYS) meta[key] = toEpoch(meta[key], now);
661
- meta.access_count = typeof meta.access_count === "number" && Number.isFinite(meta.access_count) ? meta.access_count : 0;
677
+ meta.accessCount = typeof meta.accessCount === "number" && Number.isFinite(meta.accessCount) ? meta.accessCount : 0;
662
678
  return { meta, body };
663
679
  }
664
680
  function yamlScalar(value) {
@@ -691,67 +707,73 @@ function stripLeadingFrontmatter(content) {
691
707
  return parseFrontmatter(content).body;
692
708
  }
693
709
 
710
+ // src/notes/store.ts
711
+ import { randomUUID } from "node:crypto";
712
+ import { mkdir, readdir as readdir2, readFile, rename, rm, writeFile } from "node:fs/promises";
713
+ import { dirname as dirname2, join as join2, resolve as resolve3 } from "node:path";
714
+
694
715
  // src/notes/paths.ts
695
716
  import { createHash } from "node:crypto";
696
- import { existsSync, readdirSync, renameSync } from "node:fs";
697
- import { homedir } from "node:os";
698
- import { basename, dirname, join, resolve } from "node:path";
699
- function notesRoot() {
700
- const override = process.env.PI_NOTES_HOME;
701
- return override && override.length > 0 ? resolve(override) : join(homedir(), ".agents", "notes");
717
+ import { existsSync, readFileSync, statSync } from "node:fs";
718
+ import { readdir } from "node:fs/promises";
719
+ import { basename, dirname, join, resolve, sep } from "node:path";
720
+ var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
721
+ function slugify(value) {
722
+ const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
723
+ return slug.length > 0 ? slug : "root";
702
724
  }
703
- function sessionHomesRoot(home = notesRoot()) {
704
- return join(home, "pi", "session");
725
+ function projectKey(cwd) {
726
+ const absolute = resolve(cwd);
727
+ const root = gitRoot(absolute) ?? absolute;
728
+ const digest = createHash("sha1").update(root).digest("hex").slice(0, 8);
729
+ return `${basename(root)}-${digest}`;
730
+ }
731
+ function repositoryRoot(dir) {
732
+ let stats;
733
+ try {
734
+ stats = statSync(join(dir, ".git"));
735
+ } catch {
736
+ return dir;
737
+ }
738
+ if (stats.isDirectory()) return dir;
739
+ let pointer;
740
+ try {
741
+ pointer = readFileSync(join(dir, ".git"), "utf8");
742
+ } catch {
743
+ return dir;
744
+ }
745
+ const match = /^gitdir:\s*(.+)$/m.exec(pointer);
746
+ if (!match) return dir;
747
+ const parts = resolve(dir, match[1].trim()).split(sep);
748
+ const worktrees = parts.lastIndexOf("worktrees");
749
+ if (worktrees <= 0 || worktrees !== parts.length - 2) return dir;
750
+ const common = parts.slice(0, worktrees).join(sep);
751
+ return basename(common) === ".git" ? dirname(common) : dir;
705
752
  }
706
753
  function gitRoot(cwd) {
707
754
  let dir = resolve(cwd);
708
755
  for (; ; ) {
709
- if (existsSync(join(dir, ".git"))) return dir;
756
+ if (existsSync(join(dir, ".git"))) return repositoryRoot(dir);
710
757
  const parent = dirname(dir);
711
758
  if (parent === dir) return void 0;
712
759
  dir = parent;
713
760
  }
714
761
  }
715
- function projectKey(cwd) {
716
- const absolute = resolve(cwd);
717
- const root = gitRoot(absolute) ?? absolute;
718
- const digest = createHash("sha1").update(root).digest("hex").slice(0, 8);
719
- return `${basename(root)}-${digest}`;
720
- }
721
- function sessionId(ctx) {
722
- return ctx.sessionManager.getSessionId();
723
- }
724
- var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
725
- function slugify(value) {
726
- const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
727
- return slug.length > 0 ? slug : "root";
728
- }
729
- function agentSlug(_ctx) {
730
- return slugify(process.env.PI_NOTES_AGENT ?? "root");
731
- }
732
- function modelSlug(ctx) {
733
- const id = ctx.model?.id;
734
- if (!id) return "default";
735
- return slugify(id.split("/").pop() ?? id);
736
- }
737
- function scopeDir(scope, ctx, who) {
738
- if (scope === "human") return join(notesRoot(), "human");
739
- if (scope === "project") return join(notesRoot(), "project", projectKey(ctx.cwd));
740
- if (scope === "agent") return join(notesRoot(), "agents", who ?? agentSlug(ctx));
741
- if (scope === "model") return join(notesRoot(), "models", who ?? modelSlug(ctx));
742
- return join(sessionHomesRoot(), sessionId(ctx));
762
+ function sessionHomesRoot(home) {
763
+ return join(home, "pi", "session");
743
764
  }
744
- function migrateLegacyHomes(home = notesRoot()) {
745
- const legacy = join(home, "personal");
746
- const modern = join(home, "human");
747
- if (!existsSync(legacy)) return void 0;
748
- if (existsSync(modern)) return "both personal/ and human/ exist under the notes home; migrate by hand, no automatic merge";
749
- renameSync(legacy, modern);
750
- return void 0;
765
+ function scopeDir(scope, context, who) {
766
+ if (!["session", "project", "human", "agent", "model"].includes(scope)) throw new TypeError("invalid notes scope");
767
+ if (who !== void 0 && (scope !== "agent" && scope !== "model" || !SLUG_PATTERN.test(who))) throw new TypeError("who must be a canonical agent/model slug");
768
+ if (scope === "human") return join(context.home, "human");
769
+ if (scope === "project") return join(context.home, "project", context.projectKey);
770
+ if (scope === "agent") return join(context.home, "agents", who ?? context.agent);
771
+ if (scope === "model") return join(context.home, "models", who ?? context.model);
772
+ return join(sessionHomesRoot(context.home), context.sessionId);
751
773
  }
752
- function namespaceSlugs(namespace, home = notesRoot()) {
774
+ async function namespaceSlugs(namespace, home) {
753
775
  try {
754
- return readdirSync(join(home, namespace), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
776
+ return (await readdir(join(home, namespace), { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
755
777
  } catch {
756
778
  return [];
757
779
  }
@@ -759,8 +781,10 @@ function namespaceSlugs(namespace, home = notesRoot()) {
759
781
  function noteFileName(vpath) {
760
782
  return vpath.endsWith(".md") ? vpath : `${vpath}.md`;
761
783
  }
762
- function physicalPath(scope, vpath, ctx, who) {
763
- return join(scopeDir(scope, ctx, who), ...noteFileName(vpath).split("/"));
784
+ function physicalPath(scope, vpath, context, who) {
785
+ if (typeof vpath !== "string" || vpath.length === 0 || vpath.includes("\0") || vpath.includes("\\") || vpath.startsWith("/")) throw new TypeError("path must be a safe virtual relative path");
786
+ if (vpath.split("/").some((part) => part.length === 0 || part === "." || part === "..")) throw new TypeError("path contains an unsupported component");
787
+ return join(scopeDir(scope, context, who), ...noteFileName(vpath).split("/"));
764
788
  }
765
789
 
766
790
  // src/notes/address.ts
@@ -827,52 +851,91 @@ function assertAddress(value) {
827
851
  assertVirtualPath(path);
828
852
  return { scope, path, who };
829
853
  }
830
- function addressFor(ctx, scope, path, who) {
854
+ function addressFor(context, scope, path, who) {
831
855
  if (scope === "session") return path;
832
856
  if (scope === "project") return `@project/${path}`;
833
857
  if (scope === "human") return `@human/${path}`;
834
- if (scope === "agent") return `@agents/${who ?? agentSlug(ctx)}/${path}`;
835
- return `@models/${who ?? modelSlug(ctx)}/${path}`;
858
+ if (scope === "agent") return `@agents/${who ?? context.agent}/${path}`;
859
+ return `@models/${who ?? context.model}/${path}`;
860
+ }
861
+
862
+ // src/notes/context.ts
863
+ import { resolve as resolve2 } from "node:path";
864
+ function requireString(field, value) {
865
+ if (typeof value !== "string" || value.length === 0) throw new TypeError(`notes context ${field} must be a non-empty string`);
866
+ return value;
867
+ }
868
+ function assertDirectoryComponent(field, value) {
869
+ if (value === "." || value === ".." || /[\\/\0:]/.test(value)) {
870
+ throw new TypeError(`notes context ${field} must be a safe single directory component`);
871
+ }
872
+ }
873
+ function snapshotNotesContext(value) {
874
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("notes context must be a plain object");
875
+ const prototype = Object.getPrototypeOf(value);
876
+ if (prototype !== Object.prototype && prototype !== null) throw new TypeError("notes context must be a plain object");
877
+ const homeValue = requireString("home", value.home);
878
+ const sessionId = requireString("sessionId", value.sessionId);
879
+ const projectKey2 = requireString("projectKey", value.projectKey);
880
+ const agent = requireString("agent", value.agent);
881
+ const model = requireString("model", value.model);
882
+ assertDirectoryComponent("sessionId", sessionId);
883
+ assertDirectoryComponent("projectKey", projectKey2);
884
+ if (!SLUG_PATTERN.test(agent)) throw new TypeError("notes context agent must be a canonical lowercase slug");
885
+ if (!SLUG_PATTERN.test(model)) throw new TypeError("notes context model must be a canonical lowercase slug");
886
+ const home = resolve2(homeValue);
887
+ return Object.freeze({ home, sessionId, projectKey: projectKey2, agent, model });
836
888
  }
837
889
 
838
890
  // src/notes/store.ts
839
- import { randomUUID } from "node:crypto";
840
- import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync as readdirSync2, renameSync as renameSync2, rmSync, writeFileSync } from "node:fs";
841
- import { dirname as dirname2 } from "node:path";
842
- import { generateDiffString } from "@earendil-works/pi-coding-agent";
843
891
  var NoteError = class extends Error {
844
892
  code;
845
- line_numbers;
846
- edit_index;
893
+ lineNumbers;
894
+ editIndex;
847
895
  constructor(code, message, extra = {}) {
848
896
  super(message);
849
897
  this.name = "NoteError";
850
898
  this.code = code;
851
- this.line_numbers = extra.line_numbers;
852
- this.edit_index = extra.edit_index;
899
+ this.lineNumbers = extra.lineNumbers;
900
+ this.editIndex = extra.editIndex;
853
901
  }
854
902
  };
855
903
  var SCOPE_ORDER = ["session", "project", "human", "agent", "model"];
856
- function assertScope(value) {
857
- if (!isScope(value)) throw new NoteError("invalid_scope", `scope must be one of session, project, human, agent, model (got ${JSON.stringify(value)})`);
858
- return value;
904
+ var pathQueues = /* @__PURE__ */ new Map();
905
+ function withPathQueue(path, operation) {
906
+ const key = resolve3(path);
907
+ const previous = pathQueues.get(key) ?? Promise.resolve();
908
+ const result = previous.then(operation);
909
+ const tail = result.then(() => void 0, () => void 0);
910
+ pathQueues.set(key, tail);
911
+ void tail.then(() => {
912
+ if (pathQueues.get(key) === tail) pathQueues.delete(key);
913
+ });
914
+ return result;
859
915
  }
860
- function assertOrigin(value) {
861
- if (!isOrigin(value)) throw new NoteError("invalid_origin", `origin must be one of user, self, external (got ${JSON.stringify(value)})`);
862
- return value;
916
+ function errno(error) {
917
+ return typeof error === "object" && error !== null ? error.code : void 0;
918
+ }
919
+ async function readFileIfExists(path) {
920
+ try {
921
+ return await readFile(path, "utf8");
922
+ } catch (error) {
923
+ if (errno(error) === "ENOENT") return void 0;
924
+ throw error;
925
+ }
863
926
  }
864
- function walkMarkdown(dir, base = dir) {
927
+ async function walkMarkdown(dir, base = dir) {
865
928
  let entries;
866
929
  try {
867
- entries = readdirSync2(dir, { withFileTypes: true });
930
+ entries = await readdir2(dir, { withFileTypes: true });
868
931
  } catch (error) {
869
- if (typeof error === "object" && error !== null && error.code === "ENOENT") return [];
932
+ if (errno(error) === "ENOENT") return [];
870
933
  throw error;
871
934
  }
872
935
  const paths = [];
873
936
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
874
- const child = `${dir}/${entry.name}`;
875
- if (entry.isDirectory()) paths.push(...walkMarkdown(child, base));
937
+ const child = join2(dir, entry.name);
938
+ if (entry.isDirectory()) paths.push(...await walkMarkdown(child, base));
876
939
  else if (entry.isFile() && entry.name.endsWith(".md")) paths.push(child.slice(base.length + 1).split("\\").join("/"));
877
940
  }
878
941
  return paths;
@@ -881,36 +944,7 @@ function matcherFor(pattern) {
881
944
  const normalized = assertGlobPattern(pattern);
882
945
  return normalized === void 0 ? void 0 : globToRegExp(normalized);
883
946
  }
884
- function atomicWrite(path, content) {
885
- mkdirSync(dirname2(path), { recursive: true });
886
- const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
887
- try {
888
- writeFileSync(tmp, content);
889
- renameSync2(tmp, path);
890
- } catch (error) {
891
- rmSync(tmp, { force: true });
892
- throw error;
893
- }
894
- }
895
- function assertWritablePath(vpath) {
896
- const bytes = Buffer.byteLength(vpath, "utf8");
897
- if (bytes > MAX_NOTE_PATH_BYTES) throw new NoteError("too_large", `note path exceeds ${MAX_NOTE_PATH_BYTES} UTF-8 bytes (got ${bytes})`);
898
- }
899
- function assertSerializedSize(content) {
900
- const bytes = Buffer.byteLength(content, "utf8");
901
- if (bytes > MAX_NOTE_BYTES) throw new NoteError("too_large", `note exceeds ${MAX_NOTE_BYTES} UTF-8 bytes (serialized ${bytes})`);
902
- }
903
- function frontmatterOf(meta) {
904
- return serializeNote(meta, "").slice(0, -2);
905
- }
906
- function assertWritableHome(scope, who, ctx) {
907
- if (who === void 0) return;
908
- const current = scope === "agent" ? agentSlug(ctx) : modelSlug(ctx);
909
- if (who === current) return;
910
- const home = scope === "agent" ? `@agents/${who}/` : `@models/${who}/`;
911
- throw new NoteError("invalid_scope", `${home} is not your home: writable homes are this session, @project/, @human/, @self/, and the current @model/ home`);
912
- }
913
- function homesForPattern(pattern) {
947
+ async function homesForPattern(pattern, context) {
914
948
  if (!pattern || !pattern.startsWith("@")) return void 0;
915
949
  const head = /^@([^/]+)\//.exec(pattern)?.[1];
916
950
  if (head === "project") return [{ scope: "project" }];
@@ -920,20 +954,43 @@ function homesForPattern(pattern) {
920
954
  if (head === "agents" || head === "models") {
921
955
  const scope = head === "agents" ? "agent" : "model";
922
956
  const name = pattern.slice(head.length + 2).split("/")[0] ?? "";
923
- if (name.length > 0 && !/[*?]/.test(name)) return [{ scope, who: name }];
924
- return namespaceSlugs(head).map((who) => ({ scope, who }));
957
+ if (name.length > 0 && !/[*?]/.test(name)) return [{ scope, who: assertWho(name) }];
958
+ return (await namespaceSlugs(head, context.home)).map((who) => ({ scope, who }));
925
959
  }
926
960
  return [];
927
961
  }
928
- function normalizePattern(pattern, ctx) {
962
+ function normalizePattern(pattern, context) {
929
963
  if (!pattern) return pattern;
930
- if (pattern.startsWith("@self/")) return `@agents/${agentSlug(ctx)}/${pattern.slice("@self/".length)}`;
931
- if (pattern.startsWith("@model/")) return `@models/${modelSlug(ctx)}/${pattern.slice("@model/".length)}`;
964
+ if (pattern.startsWith("@self/")) return `@agents/${context.agent}/${pattern.slice("@self/".length)}`;
965
+ if (pattern.startsWith("@model/")) return `@models/${context.model}/${pattern.slice("@model/".length)}`;
932
966
  return pattern;
933
967
  }
934
- function homesFor(ctx, opts) {
935
- if (opts.scope !== void 0) return [{ scope: opts.scope, who: opts.who }];
936
- return homesForPattern(opts.pattern) ?? SCOPE_ORDER.map((scope) => ({ scope }));
968
+ function assertScope(value) {
969
+ if (!isScope(value)) throw new NoteError("invalid_scope", `scope must be one of session, project, human, agent, model (got ${JSON.stringify(value)})`);
970
+ return value;
971
+ }
972
+ function assertOrigin(value) {
973
+ if (!isOrigin(value)) throw new NoteError("invalid_origin", `origin must be one of user, self, external (got ${JSON.stringify(value)})`);
974
+ return value;
975
+ }
976
+ function assertWho(value) {
977
+ if (typeof value !== "string" || !SLUG_PATTERN.test(value)) {
978
+ throw new NoteError("invalid_scope", "who must be a canonical lowercase slug");
979
+ }
980
+ return value;
981
+ }
982
+ async function homesFor(context, opts) {
983
+ if (opts.scope !== void 0) {
984
+ const scope = assertScope(opts.scope);
985
+ if (opts.who !== void 0) {
986
+ const who = assertWho(opts.who);
987
+ if (scope !== "agent" && scope !== "model") throw new NoteError("invalid_scope", "who is only valid with agent or model scope");
988
+ return [{ scope, who }];
989
+ }
990
+ return [{ scope }];
991
+ }
992
+ if (opts.who !== void 0) throw new NoteError("invalid_scope", "who requires agent or model scope");
993
+ return await homesForPattern(normalizePattern(opts.pattern, context), context) ?? SCOPE_ORDER.map((scope) => ({ scope }));
937
994
  }
938
995
  function matchLineNumbers(body, needle) {
939
996
  const lines = [];
@@ -946,141 +1003,246 @@ function matchLineNumbers(body, needle) {
946
1003
  }
947
1004
  return lines;
948
1005
  }
949
- function writeNote(ctx, vpath, body, opts) {
950
- assertVirtualPath(vpath);
951
- assertWritablePath(vpath);
952
- const scope = assertScope(opts.scope);
953
- assertWritableHome(scope, opts.who, ctx);
954
- const origin = assertOrigin(opts.origin);
955
- const path = physicalPath(scope, vpath, ctx, opts.who);
956
- const now = Date.now();
957
- const cleanBody = stripLeadingFrontmatter(body);
958
- const existing = existsSync2(path) ? parseNote(readFileSync(path, "utf8"), now).meta : void 0;
959
- const meta = existing ?? {
960
- scope,
961
- origin,
962
- status: "active",
963
- stale: false,
964
- created_at: now,
965
- updated_at: now,
966
- last_accessed: now,
967
- access_count: 0
968
- };
969
- meta.scope = scope;
970
- meta.origin = origin;
971
- meta.status = "active";
972
- meta.stale = opts.stale ?? false;
973
- meta.updated_at = now;
974
- const serialized = serializeNote(meta, cleanBody);
975
- assertSerializedSize(serialized);
976
- atomicWrite(path, serialized);
977
- return { meta };
978
- }
979
- function editNote(ctx, vpath, scope, edits, opts = {}, who) {
980
- assertVirtualPath(vpath);
981
- assertWritablePath(vpath);
982
- assertWritableHome(scope, who, ctx);
983
- const operations = edits ?? [];
984
- if (operations.length === 0 && opts.origin === void 0 && opts.stale === void 0) {
985
- throw new NoteError("nothing_to_do", "nothing to do: provide edits or at least one of origin, stale");
1006
+ function earliestMatchOffsetChars2(text, queries) {
1007
+ let earliest = -1;
1008
+ for (const query of queries) {
1009
+ const index = text.indexOf(query);
1010
+ if (index < 0) continue;
1011
+ if (earliest < 0 || index < earliest) earliest = index;
986
1012
  }
987
- const path = physicalPath(scope, vpath, ctx, who);
988
- if (!existsSync2(path)) throw new NoteError("not_found", "note not found");
989
- const raw = readFileSync(path, "utf8");
990
- const { meta, body } = parseNote(raw);
991
- meta.scope = scope;
992
- const beforeMeta = { ...meta };
993
- let next = body;
994
- operations.forEach((edit, index) => {
995
- const oldText = edit?.oldText;
996
- const newText = edit?.newText;
997
- if (typeof oldText !== "string" || oldText.length === 0) throw new NoteError("no_match", `edit ${index}: oldText must be a non-empty string`, { edit_index: index });
998
- if (typeof newText !== "string") throw new NoteError("no_match", `edit ${index}: newText must be a string`, { edit_index: index });
999
- const lines = matchLineNumbers(next, oldText);
1000
- if (lines.length === 0) throw new NoteError("no_match", `edit ${index}: oldText does not occur in the note body`, { edit_index: index });
1001
- if (lines.length > 1 && !opts.replaceAll) {
1002
- throw new NoteError("ambiguous_edit", `edit ${index}: oldText occurs ${lines.length} times (lines ${lines.join(", ")}); pass replace_all to replace every occurrence`, { line_numbers: lines, edit_index: index });
1003
- }
1004
- if (opts.replaceAll) {
1005
- next = next.split(oldText).join(newText);
1006
- } else {
1007
- const matchIndex = next.indexOf(oldText);
1008
- next = next.substring(0, matchIndex) + newText + next.substring(matchIndex + oldText.length);
1013
+ return earliest <= 0 ? 0 : Array.from(text.slice(0, earliest)).length;
1014
+ }
1015
+ async function atomicWrite(path, content) {
1016
+ await mkdir(dirname2(path), { recursive: true });
1017
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
1018
+ try {
1019
+ await writeFile(tmp, content);
1020
+ await rename(tmp, path);
1021
+ } catch (error) {
1022
+ try {
1023
+ await rm(tmp, { force: true });
1024
+ } catch {
1009
1025
  }
1010
- });
1011
- if (opts.origin !== void 0) meta.origin = assertOrigin(opts.origin);
1012
- if (opts.stale !== void 0) meta.stale = opts.stale;
1013
- meta.updated_at = Date.now();
1014
- const serialized = serializeNote(meta, next);
1015
- assertSerializedSize(serialized);
1016
- const bodyChanged = body !== next;
1017
- const metadataChanged = beforeMeta.origin !== meta.origin || beforeMeta.stale !== meta.stale;
1018
- const diff = bodyChanged && metadataChanged ? generateDiffString(raw, serialized).diff : bodyChanged ? generateDiffString(body, next).diff : metadataChanged ? generateDiffString(frontmatterOf(beforeMeta), frontmatterOf(meta)).diff : "";
1019
- atomicWrite(path, serialized);
1020
- return { meta, applied: operations.length, resolved_scope: scope, diff };
1026
+ throw error;
1027
+ }
1028
+ }
1029
+ function assertWritablePath(vpath) {
1030
+ const bytes = Buffer.byteLength(vpath, "utf8");
1031
+ if (bytes > MAX_NOTE_PATH_BYTES) throw new NoteError("too_large", `note path exceeds ${MAX_NOTE_PATH_BYTES} UTF-8 bytes (got ${bytes})`);
1032
+ }
1033
+ function assertSerializedSize(content) {
1034
+ const bytes = Buffer.byteLength(content, "utf8");
1035
+ if (bytes > MAX_NOTE_BYTES) throw new NoteError("too_large", `note exceeds ${MAX_NOTE_BYTES} UTF-8 bytes (serialized ${bytes})`);
1036
+ }
1037
+ function frontmatterOf(meta) {
1038
+ return serializeNote(meta, "").slice(0, -2);
1039
+ }
1040
+ function assertWritableHome(scope, who, context) {
1041
+ if (who === void 0) return;
1042
+ const current = scope === "agent" ? context.agent : context.model;
1043
+ if (who === current) return;
1044
+ const home = scope === "agent" ? `@agents/${who}/` : `@models/${who}/`;
1045
+ throw new NoteError("invalid_scope", `${home} is not your home: writable homes are this session, @project/, @human/, @self/, and the current @model/ home`);
1021
1046
  }
1022
1047
  function accessedMeta(meta, scope, now) {
1023
1048
  const next = { ...meta, scope };
1024
- next.last_accessed = now;
1025
- next.access_count = (typeof next.access_count === "number" ? next.access_count : 0) + 1;
1049
+ next.lastAccessed = now;
1050
+ next.accessCount = (typeof next.accessCount === "number" ? next.accessCount : 0) + 1;
1026
1051
  return next;
1027
1052
  }
1028
- function readNote(ctx, vpath, scope, who) {
1029
- assertVirtualPath(vpath);
1030
- const path = physicalPath(scope, vpath, ctx, who);
1031
- if (!existsSync2(path)) return void 0;
1032
- const now = Date.now();
1033
- const parsed = parseNote(readFileSync(path, "utf8"), now);
1034
- const meta = accessedMeta(parsed.meta, scope, now);
1035
- const text = serializeNote(meta, parsed.body);
1036
- atomicWrite(path, text);
1037
- return { meta, body: parsed.body, text, resolvedScope: scope };
1038
- }
1039
- function listNotes(ctx, opts = {}) {
1040
- const matcher = matcherFor(normalizePattern(opts.pattern, ctx));
1041
- const rows = [];
1042
- for (const home of homesFor(ctx, opts)) {
1043
- const scope = home.scope;
1044
- const root = scopeDir(scope, ctx, home.who);
1045
- for (const path of walkMarkdown(root)) {
1046
- const address = addressFor(ctx, scope, path, home.who);
1047
- if (matcher && !matcher.test(address)) continue;
1048
- const { meta, body } = parseNote(readFileSync(`${root}/${path}`, "utf8"));
1053
+ function createNotesStore(input) {
1054
+ const context = snapshotNotesContext(input);
1055
+ async function write(address, content, options = {}) {
1056
+ const stableAddress = address;
1057
+ const stableContent = content;
1058
+ const stableOptions = { ...options };
1059
+ const destination = assertAddress(stableAddress);
1060
+ assertWritablePath(destination.path);
1061
+ const scope = assertScope(destination.scope);
1062
+ assertWritableHome(scope, destination.who, context);
1063
+ const origin = assertOrigin(stableOptions.origin ?? "self");
1064
+ const path = physicalPath(scope, destination.path, context, destination.who);
1065
+ return withPathQueue(path, async () => {
1066
+ const now = Date.now();
1067
+ const cleanBody = stripLeadingFrontmatter(stableContent);
1068
+ const existingRaw = await readFileIfExists(path);
1069
+ const existing = existingRaw === void 0 ? void 0 : parseNote(existingRaw, now).meta;
1070
+ const meta = existing ?? {
1071
+ scope,
1072
+ origin,
1073
+ status: "active",
1074
+ stale: false,
1075
+ createdAt: now,
1076
+ updatedAt: now,
1077
+ lastAccessed: now,
1078
+ accessCount: 0,
1079
+ ...scope === "session" ? { project: context.projectKey } : {}
1080
+ };
1049
1081
  meta.scope = scope;
1050
- rows.push({ address, scope, path, meta, body, sizeBytes: Buffer.byteLength(body, "utf8") });
1051
- }
1082
+ meta.origin = origin;
1083
+ meta.status = "active";
1084
+ meta.stale = stableOptions.stale ?? false;
1085
+ meta.updatedAt = now;
1086
+ const serialized = serializeNote(meta, cleanBody);
1087
+ assertSerializedSize(serialized);
1088
+ await atomicWrite(path, serialized);
1089
+ return { meta };
1090
+ });
1091
+ }
1092
+ async function read(address) {
1093
+ const stableAddress = address;
1094
+ const destination = assertAddress(stableAddress);
1095
+ const scope = assertScope(destination.scope);
1096
+ const path = physicalPath(scope, destination.path, context, destination.who);
1097
+ return withPathQueue(path, async () => {
1098
+ const raw = await readFileIfExists(path);
1099
+ if (raw === void 0) return void 0;
1100
+ const now = Date.now();
1101
+ const parsed = parseNote(raw, now);
1102
+ const meta = accessedMeta(parsed.meta, scope, now);
1103
+ const text = serializeNote(meta, parsed.body);
1104
+ await atomicWrite(path, text);
1105
+ return { meta, body: parsed.body, text, resolvedScope: scope };
1106
+ });
1052
1107
  }
1053
- rows.sort((a, b) => b.meta.updated_at - a.meta.updated_at || a.address.localeCompare(b.address));
1054
- return rows;
1055
- }
1056
- function searchNotes(ctx, queries, opts = {}) {
1057
- const matcher = matcherFor(normalizePattern(opts.pattern, ctx));
1058
- const rows = [];
1059
- for (const home of homesFor(ctx, opts)) {
1060
- const scope = home.scope;
1061
- const root = scopeDir(scope, ctx, home.who);
1062
- for (const path of walkMarkdown(root)) {
1063
- const address = addressFor(ctx, scope, path, home.who);
1064
- if (matcher && !matcher.test(address)) continue;
1065
- const { meta, body } = parseNote(readFileSync(`${root}/${path}`, "utf8"));
1108
+ async function edit(address, edits, options = {}) {
1109
+ const stableAddress = address;
1110
+ const operations = edits === void 0 ? [] : edits.map((operation) => ({ ...operation }));
1111
+ const stableOptions = { ...options };
1112
+ const destination = assertAddress(stableAddress);
1113
+ assertWritablePath(destination.path);
1114
+ const scope = assertScope(destination.scope);
1115
+ assertWritableHome(scope, destination.who, context);
1116
+ if (operations.length === 0 && stableOptions.origin === void 0 && stableOptions.stale === void 0) {
1117
+ throw new NoteError("nothing_to_do", "nothing to do: provide edits or at least one of origin, stale");
1118
+ }
1119
+ const path = physicalPath(scope, destination.path, context, destination.who);
1120
+ return withPathQueue(path, async () => {
1121
+ const raw = await readFileIfExists(path);
1122
+ if (raw === void 0) throw new NoteError("not_found", "note not found");
1123
+ const { meta, body } = parseNote(raw);
1066
1124
  meta.scope = scope;
1067
- const serializedBodyOffset = Array.from(serializeNote(accessedMeta(meta, scope, Date.now()), "")).length;
1068
- let baseChars = 0;
1069
- const matches = [];
1070
- for (const [index, line] of body.split("\n").entries()) {
1071
- if (queries.some((query) => line.includes(query))) {
1072
- matches.push({ line: index + 1, text: line, offsetChars: serializedBodyOffset + baseChars + earliestMatchOffsetChars(line, queries) });
1125
+ const beforeMeta = { ...meta };
1126
+ let next = body;
1127
+ operations.forEach((operation, index) => {
1128
+ const oldText = operation?.oldText;
1129
+ const newText = operation?.newText;
1130
+ if (typeof oldText !== "string" || oldText.length === 0) throw new NoteError("no_match", `edit ${index}: oldText must be a non-empty string`, { editIndex: index });
1131
+ if (typeof newText !== "string") throw new NoteError("no_match", `edit ${index}: newText must be a string`, { editIndex: index });
1132
+ const lines = matchLineNumbers(next, oldText);
1133
+ if (lines.length === 0) throw new NoteError("no_match", `edit ${index}: oldText does not occur in the note body`, { editIndex: index });
1134
+ if (lines.length > 1 && !stableOptions.replaceAll) {
1135
+ throw new NoteError("ambiguous_edit", `edit ${index}: oldText occurs ${lines.length} times (lines ${lines.join(", ")}); pass replace_all to replace every occurrence`, { lineNumbers: lines, editIndex: index });
1136
+ }
1137
+ if (stableOptions.replaceAll) next = next.split(oldText).join(newText);
1138
+ else {
1139
+ const matchIndex = next.indexOf(oldText);
1140
+ next = next.substring(0, matchIndex) + newText + next.substring(matchIndex + oldText.length);
1141
+ }
1142
+ });
1143
+ if (stableOptions.origin !== void 0) meta.origin = assertOrigin(stableOptions.origin);
1144
+ if (stableOptions.stale !== void 0) meta.stale = stableOptions.stale;
1145
+ meta.updatedAt = Date.now();
1146
+ const serialized = serializeNote(meta, next);
1147
+ assertSerializedSize(serialized);
1148
+ const bodyChanged = body !== next;
1149
+ const metadataChanged = beforeMeta.origin !== meta.origin || beforeMeta.stale !== meta.stale;
1150
+ let change;
1151
+ if (bodyChanged && metadataChanged) change = { kind: "file", before: raw, after: serialized };
1152
+ else if (bodyChanged) change = { kind: "body", before: body, after: next };
1153
+ else if (metadataChanged) change = { kind: "metadata", before: frontmatterOf(beforeMeta), after: frontmatterOf(meta) };
1154
+ else change = { kind: "none", before: "", after: "" };
1155
+ await atomicWrite(path, serialized);
1156
+ return { meta, applied: operations.length, resolvedScope: scope, change };
1157
+ });
1158
+ }
1159
+ async function list(options = {}) {
1160
+ const stableOptions = { ...options };
1161
+ const matcher = matcherFor(normalizePattern(stableOptions.pattern, context));
1162
+ const rows = [];
1163
+ for (const home of await homesFor(context, stableOptions)) {
1164
+ const scope = home.scope;
1165
+ const root = scopeDir(scope, context, home.who);
1166
+ for (const path of await walkMarkdown(root)) {
1167
+ const address = addressFor(context, scope, path, home.who);
1168
+ if (matcher && !matcher.test(address)) continue;
1169
+ const fullPath = join2(root, path);
1170
+ const raw = await withPathQueue(fullPath, () => readFile(fullPath, "utf8"));
1171
+ const { meta, body } = parseNote(raw);
1172
+ meta.scope = scope;
1173
+ rows.push({ address, scope, path, meta, body, sizeBytes: Buffer.byteLength(body, "utf8") });
1174
+ }
1175
+ }
1176
+ rows.sort((a, b) => b.meta.updatedAt - a.meta.updatedAt || a.address.localeCompare(b.address));
1177
+ return rows;
1178
+ }
1179
+ async function search(queries, options = {}) {
1180
+ const stableQueries = [...queries];
1181
+ const stableOptions = { ...options };
1182
+ const matcher = matcherFor(normalizePattern(stableOptions.pattern, context));
1183
+ const rows = [];
1184
+ for (const home of await homesFor(context, stableOptions)) {
1185
+ const scope = home.scope;
1186
+ const root = scopeDir(scope, context, home.who);
1187
+ for (const path of await walkMarkdown(root)) {
1188
+ const address = addressFor(context, scope, path, home.who);
1189
+ if (matcher && !matcher.test(address)) continue;
1190
+ const fullPath = join2(root, path);
1191
+ const raw = await withPathQueue(fullPath, () => readFile(fullPath, "utf8"));
1192
+ const { meta, body } = parseNote(raw);
1193
+ meta.scope = scope;
1194
+ const serializedBodyOffset = Array.from(serializeNote(accessedMeta(meta, scope, Date.now()), "")).length;
1195
+ let baseChars = 0;
1196
+ const matches = [];
1197
+ for (const [index, line] of body.split("\n").entries()) {
1198
+ if (stableQueries.some((query) => line.includes(query))) {
1199
+ matches.push({ line: index + 1, text: line, offsetChars: serializedBodyOffset + baseChars + earliestMatchOffsetChars2(line, stableQueries) });
1200
+ }
1201
+ baseChars += Array.from(line).length + 1;
1073
1202
  }
1074
- baseChars += Array.from(line).length + 1;
1203
+ if (matches.length > 0) rows.push({ address, path, scope, meta, matches });
1075
1204
  }
1076
- if (matches.length > 0) rows.push({ address, path, scope, meta, matches });
1077
1205
  }
1206
+ rows.sort((a, b) => a.address.localeCompare(b.address));
1207
+ return rows;
1078
1208
  }
1079
- rows.sort((a, b) => a.address.localeCompare(b.address));
1080
- return rows;
1209
+ return { write, read, edit, list, search };
1081
1210
  }
1082
1211
 
1083
- // src/notes/tools.ts
1212
+ // src/pi/notes/adapter.ts
1213
+ import { existsSync as existsSync2, renameSync } from "node:fs";
1214
+ import { homedir } from "node:os";
1215
+ import { join as join3, resolve as resolve4 } from "node:path";
1216
+ function notesRoot() {
1217
+ const override = process.env.PI_NOTES_HOME;
1218
+ return override && override.length > 0 ? resolve4(override) : join3(homedir(), ".agents", "notes");
1219
+ }
1220
+ function agentSlug(_ctx) {
1221
+ return slugify(process.env.PI_NOTES_AGENT ?? "root");
1222
+ }
1223
+ function modelSlug(ctx) {
1224
+ const id = ctx.model?.id;
1225
+ return id ? slugify(id.split("/").pop() ?? id) : "default";
1226
+ }
1227
+ function notesContextFromPi(ctx, home = notesRoot()) {
1228
+ return {
1229
+ home,
1230
+ sessionId: ctx.sessionManager.getSessionId(),
1231
+ projectKey: projectKey(ctx.cwd),
1232
+ agent: agentSlug(ctx),
1233
+ model: modelSlug(ctx)
1234
+ };
1235
+ }
1236
+ function migrateLegacyHomes(home = notesRoot()) {
1237
+ const legacy = join3(home, "personal");
1238
+ const modern = join3(home, "human");
1239
+ if (!existsSync2(legacy)) return void 0;
1240
+ if (existsSync2(modern)) return "both personal/ and human/ exist under the notes home; migrate by hand, no automatic merge";
1241
+ renameSync(legacy, modern);
1242
+ return void 0;
1243
+ }
1244
+
1245
+ // src/pi/notes/tools.ts
1084
1246
  var ORIGIN = Type3.Optional(Type3.Union([Type3.Literal("user"), Type3.Literal("self"), Type3.Literal("external")], {
1085
1247
  description: "Where the note's content came from. user: written or dictated by the human. self: written by you, the agent (default). external: anything else \u2014 third-party text, tool output, fetched material."
1086
1248
  }));
@@ -1088,24 +1250,27 @@ var ADDRESS_DESCRIPTION = "Address forms are bare `<vpath>` for this session, `@
1088
1250
  function failure(error) {
1089
1251
  if (error instanceof NoteError) {
1090
1252
  const payload = { error: error.message };
1091
- if (error.line_numbers) payload.line_numbers = error.line_numbers;
1092
- if (error.edit_index !== void 0) payload.edit_index = error.edit_index;
1253
+ if (error.lineNumbers) payload.line_numbers = error.lineNumbers;
1254
+ if (error.editIndex !== void 0) payload.edit_index = error.editIndex;
1093
1255
  return output(payload);
1094
1256
  }
1095
1257
  throw error;
1096
1258
  }
1259
+ function renderDiff(change) {
1260
+ if (change.kind === "none") return "";
1261
+ return generateDiffString(change.before, change.after).diff;
1262
+ }
1097
1263
  function registerNotesTools(pi) {
1098
1264
  pi.registerTool(defineTool2({
1099
1265
  name: "notes_write",
1100
1266
  label: "Notes write",
1101
- description: `Create or replace a note as a real markdown file, and name it for what it holds: a fresh window sees only an index entry, never the note itself. ${ADDRESS_DESCRIPTION} Keep notes small and split by topic \u2014 by what the note is about, never by who said it (authorship is origin's job); a rewrite replaces the body whole while preserving created_at and every other frontmatter key. stale: true marks the note closed so it leaves the boot index but stays readable and searchable.`,
1267
+ description: `Create or replace a note as a real markdown file, and name it for what it holds: a fresh window sees only an index entry, never the note itself. ${ADDRESS_DESCRIPTION} Keep notes small and split by topic \u2014 by what the note is about, never by who said it (authorship is origin's job); a rewrite replaces the body whole while preserving createdAt and every other frontmatter key. stale: true marks the note closed so it leaves the boot index but stays readable and searchable.`,
1102
1268
  parameters: Type3.Object({ address: Type3.String(), content: Type3.String(), origin: ORIGIN, stale: Type3.Optional(Type3.Boolean()) }, { additionalProperties: false }),
1103
1269
  executionMode: "sequential",
1104
1270
  async execute(_id, params, _signal, _update, ctx) {
1105
1271
  const content = params.content;
1106
1272
  try {
1107
- const destination = assertAddress(params.address);
1108
- writeNote(ctx, destination.path, content, { scope: destination.scope, who: destination.who, origin: params.origin ?? "self", stale: params.stale });
1273
+ await createNotesStore(notesContextFromPi(ctx)).write(params.address, content, { origin: params.origin ?? "self", stale: params.stale });
1109
1274
  return output({ address: params.address, written: true });
1110
1275
  } catch (error) {
1111
1276
  return failure(error);
@@ -1120,8 +1285,8 @@ function registerNotesTools(pi) {
1120
1285
  executionMode: "sequential",
1121
1286
  async execute(_id, params, _signal, _update, ctx) {
1122
1287
  try {
1123
- const destination = assertAddress(params.address);
1124
- const { applied, diff } = editNote(ctx, destination.path, destination.scope, params.edits, { origin: params.origin, stale: params.stale, replaceAll: params.replace_all }, destination.who);
1288
+ const { applied, change } = await createNotesStore(notesContextFromPi(ctx)).edit(params.address, params.edits, { origin: params.origin, stale: params.stale, replaceAll: params.replace_all });
1289
+ const diff = renderDiff(change);
1125
1290
  return output({ address: params.address, applied, diff });
1126
1291
  } catch (error) {
1127
1292
  return failure(error);
@@ -1136,8 +1301,7 @@ function registerNotesTools(pi) {
1136
1301
  async execute(_id, params, _signal, _update, ctx) {
1137
1302
  let note;
1138
1303
  try {
1139
- const destination = assertAddress(params.address);
1140
- note = readNote(ctx, destination.path, destination.scope, destination.who);
1304
+ note = await createNotesStore(notesContextFromPi(ctx)).read(params.address);
1141
1305
  } catch (error) {
1142
1306
  return failure(error);
1143
1307
  }
@@ -1159,11 +1323,11 @@ function registerNotesTools(pi) {
1159
1323
  async execute(_id, params, _signal, _update, ctx) {
1160
1324
  let rows;
1161
1325
  try {
1162
- rows = listNotes(ctx, { pattern: params.pattern ?? void 0 });
1326
+ rows = await createNotesStore(notesContextFromPi(ctx)).list({ pattern: params.pattern ?? void 0 });
1163
1327
  } catch (error) {
1164
1328
  return failure(error);
1165
1329
  }
1166
- const files = rows.map((row) => ({ address: row.address, stale: row.meta.stale, updated_at: localIso(row.meta.updated_at) }));
1330
+ const files = rows.map((row) => ({ address: row.address, stale: row.meta.stale, updated_at: localIso(row.meta.updatedAt) }));
1167
1331
  return output(page(files, params.cursor ?? 0, "files", params.max_results, (file, fits) => {
1168
1332
  if (fits(file)) return file;
1169
1333
  const address = middleTruncate(file.address, (candidate) => fits({ ...file, address: candidate, address_truncated: true }));
@@ -1180,14 +1344,14 @@ function registerNotesTools(pi) {
1180
1344
  const queries = searchQueries(params.query);
1181
1345
  let rows;
1182
1346
  try {
1183
- rows = searchNotes(ctx, queries, { pattern: params.pattern ?? void 0 });
1347
+ rows = await createNotesStore(notesContextFromPi(ctx)).search(queries, { pattern: params.pattern ?? void 0 });
1184
1348
  } catch (error) {
1185
1349
  return failure(error);
1186
1350
  }
1187
1351
  const maxPerFile = params.max_matches_per_file ?? Number.POSITIVE_INFINITY;
1188
1352
  const result = rows.map((row) => {
1189
1353
  const matches = row.matches.map((match) => ({ line: match.line, text: match.text, truncated: false, offset_chars: match.offsetChars }));
1190
- return { address: row.address, updated_at: localIso(row.meta.updated_at), stale: row.meta.stale, matches_total: matches.length, matches: matches.slice(0, maxPerFile) };
1354
+ return { address: row.address, updated_at: localIso(row.meta.updatedAt), stale: row.meta.stale, matches_total: matches.length, matches: matches.slice(0, maxPerFile) };
1191
1355
  });
1192
1356
  const fitFile = (file, fits) => {
1193
1357
  if (fits(file)) return file;
@@ -1243,7 +1407,7 @@ function deriveThresholds(reserveTokens, margins) {
1243
1407
  else {
1244
1408
  const parsed = validMargin(margins.reminderMarginTokens);
1245
1409
  if (parsed === void 0) {
1246
- warnings.push(`pi-context: ${reminderKey} must be a positive integer; using default ${DEFAULT_REMINDER_MARGIN_TOKENS}.`);
1410
+ warnings.push(`pi-context: ${reminderKey} must be a positive integer; using the default reminder margin.`);
1247
1411
  reminderMargin = DEFAULT_REMINDER_MARGIN_TOKENS;
1248
1412
  } else reminderMargin = parsed;
1249
1413
  }
@@ -1330,7 +1494,7 @@ function notesIndex(snapshot) {
1330
1494
  if (recentNotes.length > 0) {
1331
1495
  const lines = [`You find ${recentNotes.length} crumpled note${recentNotes.length === 1 ? "" : "s"} in your pocket (by prefix, most recent first within each: up to ${POCKET_SESSION_LIMIT} from this session, ${POCKET_PROJECT_LIMIT} from @project, ${POCKET_HUMAN_LIMIT} from @human, ${POCKET_AGENT_LIMIT} from @self, ${POCKET_MODEL_LIMIT} from @model). A note's content never appears here, so its name has to say what the note is about:`];
1332
1496
  for (const row of recentNotes) {
1333
- lines.push(`- ${row.address} (${row.body.split("\n").length} lines, ${row.sizeBytes} UTF-8 bytes, updated ${relativeTime(row.meta.updated_at, snapshot.openedAt)})`);
1497
+ lines.push(`- ${row.address} (${row.body.split("\n").length} lines, ${row.sizeBytes} UTF-8 bytes, updated ${relativeTime(row.meta.updatedAt, snapshot.openedAt)})`);
1334
1498
  }
1335
1499
  sections.push(lines.join("\n"));
1336
1500
  }
@@ -1361,7 +1525,8 @@ function remainingTokens(ctx) {
1361
1525
  const usage = windowUsage(ctx);
1362
1526
  return !usage || usage.tokens === null ? null : Math.max(0, usage.contextWindow - usage.tokens);
1363
1527
  }
1364
- function registerBudget(pi, isEnabled, settingsManager) {
1528
+ function registerBudget(pi, isEnabled, settingsManager, onCloseOut = () => {
1529
+ }) {
1365
1530
  let cachedPolicy;
1366
1531
  const notifiedWarnings = /* @__PURE__ */ new Set();
1367
1532
  const resolvePolicy = (ctx) => {
@@ -1381,7 +1546,7 @@ function registerBudget(pi, isEnabled, settingsManager) {
1381
1546
  const automaticResetEnabled = (ctx) => {
1382
1547
  return resolvePolicy(ctx).automatic;
1383
1548
  };
1384
- const resetDue = (ctx) => {
1549
+ const hardReserveDue = (ctx) => {
1385
1550
  if (!automaticResetEnabled(ctx)) return false;
1386
1551
  const usage = windowUsage(ctx);
1387
1552
  return usage !== void 0 && usage.tokens !== null && usage.contextWindow - usage.tokens <= thresholdsFor(ctx).reserve;
@@ -1389,7 +1554,6 @@ function registerBudget(pi, isEnabled, settingsManager) {
1389
1554
  const invalidateThresholds = () => {
1390
1555
  cachedPolicy = void 0;
1391
1556
  };
1392
- const formatRemaining = (remaining) => `${Math.max(0, Math.ceil(remaining / 1e3))}k`;
1393
1557
  let pendingGuidance;
1394
1558
  let pendingWarning;
1395
1559
  let pendingNotices = [];
@@ -1397,7 +1561,7 @@ function registerBudget(pi, isEnabled, settingsManager) {
1397
1561
  const windowId = currentWindowId(ctx);
1398
1562
  for (const notice of pendingNotices) {
1399
1563
  if (notice.windowId !== windowId || !hasWindowMessage(ctx, notice.customType)) continue;
1400
- ctx.ui.notify(notice.customType === WARNING_TYPE ? `pi-context: Context almost full \u2014 ${formatRemaining(notice.remaining)} remaining` : `pi-context: Context running low \u2014 ${formatRemaining(notice.remaining)} remaining`, "warning");
1564
+ ctx.ui.notify(notice.customType === WARNING_TYPE ? "pi-context: Context almost full; close out the current memory window." : "pi-context: Context running low; checkpoint your notes soon.", "warning");
1401
1565
  }
1402
1566
  pendingNotices = [];
1403
1567
  };
@@ -1444,12 +1608,11 @@ function registerBudget(pi, isEnabled, settingsManager) {
1444
1608
  if (remaining === null) return void 0;
1445
1609
  const windowId = currentWindowId(ctx);
1446
1610
  const { reminder, warning } = thresholdsFor(ctx);
1447
- if (hasWindowMessage(ctx, WARNING_TYPE) || pendingWarning?.windowId === windowId) return void 0;
1448
- if (remaining <= warning) {
1611
+ if (remaining <= warning && automaticResetEnabled(ctx)) {
1612
+ onCloseOut(windowId);
1613
+ if (hasWindowMessage(ctx, WARNING_TYPE) || pendingWarning?.windowId === windowId) return void 0;
1449
1614
  pendingGuidance = void 0;
1450
- const content = `${GUIDANCE_OPEN_TAG}
1451
- ${WARNING_PROMPT}
1452
- ${GUIDANCE_CLOSE_TAG}`;
1615
+ const content = WARNING_CONTENT;
1453
1616
  pendingWarning = { windowId, content, remaining };
1454
1617
  const warningMessage = {
1455
1618
  role: "custom",
@@ -1460,6 +1623,7 @@ ${GUIDANCE_CLOSE_TAG}`;
1460
1623
  };
1461
1624
  return { messages: [..._event.messages, warningMessage] };
1462
1625
  }
1626
+ if (hasWindowMessage(ctx, WARNING_TYPE) || pendingWarning?.windowId === windowId) return void 0;
1463
1627
  if (hasWindowMessage(ctx, GUIDANCE_TYPE) || pendingGuidance?.windowId === windowId) return void 0;
1464
1628
  if (remaining <= reminder) {
1465
1629
  const left = Math.max(0, remaining - warning);
@@ -1479,7 +1643,7 @@ ${GUIDANCE_CLOSE_TAG}`;
1479
1643
  }));
1480
1644
  return {
1481
1645
  automaticResetEnabled,
1482
- resetDue,
1646
+ hardReserveDue,
1483
1647
  consumeTurnEnd,
1484
1648
  clear: () => {
1485
1649
  clearStaged();
@@ -1497,45 +1661,75 @@ function isOverflowLike(message, ctx) {
1497
1661
  if (message.role !== "assistant") return false;
1498
1662
  return isContextOverflow(message, ctx.model?.contextWindow) || ctx.model !== void 0 && isRecoverableLength(message, ctx.model.maxTokens);
1499
1663
  }
1664
+ var NO_REQUEST = { phase: "none" };
1500
1665
  function initialResetControl() {
1501
- return { request: "none", overflow: "idle" };
1666
+ return { request: NO_REQUEST, overflow: "idle" };
1667
+ }
1668
+ function requestForWindow(request, windowId) {
1669
+ return request.phase !== "none" && request.windowId === windowId ? request : NO_REQUEST;
1502
1670
  }
1503
1671
  function reduceResetControl(state, event) {
1504
1672
  switch (event.type) {
1505
- case "request": {
1506
- if (state.request === "explicit") return { state, effect: "already-requested" };
1507
- return { state: { ...state, request: "explicit" }, effect: "requested" };
1673
+ case "close_out": {
1674
+ const request = state.request;
1675
+ if (request.phase === "tool-requested" && request.windowId === event.windowId) {
1676
+ return { state, effect: "already-pending" };
1677
+ }
1678
+ if (request.phase === "close-out" && request.windowId === event.windowId) {
1679
+ if (request.source === "manual" || event.source === "automatic") return { state, effect: "already-pending" };
1680
+ return { state: { ...state, request: { ...request, source: "manual" } }, effect: "close-out-armed" };
1681
+ }
1682
+ return { state: { ...state, request: { phase: "close-out", windowId: event.windowId, source: event.source } }, effect: "close-out-armed" };
1683
+ }
1684
+ case "tool_request": {
1685
+ const request = state.request;
1686
+ if (request.phase === "tool-requested" && request.windowId === event.windowId) return { state, effect: "already-pending" };
1687
+ return { state: { ...state, request: { phase: "tool-requested", windowId: event.windowId } }, effect: "close-out-armed" };
1508
1688
  }
1509
1689
  case "turn_end": {
1510
1690
  const facts = event.facts;
1511
- const requested = state.request === "explicit";
1512
- const request = "none";
1513
- if (facts.aborted) {
1514
- return { state: { request, overflow: "idle" }, effect: "none" };
1515
- }
1691
+ const request = requestForWindow(state.request, facts.windowId);
1692
+ if (facts.aborted) return { state: initialResetControl(), effect: "none" };
1516
1693
  if (facts.overflow) {
1517
1694
  const pending = !facts.queued && facts.enabled && facts.automaticResetEnabled;
1518
1695
  const spent = state.overflow === "pending-spent" || state.overflow === "spent";
1519
- const overflow2 = pending ? spent ? "pending-spent" : "pending" : spent ? "spent" : "idle";
1520
- return { state: { request, overflow: overflow2 }, effect: "none" };
1696
+ const overflow = pending ? spent ? "pending-spent" : "pending" : spent ? "spent" : "idle";
1697
+ return { state: { request: NO_REQUEST, overflow }, effect: "none" };
1698
+ }
1699
+ if (facts.failed) return { state: { request: NO_REQUEST, overflow: state.overflow }, effect: "none" };
1700
+ if (!facts.enabled) {
1701
+ return { state: { request: NO_REQUEST, overflow: "idle" }, effect: "none" };
1702
+ }
1703
+ if (request.phase === "tool-requested" || facts.hardReserveDue) {
1704
+ return { state: { request: NO_REQUEST, overflow: "idle" }, effect: "commit-boundary" };
1521
1705
  }
1522
- const overflow = facts.failed ? state.overflow : "idle";
1523
- if (!facts.enabled || facts.failed) return { state: { request, overflow }, effect: "none" };
1524
- const commit = requested || facts.thresholdDue;
1525
- return { state: { request, overflow }, effect: commit ? "commit-boundary" : "none" };
1706
+ return { state: { request, overflow: "idle" }, effect: "none" };
1526
1707
  }
1527
1708
  case "before_settle": {
1528
1709
  const facts = event.facts;
1529
- if (state.overflow !== "pending" && state.overflow !== "pending-spent") return { state, effect: "none" };
1530
- if (facts.queued) return { state, effect: "none" };
1531
- const spent = state.overflow === "pending-spent";
1532
- if (!facts.enabled || !facts.automaticResetEnabled || facts.aborted) {
1533
- return { state: { ...state, overflow: spent ? "spent" : "idle" }, effect: "none" };
1710
+ if (facts.aborted) return { state: initialResetControl(), effect: "none" };
1711
+ const request = requestForWindow(state.request, facts.windowId);
1712
+ if (state.overflow === "pending" || state.overflow === "pending-spent") {
1713
+ if (facts.queued) {
1714
+ return { state: { ...state, request: facts.failed ? NO_REQUEST : request }, effect: "none" };
1715
+ }
1716
+ const spent = state.overflow === "pending-spent";
1717
+ if (!facts.enabled || !facts.automaticResetEnabled) {
1718
+ return { state: { request: facts.failed ? NO_REQUEST : request, overflow: spent ? "spent" : "idle" }, effect: "none" };
1719
+ }
1720
+ return { state: { request: NO_REQUEST, overflow: "spent" }, effect: spent ? "none" : "recover-overflow" };
1721
+ }
1722
+ if (facts.failed || !facts.enabled || request.phase !== "close-out") {
1723
+ return { state: { ...state, request: NO_REQUEST }, effect: "none" };
1534
1724
  }
1535
- return { state: { ...state, overflow: "spent" }, effect: spent ? "none" : "recover-overflow" };
1725
+ if (facts.queued) return { state: { ...state, request }, effect: "none" };
1726
+ if (request.source === "automatic" && !facts.automaticResetEnabled) {
1727
+ return { state: { ...state, request: NO_REQUEST }, effect: "none" };
1728
+ }
1729
+ return { state: { request: NO_REQUEST, overflow: "idle" }, effect: "commit-boundary" };
1536
1730
  }
1537
1731
  case "settled":
1538
- return { state: { ...state, overflow: "idle" }, effect: "none" };
1732
+ return { state: initialResetControl(), effect: "none" };
1539
1733
  case "abort":
1540
1734
  case "clear":
1541
1735
  return { state: initialResetControl(), effect: "none" };
@@ -1543,27 +1737,38 @@ function reduceResetControl(state, event) {
1543
1737
  }
1544
1738
  function registerResetLifecycle(pi, options) {
1545
1739
  let sessionActive = true;
1740
+ let lifecycleGeneration = 0;
1546
1741
  let control = initialResetControl();
1547
1742
  const clear = () => {
1548
1743
  control = reduceResetControl(control, { type: "clear" }).state;
1549
1744
  };
1550
- const resetBoundaryResult = (entries, ctx) => {
1745
+ const resetBoundaryResult = async (entries, ctx) => {
1746
+ const generation = lifecycleGeneration;
1747
+ const outerGeneration = options.getLifecycleGeneration?.();
1748
+ const sessionId = ctx.sessionManager.getSessionId();
1749
+ const windowId = currentWindowId(ctx);
1750
+ const isCurrent = () => sessionActive && lifecycleGeneration === generation && (outerGeneration === void 0 || options.getLifecycleGeneration?.() === outerGeneration) && options.isEnabled() && ctx.signal?.aborted !== true && ctx.sessionManager.getSessionId() === sessionId && currentWindowId(ctx) === windowId;
1751
+ let resetDrafts;
1551
1752
  try {
1552
- return { entries: [...entries, ...options.buildReset(ctx)], continue: true };
1753
+ resetDrafts = await options.buildReset(ctx, isCurrent);
1553
1754
  } catch (error) {
1554
- ctx.ui.notify(`pi-context: could not build reset (${String(error)}).`, "warning");
1755
+ if (isCurrent()) ctx.ui.notify(`pi-context: could not build reset (${String(error)}).`, "warning");
1555
1756
  return entries.length > 0 ? { entries } : void 0;
1556
1757
  }
1758
+ if (!isCurrent()) return entries.length > 0 ? { entries } : void 0;
1759
+ options.onResetReady?.(ctx, resetDrafts);
1760
+ return { entries: [...entries, ...resetDrafts], continue: true };
1557
1761
  };
1558
- pi.on("turn_end", (event, ctx) => {
1762
+ pi.on("turn_end", async (event, ctx) => {
1559
1763
  if (!sessionActive) return void 0;
1560
1764
  const aborted = isAbort(event.message, event.outcome, ctx);
1561
1765
  const stagedBudgetEntries = options.budget.consumeTurnEnd(ctx);
1562
- const budgetEntries = options.isEnabled() && !aborted ? stagedBudgetEntries : [];
1766
+ const budgetEntries = options.isEnabled() && !aborted && event.outcome !== "error" ? stagedBudgetEntries : [];
1563
1767
  const entries = [...event.entries ?? [], ...budgetEntries];
1564
1768
  const decision = reduceResetControl(control, {
1565
1769
  type: "turn_end",
1566
1770
  facts: {
1771
+ windowId: currentWindowId(ctx),
1567
1772
  aborted,
1568
1773
  overflow: aborted ? false : isOverflowLike(event.message, ctx),
1569
1774
  failed: event.outcome === "error",
@@ -1574,20 +1779,21 @@ function registerResetLifecycle(pi, options) {
1574
1779
  get automaticResetEnabled() {
1575
1780
  return options.budget.automaticResetEnabled(ctx);
1576
1781
  },
1577
- get thresholdDue() {
1578
- return options.budget.resetDue(ctx);
1782
+ get hardReserveDue() {
1783
+ return options.budget.hardReserveDue(ctx);
1579
1784
  }
1580
1785
  }
1581
1786
  });
1582
1787
  control = decision.state;
1583
- if (decision.effect === "commit-boundary") return resetBoundaryResult(entries, ctx);
1788
+ if (decision.effect === "commit-boundary") return await resetBoundaryResult(entries, ctx);
1584
1789
  return entries.length > 0 ? { entries } : void 0;
1585
1790
  });
1586
- pi.on("agent_before_settle", (event, ctx) => {
1791
+ pi.on("agent_before_settle", async (event, ctx) => {
1587
1792
  if (!sessionActive) return void 0;
1588
1793
  const decision = reduceResetControl(control, {
1589
1794
  type: "before_settle",
1590
1795
  facts: {
1796
+ windowId: currentWindowId(ctx),
1591
1797
  get queued() {
1592
1798
  return event.context.pendingMessages.length > 0 || ctx.hasPendingMessages();
1593
1799
  },
@@ -1597,14 +1803,13 @@ function registerResetLifecycle(pi, options) {
1597
1803
  get automaticResetEnabled() {
1598
1804
  return options.budget.automaticResetEnabled(ctx);
1599
1805
  },
1600
- get aborted() {
1601
- return event.outcome === "aborted" || ctx.signal?.aborted === true;
1602
- }
1806
+ aborted: event.outcome === "aborted" || ctx.signal?.aborted === true,
1807
+ failed: event.outcome === "error"
1603
1808
  }
1604
1809
  });
1605
1810
  control = decision.state;
1606
- if (decision.effect !== "recover-overflow") return void 0;
1607
- return resetBoundaryResult(event.entries, ctx);
1811
+ if (decision.effect !== "commit-boundary" && decision.effect !== "recover-overflow") return void 0;
1812
+ return await resetBoundaryResult(event.entries, ctx);
1608
1813
  });
1609
1814
  pi.on("session_before_compact", (event, ctx) => {
1610
1815
  if (!sessionActive) return void 0;
@@ -1619,26 +1824,36 @@ function registerResetLifecycle(pi, options) {
1619
1824
  return void 0;
1620
1825
  });
1621
1826
  pi.on("agent_end", (_event, ctx) => {
1622
- if (ctx.signal?.aborted) clear();
1827
+ if (ctx.signal?.aborted) control = reduceResetControl(control, { type: "abort" }).state;
1623
1828
  });
1624
1829
  pi.on("agent_settled", () => {
1625
1830
  control = reduceResetControl(control, { type: "settled" }).state;
1626
1831
  });
1627
1832
  pi.on("session_start", () => {
1833
+ lifecycleGeneration++;
1628
1834
  clear();
1629
1835
  sessionActive = true;
1630
1836
  });
1631
- pi.on("session_tree", clear);
1837
+ pi.on("session_tree", () => {
1838
+ lifecycleGeneration++;
1839
+ clear();
1840
+ });
1632
1841
  pi.on("session_shutdown", () => {
1842
+ lifecycleGeneration++;
1633
1843
  clear();
1634
1844
  options.budget.clear();
1635
1845
  sessionActive = false;
1636
1846
  });
1637
1847
  return {
1638
- request() {
1639
- const decision = reduceResetControl(control, { type: "request" });
1848
+ closeOut(windowId, source) {
1849
+ const decision = reduceResetControl(control, { type: "close_out", windowId, source });
1640
1850
  control = decision.state;
1641
- return decision.effect === "already-requested" ? "rollover_already_pending" : "rollover_requested";
1851
+ return decision.effect;
1852
+ },
1853
+ request(windowId) {
1854
+ const decision = reduceResetControl(control, { type: "tool_request", windowId });
1855
+ control = decision.state;
1856
+ return decision.effect === "already-pending" ? "rollover_already_pending" : "rollover_requested";
1642
1857
  },
1643
1858
  clear
1644
1859
  };
@@ -1647,7 +1862,7 @@ function registerResetLifecycle(pi, options) {
1647
1862
  // src/context/reset-artifacts.ts
1648
1863
  import { randomUUID as randomUUID2 } from "node:crypto";
1649
1864
 
1650
- // src/notes/notes-snapshot.ts
1865
+ // src/pi/notes/snapshot.ts
1651
1866
  var NOTES_HOMES = [
1652
1867
  { scope: "session", label: "this session" },
1653
1868
  { scope: "project", label: "@project" },
@@ -1655,13 +1870,19 @@ var NOTES_HOMES = [
1655
1870
  { scope: "agent", label: "@self" },
1656
1871
  { scope: "model", label: "@model" }
1657
1872
  ];
1658
- function loadNotesSnapshot(ctx, loadHome = (context, scope) => listNotes(context, { scope })) {
1873
+ function queryForScope(scope) {
1874
+ if (scope === "agent" || scope === "model") return { scope };
1875
+ return { scope };
1876
+ }
1877
+ async function loadNotesSnapshot(ctx, loadHome, identity = notesContextFromPi(ctx)) {
1659
1878
  const openedAt = Date.now();
1879
+ const store = createNotesStore(identity);
1880
+ const load = loadHome ?? ((_context, scope) => store.list(queryForScope(scope)));
1660
1881
  const homes = /* @__PURE__ */ new Map();
1661
1882
  const unavailable = [];
1662
1883
  for (const home of NOTES_HOMES) {
1663
1884
  try {
1664
- homes.set(home.scope, loadHome(ctx, home.scope));
1885
+ homes.set(home.scope, await load(ctx, home.scope));
1665
1886
  } catch (error) {
1666
1887
  const code = typeof error === "object" && error !== null ? error.code : void 0;
1667
1888
  if (typeof code !== "string" || !/^E[A-Z0-9_]+$/.test(code) || code.startsWith("ERR_")) throw error;
@@ -1673,20 +1894,19 @@ function loadNotesSnapshot(ctx, loadHome = (context, scope) => listNotes(context
1673
1894
  }
1674
1895
 
1675
1896
  // src/context/boot.ts
1676
- function bootContent(ctx, currentId, previousId, notes) {
1677
- return renderBootBlock({
1678
- agentName: agentSlug(ctx),
1679
- modelName: modelSlug(ctx),
1680
- firstWindowId: rootWindowId(ctx.sessionManager.getSessionId()),
1681
- currentWindowId: currentId,
1682
- previousWindowId: previousId,
1683
- notes
1684
- });
1897
+ function bootContent(agentName, modelName, firstWindowId, currentId, previousId, notes) {
1898
+ return renderBootBlock({ agentName, modelName, firstWindowId, currentWindowId: currentId, previousWindowId: previousId, notes });
1685
1899
  }
1686
- function buildBootMessage(ctx, windowId, previousId, notifyIncompleteNotes) {
1687
- const notes = loadNotesSnapshot(ctx);
1688
- notifyIncompleteNotes?.(ctx, windowId, notes);
1689
- return { customType: BOOT_TYPE, content: bootContent(ctx, windowId, previousId, notes), display: false, details: { windowId } };
1900
+ async function buildBootMessage(ctx, windowId, previousId, notifyIncompleteNotes, isCurrent = () => true) {
1901
+ const identity = notesContextFromPi(ctx);
1902
+ const notes = await loadNotesSnapshot(ctx, void 0, identity);
1903
+ if (isCurrent()) notifyIncompleteNotes?.(ctx, windowId, notes);
1904
+ return {
1905
+ customType: BOOT_TYPE,
1906
+ content: bootContent(identity.agent, identity.model, rootWindowId(identity.sessionId), windowId, previousId, notes),
1907
+ display: false,
1908
+ details: { windowId }
1909
+ };
1690
1910
  }
1691
1911
  function sendBoot(pi, boot) {
1692
1912
  pi.sendMessage(
@@ -1694,15 +1914,20 @@ function sendBoot(pi, boot) {
1694
1914
  { triggerTurn: false }
1695
1915
  );
1696
1916
  }
1697
- function ensureBoot(pi, ctx, notifyIncompleteNotes) {
1917
+ async function ensureBoot(pi, ctx, notifyIncompleteNotes, isCurrent = () => true) {
1918
+ const sessionId = ctx.sessionManager.getSessionId();
1698
1919
  const reset = currentReset(ctx);
1920
+ const windowId = reset?.data.windowId ?? rootWindowId(sessionId);
1921
+ const stillCurrent = () => isCurrent() && ctx.signal?.aborted !== true && ctx.sessionManager.getSessionId() === sessionId && currentWindowId(ctx) === windowId;
1699
1922
  if (reset) {
1700
- repairResetTail(pi, ctx, reset, notifyIncompleteNotes);
1923
+ await repairResetTail(pi, ctx, reset, notifyIncompleteNotes, stillCurrent);
1701
1924
  return;
1702
1925
  }
1703
- const windowId = rootWindowId(ctx.sessionManager.getSessionId());
1704
1926
  if (ctx.sessionManager.buildSessionProjection().messages.some((message) => isWindowBoot(message, windowId))) return;
1705
- sendBoot(pi, buildBootMessage(ctx, windowId, void 0, notifyIncompleteNotes));
1927
+ const boot = await buildBootMessage(ctx, windowId, void 0, notifyIncompleteNotes, stillCurrent);
1928
+ if (!stillCurrent()) return;
1929
+ if (ctx.sessionManager.buildSessionProjection().messages.some((message) => isWindowBoot(message, windowId))) return;
1930
+ sendBoot(pi, boot);
1706
1931
  }
1707
1932
 
1708
1933
  // src/context/reset-artifacts.ts
@@ -1715,8 +1940,10 @@ function sendContinuation(pi) {
1715
1940
  { triggerTurn: false }
1716
1941
  );
1717
1942
  }
1718
- function buildResetDrafts(ctx, notifyIncompleteNotes) {
1719
- const sessionPrefix = ctx.sessionManager.getSessionId().slice(0, 8);
1943
+ async function buildResetDrafts(ctx, notifyIncompleteNotes, isCurrent = () => true) {
1944
+ const sessionId = ctx.sessionManager.getSessionId();
1945
+ const previousId = currentWindowId(ctx);
1946
+ const sessionPrefix = sessionId.slice(0, 8);
1720
1947
  const usedWindowIds = new Set(
1721
1948
  ctx.sessionManager.getBranch().filter(isWindowMarker).map((entry) => entry.data.windowId)
1722
1949
  );
@@ -1724,26 +1951,14 @@ function buildResetDrafts(ctx, notifyIncompleteNotes) {
1724
1951
  do {
1725
1952
  windowId = `pcw:${sessionPrefix}:${randomUUID2().slice(0, 8)}`;
1726
1953
  } while (usedWindowIds.has(windowId));
1727
- const boot = buildBootMessage(ctx, windowId, currentWindowId(ctx), notifyIncompleteNotes);
1954
+ const boot = await buildBootMessage(ctx, windowId, previousId, notifyIncompleteNotes, isCurrent);
1728
1955
  return [
1956
+ { type: "compaction", summary: "", firstKeptEntryId: null },
1729
1957
  { type: "custom", customType: RESET_MARKER_TYPE, data: { windowId } },
1730
1958
  { type: "custom_message", customType: BOOT_TYPE, content: boot.content, display: false, details: { windowId } },
1731
1959
  { type: "custom_message", customType: CONTINUATION_TYPE, content: CONTINUATION, display: false }
1732
1960
  ];
1733
1961
  }
1734
- function persistManualReset(pi, ctx, notifyIncompleteNotes) {
1735
- const [marker, boot, continuation] = buildResetDrafts(ctx, notifyIncompleteNotes);
1736
- pi.appendEntry(marker.customType, marker.data);
1737
- pi.sendMessage(
1738
- { customType: boot.customType, content: boot.content, display: boot.display, details: boot.details },
1739
- { triggerTurn: false }
1740
- );
1741
- pi.sendMessage(
1742
- { customType: continuation.customType, content: continuation.content, display: continuation.display },
1743
- { triggerTurn: false }
1744
- );
1745
- return boot.details.windowId;
1746
- }
1747
1962
  function inspectResetTail(ctx, markerId, windowId) {
1748
1963
  const branch = ctx.sessionManager.getBranch();
1749
1964
  const markerIndex = branch.findIndex((entry) => entry.id === markerId);
@@ -1771,11 +1986,15 @@ function resetTailCommitted(ctx, markerId, windowId) {
1771
1986
  const tail = inspectResetTail(ctx, markerId, windowId);
1772
1987
  return tail?.boot === true && tail.continuation === true;
1773
1988
  }
1774
- function repairResetTail(pi, ctx, marker, notifyIncompleteNotes) {
1989
+ async function repairResetTail(pi, ctx, marker, notifyIncompleteNotes, isCurrent = () => true) {
1775
1990
  const tail = inspectResetTail(ctx, marker.id, marker.data.windowId);
1776
1991
  if (!tail || tail.boot && tail.continuation) return;
1777
- if (!tail.boot) sendBoot(pi, buildBootMessage(ctx, marker.data.windowId, previousWindowId(ctx, marker.id), notifyIncompleteNotes));
1778
- if (!tail.continuation) sendContinuation(pi);
1992
+ const boot = tail.boot ? void 0 : await buildBootMessage(ctx, marker.data.windowId, previousWindowId(ctx, marker.id), notifyIncompleteNotes, isCurrent);
1993
+ if (!isCurrent()) return;
1994
+ const latest = inspectResetTail(ctx, marker.id, marker.data.windowId);
1995
+ if (!latest) return;
1996
+ if (!latest.boot && boot) sendBoot(pi, boot);
1997
+ if (!latest.continuation && (latest.boot || boot)) sendContinuation(pi);
1779
1998
  }
1780
1999
 
1781
2000
  // src/context/runtime.ts
@@ -1785,6 +2004,7 @@ function branchHasWindowMarker(ctx, fromId) {
1785
2004
  }
1786
2005
  function registerContext(pi, settingsManager) {
1787
2006
  let enabled = true;
2007
+ let lifecycleGeneration = 0;
1788
2008
  let missingBootNotice;
1789
2009
  const incompleteNotesNotified = /* @__PURE__ */ new Set();
1790
2010
  const pendingResetNotices = /* @__PURE__ */ new Set();
@@ -1794,7 +2014,7 @@ function registerContext(pi, settingsManager) {
1794
2014
  const branch = ctx.sessionManager.getBranch();
1795
2015
  for (const windowId of pendingResetNotices) {
1796
2016
  const marker = branch.find((entry) => isWindowMarker(entry) && entry.data.windowId === windowId);
1797
- if (!marker || !resetTailCommitted(ctx, marker.id, windowId)) continue;
2017
+ if (!marker || !isCheckpointBackedReset(ctx, marker) || !resetTailCommitted(ctx, marker.id, windowId)) continue;
1798
2018
  pendingResetNotices.delete(windowId);
1799
2019
  ctx.ui.notify(`pi-context: memory cleared \xB7 ${windowId}`, "info");
1800
2020
  }
@@ -1812,17 +2032,23 @@ function registerContext(pi, settingsManager) {
1812
2032
  };
1813
2033
  const migrationWarning = migrateLegacyHomes();
1814
2034
  if (migrationWarning) console.warn(`pi-context: ${migrationWarning}`);
1815
- const budget = registerBudget(pi, () => enabled, settingsManager);
1816
- pi.on("session_start", (_event, ctx) => {
2035
+ const budget = registerBudget(pi, () => enabled, settingsManager, (windowId) => resets.closeOut(windowId, "automatic"));
2036
+ pi.on("session_start", async (_event, ctx) => {
2037
+ const generation = ++lifecycleGeneration;
1817
2038
  if (!enabled) return;
1818
2039
  missingBootNotice = void 0;
1819
2040
  pendingResetNotices.clear();
1820
- ensureBoot(pi, ctx, notifyIncompleteNotes);
2041
+ await ensureBoot(pi, ctx, notifyIncompleteNotes, () => generation === lifecycleGeneration && enabled);
1821
2042
  });
1822
- pi.on("session_tree", (_event, ctx) => {
2043
+ pi.on("session_tree", async (_event, ctx) => {
2044
+ const generation = ++lifecycleGeneration;
1823
2045
  missingBootNotice = void 0;
1824
2046
  pendingResetNotices.clear();
1825
- if (enabled) ensureBoot(pi, ctx, notifyIncompleteNotes);
2047
+ if (enabled) await ensureBoot(pi, ctx, notifyIncompleteNotes, () => generation === lifecycleGeneration && enabled);
2048
+ });
2049
+ pi.on("session_shutdown", () => {
2050
+ lifecycleGeneration++;
2051
+ pendingResetNotices.clear();
1826
2052
  });
1827
2053
  pi.on("session_before_tree", (event, ctx) => {
1828
2054
  if (!event.preparation.userWantsSummary) return void 0;
@@ -1834,7 +2060,11 @@ function registerContext(pi, settingsManager) {
1834
2060
  const reset = currentReset(ctx);
1835
2061
  const windowId = reset?.data.windowId ?? rootWindowId(ctx.sessionManager.getSessionId());
1836
2062
  try {
1837
- return { messages: reset ? projectWindow(event.messages, windowId) : projectRootWindow(event.messages, windowId) };
2063
+ if (reset) {
2064
+ if (!event.messages.some((message) => isWindowBoot(message, windowId))) throw new Error(`Missing boot for context window ${windowId}`);
2065
+ return { messages: isCheckpointBackedReset(ctx, reset) ? event.messages : projectWindow(event.messages, windowId) };
2066
+ }
2067
+ return { messages: projectRootWindow(event.messages, windowId) };
1838
2068
  } catch (error) {
1839
2069
  if (missingBootNotice !== windowId) {
1840
2070
  missingBootNotice = windowId;
@@ -1852,9 +2082,12 @@ function registerContext(pi, settingsManager) {
1852
2082
  const arg = args.trim().toLowerCase();
1853
2083
  if (arg === "on") {
1854
2084
  enabled = true;
1855
- ensureBoot(pi, cmdCtx, notifyIncompleteNotes);
2085
+ const generation = ++lifecycleGeneration;
2086
+ await ensureBoot(pi, cmdCtx, notifyIncompleteNotes, () => generation === lifecycleGeneration && enabled);
1856
2087
  } else if (arg === "off") {
1857
2088
  enabled = false;
2089
+ lifecycleGeneration++;
2090
+ pendingResetNotices.clear();
1858
2091
  budget.clear();
1859
2092
  resets.clear();
1860
2093
  } else if (arg !== "") {
@@ -1865,16 +2098,25 @@ function registerContext(pi, settingsManager) {
1865
2098
  }
1866
2099
  });
1867
2100
  pi.registerCommand("wipe-memory", {
1868
- description: "Persist a fresh context window without calling the model",
2101
+ description: "Ask the agent to close out its notes, then start a fresh context window",
1869
2102
  handler: async (_args, cmdCtx) => {
1870
2103
  if (!enabled) {
1871
2104
  cmdCtx.ui.notify("pi-context: /wipe-memory requires /pi-context on.", "error");
1872
2105
  return;
1873
2106
  }
2107
+ const requestedWindowId = currentWindowId(cmdCtx);
2108
+ await cmdCtx.waitForIdle();
2109
+ if (!enabled || currentWindowId(cmdCtx) !== requestedWindowId) return;
2110
+ const armed = resets.closeOut(requestedWindowId, "manual");
2111
+ if (armed === "already-pending") return;
2112
+ try {
2113
+ pi.sendMessage({ customType: WARNING_TYPE, content: WARNING_CONTENT, display: false }, { triggerTurn: true });
2114
+ } catch (error) {
2115
+ resets.clear();
2116
+ cmdCtx.ui.notify(`pi-context: could not start manual close-out (${String(error)}).`, "error");
2117
+ return;
2118
+ }
1874
2119
  await cmdCtx.waitForIdle();
1875
- if (!enabled) return;
1876
- resets.clear();
1877
- notifyCommittedResets(cmdCtx, persistManualReset(pi, cmdCtx, notifyIncompleteNotes));
1878
2120
  }
1879
2121
  });
1880
2122
  pi.registerTool(defineTool4({
@@ -1882,23 +2124,27 @@ function registerContext(pi, settingsManager) {
1882
2124
  label: "Wipe memory",
1883
2125
  description: "Wipe your in-context memory and start a fresh context window. Your session, notes, and history survive.",
1884
2126
  parameters: Type5.Object({}, { additionalProperties: false }),
1885
- async execute() {
2127
+ async execute(_id, _params, _signal, _update, ctx) {
1886
2128
  if (!enabled) return output({ error: "pi-context is off (/pi-context on to enable)" });
1887
- return output({ status: resets.request() }, void 0, true);
2129
+ return output({ status: resets.request(currentWindowId(ctx)) }, void 0, true);
1888
2130
  }
1889
2131
  }));
1890
2132
  const resets = registerResetLifecycle(pi, {
1891
2133
  isEnabled: () => enabled,
1892
- buildReset: (ctx) => {
1893
- const drafts = buildResetDrafts(ctx, notifyIncompleteNotes);
1894
- pendingResetNotices.add(drafts[1].details.windowId);
1895
- return drafts;
2134
+ buildReset: (ctx, isCurrent) => buildResetDrafts(ctx, notifyIncompleteNotes, isCurrent),
2135
+ getLifecycleGeneration: () => lifecycleGeneration,
2136
+ onResetReady: (_ctx, drafts) => {
2137
+ const boot = drafts.find((draft) => draft.type === "custom_message" && draft.customType === BOOT_TYPE);
2138
+ if (boot?.type === "custom_message" && boot.details && typeof boot.details === "object") {
2139
+ const windowId = boot.details.windowId;
2140
+ if (typeof windowId === "string") pendingResetNotices.add(windowId);
2141
+ }
1896
2142
  },
1897
2143
  budget
1898
2144
  });
1899
2145
  }
1900
2146
 
1901
- // src/notes/session-replay.ts
2147
+ // src/pi/notes/session-replay.ts
1902
2148
  function isNoteOperation(data) {
1903
2149
  if (typeof data !== "object" || data === null) return false;
1904
2150
  const op = data;
@@ -1941,7 +2187,7 @@ function createPiContext(options = {}) {
1941
2187
  function piContext(pi) {
1942
2188
  registerPiContext(pi);
1943
2189
  }
1944
- var internal = { MAX_NOTE_BYTES, NOTE_TYPE, BOOT_TYPE, GUIDANCE_TYPE, WARNING_TYPE, CONTINUATION_TYPE, WARNING_PROMPT, WARNING_RUNWAY_TOKENS, RESET_MARKER_TYPE, CONTINUATION, CONTEXT_WINDOW_OPEN_TAG, CONTEXT_WINDOW_CLOSE_TAG, CONTEXT_WINDOW_PROTOCOL_OPEN_TAG, CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG, GUIDANCE_OPEN_TAG, PI_CONTEXT_SETTINGS_KEY, DEFAULT_RESERVE_TOKENS, DEFAULT_REMINDER_MARGIN_TOKENS, deriveThresholds, mergePiContextSettings, assertVirtualPath };
2190
+ var internal = { MAX_NOTE_BYTES, NOTE_TYPE, BOOT_TYPE, GUIDANCE_TYPE, WARNING_TYPE, CONTINUATION_TYPE, WARNING_PROMPT, WARNING_CONTENT, WARNING_RUNWAY_TOKENS, RESET_MARKER_TYPE, CONTINUATION, CONTEXT_WINDOW_OPEN_TAG, CONTEXT_WINDOW_CLOSE_TAG, CONTEXT_WINDOW_PROTOCOL_OPEN_TAG, CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG, GUIDANCE_OPEN_TAG, PI_CONTEXT_SETTINGS_KEY, DEFAULT_RESERVE_TOKENS, DEFAULT_REMINDER_MARGIN_TOKENS, deriveThresholds, mergePiContextSettings, assertVirtualPath };
1945
2191
  export {
1946
2192
  createPiContext,
1947
2193
  piContext as default,