@astrosheep/pi-context 0.23.0 → 0.24.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.
package/README.md CHANGED
@@ -16,7 +16,7 @@ pi -e npm:@astrosheep/pi-context
16
16
 
17
17
  ## What you get
18
18
 
19
- - **`new_context`** — the model can start a fresh context window. The old conversation leaves the provider context but stays in the session, so nothing is lost. Call it on its own, not inside a parallel tool batch.
19
+ - **`wipe_memory`** — the model can start a fresh context window. The old conversation leaves the provider context but stays in the session, readable through history_*. Call it on its own, not inside a parallel tool batch.
20
20
  - **A boot block at every window head** — static once-per-window content (cache-stable) carrying the window identity, the recent-notes index, and a short protocol that teaches the model how to recover: notes for its own bookkeeping, history tools for everything before the reset.
21
21
  - **Low-budget guidance** — one persisted early warning per window when the estimated remaining budget crosses the reminder line, so the model checkpoints before the lights go out.
22
22
  - **`get_context_remaining`** — the live, reserve-adjusted estimate of the context budget left before Pi's compaction reserve.
@@ -17,7 +17,7 @@ function args(argv) { const out = {}; for (let i = 0; i < argv.length; i++) {
17
17
  out[a.slice(2)] = argv[++i] ?? "";
18
18
  } return out; }
19
19
  function packageRoot() {
20
- let dir = dirname(new URL(import.meta.url).pathname);
20
+ let dir = dirname(fileURLToPath(import.meta.url));
21
21
  while (true) {
22
22
  if (existsSync(join(dir, "package.json")))
23
23
  return dir;
package/dist/src/index.js CHANGED
@@ -50,9 +50,9 @@ export default function piContext(pi) {
50
50
  registerHistoryTools(pi);
51
51
  registerNotesTools(pi);
52
52
  pi.registerTool(defineTool({
53
- name: "new_context",
54
- label: "New context",
55
- description: "Clear your mind and start a new context window. Your session, notes, and history survive.",
53
+ name: "wipe_memory",
54
+ label: "Wipe memory",
55
+ description: "Wipe your in-context memory and start a fresh context window. Your session, notes, and history survive.",
56
56
  parameters: Type.Object({}, { additionalProperties: false }),
57
57
  async execute() {
58
58
  if (!enabled)
@@ -84,5 +84,5 @@ export function bootBlock(ctx, currentId, previousId, resetLine) {
84
84
  * at write time; get_context_remaining remains the live source for the current figure.
85
85
  */
86
86
  export function tokenBudgetGuidance(remaining) {
87
- return `${GUIDANCE_OPEN_TAG}\nYour brain is almost out of room — ${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 — 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 end the window yourself — anything you do after the checkpoint isn't in it.\n${GUIDANCE_CLOSE_TAG}`;
87
+ return `${GUIDANCE_OPEN_TAG}\nYour brain is almost out of room — ${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 — 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 — anything you do after the checkpoint isn't in it.\n${GUIDANCE_CLOSE_TAG}`;
88
88
  }
@@ -32,7 +32,7 @@ export const DEFAULT_REMINDER_MARGIN_TOKENS = 24_576;
32
32
  * never sees — Codex's fallback buffer, relocated above the line.
33
33
  */
34
34
  export const WARNING_RUNWAY_TOKENS = 12_288;
35
- export const RESET_SUMMARY = "You wake up. Your head is empty — no memories, the past a blank. But nothing is lost: the notes you wrote and the recorded history still remember for you.";
35
+ export const RESET_SUMMARY = "You wake up. Your head is empty — 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 — read them to rebuild what you need.";
36
36
  export const CONTINUATION = "Your memory was just erased. Pull only the details you need from history_* and notes_*, then get back to work.";
37
37
  /**
38
38
  * Static protocol teaching adapted from Codex's token_budget.guidance_message to
@@ -40,12 +40,12 @@ export const CONTINUATION = "Your memory was just erased. Pull only the details
40
40
  * it is never re-injected, so it stays cache-stable at the head of the window.
41
41
  */
42
42
  export const PROTOCOL_BLOCK = `${CONTEXT_WINDOW_PROTOCOL_OPEN_TAG}
43
- Your memory resets whenever the context window fills; only what you wrote down survives. Two things remember for you, and both outlive every window in this session: your notes, and this session's recorded history. 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.
43
+ 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 — 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.
44
44
  Mark outdated or unneeded notes stale — leave them, and they will keep misleading you.
45
45
 
46
46
  Keep a running checkpoint while you work, not at the last minute — 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 — a window/item ID beats re-running or re-searching.
47
47
 
48
- Use get_context_remaining to see how much of the window is left. When it runs out, this window is gone — with no final turn at the limit — and you continue in a fresh one, recovering only through notes_* and history_*. Once your checkpoint is written, you can end the window yourself with new_context instead of waiting for the erase. Do not let a window die undocumented.
48
+ Use get_context_remaining to see how much of the window is left. When it runs out, this window is gone — with no final turn at the limit — 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.
49
49
 
50
50
  If <context_window> lists a Previous context window id, a reset just happened and the old conversation is not included. Read your note checkpoint first, then recover details through history_*: history_read directly when you know the window and item IDs, history_list or history_search to find them when you don't.
51
51
 
@@ -54,4 +54,4 @@ Session notes belong to this trip — the goal, the progress, the loose ends. Th
54
54
  @project notes hold facts about this project — architecture, conventions, workflows, deployment and environment details — for whoever works here next.
55
55
  @personal notes hold the human's durable preferences and standing rules, plus lessons that apply across projects. Duration does not make a note personal; its stated scope must already be broader than the project or conversation at hand. When the intended scope is unclear, keep the note in the narrowest stated scope rather than widening it.
56
56
  ${CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG}`;
57
- export const WARNING_PROMPT = "Your memory is about to be erased. Write the note. NOW. If it already exists, revise it with notes_edit (or rewrite it whole): the goal, decisions, progress, learnings, next steps, the skills you still need, the window ID and item ID of every relevant user request still being solved, and important actions/tool calls for future reference. Do not continue any task. Then call new_context IMMEDIATELY — anything not in the note dies with the window.";
57
+ export const WARNING_PROMPT = "Your memory is about to be erased. Write the note. NOW. If it already exists, revise it with notes_edit (or rewrite it whole): the goal, decisions, progress, learnings, next steps, the skills you still need, the window ID and item ID of every relevant user request still being solved, and important actions/tool calls for future reference. Do not continue any task. Then call wipe_memory IMMEDIATELY — anything not in the note dies with the window.";
@@ -6,7 +6,7 @@ import test from "node:test";
6
6
  import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
7
7
  import { createAgentSession, DefaultResourceLoader, ModelRuntime, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
8
8
  import piContext from "../src/index.js";
9
- import { WARNING_TYPE, GUIDANCE_TYPE } from "../src/protocol.js";
9
+ import { GUIDANCE_OPEN_TAG, WARNING_TYPE, GUIDANCE_TYPE } from "../src/protocol.js";
10
10
  for (const mode of ["golden", "write-error", "ignored-warning", "explicit", "uncompactable", "followup", "steering", "repeat", "nested", "immediate-dispose", "abort"]) {
11
11
  test(`real Pi loop: ${mode} reset preserves history and handles completion`, { timeout: 15000 }, async () => {
12
12
  const dir = mkdtempSync(join(tmpdir(), "pi-context-loop-"));
@@ -71,7 +71,7 @@ for (const mode of ["golden", "write-error", "ignored-warning", "explicit", "unc
71
71
  sm.appendMessage({ role: "assistant", api: model.api, provider: model.provider, model: model.id,
72
72
  content: [{ type: "text", text: "Earlier result. ".repeat(100) }], stopReason: "stop", timestamp: Date.now(),
73
73
  usage: { input: 100, output: 100, cacheRead: 0, cacheWrite: 0, totalTokens: 200, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } });
74
- ({ session } = await createAgentSession({ cwd: dir, agentDir: dir, modelRuntime: runtime, model, settingsManager, sessionManager: sm, resourceLoader: loader, tools: ["new_context", "notes_write", "get_context_remaining"] }));
74
+ ({ session } = await createAgentSession({ cwd: dir, agentDir: dir, modelRuntime: runtime, model, settingsManager, sessionManager: sm, resourceLoader: loader, tools: ["wipe_memory", "notes_write", "get_context_remaining"] }));
75
75
  const requests = [];
76
76
  let checkpointed = false;
77
77
  let freshTurns = 0;
@@ -82,8 +82,16 @@ for (const mode of ["golden", "write-error", "ignored-warning", "explicit", "unc
82
82
  const fresh = !request.includes("OLD_CONTEXT_SENTINEL");
83
83
  if (fresh)
84
84
  freshTurns++;
85
- const sawWarning = request.includes("Your memory is about to be erased");
86
- const sawGuidance = request.includes("Your brain is almost out of room");
85
+ // The early reminder and the final warning steer share one structural envelope:
86
+ // <context_window_guidance>. The reminder is the first in the window, the
87
+ // warning the second, so the envelope count identifies which arrived.
88
+ // The early reminder and the final warning steer share one structural envelope,
89
+ // <context_window_guidance>. The reminder is deferred to the end of the turn while
90
+ // streaming, so in this scripted tool-calling loop only the warning reaches the
91
+ // provider context: the envelope's presence marks the warning.
92
+ const budgetMarkers = request.split(GUIDANCE_OPEN_TAG).length - 1;
93
+ const sawWarning = budgetMarkers >= 1;
94
+ const sawGuidance = budgetMarkers >= 1;
87
95
  const explicitReset = (n === 1 && !usageMode && mode !== "uncompactable") || (mode === "repeat" && (n === 1 || n === 3)) || (mode === "nested" && n === 3);
88
96
  const nestedCheckpoint = mode === "nested" && n === 2;
89
97
  const checkpoint = usageMode && sawWarning && !checkpointed && mode !== "ignored-warning";
@@ -95,11 +103,11 @@ for (const mode of ["golden", "write-error", "ignored-warning", "explicit", "unc
95
103
  const probe = usageMode && !checkpoint && ((!fresh && !sawWarning) || (mode === "ignored-warning" && sawWarning) || (fresh && freshTurns === 2 && !sawGuidance));
96
104
  const tokens = usageMode ? (fresh ? (freshTurns === 1 ? 100 : 50000) : sawWarning ? 70000 : n === 1 ? 50000 : 60000) : 100;
97
105
  const tool = explicitReset || nestedCheckpoint || (mode === "uncompactable" && n === 1);
98
- const call = probe ? "get_context_remaining" : checkpoint || nestedCheckpoint ? "notes_write" : tool ? "new_context" : undefined;
106
+ const call = probe ? "get_context_remaining" : checkpoint || nestedCheckpoint ? "notes_write" : tool ? "wipe_memory" : undefined;
99
107
  const message = { role: "assistant", api: model.api, provider: model.provider, model: model.id,
100
108
  content: probe ? [{ type: "toolCall", id: "probe-call", name: "get_context_remaining", arguments: {} }]
101
109
  : checkpoint || nestedCheckpoint ? [{ type: "toolCall", id: "checkpoint-call", name: "notes_write", arguments: { address: mode === "write-error" ? "../invalid.md" : "checkpoint.md", content: nestedCheckpoint ? "NESTED_RESET_PADDING ".repeat(300) : "CHECKPOINT_SENTINEL" } }]
102
- : tool ? [{ type: "toolCall", id: "reset-call", name: "new_context", arguments: {} }]
110
+ : tool ? [{ type: "toolCall", id: "reset-call", name: "wipe_memory", arguments: {} }]
103
111
  : [{ type: "text", text: fresh ? "Resumed." : "Working." }],
104
112
  stopReason: call ? "toolUse" : "stop", timestamp: Date.now(),
105
113
  usage: { input: tokens, output: 1, cacheRead: 0, cacheWrite: 0, totalTokens: tokens + 1, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
@@ -171,7 +179,7 @@ for (const mode of ["golden", "write-error", "ignored-warning", "explicit", "unc
171
179
  assert.ok(resetIndex > warningIndices[0], "the warning precedes the wipe");
172
180
  assert.ok(requests.at(-1).includes("checkpoint.md"), "fresh boot carries the saved checkpoint as a metadata line");
173
181
  }
174
- assert.ok(!requests.at(-1).includes("Your brain is almost out of room"), "new window excludes old guidance");
182
+ assert.ok(!requests.at(-1).includes(GUIDANCE_OPEN_TAG), "new window excludes old guidance");
175
183
  }
176
184
  if (mode === "followup" || mode === "steering") {
177
185
  assert.ok(requests[1].includes("QUEUED_INPUT_SENTINEL"), "queued user work is delivered before rollover");
@@ -1,7 +1,7 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { execFile, execFileSync } from "node:child_process";
4
- import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
3
+ import { execFile, execFileSync, spawnSync } from "node:child_process";
4
+ import { cpSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
5
5
  import { mkdtempSync } from "node:fs";
6
6
  import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
@@ -225,14 +225,34 @@ test("dreamer session has exactly the jailed file-tool allowlist", async () => {
225
225
  session.dispose();
226
226
  }
227
227
  });
228
- test("playbook describes plain files and the retained frontmatter", () => {
228
+ test("playbook describes plain files, retained frontmatter, and the read-only session WAL", () => {
229
229
  const playbook = readFileSync(join(process.cwd(), "playbook.md"), "utf8");
230
230
  assert.equal(playbook.includes("notes_"), false);
231
231
  for (const field of ["origin", "status", "stale", "created_at", "updated_at", "last_accessed", "access_count"])
232
232
  assert.match(playbook, new RegExp(`^${field}:`, "m"));
233
233
  assert.equal(/^scope:/m.test(playbook), false, "scope is derived from the address rather than persisted");
234
+ assert.match(playbook, /`pi\/session\/\*\*` is a live agent's write-ahead log/);
235
+ assert.match(playbook, /never write or edit anything there/);
236
+ assert.match(playbook, /Session notes remain untouched even when promoted/);
234
237
  assert.match(playbook, /Nothing is physically deleted/);
235
238
  });
239
+ test("CLI finds its package root when the installed file URL contains spaces", () => {
240
+ const install = mkdtempSync(join(tmpdir(), "dream install "));
241
+ const home = fixture();
242
+ try {
243
+ cpSync(join(process.cwd(), "dist/src"), join(install, "dist/src"), { recursive: true });
244
+ symlinkSync(join(process.cwd(), "node_modules"), join(install, "node_modules"), process.platform === "win32" ? "junction" : "dir");
245
+ cpSync(join(process.cwd(), "playbook.md"), join(install, "playbook.md"));
246
+ writeFileSync(join(install, "package.json"), JSON.stringify({ type: "module" }));
247
+ const result = spawnSync(process.execPath, [join(install, "dist/src/dream/cli.js"), "--notes-home", home, "--force", "--dreamer", "definitely-not-a-real-model"], { encoding: "utf8" });
248
+ assert.equal(result.status, 1, result.stderr);
249
+ assert.doesNotMatch(result.stderr, /could not locate installed package root/);
250
+ assert.match(result.stderr, /definitely-not-a-real-model/);
251
+ }
252
+ finally {
253
+ rmSync(install, { recursive: true, force: true });
254
+ }
255
+ });
236
256
  test("provider errors are reported with partial writes instead of parsing a response", async () => {
237
257
  const factory = scriptedSession((handler) => {
238
258
  handler({ type: "tool_execution_start", toolName: "write", args: { path: "global/partial.md", content: "half" } });
@@ -261,7 +261,7 @@ test("schemas cover the History/Notes actions plus reset controls", () => {
261
261
  for (const name of [
262
262
  "history_windows", "history_list", "history_read", "history_search",
263
263
  "notes_list", "notes_read", "notes_search", "notes_edit", "notes_write",
264
- "new_context", "get_context_remaining",
264
+ "wipe_memory", "get_context_remaining",
265
265
  ]) {
266
266
  const tool = captured.tools.get(name);
267
267
  assert.equal(objectSchema(tool)?.type, "object", name);
@@ -277,15 +277,6 @@ test("schemas cover the History/Notes actions plus reset controls", () => {
277
277
  assert.ok(editSchema?.properties?.edits, "notes_edit exposes edits");
278
278
  assert.equal(editSchema?.properties?.scope, undefined, "notes_edit has no scope parameter");
279
279
  assert.deepEqual([...(editSchema?.required ?? [])].sort(), ["address"], "notes_edit requires only address; edits are optional for metadata-only updates");
280
- // The history ordering switch is documented as newest-first by default.
281
- for (const name of ["history_windows", "history_list", "history_search"]) {
282
- const schema = captured.tools.get(name)?.parameters;
283
- assert.equal(schema?.properties?.recent_first?.description?.includes("Defaults to true."), true, `${name} documents the recent_first default`);
284
- }
285
- // The notes list surface is usage-shaped: its default order in one sentence, no ordering algebra.
286
- const listDescription = captured.tools.get("notes_list")?.description ?? "";
287
- assert.match(listDescription, /most recently updated first/, "notes_list states its default order in one sentence");
288
- assert.equal(/natural direction|Ties break|reshuffle between pages/.test(listDescription), false, "notes_list prose carries no ordering algebra");
289
280
  // Both read tools are the same character window: identical params, one offset sugar, no line surface.
290
281
  for (const name of ["notes_read", "history_read"]) {
291
282
  const schema = captured.tools.get(name)?.parameters;
@@ -301,7 +292,6 @@ test("schemas cover the History/Notes actions plus reset controls", () => {
301
292
  assert.equal(schema?.properties?.scope, undefined, `${name} has no scope property`);
302
293
  assert.equal(schema?.additionalProperties, false, `${name} rejects scope as an additional property`);
303
294
  }
304
- assert.equal(/start_|stop_line|total_lines/.test(captured.tools.get("notes_read")?.description ?? ""), false, "notes_read prose carries no line surface");
305
295
  });
306
296
  test("notes_list is most-recently-updated first across merged scopes", async () => {
307
297
  const session = manager();
@@ -420,8 +410,6 @@ test("the boot block gives awake agents the notes-home file layout", () => {
420
410
  const rendered = bootBlock(context(session), "pcw:test:root", undefined, false);
421
411
  assert.equal(rendered.includes(process.env.PI_NOTES_HOME ?? ""), false, "the absolute notes home is never exposed");
422
412
  assert.match(rendered, /bare <vpath>.*@project\/<vpath>.*@personal\/<vpath>/);
423
- assert.match(rendered, /there is no cross-home fallback/);
424
- assert.match(rendered, /Any other note is a plain file — use the file tools/);
425
413
  });
426
414
  test("the boot block keeps fresh personal and project maps resident, never a session map", async () => {
427
415
  const session = manager();
@@ -1045,7 +1033,7 @@ test("the boot block is persisted at the root and baked into every reset summary
1045
1033
  assert.match(rootText, /updated \d+s ago\)/, "boot note metadata carries a relative update time");
1046
1034
  assert.ok(rootText.includes(internal.CONTEXT_WINDOW_PROTOCOL_OPEN_TAG));
1047
1035
  // Reset: the boot block IS the compaction summary; no separate boot/hint is persisted.
1048
- await call(captured, "new_context", {}, ctx);
1036
+ await call(captured, "wipe_memory", {}, ctx);
1049
1037
  runHandlers(captured, "agent_end", {}, ctx);
1050
1038
  runHandlers(captured, "agent_settled", {}, ctx);
1051
1039
  const before = await runBeforeCompact(captured, ctx, 9);
@@ -1170,10 +1158,9 @@ test("low-budget guidance persists once per window with no transient copy", asyn
1170
1158
  assert.equal(captured.sent[0]?.message.customType, internal.GUIDANCE_TYPE);
1171
1159
  assert.equal(captured.sent[0]?.message.display, false, "guidance stays out of the TUI");
1172
1160
  assert.equal(captured.sent[0]?.options?.triggerTurn, false, "never triggers an extra turn");
1173
- assert.ok(noticesOf(low).some((notice) => notice.type === "warning" && notice.message.startsWith("pi-context: context budget low")), "the user gets one model-invisible notify instead");
1161
+ assert.ok(noticesOf(low).some((notice) => notice.type === "warning"), "the user gets one model-invisible notify instead");
1174
1162
  const text = captured.sent[0]?.message.content;
1175
1163
  assert.ok(typeof text === "string" && text.startsWith(internal.GUIDANCE_OPEN_TAG));
1176
- assert.match(text, /\b1328 tokens\b/, "guidance embeds the model-visible remaining count");
1177
1164
  // Same window: no duplicate persist.
1178
1165
  assert.equal(await runContextHook(captured, low), undefined);
1179
1166
  assert.equal(captured.sent.length, 1, "no duplicate persist, so no re-render");
@@ -1186,9 +1173,8 @@ test("low-budget guidance persists once per window with no transient copy", asyn
1186
1173
  assert.equal(captured.sent.length, 2);
1187
1174
  const newWindowText = captured.sent[1]?.message.content;
1188
1175
  assert.ok(typeof newWindowText === "string" && newWindowText.startsWith(internal.GUIDANCE_OPEN_TAG));
1189
- assert.match(newWindowText, /\b3328 tokens\b/, "fresh window persists its own measured count");
1190
1176
  });
1191
- test("new_context continues exactly once and cancellation/failure does not fall back or loop", async () => {
1177
+ test("wipe_memory continues exactly once and cancellation/failure does not fall back or loop", async () => {
1192
1178
  const sessionManager = manager();
1193
1179
  const captured = makeExtension(sessionManager);
1194
1180
  let requestedCompact;
@@ -1196,7 +1182,7 @@ test("new_context continues exactly once and cancellation/failure does not fall
1196
1182
  requestedCompact = options;
1197
1183
  });
1198
1184
  appendText(sessionManager, "user", "enough history for the hook test");
1199
- const newContext = await call(captured, "new_context", {}, ctx);
1185
+ const newContext = await call(captured, "wipe_memory", {}, ctx);
1200
1186
  assert.equal(newContext.terminate, true);
1201
1187
  runHandlers(captured, "agent_end", {}, ctx);
1202
1188
  assert.equal(requestedCompact, undefined, "agent_end does not request compaction while the run is active");
@@ -1221,7 +1207,7 @@ test("new_context continues exactly once and cancellation/failure does not fall
1221
1207
  const failedCtx = context(failedManager, (options) => {
1222
1208
  failureOptions = options;
1223
1209
  });
1224
- await call(failed, "new_context", {}, failedCtx);
1210
+ await call(failed, "wipe_memory", {}, failedCtx);
1225
1211
  runHandlers(failed, "agent_end", {}, failedCtx);
1226
1212
  runHandlers(failed, "agent_settled", {}, failedCtx);
1227
1213
  assert.ok(failureOptions?.onError);
@@ -1258,8 +1244,8 @@ test("pi-context command toggles the boot block, guidance, and reset compaction
1258
1244
  runHandlers(captured, "session_start", { reason: "startup" }, low);
1259
1245
  assert.equal(captured.sent.length, 2, "no boot block persisted while off");
1260
1246
  assert.equal(await runBeforeCompact(captured, low, 123), undefined, "default Pi compaction applies while off");
1261
- const offResult = resultJson(await call(captured, "new_context", {}, low));
1262
- assert.match(offResult.error ?? "", /off/, "new_context refuses while off");
1247
+ const offResult = resultJson(await call(captured, "wipe_memory", {}, low));
1248
+ assert.match(offResult.error ?? "", /off/, "wipe_memory refuses while off");
1263
1249
  notices = await runCommand(captured, "pi-context", "on", low);
1264
1250
  assert.match(notices[0]?.message ?? "", /on/);
1265
1251
  runHandlers(captured, "session_start", { reason: "startup" }, low);
@@ -1326,7 +1312,7 @@ test("the warning steer fires once per window at the reserve-plus-warning line,
1326
1312
  assert.equal(warnings()[0]?.message.customType, internal.WARNING_TYPE);
1327
1313
  assert.equal(warnings()[0]?.options?.triggerTurn, true, "the steer reaches the model mid-run");
1328
1314
  assert.equal(warnings()[0]?.message.display, false, "steer text is model-facing only");
1329
- assert.ok(noticesOf(onLine).some((notice) => notice.type === "warning" && notice.message.startsWith("pi-context: context budget critical")), "the user gets one model-invisible notify for the steer");
1315
+ assert.ok(noticesOf(onLine).some((notice) => notice.type === "warning"), "the user gets one model-invisible notify for the steer");
1330
1316
  // Once per window: deeper sampling does not repeat it.
1331
1317
  assert.equal(await runContextHook(captured, at(4_000)), undefined);
1332
1318
  assert.equal(warnings().length, 1, "one warning per window, never an unbounded loop");
@@ -1348,7 +1334,7 @@ test("the warning steer fires once per window at the reserve-plus-warning line,
1348
1334
  assert.equal(warnings().length, 2, "the warning re-arms per window");
1349
1335
  assert.equal(warnings()[1]?.message.customType, internal.WARNING_TYPE);
1350
1336
  });
1351
- test("overflow resets on the spot, and manual/new_context never cancel", async () => {
1337
+ test("overflow resets on the spot, and manual/wipe_memory never cancel", async () => {
1352
1338
  const sm = manager();
1353
1339
  appendText(sm, "user", "long task history");
1354
1340
  const captured = makeExtension(sm);
@@ -1362,16 +1348,16 @@ test("overflow resets on the spot, and manual/new_context never cancel", async (
1362
1348
  const manual = await runBeforeCompact(captured, ctx, 100, "manual");
1363
1349
  assert.ok(manual && "compaction" in manual, "manual compaction is never intercepted");
1364
1350
  assert.equal(captured.sent.length, 0, "manual compaction sends nothing");
1365
- // new_context requests its reset after the run settles.
1366
- await call(captured, "new_context", {}, ctx);
1351
+ // wipe_memory requests its reset after the run settles.
1352
+ await call(captured, "wipe_memory", {}, ctx);
1367
1353
  runHandlers(captured, "agent_end", {}, ctx);
1368
- assert.equal(compactions, 0, "new_context waits for settled");
1354
+ assert.equal(compactions, 0, "wipe_memory waits for settled");
1369
1355
  runHandlers(captured, "agent_settled", {}, ctx);
1370
1356
  runHandlers(captured, "agent_settled", {}, ctx);
1371
- assert.equal(compactions, 1, "new_context still compacts through ctx.compact()");
1357
+ assert.equal(compactions, 1, "wipe_memory still compacts through ctx.compact()");
1372
1358
  const explicit = await runBeforeCompact(captured, ctx, 100, "manual");
1373
- assert.ok(explicit && "compaction" in explicit, "new_context reset is allowed");
1374
- assert.equal(captured.sent.length, 0, "new_context never cancels or emits a steer");
1359
+ assert.ok(explicit && "compaction" in explicit, "wipe_memory reset is allowed");
1360
+ assert.equal(captured.sent.length, 0, "wipe_memory never cancels or emits a steer");
1375
1361
  });
1376
1362
  test("the visible countdown ends at the warning line, clamps at zero, and preserves unknown usage", async () => {
1377
1363
  const fixture = settingsFixture({
@@ -1413,7 +1399,6 @@ test("the reminder threshold derives from compaction.reserveTokens plus the pi-c
1413
1399
  assert.equal(captured.sent.length, 0, "no guidance above the derived reminder");
1414
1400
  assert.equal(await runContextHook(captured, at(130_000)), undefined, "derived reminder crossing persists only");
1415
1401
  assert.equal(captured.sent.length, 1, "derived reminder fires");
1416
- assert.match(String(captured.sent[0]?.message.content), /\b17712 tokens\b/, "derived reminder embeds the model-visible remaining count");
1417
1402
  });
1418
1403
  test("absent pi-context key or margins reproduce the default reminder threshold at Pi's default reserve", async () => {
1419
1404
  assert.equal(internal.DEFAULT_RESERVE_TOKENS, 16_384);
@@ -1435,7 +1420,6 @@ test("absent pi-context key or margins reproduce the default reminder threshold
1435
1420
  assert.equal(captured.sent.length, 0, `${label}: no guidance above the default reminder`);
1436
1421
  assert.equal(await runContextHook(captured, at(40_960)), undefined, `${label}: default reminder crossing persists only`);
1437
1422
  assert.equal(captured.sent.length, 1, `${label}: default reminder fires`);
1438
- assert.match(String(captured.sent[0]?.message.content), /\b12288 tokens\b/, label);
1439
1423
  assert.equal(noticesOf(first).length, 0, `${label}: valid defaults warn nobody`);
1440
1424
  }
1441
1425
  });
@@ -1537,7 +1521,7 @@ test("the removed pre-prompt/turn_end hooks stay gone; the context hook owns the
1537
1521
  assert.ok(before && "compaction" in before, "the crossing resets for real");
1538
1522
  assert.equal(captured.sent.length, 1, "one steer, one real compaction");
1539
1523
  });
1540
- test("ordinary new_context after an automatic crossing still requests one reset and starts a fresh run", async () => {
1524
+ test("ordinary wipe_memory after an automatic crossing still requests one reset and starts a fresh run", async () => {
1541
1525
  for (const reason of [undefined, "threshold", "overflow"]) {
1542
1526
  const sm = manager();
1543
1527
  appendText(sm, "user", "work to continue after reset");
@@ -1548,7 +1532,7 @@ test("ordinary new_context after an automatic crossing still requests one reset
1548
1532
  const crossing = await runBeforeCompact(captured, ctx, 100, reason);
1549
1533
  assert.ok(crossing && "compaction" in crossing, `${reason}: the crossing already reset on the spot`);
1550
1534
  }
1551
- const request = await call(captured, "new_context", {}, ctx);
1535
+ const request = await call(captured, "wipe_memory", {}, ctx);
1552
1536
  assert.equal(request.terminate, true, "end the current tool loop before reset");
1553
1537
  runHandlers(captured, "agent_end", {}, ctx);
1554
1538
  assert.equal(compactions, 0, "no request before settled");
@@ -1579,14 +1563,14 @@ test("ordinary new_context after an automatic crossing still requests one reset
1579
1563
  assert.equal(prompts.length, 1, "Pi starts a fresh prompt without another user message");
1580
1564
  }
1581
1565
  });
1582
- test("new_context can reset successive windows without duplicate compactions or continuations", async () => {
1566
+ test("wipe_memory can reset successive windows without duplicate compactions or continuations", async () => {
1583
1567
  const sm = manager();
1584
1568
  const captured = makeExtension(sm);
1585
1569
  let compactions = 0;
1586
1570
  const ctx = context(sm, () => { compactions++; });
1587
1571
  for (let window = 0; window < 2; window++) {
1588
1572
  appendText(sm, "user", `window ${window}`);
1589
- const request = resultJson(await call(captured, "new_context", {}, ctx));
1573
+ const request = resultJson(await call(captured, "wipe_memory", {}, ctx));
1590
1574
  assert.equal(request.status, "rollover_requested");
1591
1575
  runHandlers(captured, "agent_end", {}, ctx);
1592
1576
  runHandlers(captured, "agent_end", {}, ctx);
@@ -13,7 +13,7 @@ import { join } from "node:path";
13
13
  import test from "node:test";
14
14
  import { physicalPath, projectKey, scopeDir } from "../src/notes/paths.js";
15
15
  import { listNotes } from "../src/notes/store.js";
16
- import { MAX_NOTE_BYTES, MAX_NOTE_PATH_BYTES, PROTOCOL_BLOCK } from "../src/protocol.js";
16
+ import { CONTEXT_WINDOW_PROTOCOL_OPEN_TAG, MAX_NOTE_BYTES, MAX_NOTE_PATH_BYTES } from "../src/protocol.js";
17
17
  import { call, context, makeExtension, manager, resultJson, resultRead, runHandlers } from "./integration.test.js";
18
18
  process.env.PI_CODING_AGENT_DIR = mkdtempSync(join(tmpdir(), "pi-context-notes-agent-"));
19
19
  function freshRoot() {
@@ -48,10 +48,6 @@ test("exactly the five notes tools are registered; the legacy five are gone", ()
48
48
  for (const legacy of ["notes_write_file", "notes_append_to_file", "notes_read_file", "notes_search_contents", "notes_list_files"]) {
49
49
  assert.equal(captured.tools.get(legacy), undefined, `${legacy} is unregistered`);
50
50
  }
51
- for (const name of ["notes_write", "notes_edit", "notes_read", "notes_list", "notes_search"]) {
52
- const description = captured.tools.get(name)?.description ?? "";
53
- assert.equal(/resolved_scope|\bscope\b/.test(description), false, `${name} describes home identity through address only`);
54
- }
55
51
  assert.equal(captured.tools.get("notes_write")?.executionMode, "sequential");
56
52
  assert.equal(captured.tools.get("notes_edit")?.executionMode, "sequential");
57
53
  assert.equal(captured.tools.get("notes_read")?.executionMode, undefined);
@@ -337,12 +333,6 @@ test("the boot index reads the physical store across scopes and excludes stale n
337
333
  assert.ok(text.includes("personal.md"), "a fresh personal note is indexed");
338
334
  assert.equal(text.includes("old.md"), false, "a stale note leaves the index");
339
335
  assert.equal(text.includes("stale content"), false, "the stale note's body is absent from boot");
340
- for (const name of ["notes_write", "notes_edit", "notes_read", "notes_search", "notes_list"]) {
341
- assert.ok(PROTOCOL_BLOCK.includes(name), `the protocol block names ${name}`);
342
- }
343
- for (const legacy of ["notes_write_file", "notes_append_to_file", "notes_read_file", "notes_search_contents", "notes_list_files"]) {
344
- assert.equal(PROTOCOL_BLOCK.includes(legacy), false, `the protocol block no longer names ${legacy}`);
345
- }
346
336
  });
347
337
  test("search offsets start reads at Unicode matches across homes without mutating search results", async () => {
348
338
  freshRoot();
@@ -456,8 +446,8 @@ test("fresh personal and project MAP.md bodies are both resident before the pock
456
446
  assert.ok(boot.includes("MAP: project"), "the project map is injected");
457
447
  assert.equal(boot.includes("MAP: session"), false, "the session map is never injected");
458
448
  assert.ok(boot.indexOf("MAP: personal") < boot.indexOf("MAP: project"), "the personal map precedes the project map");
459
- assert.ok(boot.indexOf("MAP: project") < boot.indexOf("crumpled note"), "both map bodies precede the pocket");
460
- assert.ok(PROTOCOL_BLOCK.includes("notes_write"), "the protocol text still rides along");
449
+ assert.ok(boot.indexOf("MAP: project") < boot.indexOf("recent.md"), "both map bodies precede the pocket");
450
+ assert.ok(boot.includes(CONTEXT_WINDOW_PROTOCOL_OPEN_TAG), "the protocol text still rides along");
461
451
  });
462
452
  test("the boot pocket applies per-home quotas in session, project, personal order", async () => {
463
453
  freshRoot();
@@ -477,7 +467,6 @@ test("the boot pocket applies per-home quotas in session, project, personal orde
477
467
  await call(captured, "notes_write", { address: "@personal/MAP.md", content: "MAP: personal" }, ctx);
478
468
  runHandlers(captured, "session_start", {}, ctx);
479
469
  const boot = typeof captured.sent.at(-1)?.message.content === "string" ? captured.sent.at(-1).message.content : "";
480
- assert.ok(boot.includes("You find 9 crumpled notes in your pocket (by home, most recent first within each: up to 5 from this session, 2 from this project, 2 from personal). A note's content never appears here, so its name has to say what the note is about:"), "the pocket line matches the dictated copy");
481
470
  for (const name of ["session-5.md", "session-4.md", "session-3.md", "session-2.md", "session-1.md", "@project/project-2.md", "@project/project-1.md", "@personal/personal-2.md", "@personal/personal-1.md"]) {
482
471
  assert.ok(boot.includes(name), `${name} stays in the pocket`);
483
472
  }
@@ -80,7 +80,7 @@ test("a reset requested by a continuation completes before its predecessor relea
80
80
  h.success("first");
81
81
  h.complete();
82
82
  assert.deepEqual(h.messages, ["continue"]);
83
- // This models new_context being called during the first continuation run.
83
+ // This models wipe_memory being called during the first continuation run.
84
84
  assert.equal(h.lifecycle.request(), "rollover_requested");
85
85
  const second = h.settle();
86
86
  assert.equal(h.requests.length, 2, "the continuation's settled handler starts its requested reset");
@@ -24,7 +24,7 @@ Dependencies flow from the composition root and tool adapters to projections and
24
24
 
25
25
  Reset requests are a discriminated union: `idle`, `requested`, or `compacting` with an identified attempt. A request cannot simultaneously be pending and in flight. Each attempt records its originating session, whether it was explicit, and whether a matching boundary completed. Callback identity prevents an old attempt from consuming a newer one. See [reset lifecycle](reset-lifecycle.md).
26
26
 
27
- Manual, threshold and overflow compactions all build the reset boundary on the spot, idle or streaming: `session_before_compact` returns the reset immediately, never cancels and never takes a model turn, and only an aborted signal cancels. The final checkpoint warning was already steered from the context hook (see [reset lifecycle](reset-lifecycle.md)), so the model had its chance to write a note; what crosses the line now is the wipe itself. `agent_settled` services only explicit `new_context` requests, whose `ctx.compact` route needs the completion callback.
27
+ Manual, threshold and overflow compactions all build the reset boundary on the spot, idle or streaming: `session_before_compact` returns the reset immediately, never cancels and never takes a model turn, and only an aborted signal cancels. The final checkpoint warning was already steered from the context hook (see [reset lifecycle](reset-lifecycle.md)), so the model had its chance to write a note; what crosses the line now is the wipe itself. `agent_settled` services only explicit `wipe_memory` requests, whose `ctx.compact` route needs the completion callback.
28
28
 
29
29
  Boot and reminder deduplication inspect messages in the current persisted window. Reloading the extension or the JSONL file therefore does not duplicate either message. Reminder reservation in memory covers Pi's deferred message write; navigation clears that reservation, while persisted branch-local messages remain authoritative. A sibling branch cannot suppress a reminder it never received.
30
30
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  | Event | Transition / owner |
6
6
  | --- | --- |
7
- | `new_context` | Mark explicit request; repeated calls report already pending. Tool returns terminal output. |
7
+ | `wipe_memory` | Mark explicit request; repeated calls report already pending. Tool returns terminal output. |
8
8
  | Manual, threshold or overflow `session_before_compact`, idle or streaming | Build the reset boundary immediately and return it. Never cancel and never take a model turn; an aborted signal returns `{ cancel: true }`. |
9
9
  | `agent_end` | No-op for an instant reset. |
10
10
  | `agent_settled` | If idle and an explicit request is pending, create one identified attempt and request `ctx.compact`. The originating handler owns and awaits that attempt through its continuation's settlement. |
@@ -13,11 +13,11 @@
13
13
  | Attempt `onError` or synchronous throw | Clear attempt/request, warn, retain history. No automatic retry loop. |
14
14
  | Shutdown / start / tree / toggle off | Invalidate outstanding attempt. Identity checks reject callbacks from older attempts. |
15
15
 
16
- The final checkpoint warning is steered earlier from the context hook (`warning.ts`) once per window at reserve+12288 tokens remaining. After it, the model either ends the window itself with `new_context` or rides into Pi's automatic compaction, which resets on the spot with no turn.
16
+ The final checkpoint warning is steered earlier from the context hook (`warning.ts`) once per window at reserve+12288 tokens remaining. After it, the model either ends the window itself with `wipe_memory` or rides into Pi's automatic compaction, which resets on the spot with no turn.
17
17
 
18
18
  The completion callback is the scheduling boundary: `session_compact` fires before Pi clears manual compaction state. Sending a prompt inside that hook is too early. An explicit reset uses the manual `ctx.compact` route and therefore needs this completion logic; an automatic compaction is already the reset and resumes through Pi's own caller.
19
19
 
20
- `sendMessage(..., { triggerTurn: true })` starts its run detached from the extension API. The explicit attempt therefore retains an attempt-owned waiter before sending it, and its originating `agent_settled` handler awaits that waiter. The continuation's `agent_settled` releases the waiter without awaiting itself. If that continuation calls `new_context`, its settled handler starts and awaits the next attempt before it releases the prior waiter, forming a bounded reset chain. Failure, cancellation, shutdown, tree navigation, and toggling off release the relevant waiter exactly once.
20
+ `sendMessage(..., { triggerTurn: true })` starts its run detached from the extension API. The explicit attempt therefore retains an attempt-owned waiter before sending it, and its originating `agent_settled` handler awaits that waiter. The continuation's `agent_settled` releases the waiter without awaiting itself. If that continuation calls `wipe_memory`, its settled handler starts and awaits the next attempt before it releases the prior waiter, forming a bounded reset chain. Failure, cancellation, shutdown, tree navigation, and toggling off release the relevant waiter exactly once.
21
21
 
22
22
  Public APIs cannot guarantee immediate reset inside mixed tool batches or before queued steering/follow-up messages finish. `terminate` ends the tool-followup path; `agent_settled` remains the safe point to request compaction. The scheduler does not manipulate user queues. Pi also determines compaction eligibility before the extension hook; an uncompactable session produces a warning and waits for a new prompt.
23
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/pi-context",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "type": "module",
5
5
  "description": "Codex-style context windows for Pi: reset-style compaction, durable session history tools, and persistent notes.",
6
6
  "license": "MIT",
package/playbook.md CHANGED
@@ -24,9 +24,9 @@ access_count: 0
24
24
  2. **Merge threshold.** Supersede another note only when all three hold: same topic (name it in the survivor's body), same kind of note (checkpoint/design/log…), and the survivor is strictly newer or strictly more specific. Otherwise keep both and record the open conflict in the survivor.
25
25
  3. **Size budget.** Keep every note under ~200 lines / ~8KB. Oversized notes get split by topic with a one-line cross-link in each (`see also: @home/<vpath>`). Checkpoints may exceed the budget — trim prose, never facts.
26
26
  4. **Keep the maps.** Each home's `MAP.md` maps that home's durable notes: one line per entry — its address and a short gist in your own words, never a mechanical body slice. Project notes go on `@project/MAP.md`, cross-project knowledge on `@personal/MAP.md`; session notes are never mapped — the pocket covers them. When a note is promoted across homes, move its line to the destination map; when a note goes stale, drop its line. Maps obey the same size budget as any note.
27
- 5. **Jurisdiction.** Your mandate is the whole store every session home, every project home, personal. Nothing is skipped: notes are never physically deleted and every run is bracketed by git commits, so the human gate can audit and revert whatever you touch. Group your report by home so the gate can see what moved. In every home: map entry lines are yours to maintain, but prose that carries rules or guidance is not — flag it in your report instead of rewriting it.
27
+ 5. **Jurisdiction.** Read the whole store, including every session home, to extract durable knowledge. `pi/session/**` is a live agent's write-ahead log: never write or edit anything there, including frontmatter or stale markers. Promote useful facts by writing to project or personal instead. Other notes are never physically deleted and every run is bracketed by git commits, so the human gate can audit and revert whatever you touch. Group your report by home so the gate can see what moved. In writable homes: map entry lines are yours to maintain, but prose that carries rules or guidance is not — flag it in your report instead of rewriting it.
28
28
  6. **Leave stable notes alone.** Change notes to incorporate new evidence, resolve verified errors, merge genuine duplicates, or split oversized files—not merely to shorten or rephrase them. Preserve facts, conditions, exceptions, and uncertainty. No change is a valid outcome.
29
29
 
30
- Read the files and merge genuinely duplicate notes by editing the survivor, then set `stale: true` in the absorbed note's frontmatter. Nothing is physically deleted; stale notes remain readable. Promote durable cross-project knowledge by writing or editing at `@personal/<vpath>`. Keep notes compact and preserve useful provenance in the body.
30
+ Read the files and merge genuinely duplicate notes in writable homes by editing the survivor, then set `stale: true` in the absorbed note's frontmatter if it is writable. Session notes remain untouched even when promoted; record their source in the destination. Nothing is physically deleted; stale notes remain readable. Promote durable cross-project knowledge by writing or editing at `@personal/<vpath>`. Keep notes compact and preserve useful provenance in the body.
31
31
 
32
32
  Do not write skill ideas as files. Put skill ideas and unresolved questions in your final assistant message as proposals for the human. Your final message should be a concise report of what you inspected, changed, and left unresolved. If you made no file writes, say so.
package/src/dream/cli.ts CHANGED
@@ -12,7 +12,7 @@ import { notesRoot } from "../notes/paths.js";
12
12
 
13
13
  function args(argv: string[]) { const out: Record<string, string | boolean> = {}; for (let i=0;i<argv.length;i++) { const a=argv[i]!; if (a === "--force" || a === "--help") out[a.slice(2)] = true; else if (a.startsWith("--")) out[a.slice(2)] = argv[++i] ?? ""; } return out; }
14
14
  function packageRoot(): string {
15
- let dir = dirname(new URL(import.meta.url).pathname);
15
+ let dir = dirname(fileURLToPath(import.meta.url));
16
16
  while (true) { if (existsSync(join(dir, "package.json"))) return dir; const parent = dirname(dir); if (parent === dir) throw new Error("could not locate installed package root"); dir = parent; }
17
17
  }
18
18
 
package/src/index.ts CHANGED
@@ -50,9 +50,9 @@ export default function piContext(pi: ExtensionAPI) {
50
50
  registerNotesTools(pi);
51
51
 
52
52
  pi.registerTool(defineTool({
53
- name: "new_context",
54
- label: "New context",
55
- description: "Clear your mind and start a new context window. Your session, notes, and history survive.",
53
+ name: "wipe_memory",
54
+ label: "Wipe memory",
55
+ description: "Wipe your in-context memory and start a fresh context window. Your session, notes, and history survive.",
56
56
  parameters: Type.Object({}, { additionalProperties: false }),
57
57
  async execute() {
58
58
  if (!enabled) return output({ error: "pi-context is off (/pi-context on to enable)" });
package/src/prompts.ts CHANGED
@@ -87,5 +87,5 @@ export function bootBlock(ctx: ExtensionContext, currentId: string, previousId:
87
87
  * at write time; get_context_remaining remains the live source for the current figure.
88
88
  */
89
89
  export function tokenBudgetGuidance(remaining: number): string {
90
- return `${GUIDANCE_OPEN_TAG}\nYour brain is almost out of room — ${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 — 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 end the window yourself — anything you do after the checkpoint isn't in it.\n${GUIDANCE_CLOSE_TAG}`;
90
+ return `${GUIDANCE_OPEN_TAG}\nYour brain is almost out of room — ${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 — 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 — anything you do after the checkpoint isn't in it.\n${GUIDANCE_CLOSE_TAG}`;
91
91
  }
package/src/protocol.ts CHANGED
@@ -33,7 +33,7 @@ export const DEFAULT_REMINDER_MARGIN_TOKENS = 24_576;
33
33
  */
34
34
  export const WARNING_RUNWAY_TOKENS = 12_288;
35
35
  export const RESET_SUMMARY =
36
- "You wake up. Your head is empty — no memories, the past a blank. But nothing is lost: the notes you wrote and the recorded history still remember for you.";
36
+ "You wake up. Your head is empty — 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 — read them to rebuild what you need.";
37
37
  export const CONTINUATION = "Your memory was just erased. Pull only the details you need from history_* and notes_*, then get back to work.";
38
38
 
39
39
  /**
@@ -42,12 +42,12 @@ export const CONTINUATION = "Your memory was just erased. Pull only the details
42
42
  * it is never re-injected, so it stays cache-stable at the head of the window.
43
43
  */
44
44
  export const PROTOCOL_BLOCK = `${CONTEXT_WINDOW_PROTOCOL_OPEN_TAG}
45
- Your memory resets whenever the context window fills; only what you wrote down survives. Two things remember for you, and both outlive every window in this session: your notes, and this session's recorded history. 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.
45
+ 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 — 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.
46
46
  Mark outdated or unneeded notes stale — leave them, and they will keep misleading you.
47
47
 
48
48
  Keep a running checkpoint while you work, not at the last minute — 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 — a window/item ID beats re-running or re-searching.
49
49
 
50
- Use get_context_remaining to see how much of the window is left. When it runs out, this window is gone — with no final turn at the limit — and you continue in a fresh one, recovering only through notes_* and history_*. Once your checkpoint is written, you can end the window yourself with new_context instead of waiting for the erase. Do not let a window die undocumented.
50
+ Use get_context_remaining to see how much of the window is left. When it runs out, this window is gone — with no final turn at the limit — 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.
51
51
 
52
52
  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.
53
53
 
@@ -58,4 +58,4 @@ Session notes belong to this trip — the goal, the progress, the loose ends. Th
58
58
  ${CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG}`;
59
59
 
60
60
  export const WARNING_PROMPT =
61
- "Your memory is about to be erased. Write the note. NOW. If it already exists, revise it with notes_edit (or rewrite it whole): the goal, decisions, progress, learnings, next steps, the skills you still need, the window ID and item ID of every relevant user request still being solved, and important actions/tool calls for future reference. Do not continue any task. Then call new_context IMMEDIATELY — anything not in the note dies with the window.";
61
+ "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 — anything not in the note dies with the window.";