@astrosheep/pi-context 0.25.2 → 0.26.1

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