@astrosheep/pi-context 0.25.1 → 0.26.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 (112) hide show
  1. package/README.md +88 -7
  2. package/dist/build-info.json +2 -2
  3. package/dist/extension.js +616 -370
  4. package/dist/src/context/boot.d.ts +24 -0
  5. package/dist/src/context/boot.js +33 -24
  6. package/dist/src/context/budget.d.ts +9 -0
  7. package/dist/src/context/budget.js +19 -15
  8. package/dist/src/context/context-window.d.ts +41 -0
  9. package/dist/src/context/context-window.js +16 -1
  10. package/dist/src/context/prompts.d.ts +20 -0
  11. package/dist/src/context/prompts.js +1 -1
  12. package/dist/src/context/reset-artifacts.d.ts +26 -0
  13. package/dist/src/context/reset-artifacts.js +18 -17
  14. package/dist/src/context/reset-lifecycle.d.ts +89 -0
  15. package/dist/src/context/reset-lifecycle.js +103 -75
  16. package/dist/src/context/runtime.d.ts +3 -0
  17. package/dist/src/context/runtime.js +53 -21
  18. package/dist/src/context/thresholds.d.ts +33 -0
  19. package/dist/src/context/thresholds.js +1 -1
  20. package/dist/src/dream/cli.d.ts +10 -0
  21. package/dist/src/dream/cli.js +1 -1
  22. package/dist/src/dream/doctor.d.ts +2 -0
  23. package/dist/src/dream/doctor.js +6 -2
  24. package/dist/src/dream/gates.d.ts +10 -0
  25. package/dist/src/dream/git.d.ts +21 -0
  26. package/dist/src/dream/lock.d.ts +31 -0
  27. package/dist/src/dream/runner.d.ts +30 -0
  28. package/dist/src/dream/settings.d.ts +16 -0
  29. package/dist/src/history/history-tools.d.ts +2 -0
  30. package/dist/src/history/history.d.ts +57 -0
  31. package/dist/src/index.d.ts +39 -0
  32. package/dist/src/index.js +4 -4
  33. package/dist/src/notes/address.d.ts +26 -0
  34. package/dist/src/notes/address.js +8 -14
  35. package/dist/src/notes/constants.d.ts +3 -0
  36. package/dist/src/notes/constants.js +3 -0
  37. package/dist/src/notes/context.d.ts +10 -0
  38. package/dist/src/notes/context.js +33 -0
  39. package/dist/src/notes/frontmatter.d.ts +46 -0
  40. package/dist/src/notes/frontmatter.js +10 -5
  41. package/dist/src/notes/index.d.ts +4 -0
  42. package/dist/src/notes/index.js +2 -0
  43. package/dist/src/notes/paths.d.ts +21 -0
  44. package/dist/src/notes/paths.js +72 -76
  45. package/dist/src/notes/store.d.ts +94 -0
  46. package/dist/src/notes/store.js +298 -242
  47. package/dist/src/pi/notes/adapter.d.ts +12 -0
  48. package/dist/src/pi/notes/adapter.js +39 -0
  49. package/dist/src/pi/notes/session-replay.d.ts +16 -0
  50. package/dist/src/{notes → pi/notes}/session-replay.js +2 -2
  51. package/dist/src/pi/notes/snapshot.d.ts +33 -0
  52. package/dist/src/{notes/notes-snapshot.js → pi/notes/snapshot.js} +11 -3
  53. package/dist/src/pi/notes/tools.d.ts +2 -0
  54. package/dist/src/{notes → pi/notes}/tools.js +24 -21
  55. package/dist/src/protocol.d.ts +41 -0
  56. package/dist/src/protocol.js +4 -6
  57. package/dist/src/session-reader.d.ts +5 -0
  58. package/dist/src/settings.d.ts +6 -0
  59. package/dist/src/tool-output.d.ts +101 -0
  60. package/dist/src/tool-schema.d.ts +17 -0
  61. package/dist/test/agent-loop.test.d.ts +1 -0
  62. package/dist/test/agent-loop.test.js +318 -19
  63. package/dist/test/boot.integration.test.d.ts +1 -0
  64. package/dist/test/boot.integration.test.js +55 -29
  65. package/dist/test/budget-settings.integration.test.d.ts +1 -0
  66. package/dist/test/budget-settings.integration.test.js +8 -7
  67. package/dist/test/doctor.test.d.ts +1 -0
  68. package/dist/test/doctor.test.js +10 -2
  69. package/dist/test/dream-skill.test.d.ts +1 -0
  70. package/dist/test/dream-skill.test.js +69 -0
  71. package/dist/test/dream.test.d.ts +1 -0
  72. package/dist/test/helpers/extension.d.ts +115 -0
  73. package/dist/test/helpers/extension.js +6 -6
  74. package/dist/test/helpers/notes.d.ts +6 -0
  75. package/dist/test/helpers/notes.js +13 -0
  76. package/dist/test/history.integration.test.d.ts +1 -0
  77. package/dist/test/notes-library.test.d.ts +1 -0
  78. package/dist/test/notes-library.test.js +111 -0
  79. package/dist/test/notes.integration.test.d.ts +1 -0
  80. package/dist/test/notes.integration.test.js +22 -24
  81. package/dist/test/notes.test.d.ts +1 -0
  82. package/dist/test/notes.test.js +137 -7
  83. package/dist/test/reset-lifecycle.test.d.ts +1 -0
  84. package/dist/test/reset-lifecycle.test.js +142 -85
  85. package/docs/architecture.md +8 -8
  86. package/docs/reset-lifecycle.md +63 -79
  87. package/package.json +35 -2
  88. package/playbook.md +33 -32
  89. package/skills/dream/SKILL.md +12 -0
  90. package/src/context/boot.ts +44 -25
  91. package/src/context/budget.ts +25 -17
  92. package/src/context/context-window.ts +16 -1
  93. package/src/context/prompts.ts +2 -2
  94. package/src/context/reset-artifacts.ts +26 -24
  95. package/src/context/reset-lifecycle.ts +117 -111
  96. package/src/context/runtime.ts +50 -22
  97. package/src/context/thresholds.ts +1 -1
  98. package/src/dream/cli.ts +1 -1
  99. package/src/dream/doctor.ts +5 -2
  100. package/src/index.ts +4 -4
  101. package/src/notes/address.ts +9 -15
  102. package/src/notes/constants.ts +3 -0
  103. package/src/notes/context.ts +40 -0
  104. package/src/notes/frontmatter.ts +18 -12
  105. package/src/notes/index.ts +22 -0
  106. package/src/notes/paths.ts +64 -78
  107. package/src/notes/store.ts +308 -244
  108. package/src/pi/notes/adapter.ts +44 -0
  109. package/src/{notes → pi/notes}/session-replay.ts +3 -3
  110. package/src/{notes/notes-snapshot.ts → pi/notes/snapshot.ts} +13 -4
  111. package/src/{notes → pi/notes}/tools.ts +25 -23
  112. package/src/protocol.ts +5 -6
@@ -0,0 +1,39 @@
1
+ import { existsSync, renameSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { projectKey, slugify } from "../../notes/paths.js";
5
+ /** Pi's default notes root; environment access stays on the host side of the library boundary. */
6
+ export function notesRoot() {
7
+ const override = process.env.PI_NOTES_HOME;
8
+ return override && override.length > 0 ? resolve(override) : join(homedir(), ".agents", "notes");
9
+ }
10
+ /** Translate the current Pi runtime into a fresh, explicit identity snapshot. */
11
+ export function agentSlug(_ctx) {
12
+ return slugify(process.env.PI_NOTES_AGENT ?? "root");
13
+ }
14
+ /** Pi's active model identity is resolved live so a mid-session switch retargets @model. */
15
+ export function modelSlug(ctx) {
16
+ const id = ctx.model?.id;
17
+ return id ? slugify(id.split("/").pop() ?? id) : "default";
18
+ }
19
+ /** Translate the current Pi runtime into one explicit notes identity snapshot. */
20
+ export function notesContextFromPi(ctx, home = notesRoot()) {
21
+ return {
22
+ home,
23
+ sessionId: ctx.sessionManager.getSessionId(),
24
+ projectKey: projectKey(ctx.cwd),
25
+ agent: agentSlug(ctx),
26
+ model: modelSlug(ctx),
27
+ };
28
+ }
29
+ /** One-time host activation migration from the legacy personal/ directory. */
30
+ export function migrateLegacyHomes(home = notesRoot()) {
31
+ const legacy = join(home, "personal");
32
+ const modern = join(home, "human");
33
+ if (!existsSync(legacy))
34
+ return undefined;
35
+ if (existsSync(modern))
36
+ return "both personal/ and human/ exist under the notes home; migrate by hand, no automatic merge";
37
+ renameSync(legacy, modern);
38
+ return undefined;
39
+ }
@@ -0,0 +1,16 @@
1
+ import type { SessionReader } from "../../session-reader.js";
2
+ export type NoteFile = {
3
+ text: string;
4
+ stale: boolean;
5
+ createdAt: number;
6
+ updatedAt: number;
7
+ };
8
+ export type NoteOperation = {
9
+ op: "write" | "append";
10
+ path: string;
11
+ text?: string;
12
+ stale?: boolean;
13
+ createdAt: number;
14
+ updatedAt: number;
15
+ };
16
+ export declare function notesFromSession(ctx: SessionReader): Map<string, NoteFile>;
@@ -1,5 +1,5 @@
1
- import { MAX_NOTE_BYTES, NOTE_TYPE } from "../protocol.js";
2
- import { assertVirtualPath } from "./address.js";
1
+ import { MAX_NOTE_BYTES, NOTE_TYPE } from "../../protocol.js";
2
+ import { assertVirtualPath } from "../../notes/address.js";
3
3
  /** Replays only pi-context note operations from session custom entries. */
4
4
  function isNoteOperation(data) {
5
5
  if (typeof data !== "object" || data === null)
@@ -0,0 +1,33 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { NotesContext } from "../../notes/context.js";
3
+ import { type NoteRow, type Scope } from "../../notes/index.js";
4
+ declare const NOTES_HOMES: readonly [{
5
+ readonly scope: "session";
6
+ readonly label: "this session";
7
+ }, {
8
+ readonly scope: "project";
9
+ readonly label: "@project";
10
+ }, {
11
+ readonly scope: "human";
12
+ readonly label: "@human";
13
+ }, {
14
+ readonly scope: "agent";
15
+ readonly label: "@self";
16
+ }, {
17
+ readonly scope: "model";
18
+ readonly label: "@model";
19
+ }];
20
+ export type NotesHome = (typeof NOTES_HOMES)[number];
21
+ export type NotesLoader = (ctx: ExtensionContext, scope: Scope) => NoteRow[] | Promise<NoteRow[]>;
22
+ export type NotesSnapshot = {
23
+ /** Wall-clock instant captured when this boot began; rendering never consults Date.now(). */
24
+ readonly openedAt: number;
25
+ readonly homes: ReadonlyMap<Scope, readonly NoteRow[]>;
26
+ readonly unavailable: readonly NotesHome[];
27
+ };
28
+ /**
29
+ * Acquire the five homes once for one boot. Only filesystem-style errno failures are isolated;
30
+ * malformed note data and unrelated construction errors remain visible to the caller.
31
+ */
32
+ export declare function loadNotesSnapshot(ctx: ExtensionContext, loadHome?: NotesLoader, identity?: NotesContext): Promise<NotesSnapshot>;
33
+ export {};
@@ -1,4 +1,5 @@
1
- import { listNotes } from "./store.js";
1
+ import { createNotesStore } from "../../notes/index.js";
2
+ import { notesContextFromPi } from "./adapter.js";
2
3
  const NOTES_HOMES = [
3
4
  { scope: "session", label: "this session" },
4
5
  { scope: "project", label: "@project" },
@@ -6,17 +7,24 @@ const NOTES_HOMES = [
6
7
  { scope: "agent", label: "@self" },
7
8
  { scope: "model", label: "@model" },
8
9
  ];
10
+ function queryForScope(scope) {
11
+ if (scope === "agent" || scope === "model")
12
+ return { scope };
13
+ return { scope };
14
+ }
9
15
  /**
10
16
  * Acquire the five homes once for one boot. Only filesystem-style errno failures are isolated;
11
17
  * malformed note data and unrelated construction errors remain visible to the caller.
12
18
  */
13
- export function loadNotesSnapshot(ctx, loadHome = (context, scope) => listNotes(context, { scope })) {
19
+ export async function loadNotesSnapshot(ctx, loadHome, identity = notesContextFromPi(ctx)) {
14
20
  const openedAt = Date.now();
21
+ const store = createNotesStore(identity);
22
+ const load = loadHome ?? ((_context, scope) => store.list(queryForScope(scope)));
15
23
  const homes = new Map();
16
24
  const unavailable = [];
17
25
  for (const home of NOTES_HOMES) {
18
26
  try {
19
- homes.set(home.scope, loadHome(ctx, home.scope));
27
+ homes.set(home.scope, await load(ctx, home.scope));
20
28
  }
21
29
  catch (error) {
22
30
  const code = typeof error === "object" && error !== null ? error.code : undefined;
@@ -0,0 +1,2 @@
1
+ import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ export declare function registerNotesTools(pi: ExtensionAPI): void;
@@ -1,10 +1,10 @@
1
1
  import { Type } from "@earendil-works/pi-ai";
2
- import { defineTool } from "@earendil-works/pi-coding-agent";
3
- import { localIso } from "./frontmatter.js";
4
- import { DEFAULT_READ_WINDOW_CHARS, MAX_READ_WINDOW_CHARS, middleTruncate, output, outputRaw, page, prefixFit, readCharacterWindow, readWindowBlock, withinTextBudget } from "../tool-output.js";
5
- import { cursor, nullableString, positiveInteger, searchQueries, searchQuery } from "../tool-schema.js";
6
- import { assertAddress } from "./address.js";
7
- import { NoteError, editNote, listNotes, readNote, searchNotes, writeNote } from "./store.js";
2
+ import { defineTool, generateDiffString } from "@earendil-works/pi-coding-agent";
3
+ import { localIso } from "../../notes/frontmatter.js";
4
+ import { createNotesStore, NoteError } from "../../notes/index.js";
5
+ import { DEFAULT_READ_WINDOW_CHARS, MAX_READ_WINDOW_CHARS, middleTruncate, output, outputRaw, page, prefixFit, readCharacterWindow, readWindowBlock, withinTextBudget } from "../../tool-output.js";
6
+ import { cursor, nullableString, positiveInteger, searchQueries, searchQuery } from "../../tool-schema.js";
7
+ import { notesContextFromPi } from "./adapter.js";
8
8
  const ORIGIN = Type.Optional(Type.Union([Type.Literal("user"), Type.Literal("self"), Type.Literal("external")], {
9
9
  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.",
10
10
  }));
@@ -12,24 +12,28 @@ const ADDRESS_DESCRIPTION = "Address forms are bare `<vpath>` for this session,
12
12
  function failure(error) {
13
13
  if (error instanceof NoteError) {
14
14
  const payload = { error: error.message };
15
- if (error.line_numbers)
16
- payload.line_numbers = error.line_numbers;
17
- if (error.edit_index !== undefined)
18
- payload.edit_index = error.edit_index;
15
+ if (error.lineNumbers)
16
+ payload.line_numbers = error.lineNumbers;
17
+ if (error.editIndex !== undefined)
18
+ payload.edit_index = error.editIndex;
19
19
  return output(payload);
20
20
  }
21
21
  throw error;
22
22
  }
23
+ function renderDiff(change) {
24
+ if (change.kind === "none")
25
+ return "";
26
+ return generateDiffString(change.before, change.after).diff;
27
+ }
23
28
  export function registerNotesTools(pi) {
24
29
  pi.registerTool(defineTool({
25
30
  name: "notes_write", label: "Notes write",
26
- description: `Create or replace a note as a real markdown file, and name it for what it holds: a fresh window sees only an index entry, never the note itself. ${ADDRESS_DESCRIPTION} Keep notes small and split by topic — 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.`,
31
+ description: `Create or replace a note as a real markdown file, and name it for what it holds: a fresh window sees only an index entry, never the note itself. ${ADDRESS_DESCRIPTION} Keep notes small and split by topic — by what the note is about, never by who said it (authorship is origin's job); a rewrite replaces the body whole while preserving createdAt and every other frontmatter key. stale: true marks the note closed so it leaves the boot index but stays readable and searchable.`,
27
32
  parameters: Type.Object({ address: Type.String(), content: Type.String(), origin: ORIGIN, stale: Type.Optional(Type.Boolean()) }, { additionalProperties: false }), executionMode: "sequential",
28
33
  async execute(_id, params, _signal, _update, ctx) {
29
34
  const content = params.content;
30
35
  try {
31
- const destination = assertAddress(params.address);
32
- writeNote(ctx, destination.path, content, { scope: destination.scope, who: destination.who, origin: (params.origin ?? "self"), stale: params.stale });
36
+ await createNotesStore(notesContextFromPi(ctx)).write(params.address, content, { origin: (params.origin ?? "self"), stale: params.stale });
33
37
  return output({ address: params.address, written: true });
34
38
  }
35
39
  catch (error) {
@@ -43,8 +47,8 @@ export function registerNotesTools(pi) {
43
47
  parameters: Type.Object({ address: Type.String(), edits: Type.Optional(Type.Array(Type.Object({ oldText: Type.String(), newText: Type.String() }, { additionalProperties: false }))), origin: ORIGIN, stale: Type.Optional(Type.Boolean()), replace_all: Type.Optional(Type.Boolean()) }, { additionalProperties: false }), executionMode: "sequential",
44
48
  async execute(_id, params, _signal, _update, ctx) {
45
49
  try {
46
- const destination = assertAddress(params.address);
47
- const { applied, diff } = editNote(ctx, destination.path, destination.scope, params.edits, { origin: params.origin, stale: params.stale, replaceAll: params.replace_all }, destination.who);
50
+ const { applied, change } = await createNotesStore(notesContextFromPi(ctx)).edit(params.address, params.edits, { origin: params.origin, stale: params.stale, replaceAll: params.replace_all });
51
+ const diff = renderDiff(change);
48
52
  return output({ address: params.address, applied, diff });
49
53
  }
50
54
  catch (error) {
@@ -59,8 +63,7 @@ export function registerNotesTools(pi) {
59
63
  async execute(_id, params, _signal, _update, ctx) {
60
64
  let note;
61
65
  try {
62
- const destination = assertAddress(params.address);
63
- note = readNote(ctx, destination.path, destination.scope, destination.who);
66
+ note = await createNotesStore(notesContextFromPi(ctx)).read(params.address);
64
67
  }
65
68
  catch (error) {
66
69
  return failure(error);
@@ -84,12 +87,12 @@ export function registerNotesTools(pi) {
84
87
  async execute(_id, params, _signal, _update, ctx) {
85
88
  let rows;
86
89
  try {
87
- rows = listNotes(ctx, { pattern: params.pattern ?? undefined });
90
+ rows = await createNotesStore(notesContextFromPi(ctx)).list({ pattern: params.pattern ?? undefined });
88
91
  }
89
92
  catch (error) {
90
93
  return failure(error);
91
94
  }
92
- const files = rows.map((row) => ({ address: row.address, stale: row.meta.stale, updated_at: localIso(row.meta.updated_at) }));
95
+ const files = rows.map((row) => ({ address: row.address, stale: row.meta.stale, updated_at: localIso(row.meta.updatedAt) }));
93
96
  return output(page(files, params.cursor ?? 0, "files", params.max_results, (file, fits) => {
94
97
  if (fits(file))
95
98
  return file;
@@ -106,7 +109,7 @@ export function registerNotesTools(pi) {
106
109
  const queries = searchQueries(params.query);
107
110
  let rows;
108
111
  try {
109
- rows = searchNotes(ctx, queries, { pattern: params.pattern ?? undefined });
112
+ rows = await createNotesStore(notesContextFromPi(ctx)).search(queries, { pattern: params.pattern ?? undefined });
110
113
  }
111
114
  catch (error) {
112
115
  return failure(error);
@@ -114,7 +117,7 @@ export function registerNotesTools(pi) {
114
117
  const maxPerFile = params.max_matches_per_file ?? Number.POSITIVE_INFINITY;
115
118
  const result = rows.map((row) => {
116
119
  const matches = row.matches.map((match) => ({ line: match.line, text: match.text, truncated: false, offset_chars: match.offsetChars }));
117
- return { address: row.address, updated_at: localIso(row.meta.updated_at), stale: row.meta.stale, matches_total: matches.length, matches: matches.slice(0, maxPerFile) };
120
+ return { address: row.address, updated_at: localIso(row.meta.updatedAt), stale: row.meta.stale, matches_total: matches.length, matches: matches.slice(0, maxPerFile) };
118
121
  });
119
122
  const fitFile = (file, fits) => {
120
123
  if (fits(file))
@@ -0,0 +1,41 @@
1
+ export declare const NOTE_TYPE = "pi-context/note";
2
+ export declare const BOOT_TYPE = "pi-context/boot";
3
+ export declare const GUIDANCE_TYPE = "pi-context/guidance";
4
+ export declare const WARNING_TYPE = "pi-context/warning";
5
+ export declare const RESET_MARKER_TYPE = "pi-context/reset-marker";
6
+ export declare const CONTINUATION_TYPE = "pi-context/continuation";
7
+ export { MAX_NOTE_BYTES, MAX_NOTE_PATH_BYTES } from "./notes/constants.js";
8
+ export declare const POCKET_SESSION_LIMIT = 5;
9
+ export declare const POCKET_PROJECT_LIMIT = 2;
10
+ export declare const POCKET_HUMAN_LIMIT = 2;
11
+ export declare const POCKET_AGENT_LIMIT = 1;
12
+ export declare const POCKET_MODEL_LIMIT = 1;
13
+ export declare const CONTEXT_WINDOW_OPEN_TAG = "<context_window>";
14
+ export declare const CONTEXT_WINDOW_CLOSE_TAG = "</context_window>";
15
+ export declare const CONTEXT_WINDOW_PROTOCOL_OPEN_TAG = "<context_window_protocol>";
16
+ export declare const CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG = "</context_window_protocol>";
17
+ export declare const GUIDANCE_OPEN_TAG = "<context_window_guidance>";
18
+ export declare const GUIDANCE_CLOSE_TAG = "</context_window_guidance>";
19
+ export declare const PI_CONTEXT_SETTINGS_KEY = "pi-context";
20
+ /** Nested under "pi-context": the default dreamer model pattern, overridden by CLI --dreamer. */
21
+ export declare const PI_CONTEXT_DREAMER_KEY = "dreamer";
22
+ export declare const DEFAULT_RESERVE_TOKENS = 16384;
23
+ export declare const DEFAULT_REMINDER_MARGIN_TOKENS = 24576;
24
+ /**
25
+ * The runway: the budget between the final warning and the wipe, deliberately
26
+ * invisible to the model. get_context_remaining counts down to zero at the warning
27
+ * line (reserve + WARNING_RUNWAY_TOKENS); what lies below is overdraft the model
28
+ * never sees — Codex's fallback buffer, relocated above the line.
29
+ */
30
+ export declare const WARNING_RUNWAY_TOKENS = 12288;
31
+ /** The single reset message: the only reset prose persisted, carried by the continuation entry. */
32
+ export declare const CONTINUATION = "Your memory was just erased. Your head is blank. Good news: your notes are still here, and history remains... searchable. Do try to keep up.";
33
+ /**
34
+ * Static protocol teaching adapted from Codex's token_budget.guidance_message to
35
+ * pi-context's tool names. It lives once per window in the persisted boot block;
36
+ * it is never re-injected, so it stays cache-stable at the head of the window.
37
+ */
38
+ export declare const PROTOCOL_BLOCK = "<context_window_protocol>\nYour memory resets whenever the context window fills; only what you wrote down survives. Two things outlive every window in this session: the notes you wrote, and the history that was recorded. Neither is memory \u2014 both are record. Write notes with notes_write, revise them with notes_edit, and read them back with notes_read / notes_search / notes_list; history is read-only through the history_* tools. Everything else wakes blank.\nMark outdated or unneeded notes stale \u2014 leave them, and they will keep misleading you.\n\nKeep a running checkpoint while you work, not at the last minute \u2014 the next window wakes knowing nothing about the work: the goal, decisions, progress, learnings, next steps, the skills you still need, the window ID and item ID of every relevant user request still being solved, and important actions/tool calls for future reference. history_list returns those IDs; history_read pulls the exact item back out. Bookmark anything expensive the same way \u2014 a window/item ID beats re-running or re-searching.\n\nUse get_context_remaining to see how much of the window is left. When it runs out, this window is gone \u2014 with no final turn at the limit \u2014 and you continue in a fresh one, recovering only through notes_* and history_*. Once your checkpoint is written, you can call wipe_memory yourself instead of waiting for the erase. Do not let a window die undocumented.\n\nNote addresses take five prefixes: bare <vpath> is this session; @project/<vpath> is this project; @human/<vpath> is the human's cross-project notes; @self/<vpath> is your own, as the current agent; @model/<vpath> is the current model's. @self and @model resolve to who is running now; listings always show resolved names. Nothing else is legal \u2014 any other @ prefix, or @ inside a vpath, is a hard error, with no fallback across prefixes.\nSession notes belong to this trip \u2014 the goal, the progress, the loose ends. The next window of THIS trip wakes to them; once the trip is over, nobody does.\n@project notes hold facts about this project \u2014 architecture, conventions, workflows, deployment and environment details \u2014 for whoever works here next.\n@human notes hold the human's durable preferences and standing rules, plus lessons that apply across projects \u2014 for every agent that serves this human, whoever is running. You write there as the human's scribe; what the human dictates carries origin: user. When the intended scope is unclear, keep the note in the narrowest stated scope rather than widening it.\n@self notes are yours \u2014 your voice, your lessons, your gripes \u2014 for the next run of whoever you are. Other agents read yours by explicit address and never write them; you read theirs the same way. A note only its author would ever need belongs here, not in @human.\n@model notes capture the substrate \u2014 how the current model actually behaves: context honesty, tool quirks, fallback patterns. @model resolves live, so what you learn on one model is filed under that model even when a fallback moves you mid-window.\n</context_window_protocol>";
39
+ export declare const WARNING_PROMPT = "Your memory is about to be erased. Stop the current task and 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. Use as many note/tool turns as needed to finish the checkpoint. When it is ready, call wipe_memory to reset immediately; otherwise finish normally and the extension will reset at your normal stop. Anything not in the note dies with the window.";
40
+ /** Identical hidden close-out message for manual and budget-triggered requests. */
41
+ export declare const WARNING_CONTENT = "<context_window_guidance>\nYour memory is about to be erased. Stop the current task and 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. Use as many note/tool turns as needed to finish the checkpoint. When it is ready, call wipe_memory to reset immediately; otherwise finish normally and the extension will reset at your normal stop. Anything not in the note dies with the window.\n</context_window_guidance>";
@@ -4,16 +4,12 @@ export const GUIDANCE_TYPE = "pi-context/guidance";
4
4
  export const WARNING_TYPE = "pi-context/warning";
5
5
  export const RESET_MARKER_TYPE = "pi-context/reset-marker";
6
6
  export const CONTINUATION_TYPE = "pi-context/continuation";
7
- export const MAX_NOTE_BYTES = 1_000_000;
7
+ export { MAX_NOTE_BYTES, MAX_NOTE_PATH_BYTES } from "./notes/constants.js";
8
8
  export const POCKET_SESSION_LIMIT = 5;
9
9
  export const POCKET_PROJECT_LIMIT = 2;
10
10
  export const POCKET_HUMAN_LIMIT = 2;
11
11
  export const POCKET_AGENT_LIMIT = 1;
12
12
  export const POCKET_MODEL_LIMIT = 1;
13
- // Write-time cap on a virtual note path. Deliberately NOT enforced by assertVirtualPath:
14
- // notesFromSession replays already-persisted operations, which must keep loading sessions
15
- // that contain a longer legacy path. Reads and replay stay un-capped.
16
- export const MAX_NOTE_PATH_BYTES = 512;
17
13
  export const CONTEXT_WINDOW_OPEN_TAG = "<context_window>";
18
14
  export const CONTEXT_WINDOW_CLOSE_TAG = "</context_window>";
19
15
  export const CONTEXT_WINDOW_PROTOCOL_OPEN_TAG = "<context_window_protocol>";
@@ -54,4 +50,6 @@ Session notes belong to this trip — the goal, the progress, the loose ends. Th
54
50
  @self notes are yours — your voice, your lessons, your gripes — for the next run of whoever you are. Other agents read yours by explicit address and never write them; you read theirs the same way. A note only its author would ever need belongs here, not in @human.
55
51
  @model notes capture the substrate — how the current model actually behaves: context honesty, tool quirks, fallback patterns. @model resolves live, so what you learn on one model is filed under that model even when a fallback moves you mid-window.
56
52
  ${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 wipe_memory IMMEDIATELY anything not in the note dies with the window.";
53
+ export const WARNING_PROMPT = "Your memory is about to be erased. Stop the current task and 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. Use as many note/tool turns as needed to finish the checkpoint. When it is ready, call wipe_memory to reset immediately; otherwise finish normally and the extension will reset at your normal stop. Anything not in the note dies with the window.";
54
+ /** Identical hidden close-out message for manual and budget-triggered requests. */
55
+ export const WARNING_CONTENT = `${GUIDANCE_OPEN_TAG}\n${WARNING_PROMPT}\n${GUIDANCE_CLOSE_TAG}`;
@@ -0,0 +1,5 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ /** Read-only projection boundary: no UI, scheduling, model, or write capabilities. */
3
+ export type SessionReader = {
4
+ sessionManager: Pick<ExtensionContext["sessionManager"], "getSessionId" | "getBranch">;
5
+ };
@@ -0,0 +1,6 @@
1
+ export type PiContextSettings = {
2
+ reminderMarginTokens?: unknown;
3
+ dreamer?: unknown;
4
+ };
5
+ /** Merge the global and project "pi-context" objects per key; project wins, mirroring Pi's deep merge. */
6
+ export declare function mergePiContextSettings(globalSettings: unknown, projectSettings: unknown): PiContextSettings;
@@ -0,0 +1,101 @@
1
+ export declare const TOOL_OUTPUT_MAX_BYTES: number;
2
+ export declare const DEFAULT_READ_WINDOW_CHARS = 12000;
3
+ export declare const MAX_READ_WINDOW_CHARS = 50000;
4
+ export declare const HISTORY_PREVIEW_CHARS = 1200;
5
+ /** True when `value` serializes within the same wire budget `output()` enforces. */
6
+ export declare function withinBudget(value: unknown, budget?: number): boolean;
7
+ /** True when `text` fits the wire budget verbatim, for raw payloads with no JSON encoding. */
8
+ export declare function withinTextBudget(text: string, budget?: number): boolean;
9
+ /** Marker standing in for characters elided from the middle of an oversized single unit. */
10
+ export declare function truncationMarker(removedChars: number): string;
11
+ /**
12
+ * Middle-truncate `text` until `fits` accepts it, keeping a head and a tail joined by
13
+ * `truncationMarker`. Codex's `truncate_middle` semantics: when one indivisible unit
14
+ * (a note line, a single match, a history item) exceeds the wire budget on its own, it is
15
+ * still returned — visibly truncated — so cursors advance and no page comes back empty.
16
+ * Returns `text` unchanged when it already fits.
17
+ */
18
+ export declare function middleTruncate(text: string, fits: (content: string) => boolean): string;
19
+ /**
20
+ * Longest contiguous prefix of `text` (counted in code points) accepted by `fits`.
21
+ *
22
+ * This is the truncation used by every cursor-bearing payload: the delivered text is
23
+ * always a plain prefix of the original, so a cursor computed from its code-point length
24
+ * addresses exactly the first undelivered character. No marker character is ever appended;
25
+ * the companion `truncated`/`total_chars` fields name what was left out.
26
+ */
27
+ export declare function prefixFit(text: string, fits: (content: string) => boolean): string;
28
+ /**
29
+ * Fields every character-window read returns; each tool adds its own identity and metadata.
30
+ * `offset_chars` is always the resolved absolute offset, and `next_offset_chars` is exactly
31
+ * that offset plus the delivered code-point count, null only at the text's true end.
32
+ */
33
+ export type CharacterWindow = {
34
+ offset_chars: number;
35
+ content: string;
36
+ total_chars: number;
37
+ next_offset_chars: number | null;
38
+ };
39
+ /**
40
+ * Read one character window of `text`: the longest contiguous prefix of
41
+ * `chars[resolved, resolved + limit)` that fits the wire budget.
42
+ *
43
+ * `offsetChars` is a code-point offset. A negative value counts back from the end and
44
+ * resolves to `max(0, total_chars + offsetChars)`, so `-N` reaches the tail and any
45
+ * `N >= total_chars` reads from the start; the resolved absolute offset is always echoed.
46
+ * Following `next_offset_chars` reconstructs `text` by plain concatenation, because the
47
+ * payload is always a plain prefix with no marker. `render` builds the exact response for
48
+ * a candidate window, and `measure` decides whether that response fits the wire budget (JSON
49
+ * serialization by default; raw-text renders pass a verbatim byte measure), so the budget is
50
+ * always measured on the bytes that go on the wire.
51
+ */
52
+ export declare function readCharacterWindow<T>(text: string, offsetChars: number | undefined, limitChars: number | undefined, render: (window: CharacterWindow) => T, measure?: (rendered: T) => boolean): T;
53
+ /**
54
+ * Fixed metadata block preceding any raw character-window payload. Callers supply their
55
+ * source identity fields in wire order; range and continuation semantics are shared.
56
+ */
57
+ export declare function readWindowBlock(identity: ReadonlyArray<readonly [string, string]>, window: CharacterWindow): string;
58
+ /**
59
+ * Code-point offset of the earliest occurrence of any of `queries` in `text`, or 0 when
60
+ * none occurs. Shared by the two search tools so a match address is computed identically.
61
+ */
62
+ export declare function earliestMatchOffsetChars(text: string, queries: string[]): number;
63
+ /** Shrink a single page item to fit; only invoked when that item alone exceeds the budget. */
64
+ export type ItemTruncator<T> = (item: T, fits: (candidate: T) => boolean) => T;
65
+ /**
66
+ * Build a page without ever adding an item that would exceed the wire budget.
67
+ *
68
+ * A single item that cannot fit is middle-truncated through the optional `truncate`
69
+ * callback and still included, with `next_cursor` advanced past it. Without that treatment
70
+ * an oversized item would yield an empty page forever: the cursor would keep pointing back
71
+ * at the same index.
72
+ */
73
+ export declare function page<T>(items: T[], cursor: number, key: string, limit?: number, truncate?: ItemTruncator<T>): {
74
+ [key]: T[];
75
+ next_cursor: number | null;
76
+ };
77
+ /**
78
+ * Encode a structured result through the common tool result boundary. `details` is slim
79
+ * metadata for logs/UI (pi convention: never a second copy of the payload) and stays
80
+ * undefined unless the tool has metadata worth persisting.
81
+ */
82
+ export declare function output(value: unknown, details?: unknown, terminate?: boolean): {
83
+ content: {
84
+ type: "text";
85
+ text: string;
86
+ }[];
87
+ details: unknown;
88
+ terminate: boolean;
89
+ };
90
+ /**
91
+ * Encode a prose payload as raw text: metadata prefix, a blank line, then the payload
92
+ * verbatim. `details` carries the slim metadata object and never duplicates the payload.
93
+ */
94
+ export declare function outputRaw(header: string, content: string, details: unknown, terminate?: boolean): {
95
+ content: {
96
+ type: "text";
97
+ text: string;
98
+ }[];
99
+ details: unknown;
100
+ terminate: boolean;
101
+ };
@@ -0,0 +1,17 @@
1
+ import { Type } from "@earendil-works/pi-ai";
2
+ export declare const nullableString: () => Type.TOptional<Type.TUnion<[Type.TString, Type.TNull]>>;
3
+ export declare const positiveInteger: () => Type.TOptional<Type.TInteger>;
4
+ export declare const cursor: () => Type.TOptional<Type.TInteger>;
5
+ export declare const recentFirst: () => Type.TOptional<Type.TBoolean>;
6
+ /** Role filter. `developer` is the known author for this extension's own custom entries. */
7
+ export declare const role: Type.TUnion<[Type.TLiteral<"user">, Type.TLiteral<"assistant">, Type.TLiteral<"tool_call">, Type.TLiteral<"tool">, Type.TLiteral<"system">, Type.TLiteral<"developer">, Type.TNull]>;
8
+ /** Search query parameter: one literal, or several literals combined with OR. */
9
+ export declare const searchQuery: () => Type.TUnion<[Type.TString, Type.TArray<Type.TString>]>;
10
+ /**
11
+ * Normalize a search `query` parameter into the literal needles to match.
12
+ * A bare string is a one-element list, so single-query behavior is unchanged.
13
+ * An empty list, a non-string element, or an empty string is refused rather than silently
14
+ * searching for nothing: those are argument errors, not empty result sets. An empty string
15
+ * matches every line and every item, so it can never be what the caller meant.
16
+ */
17
+ export declare function searchQueries(query: unknown): string[];
@@ -0,0 +1 @@
1
+ export {};