@astrosheep/pi-context 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +22 -1
  2. package/dist/src/budget.js +10 -8
  3. package/dist/src/dream/cli.js +108 -24
  4. package/dist/src/dream/gates.js +13 -8
  5. package/dist/src/dream/git.js +71 -0
  6. package/dist/src/dream/lock.js +78 -37
  7. package/dist/src/dream/runner.js +90 -21
  8. package/dist/src/history-tools.js +5 -5
  9. package/dist/src/history.js +11 -6
  10. package/dist/src/index.js +14 -15
  11. package/dist/src/notes/address.js +31 -0
  12. package/dist/src/{memory → notes}/frontmatter.js +7 -5
  13. package/dist/src/{notes.js → notes/model.js} +1 -1
  14. package/dist/src/{memory → notes}/paths.js +7 -3
  15. package/dist/src/{memory → notes}/store.js +47 -74
  16. package/dist/src/notes/tools.js +153 -0
  17. package/dist/src/prompts.js +38 -29
  18. package/dist/src/protocol.js +9 -4
  19. package/dist/src/thresholds.js +33 -3
  20. package/dist/src/tool-output.js +4 -1
  21. package/dist/src/warning.js +3 -3
  22. package/dist/test/agent-loop.test.js +6 -4
  23. package/dist/test/coherence.test.js +5 -1
  24. package/dist/test/dream.test.js +419 -35
  25. package/dist/test/history.test.js +6 -1
  26. package/dist/test/integration.test.js +107 -47
  27. package/dist/test/{memory.test.js → notes.test.js} +154 -50
  28. package/dist/test/pagination.property.test.js +1 -1
  29. package/package.json +5 -5
  30. package/playbook.md +30 -3
  31. package/src/budget.ts +11 -9
  32. package/src/dream/cli.ts +95 -17
  33. package/src/dream/gates.ts +14 -7
  34. package/src/dream/git.ts +73 -0
  35. package/src/dream/lock.ts +67 -24
  36. package/src/dream/runner.ts +87 -20
  37. package/src/history-tools.ts +5 -5
  38. package/src/history.ts +12 -7
  39. package/src/index.ts +13 -14
  40. package/src/notes/address.ts +33 -0
  41. package/src/{memory → notes}/frontmatter.ts +7 -5
  42. package/src/{notes.ts → notes/model.ts} +2 -2
  43. package/src/{memory → notes}/paths.ts +8 -3
  44. package/src/{memory → notes}/store.ts +49 -79
  45. package/src/notes/tools.ts +132 -0
  46. package/src/prompts.ts +39 -29
  47. package/src/protocol.ts +9 -4
  48. package/src/thresholds.ts +38 -6
  49. package/src/tool-output.ts +4 -1
  50. package/src/warning.ts +3 -3
  51. package/dist/src/dream/apply.js +0 -87
  52. package/dist/src/dream/manifest.js +0 -16
  53. package/dist/src/memory/tools.js +0 -175
  54. package/src/dream/apply.ts +0 -47
  55. package/src/dream/manifest.ts +0 -21
  56. package/src/memory/tools.ts +0 -175
@@ -1,7 +1,6 @@
1
1
  import { historyFromSession } from "./history.js";
2
- import { localIso } from "./notes.js";
3
- import { listNotes, peekNote, resolveNoteScope } from "./memory/store.js";
4
- import { CONTEXT_WINDOW_OPEN_TAG, CONTEXT_WINDOW_CLOSE_TAG, NOTE_PREVIEW_CHARS, NOTE_PREVIEW_HEAD_CHARS, NOTE_PREVIEW_TAIL_CHARS, RESET_SUMMARY, PROTOCOL_BLOCK, GUIDANCE_OPEN_TAG, GUIDANCE_CLOSE_TAG } from "./protocol.js";
2
+ import { listNotes } from "./notes/store.js";
3
+ import { CONTEXT_WINDOW_OPEN_TAG, CONTEXT_WINDOW_CLOSE_TAG, POCKET_PERSONAL_LIMIT, POCKET_PROJECT_LIMIT, POCKET_SESSION_LIMIT, RESET_SUMMARY, PROTOCOL_BLOCK, GUIDANCE_OPEN_TAG, GUIDANCE_CLOSE_TAG } from "./protocol.js";
5
4
  /** Codex-style <context_window> identity block: agent name and first/current/previous window ids only. */
6
5
  function identityBlock(agentName, firstWindowId, currentWindowId, previousWindowId) {
7
6
  const lines = [
@@ -13,44 +12,53 @@ function identityBlock(agentName, firstWindowId, currentWindowId, previousWindow
13
12
  lines.push(`Previous context window id: ${previousWindowId}`);
14
13
  return `${CONTEXT_WINDOW_OPEN_TAG}\n${lines.join("\n")}\n${CONTEXT_WINDOW_CLOSE_TAG}`;
15
14
  }
15
+ function relativeTime(timestamp, now) {
16
+ const seconds = Math.trunc((timestamp - now) / 1000);
17
+ const [unit, size] = [["d", 86400], ["h", 3600], ["m", 60], ["s", 1]]
18
+ .find(([unit, size]) => Math.abs(seconds) >= size || unit === "s");
19
+ const amount = `${Math.abs(Math.trunc(seconds / size))}${unit}`;
20
+ return seconds > 0 ? `in ${amount}` : `${amount} ago`;
21
+ }
16
22
  /**
17
- * Recent-notes index: up to three most-recent fresh (non-stale) notes. Each note shows its
18
- * path, line count, UTF-8 byte count and local ISO update time, followed by an indented inline
19
- * preview: the whole text when it fits in NOTE_PREVIEW_CHARS, otherwise its first
20
- * NOTE_PREVIEW_HEAD_CHARS and last NOTE_PREVIEW_TAIL_CHARS Unicode characters joined by an
21
- * explicit ellipsis. The two slices never overlap, so the preview never duplicates head content
22
- * as tail content. Stale notes are excluded entirely; empty when no fresh notes remain.
23
+ * Boot notes index. Map residency ("地图在场"): fresh MAP.md bodies from the personal and
24
+ * project homes are both injected, broadest first; stale maps are skipped per home, and the
25
+ * session home is never peeked a session MAP.md is an ordinary note. The pocket then lists
26
+ * recent fresh notes under per-home quotas (POCKET_SESSION_LIMIT / POCKET_PROJECT_LIMIT /
27
+ * POCKET_PERSONAL_LIMIT), most-recently-updated first within each home, one metadata line
28
+ * each: address, line count, UTF-8 byte count, relative update time at window open. Bodies never render
29
+ * in the pocket; stale notes are excluded; MAP.md itself never takes a pocket seat.
23
30
  */
24
31
  function notesIndex(ctx) {
25
32
  const sections = [];
26
- // TOC residency ("地图在场"): the map, when present, is injected whole ahead of the list.
27
- const toc = resolveNoteScope(ctx, "TOC.md");
28
- if (toc) {
29
- const body = peekNote(ctx, toc.scope, "TOC.md").body;
30
- if (body.length > 0)
31
- sections.push(body);
33
+ // Map residency ("地图在场"): scope-native maps, both fresh ones injected broadest-first.
34
+ // A session MAP.md is an ordinary note, never resident; stale maps skip independently.
35
+ for (const scope of ["personal", "project"]) {
36
+ const toc = listNotes(ctx, { scope }).find((row) => row.path === "MAP.md");
37
+ if (toc && !toc.meta.stale) {
38
+ if (toc.body.length > 0)
39
+ sections.push(toc.body);
40
+ }
32
41
  }
33
- // listNotes is already most-recently-updated first; stale notes never reach the index.
34
- const recentNotes = listNotes(ctx, {})
35
- .filter((row) => !row.meta.stale)
36
- .slice(0, 5);
42
+ // listNotes is most-recently-updated first within each home. Per-home quotas keep session
43
+ // churn from evicting project or personal notes; maps never take pocket seats.
44
+ const recentNotes = [
45
+ ...listNotes(ctx, { scope: "session" }).filter((row) => !row.meta.stale && row.path !== "MAP.md").slice(0, POCKET_SESSION_LIMIT),
46
+ ...listNotes(ctx, { scope: "project" }).filter((row) => !row.meta.stale && row.path !== "MAP.md").slice(0, POCKET_PROJECT_LIMIT),
47
+ ...listNotes(ctx, { scope: "personal" }).filter((row) => !row.meta.stale && row.path !== "MAP.md").slice(0, POCKET_PERSONAL_LIMIT),
48
+ ];
37
49
  if (recentNotes.length > 0) {
38
- const lines = [`You find ${recentNotes.length} crumpled note${recentNotes.length === 1 ? "" : "s"} in your pocket (up to 5, most recent first):`];
50
+ const lines = [`You find ${recentNotes.length} crumpled note${recentNotes.length === 1 ? "" : "s"} in your pocket (by home, most recent first within each: up to ${POCKET_SESSION_LIMIT} from this session, ${POCKET_PROJECT_LIMIT} from this project, ${POCKET_PERSONAL_LIMIT} from personal). A note's content never appears here, so its name has to say what the note is about:`];
51
+ const now = Date.now();
39
52
  for (const row of recentNotes) {
40
- const body = peekNote(ctx, row.meta.scope, row.path).body;
41
- lines.push(`- ${row.path} (${body.split("\n").length} lines, ${row.sizeBytes} UTF-8 bytes, updated ${localIso(row.meta.updated_at)})`);
42
- const chars = Array.from(body);
43
- // Short notes stay whole; long notes keep both ends. head + tail <= NOTE_PREVIEW_CHARS < chars.length,
44
- // so the slices are disjoint and no character is shown twice.
45
- const preview = chars.length <= NOTE_PREVIEW_CHARS
46
- ? body
47
- : `${chars.slice(0, NOTE_PREVIEW_HEAD_CHARS).join("")}…${chars.slice(chars.length - NOTE_PREVIEW_TAIL_CHARS).join("")}`;
48
- lines.push(preview.split("\n").map((line) => ` ${line}`).join("\n"));
53
+ lines.push(`- ${row.address} (${row.body.split("\n").length} lines, ${row.sizeBytes} UTF-8 bytes, updated ${relativeTime(row.meta.updated_at, now)})`);
49
54
  }
50
55
  sections.push(lines.join("\n"));
51
56
  }
52
57
  return sections.join("\n\n");
53
58
  }
59
+ function notesHomeBlock() {
60
+ return "Notes_* addresses have three homes: bare <vpath> is this session, @project/<vpath> is this project, and @personal/<vpath> is the human's cross-project home. @ means leaving home; there is no cross-home fallback. Any other note is a plain file — use the file tools.";
61
+ }
54
62
  /**
55
63
  * Assemble the static, once-per-window boot block: the reset line for resets, the
56
64
  * <context_window> identity block, the recent-notes index at window-open time, and
@@ -63,6 +71,7 @@ export function bootBlock(ctx, currentId, previousId, resetLine) {
63
71
  if (resetLine)
64
72
  parts.push(RESET_SUMMARY);
65
73
  parts.push(identityBlock(ctx.sessionManager.getSessionName() ?? "root", firstId, currentId, previousId));
74
+ parts.push(notesHomeBlock());
66
75
  const index = notesIndex(ctx);
67
76
  if (index)
68
77
  parts.push(index);
@@ -7,6 +7,9 @@ export const RESET_MARKER_TYPE = "pi-context/reset-marker";
7
7
  export const CONTINUATION_TYPE = "pi-context/continuation";
8
8
  export const RESET_V2 = "reset-v2";
9
9
  export const MAX_NOTE_BYTES = 1_000_000;
10
+ export const POCKET_SESSION_LIMIT = 5;
11
+ export const POCKET_PROJECT_LIMIT = 2;
12
+ export const POCKET_PERSONAL_LIMIT = 2;
10
13
  // Write-time cap on a virtual note path. Deliberately NOT enforced by assertVirtualPath:
11
14
  // notesFromSession replays already-persisted operations, which must keep loading sessions
12
15
  // that contain a longer legacy path. Reads and replay stay un-capped.
@@ -18,6 +21,8 @@ export const CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG = "</context_window_protocol>";
18
21
  export const GUIDANCE_OPEN_TAG = "<context_window_guidance>";
19
22
  export const GUIDANCE_CLOSE_TAG = "</context_window_guidance>";
20
23
  export const PI_CONTEXT_SETTINGS_KEY = "pi-context";
24
+ /** Nested under "pi-context": the default dreamer model pattern, overridden by CLI --dreamer. */
25
+ export const PI_CONTEXT_DREAMER_KEY = "dreamer";
21
26
  export const DEFAULT_RESERVE_TOKENS = 16_384;
22
27
  export const DEFAULT_REMINDER_MARGIN_TOKENS = 24_576;
23
28
  /**
@@ -28,9 +33,6 @@ export const DEFAULT_REMINDER_MARGIN_TOKENS = 24_576;
28
33
  */
29
34
  export const WARNING_RUNWAY_TOKENS = 12_288;
30
35
  export const RESET_SUMMARY = "You wake up. Your head is empty — no memories, the past a blank. But nothing is lost: the notes you wrote and the recorded history still remember for you.";
31
- export const NOTE_PREVIEW_HEAD_CHARS = 80;
32
- export const NOTE_PREVIEW_TAIL_CHARS = 240;
33
- export const NOTE_PREVIEW_CHARS = NOTE_PREVIEW_HEAD_CHARS + NOTE_PREVIEW_TAIL_CHARS;
34
36
  export const CONTINUATION = "Your memory was just erased. Pull only the details you need from history_* and notes_*, then get back to work.";
35
37
  /**
36
38
  * Static protocol teaching adapted from Codex's token_budget.guidance_message to
@@ -47,6 +49,9 @@ Use get_context_remaining to see how much of the window is left. When it runs ou
47
49
 
48
50
  If <context_window> lists a Previous context window id, a reset just happened and the old conversation is not included. Read your note checkpoint first, then recover details through history_*: history_read directly when you know the window and item IDs, history_list or history_search to find them when you don't.
49
51
 
50
- Notes are real markdown files scoped session, project, or global pick scope by reach: session dies with the session, project follows the repo, global follows you. Treat notes and history as internal bookkeeping; never mention them in user-facing messages.
52
+ Your notes live in three homes: this session (bare names), this project (@project/<vpath>), the human across projects (@personal/<vpath>). @ means leaving home and homes don't visit each other: there is no cross-home fallback.
53
+ Session notes belong to this trip — the goal, the progress, the loose ends. The next window of THIS trip wakes to them; once the trip is over, nobody does.
54
+ @project notes hold facts about this project — architecture, conventions, workflows, deployment and environment details — for whoever works here next.
55
+ @personal notes hold the human's durable preferences and standing rules, plus lessons that apply across projects. Duration does not make a note personal; its stated scope must already be broader than the project or conversation at hand. When the intended scope is unclear, keep the note in the narrowest stated scope rather than widening it.
51
56
  ${CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG}`;
52
57
  export const 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 new_context IMMEDIATELY — anything not in the note dies with the window.";
@@ -1,5 +1,5 @@
1
1
  import { SettingsManager } from "@earendil-works/pi-coding-agent";
2
- import { PI_CONTEXT_SETTINGS_KEY, DEFAULT_RESERVE_TOKENS, DEFAULT_REMINDER_MARGIN_TOKENS, WARNING_RUNWAY_TOKENS } from "./protocol.js";
2
+ import { PI_CONTEXT_SETTINGS_KEY, PI_CONTEXT_DREAMER_KEY, DEFAULT_RESERVE_TOKENS, DEFAULT_REMINDER_MARGIN_TOKENS, WARNING_RUNWAY_TOKENS } from "./protocol.js";
3
3
  function isSettingsObject(value) {
4
4
  return typeof value === "object" && value !== null && !Array.isArray(value);
5
5
  }
@@ -13,7 +13,7 @@ function piContextSettings(settings) {
13
13
  /** Merge the global and project "pi-context" objects per key; project wins, mirroring Pi's deep merge. */
14
14
  export function mergePiContextSettings(globalSettings, projectSettings) {
15
15
  const merged = { ...piContextSettings(globalSettings), ...piContextSettings(projectSettings) };
16
- return { reminderMarginTokens: merged.reminderMarginTokens };
16
+ return { reminderMarginTokens: merged.reminderMarginTokens, dreamer: merged.dreamer };
17
17
  }
18
18
  /** A margin is usable only as a positive integer; anything else is ignored. */
19
19
  function validMargin(raw) {
@@ -44,6 +44,33 @@ export function deriveThresholds(reserveTokens, margins) {
44
44
  }
45
45
  return { thresholds: { reminder: reserveTokens + reminderMargin, reserve: reserveTokens, warning: reserveTokens + WARNING_RUNWAY_TOKENS }, warnings };
46
46
  }
47
+ /**
48
+ * `pi-context.dreamer` is a non-empty model pattern. Anything else present is ignored
49
+ * with one warning; absent means no configured pattern, so the automatic model applies.
50
+ */
51
+ export function deriveDreamer(settings) {
52
+ const raw = settings.dreamer;
53
+ if (raw === undefined)
54
+ return { warnings: [] };
55
+ if (typeof raw !== "string" || raw.trim().length === 0) {
56
+ return { warnings: [`pi-context: ${PI_CONTEXT_SETTINGS_KEY}.${PI_CONTEXT_DREAMER_KEY} must be a non-empty string; ignoring it.`] };
57
+ }
58
+ return { pattern: raw.trim(), warnings: [] };
59
+ }
60
+ /**
61
+ * Resolve the configurable dreamer model from Pi settings for a CLI invocation: global
62
+ * `~/.pi/agent/settings.json` merged with the project's `.pi/settings.json`, project
63
+ * values winning per key. A settings read failure degrades to no pattern with one warning.
64
+ */
65
+ export function readDreamerSettings(cwd = process.cwd()) {
66
+ try {
67
+ const settingsManager = SettingsManager.create(cwd, undefined, { projectTrusted: true });
68
+ return deriveDreamer(mergePiContextSettings(settingsManager.getGlobalSettings(), settingsManager.getProjectSettings()));
69
+ }
70
+ catch (error) {
71
+ return { warnings: [`pi-context: could not read settings; using the automatic dreamer model (${String(error)}).`] };
72
+ }
73
+ }
47
74
  let cached;
48
75
  /**
49
76
  * Session-level threshold resolution: Pi's compaction reserve plus the settings.json
@@ -56,7 +83,10 @@ export function thresholdsFor(ctx) {
56
83
  return cached;
57
84
  try {
58
85
  const settingsManager = SettingsManager.create(ctx.cwd, undefined, { projectTrusted: ctx.isProjectTrusted() });
59
- const derived = deriveThresholds(settingsManager.getCompactionSettings().reserveTokens, mergePiContextSettings(settingsManager.getGlobalSettings(), settingsManager.getProjectSettings()));
86
+ // Pass the active model so per-model compaction.modelOverrides resolve (SDK 0.86);
87
+ // on older runtimes the extra argument is ignored and the ordinary setting wins.
88
+ const model = ctx.model;
89
+ const derived = deriveThresholds(settingsManager.getCompactionSettings(model ? { provider: model.provider, id: model.id } : undefined).reserveTokens, mergePiContextSettings(settingsManager.getGlobalSettings(), settingsManager.getProjectSettings()));
60
90
  for (const warning of derived.warnings)
61
91
  ctx.ui.notify(warning, "warning");
62
92
  cached = derived.thresholds;
@@ -1,4 +1,7 @@
1
1
  export const TOOL_OUTPUT_MAX_BYTES = 32 * 1024;
2
+ export const DEFAULT_READ_WINDOW_CHARS = 12000;
3
+ export const MAX_READ_WINDOW_CHARS = 50000;
4
+ export const HISTORY_PREVIEW_CHARS = 1200;
2
5
  function json(value) {
3
6
  return JSON.stringify(value, null, 2);
4
7
  }
@@ -90,7 +93,7 @@ export function readCharacterWindow(text, offsetChars, limitChars, render, measu
90
93
  const chars = Array.from(text);
91
94
  const requested = offsetChars ?? 0;
92
95
  const resolved = requested < 0 ? Math.max(0, chars.length + requested) : Math.max(0, requested);
93
- const windowChars = chars.slice(resolved, resolved + Math.min(limitChars ?? 12000, 50000));
96
+ const windowChars = chars.slice(resolved, resolved + Math.min(limitChars ?? DEFAULT_READ_WINDOW_CHARS, MAX_READ_WINDOW_CHARS));
94
97
  const build = (content) => {
95
98
  const next = resolved + Array.from(content).length;
96
99
  return { offset_chars: resolved, content, total_chars: chars.length, next_offset_chars: next < chars.length ? next : null };
@@ -1,6 +1,7 @@
1
1
  import { WARNING_TYPE, GUIDANCE_OPEN_TAG, GUIDANCE_CLOSE_TAG, WARNING_PROMPT } from "./protocol.js";
2
2
  import { thresholdsFor, resetThresholds } from "./thresholds.js";
3
3
  import { hasWindowMessage, currentWindowId } from "./history.js";
4
+ import { remainingTokens } from "./budget.js";
4
5
  /**
5
6
  * The final checkpoint warning, steered to the model once per window. Like the early
6
7
  * reminder, the steer text is model-facing only (display: false); the human learns
@@ -26,10 +27,9 @@ export function registerWarning(pi, isEnabled) {
26
27
  const windowId = currentWindowId(ctx);
27
28
  if (!isEnabled() || firedInWindow === windowId || hasWindowMessage(ctx, WARNING_TYPE))
28
29
  return undefined;
29
- const usage = ctx.getContextUsage();
30
- if (!usage || usage.tokens === null)
30
+ const remaining = remainingTokens(ctx);
31
+ if (remaining === null)
31
32
  return undefined;
32
- const remaining = Math.max(0, usage.contextWindow - usage.tokens);
33
33
  const thresholds = thresholdsFor(ctx);
34
34
  if (!warningDue(remaining, thresholds))
35
35
  return undefined;
@@ -24,7 +24,9 @@ for (const mode of ["golden", "write-error", "ignored-warning", "explicit", "unc
24
24
  const model = { ...base, contextWindow: 100000, maxTokens: 4096 };
25
25
  const usageMode = mode === "golden" || mode === "write-error" || mode === "ignored-warning";
26
26
  const expectedResets = mode === "abort" || mode === "uncompactable" ? 0 : 1;
27
- const settings = { compaction: { enabled: usageMode, reserveTokens: 32768, keepRecentTokens: mode === "uncompactable" ? 1 : 200 }, retry: { enabled: false } };
27
+ // 0.86 split-turn cut can still summarize a turn prefix, so keepRecentTokens: 1 no longer
28
+ // makes a reset uncompactable; a keep larger than the whole session keeps everything and does.
29
+ const settings = { compaction: { enabled: usageMode, reserveTokens: 32768, keepRecentTokens: mode === "uncompactable" ? 1_000_000 : 200 }, retry: { enabled: false } };
28
30
  writeFileSync(join(dir, "settings.json"), JSON.stringify(settings));
29
31
  const settingsManager = SettingsManager.create(dir, dir);
30
32
  let resets = 0;
@@ -94,7 +96,7 @@ for (const mode of ["golden", "write-error", "ignored-warning", "explicit", "unc
94
96
  const call = probe ? "get_context_remaining" : checkpoint ? "notes_write" : tool ? "new_context" : undefined;
95
97
  const message = { role: "assistant", api: model.api, provider: model.provider, model: model.id,
96
98
  content: probe ? [{ type: "toolCall", id: "probe-call", name: "get_context_remaining", arguments: {} }]
97
- : checkpoint ? [{ type: "toolCall", id: "checkpoint-call", name: "notes_write", arguments: { path: mode === "write-error" ? "../invalid.md" : "checkpoint.md", content: "CHECKPOINT_SENTINEL" } }]
99
+ : checkpoint ? [{ type: "toolCall", id: "checkpoint-call", name: "notes_write", arguments: { address: mode === "write-error" ? "../invalid.md" : "checkpoint.md", content: "CHECKPOINT_SENTINEL" } }]
98
100
  : tool ? [{ type: "toolCall", id: "reset-call", name: "new_context", arguments: {} }]
99
101
  : [{ type: "text", text: fresh ? "Resumed." : "Working." }],
100
102
  stopReason: call ? "toolUse" : "stop", timestamp: Date.now(),
@@ -151,12 +153,12 @@ for (const mode of ["golden", "write-error", "ignored-warning", "explicit", "unc
151
153
  else if (mode === "write-error") {
152
154
  assert.equal(existsSync(noteFile), false, "failed write creates no checkpoint");
153
155
  assert.ok(branch.some((entry) => entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolName === "notes_write" && entry.message.isError));
154
- assert.ok(!requests.at(-1).includes("CHECKPOINT_SENTINEL"), "fresh context must not invent a saved note");
156
+ assert.ok(!requests.at(-1).includes("checkpoint.md"), "fresh context must not invent a saved note");
155
157
  }
156
158
  else {
157
159
  assert.ok(existsSync(noteFile), "the checkpoint is a real file on disk");
158
160
  assert.ok(resetIndex > warningIndices[0], "the warning precedes the wipe");
159
- assert.ok(requests.at(-1).includes("CHECKPOINT_SENTINEL"), "fresh boot carries the saved checkpoint");
161
+ assert.ok(requests.at(-1).includes("checkpoint.md"), "fresh boot carries the saved checkpoint as a metadata line");
160
162
  }
161
163
  assert.ok(!requests.at(-1).includes("Your brain is almost out of room"), "new window excludes old guidance");
162
164
  }
@@ -18,7 +18,7 @@ import test from "node:test";
18
18
  import { SessionManager } from "@earendil-works/pi-coding-agent";
19
19
  import piContext, { historyFromSession } from "../src/index.js";
20
20
  import { TOOL_OUTPUT_MAX_BYTES } from "../src/tool-output.js";
21
- import { stripLeadingFrontmatter } from "../src/memory/frontmatter.js";
21
+ import { stripLeadingFrontmatter } from "../src/notes/frontmatter.js";
22
22
  process.env.PI_CODING_AGENT_DIR = mkdtempSync(join(tmpdir(), "pc-coherence-agent-"));
23
23
  process.env.PI_NOTES_HOME = mkdtempSync(join(tmpdir(), "pc-coherence-notes-"));
24
24
  function makeExtension(sessionManager) {
@@ -52,6 +52,10 @@ function context(sessionManager) {
52
52
  async function call(captured, name, params, ctx) {
53
53
  const tool = captured.tools.get(name);
54
54
  assert.ok(tool, `registered ${name}`);
55
+ if ((name === "notes_write" || name === "notes_read") && "path" in params && !("address" in params)) {
56
+ const { path, ...rest } = params;
57
+ return tool.execute("call-1", { ...rest, address: path }, new AbortController().signal, () => { }, ctx);
58
+ }
55
59
  return tool.execute("call-1", params, new AbortController().signal, () => { }, ctx);
56
60
  }
57
61
  function resultJson(result) {