@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 +1 -1
- package/dist/src/dream/cli.js +1 -1
- package/dist/src/index.js +3 -3
- package/dist/src/prompts.js +1 -1
- package/dist/src/protocol.js +4 -4
- package/dist/test/agent-loop.test.js +15 -7
- package/dist/test/dream.test.js +23 -3
- package/dist/test/integration.test.js +20 -36
- package/dist/test/notes.test.js +3 -14
- package/dist/test/reset-lifecycle.test.js +1 -1
- package/docs/architecture.md +1 -1
- package/docs/reset-lifecycle.md +3 -3
- package/package.json +1 -1
- package/playbook.md +2 -2
- package/src/dream/cli.ts +1 -1
- package/src/index.ts +3 -3
- package/src/prompts.ts +1 -1
- package/src/protocol.ts +4 -4
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ pi -e npm:@astrosheep/pi-context
|
|
|
16
16
|
|
|
17
17
|
## What you get
|
|
18
18
|
|
|
19
|
-
- **`
|
|
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.
|
package/dist/src/dream/cli.js
CHANGED
|
@@ -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(
|
|
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: "
|
|
54
|
-
label: "
|
|
55
|
-
description: "
|
|
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)
|
package/dist/src/prompts.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/dist/src/protocol.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
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: ["
|
|
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
|
-
|
|
86
|
-
|
|
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 ? "
|
|
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: "
|
|
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(
|
|
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");
|
package/dist/test/dream.test.js
CHANGED
|
@@ -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
|
|
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
|
-
"
|
|
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, "
|
|
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"
|
|
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("
|
|
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, "
|
|
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, "
|
|
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, "
|
|
1262
|
-
assert.match(offResult.error ?? "", /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"
|
|
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/
|
|
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
|
-
//
|
|
1366
|
-
await call(captured, "
|
|
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, "
|
|
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, "
|
|
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, "
|
|
1374
|
-
assert.equal(captured.sent.length, 0, "
|
|
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
|
|
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, "
|
|
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("
|
|
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, "
|
|
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);
|
package/dist/test/notes.test.js
CHANGED
|
@@ -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
|
|
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("
|
|
460
|
-
assert.ok(
|
|
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
|
|
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");
|
package/docs/architecture.md
CHANGED
|
@@ -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 `
|
|
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
|
|
package/docs/reset-lifecycle.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
| Event | Transition / owner |
|
|
6
6
|
| --- | --- |
|
|
7
|
-
| `
|
|
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 `
|
|
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 `
|
|
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
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.**
|
|
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(
|
|
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: "
|
|
54
|
-
label: "
|
|
55
|
-
description: "
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.";
|