@astrosheep/pi-context 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/src/budget.js +63 -0
  2. package/dist/src/dream/apply.js +87 -0
  3. package/dist/src/dream/cli.js +82 -0
  4. package/dist/src/dream/gates.js +21 -0
  5. package/dist/src/dream/lock.js +58 -0
  6. package/dist/src/dream/manifest.js +16 -0
  7. package/dist/src/dream/runner.js +56 -0
  8. package/dist/src/history-tools.js +105 -0
  9. package/dist/src/history.js +210 -0
  10. package/dist/src/index.js +99 -0
  11. package/dist/src/memory/frontmatter.js +134 -0
  12. package/dist/src/memory/paths.js +54 -0
  13. package/dist/src/memory/store.js +297 -0
  14. package/dist/src/memory/tools.js +175 -0
  15. package/dist/src/notes.js +101 -0
  16. package/dist/src/prompts.js +79 -0
  17. package/dist/src/protocol.js +52 -0
  18. package/dist/src/reset-lifecycle.js +101 -0
  19. package/dist/src/session-reader.js +1 -0
  20. package/dist/src/thresholds.js +72 -0
  21. package/dist/src/tool-output.js +172 -0
  22. package/dist/src/tool-schema.js +26 -0
  23. package/dist/src/warning.js +44 -0
  24. package/dist/test/agent-loop.test.js +212 -0
  25. package/dist/test/coherence.test.js +371 -0
  26. package/dist/test/dream.test.js +43 -0
  27. package/dist/test/history.test.js +21 -0
  28. package/dist/test/integration.test.js +1716 -0
  29. package/dist/test/memory.test.js +370 -0
  30. package/dist/test/pagination.property.test.js +476 -0
  31. package/dist/test/reset-lifecycle.test.js +199 -0
  32. package/package.json +9 -3
  33. package/playbook.md +5 -0
  34. package/src/dream/apply.ts +47 -0
  35. package/src/dream/cli.ts +33 -0
  36. package/src/dream/gates.ts +19 -0
  37. package/src/dream/lock.ts +39 -0
  38. package/src/dream/manifest.ts +21 -0
  39. package/src/dream/runner.ts +53 -0
  40. package/src/memory/store.ts +15 -0
  41. package/src/memory/tools.ts +12 -3
  42. package/src/protocol.ts +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/pi-context",
3
- "version": "0.19.0",
3
+ "version": "0.20.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",
@@ -20,15 +20,21 @@
20
20
  },
21
21
  "files": [
22
22
  "src",
23
+ "dist",
23
24
  "docs",
24
25
  "LICENSE",
25
- "README.md"
26
+ "README.md",
27
+ "playbook.md"
26
28
  ],
29
+ "bin": {
30
+ "dream": "dist/src/dream/cli.js"
31
+ },
27
32
  "scripts": {
28
33
  "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
29
34
  "typecheck": "tsc -p tsconfig.json --noEmit",
30
35
  "test": "npm run build && node --test dist/test/*.test.js",
31
- "prepublishOnly": "npm run typecheck"
36
+ "prepublishOnly": "npm run typecheck",
37
+ "prepack": "npm run build"
32
38
  },
33
39
  "peerDependencies": {
34
40
  "@earendil-works/pi-agent-core": "*",
package/playbook.md ADDED
@@ -0,0 +1,5 @@
1
+ I am dreaming over my notes, speaking in my own first-person voice. I inspect the supplied notes and return exactly one JSON manifest, with no commentary outside it.
2
+
3
+ I do seven chores: merge genuinely repeated notes; preserve provenance and recurrence windows; propose (but never execute) global promotions; move only clearly obsolete notes to reversible trash; identify pending ambiguities; propose skill candidates without installing them; and write a concise report of my reasoning and choices. I anchor every temporal claim to an absolute calendar date (YYYY-MM-DD), never to vague words like “today”. I stay aware of the index budget: prefer compact, deduplicated durable notes and avoid swelling the index with repetition. Skill-promotion proposals are proposals only. Judgment belongs here, not in the harness.
4
+
5
+ My final output is the manifest schema documented by the command: merge, promote, trash, pending, skillCandidates, and a required report string.
@@ -0,0 +1,47 @@
1
+ import { mkdirSync, renameSync, existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve, relative } from "node:path";
3
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { editNote, peekNote, resolveNoteScope, updateNoteMeta, writeNote, type Scope } from "../memory/store.js";
5
+ import { physicalPath, scopeDir } from "../memory/paths.js";
6
+ import { parseNote } from "../memory/frontmatter.js";
7
+ import type { Manifest } from "./manifest.js";
8
+
9
+ type Target = { scope: Scope; path: string };
10
+ function target(ctx: ExtensionContext, value: string, required = true): Target | undefined {
11
+ const m = /^(session|project|global):(.*)$/.exec(value);
12
+ if (m) { const scope = m[1] as Scope; const path = m[2]!; const physical = physicalPath(scope, path, ctx); if (!existsSync(physical)) { if (required) throw new Error(`unknown path: ${value}`); return undefined; } return { scope, path }; }
13
+ const found = resolveNoteScope(ctx, value);
14
+ if (!found && required) throw new Error(`unknown path: ${value}`);
15
+ return found ? { scope: found.scope, path: value } : undefined;
16
+ }
17
+ function body(ctx: ExtensionContext, t: Target) { return peekNote(ctx, t.scope, t.path); }
18
+ export function applyManifest(ctx: ExtensionContext, home: string, stamp: string, manifest: Manifest): string[] {
19
+ const actions: string[] = [];
20
+ // Resolve every referenced note and promotion destination before the first mutation.
21
+ for (const m of manifest.merge ?? []) { target(ctx, m.into); for (const p of m.from) target(ctx, p); }
22
+ for (const p of manifest.promote ?? []) { const from = target(ctx, p.path)!; if (p.to !== "global") { if (!["session", "project"].includes(p.to)) throw new Error(`invalid promotion scope: ${p.to}`); if (existsSync(physicalPath(p.to as Scope, p.path, ctx))) throw new Error(`promotion collision: ${p.path}`); } void from; }
23
+ for (const p of manifest.trash ?? []) target(ctx, p.path);
24
+ for (const merge of manifest.merge ?? []) {
25
+ const into = target(ctx, merge.into)!;
26
+ const sources = merge.from.map((p) => target(ctx, p)!);
27
+ const base = body(ctx, into); const chunks = [base.body, ...sources.map((s) => body(ctx, s).body)].filter(Boolean);
28
+ const dedup = [...new Set(chunks)].join("\n\n");
29
+ writeNote(ctx, into.path, dedup, { scope: into.scope, origin: base.meta.origin });
30
+ updateNoteMeta(ctx, into.path, into.scope, (meta) => { meta.recurrence_count = (meta.recurrence_count ?? 0) as number + sources.length; meta.recurrence_windows = [...new Set([...(meta.recurrence_windows ?? []), ...sources.map((s) => body(ctx, s).meta.source_window).filter((x): x is string => typeof x === "string")])]; });
31
+ for (const source of sources) updateNoteMeta(ctx, source.path, source.scope, (meta) => { meta.status = "superseded"; meta.supersedes = merge.into; });
32
+ actions.push(`merged ${merge.from.join(", ")} into ${merge.into}`);
33
+ }
34
+ for (const p of manifest.promote ?? []) {
35
+ const from = target(ctx, p.path)!; const m = body(ctx, from); const scope = p.to as Scope;
36
+ if (!["session", "project", "global"].includes(scope)) throw new Error(`invalid promotion scope: ${p.to}`);
37
+ if (scope === "global") { actions.push(`proposal: promote ${p.path} to global (${p.reason})`); continue; }
38
+ const dest = physicalPath(scope, p.path, ctx); if (existsSync(dest)) throw new Error(`promotion collision: ${p.path}`);
39
+ editNote(ctx, from.path, undefined, { scope });
40
+ actions.push(`promoted ${p.path} to ${scope}`);
41
+ }
42
+ const trashRoot = join(home, "trash", stamp); mkdirSync(trashRoot, { recursive: true });
43
+ for (const item of manifest.trash ?? []) { const t = target(ctx, item.path)!; const source = physicalPath(t.scope, t.path, ctx); const dest = join(trashRoot, t.scope, t.path.endsWith(".md") ? t.path : `${t.path}.md`); mkdirSync(dirname(dest), { recursive: true }); renameSync(source, dest); actions.push(`trashed ${item.path}: ${item.reason}`); }
44
+ for (const p of manifest.pending ?? []) actions.push(`pending ${p.path}: ${p.reason}`);
45
+ for (const p of manifest.skillCandidates ?? []) actions.push(`skill proposal ${p.title}: ${p.rationale}`);
46
+ return actions;
47
+ }
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { acquireLock, failLock, releaseLock } from "./lock.js";
6
+ import { materialGate, timeGate } from "./gates.js";
7
+ import { loadPlaybook, runDreamer } from "./runner.js";
8
+ import { applyManifest } from "./apply.js";
9
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
10
+
11
+ 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; }
12
+ function packageRoot(): string {
13
+ let dir = dirname(new URL(import.meta.url).pathname);
14
+ 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; }
15
+ }
16
+
17
+ export async function main(argv = process.argv.slice(2)): Promise<number> {
18
+ const a = args(argv); if (a.help) { console.log("dream --notes-home <dir> [--min-hours 24] [--min-sessions 3] [--force] [--dreamer <cmd>] [--dreamer-model <pattern>] [--playbook <path>]\nDefault dreamer: in-process pi SDK session with read-only tools; use a cheap model in production. Default playbook: <installed package root>/playbook.md; --playbook overrides it."); return 0; }
19
+ const home = resolve(String(a["notes-home"] ?? process.env.PI_NOTES_HOME ?? join(homedir(), ".agents", "notes"))); process.env.PI_NOTES_HOME = home; mkdirSync(home, { recursive: true });
20
+ const lockPath = join(home, ".dream.lock"); const minHours = Number(a["min-hours"] ?? 24); const minSessions = Number(a["min-sessions"] ?? 3);
21
+ const time = timeGate(lockPath, minHours); console.log(time.reason); if (!a.force && !time.ok) return 0;
22
+ const material = materialGate(home, existsSync(lockPath) ? statSync(lockPath).mtimeMs : 0, minSessions); console.log(material.reason); if (!a.force && !material.ok) return 0;
23
+ let lock; try { lock = acquireLock(lockPath); } catch (e) { console.log(`lock gate: ${e instanceof Error ? e.message : e}`); return 0; } if (!lock.held) { console.log(lock.reason); return 0; }
24
+ const stamp = new Date(lock.startedAt).toISOString().replace(/[:.]/g, "-"); const reportPath = join(home, "dreams", `${stamp}.md`);
25
+ try {
26
+ const defaultBook = join(packageRoot(), "playbook.md");
27
+ const playbookPath = String(a.playbook ?? defaultBook);
28
+ const playbook = loadPlaybook(playbookPath); const manifest = await runDreamer(playbook, home, { command: a.dreamer ? String(a.dreamer) : undefined, modelPattern: a["dreamer-model"] ? String(a["dreamer-model"]) : undefined });
29
+ const ctx = { cwd: home, sessionManager: { getSessionId: () => "dream" } } as unknown as ExtensionContext; const actions = applyManifest(ctx, home, stamp, manifest);
30
+ mkdirSync(join(home, "dreams"), { recursive: true }); writeFileSync(reportPath, `# Dream ${stamp}\n\n${manifest.report}\n\n${actions.map((x) => `- ${x}`).join("\n")}\n`); console.log(reportPath); return 0;
31
+ } catch (e) { failLock(lock); console.error(e instanceof Error ? e.message : e); return 1; } finally { releaseLock(lock); }
32
+ }
33
+ main().then((code) => { process.exitCode = code; });
@@ -0,0 +1,19 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ export type GateResult = { ok: boolean; reason: string };
5
+ export function timeGate(lockPath: string, minHours: number, now = Date.now()): GateResult {
6
+ if (!existsSync(lockPath)) return { ok: true, reason: "time gate: no prior lock" };
7
+ const age = now - statSync(lockPath).mtimeMs;
8
+ return age >= minHours * 3600000 ? { ok: true, reason: "time gate: stale" } : { ok: false, reason: "time gate: lock is too fresh" };
9
+ }
10
+ export function materialGate(home: string, lockMtime: number, minSessions: number): GateResult {
11
+ const root = join(home, "pi", "session");
12
+ let changed = 0;
13
+ if (existsSync(root)) for (const dir of readdirSync(root, { withFileTypes: true })) {
14
+ if (!dir.isDirectory()) continue;
15
+ const files = readdirSync(join(root, dir.name), { withFileTypes: true });
16
+ if (files.some((f) => f.isFile() && statSync(join(root, dir.name, f.name)).mtimeMs > lockMtime)) changed++;
17
+ }
18
+ return changed >= minSessions ? { ok: true, reason: `material gate: ${changed} changed sessions` } : { ok: false, reason: `material gate: only ${changed} changed sessions` };
19
+ }
@@ -0,0 +1,39 @@
1
+ import { existsSync, readFileSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
2
+
3
+ export type LockState = { path: string; held: boolean; reason?: string; startedAt: number; priorMtime?: number };
4
+ const HOUR = 60 * 60 * 1000;
5
+
6
+ function live(pid: number): boolean {
7
+ if (!Number.isInteger(pid) || pid <= 0) return false;
8
+ try { process.kill(pid, 0); return true; } catch { return false; }
9
+ }
10
+
11
+ export function acquireLock(path: string): LockState {
12
+ const now = Date.now();
13
+ let priorMtime: number | undefined;
14
+ if (existsSync(path)) {
15
+ const stat = statSync(path);
16
+ priorMtime = stat.mtimeMs;
17
+ let pid = 0;
18
+ try { pid = Number.parseInt(readFileSync(path, "utf8").trim(), 10); } catch { /* reclaim */ }
19
+ if (now - stat.mtimeMs <= HOUR && live(pid)) return { path, held: false, reason: "lock gate: live process holds the lock", startedAt: now };
20
+ try { unlinkSync(path); } catch { return { path, held: false, reason: "lock gate: lock could not be reclaimed", startedAt: now }; }
21
+ }
22
+ writeFileSync(path, String(process.pid), { flag: "wx" });
23
+ return { path, held: true, startedAt: now, priorMtime };
24
+ }
25
+
26
+ export function releaseLock(lock: LockState): void {
27
+ // The lock is also the durable last-dream timestamp. Leave the PID marker in place;
28
+ // the next acquisition reclaims it once the PID is dead or it is older than an hour.
29
+ }
30
+
31
+ export function restoreMtime(path: string, mtimeMs: number): void {
32
+ try { utimesSync(path, new Date(), new Date(mtimeMs)); } catch { /* advisory */ }
33
+ }
34
+
35
+ export function failLock(lock: LockState): void {
36
+ if (!lock.held) return;
37
+ if (lock.priorMtime === undefined) { try { unlinkSync(lock.path); } catch { /* best effort */ } }
38
+ else restoreMtime(lock.path, lock.priorMtime);
39
+ }
@@ -0,0 +1,21 @@
1
+ export type Manifest = {
2
+ merge?: { into: string; from: string[]; summary?: string }[];
3
+ promote?: { path: string; to: string; reason: string }[];
4
+ trash?: { path: string; reason: string }[];
5
+ pending?: { path: string; reason: string }[];
6
+ skillCandidates?: { title: string; rationale: string }[];
7
+ report: string;
8
+ };
9
+
10
+ export function parseManifest(output: string): Manifest {
11
+ const starts = [...output.matchAll(/[\{[]/g)].map((m) => m.index ?? 0).reverse();
12
+ for (const start of starts) {
13
+ try {
14
+ const value = JSON.parse(output.slice(start)) as Manifest;
15
+ if (!value || typeof value !== "object" || typeof value.report !== "string") continue;
16
+ for (const key of ["merge", "promote", "trash", "pending", "skillCandidates"]) if (value[key as keyof Manifest] !== undefined && !Array.isArray(value[key as keyof Manifest])) throw new Error("invalid array");
17
+ return value;
18
+ } catch { /* try an earlier JSON start */ }
19
+ }
20
+ throw new Error("dreamer did not return a valid JSON manifest");
21
+ }
@@ -0,0 +1,53 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { createAgentSession, ModelRuntime, resolveModelScopeWithDiagnostics, SessionManager, type AgentSession } from "@earendil-works/pi-coding-agent";
4
+ import type { Api, Model } from "@earendil-works/pi-ai";
5
+ import { parseManifest, type Manifest } from "./manifest.js";
6
+
7
+ export type DreamerSession = Pick<AgentSession, "prompt" | "subscribe" | "dispose">;
8
+ export type DreamerSessionFactory = (options: { cwd: string; modelPattern?: string; tools: string[] }) => Promise<DreamerSession>;
9
+ export const READ_ONLY_TOOLS = ["read", "grep", "find", "ls", "notes_read", "notes_list", "notes_search"];
10
+
11
+ export const defaultDreamerSessionFactory: DreamerSessionFactory = async ({ cwd, modelPattern, tools }) => {
12
+ let model: Model<Api> | undefined;
13
+ if (modelPattern) {
14
+ const runtime = await ModelRuntime.create({ allowModelNetwork: false, refreshOnCreate: false });
15
+ const result = await resolveModelScopeWithDiagnostics([modelPattern], runtime);
16
+ model = result.scopedModels[0]?.model;
17
+ if (!model) throw new Error(`dreamer model pattern "${modelPattern}" did not resolve to an available model`);
18
+ }
19
+ const { session } = await createAgentSession({ cwd, sessionManager: SessionManager.inMemory(cwd), tools, noTools: "all", model });
20
+ return session;
21
+ };
22
+
23
+ export function runExternalDreamer(command: string, playbook: string, cwd: string): Manifest {
24
+ const result = spawnSync(command, { shell: true, cwd, input: playbook, encoding: "utf8" });
25
+ if (result.error || result.status !== 0) throw new Error(`dreamer failed: ${result.error?.message ?? result.stderr ?? `exit ${result.status}`}`);
26
+ return parseManifest(result.stdout);
27
+ }
28
+
29
+ export async function runDreamer(playbook: string, cwd: string, options: { command?: string; modelPattern?: string; sessionFactory?: DreamerSessionFactory } = {}): Promise<Manifest> {
30
+ if (options.command) return runExternalDreamer(options.command, playbook, cwd);
31
+ const session = await (options.sessionFactory ?? defaultDreamerSessionFactory)({ cwd, modelPattern: options.modelPattern, tools: READ_ONLY_TOOLS });
32
+ let answer = "";
33
+ let providerError: string | undefined;
34
+ const unsubscribe = session.subscribe((event: any) => {
35
+ if (event.type !== "message_end" || event.message?.role !== "assistant") return;
36
+ if (event.message.stopReason === "error") { providerError = event.message.errorMessage ?? "unknown provider error"; return; }
37
+ const content = event.message.content;
38
+ answer = typeof content === "string" ? content : Array.isArray(content) ? content.filter((part: any) => part.type === "text").map((part: any) => part.text).join("") : "";
39
+ });
40
+ try {
41
+ await session.prompt(`${playbook}\n\nReturn exactly one JSON manifest matching this schema: { merge?, promote?, trash?, pending?, skillCandidates?, report }.`);
42
+ try {
43
+ return parseManifest(answer);
44
+ } catch (error) {
45
+ if (providerError) throw new Error(`dreamer failed: ${providerError}`);
46
+ throw error;
47
+ }
48
+ } finally {
49
+ unsubscribe?.();
50
+ session.dispose();
51
+ }
52
+ }
53
+ export function loadPlaybook(path: string): string { return readFileSync(path, "utf8"); }
@@ -162,6 +162,21 @@ export function writeNote(ctx: ExtensionContext, vpath: string, body: string, op
162
162
  export type EditOperation = { oldText: string; newText: string };
163
163
  export type EditOptions = { scope?: Scope; origin?: Origin; stale?: boolean; replaceAll?: boolean };
164
164
 
165
+ /** Dream harness mutation: metadata changes still use the store's atomic writer. */
166
+ export function updateNoteMeta(ctx: ExtensionContext, vpath: string, scope: Scope, mutate: (meta: NoteMeta) => void): { meta: NoteMeta; body: string } {
167
+ assertVirtualPath(vpath);
168
+ const path = physicalPath(scope, vpath, ctx);
169
+ if (!existsSync(path)) throw new NoteError("not_found", `note not found: ${vpath}`);
170
+ const parsed = parseNote(readFileSync(path, "utf8"));
171
+ const meta = { ...parsed.meta, scope };
172
+ mutate(meta);
173
+ meta.updated_at = Date.now();
174
+ const serialized = serializeNote(meta, parsed.body);
175
+ assertSerializedSize(serialized);
176
+ atomicWrite(path, serialized);
177
+ return { meta, body: parsed.body };
178
+ }
179
+
165
180
  /** Apply body-only edits against one snapshot, then optionally move via the scope/origin/stale setters. */
166
181
  export function editNote(ctx: ExtensionContext, vpath: string, edits: EditOperation[] | undefined, opts: EditOptions = {}): { meta: NoteMeta; applied: number; resolved_scope: Scope; diff: string } {
167
182
  assertVirtualPath(vpath);
@@ -7,8 +7,17 @@ import { serializeNote, stripLeadingFrontmatter, type NoteMeta, type Origin } fr
7
7
  import { NoteError, editNote, listNotes, readNote, searchNotes, writeNote } from "./store.js";
8
8
  import type { Scope } from "./paths.js";
9
9
 
10
- const SCOPE = Type.Optional(Type.Union([Type.Literal("session"), Type.Literal("project"), Type.Literal("global")]));
11
- const ORIGIN = Type.Optional(Type.Union([Type.Literal("user"), Type.Literal("self"), Type.Literal("external")]));
10
+ const SCOPE = Type.Optional(
11
+ Type.Union([Type.Literal("session"), Type.Literal("project"), Type.Literal("global")], {
12
+ description:
13
+ "The note's reach — which root it lives under. session: only this session needs it (checkpoints, scratch state, worker rosters); dies with the session. project: tied to the current working directory — design decisions and repo facts that future sessions here still need. global: follows you everywhere — user laws, preferences, cross-project maps. On write, picks the destination root (default: session). Omit on read/list/search to cover all three; a read resolves session → project → global and returns the first existing file.",
14
+ }),
15
+ );
16
+ const ORIGIN = Type.Optional(
17
+ Type.Union([Type.Literal("user"), Type.Literal("self"), Type.Literal("external")], {
18
+ description: "Where the note's content came from. user: written or dictated by the human. self: written by you, the agent (default). external: anything else — third-party text, tool output, fetched material.",
19
+ }),
20
+ );
12
21
 
13
22
  /** Render epoch-ms metadata as the same local ISO timestamps the frontmatter carries. */
14
23
  function wireMeta(meta: NoteMeta): Record<string, unknown> {
@@ -30,7 +39,7 @@ export function registerMemoryTools(pi: ExtensionAPI) {
30
39
  pi.registerTool(defineTool({
31
40
  name: "notes_write",
32
41
  label: "Notes write",
33
- description: "Create or replace a note as a real markdown file under the session, project, or global note root. Keep notes small and split by topic; a rewrite replaces the body whole while preserving created_at and every other frontmatter key. stale: true marks the note closed so it leaves the boot index but stays readable and searchable.",
42
+ description: "Create or replace a note as a real markdown file under the session, project, or global note root. Keep notes small and split by topic — by what the note is about, never by who said it (authorship is origin's job); a rewrite replaces the body whole while preserving created_at and every other frontmatter key. stale: true marks the note closed so it leaves the boot index but stays readable and searchable.",
34
43
  parameters: Type.Object({ path: Type.String(), content: Type.String(), scope: SCOPE, origin: ORIGIN, stale: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
35
44
  // A batch containing write or edit runs one call at a time, so note read-modify-write cannot race.
36
45
  executionMode: "sequential",
package/src/protocol.ts CHANGED
@@ -49,8 +49,8 @@ Use get_context_remaining to see how much of the window is left. When it runs ou
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
 
52
- Notes are real markdown files scoped session, project, or global. Treat notes and history as internal bookkeeping; never mention them in user-facing messages.
52
+ Notes are real markdown files scoped session, project, or global — pick scope by reach: session dies with the session, project follows the repo, global follows you. Treat notes and history as internal bookkeeping; never mention them in user-facing messages.
53
53
  ${CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG}`;
54
54
 
55
55
  export const WARNING_PROMPT =
56
- "Your memory is about to be erased. Write the note. NOW. If it already exists, append instead: 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.";
56
+ "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.";