@astrosheep/pi-context 0.23.1 → 0.25.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 (78) hide show
  1. package/README.md +52 -5
  2. package/dist/build-info.json +4 -0
  3. package/dist/extension.js +1861 -0
  4. package/dist/src/context/budget.js +150 -0
  5. package/dist/src/context/context-window.js +97 -0
  6. package/dist/src/context/prompts.js +94 -0
  7. package/dist/src/context/reset-lifecycle.js +134 -0
  8. package/dist/src/context/runtime.js +236 -0
  9. package/dist/src/context/thresholds.js +62 -0
  10. package/dist/src/dream/cli.js +1 -1
  11. package/dist/src/dream/doctor.js +34 -6
  12. package/dist/src/dream/runner.js +1 -1
  13. package/dist/src/dream/settings.js +30 -0
  14. package/dist/src/{history-tools.js → history/history-tools.js} +3 -3
  15. package/dist/src/{history.js → history/history.js} +8 -46
  16. package/dist/src/index.js +27 -94
  17. package/dist/src/notes/address.js +97 -16
  18. package/dist/src/notes/frontmatter.js +18 -3
  19. package/dist/src/notes/notes-snapshot.js +30 -0
  20. package/dist/src/notes/paths.js +64 -7
  21. package/dist/src/notes/session-replay.js +41 -0
  22. package/dist/src/notes/store.js +76 -22
  23. package/dist/src/notes/tools.js +7 -7
  24. package/dist/src/protocol.js +11 -9
  25. package/dist/src/settings.js +16 -0
  26. package/dist/src/tool-schema.js +1 -1
  27. package/dist/test/agent-loop.test.js +813 -213
  28. package/dist/test/boot.integration.test.js +167 -0
  29. package/dist/test/budget-settings.integration.test.js +126 -0
  30. package/dist/test/doctor.test.js +14 -36
  31. package/dist/test/dream.test.js +37 -380
  32. package/dist/test/helpers/extension.js +393 -0
  33. package/dist/test/history.integration.test.js +316 -0
  34. package/dist/test/notes.integration.test.js +273 -0
  35. package/dist/test/notes.test.js +40 -370
  36. package/dist/test/reset-lifecycle.test.js +248 -180
  37. package/docs/architecture.md +35 -18
  38. package/docs/reset-lifecycle.md +16 -14
  39. package/package.json +11 -10
  40. package/src/context/budget.ts +148 -0
  41. package/src/context/context-window.ts +103 -0
  42. package/src/context/prompts.ts +111 -0
  43. package/src/context/reset-lifecycle.ts +145 -0
  44. package/src/context/runtime.ts +246 -0
  45. package/src/context/thresholds.ts +78 -0
  46. package/src/dream/cli.ts +1 -1
  47. package/src/dream/doctor.ts +27 -6
  48. package/src/dream/runner.ts +1 -1
  49. package/src/dream/settings.ts +32 -0
  50. package/src/{history-tools.ts → history/history-tools.ts} +3 -3
  51. package/src/{history.ts → history/history.ts} +9 -48
  52. package/src/index.ts +27 -89
  53. package/src/notes/address.ts +82 -16
  54. package/src/notes/frontmatter.ts +20 -3
  55. package/src/notes/notes-snapshot.ts +40 -0
  56. package/src/notes/paths.ts +64 -7
  57. package/src/notes/session-replay.ts +53 -0
  58. package/src/notes/store.ts +78 -25
  59. package/src/notes/tools.ts +7 -7
  60. package/src/protocol.ts +11 -9
  61. package/src/settings.ts +20 -0
  62. package/src/tool-schema.ts +1 -2
  63. package/dist/src/budget.js +0 -65
  64. package/dist/src/notes/model.js +0 -101
  65. package/dist/src/prompts.js +0 -88
  66. package/dist/src/reset-lifecycle.js +0 -155
  67. package/dist/src/thresholds.js +0 -102
  68. package/dist/src/warning.js +0 -44
  69. package/dist/test/coherence.test.js +0 -371
  70. package/dist/test/history.test.js +0 -26
  71. package/dist/test/integration.test.js +0 -1775
  72. package/dist/test/pagination.property.test.js +0 -471
  73. package/src/budget.ts +0 -67
  74. package/src/notes/model.ts +0 -109
  75. package/src/prompts.ts +0 -91
  76. package/src/reset-lifecycle.ts +0 -173
  77. package/src/thresholds.ts +0 -110
  78. package/src/warning.ts +0 -46
@@ -0,0 +1,1861 @@
1
+ // <define:__PI_CONTEXT_BUILD__>
2
+ var define_PI_CONTEXT_BUILD_default = { version: "0.25.0", sourceHash: "8a2495924871c544d2919a9301b6017c61ebe0c4bc38242b7e8c5b95d50f8df3" };
3
+
4
+ // src/index.ts
5
+ import { VERSION as VERSION2 } from "@earendil-works/pi-coding-agent";
6
+
7
+ // src/history/history-tools.ts
8
+ import { Type as Type2 } from "@earendil-works/pi-ai";
9
+ import { defineTool } from "@earendil-works/pi-coding-agent";
10
+
11
+ // src/tool-output.ts
12
+ var TOOL_OUTPUT_MAX_BYTES = 32 * 1024;
13
+ var DEFAULT_READ_WINDOW_CHARS = 12e3;
14
+ var MAX_READ_WINDOW_CHARS = 5e4;
15
+ var HISTORY_PREVIEW_CHARS = 1200;
16
+ function json(value) {
17
+ return JSON.stringify(value, null, 2);
18
+ }
19
+ function withinBudget(value, budget = TOOL_OUTPUT_MAX_BYTES) {
20
+ return Buffer.byteLength(json(value), "utf8") <= budget;
21
+ }
22
+ function withinTextBudget(text, budget = TOOL_OUTPUT_MAX_BYTES) {
23
+ return Buffer.byteLength(text, "utf8") <= budget;
24
+ }
25
+ function truncationMarker(removedChars) {
26
+ return `\u2026[truncated ${removedChars} chars]\u2026`;
27
+ }
28
+ function middleTruncate(text, fits) {
29
+ if (fits(text)) return text;
30
+ const chars = Array.from(text);
31
+ const build = (kept) => {
32
+ const head = Math.ceil(kept / 2);
33
+ return chars.slice(0, head).join("") + truncationMarker(chars.length - kept) + chars.slice(chars.length - (kept - head)).join("");
34
+ };
35
+ let low = 0;
36
+ let high = chars.length;
37
+ while (low < high) {
38
+ const mid = Math.ceil((low + high) / 2);
39
+ if (fits(build(mid))) low = mid;
40
+ else high = mid - 1;
41
+ }
42
+ return build(low);
43
+ }
44
+ function prefixFit(text, fits) {
45
+ if (fits(text)) return text;
46
+ const chars = Array.from(text);
47
+ let low = 0;
48
+ let high = chars.length;
49
+ while (low < high) {
50
+ const mid = Math.ceil((low + high) / 2);
51
+ if (fits(chars.slice(0, mid).join(""))) low = mid;
52
+ else high = mid - 1;
53
+ }
54
+ while (low > 0 && !fits(chars.slice(0, low).join(""))) low -= 1;
55
+ return chars.slice(0, low).join("");
56
+ }
57
+ function readCharacterWindow(text, offsetChars, limitChars, render, measure = withinBudget) {
58
+ const chars = Array.from(text);
59
+ const requested = offsetChars ?? 0;
60
+ const resolved = requested < 0 ? Math.max(0, chars.length + requested) : Math.max(0, requested);
61
+ const windowChars = chars.slice(resolved, resolved + Math.min(limitChars ?? DEFAULT_READ_WINDOW_CHARS, MAX_READ_WINDOW_CHARS));
62
+ const build = (content2) => {
63
+ const next = resolved + Array.from(content2).length;
64
+ return { offset_chars: resolved, content: content2, total_chars: chars.length, next_offset_chars: next < chars.length ? next : null };
65
+ };
66
+ const content = prefixFit(windowChars.join(""), (candidate) => measure(render(build(candidate))));
67
+ return render(build(content));
68
+ }
69
+ function readWindowBlock(identity, window) {
70
+ const end = window.offset_chars + Array.from(window.content).length;
71
+ const next = window.next_offset_chars === null ? "null" : String(window.next_offset_chars);
72
+ const fields = identity.map(([name, value]) => `${name}: ${value}`).join("\n");
73
+ return `--- READ WINDOW ---
74
+ ${fields}
75
+ chars: [${window.offset_chars},${end}) of ${window.total_chars}
76
+ next_offset_chars: ${next}
77
+ `;
78
+ }
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
+ function page(items, cursor2, key, limit, truncate) {
89
+ const end = Math.min(items.length, cursor2 + (limit ?? items.length));
90
+ const selected = [];
91
+ let next = end < items.length ? end : null;
92
+ for (let index = cursor2; index < end; index++) {
93
+ const candidateNext = index + 1 < end || end < items.length ? index + 1 : null;
94
+ const fits = (list) => withinBudget({ [key]: list, next_cursor: candidateNext });
95
+ if (!fits([...selected, items[index]])) {
96
+ if (selected.length === 0 && truncate) {
97
+ selected.push(truncate(items[index], (candidate) => fits([candidate])));
98
+ next = candidateNext;
99
+ } else {
100
+ next = index;
101
+ }
102
+ break;
103
+ }
104
+ selected.push(items[index]);
105
+ }
106
+ return { [key]: selected, next_cursor: next };
107
+ }
108
+ function output(value, details, terminate = false) {
109
+ return { content: [{ type: "text", text: json(value) }], details, terminate };
110
+ }
111
+ function outputRaw(header, content, details, terminate = false) {
112
+ return { content: [{ type: "text", text: `${header}
113
+ ${content}` }], details, terminate };
114
+ }
115
+
116
+ // src/tool-schema.ts
117
+ import { Type } from "@earendil-works/pi-ai";
118
+ var nullableString = () => Type.Optional(Type.Union([Type.String(), Type.Null()]));
119
+ var positiveInteger = () => Type.Optional(Type.Integer({ minimum: 1 }));
120
+ var cursor = () => Type.Optional(Type.Integer({ minimum: 0, description: "Continuation cursor: pass the previous next_cursor back unchanged, with the same filters and ordering. Omit to start. next_cursor is null only when the set is exhausted." }));
121
+ var recentFirst = () => Type.Optional(Type.Boolean({ description: "Return newest-first. Only an explicit false returns oldest-first. Defaults to true." }));
122
+ var role = Type.Union([Type.Literal("user"), Type.Literal("assistant"), Type.Literal("tool_call"), Type.Literal("tool"), Type.Literal("system"), Type.Literal("developer"), Type.Null()], { description: `Filter by the item's role. Exactly six: "user" and "assistant" are a message's visible text (assistant text never contains tool calls); "tool_call" is one tool invocation (tool_name set, content = the call's JSON arguments); "tool" is one tool run's output (tool_name set); "system" is a native Pi compaction or branch summary; "developer" is an entry this extension authored (boot, guidance, warning, continuation messages, or any pi-context/* custom message).` });
123
+ var searchQuery = () => Type.Union([Type.String(), Type.Array(Type.String(), { minItems: 1 })]);
124
+ function searchQueries(query) {
125
+ const candidates = typeof query === "string" ? [query] : query;
126
+ if (!Array.isArray(candidates) || candidates.length === 0) throw new Error("query must be a string or a non-empty array of strings");
127
+ if (!candidates.every((candidate) => typeof candidate === "string")) throw new Error("query array elements must be strings");
128
+ if (candidates.some((candidate) => candidate === "")) throw new Error("query strings must be non-empty: an empty query matches everything");
129
+ return candidates;
130
+ }
131
+
132
+ // src/context/context-window.ts
133
+ import { getCurrentSystemMessage } from "@earendil-works/pi-ai";
134
+
135
+ // node_modules/@earendil-works/pi-ai/dist/utils/text.js
136
+ function contentText(content, separator = "\n") {
137
+ if (typeof content === "string")
138
+ return content;
139
+ return content.filter((block) => block.type === "text").map((block) => block.text).join(separator);
140
+ }
141
+ function getSystemMessageText(message) {
142
+ const parts = [contentText(message.content)];
143
+ for (const text of Object.values(message.sections ?? {})) {
144
+ if (text !== null)
145
+ parts.push(text);
146
+ }
147
+ return parts.filter((part) => part.length > 0).join("\n\n");
148
+ }
149
+
150
+ // node_modules/@earendil-works/pi-ai/dist/utils/estimate.js
151
+ var CHARS_PER_TOKEN = 4;
152
+ var ESTIMATED_IMAGE_CHARS = 4800;
153
+ function calculateContextTokens(usage) {
154
+ return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
155
+ }
156
+ function safeJsonStringify(value) {
157
+ try {
158
+ return JSON.stringify(value) ?? "undefined";
159
+ } catch {
160
+ return "[unserializable]";
161
+ }
162
+ }
163
+ function estimateTextAndImageContentChars(content) {
164
+ if (typeof content === "string")
165
+ return content.length;
166
+ let chars = 0;
167
+ for (const block of content)
168
+ chars += block.type === "text" ? block.text.length : ESTIMATED_IMAGE_CHARS;
169
+ return chars;
170
+ }
171
+ function estimateTextTokens(text) {
172
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
173
+ }
174
+ function estimateTextAndImageContentTokens(content) {
175
+ return Math.ceil(estimateTextAndImageContentChars(content) / CHARS_PER_TOKEN);
176
+ }
177
+ function estimateMessageTokens(message) {
178
+ let chars = 0;
179
+ if (message.role === "system") {
180
+ return estimateTextTokens(getSystemMessageText(message)) + estimateToolsTokens(message.toolsAdded) + estimateToolsTokens(message.toolsRemoved);
181
+ }
182
+ if (message.role === "user")
183
+ return estimateTextAndImageContentTokens(message.content);
184
+ if (message.role === "toolResult")
185
+ return estimateTextAndImageContentTokens(message.content);
186
+ for (const block of message.content) {
187
+ if (block.type === "text") {
188
+ chars += block.text.length;
189
+ } else if (block.type === "thinking") {
190
+ chars += block.thinking.length;
191
+ } else {
192
+ chars += block.name.length + safeJsonStringify(block.arguments).length;
193
+ }
194
+ }
195
+ return Math.ceil(chars / CHARS_PER_TOKEN);
196
+ }
197
+ function getLastAssistantUsageInfo(messages) {
198
+ let latestPrefixTimestamp = Number.NEGATIVE_INFINITY;
199
+ let usageInfo;
200
+ for (let i = 0; i < messages.length; i++) {
201
+ const message = messages[i];
202
+ if (message.role === "assistant") {
203
+ const assistant = message;
204
+ const usageAppliesToPrefix = assistant.timestamp >= latestPrefixTimestamp;
205
+ if (usageAppliesToPrefix && assistant.stopReason !== "aborted" && assistant.stopReason !== "error" && calculateContextTokens(assistant.usage) > 0) {
206
+ usageInfo = { usage: assistant.usage, index: i };
207
+ }
208
+ }
209
+ latestPrefixTimestamp = Math.max(latestPrefixTimestamp, message.timestamp);
210
+ }
211
+ return usageInfo;
212
+ }
213
+ function estimateContextTokens(context) {
214
+ const messages = "messages" in context ? context.messages : context;
215
+ const usageInfo = getLastAssistantUsageInfo(messages);
216
+ if (usageInfo) {
217
+ const usageTokens = calculateContextTokens(usageInfo.usage);
218
+ let trailingTokens = 0;
219
+ for (let i = usageInfo.index + 1; i < messages.length; i++) {
220
+ trailingTokens += estimateMessageTokens(messages[i]);
221
+ }
222
+ return { tokens: usageTokens + trailingTokens, usageTokens, trailingTokens, lastUsageIndex: usageInfo.index };
223
+ }
224
+ let tokens = 0;
225
+ for (const message of messages)
226
+ tokens += estimateMessageTokens(message);
227
+ return { tokens, usageTokens: 0, trailingTokens: tokens, lastUsageIndex: null };
228
+ }
229
+ function estimateToolsTokens(tools) {
230
+ if (!tools || tools.length === 0)
231
+ return 0;
232
+ return estimateTextTokens(safeJsonStringify(tools));
233
+ }
234
+
235
+ // src/context/context-window.ts
236
+ import { convertToLlm } from "@earendil-works/pi-coding-agent";
237
+
238
+ // src/protocol.ts
239
+ var NOTE_TYPE = "pi-context/note";
240
+ var BOOT_TYPE = "pi-context/boot";
241
+ var GUIDANCE_TYPE = "pi-context/guidance";
242
+ var WARNING_TYPE = "pi-context/warning";
243
+ var RESET_MARKER_TYPE = "pi-context/reset-marker";
244
+ var CONTINUATION_TYPE = "pi-context/continuation";
245
+ var MAX_NOTE_BYTES = 1e6;
246
+ var POCKET_SESSION_LIMIT = 5;
247
+ var POCKET_PROJECT_LIMIT = 2;
248
+ var POCKET_HUMAN_LIMIT = 2;
249
+ var POCKET_AGENT_LIMIT = 1;
250
+ var POCKET_MODEL_LIMIT = 1;
251
+ var MAX_NOTE_PATH_BYTES = 512;
252
+ var CONTEXT_WINDOW_OPEN_TAG = "<context_window>";
253
+ var CONTEXT_WINDOW_CLOSE_TAG = "</context_window>";
254
+ var CONTEXT_WINDOW_PROTOCOL_OPEN_TAG = "<context_window_protocol>";
255
+ var CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG = "</context_window_protocol>";
256
+ var GUIDANCE_OPEN_TAG = "<context_window_guidance>";
257
+ var GUIDANCE_CLOSE_TAG = "</context_window_guidance>";
258
+ var PI_CONTEXT_SETTINGS_KEY = "pi-context";
259
+ var DEFAULT_RESERVE_TOKENS = 16384;
260
+ var DEFAULT_REMINDER_MARGIN_TOKENS = 24576;
261
+ var WARNING_RUNWAY_TOKENS = 12288;
262
+ var RESET_SUMMARY = "You wake up. Your head is empty \u2014 no memories, the past a blank. The memory is gone for good. What outlived it: the notes you wrote, and the history that was recorded. They are not your memory \u2014 read them to rebuild what you need.";
263
+ var CONTINUATION = "Your memory was just erased. Pull only the details you need from history_* and notes_*, then get back to work.";
264
+ var PROTOCOL_BLOCK = `${CONTEXT_WINDOW_PROTOCOL_OPEN_TAG}
265
+ Your memory resets whenever the context window fills; only what you wrote down survives. Two things outlive every window in this session: the notes you wrote, and the history that was recorded. Neither is memory \u2014 both are record. Write notes with notes_write, revise them with notes_edit, and read them back with notes_read / notes_search / notes_list; history is read-only through the history_* tools. Everything else wakes blank.
266
+ Mark outdated or unneeded notes stale \u2014 leave them, and they will keep misleading you.
267
+
268
+ Keep a running checkpoint while you work, not at the last minute \u2014 the next window wakes knowing nothing about the work: 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. history_list returns those IDs; history_read pulls the exact item back out. Bookmark anything expensive the same way \u2014 a window/item ID beats re-running or re-searching.
269
+
270
+ Use get_context_remaining to see how much of the window is left. When it runs out, this window is gone \u2014 with no final turn at the limit \u2014 and you continue in a fresh one, recovering only through notes_* and history_*. Once your checkpoint is written, you can call wipe_memory yourself instead of waiting for the erase. Do not let a window die undocumented.
271
+
272
+ 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.
273
+
274
+ Notes live in five homes, and the word after @ is always one of their reserved names \u2014 your own name and other people's names live at the second level (@agents/faye/, never @faye/). Bare names are this session; @project/<vpath> is this project's workspace; @human/<vpath> is the human's cross-project home; @self/<vpath> and @agents/<name>/<vpath> are agent homes; @model/<vpath> and @models/<name>/<vpath> are model homes. @self and @model are the only relative forms \u2014 the current agent, the current model \u2014 and listings never show them, only the resolved name. There is no cross-home fallback.
275
+ Session notes belong to this trip \u2014 the goal, the progress, the loose ends. The next window of THIS trip wakes to them; once the trip is over, nobody does.
276
+ @project notes hold facts about this project \u2014 architecture, conventions, workflows, deployment and environment details \u2014 for whoever works here next.
277
+ @human notes hold the human's durable preferences and standing rules, plus lessons that apply across projects \u2014 for every agent that serves this human, whoever is running. You write there as the human's scribe; what the human dictates carries origin: user. When the intended scope is unclear, keep the note in the narrowest stated scope rather than widening it.
278
+ @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.
279
+ @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.
280
+ ${CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG}`;
281
+ 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
+
283
+ // src/context/context-window.ts
284
+ function isWindowMarker(entry) {
285
+ return entry.type === "custom" && entry.customType === RESET_MARKER_TYPE && typeof entry.data === "object" && entry.data !== null && typeof entry.data.windowId === "string" && entry.data.windowId.length > 0;
286
+ }
287
+ function currentReset(ctx) {
288
+ const branch = ctx.sessionManager.getBranch();
289
+ for (let i = branch.length - 1; i >= 0; i--) {
290
+ const entry = branch[i];
291
+ if (entry && isWindowMarker(entry)) return entry;
292
+ }
293
+ return void 0;
294
+ }
295
+ function rootWindowId(sessionId2) {
296
+ return `pcw:${sessionId2.slice(0, 8)}:root`;
297
+ }
298
+ function hasWindowMessage(ctx, customType) {
299
+ const branch = ctx.sessionManager.getBranch();
300
+ for (let i = branch.length - 1; i >= 0; i--) {
301
+ const entry = branch[i];
302
+ if (isWindowMarker(entry)) break;
303
+ if (entry.type === "custom_message" && entry.customType === customType) return true;
304
+ }
305
+ return false;
306
+ }
307
+ function currentWindowId(ctx) {
308
+ return currentReset(ctx)?.data.windowId ?? rootWindowId(ctx.sessionManager.getSessionId());
309
+ }
310
+ function hasWindowId(details, windowId) {
311
+ return typeof details === "object" && details !== null && typeof details.windowId === "string" && details.windowId === windowId;
312
+ }
313
+ function isWindowBoot(message, windowId) {
314
+ return message.role === "custom" && message.customType === BOOT_TYPE && (windowId === void 0 || hasWindowId(message.details, windowId));
315
+ }
316
+ function projectWindow(messages, windowId) {
317
+ const cut = messages.findIndex((message) => isWindowBoot(message, windowId));
318
+ if (cut < 0) throw new Error(`Missing boot for context window ${windowId}`);
319
+ const head = getCurrentSystemMessage(messages.slice(0, cut));
320
+ const suffix = messages.slice(cut);
321
+ return head ? [head, ...suffix] : suffix;
322
+ }
323
+ function projectRootWindow(messages, windowId) {
324
+ const matching = messages.filter((message) => isWindowBoot(message, windowId));
325
+ if (matching.length === 0) return messages;
326
+ const activeBoot = matching[matching.length - 1];
327
+ const firstBoot = messages.findIndex((message) => isWindowBoot(message));
328
+ const withoutBoots = messages.filter((message) => !isWindowBoot(message));
329
+ return [...withoutBoots.slice(0, firstBoot), activeBoot, ...withoutBoots.slice(firstBoot)];
330
+ }
331
+ function windowUsage(ctx) {
332
+ const reset = currentReset(ctx);
333
+ if (!reset) return ctx.getContextUsage();
334
+ const contextWindow = ctx.model?.contextWindow ?? ctx.getContextUsage()?.contextWindow;
335
+ if (!contextWindow) return void 0;
336
+ const windowId = reset.data.windowId;
337
+ try {
338
+ const messages = projectWindow(ctx.sessionManager.buildSessionProjection().messages, windowId);
339
+ const { tokens } = estimateContextTokens(convertToLlm(messages));
340
+ return { tokens, contextWindow, percent: tokens / contextWindow * 100 };
341
+ } catch {
342
+ return void 0;
343
+ }
344
+ }
345
+
346
+ // src/history/history.ts
347
+ function isTextContent(part) {
348
+ return typeof part === "object" && part !== null && part.type === "text" && typeof part.text === "string";
349
+ }
350
+ function contentText2(content) {
351
+ if (typeof content === "string") return content;
352
+ return Array.isArray(content) ? content.filter(isTextContent).map((part) => part.text).join("\n") : "";
353
+ }
354
+ function mapRole(role2) {
355
+ if (role2 === "user" || role2 === "assistant") return role2;
356
+ if (role2 === "toolResult" || role2 === "bashExecution") return "tool";
357
+ if (role2 === "custom") return "user";
358
+ if (role2 === "compactionSummary" || role2 === "branchSummary") return "system";
359
+ return void 0;
360
+ }
361
+ var PI_CONTEXT_ENTRY_PREFIX = "pi-context/";
362
+ function messageContent(message) {
363
+ switch (message.role) {
364
+ case "bashExecution":
365
+ return message.output ? `${message.command}
366
+ ${message.output}` : message.command;
367
+ case "branchSummary":
368
+ case "compactionSummary":
369
+ return message.summary;
370
+ default:
371
+ return contentText2(message.content);
372
+ }
373
+ }
374
+ function toolInfo(message) {
375
+ if (message.role === "bashExecution") {
376
+ return { toolName: "bash", outputTruncated: message.truncated || void 0, fullOutputPath: message.truncated ? message.fullOutputPath : void 0 };
377
+ }
378
+ if (message.role !== "toolResult") return {};
379
+ return { toolName: message.toolName, toolError: message.isError === true ? true : void 0 };
380
+ }
381
+ function toolCallItems(windowId, entry, message) {
382
+ if (message.role !== "assistant" || !Array.isArray(message.content)) return [];
383
+ const items = [];
384
+ let callIndex = 0;
385
+ for (const part of message.content) {
386
+ if (typeof part !== "object" || part === null || part.type !== "toolCall") continue;
387
+ const call = part;
388
+ items.push({
389
+ windowId,
390
+ itemId: `${entry.id}#${callIndex++}`,
391
+ role: "tool_call",
392
+ content: JSON.stringify(call.arguments),
393
+ createdAt: entry.timestamp,
394
+ toolName: call.name
395
+ });
396
+ }
397
+ return items;
398
+ }
399
+ function historyFromSession(ctx) {
400
+ const sessionId2 = ctx.sessionManager.getSessionId();
401
+ let window = { windowId: rootWindowId(sessionId2), items: [] };
402
+ const windows = [window];
403
+ for (const entry of ctx.sessionManager.getBranch()) {
404
+ if (isWindowMarker(entry)) {
405
+ window = { windowId: entry.data.windowId, createdAt: entry.timestamp, items: [] };
406
+ windows.push(window);
407
+ continue;
408
+ }
409
+ if (entry.type === "compaction" || entry.type === "branch_summary") {
410
+ window.items.push({
411
+ windowId: window.windowId,
412
+ itemId: entry.id,
413
+ role: "system",
414
+ content: entry.summary,
415
+ createdAt: entry.timestamp
416
+ });
417
+ continue;
418
+ }
419
+ if (entry.type === "message") {
420
+ const role2 = mapRole(entry.message.role);
421
+ if (!role2) continue;
422
+ window.items.push({
423
+ windowId: window.windowId,
424
+ itemId: entry.id,
425
+ role: role2,
426
+ content: messageContent(entry.message),
427
+ createdAt: entry.timestamp,
428
+ ...toolInfo(entry.message)
429
+ });
430
+ window.items.push(...toolCallItems(window.windowId, entry, entry.message));
431
+ continue;
432
+ }
433
+ if (entry.type === "custom_message") {
434
+ window.items.push({
435
+ windowId: window.windowId,
436
+ itemId: entry.id,
437
+ // Only entries this extension wrote are its own; every foreign custom message stays a user turn.
438
+ role: entry.customType.startsWith(PI_CONTEXT_ENTRY_PREFIX) ? "developer" : "user",
439
+ content: contentText2(entry.content),
440
+ createdAt: entry.timestamp
441
+ });
442
+ }
443
+ }
444
+ return windows;
445
+ }
446
+ function visibleItem(item, maxChars = HISTORY_PREVIEW_CHARS) {
447
+ const characters = Array.from(item.content);
448
+ const truncated = characters.length > maxChars;
449
+ return {
450
+ window_id: item.windowId,
451
+ item_id: item.itemId,
452
+ role: item.role,
453
+ tool_name: item.toolName ?? null,
454
+ // Surfaced only when set: a truncated bash run names its full-output path, and an
455
+ // errored tool run says so. Absent keys mean nothing special happened.
456
+ ...item.outputTruncated ? { output_truncated: true, full_output_path: item.fullOutputPath ?? null } : {},
457
+ ...item.toolError ? { tool_error: true } : {},
458
+ truncated,
459
+ total_chars: characters.length,
460
+ // A truncated payload is a plain prefix: no synthetic marker is appended, and
461
+ // `total_chars` names exactly how many code points were left out.
462
+ truncated_content: truncated ? characters.slice(0, maxChars).join("") : item.content
463
+ };
464
+ }
465
+ function allItems(ctx) {
466
+ return historyFromSession(ctx).flatMap((window) => window.items);
467
+ }
468
+ function unknownWindowId(ctx, params) {
469
+ if (typeof params.window_id !== "string") return void 0;
470
+ const known = historyFromSession(ctx).map((window) => window.windowId);
471
+ return known.includes(params.window_id) ? void 0 : { message: `unknown window_id "${params.window_id}"`, known };
472
+ }
473
+ function vacuousRoleToolCombo(params) {
474
+ if (typeof params.tool_name === "string" && typeof params.role === "string" && params.role !== "tool_call" && params.role !== "tool") {
475
+ return `tool_name is only set on "tool_call" and "tool" items; role "${params.role}" never carries one`;
476
+ }
477
+ return void 0;
478
+ }
479
+ function filteredItems(ctx, params) {
480
+ let items = allItems(ctx);
481
+ if (typeof params.window_id === "string") items = items.filter((item) => item.windowId === params.window_id);
482
+ if (typeof params.role === "string") items = items.filter((item) => item.role === params.role);
483
+ if (typeof params.tool_name === "string") items = items.filter((item) => item.toolName === params.tool_name);
484
+ if (params.recent_first !== false) items.reverse();
485
+ return items;
486
+ }
487
+
488
+ // src/history/history-tools.ts
489
+ function truncateHistoryItem(item, fits) {
490
+ if (fits(item)) return item;
491
+ const shrinkContent = (base) => ({
492
+ ...base,
493
+ truncated: true,
494
+ truncated_content: prefixFit(base.truncated_content, (candidate) => fits({ ...base, truncated: true, truncated_content: candidate }))
495
+ });
496
+ const withContent = shrinkContent(item);
497
+ if (fits(withContent)) return withContent;
498
+ if (item.tool_name === null) return withContent;
499
+ const withName = { ...item, tool_name: middleTruncate(item.tool_name, (candidate) => fits({ ...item, tool_name: candidate })) };
500
+ if (fits(withName)) return withName;
501
+ return shrinkContent(withName);
502
+ }
503
+ function registerHistoryTools(pi) {
504
+ pi.registerTool(defineTool({
505
+ name: "history_windows",
506
+ label: "History list windows",
507
+ description: "List durable Pi session-history windows.",
508
+ parameters: Type2.Object({ limit: positiveInteger(), recent_first: recentFirst() }, { additionalProperties: false }),
509
+ async execute(_id, params, _signal, _update, ctx) {
510
+ let windows = historyFromSession(ctx);
511
+ if (params.recent_first !== false) windows = [...windows].reverse();
512
+ const limit = params.limit ?? windows.length;
513
+ return output({ windows: windows.slice(0, limit).map((window) => ({ window_id: window.windowId, item_count: window.items.length })) });
514
+ }
515
+ }));
516
+ pi.registerTool(defineTool({
517
+ name: "history_list",
518
+ label: "History list items",
519
+ description: "List durable session items, including items from earlier reset windows, using opaque item and window IDs; native compaction and branch summaries remain history items in their current window. The role parameter's description enumerates the six roles. Every item carries truncated and total_chars: when truncated is true, truncated_content is a plain prefix of the item's content with no marker, and total_chars is its full code-point length. max_chars_per_item: 1 therefore yields pure addresses you can resolve with history_read.",
520
+ parameters: Type2.Object({ limit: positiveInteger(), cursor: cursor(), recent_first: recentFirst(), role: Type2.Optional(role), tool_name: nullableString(), window_id: nullableString(), max_chars_per_item: positiveInteger() }, { additionalProperties: false }),
521
+ async execute(_id, params, _signal, _update, ctx) {
522
+ const invalid = vacuousRoleToolCombo(params);
523
+ if (invalid) return output({ error: invalid, role: params.role, tool_name: params.tool_name });
524
+ const badWindow = unknownWindowId(ctx, params);
525
+ if (badWindow) return output({ error: badWindow.message, window_id: params.window_id, known_windows: badWindow.known });
526
+ const items = filteredItems(ctx, params).map((item) => visibleItem(item, params.max_chars_per_item ?? HISTORY_PREVIEW_CHARS));
527
+ return output(page(items, params.cursor ?? 0, "items", params.limit, truncateHistoryItem));
528
+ }
529
+ }));
530
+ pi.registerTool(defineTool({
531
+ name: "history_read",
532
+ label: "History read item",
533
+ description: "Read a bounded character range from one session item. Each response delivers the longest contiguous prefix of the requested window that fits the wire budget: follow the resume cursor to reconstruct the item exactly. A negative offset_chars counts back from the item's end. Offsets and counts are code points (an emoji or CJK character counts as one). The response begins with the shared READ WINDOW block naming window_id and item_id; concatenate only the content after that block to reconstruct the item.",
534
+ parameters: Type2.Object({ item_id: Type2.String(), offset_chars: Type2.Optional(Type2.Integer({ description: "Code-point offset to start from. A negative value counts back from the end; the response echoes the resolved absolute offset. Pass the previous next_offset_chars back unchanged to continue." })), limit_chars: Type2.Optional(Type2.Integer({ minimum: 1, maximum: MAX_READ_WINDOW_CHARS, description: `Largest requested window in code points (default ${DEFAULT_READ_WINDOW_CHARS}). A window too large for the wire budget is cut short; next_offset_chars names where the next read resumes.` })), window_id: Type2.String() }, { additionalProperties: false }),
535
+ async execute(_id, params, _signal, _update, ctx) {
536
+ const item = allItems(ctx).find((candidate) => candidate.windowId === params.window_id && candidate.itemId === params.item_id);
537
+ if (!item) return output({ error: "unknown item_id or window_id" });
538
+ const totalChars = Array.from(item.content).length;
539
+ if (typeof params.offset_chars === "number" && params.offset_chars > totalChars) {
540
+ return output({ error: `offset_chars ${params.offset_chars} is past the end: the item has ${totalChars} chars; the largest legal offset is ${totalChars} (an empty end-read)`, window_id: item.windowId, item_id: item.itemId, offset_chars: params.offset_chars, total_chars: totalChars });
541
+ }
542
+ return readCharacterWindow(item.content, params.offset_chars, params.limit_chars, (window) => {
543
+ const { content, ...cursor2 } = window;
544
+ return outputRaw(readWindowBlock([["window_id", item.windowId], ["item_id", item.itemId]], window), content, { window_id: item.windowId, item_id: item.itemId, ...cursor2 });
545
+ }, (result) => withinTextBudget(result.content[0].text));
546
+ }
547
+ }));
548
+ pi.registerTool(defineTool({
549
+ name: "history_search",
550
+ label: "History search",
551
+ description: `Case-sensitive literal substring search over durable Pi session history; query accepts one string or an array of strings, an item matches when it contains any of them (OR), and each item appears once. No semantic search. Invocations and outputs are separate items (roles "tool_call" and "tool"), so both are searchable; the role parameter's description enumerates all six. Each hit carries truncated and total_chars plus match_offset_chars: the code-point offset of the earliest query occurrence in the item's full content. With max_chars_per_item: 1 the page is an address list; resolve an address with history_read at match_offset_chars.`,
552
+ parameters: Type2.Object({ limit: positiveInteger(), cursor: cursor(), query: searchQuery(), recent_first: recentFirst(), role: Type2.Optional(role), tool_name: nullableString(), window_id: nullableString(), max_chars_per_item: positiveInteger() }, { additionalProperties: false }),
553
+ async execute(_id, params, _signal, _update, ctx) {
554
+ const invalid = vacuousRoleToolCombo(params);
555
+ if (invalid) return output({ error: invalid, role: params.role, tool_name: params.tool_name });
556
+ const badWindow = unknownWindowId(ctx, params);
557
+ if (badWindow) return output({ error: badWindow.message, window_id: params.window_id, known_windows: badWindow.known });
558
+ const queries = searchQueries(params.query);
559
+ const matching = filteredItems(ctx, params).filter((item) => queries.some((query) => item.content.includes(query))).map((item) => ({ ...visibleItem(item, params.max_chars_per_item ?? HISTORY_PREVIEW_CHARS), match_offset_chars: earliestMatchOffsetChars(item.content, queries) }));
560
+ return output(page(matching, params.cursor ?? 0, "items", params.limit, truncateHistoryItem));
561
+ }
562
+ }));
563
+ }
564
+
565
+ // src/notes/tools.ts
566
+ import { Type as Type3 } from "@earendil-works/pi-ai";
567
+ import { defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
568
+
569
+ // src/notes/frontmatter.ts
570
+ var SCOPES = ["session", "project", "human", "agent", "model"];
571
+ var ORIGINS = ["user", "self", "external"];
572
+ var STATUSES = ["active", "superseded", "pending", "archived"];
573
+ var TIMESTAMP_KEYS = ["created_at", "updated_at", "last_accessed"];
574
+ var KNOWN_KEYS = ["origin", "status", "stale", "created_at", "updated_at", "last_accessed", "access_count", "source_window", "supersedes", "recurrence_count", "recurrence_windows"];
575
+ var pad2 = (value) => String(value).padStart(2, "0");
576
+ function localIso(epochMs) {
577
+ const date = new Date(epochMs);
578
+ const offsetMinutes = -date.getTimezoneOffset();
579
+ const absOffset = Math.abs(offsetMinutes);
580
+ const offset = `${offsetMinutes < 0 ? "-" : "+"}${pad2(Math.floor(absOffset / 60))}:${pad2(absOffset % 60)}`;
581
+ const wallClock = `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}T${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}.${String(date.getMilliseconds()).padStart(3, "0")}`;
582
+ return `${wallClock}${offset}`;
583
+ }
584
+ function isScope(value) {
585
+ return typeof value === "string" && SCOPES.includes(value);
586
+ }
587
+ function isOrigin(value) {
588
+ return typeof value === "string" && ORIGINS.includes(value);
589
+ }
590
+ function isStatus(value) {
591
+ return typeof value === "string" && STATUSES.includes(value);
592
+ }
593
+ function toEpoch(value, fallback) {
594
+ if (typeof value === "number" && Number.isFinite(value)) return value;
595
+ if (typeof value === "string") {
596
+ const parsed = Date.parse(value);
597
+ if (Number.isFinite(parsed)) return parsed;
598
+ }
599
+ return fallback;
600
+ }
601
+ function parseScalar(text) {
602
+ const trimmed = text.trim();
603
+ if (trimmed === "") return "";
604
+ try {
605
+ return JSON.parse(trimmed);
606
+ } catch {
607
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1);
608
+ return trimmed;
609
+ }
610
+ }
611
+ function parseFrontmatter(raw) {
612
+ const stripped = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
613
+ const lines = stripped.split("\n").map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
614
+ if (lines[0]?.trim() !== "---") return { fields: {}, body: raw };
615
+ let close = -1;
616
+ for (let index = 1; index < lines.length; index++) {
617
+ if (lines[index]?.trim() === "---") {
618
+ close = index;
619
+ break;
620
+ }
621
+ }
622
+ if (close === -1) return { fields: {}, body: raw };
623
+ const fields = {};
624
+ for (let index = 1; index < close; index++) {
625
+ const line = lines[index];
626
+ const match = /^([A-Za-z_][A-Za-z0-9_-]*):(.*)$/.exec(line);
627
+ if (!match) continue;
628
+ const key = match[1];
629
+ const rest2 = match[2];
630
+ if (rest2.trim() === "") {
631
+ const items = [];
632
+ while (index + 1 < close && /^\s*-\s+/.test(lines[index + 1])) {
633
+ index++;
634
+ items.push(parseScalar(lines[index].replace(/^\s*-\s+/, "")));
635
+ }
636
+ fields[key] = items;
637
+ } else {
638
+ fields[key] = parseScalar(rest2);
639
+ }
640
+ }
641
+ const rest = lines.slice(close + 1);
642
+ if (rest[0] === "") rest.shift();
643
+ return { fields, body: rest.join("\n") };
644
+ }
645
+ function parseNote(raw, now = Date.now()) {
646
+ const { fields, body } = parseFrontmatter(raw);
647
+ const meta = { ...fields };
648
+ meta.scope = isScope(meta.scope) ? meta.scope : "session";
649
+ meta.origin = isOrigin(meta.origin) ? meta.origin : "self";
650
+ meta.status = isStatus(meta.status) ? meta.status : "active";
651
+ meta.stale = meta.stale === true;
652
+ for (const key of TIMESTAMP_KEYS) meta[key] = toEpoch(meta[key], now);
653
+ meta.access_count = typeof meta.access_count === "number" && Number.isFinite(meta.access_count) ? meta.access_count : 0;
654
+ return { meta, body };
655
+ }
656
+ function yamlScalar(value) {
657
+ if (typeof value === "string") {
658
+ const reserved = /* @__PURE__ */ new Set(["true", "false", "null", "yes", "no", "on", "off", "~"]);
659
+ if (/^[A-Za-z0-9_.+\-:/]+$/.test(value) && !reserved.has(value.toLowerCase())) return value;
660
+ }
661
+ return JSON.stringify(value);
662
+ }
663
+ function serializeNote(meta, body) {
664
+ const lines = [];
665
+ for (const key of KNOWN_KEYS) {
666
+ const value = meta[key];
667
+ if (value === void 0) continue;
668
+ if (TIMESTAMP_KEYS.includes(key)) lines.push(`${key}: ${yamlScalar(localIso(value))}`);
669
+ else lines.push(`${key}: ${yamlScalar(value)}`);
670
+ }
671
+ for (const key of Object.keys(meta)) {
672
+ if (key === "scope" || KNOWN_KEYS.includes(key)) continue;
673
+ if (meta[key] === void 0) continue;
674
+ lines.push(`${key}: ${yamlScalar(meta[key])}`);
675
+ }
676
+ return `---
677
+ ${lines.join("\n")}
678
+ ---
679
+
680
+ ${body}`;
681
+ }
682
+ function stripLeadingFrontmatter(content) {
683
+ return parseFrontmatter(content).body;
684
+ }
685
+
686
+ // src/notes/paths.ts
687
+ import { createHash } from "node:crypto";
688
+ import { existsSync, readdirSync, renameSync } from "node:fs";
689
+ import { homedir } from "node:os";
690
+ import { basename, dirname, join, resolve } from "node:path";
691
+ function notesRoot() {
692
+ const override = process.env.PI_NOTES_HOME;
693
+ return override && override.length > 0 ? resolve(override) : join(homedir(), ".agents", "notes");
694
+ }
695
+ function sessionHomesRoot(home = notesRoot()) {
696
+ return join(home, "pi", "session");
697
+ }
698
+ function gitRoot(cwd) {
699
+ let dir = resolve(cwd);
700
+ for (; ; ) {
701
+ if (existsSync(join(dir, ".git"))) return dir;
702
+ const parent = dirname(dir);
703
+ if (parent === dir) return void 0;
704
+ dir = parent;
705
+ }
706
+ }
707
+ function projectKey(cwd) {
708
+ const absolute = resolve(cwd);
709
+ const root = gitRoot(absolute) ?? absolute;
710
+ const digest = createHash("sha1").update(root).digest("hex").slice(0, 8);
711
+ return `${basename(root)}-${digest}`;
712
+ }
713
+ function sessionId(ctx) {
714
+ return ctx.sessionManager.getSessionId();
715
+ }
716
+ var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
717
+ function slugify(value) {
718
+ const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
719
+ return slug.length > 0 ? slug : "root";
720
+ }
721
+ function agentSlug(_ctx) {
722
+ return slugify(process.env.PI_NOTES_AGENT ?? "root");
723
+ }
724
+ function modelSlug(ctx) {
725
+ const id = ctx.model?.id;
726
+ if (!id) return "default";
727
+ return slugify(id.split("/").pop() ?? id);
728
+ }
729
+ function scopeDir(scope, ctx, who) {
730
+ if (scope === "human") return join(notesRoot(), "human");
731
+ if (scope === "project") return join(notesRoot(), "project", projectKey(ctx.cwd));
732
+ if (scope === "agent") return join(notesRoot(), "agents", who ?? agentSlug(ctx));
733
+ if (scope === "model") return join(notesRoot(), "models", who ?? modelSlug(ctx));
734
+ return join(sessionHomesRoot(), sessionId(ctx));
735
+ }
736
+ function migrateLegacyHomes(home = notesRoot()) {
737
+ const legacy = join(home, "personal");
738
+ const modern = join(home, "human");
739
+ if (!existsSync(legacy)) return void 0;
740
+ if (existsSync(modern)) return "both personal/ and human/ exist under the notes home; migrate by hand, no automatic merge";
741
+ renameSync(legacy, modern);
742
+ return void 0;
743
+ }
744
+ function namespaceSlugs(namespace, home = notesRoot()) {
745
+ try {
746
+ return readdirSync(join(home, namespace), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
747
+ } catch {
748
+ return [];
749
+ }
750
+ }
751
+ function noteFileName(vpath) {
752
+ return vpath.endsWith(".md") ? vpath : `${vpath}.md`;
753
+ }
754
+ function physicalPath(scope, vpath, ctx, who) {
755
+ return join(scopeDir(scope, ctx, who), ...noteFileName(vpath).split("/"));
756
+ }
757
+
758
+ // src/notes/address.ts
759
+ var ADDRESS_FORMS = "legal prefixes are @project/, @human/, @self/, @agents/<name>/, @model/, and @models/<name>/; bare names are the session home";
760
+ function assertVirtualPath(value) {
761
+ if (typeof value !== "string" || value.length === 0) throw new Error("path must be a non-empty virtual relative path");
762
+ if (value.includes("\0") || value.includes("\\") || value.startsWith("/")) throw new Error("path must be a safe virtual relative path");
763
+ const parts = value.split("/");
764
+ if (parts.some((part) => part.length === 0 || part === "." || part === "..")) throw new Error("path contains an unsupported component");
765
+ return value;
766
+ }
767
+ function globToRegExp(pattern) {
768
+ let source = "^";
769
+ for (let index = 0; index < pattern.length; index++) {
770
+ const char = pattern[index];
771
+ if (char === "*") {
772
+ if (pattern[index + 1] === "*") {
773
+ const followedBySlash = pattern[index + 2] === "/";
774
+ source += followedBySlash ? "(?:[^]*\\/)?" : "[^]*";
775
+ index += followedBySlash ? 2 : 1;
776
+ } else {
777
+ source += "[^/]*";
778
+ }
779
+ } else {
780
+ source += char.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
781
+ }
782
+ }
783
+ return new RegExp(`${source}$`);
784
+ }
785
+ function assertGlobPattern(value) {
786
+ if (value === void 0 || value === null || value === "") return void 0;
787
+ if (typeof value !== "string") throw new Error("glob pattern must be a string");
788
+ if (value.includes("\0") || value.includes("\\")) throw new Error("glob pattern must not contain NUL or backslashes");
789
+ return value;
790
+ }
791
+ function assertAddress(value) {
792
+ if (typeof value !== "string") throw new Error(`invalid note address: ${ADDRESS_FORMS}`);
793
+ let scope = "session";
794
+ let path = value;
795
+ let who;
796
+ if (value.startsWith("@")) {
797
+ const rest = value.slice(1);
798
+ const headEnd = rest.indexOf("/");
799
+ const head = headEnd === -1 ? rest : rest.slice(0, headEnd);
800
+ const tail = headEnd === -1 ? "" : rest.slice(headEnd + 1);
801
+ path = tail;
802
+ if (head === "project") scope = "project";
803
+ else if (head === "human") scope = "human";
804
+ else if (head === "self") scope = "agent";
805
+ else if (head === "model") scope = "model";
806
+ else if (head === "agents" || head === "models") {
807
+ const nameEnd = tail.indexOf("/");
808
+ who = nameEnd === -1 ? tail : tail.slice(0, nameEnd);
809
+ if (!SLUG_PATTERN.test(who)) throw new Error(`invalid note address: ${ADDRESS_FORMS}`);
810
+ scope = head === "agents" ? "agent" : "model";
811
+ path = nameEnd === -1 ? "" : tail.slice(nameEnd + 1);
812
+ } else {
813
+ throw new Error(`invalid note address: ${ADDRESS_FORMS}`);
814
+ }
815
+ if (path === "" && scope !== "agent" && scope !== "model") throw new Error(`invalid note address: ${ADDRESS_FORMS}`);
816
+ if (path === "" && who === void 0) throw new Error(`invalid note address: ${ADDRESS_FORMS}`);
817
+ }
818
+ if (path.includes("@")) throw new Error(`invalid note address: ${ADDRESS_FORMS}`);
819
+ assertVirtualPath(path);
820
+ return { scope, path, who };
821
+ }
822
+ function addressFor(ctx, scope, path, who) {
823
+ if (scope === "session") return path;
824
+ if (scope === "project") return `@project/${path}`;
825
+ if (scope === "human") return `@human/${path}`;
826
+ if (scope === "agent") return `@agents/${who ?? agentSlug(ctx)}/${path}`;
827
+ return `@models/${who ?? modelSlug(ctx)}/${path}`;
828
+ }
829
+
830
+ // src/notes/store.ts
831
+ import { randomUUID } from "node:crypto";
832
+ import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync as readdirSync2, renameSync as renameSync2, rmSync, writeFileSync } from "node:fs";
833
+ import { dirname as dirname2 } from "node:path";
834
+ import { generateDiffString } from "@earendil-works/pi-coding-agent";
835
+ var NoteError = class extends Error {
836
+ code;
837
+ line_numbers;
838
+ edit_index;
839
+ constructor(code, message, extra = {}) {
840
+ super(message);
841
+ this.name = "NoteError";
842
+ this.code = code;
843
+ this.line_numbers = extra.line_numbers;
844
+ this.edit_index = extra.edit_index;
845
+ }
846
+ };
847
+ var SCOPE_ORDER = ["session", "project", "human", "agent", "model"];
848
+ function assertScope(value) {
849
+ if (!isScope(value)) throw new NoteError("invalid_scope", `scope must be one of session, project, human, agent, model (got ${JSON.stringify(value)})`);
850
+ return value;
851
+ }
852
+ function assertOrigin(value) {
853
+ if (!isOrigin(value)) throw new NoteError("invalid_origin", `origin must be one of user, self, external (got ${JSON.stringify(value)})`);
854
+ return value;
855
+ }
856
+ function walkMarkdown(dir, base = dir) {
857
+ let entries;
858
+ try {
859
+ entries = readdirSync2(dir, { withFileTypes: true });
860
+ } catch (error) {
861
+ if (typeof error === "object" && error !== null && error.code === "ENOENT") return [];
862
+ throw error;
863
+ }
864
+ const paths = [];
865
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
866
+ const child = `${dir}/${entry.name}`;
867
+ if (entry.isDirectory()) paths.push(...walkMarkdown(child, base));
868
+ else if (entry.isFile() && entry.name.endsWith(".md")) paths.push(child.slice(base.length + 1).split("\\").join("/"));
869
+ }
870
+ return paths;
871
+ }
872
+ function matcherFor(pattern) {
873
+ const normalized = assertGlobPattern(pattern);
874
+ return normalized === void 0 ? void 0 : globToRegExp(normalized);
875
+ }
876
+ function atomicWrite(path, content) {
877
+ mkdirSync(dirname2(path), { recursive: true });
878
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
879
+ try {
880
+ writeFileSync(tmp, content);
881
+ renameSync2(tmp, path);
882
+ } catch (error) {
883
+ rmSync(tmp, { force: true });
884
+ throw error;
885
+ }
886
+ }
887
+ function assertWritablePath(vpath) {
888
+ const bytes = Buffer.byteLength(vpath, "utf8");
889
+ if (bytes > MAX_NOTE_PATH_BYTES) throw new NoteError("too_large", `note path exceeds ${MAX_NOTE_PATH_BYTES} UTF-8 bytes (got ${bytes})`);
890
+ }
891
+ function assertSerializedSize(content) {
892
+ const bytes = Buffer.byteLength(content, "utf8");
893
+ if (bytes > MAX_NOTE_BYTES) throw new NoteError("too_large", `note exceeds ${MAX_NOTE_BYTES} UTF-8 bytes (serialized ${bytes})`);
894
+ }
895
+ function frontmatterOf(meta) {
896
+ return serializeNote(meta, "").slice(0, -2);
897
+ }
898
+ function assertWritableHome(scope, who, ctx) {
899
+ if (who === void 0) return;
900
+ const current = scope === "agent" ? agentSlug(ctx) : modelSlug(ctx);
901
+ if (who === current) return;
902
+ const home = scope === "agent" ? `@agents/${who}/` : `@models/${who}/`;
903
+ throw new NoteError("invalid_scope", `${home} is not your home: writable homes are this session, @project/, @human/, @self/, and the current @model/ home`);
904
+ }
905
+ function homesForPattern(pattern) {
906
+ if (!pattern || !pattern.startsWith("@")) return void 0;
907
+ const head = /^@([^/]+)\//.exec(pattern)?.[1];
908
+ if (head === "project") return [{ scope: "project" }];
909
+ if (head === "human") return [{ scope: "human" }];
910
+ if (head === "self") return [{ scope: "agent" }];
911
+ if (head === "model") return [{ scope: "model" }];
912
+ if (head === "agents" || head === "models") {
913
+ const scope = head === "agents" ? "agent" : "model";
914
+ const name = pattern.slice(head.length + 2).split("/")[0] ?? "";
915
+ if (name.length > 0 && !/[*?]/.test(name)) return [{ scope, who: name }];
916
+ return namespaceSlugs(head).map((who) => ({ scope, who }));
917
+ }
918
+ return [];
919
+ }
920
+ function normalizePattern(pattern, ctx) {
921
+ if (!pattern) return pattern;
922
+ if (pattern.startsWith("@self/")) return `@agents/${agentSlug(ctx)}/${pattern.slice("@self/".length)}`;
923
+ if (pattern.startsWith("@model/")) return `@models/${modelSlug(ctx)}/${pattern.slice("@model/".length)}`;
924
+ return pattern;
925
+ }
926
+ function homesFor(ctx, opts) {
927
+ if (opts.scope !== void 0) return [{ scope: opts.scope, who: opts.who }];
928
+ return homesForPattern(opts.pattern) ?? SCOPE_ORDER.map((scope) => ({ scope }));
929
+ }
930
+ function matchLineNumbers(body, needle) {
931
+ const lines = [];
932
+ let cursor2 = 0;
933
+ for (; ; ) {
934
+ const index = body.indexOf(needle, cursor2);
935
+ if (index === -1) break;
936
+ lines.push(body.slice(0, index).split("\n").length);
937
+ cursor2 = index + Math.max(needle.length, 1);
938
+ }
939
+ return lines;
940
+ }
941
+ function writeNote(ctx, vpath, body, opts) {
942
+ assertVirtualPath(vpath);
943
+ assertWritablePath(vpath);
944
+ const scope = assertScope(opts.scope);
945
+ assertWritableHome(scope, opts.who, ctx);
946
+ const origin = assertOrigin(opts.origin);
947
+ const path = physicalPath(scope, vpath, ctx, opts.who);
948
+ const now = Date.now();
949
+ const cleanBody = stripLeadingFrontmatter(body);
950
+ const existing = existsSync2(path) ? parseNote(readFileSync(path, "utf8"), now).meta : void 0;
951
+ const meta = existing ?? {
952
+ scope,
953
+ origin,
954
+ status: "active",
955
+ stale: false,
956
+ created_at: now,
957
+ updated_at: now,
958
+ last_accessed: now,
959
+ access_count: 0
960
+ };
961
+ meta.scope = scope;
962
+ meta.origin = origin;
963
+ meta.status = "active";
964
+ meta.stale = opts.stale ?? false;
965
+ meta.updated_at = now;
966
+ const serialized = serializeNote(meta, cleanBody);
967
+ assertSerializedSize(serialized);
968
+ atomicWrite(path, serialized);
969
+ return { meta };
970
+ }
971
+ function editNote(ctx, vpath, scope, edits, opts = {}, who) {
972
+ assertVirtualPath(vpath);
973
+ assertWritablePath(vpath);
974
+ assertWritableHome(scope, who, ctx);
975
+ const operations = edits ?? [];
976
+ if (operations.length === 0 && opts.origin === void 0 && opts.stale === void 0) {
977
+ throw new NoteError("nothing_to_do", "nothing to do: provide edits or at least one of origin, stale");
978
+ }
979
+ const path = physicalPath(scope, vpath, ctx, who);
980
+ if (!existsSync2(path)) throw new NoteError("not_found", "note not found");
981
+ const raw = readFileSync(path, "utf8");
982
+ const { meta, body } = parseNote(raw);
983
+ meta.scope = scope;
984
+ const beforeMeta = { ...meta };
985
+ let next = body;
986
+ operations.forEach((edit, index) => {
987
+ const oldText = edit?.oldText;
988
+ const newText = edit?.newText;
989
+ if (typeof oldText !== "string" || oldText.length === 0) throw new NoteError("no_match", `edit ${index}: oldText must be a non-empty string`, { edit_index: index });
990
+ if (typeof newText !== "string") throw new NoteError("no_match", `edit ${index}: newText must be a string`, { edit_index: index });
991
+ const lines = matchLineNumbers(next, oldText);
992
+ if (lines.length === 0) throw new NoteError("no_match", `edit ${index}: oldText does not occur in the note body`, { edit_index: index });
993
+ if (lines.length > 1 && !opts.replaceAll) {
994
+ 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 });
995
+ }
996
+ if (opts.replaceAll) {
997
+ next = next.split(oldText).join(newText);
998
+ } else {
999
+ const matchIndex = next.indexOf(oldText);
1000
+ next = next.substring(0, matchIndex) + newText + next.substring(matchIndex + oldText.length);
1001
+ }
1002
+ });
1003
+ if (opts.origin !== void 0) meta.origin = assertOrigin(opts.origin);
1004
+ if (opts.stale !== void 0) meta.stale = opts.stale;
1005
+ meta.updated_at = Date.now();
1006
+ const serialized = serializeNote(meta, next);
1007
+ assertSerializedSize(serialized);
1008
+ const bodyChanged = body !== next;
1009
+ const metadataChanged = beforeMeta.origin !== meta.origin || beforeMeta.stale !== meta.stale;
1010
+ const diff = bodyChanged && metadataChanged ? generateDiffString(raw, serialized).diff : bodyChanged ? generateDiffString(body, next).diff : metadataChanged ? generateDiffString(frontmatterOf(beforeMeta), frontmatterOf(meta)).diff : "";
1011
+ atomicWrite(path, serialized);
1012
+ return { meta, applied: operations.length, resolved_scope: scope, diff };
1013
+ }
1014
+ function accessedMeta(meta, scope, now) {
1015
+ const next = { ...meta, scope };
1016
+ next.last_accessed = now;
1017
+ next.access_count = (typeof next.access_count === "number" ? next.access_count : 0) + 1;
1018
+ return next;
1019
+ }
1020
+ function readNote(ctx, vpath, scope, who) {
1021
+ assertVirtualPath(vpath);
1022
+ const path = physicalPath(scope, vpath, ctx, who);
1023
+ if (!existsSync2(path)) return void 0;
1024
+ const now = Date.now();
1025
+ const parsed = parseNote(readFileSync(path, "utf8"), now);
1026
+ const meta = accessedMeta(parsed.meta, scope, now);
1027
+ const text = serializeNote(meta, parsed.body);
1028
+ atomicWrite(path, text);
1029
+ return { meta, body: parsed.body, text, resolvedScope: scope };
1030
+ }
1031
+ function listNotes(ctx, opts = {}) {
1032
+ const matcher = matcherFor(normalizePattern(opts.pattern, ctx));
1033
+ const rows = [];
1034
+ for (const home of homesFor(ctx, opts)) {
1035
+ const scope = home.scope;
1036
+ const root = scopeDir(scope, ctx, home.who);
1037
+ for (const path of walkMarkdown(root)) {
1038
+ const address = addressFor(ctx, scope, path, home.who);
1039
+ if (matcher && !matcher.test(address)) continue;
1040
+ const { meta, body } = parseNote(readFileSync(`${root}/${path}`, "utf8"));
1041
+ meta.scope = scope;
1042
+ rows.push({ address, scope, path, meta, body, sizeBytes: Buffer.byteLength(body, "utf8") });
1043
+ }
1044
+ }
1045
+ rows.sort((a, b) => b.meta.updated_at - a.meta.updated_at || a.address.localeCompare(b.address));
1046
+ return rows;
1047
+ }
1048
+ function searchNotes(ctx, queries, opts = {}) {
1049
+ const matcher = matcherFor(normalizePattern(opts.pattern, ctx));
1050
+ const rows = [];
1051
+ for (const home of homesFor(ctx, opts)) {
1052
+ const scope = home.scope;
1053
+ const root = scopeDir(scope, ctx, home.who);
1054
+ for (const path of walkMarkdown(root)) {
1055
+ const address = addressFor(ctx, scope, path, home.who);
1056
+ if (matcher && !matcher.test(address)) continue;
1057
+ const { meta, body } = parseNote(readFileSync(`${root}/${path}`, "utf8"));
1058
+ meta.scope = scope;
1059
+ const serializedBodyOffset = Array.from(serializeNote(accessedMeta(meta, scope, Date.now()), "")).length;
1060
+ let baseChars = 0;
1061
+ const matches = [];
1062
+ for (const [index, line] of body.split("\n").entries()) {
1063
+ if (queries.some((query) => line.includes(query))) {
1064
+ matches.push({ line: index + 1, text: line, offsetChars: serializedBodyOffset + baseChars + earliestMatchOffsetChars(line, queries) });
1065
+ }
1066
+ baseChars += Array.from(line).length + 1;
1067
+ }
1068
+ if (matches.length > 0) rows.push({ address, path, scope, meta, matches });
1069
+ }
1070
+ }
1071
+ rows.sort((a, b) => a.address.localeCompare(b.address));
1072
+ return rows;
1073
+ }
1074
+
1075
+ // src/notes/tools.ts
1076
+ var ORIGIN = Type3.Optional(Type3.Union([Type3.Literal("user"), Type3.Literal("self"), Type3.Literal("external")], {
1077
+ 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."
1078
+ }));
1079
+ var ADDRESS_DESCRIPTION = "Address forms are bare `<vpath>` for this session, `@project/<vpath>` for this project, `@human/<vpath>` for the human's cross-project home, `@self/<vpath>` / `@agents/<name>/<vpath>` for agent homes, and `@model/<vpath>` / `@models/<name>/<vpath>` for model homes. `@self` and `@model` mean the current agent/model; the `<name>` forms name one absolutely. The word after `@` is always one of the reserved home names \u2014 names live at the second level, never `@faye/`. `@` means leaving home. Any other `@` prefix, or `@` inside a vpath, is a hard error. There is no cross-home fallback. Paths reject `..`, absolute paths, and backslashes. Homes you do not own (`@agents/<other>/`, `@models/<other>/`) are read-only.";
1080
+ function failure(error) {
1081
+ if (error instanceof NoteError) {
1082
+ const payload = { error: error.message };
1083
+ if (error.line_numbers) payload.line_numbers = error.line_numbers;
1084
+ if (error.edit_index !== void 0) payload.edit_index = error.edit_index;
1085
+ return output(payload);
1086
+ }
1087
+ throw error;
1088
+ }
1089
+ function registerNotesTools(pi) {
1090
+ pi.registerTool(defineTool2({
1091
+ name: "notes_write",
1092
+ label: "Notes write",
1093
+ 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.`,
1094
+ parameters: Type3.Object({ address: Type3.String(), content: Type3.String(), origin: ORIGIN, stale: Type3.Optional(Type3.Boolean()) }, { additionalProperties: false }),
1095
+ executionMode: "sequential",
1096
+ async execute(_id, params, _signal, _update, ctx) {
1097
+ const content = params.content;
1098
+ try {
1099
+ const destination = assertAddress(params.address);
1100
+ writeNote(ctx, destination.path, content, { scope: destination.scope, who: destination.who, origin: params.origin ?? "self", stale: params.stale });
1101
+ return output({ address: params.address, written: true });
1102
+ } catch (error) {
1103
+ return failure(error);
1104
+ }
1105
+ }
1106
+ }));
1107
+ pi.registerTool(defineTool2({
1108
+ name: "notes_edit",
1109
+ label: "Notes edit",
1110
+ description: `Edit a note body by exact-text replacement; frontmatter is never editable this way. ${ADDRESS_DESCRIPTION} Each oldText must occur exactly once unless replace_all is set; a multi-match anchor fails with its match line numbers and a zero-match anchor names the failing edit index. edits may be omitted (or empty) for a metadata-only update, which requires at least one of origin/stale. Moving while awake means notes_write at a new address and notes_edit at the old address with stale=true. The success return carries the address and a diff of what changed.`,
1111
+ parameters: Type3.Object({ address: Type3.String(), edits: Type3.Optional(Type3.Array(Type3.Object({ oldText: Type3.String(), newText: Type3.String() }, { additionalProperties: false }))), origin: ORIGIN, stale: Type3.Optional(Type3.Boolean()), replace_all: Type3.Optional(Type3.Boolean()) }, { additionalProperties: false }),
1112
+ executionMode: "sequential",
1113
+ async execute(_id, params, _signal, _update, ctx) {
1114
+ try {
1115
+ const destination = assertAddress(params.address);
1116
+ const { applied, diff } = editNote(ctx, destination.path, destination.scope, params.edits, { origin: params.origin, stale: params.stale, replaceAll: params.replace_all }, destination.who);
1117
+ return output({ address: params.address, applied, diff });
1118
+ } catch (error) {
1119
+ return failure(error);
1120
+ }
1121
+ }
1122
+ }));
1123
+ pi.registerTool(defineTool2({
1124
+ name: "notes_read",
1125
+ label: "Notes read",
1126
+ description: `Read a character window of a note file, frontmatter included. ${ADDRESS_DESCRIPTION} offset_chars is the code-point offset to start from (default 0) \u2014 a negative value counts back from the end \u2014 and limit_chars caps the window (default ${DEFAULT_READ_WINDOW_CHARS}, max ${MAX_READ_WINDOW_CHARS}). Each response delivers the longest fitting prefix of that window in the shared READ WINDOW block: concatenate only the content after the block to reconstruct the note.`,
1127
+ parameters: Type3.Object({ address: Type3.String(), offset_chars: Type3.Optional(Type3.Integer({ description: "Code-point offset to start from (default 0). A negative value counts back from the end; the response echoes the resolved absolute offset. Pass the previous next_offset_chars back unchanged to continue." })), limit_chars: Type3.Optional(Type3.Integer({ minimum: 1, maximum: MAX_READ_WINDOW_CHARS, description: `Largest requested window in code points (default ${DEFAULT_READ_WINDOW_CHARS}). A window too large for the wire budget is cut short; next_offset_chars names where the next read resumes.` })) }, { additionalProperties: false }),
1128
+ async execute(_id, params, _signal, _update, ctx) {
1129
+ let note;
1130
+ try {
1131
+ const destination = assertAddress(params.address);
1132
+ note = readNote(ctx, destination.path, destination.scope, destination.who);
1133
+ } catch (error) {
1134
+ return failure(error);
1135
+ }
1136
+ if (!note) return output({ error: "note not found", address: params.address });
1137
+ const text = note.text;
1138
+ const totalChars = Array.from(text).length;
1139
+ if (typeof params.offset_chars === "number" && params.offset_chars > totalChars) return output({ error: `offset_chars ${params.offset_chars} is past the end: the note has ${totalChars} chars; the largest legal offset is ${totalChars} (an empty end-read)`, address: params.address, offset_chars: params.offset_chars, total_chars: totalChars });
1140
+ return readCharacterWindow(text, params.offset_chars, params.limit_chars, (window) => {
1141
+ const { content, ...rest } = window;
1142
+ return outputRaw(readWindowBlock([["address", params.address]], window), content, { address: params.address, ...rest });
1143
+ }, (result) => withinTextBudget(result.content[0].text));
1144
+ }
1145
+ }));
1146
+ pi.registerTool(defineTool2({
1147
+ name: "notes_list",
1148
+ label: "Notes list",
1149
+ description: `List note files as rows carrying address, updated_at, and stale, most recently updated first. ${ADDRESS_DESCRIPTION} Listings merge your five reachable homes: this session, @project/, @human/, your @self home, and the current @model home; other agents and models appear only under an explicit glob (@agents/<name>/**, @models/<name>/**, or a glob in the name segment to scan a whole namespace).`,
1150
+ parameters: Type3.Object({ pattern: nullableString(), cursor: cursor(), max_results: positiveInteger() }, { additionalProperties: false }),
1151
+ async execute(_id, params, _signal, _update, ctx) {
1152
+ let rows;
1153
+ try {
1154
+ rows = listNotes(ctx, { pattern: params.pattern ?? void 0 });
1155
+ } catch (error) {
1156
+ return failure(error);
1157
+ }
1158
+ const files = rows.map((row) => ({ address: row.address, stale: row.meta.stale, updated_at: localIso(row.meta.updated_at) }));
1159
+ return output(page(files, params.cursor ?? 0, "files", params.max_results, (file, fits) => {
1160
+ if (fits(file)) return file;
1161
+ const address = middleTruncate(file.address, (candidate) => fits({ ...file, address: candidate, address_truncated: true }));
1162
+ return { ...file, address, address_truncated: true };
1163
+ }));
1164
+ }
1165
+ }));
1166
+ pi.registerTool(defineTool2({
1167
+ name: "notes_search",
1168
+ label: "Notes search",
1169
+ description: `Case-sensitive literal substring search over note bodies; query is one string or several (OR), each matched line appears once. ${ADDRESS_DESCRIPTION} Search merges the same five reachable homes as notes_list; explicit globs reach other agents and models. Patterns glob over full address strings. Each file entry carries matches_total, its full match count before capping. Each match carries line, text, offset_chars (a code-point offset into the serialized note returned by notes_read, at the earliest query match), and truncated.`,
1170
+ parameters: Type3.Object({ query: searchQuery(), pattern: nullableString(), cursor: cursor(), max_matches_per_file: positiveInteger(), max_files: positiveInteger() }, { additionalProperties: false }),
1171
+ async execute(_id, params, _signal, _update, ctx) {
1172
+ const queries = searchQueries(params.query);
1173
+ let rows;
1174
+ try {
1175
+ rows = searchNotes(ctx, queries, { pattern: params.pattern ?? void 0 });
1176
+ } catch (error) {
1177
+ return failure(error);
1178
+ }
1179
+ const maxPerFile = params.max_matches_per_file ?? Number.POSITIVE_INFINITY;
1180
+ const result = rows.map((row) => {
1181
+ const matches = row.matches.map((match) => ({ line: match.line, text: match.text, truncated: false, offset_chars: match.offsetChars }));
1182
+ return { address: row.address, updated_at: localIso(row.meta.updated_at), stale: row.meta.stale, matches_total: matches.length, matches: matches.slice(0, maxPerFile) };
1183
+ });
1184
+ const fitFile = (file, fits) => {
1185
+ if (fits(file)) return file;
1186
+ const matches = file.matches;
1187
+ let low = 0;
1188
+ let high = matches.length;
1189
+ while (low < high) {
1190
+ const mid = Math.ceil((low + high) / 2);
1191
+ if (mid >= 1 && fits({ ...file, matches: matches.slice(0, mid) })) low = mid;
1192
+ else high = mid - 1;
1193
+ }
1194
+ if (low >= 1) return { ...file, matches: matches.slice(0, low) };
1195
+ const first = matches[0];
1196
+ const fitted = (text2) => ({ ...file, matches: [{ ...first, text: text2, truncated: true }] });
1197
+ const text = prefixFit(first.text, (candidate) => fits(fitted(candidate)));
1198
+ const prefix = fitted(text);
1199
+ if (fits(prefix)) return prefix;
1200
+ const address = middleTruncate(prefix.address, (candidate) => fits({ ...prefix, address: candidate, address_truncated: true }));
1201
+ return { ...prefix, address, address_truncated: true };
1202
+ };
1203
+ return output(page(result, params.cursor ?? 0, "files", params.max_files, fitFile));
1204
+ }
1205
+ }));
1206
+ }
1207
+
1208
+ // src/context/thresholds.ts
1209
+ import { SettingsManager } from "@earendil-works/pi-coding-agent";
1210
+
1211
+ // src/settings.ts
1212
+ function isSettingsObject(value) {
1213
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1214
+ }
1215
+ function piContextSettings(settings) {
1216
+ if (!isSettingsObject(settings)) return {};
1217
+ const value = settings[PI_CONTEXT_SETTINGS_KEY];
1218
+ return isSettingsObject(value) ? value : {};
1219
+ }
1220
+ function mergePiContextSettings(globalSettings, projectSettings) {
1221
+ const merged = { ...piContextSettings(globalSettings), ...piContextSettings(projectSettings) };
1222
+ return { reminderMarginTokens: merged.reminderMarginTokens, dreamer: merged.dreamer };
1223
+ }
1224
+
1225
+ // src/context/thresholds.ts
1226
+ function validMargin(raw) {
1227
+ if (typeof raw !== "number" || !Number.isSafeInteger(raw) || raw <= 0) return void 0;
1228
+ return raw;
1229
+ }
1230
+ function deriveThresholds(reserveTokens, margins) {
1231
+ const warnings = [];
1232
+ const reminderKey = `${PI_CONTEXT_SETTINGS_KEY}.reminderMarginTokens`;
1233
+ let reminderMargin;
1234
+ if (margins.reminderMarginTokens === void 0) reminderMargin = DEFAULT_REMINDER_MARGIN_TOKENS;
1235
+ else {
1236
+ const parsed = validMargin(margins.reminderMarginTokens);
1237
+ if (parsed === void 0) {
1238
+ warnings.push(`pi-context: ${reminderKey} must be a positive integer; using default ${DEFAULT_REMINDER_MARGIN_TOKENS}.`);
1239
+ reminderMargin = DEFAULT_REMINDER_MARGIN_TOKENS;
1240
+ } else reminderMargin = parsed;
1241
+ }
1242
+ return { thresholds: { reminder: reserveTokens + reminderMargin, reserve: reserveTokens, warning: reserveTokens + WARNING_RUNWAY_TOKENS }, warnings };
1243
+ }
1244
+ function readThresholdSettingsFromManager(ctx, settingsManager) {
1245
+ const model = ctx.model;
1246
+ const compaction = settingsManager.getCompactionSettings(model ? { provider: model.provider, id: model.id } : void 0);
1247
+ const derived = deriveThresholds(
1248
+ compaction.reserveTokens,
1249
+ mergePiContextSettings(settingsManager.getGlobalSettings(), settingsManager.getProjectSettings())
1250
+ );
1251
+ return { thresholds: derived.thresholds, automatic: compaction.enabled, warnings: derived.warnings };
1252
+ }
1253
+ function readThresholdSettings(ctx, settingsManager) {
1254
+ try {
1255
+ if (settingsManager) return readThresholdSettingsFromManager(ctx, settingsManager);
1256
+ return readThresholdSettingsFromManager(
1257
+ ctx,
1258
+ SettingsManager.create(ctx.cwd, void 0, { projectTrusted: ctx.isProjectTrusted() })
1259
+ );
1260
+ } catch (error) {
1261
+ return {
1262
+ thresholds: {
1263
+ reminder: DEFAULT_RESERVE_TOKENS + DEFAULT_REMINDER_MARGIN_TOKENS,
1264
+ reserve: DEFAULT_RESERVE_TOKENS,
1265
+ warning: DEFAULT_RESERVE_TOKENS + WARNING_RUNWAY_TOKENS
1266
+ },
1267
+ automatic: true,
1268
+ warnings: [`pi-context: could not read settings; using defaults (${String(error)}).`]
1269
+ };
1270
+ }
1271
+ }
1272
+
1273
+ // src/context/runtime.ts
1274
+ import { getCurrentSystemMessage as getCurrentSystemMessage2, Type as Type5 } from "@earendil-works/pi-ai";
1275
+ import { VERSION, defineTool as defineTool4 } from "@earendil-works/pi-coding-agent";
1276
+ import { randomUUID as randomUUID2 } from "node:crypto";
1277
+
1278
+ // src/context/budget.ts
1279
+ import { Type as Type4 } from "@earendil-works/pi-ai";
1280
+ import { defineTool as defineTool3 } from "@earendil-works/pi-coding-agent";
1281
+
1282
+ // src/context/prompts.ts
1283
+ function identityBlock(agentName, modelName, firstWindowId, currentWindowId2, previousWindowId) {
1284
+ const lines = [
1285
+ `Agent name: ${agentName} (brain: ${modelName})`,
1286
+ `First context window id: ${firstWindowId}`,
1287
+ `Current context window id: ${currentWindowId2}`
1288
+ ];
1289
+ if (previousWindowId) lines.push(`Previous context window id: ${previousWindowId}`);
1290
+ return `${CONTEXT_WINDOW_OPEN_TAG}
1291
+ ${lines.join("\n")}
1292
+ ${CONTEXT_WINDOW_CLOSE_TAG}`;
1293
+ }
1294
+ function relativeTime(timestamp, now) {
1295
+ const seconds = Math.trunc((timestamp - now) / 1e3);
1296
+ const [unit, size] = [["d", 86400], ["h", 3600], ["m", 60], ["s", 1]].find(([unit2, size2]) => Math.abs(seconds) >= size2 || unit2 === "s");
1297
+ const amount = `${Math.abs(Math.trunc(seconds / size))}${unit}`;
1298
+ return seconds > 0 ? `in ${amount}` : `${amount} ago`;
1299
+ }
1300
+ function rowsFor(snapshot, scope) {
1301
+ return snapshot.homes.get(scope) ?? [];
1302
+ }
1303
+ function notesUnavailableNotice(snapshot) {
1304
+ if (snapshot.unavailable.length === 0) return void 0;
1305
+ const homes = snapshot.unavailable.map((home) => home.label).join(", ");
1306
+ const noun = snapshot.unavailable.length === 1 ? "home's index was" : "home indexes were";
1307
+ return `Notes index incomplete: ${homes} ${noun} unavailable during boot; notes_list can retry after recovery.`;
1308
+ }
1309
+ function notesIndex(snapshot) {
1310
+ const sections = [];
1311
+ for (const scope of ["human", "project", "agent", "model"]) {
1312
+ const toc = rowsFor(snapshot, scope).find((row) => row.path === "MAP.md");
1313
+ if (toc && !toc.meta.stale) {
1314
+ if (toc.body.length > 0) sections.push(toc.body);
1315
+ }
1316
+ }
1317
+ const recentNotes = [
1318
+ ...rowsFor(snapshot, "session").filter((row) => !row.meta.stale && row.path !== "MAP.md").slice(0, POCKET_SESSION_LIMIT),
1319
+ ...rowsFor(snapshot, "project").filter((row) => !row.meta.stale && row.path !== "MAP.md").slice(0, POCKET_PROJECT_LIMIT),
1320
+ ...rowsFor(snapshot, "human").filter((row) => !row.meta.stale && row.path !== "MAP.md").slice(0, POCKET_HUMAN_LIMIT),
1321
+ ...rowsFor(snapshot, "agent").filter((row) => !row.meta.stale && row.path !== "MAP.md").slice(0, POCKET_AGENT_LIMIT),
1322
+ ...rowsFor(snapshot, "model").filter((row) => !row.meta.stale && row.path !== "MAP.md").slice(0, POCKET_MODEL_LIMIT)
1323
+ ];
1324
+ if (recentNotes.length > 0) {
1325
+ 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_HUMAN_LIMIT} from @human, ${POCKET_AGENT_LIMIT} from your @self home, ${POCKET_MODEL_LIMIT} from the current @model home). A note's content never appears here, so its name has to say what the note is about:`];
1326
+ for (const row of recentNotes) {
1327
+ lines.push(`- ${row.address} (${row.body.split("\n").length} lines, ${row.sizeBytes} UTF-8 bytes, updated ${relativeTime(row.meta.updated_at, snapshot.openedAt)})`);
1328
+ }
1329
+ sections.push(lines.join("\n"));
1330
+ }
1331
+ return sections.join("\n\n");
1332
+ }
1333
+ function notesHomeBlock() {
1334
+ return "Notes_* addresses have five homes: bare <vpath> is this session, @project/<vpath> is this project, @human/<vpath> is the human's cross-project home, @self/<vpath> and @agents/<name>/<vpath> are agent homes (current vs named), and @model/<vpath> and @models/<name>/<vpath> are model homes. @self and @model resolve to who is running now; listings always show resolved names. @ means leaving home; there is no cross-home fallback. Anything else after @ \u2014 or @ inside a vpath \u2014 is a hard error. Any other note is a plain file \u2014 use the file tools.";
1335
+ }
1336
+ function renderBootBlock(data) {
1337
+ const parts = [];
1338
+ if (data.resetLine) parts.push(RESET_SUMMARY);
1339
+ parts.push(identityBlock(data.agentName, data.modelName, data.firstWindowId, data.currentWindowId, data.previousWindowId));
1340
+ parts.push(notesHomeBlock());
1341
+ const incomplete = notesUnavailableNotice(data.notes);
1342
+ if (incomplete) parts.push(incomplete);
1343
+ const index = notesIndex(data.notes);
1344
+ if (index) parts.push(index);
1345
+ parts.push(PROTOCOL_BLOCK);
1346
+ return parts.join("\n\n");
1347
+ }
1348
+ function tokenBudgetGuidance(remaining) {
1349
+ return `${GUIDANCE_OPEN_TAG}
1350
+ Your brain is almost out of room \u2014 ${remaining} tokens left, and then your memory gets wiped. The wipe is automatic: there is no final turn to write then. Grab the notebook now \u2014 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. Replacing an older checkpoint? Mark it stale. Then call wipe_memory yourself \u2014 anything you do after the checkpoint isn't in it.
1351
+ ${GUIDANCE_CLOSE_TAG}`;
1352
+ }
1353
+
1354
+ // src/context/budget.ts
1355
+ function remainingTokens(ctx) {
1356
+ const usage = windowUsage(ctx);
1357
+ return !usage || usage.tokens === null ? null : Math.max(0, usage.contextWindow - usage.tokens);
1358
+ }
1359
+ function registerBudget(pi, isEnabled, settingsManager) {
1360
+ let cachedPolicy;
1361
+ const notifiedWarnings = /* @__PURE__ */ new Set();
1362
+ const resolvePolicy = (ctx) => {
1363
+ if (!settingsManager && cachedPolicy) return { ...cachedPolicy, warnings: [] };
1364
+ const resolution = readThresholdSettings(ctx, settingsManager);
1365
+ for (const warning of resolution.warnings) {
1366
+ if (notifiedWarnings.has(warning)) continue;
1367
+ notifiedWarnings.add(warning);
1368
+ ctx.ui.notify(warning, "warning");
1369
+ }
1370
+ if (!settingsManager) cachedPolicy = { thresholds: resolution.thresholds, automatic: resolution.automatic };
1371
+ return resolution;
1372
+ };
1373
+ const thresholdsFor = (ctx) => {
1374
+ return resolvePolicy(ctx).thresholds;
1375
+ };
1376
+ const automaticResetEnabled = (ctx) => {
1377
+ return resolvePolicy(ctx).automatic;
1378
+ };
1379
+ const resetDue = (ctx) => {
1380
+ if (!automaticResetEnabled(ctx)) return false;
1381
+ const usage = windowUsage(ctx);
1382
+ return usage !== void 0 && usage.tokens !== null && usage.contextWindow - usage.tokens <= thresholdsFor(ctx).reserve;
1383
+ };
1384
+ const invalidateThresholds = () => {
1385
+ cachedPolicy = void 0;
1386
+ };
1387
+ let pendingGuidance;
1388
+ let pendingWarning;
1389
+ let pendingNotices = [];
1390
+ const notifyCommittedReminders = (ctx) => {
1391
+ const windowId = currentWindowId(ctx);
1392
+ for (const notice of pendingNotices) {
1393
+ if (notice.windowId !== windowId || !hasWindowMessage(ctx, notice.customType)) continue;
1394
+ ctx.ui.notify(notice.customType === WARNING_TYPE ? "pi-context: context budget critical \u2014 final checkpoint warning recorded for the model." : "pi-context: context budget low \u2014 checkpoint reminder recorded for the model, kept out of the chat view.", "warning");
1395
+ }
1396
+ pendingNotices = [];
1397
+ };
1398
+ const clearStaged = () => {
1399
+ pendingGuidance = void 0;
1400
+ pendingWarning = void 0;
1401
+ };
1402
+ const resetForTransition = () => {
1403
+ clearStaged();
1404
+ pendingNotices = [];
1405
+ invalidateThresholds();
1406
+ notifiedWarnings.clear();
1407
+ };
1408
+ const consumeTurnEnd = (ctx) => {
1409
+ const staged = [
1410
+ pendingGuidance ? { ...pendingGuidance, customType: GUIDANCE_TYPE } : void 0,
1411
+ pendingWarning ? { ...pendingWarning, customType: WARNING_TYPE } : void 0
1412
+ ];
1413
+ clearStaged();
1414
+ const windowId = currentWindowId(ctx);
1415
+ const drafts = staged.filter((draft) => draft !== void 0 && draft.windowId === windowId);
1416
+ pendingNotices = drafts.map(({ windowId: windowId2, customType }) => ({ windowId: windowId2, customType }));
1417
+ return drafts.map((draft) => ({
1418
+ type: "custom_message",
1419
+ customType: draft.customType,
1420
+ content: draft.content,
1421
+ display: false
1422
+ }));
1423
+ };
1424
+ pi.on("session_start", (_event, ctx) => {
1425
+ resetForTransition();
1426
+ thresholdsFor(ctx);
1427
+ });
1428
+ pi.on("session_tree", resetForTransition);
1429
+ pi.on("model_select", resetForTransition);
1430
+ pi.on("session_shutdown", resetForTransition);
1431
+ pi.on("turn_start", (_event, ctx) => notifyCommittedReminders(ctx));
1432
+ pi.on("agent_settled", (_event, ctx) => {
1433
+ notifyCommittedReminders(ctx);
1434
+ clearStaged();
1435
+ });
1436
+ pi.on("context", (_event, ctx) => {
1437
+ if (!isEnabled()) return void 0;
1438
+ const remaining = remainingTokens(ctx);
1439
+ if (remaining === null) return void 0;
1440
+ const windowId = currentWindowId(ctx);
1441
+ const { reminder, warning } = thresholdsFor(ctx);
1442
+ if (hasWindowMessage(ctx, WARNING_TYPE) || pendingWarning?.windowId === windowId) return void 0;
1443
+ if (remaining <= warning) {
1444
+ pendingGuidance = void 0;
1445
+ const content = `${GUIDANCE_OPEN_TAG}
1446
+ ${WARNING_PROMPT}
1447
+ ${GUIDANCE_CLOSE_TAG}`;
1448
+ pendingWarning = { windowId, content };
1449
+ const warningMessage = {
1450
+ role: "custom",
1451
+ customType: WARNING_TYPE,
1452
+ content,
1453
+ display: false,
1454
+ timestamp: Date.now()
1455
+ };
1456
+ return { messages: [..._event.messages, warningMessage] };
1457
+ }
1458
+ if (hasWindowMessage(ctx, GUIDANCE_TYPE) || pendingGuidance?.windowId === windowId) return void 0;
1459
+ if (remaining <= reminder) {
1460
+ const left = Math.max(0, remaining - warning);
1461
+ pendingGuidance = { windowId, content: tokenBudgetGuidance(left) };
1462
+ }
1463
+ return void 0;
1464
+ });
1465
+ pi.registerTool(defineTool3({
1466
+ name: "get_context_remaining",
1467
+ label: "Get context remaining",
1468
+ description: "Return estimated context tokens left before your memory is wiped; null when Pi cannot estimate usage.",
1469
+ parameters: Type4.Object({}, { additionalProperties: false }),
1470
+ async execute(_id, _params, _signal, _update, ctx) {
1471
+ const remaining = remainingTokens(ctx);
1472
+ return output({ remaining_tokens: remaining === null ? null : Math.max(0, remaining - thresholdsFor(ctx).warning) });
1473
+ }
1474
+ }));
1475
+ return {
1476
+ automaticResetEnabled,
1477
+ resetDue,
1478
+ consumeTurnEnd,
1479
+ clear: () => {
1480
+ clearStaged();
1481
+ pendingNotices = [];
1482
+ }
1483
+ };
1484
+ }
1485
+
1486
+ // src/notes/notes-snapshot.ts
1487
+ var NOTES_HOMES = [
1488
+ { scope: "session", label: "this session" },
1489
+ { scope: "project", label: "@project" },
1490
+ { scope: "human", label: "@human" },
1491
+ { scope: "agent", label: "@self" },
1492
+ { scope: "model", label: "@model" }
1493
+ ];
1494
+ function loadNotesSnapshot(ctx, loadHome = (context, scope) => listNotes(context, { scope })) {
1495
+ const openedAt = Date.now();
1496
+ const homes = /* @__PURE__ */ new Map();
1497
+ const unavailable = [];
1498
+ for (const home of NOTES_HOMES) {
1499
+ try {
1500
+ homes.set(home.scope, loadHome(ctx, home.scope));
1501
+ } catch (error) {
1502
+ const code = typeof error === "object" && error !== null ? error.code : void 0;
1503
+ if (typeof code !== "string" || !/^E[A-Z0-9_]+$/.test(code) || code.startsWith("ERR_")) throw error;
1504
+ homes.set(home.scope, []);
1505
+ unavailable.push(home);
1506
+ }
1507
+ }
1508
+ return { openedAt, homes, unavailable };
1509
+ }
1510
+
1511
+ // src/context/reset-lifecycle.ts
1512
+ import { isContextOverflow, isRecoverableLength } from "@earendil-works/pi-ai";
1513
+ function isAbort(message, outcome, ctx) {
1514
+ return outcome === "aborted" || message.role === "assistant" && message.stopReason === "aborted" || ctx.signal?.aborted === true;
1515
+ }
1516
+ function isOverflowLike(message, ctx) {
1517
+ if (message.role !== "assistant") return false;
1518
+ return isContextOverflow(message, ctx.model?.contextWindow) || ctx.model !== void 0 && isRecoverableLength(message, ctx.model.maxTokens);
1519
+ }
1520
+ function registerResetLifecycle(pi, options) {
1521
+ let explicitRequested = false;
1522
+ let overflowPending = false;
1523
+ let overflowRecoveryUsed = false;
1524
+ let active = true;
1525
+ const clear = () => {
1526
+ explicitRequested = false;
1527
+ overflowPending = false;
1528
+ overflowRecoveryUsed = false;
1529
+ };
1530
+ const resetBoundaryResult = (entries, ctx) => {
1531
+ try {
1532
+ return { entries: [...entries, ...options.buildReset(ctx)], continue: true };
1533
+ } catch (error) {
1534
+ ctx.ui.notify(`pi-context: could not build reset (${String(error)}).`, "warning");
1535
+ return entries.length > 0 ? { entries } : void 0;
1536
+ }
1537
+ };
1538
+ pi.on("turn_end", (event, ctx) => {
1539
+ if (!active) return void 0;
1540
+ const requested = explicitRequested;
1541
+ explicitRequested = false;
1542
+ const aborted = isAbort(event.message, event.outcome, ctx);
1543
+ const stagedBudgetEntries = options.budget.consumeTurnEnd(ctx);
1544
+ const budgetEntries = options.isEnabled() && !aborted ? stagedBudgetEntries : [];
1545
+ const entries = [...event.entries ?? [], ...budgetEntries];
1546
+ if (aborted) {
1547
+ overflowPending = false;
1548
+ overflowRecoveryUsed = false;
1549
+ return entries.length > 0 ? { entries } : void 0;
1550
+ }
1551
+ if (isOverflowLike(event.message, ctx)) {
1552
+ const queued = event.context.pendingMessages.length > 0 || ctx.hasPendingMessages();
1553
+ overflowPending = !queued && options.isEnabled() && options.budget.automaticResetEnabled(ctx);
1554
+ return entries.length > 0 ? { entries } : void 0;
1555
+ }
1556
+ if (event.outcome !== "error") {
1557
+ overflowPending = false;
1558
+ overflowRecoveryUsed = false;
1559
+ }
1560
+ if (!options.isEnabled() || event.outcome === "error") return entries.length > 0 ? { entries } : void 0;
1561
+ const autoThreshold = options.budget.resetDue(ctx);
1562
+ if (!requested && !autoThreshold) return entries.length > 0 ? { entries } : void 0;
1563
+ return resetBoundaryResult(entries, ctx);
1564
+ });
1565
+ pi.on("agent_before_settle", (event, ctx) => {
1566
+ if (!active || !overflowPending) return void 0;
1567
+ if (event.context.pendingMessages.length > 0 || ctx.hasPendingMessages()) return void 0;
1568
+ overflowPending = false;
1569
+ if (!options.isEnabled() || !options.budget.automaticResetEnabled(ctx) || event.outcome === "aborted" || ctx.signal?.aborted) return void 0;
1570
+ if (overflowRecoveryUsed) return void 0;
1571
+ overflowRecoveryUsed = true;
1572
+ return resetBoundaryResult(event.entries, ctx);
1573
+ });
1574
+ pi.on("session_before_compact", (event, ctx) => {
1575
+ if (!active) return void 0;
1576
+ if (event.signal.aborted) return { cancel: true };
1577
+ const markerExists = currentReset(ctx) !== void 0;
1578
+ if (options.isEnabled() || markerExists) {
1579
+ if (event.reason === "manual") {
1580
+ ctx.ui.notify("pi-context: /compact is disabled while context windows are active; use /wipe-memory to start a fresh window.", "warning");
1581
+ }
1582
+ return { cancel: true };
1583
+ }
1584
+ return void 0;
1585
+ });
1586
+ pi.on("agent_end", (_event, ctx) => {
1587
+ if (ctx.signal?.aborted) clear();
1588
+ });
1589
+ pi.on("agent_settled", () => {
1590
+ overflowPending = false;
1591
+ overflowRecoveryUsed = false;
1592
+ });
1593
+ pi.on("session_start", () => {
1594
+ clear();
1595
+ active = true;
1596
+ });
1597
+ pi.on("session_tree", clear);
1598
+ pi.on("session_shutdown", () => {
1599
+ clear();
1600
+ options.budget.clear();
1601
+ active = false;
1602
+ });
1603
+ return {
1604
+ request() {
1605
+ if (explicitRequested) return "rollover_already_pending";
1606
+ explicitRequested = true;
1607
+ return "rollover_requested";
1608
+ },
1609
+ clear
1610
+ };
1611
+ }
1612
+
1613
+ // src/context/runtime.ts
1614
+ var buildLabel = typeof define_PI_CONTEXT_BUILD_default === "undefined" ? "unbundled source (build unknown)" : `${define_PI_CONTEXT_BUILD_default.version} \xB7 build ${define_PI_CONTEXT_BUILD_default.sourceHash.slice(0, 12)}`;
1615
+ function bootContent(ctx, currentId, previousId, resetLine, notes) {
1616
+ return renderBootBlock({
1617
+ agentName: agentSlug(ctx),
1618
+ modelName: modelSlug(ctx),
1619
+ firstWindowId: rootWindowId(ctx.sessionManager.getSessionId()),
1620
+ currentWindowId: currentId,
1621
+ previousWindowId: previousId,
1622
+ resetLine,
1623
+ notes
1624
+ });
1625
+ }
1626
+ function buildResetDrafts(ctx, notifyIncompleteNotes) {
1627
+ const sessionPrefix = ctx.sessionManager.getSessionId().slice(0, 8);
1628
+ const usedWindowIds = new Set(
1629
+ ctx.sessionManager.getBranch().filter(isWindowMarker).map((entry) => entry.data.windowId)
1630
+ );
1631
+ let windowId;
1632
+ do {
1633
+ windowId = `pcw:${sessionPrefix}:${randomUUID2().slice(0, 8)}`;
1634
+ } while (usedWindowIds.has(windowId));
1635
+ const notes = loadNotesSnapshot(ctx);
1636
+ notifyIncompleteNotes?.(ctx, windowId, notes);
1637
+ return [
1638
+ { type: "custom", customType: RESET_MARKER_TYPE, data: { windowId } },
1639
+ {
1640
+ type: "custom_message",
1641
+ customType: BOOT_TYPE,
1642
+ content: bootContent(ctx, windowId, currentWindowId(ctx), true, notes),
1643
+ display: false,
1644
+ details: { windowId }
1645
+ },
1646
+ {
1647
+ type: "custom_message",
1648
+ customType: CONTINUATION_TYPE,
1649
+ content: CONTINUATION,
1650
+ display: false
1651
+ }
1652
+ ];
1653
+ }
1654
+ function ensureBoot(pi, ctx, notifyIncompleteNotes) {
1655
+ const reset = currentReset(ctx);
1656
+ const sessionId2 = ctx.sessionManager.getSessionId();
1657
+ const windowId = reset?.data?.windowId ?? rootWindowId(sessionId2);
1658
+ if (ctx.sessionManager.buildSessionProjection().messages.some((message) => isWindowBoot(message, windowId))) return;
1659
+ if (reset && !resetBootMayBeRepaired(ctx, reset.id, windowId)) return;
1660
+ let previousId = reset ? rootWindowId(sessionId2) : void 0;
1661
+ if (reset) {
1662
+ for (const entry of ctx.sessionManager.getBranch()) {
1663
+ if (entry.id === reset.id) break;
1664
+ if (isWindowMarker(entry)) previousId = entry.data.windowId;
1665
+ }
1666
+ }
1667
+ const notes = loadNotesSnapshot(ctx);
1668
+ notifyIncompleteNotes?.(ctx, windowId, notes);
1669
+ pi.sendMessage(
1670
+ { customType: BOOT_TYPE, content: bootContent(ctx, windowId, previousId, reset !== void 0, notes), display: false, details: { windowId } },
1671
+ { triggerTurn: false }
1672
+ );
1673
+ }
1674
+ function persistManualReset(pi, ctx, notifyIncompleteNotes) {
1675
+ const [marker, boot] = buildResetDrafts(ctx, notifyIncompleteNotes);
1676
+ pi.appendEntry(marker.customType, marker.data);
1677
+ pi.sendMessage(
1678
+ { customType: boot.customType, content: boot.content, display: boot.display, details: boot.details },
1679
+ { triggerTurn: false }
1680
+ );
1681
+ return boot.details.windowId;
1682
+ }
1683
+ function resetBootMayBeRepaired(ctx, markerId, windowId) {
1684
+ const branch = ctx.sessionManager.getBranch();
1685
+ const markerIndex = branch.findIndex((entry) => entry.id === markerId);
1686
+ if (markerIndex < 0) return false;
1687
+ const afterMarker = branch.slice(markerIndex + 1);
1688
+ if (afterMarker.some((entry) => isWindowBootEntry(entry, windowId))) return false;
1689
+ return !afterMarker.some((entry) => entry.type === "message" || entry.type === "custom_message" || entry.type === "compaction" || entry.type === "branch_summary");
1690
+ }
1691
+ function isWindowBootEntry(entry, windowId) {
1692
+ return entry.type === "custom_message" && entry.customType === BOOT_TYPE && typeof entry.details === "object" && entry.details !== null && typeof entry.details.windowId === "string" && entry.details.windowId === windowId;
1693
+ }
1694
+ function branchHasWindowMarker(ctx, fromId) {
1695
+ return ctx.sessionManager.getBranch(fromId).some((entry) => isWindowMarker(entry));
1696
+ }
1697
+ function registerContext(pi, settingsManager) {
1698
+ let enabled = true;
1699
+ let missingBootNotice;
1700
+ const incompleteNotesNotified = /* @__PURE__ */ new Set();
1701
+ const pendingResetNotices = /* @__PURE__ */ new Set();
1702
+ const notifyCommittedResets = (ctx, addedWindowId) => {
1703
+ if (addedWindowId) pendingResetNotices.add(addedWindowId);
1704
+ if (pendingResetNotices.size === 0) return;
1705
+ const branch = ctx.sessionManager.getBranch();
1706
+ for (const windowId of pendingResetNotices) {
1707
+ if (!branch.some((entry) => isWindowMarker(entry) && entry.data.windowId === windowId) || !branch.some((entry) => isWindowBootEntry(entry, windowId))) continue;
1708
+ pendingResetNotices.delete(windowId);
1709
+ ctx.ui.notify(`pi-context: memory cleared \xB7 ${windowId}`, "info");
1710
+ }
1711
+ };
1712
+ pi.on("turn_start", (_event, ctx) => notifyCommittedResets(ctx));
1713
+ pi.on("agent_settled", (_event, ctx) => {
1714
+ notifyCommittedResets(ctx);
1715
+ pendingResetNotices.clear();
1716
+ });
1717
+ const notifyIncompleteNotes = (ctx, windowId, snapshot) => {
1718
+ if (snapshot.unavailable.length === 0 || incompleteNotesNotified.has(windowId)) return;
1719
+ incompleteNotesNotified.add(windowId);
1720
+ const homes = snapshot.unavailable.map((home) => home.label).join(", ");
1721
+ ctx.ui.notify(`pi-context: notes index incomplete for ${homes}; notes_list can retry after recovery.`, "warning");
1722
+ };
1723
+ const migrationWarning = migrateLegacyHomes();
1724
+ if (migrationWarning) console.warn(`pi-context: ${migrationWarning}`);
1725
+ const budget = registerBudget(pi, () => enabled, settingsManager);
1726
+ pi.on("session_start", (_event, ctx) => {
1727
+ if (!enabled) return;
1728
+ missingBootNotice = void 0;
1729
+ pendingResetNotices.clear();
1730
+ ensureBoot(pi, ctx, notifyIncompleteNotes);
1731
+ });
1732
+ pi.on("session_tree", (_event, ctx) => {
1733
+ missingBootNotice = void 0;
1734
+ pendingResetNotices.clear();
1735
+ if (enabled) ensureBoot(pi, ctx, notifyIncompleteNotes);
1736
+ });
1737
+ pi.on("session_before_tree", (event, ctx) => {
1738
+ if (!event.preparation.userWantsSummary) return void 0;
1739
+ if (!branchHasWindowMarker(ctx) && !branchHasWindowMarker(ctx, event.preparation.targetId)) return void 0;
1740
+ ctx.ui.notify("pi-context: skipped branch summary across a reset window; navigation continues without erased history.", "info");
1741
+ return { summary: { summary: "" } };
1742
+ });
1743
+ pi.on("context_with_system", (event, ctx) => {
1744
+ const reset = currentReset(ctx);
1745
+ const windowId = reset?.data.windowId ?? rootWindowId(ctx.sessionManager.getSessionId());
1746
+ try {
1747
+ return { messages: reset ? projectWindow(event.messages, windowId) : projectRootWindow(event.messages, windowId) };
1748
+ } catch (error) {
1749
+ if (missingBootNotice !== windowId) {
1750
+ missingBootNotice = windowId;
1751
+ ctx.ui.notify(`pi-context: active context window ${windowId} has no visible boot; request cancelled safely. Use /wipe-memory to start another window.`, "error");
1752
+ }
1753
+ ctx.abort();
1754
+ const safeHead = getCurrentSystemMessage2(event.messages);
1755
+ return { messages: safeHead ? [safeHead] : [] };
1756
+ }
1757
+ });
1758
+ pi.registerCommand("pi-context", {
1759
+ description: "Show loaded version/build and toggle pi-context context windows",
1760
+ getArgumentCompletions: (prefix) => ["on", "off"].filter((a) => a.startsWith(prefix)).map((a) => ({ value: a, label: a })),
1761
+ handler: async (args, cmdCtx) => {
1762
+ const arg = args.trim().toLowerCase();
1763
+ if (arg === "on") {
1764
+ enabled = true;
1765
+ ensureBoot(pi, cmdCtx, notifyIncompleteNotes);
1766
+ } else if (arg === "off") {
1767
+ enabled = false;
1768
+ budget.clear();
1769
+ resets.clear();
1770
+ } else if (arg !== "") {
1771
+ cmdCtx.ui.notify("Usage: /pi-context [on|off]", "error");
1772
+ return;
1773
+ }
1774
+ cmdCtx.ui.notify(`pi-context: ${enabled ? "on" : "off"} \xB7 ${buildLabel} \xB7 Pi ${VERSION}`, "info");
1775
+ }
1776
+ });
1777
+ pi.registerCommand("wipe-memory", {
1778
+ description: "Persist a fresh context window without calling the model",
1779
+ handler: async (_args, cmdCtx) => {
1780
+ if (!enabled) {
1781
+ cmdCtx.ui.notify("pi-context: /wipe-memory requires /pi-context on.", "error");
1782
+ return;
1783
+ }
1784
+ await cmdCtx.waitForIdle();
1785
+ if (!enabled) return;
1786
+ resets.clear();
1787
+ notifyCommittedResets(cmdCtx, persistManualReset(pi, cmdCtx, notifyIncompleteNotes));
1788
+ }
1789
+ });
1790
+ pi.registerTool(defineTool4({
1791
+ name: "wipe_memory",
1792
+ label: "Wipe memory",
1793
+ description: "Wipe your in-context memory and start a fresh context window. Your session, notes, and history survive.",
1794
+ parameters: Type5.Object({}, { additionalProperties: false }),
1795
+ async execute() {
1796
+ if (!enabled) return output({ error: "pi-context is off (/pi-context on to enable)" });
1797
+ return output({ status: resets.request() }, void 0, true);
1798
+ }
1799
+ }));
1800
+ const resets = registerResetLifecycle(pi, {
1801
+ isEnabled: () => enabled,
1802
+ buildReset: (ctx) => {
1803
+ const drafts = buildResetDrafts(ctx, notifyIncompleteNotes);
1804
+ pendingResetNotices.add(drafts[1].details.windowId);
1805
+ return drafts;
1806
+ },
1807
+ budget
1808
+ });
1809
+ }
1810
+
1811
+ // src/notes/session-replay.ts
1812
+ function isNoteOperation(data) {
1813
+ if (typeof data !== "object" || data === null) return false;
1814
+ const op = data;
1815
+ return (op.op === "write" || op.op === "append") && typeof op.path === "string" && (op.text === void 0 || typeof op.text === "string") && (op.stale === void 0 || typeof op.stale === "boolean") && (op.text !== void 0 || op.stale !== void 0) && typeof op.createdAt === "number" && Number.isFinite(new Date(op.createdAt).getTime()) && typeof op.updatedAt === "number" && Number.isFinite(new Date(op.updatedAt).getTime());
1816
+ }
1817
+ function notesFromSession(ctx) {
1818
+ const files = /* @__PURE__ */ new Map();
1819
+ for (const entry of ctx.sessionManager.getBranch()) {
1820
+ if (entry.type !== "custom" || entry.customType !== NOTE_TYPE || !isNoteOperation(entry.data)) continue;
1821
+ const op = entry.data;
1822
+ try {
1823
+ assertVirtualPath(op.path);
1824
+ } catch {
1825
+ continue;
1826
+ }
1827
+ const previous = files.get(op.path);
1828
+ const hasText = op.text !== void 0;
1829
+ if (!hasText && !previous) continue;
1830
+ const text = hasText ? op.op === "append" ? `${previous?.text ?? ""}${op.text}` : op.text : previous.text;
1831
+ if (Buffer.byteLength(text, "utf8") > MAX_NOTE_BYTES) continue;
1832
+ const stale = hasText ? op.stale ?? false : op.stale ?? previous.stale;
1833
+ files.set(op.path, { text, stale, createdAt: previous?.createdAt ?? op.createdAt, updatedAt: op.updatedAt });
1834
+ }
1835
+ return files;
1836
+ }
1837
+
1838
+ // src/index.ts
1839
+ function registerPiContext(pi, settingsManager) {
1840
+ const [major, minor] = VERSION2.split(".").map(Number);
1841
+ if (!(major > 0 || major === 0 && minor >= 87)) {
1842
+ throw new Error(`pi-context requires Pi >= 0.87.0; running ${VERSION2}. Upgrade Pi and restart the process; /reload only reloads extensions.`);
1843
+ }
1844
+ registerContext(pi, settingsManager);
1845
+ registerHistoryTools(pi);
1846
+ registerNotesTools(pi);
1847
+ }
1848
+ function createPiContext(options = {}) {
1849
+ return (pi) => registerPiContext(pi, options.settingsManager);
1850
+ }
1851
+ function piContext(pi) {
1852
+ registerPiContext(pi);
1853
+ }
1854
+ var internal = { MAX_NOTE_BYTES, NOTE_TYPE, BOOT_TYPE, GUIDANCE_TYPE, WARNING_TYPE, CONTINUATION_TYPE, WARNING_PROMPT, WARNING_RUNWAY_TOKENS, RESET_MARKER_TYPE, RESET_SUMMARY, 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 };
1855
+ export {
1856
+ createPiContext,
1857
+ piContext as default,
1858
+ historyFromSession,
1859
+ internal,
1860
+ notesFromSession
1861
+ };