@astrosheep/pi-context 0.25.2 → 0.26.1
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 +88 -7
- package/dist/build-info.json +2 -2
- package/dist/extension.js +635 -381
- package/dist/src/context/boot.d.ts +24 -0
- package/dist/src/context/boot.js +33 -24
- package/dist/src/context/budget.d.ts +9 -0
- package/dist/src/context/budget.js +51 -28
- package/dist/src/context/context-window.d.ts +41 -0
- package/dist/src/context/context-window.js +16 -1
- package/dist/src/context/prompts.d.ts +20 -0
- package/dist/src/context/prompts.js +1 -1
- package/dist/src/context/reset-artifacts.d.ts +26 -0
- package/dist/src/context/reset-artifacts.js +18 -17
- package/dist/src/context/reset-lifecycle.d.ts +89 -0
- package/dist/src/context/reset-lifecycle.js +103 -75
- package/dist/src/context/runtime.d.ts +3 -0
- package/dist/src/context/runtime.js +53 -21
- package/dist/src/context/thresholds.d.ts +33 -0
- package/dist/src/context/thresholds.js +1 -1
- package/dist/src/dream/cli.d.ts +10 -0
- package/dist/src/dream/cli.js +1 -1
- package/dist/src/dream/doctor.d.ts +2 -0
- package/dist/src/dream/doctor.js +6 -2
- package/dist/src/dream/gates.d.ts +10 -0
- package/dist/src/dream/git.d.ts +21 -0
- package/dist/src/dream/lock.d.ts +31 -0
- package/dist/src/dream/runner.d.ts +30 -0
- package/dist/src/dream/settings.d.ts +16 -0
- package/dist/src/history/history-tools.d.ts +2 -0
- package/dist/src/history/history.d.ts +57 -0
- package/dist/src/index.d.ts +39 -0
- package/dist/src/index.js +4 -4
- package/dist/src/notes/address.d.ts +26 -0
- package/dist/src/notes/address.js +8 -14
- package/dist/src/notes/constants.d.ts +3 -0
- package/dist/src/notes/constants.js +3 -0
- package/dist/src/notes/context.d.ts +10 -0
- package/dist/src/notes/context.js +33 -0
- package/dist/src/notes/frontmatter.d.ts +46 -0
- package/dist/src/notes/frontmatter.js +10 -5
- package/dist/src/notes/index.d.ts +4 -0
- package/dist/src/notes/index.js +2 -0
- package/dist/src/notes/paths.d.ts +21 -0
- package/dist/src/notes/paths.js +72 -76
- package/dist/src/notes/store.d.ts +94 -0
- package/dist/src/notes/store.js +276 -236
- package/dist/src/pi/notes/adapter.d.ts +12 -0
- package/dist/src/pi/notes/adapter.js +39 -0
- package/dist/src/pi/notes/session-replay.d.ts +16 -0
- package/dist/src/{notes → pi/notes}/session-replay.js +2 -2
- package/dist/src/pi/notes/snapshot.d.ts +33 -0
- package/dist/src/{notes/notes-snapshot.js → pi/notes/snapshot.js} +11 -3
- package/dist/src/pi/notes/tools.d.ts +2 -0
- package/dist/src/{notes → pi/notes}/tools.js +24 -21
- package/dist/src/protocol.d.ts +41 -0
- package/dist/src/protocol.js +4 -6
- package/dist/src/session-reader.d.ts +5 -0
- package/dist/src/settings.d.ts +6 -0
- package/dist/src/text-match.d.ts +5 -0
- package/dist/src/text-match.js +15 -0
- package/dist/src/tool-output.d.ts +97 -0
- package/dist/src/tool-output.js +1 -15
- package/dist/src/tool-schema.d.ts +17 -0
- package/dist/test/agent-loop.test.d.ts +1 -0
- package/dist/test/agent-loop.test.js +387 -20
- package/dist/test/boot.integration.test.d.ts +1 -0
- package/dist/test/boot.integration.test.js +59 -35
- package/dist/test/budget-settings.integration.test.d.ts +1 -0
- package/dist/test/budget-settings.integration.test.js +28 -12
- package/dist/test/doctor.test.d.ts +1 -0
- package/dist/test/doctor.test.js +10 -2
- package/dist/test/dream-skill.test.d.ts +1 -0
- package/dist/test/dream-skill.test.js +69 -0
- package/dist/test/dream.test.d.ts +1 -0
- package/dist/test/helpers/extension-test-environment.d.ts +1 -0
- package/dist/test/helpers/extension-test-environment.js +9 -0
- package/dist/test/helpers/extension.d.ts +104 -0
- package/dist/test/helpers/extension.js +7 -81
- package/dist/test/helpers/notes.d.ts +6 -0
- package/dist/test/helpers/notes.js +13 -0
- package/dist/test/history.integration.test.d.ts +1 -0
- package/dist/test/history.integration.test.js +17 -19
- package/dist/test/notes-library.test.d.ts +1 -0
- package/dist/test/notes-library.test.js +128 -0
- package/dist/test/notes.integration.test.d.ts +1 -0
- package/dist/test/notes.integration.test.js +51 -55
- package/dist/test/notes.test.d.ts +1 -0
- package/dist/test/notes.test.js +162 -34
- package/dist/test/reset-lifecycle.test.d.ts +1 -0
- package/dist/test/reset-lifecycle.test.js +142 -85
- package/docs/architecture.md +8 -8
- package/docs/reset-lifecycle.md +63 -79
- package/package.json +35 -2
- package/playbook.md +33 -32
- package/skills/dream/SKILL.md +12 -0
- package/src/context/boot.ts +44 -25
- package/src/context/budget.ts +52 -27
- package/src/context/context-window.ts +16 -1
- package/src/context/prompts.ts +2 -2
- package/src/context/reset-artifacts.ts +26 -24
- package/src/context/reset-lifecycle.ts +117 -111
- package/src/context/runtime.ts +50 -22
- package/src/context/thresholds.ts +1 -1
- package/src/dream/cli.ts +1 -1
- package/src/dream/doctor.ts +5 -2
- package/src/index.ts +4 -4
- package/src/notes/address.ts +9 -15
- package/src/notes/constants.ts +3 -0
- package/src/notes/context.ts +40 -0
- package/src/notes/frontmatter.ts +18 -12
- package/src/notes/index.ts +22 -0
- package/src/notes/paths.ts +64 -78
- package/src/notes/store.ts +289 -238
- package/src/pi/notes/adapter.ts +44 -0
- package/src/{notes → pi/notes}/session-replay.ts +3 -3
- package/src/{notes/notes-snapshot.ts → pi/notes/snapshot.ts} +13 -4
- package/src/{notes → pi/notes}/tools.ts +25 -23
- package/src/protocol.ts +5 -6
- package/src/text-match.ts +13 -0
- package/src/tool-output.ts +2 -14
package/dist/extension.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// <define:__PI_CONTEXT_BUILD__>
|
|
2
|
-
var define_PI_CONTEXT_BUILD_default = { version: "0.
|
|
2
|
+
var define_PI_CONTEXT_BUILD_default = { version: "0.26.1", sourceHash: "fb313022cd1aab93e422144e8f336eced42b67c4d35b89a1133be2254f066093" };
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
5
|
import { VERSION as VERSION2 } from "@earendil-works/pi-coding-agent";
|
|
@@ -8,6 +8,17 @@ import { VERSION as VERSION2 } from "@earendil-works/pi-coding-agent";
|
|
|
8
8
|
import { Type as Type2 } from "@earendil-works/pi-ai";
|
|
9
9
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
|
|
11
|
+
// src/text-match.ts
|
|
12
|
+
function earliestMatchOffsetChars(text, queries) {
|
|
13
|
+
let earliest = -1;
|
|
14
|
+
for (const query of queries) {
|
|
15
|
+
const index = text.indexOf(query);
|
|
16
|
+
if (index < 0) continue;
|
|
17
|
+
if (earliest < 0 || index < earliest) earliest = index;
|
|
18
|
+
}
|
|
19
|
+
return earliest <= 0 ? 0 : Array.from(text.slice(0, earliest)).length;
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
// src/tool-output.ts
|
|
12
23
|
var TOOL_OUTPUT_MAX_BYTES = 32 * 1024;
|
|
13
24
|
var DEFAULT_READ_WINDOW_CHARS = 12e3;
|
|
@@ -76,15 +87,6 @@ chars: [${window.offset_chars},${end}) of ${window.total_chars}
|
|
|
76
87
|
next_offset_chars: ${next}
|
|
77
88
|
`;
|
|
78
89
|
}
|
|
79
|
-
function earliestMatchOffsetChars(text, queries) {
|
|
80
|
-
let earliest = -1;
|
|
81
|
-
for (const query of queries) {
|
|
82
|
-
const index = text.indexOf(query);
|
|
83
|
-
if (index < 0) continue;
|
|
84
|
-
if (earliest < 0 || index < earliest) earliest = index;
|
|
85
|
-
}
|
|
86
|
-
return earliest <= 0 ? 0 : Array.from(text.slice(0, earliest)).length;
|
|
87
|
-
}
|
|
88
90
|
function page(items, cursor2, key, limit, truncate) {
|
|
89
91
|
const end = Math.min(items.length, cursor2 + (limit ?? items.length));
|
|
90
92
|
const selected = [];
|
|
@@ -235,6 +237,10 @@ function estimateToolsTokens(tools) {
|
|
|
235
237
|
// src/context/context-window.ts
|
|
236
238
|
import { convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
237
239
|
|
|
240
|
+
// src/notes/constants.ts
|
|
241
|
+
var MAX_NOTE_BYTES = 1e6;
|
|
242
|
+
var MAX_NOTE_PATH_BYTES = 512;
|
|
243
|
+
|
|
238
244
|
// src/protocol.ts
|
|
239
245
|
var NOTE_TYPE = "pi-context/note";
|
|
240
246
|
var BOOT_TYPE = "pi-context/boot";
|
|
@@ -242,13 +248,11 @@ var GUIDANCE_TYPE = "pi-context/guidance";
|
|
|
242
248
|
var WARNING_TYPE = "pi-context/warning";
|
|
243
249
|
var RESET_MARKER_TYPE = "pi-context/reset-marker";
|
|
244
250
|
var CONTINUATION_TYPE = "pi-context/continuation";
|
|
245
|
-
var MAX_NOTE_BYTES = 1e6;
|
|
246
251
|
var POCKET_SESSION_LIMIT = 5;
|
|
247
252
|
var POCKET_PROJECT_LIMIT = 2;
|
|
248
253
|
var POCKET_HUMAN_LIMIT = 2;
|
|
249
254
|
var POCKET_AGENT_LIMIT = 1;
|
|
250
255
|
var POCKET_MODEL_LIMIT = 1;
|
|
251
|
-
var MAX_NOTE_PATH_BYTES = 512;
|
|
252
256
|
var CONTEXT_WINDOW_OPEN_TAG = "<context_window>";
|
|
253
257
|
var CONTEXT_WINDOW_CLOSE_TAG = "</context_window>";
|
|
254
258
|
var CONTEXT_WINDOW_PROTOCOL_OPEN_TAG = "<context_window_protocol>";
|
|
@@ -275,7 +279,10 @@ Session notes belong to this trip \u2014 the goal, the progress, the loose ends.
|
|
|
275
279
|
@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.
|
|
276
280
|
@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.
|
|
277
281
|
${CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG}`;
|
|
278
|
-
var WARNING_PROMPT = "Your memory is about to be erased.
|
|
282
|
+
var 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.";
|
|
283
|
+
var WARNING_CONTENT = `${GUIDANCE_OPEN_TAG}
|
|
284
|
+
${WARNING_PROMPT}
|
|
285
|
+
${GUIDANCE_CLOSE_TAG}`;
|
|
279
286
|
|
|
280
287
|
// src/context/context-window.ts
|
|
281
288
|
function isWindowMarker(entry) {
|
|
@@ -289,8 +296,15 @@ function currentReset(ctx) {
|
|
|
289
296
|
}
|
|
290
297
|
return void 0;
|
|
291
298
|
}
|
|
292
|
-
function
|
|
293
|
-
|
|
299
|
+
function isCheckpointBackedReset(ctx, marker = currentReset(ctx)) {
|
|
300
|
+
if (!marker?.parentId) return false;
|
|
301
|
+
const branch = ctx.sessionManager.getBranch();
|
|
302
|
+
const markerIndex = branch.findIndex((entry) => entry.id === marker.id);
|
|
303
|
+
const checkpoint = branch[markerIndex - 1];
|
|
304
|
+
return markerIndex > 0 && checkpoint?.id === marker.parentId && checkpoint.type === "compaction" && checkpoint.summary === "" && checkpoint.firstKeptEntryId === checkpoint.id;
|
|
305
|
+
}
|
|
306
|
+
function rootWindowId(sessionId) {
|
|
307
|
+
return `pcw:${sessionId.slice(0, 8)}:root`;
|
|
294
308
|
}
|
|
295
309
|
function hasWindowMessage(ctx, customType) {
|
|
296
310
|
const branch = ctx.sessionManager.getBranch();
|
|
@@ -343,7 +357,8 @@ function windowUsage(ctx) {
|
|
|
343
357
|
if (!contextWindow) return void 0;
|
|
344
358
|
const windowId = reset.data.windowId;
|
|
345
359
|
try {
|
|
346
|
-
const
|
|
360
|
+
const canonicalMessages = ctx.sessionManager.buildSessionProjection().messages;
|
|
361
|
+
const messages = isCheckpointBackedReset(ctx, reset) ? canonicalMessages : projectWindow(canonicalMessages, windowId);
|
|
347
362
|
const { tokens } = estimateContextTokens(convertToLlm(messages));
|
|
348
363
|
return { tokens, contextWindow, percent: tokens / contextWindow * 100 };
|
|
349
364
|
} catch {
|
|
@@ -405,8 +420,8 @@ function toolCallItems(windowId, entry, message) {
|
|
|
405
420
|
return items;
|
|
406
421
|
}
|
|
407
422
|
function historyFromSession(ctx) {
|
|
408
|
-
const
|
|
409
|
-
let window = { windowId: rootWindowId(
|
|
423
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
424
|
+
let window = { windowId: rootWindowId(sessionId), items: [] };
|
|
410
425
|
const windows = [window];
|
|
411
426
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
412
427
|
if (isWindowMarker(entry)) {
|
|
@@ -570,16 +585,17 @@ function registerHistoryTools(pi) {
|
|
|
570
585
|
}));
|
|
571
586
|
}
|
|
572
587
|
|
|
573
|
-
// src/notes/tools.ts
|
|
588
|
+
// src/pi/notes/tools.ts
|
|
574
589
|
import { Type as Type3 } from "@earendil-works/pi-ai";
|
|
575
|
-
import { defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
|
|
590
|
+
import { defineTool as defineTool2, generateDiffString } from "@earendil-works/pi-coding-agent";
|
|
576
591
|
|
|
577
592
|
// src/notes/frontmatter.ts
|
|
578
593
|
var SCOPES = ["session", "project", "human", "agent", "model"];
|
|
579
594
|
var ORIGINS = ["user", "self", "external"];
|
|
580
595
|
var STATUSES = ["active", "superseded", "pending", "archived"];
|
|
581
|
-
var TIMESTAMP_KEYS = ["
|
|
582
|
-
var
|
|
596
|
+
var TIMESTAMP_KEYS = ["createdAt", "updatedAt", "lastAccessed"];
|
|
597
|
+
var LEGACY_KNOWN_KEYS = ["created_at", "updated_at", "last_accessed", "access_count", "source_window", "recurrence_count", "recurrence_windows"];
|
|
598
|
+
var KNOWN_KEYS = ["origin", "status", "stale", "createdAt", "updatedAt", "lastAccessed", "accessCount", "sourceWindow", "supersedes", "recurrenceCount", "recurrenceWindows"];
|
|
583
599
|
var pad2 = (value) => String(value).padStart(2, "0");
|
|
584
600
|
function localIso(epochMs) {
|
|
585
601
|
const date = new Date(epochMs);
|
|
@@ -628,7 +644,7 @@ function parseFrontmatter(raw) {
|
|
|
628
644
|
}
|
|
629
645
|
}
|
|
630
646
|
if (close === -1) return { fields: {}, body: raw };
|
|
631
|
-
const fields =
|
|
647
|
+
const fields = /* @__PURE__ */ Object.create(null);
|
|
632
648
|
for (let index = 1; index < close; index++) {
|
|
633
649
|
const line = lines[index];
|
|
634
650
|
const match = /^([A-Za-z_][A-Za-z0-9_-]*):(.*)$/.exec(line);
|
|
@@ -652,13 +668,15 @@ function parseFrontmatter(raw) {
|
|
|
652
668
|
}
|
|
653
669
|
function parseNote(raw, now = Date.now()) {
|
|
654
670
|
const { fields, body } = parseFrontmatter(raw);
|
|
671
|
+
const legacyKeys = LEGACY_KNOWN_KEYS.filter((key) => Object.hasOwn(fields, key));
|
|
672
|
+
if (legacyKeys.length > 0) throw new Error(`legacy note metadata ${legacyKeys.join(", ")} requires manual migration to camelCase before this note can be used`);
|
|
655
673
|
const meta = { ...fields };
|
|
656
674
|
meta.scope = isScope(meta.scope) ? meta.scope : "session";
|
|
657
675
|
meta.origin = isOrigin(meta.origin) ? meta.origin : "self";
|
|
658
676
|
meta.status = isStatus(meta.status) ? meta.status : "active";
|
|
659
677
|
meta.stale = meta.stale === true;
|
|
660
678
|
for (const key of TIMESTAMP_KEYS) meta[key] = toEpoch(meta[key], now);
|
|
661
|
-
meta.
|
|
679
|
+
meta.accessCount = typeof meta.accessCount === "number" && Number.isFinite(meta.accessCount) ? meta.accessCount : 0;
|
|
662
680
|
return { meta, body };
|
|
663
681
|
}
|
|
664
682
|
function yamlScalar(value) {
|
|
@@ -691,67 +709,73 @@ function stripLeadingFrontmatter(content) {
|
|
|
691
709
|
return parseFrontmatter(content).body;
|
|
692
710
|
}
|
|
693
711
|
|
|
712
|
+
// src/notes/store.ts
|
|
713
|
+
import { randomUUID } from "node:crypto";
|
|
714
|
+
import { mkdir, readdir as readdir2, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
715
|
+
import { dirname as dirname2, join as join2, resolve as resolve3 } from "node:path";
|
|
716
|
+
|
|
694
717
|
// src/notes/paths.ts
|
|
695
718
|
import { createHash } from "node:crypto";
|
|
696
|
-
import { existsSync,
|
|
697
|
-
import {
|
|
698
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
719
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
720
|
+
import { readdir } from "node:fs/promises";
|
|
721
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
722
|
+
var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
723
|
+
function slugify(value) {
|
|
724
|
+
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
725
|
+
return slug.length > 0 ? slug : "root";
|
|
702
726
|
}
|
|
703
|
-
function
|
|
704
|
-
|
|
727
|
+
function projectKey(cwd) {
|
|
728
|
+
const absolute = resolve(cwd);
|
|
729
|
+
const root = gitRoot(absolute) ?? absolute;
|
|
730
|
+
const digest = createHash("sha1").update(root).digest("hex").slice(0, 8);
|
|
731
|
+
return `${basename(root)}-${digest}`;
|
|
732
|
+
}
|
|
733
|
+
function repositoryRoot(dir) {
|
|
734
|
+
let stats;
|
|
735
|
+
try {
|
|
736
|
+
stats = statSync(join(dir, ".git"));
|
|
737
|
+
} catch {
|
|
738
|
+
return dir;
|
|
739
|
+
}
|
|
740
|
+
if (stats.isDirectory()) return dir;
|
|
741
|
+
let pointer;
|
|
742
|
+
try {
|
|
743
|
+
pointer = readFileSync(join(dir, ".git"), "utf8");
|
|
744
|
+
} catch {
|
|
745
|
+
return dir;
|
|
746
|
+
}
|
|
747
|
+
const match = /^gitdir:\s*(.+)$/m.exec(pointer);
|
|
748
|
+
if (!match) return dir;
|
|
749
|
+
const parts = resolve(dir, match[1].trim()).split(sep);
|
|
750
|
+
const worktrees = parts.lastIndexOf("worktrees");
|
|
751
|
+
if (worktrees <= 0 || worktrees !== parts.length - 2) return dir;
|
|
752
|
+
const common = parts.slice(0, worktrees).join(sep);
|
|
753
|
+
return basename(common) === ".git" ? dirname(common) : dir;
|
|
705
754
|
}
|
|
706
755
|
function gitRoot(cwd) {
|
|
707
756
|
let dir = resolve(cwd);
|
|
708
757
|
for (; ; ) {
|
|
709
|
-
if (existsSync(join(dir, ".git"))) return dir;
|
|
758
|
+
if (existsSync(join(dir, ".git"))) return repositoryRoot(dir);
|
|
710
759
|
const parent = dirname(dir);
|
|
711
760
|
if (parent === dir) return void 0;
|
|
712
761
|
dir = parent;
|
|
713
762
|
}
|
|
714
763
|
}
|
|
715
|
-
function
|
|
716
|
-
|
|
717
|
-
const root = gitRoot(absolute) ?? absolute;
|
|
718
|
-
const digest = createHash("sha1").update(root).digest("hex").slice(0, 8);
|
|
719
|
-
return `${basename(root)}-${digest}`;
|
|
720
|
-
}
|
|
721
|
-
function sessionId(ctx) {
|
|
722
|
-
return ctx.sessionManager.getSessionId();
|
|
723
|
-
}
|
|
724
|
-
var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
725
|
-
function slugify(value) {
|
|
726
|
-
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
727
|
-
return slug.length > 0 ? slug : "root";
|
|
728
|
-
}
|
|
729
|
-
function agentSlug(_ctx) {
|
|
730
|
-
return slugify(process.env.PI_NOTES_AGENT ?? "root");
|
|
731
|
-
}
|
|
732
|
-
function modelSlug(ctx) {
|
|
733
|
-
const id = ctx.model?.id;
|
|
734
|
-
if (!id) return "default";
|
|
735
|
-
return slugify(id.split("/").pop() ?? id);
|
|
736
|
-
}
|
|
737
|
-
function scopeDir(scope, ctx, who) {
|
|
738
|
-
if (scope === "human") return join(notesRoot(), "human");
|
|
739
|
-
if (scope === "project") return join(notesRoot(), "project", projectKey(ctx.cwd));
|
|
740
|
-
if (scope === "agent") return join(notesRoot(), "agents", who ?? agentSlug(ctx));
|
|
741
|
-
if (scope === "model") return join(notesRoot(), "models", who ?? modelSlug(ctx));
|
|
742
|
-
return join(sessionHomesRoot(), sessionId(ctx));
|
|
764
|
+
function sessionHomesRoot(home) {
|
|
765
|
+
return join(home, "pi", "session");
|
|
743
766
|
}
|
|
744
|
-
function
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
if (
|
|
748
|
-
if (
|
|
749
|
-
|
|
750
|
-
return
|
|
767
|
+
function scopeDir(scope, context, who) {
|
|
768
|
+
if (!["session", "project", "human", "agent", "model"].includes(scope)) throw new TypeError("invalid notes scope");
|
|
769
|
+
if (who !== void 0 && (scope !== "agent" && scope !== "model" || !SLUG_PATTERN.test(who))) throw new TypeError("who must be a canonical agent/model slug");
|
|
770
|
+
if (scope === "human") return join(context.home, "human");
|
|
771
|
+
if (scope === "project") return join(context.home, "project", context.projectKey);
|
|
772
|
+
if (scope === "agent") return join(context.home, "agents", who ?? context.agent);
|
|
773
|
+
if (scope === "model") return join(context.home, "models", who ?? context.model);
|
|
774
|
+
return join(sessionHomesRoot(context.home), context.sessionId);
|
|
751
775
|
}
|
|
752
|
-
function namespaceSlugs(namespace, home
|
|
776
|
+
async function namespaceSlugs(namespace, home) {
|
|
753
777
|
try {
|
|
754
|
-
return
|
|
778
|
+
return (await readdir(join(home, namespace), { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
755
779
|
} catch {
|
|
756
780
|
return [];
|
|
757
781
|
}
|
|
@@ -759,8 +783,10 @@ function namespaceSlugs(namespace, home = notesRoot()) {
|
|
|
759
783
|
function noteFileName(vpath) {
|
|
760
784
|
return vpath.endsWith(".md") ? vpath : `${vpath}.md`;
|
|
761
785
|
}
|
|
762
|
-
function physicalPath(scope, vpath,
|
|
763
|
-
|
|
786
|
+
function physicalPath(scope, vpath, context, who) {
|
|
787
|
+
if (typeof vpath !== "string" || vpath.length === 0 || vpath.includes("\0") || vpath.includes("\\") || vpath.startsWith("/")) throw new TypeError("path must be a safe virtual relative path");
|
|
788
|
+
if (vpath.split("/").some((part) => part.length === 0 || part === "." || part === "..")) throw new TypeError("path contains an unsupported component");
|
|
789
|
+
return join(scopeDir(scope, context, who), ...noteFileName(vpath).split("/"));
|
|
764
790
|
}
|
|
765
791
|
|
|
766
792
|
// src/notes/address.ts
|
|
@@ -827,52 +853,91 @@ function assertAddress(value) {
|
|
|
827
853
|
assertVirtualPath(path);
|
|
828
854
|
return { scope, path, who };
|
|
829
855
|
}
|
|
830
|
-
function addressFor(
|
|
856
|
+
function addressFor(context, scope, path, who) {
|
|
831
857
|
if (scope === "session") return path;
|
|
832
858
|
if (scope === "project") return `@project/${path}`;
|
|
833
859
|
if (scope === "human") return `@human/${path}`;
|
|
834
|
-
if (scope === "agent") return `@agents/${who ??
|
|
835
|
-
return `@models/${who ??
|
|
860
|
+
if (scope === "agent") return `@agents/${who ?? context.agent}/${path}`;
|
|
861
|
+
return `@models/${who ?? context.model}/${path}`;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/notes/context.ts
|
|
865
|
+
import { resolve as resolve2 } from "node:path";
|
|
866
|
+
function requireString(field, value) {
|
|
867
|
+
if (typeof value !== "string" || value.length === 0) throw new TypeError(`notes context ${field} must be a non-empty string`);
|
|
868
|
+
return value;
|
|
869
|
+
}
|
|
870
|
+
function assertDirectoryComponent(field, value) {
|
|
871
|
+
if (value === "." || value === ".." || /[\\/\0:]/.test(value)) {
|
|
872
|
+
throw new TypeError(`notes context ${field} must be a safe single directory component`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function snapshotNotesContext(value) {
|
|
876
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("notes context must be a plain object");
|
|
877
|
+
const prototype = Object.getPrototypeOf(value);
|
|
878
|
+
if (prototype !== Object.prototype && prototype !== null) throw new TypeError("notes context must be a plain object");
|
|
879
|
+
const homeValue = requireString("home", value.home);
|
|
880
|
+
const sessionId = requireString("sessionId", value.sessionId);
|
|
881
|
+
const projectKey2 = requireString("projectKey", value.projectKey);
|
|
882
|
+
const agent = requireString("agent", value.agent);
|
|
883
|
+
const model = requireString("model", value.model);
|
|
884
|
+
assertDirectoryComponent("sessionId", sessionId);
|
|
885
|
+
assertDirectoryComponent("projectKey", projectKey2);
|
|
886
|
+
if (!SLUG_PATTERN.test(agent)) throw new TypeError("notes context agent must be a canonical lowercase slug");
|
|
887
|
+
if (!SLUG_PATTERN.test(model)) throw new TypeError("notes context model must be a canonical lowercase slug");
|
|
888
|
+
const home = resolve2(homeValue);
|
|
889
|
+
return Object.freeze({ home, sessionId, projectKey: projectKey2, agent, model });
|
|
836
890
|
}
|
|
837
891
|
|
|
838
892
|
// src/notes/store.ts
|
|
839
|
-
import { randomUUID } from "node:crypto";
|
|
840
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync as readdirSync2, renameSync as renameSync2, rmSync, writeFileSync } from "node:fs";
|
|
841
|
-
import { dirname as dirname2 } from "node:path";
|
|
842
|
-
import { generateDiffString } from "@earendil-works/pi-coding-agent";
|
|
843
893
|
var NoteError = class extends Error {
|
|
844
894
|
code;
|
|
845
|
-
|
|
846
|
-
|
|
895
|
+
lineNumbers;
|
|
896
|
+
editIndex;
|
|
847
897
|
constructor(code, message, extra = {}) {
|
|
848
898
|
super(message);
|
|
849
899
|
this.name = "NoteError";
|
|
850
900
|
this.code = code;
|
|
851
|
-
this.
|
|
852
|
-
this.
|
|
901
|
+
this.lineNumbers = extra.lineNumbers;
|
|
902
|
+
this.editIndex = extra.editIndex;
|
|
853
903
|
}
|
|
854
904
|
};
|
|
855
905
|
var SCOPE_ORDER = ["session", "project", "human", "agent", "model"];
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
906
|
+
var pathQueues = /* @__PURE__ */ new Map();
|
|
907
|
+
function withPathQueue(path, operation) {
|
|
908
|
+
const key = resolve3(path);
|
|
909
|
+
const previous = pathQueues.get(key) ?? Promise.resolve();
|
|
910
|
+
const result = previous.then(operation);
|
|
911
|
+
const tail = result.then(() => void 0, () => void 0);
|
|
912
|
+
pathQueues.set(key, tail);
|
|
913
|
+
void tail.then(() => {
|
|
914
|
+
if (pathQueues.get(key) === tail) pathQueues.delete(key);
|
|
915
|
+
});
|
|
916
|
+
return result;
|
|
859
917
|
}
|
|
860
|
-
function
|
|
861
|
-
|
|
862
|
-
|
|
918
|
+
function errno(error) {
|
|
919
|
+
return typeof error === "object" && error !== null ? error.code : void 0;
|
|
920
|
+
}
|
|
921
|
+
async function readFileIfExists(path) {
|
|
922
|
+
try {
|
|
923
|
+
return await readFile(path, "utf8");
|
|
924
|
+
} catch (error) {
|
|
925
|
+
if (errno(error) === "ENOENT") return void 0;
|
|
926
|
+
throw error;
|
|
927
|
+
}
|
|
863
928
|
}
|
|
864
|
-
function walkMarkdown(dir, base = dir) {
|
|
929
|
+
async function walkMarkdown(dir, base = dir) {
|
|
865
930
|
let entries;
|
|
866
931
|
try {
|
|
867
|
-
entries =
|
|
932
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
868
933
|
} catch (error) {
|
|
869
|
-
if (
|
|
934
|
+
if (errno(error) === "ENOENT") return [];
|
|
870
935
|
throw error;
|
|
871
936
|
}
|
|
872
937
|
const paths = [];
|
|
873
938
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
874
|
-
const child =
|
|
875
|
-
if (entry.isDirectory()) paths.push(...walkMarkdown(child, base));
|
|
939
|
+
const child = join2(dir, entry.name);
|
|
940
|
+
if (entry.isDirectory()) paths.push(...await walkMarkdown(child, base));
|
|
876
941
|
else if (entry.isFile() && entry.name.endsWith(".md")) paths.push(child.slice(base.length + 1).split("\\").join("/"));
|
|
877
942
|
}
|
|
878
943
|
return paths;
|
|
@@ -881,36 +946,7 @@ function matcherFor(pattern) {
|
|
|
881
946
|
const normalized = assertGlobPattern(pattern);
|
|
882
947
|
return normalized === void 0 ? void 0 : globToRegExp(normalized);
|
|
883
948
|
}
|
|
884
|
-
function
|
|
885
|
-
mkdirSync(dirname2(path), { recursive: true });
|
|
886
|
-
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
887
|
-
try {
|
|
888
|
-
writeFileSync(tmp, content);
|
|
889
|
-
renameSync2(tmp, path);
|
|
890
|
-
} catch (error) {
|
|
891
|
-
rmSync(tmp, { force: true });
|
|
892
|
-
throw error;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
function assertWritablePath(vpath) {
|
|
896
|
-
const bytes = Buffer.byteLength(vpath, "utf8");
|
|
897
|
-
if (bytes > MAX_NOTE_PATH_BYTES) throw new NoteError("too_large", `note path exceeds ${MAX_NOTE_PATH_BYTES} UTF-8 bytes (got ${bytes})`);
|
|
898
|
-
}
|
|
899
|
-
function assertSerializedSize(content) {
|
|
900
|
-
const bytes = Buffer.byteLength(content, "utf8");
|
|
901
|
-
if (bytes > MAX_NOTE_BYTES) throw new NoteError("too_large", `note exceeds ${MAX_NOTE_BYTES} UTF-8 bytes (serialized ${bytes})`);
|
|
902
|
-
}
|
|
903
|
-
function frontmatterOf(meta) {
|
|
904
|
-
return serializeNote(meta, "").slice(0, -2);
|
|
905
|
-
}
|
|
906
|
-
function assertWritableHome(scope, who, ctx) {
|
|
907
|
-
if (who === void 0) return;
|
|
908
|
-
const current = scope === "agent" ? agentSlug(ctx) : modelSlug(ctx);
|
|
909
|
-
if (who === current) return;
|
|
910
|
-
const home = scope === "agent" ? `@agents/${who}/` : `@models/${who}/`;
|
|
911
|
-
throw new NoteError("invalid_scope", `${home} is not your home: writable homes are this session, @project/, @human/, @self/, and the current @model/ home`);
|
|
912
|
-
}
|
|
913
|
-
function homesForPattern(pattern) {
|
|
949
|
+
async function homesForPattern(pattern, context) {
|
|
914
950
|
if (!pattern || !pattern.startsWith("@")) return void 0;
|
|
915
951
|
const head = /^@([^/]+)\//.exec(pattern)?.[1];
|
|
916
952
|
if (head === "project") return [{ scope: "project" }];
|
|
@@ -920,20 +956,43 @@ function homesForPattern(pattern) {
|
|
|
920
956
|
if (head === "agents" || head === "models") {
|
|
921
957
|
const scope = head === "agents" ? "agent" : "model";
|
|
922
958
|
const name = pattern.slice(head.length + 2).split("/")[0] ?? "";
|
|
923
|
-
if (name.length > 0 && !/[*?]/.test(name)) return [{ scope, who: name }];
|
|
924
|
-
return namespaceSlugs(head).map((who) => ({ scope, who }));
|
|
959
|
+
if (name.length > 0 && !/[*?]/.test(name)) return [{ scope, who: assertWho(name) }];
|
|
960
|
+
return (await namespaceSlugs(head, context.home)).map((who) => ({ scope, who }));
|
|
925
961
|
}
|
|
926
962
|
return [];
|
|
927
963
|
}
|
|
928
|
-
function normalizePattern(pattern,
|
|
964
|
+
function normalizePattern(pattern, context) {
|
|
929
965
|
if (!pattern) return pattern;
|
|
930
|
-
if (pattern.startsWith("@self/")) return `@agents/${
|
|
931
|
-
if (pattern.startsWith("@model/")) return `@models/${
|
|
966
|
+
if (pattern.startsWith("@self/")) return `@agents/${context.agent}/${pattern.slice("@self/".length)}`;
|
|
967
|
+
if (pattern.startsWith("@model/")) return `@models/${context.model}/${pattern.slice("@model/".length)}`;
|
|
932
968
|
return pattern;
|
|
933
969
|
}
|
|
934
|
-
function
|
|
935
|
-
if (
|
|
936
|
-
return
|
|
970
|
+
function assertScope(value) {
|
|
971
|
+
if (!isScope(value)) throw new NoteError("invalid_scope", `scope must be one of session, project, human, agent, model (got ${JSON.stringify(value)})`);
|
|
972
|
+
return value;
|
|
973
|
+
}
|
|
974
|
+
function assertOrigin(value) {
|
|
975
|
+
if (!isOrigin(value)) throw new NoteError("invalid_origin", `origin must be one of user, self, external (got ${JSON.stringify(value)})`);
|
|
976
|
+
return value;
|
|
977
|
+
}
|
|
978
|
+
function assertWho(value) {
|
|
979
|
+
if (typeof value !== "string" || !SLUG_PATTERN.test(value)) {
|
|
980
|
+
throw new NoteError("invalid_scope", "who must be a canonical lowercase slug");
|
|
981
|
+
}
|
|
982
|
+
return value;
|
|
983
|
+
}
|
|
984
|
+
async function homesFor(context, opts) {
|
|
985
|
+
if (opts.scope !== void 0) {
|
|
986
|
+
const scope = assertScope(opts.scope);
|
|
987
|
+
if (opts.who !== void 0) {
|
|
988
|
+
const who = assertWho(opts.who);
|
|
989
|
+
if (scope !== "agent" && scope !== "model") throw new NoteError("invalid_scope", "who is only valid with agent or model scope");
|
|
990
|
+
return [{ scope, who }];
|
|
991
|
+
}
|
|
992
|
+
return [{ scope }];
|
|
993
|
+
}
|
|
994
|
+
if (opts.who !== void 0) throw new NoteError("invalid_scope", "who requires agent or model scope");
|
|
995
|
+
return await homesForPattern(normalizePattern(opts.pattern, context), context) ?? SCOPE_ORDER.map((scope) => ({ scope }));
|
|
937
996
|
}
|
|
938
997
|
function matchLineNumbers(body, needle) {
|
|
939
998
|
const lines = [];
|
|
@@ -946,141 +1005,233 @@ function matchLineNumbers(body, needle) {
|
|
|
946
1005
|
}
|
|
947
1006
|
return lines;
|
|
948
1007
|
}
|
|
949
|
-
function
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
const meta = existing ?? {
|
|
960
|
-
scope,
|
|
961
|
-
origin,
|
|
962
|
-
status: "active",
|
|
963
|
-
stale: false,
|
|
964
|
-
created_at: now,
|
|
965
|
-
updated_at: now,
|
|
966
|
-
last_accessed: now,
|
|
967
|
-
access_count: 0
|
|
968
|
-
};
|
|
969
|
-
meta.scope = scope;
|
|
970
|
-
meta.origin = origin;
|
|
971
|
-
meta.status = "active";
|
|
972
|
-
meta.stale = opts.stale ?? false;
|
|
973
|
-
meta.updated_at = now;
|
|
974
|
-
const serialized = serializeNote(meta, cleanBody);
|
|
975
|
-
assertSerializedSize(serialized);
|
|
976
|
-
atomicWrite(path, serialized);
|
|
977
|
-
return { meta };
|
|
978
|
-
}
|
|
979
|
-
function editNote(ctx, vpath, scope, edits, opts = {}, who) {
|
|
980
|
-
assertVirtualPath(vpath);
|
|
981
|
-
assertWritablePath(vpath);
|
|
982
|
-
assertWritableHome(scope, who, ctx);
|
|
983
|
-
const operations = edits ?? [];
|
|
984
|
-
if (operations.length === 0 && opts.origin === void 0 && opts.stale === void 0) {
|
|
985
|
-
throw new NoteError("nothing_to_do", "nothing to do: provide edits or at least one of origin, stale");
|
|
986
|
-
}
|
|
987
|
-
const path = physicalPath(scope, vpath, ctx, who);
|
|
988
|
-
if (!existsSync2(path)) throw new NoteError("not_found", "note not found");
|
|
989
|
-
const raw = readFileSync(path, "utf8");
|
|
990
|
-
const { meta, body } = parseNote(raw);
|
|
991
|
-
meta.scope = scope;
|
|
992
|
-
const beforeMeta = { ...meta };
|
|
993
|
-
let next = body;
|
|
994
|
-
operations.forEach((edit, index) => {
|
|
995
|
-
const oldText = edit?.oldText;
|
|
996
|
-
const newText = edit?.newText;
|
|
997
|
-
if (typeof oldText !== "string" || oldText.length === 0) throw new NoteError("no_match", `edit ${index}: oldText must be a non-empty string`, { edit_index: index });
|
|
998
|
-
if (typeof newText !== "string") throw new NoteError("no_match", `edit ${index}: newText must be a string`, { edit_index: index });
|
|
999
|
-
const lines = matchLineNumbers(next, oldText);
|
|
1000
|
-
if (lines.length === 0) throw new NoteError("no_match", `edit ${index}: oldText does not occur in the note body`, { edit_index: index });
|
|
1001
|
-
if (lines.length > 1 && !opts.replaceAll) {
|
|
1002
|
-
throw new NoteError("ambiguous_edit", `edit ${index}: oldText occurs ${lines.length} times (lines ${lines.join(", ")}); pass replace_all to replace every occurrence`, { line_numbers: lines, edit_index: index });
|
|
1003
|
-
}
|
|
1004
|
-
if (opts.replaceAll) {
|
|
1005
|
-
next = next.split(oldText).join(newText);
|
|
1006
|
-
} else {
|
|
1007
|
-
const matchIndex = next.indexOf(oldText);
|
|
1008
|
-
next = next.substring(0, matchIndex) + newText + next.substring(matchIndex + oldText.length);
|
|
1008
|
+
async function atomicWrite(path, content) {
|
|
1009
|
+
await mkdir(dirname2(path), { recursive: true });
|
|
1010
|
+
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
1011
|
+
try {
|
|
1012
|
+
await writeFile(tmp, content);
|
|
1013
|
+
await rename(tmp, path);
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
try {
|
|
1016
|
+
await rm(tmp, { force: true });
|
|
1017
|
+
} catch {
|
|
1009
1018
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1019
|
+
throw error;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function assertWritablePath(vpath) {
|
|
1023
|
+
const bytes = Buffer.byteLength(vpath, "utf8");
|
|
1024
|
+
if (bytes > MAX_NOTE_PATH_BYTES) throw new NoteError("too_large", `note path exceeds ${MAX_NOTE_PATH_BYTES} UTF-8 bytes (got ${bytes})`);
|
|
1025
|
+
}
|
|
1026
|
+
function assertSerializedSize(content) {
|
|
1027
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
1028
|
+
if (bytes > MAX_NOTE_BYTES) throw new NoteError("too_large", `note exceeds ${MAX_NOTE_BYTES} UTF-8 bytes (serialized ${bytes})`);
|
|
1029
|
+
}
|
|
1030
|
+
function frontmatterOf(meta) {
|
|
1031
|
+
return serializeNote(meta, "").slice(0, -2);
|
|
1032
|
+
}
|
|
1033
|
+
function assertWritableHome(scope, who, context) {
|
|
1034
|
+
if (who === void 0) return;
|
|
1035
|
+
const current = scope === "agent" ? context.agent : context.model;
|
|
1036
|
+
if (who === current) return;
|
|
1037
|
+
const home = scope === "agent" ? `@agents/${who}/` : `@models/${who}/`;
|
|
1038
|
+
throw new NoteError("invalid_scope", `${home} is not your home: writable homes are this session, @project/, @human/, @self/, and the current @model/ home`);
|
|
1021
1039
|
}
|
|
1022
1040
|
function accessedMeta(meta, scope, now) {
|
|
1023
1041
|
const next = { ...meta, scope };
|
|
1024
|
-
next.
|
|
1025
|
-
next.
|
|
1042
|
+
next.lastAccessed = now;
|
|
1043
|
+
next.accessCount = (typeof next.accessCount === "number" ? next.accessCount : 0) + 1;
|
|
1026
1044
|
return next;
|
|
1027
1045
|
}
|
|
1028
|
-
function
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1046
|
+
function createNotesStore(input) {
|
|
1047
|
+
const context = snapshotNotesContext(input);
|
|
1048
|
+
async function write(address, content, options = {}) {
|
|
1049
|
+
const stableAddress = address;
|
|
1050
|
+
const stableContent = content;
|
|
1051
|
+
const stableOptions = { ...options };
|
|
1052
|
+
const destination = assertAddress(stableAddress);
|
|
1053
|
+
assertWritablePath(destination.path);
|
|
1054
|
+
const scope = assertScope(destination.scope);
|
|
1055
|
+
assertWritableHome(scope, destination.who, context);
|
|
1056
|
+
const origin = assertOrigin(stableOptions.origin ?? "self");
|
|
1057
|
+
const path = physicalPath(scope, destination.path, context, destination.who);
|
|
1058
|
+
return withPathQueue(path, async () => {
|
|
1059
|
+
const now = Date.now();
|
|
1060
|
+
const cleanBody = stripLeadingFrontmatter(stableContent);
|
|
1061
|
+
const existingRaw = await readFileIfExists(path);
|
|
1062
|
+
const existing = existingRaw === void 0 ? void 0 : parseNote(existingRaw, now).meta;
|
|
1063
|
+
const meta = existing ?? {
|
|
1064
|
+
scope,
|
|
1065
|
+
origin,
|
|
1066
|
+
status: "active",
|
|
1067
|
+
stale: false,
|
|
1068
|
+
createdAt: now,
|
|
1069
|
+
updatedAt: now,
|
|
1070
|
+
lastAccessed: now,
|
|
1071
|
+
accessCount: 0,
|
|
1072
|
+
...scope === "session" ? { project: context.projectKey } : {}
|
|
1073
|
+
};
|
|
1049
1074
|
meta.scope = scope;
|
|
1050
|
-
|
|
1051
|
-
|
|
1075
|
+
meta.origin = origin;
|
|
1076
|
+
meta.status = "active";
|
|
1077
|
+
meta.stale = stableOptions.stale ?? false;
|
|
1078
|
+
meta.updatedAt = now;
|
|
1079
|
+
const serialized = serializeNote(meta, cleanBody);
|
|
1080
|
+
assertSerializedSize(serialized);
|
|
1081
|
+
await atomicWrite(path, serialized);
|
|
1082
|
+
return { meta };
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
async function read(address) {
|
|
1086
|
+
const stableAddress = address;
|
|
1087
|
+
const destination = assertAddress(stableAddress);
|
|
1088
|
+
const scope = assertScope(destination.scope);
|
|
1089
|
+
const path = physicalPath(scope, destination.path, context, destination.who);
|
|
1090
|
+
return withPathQueue(path, async () => {
|
|
1091
|
+
const raw = await readFileIfExists(path);
|
|
1092
|
+
if (raw === void 0) return void 0;
|
|
1093
|
+
const now = Date.now();
|
|
1094
|
+
const parsed = parseNote(raw, now);
|
|
1095
|
+
const meta = accessedMeta(parsed.meta, scope, now);
|
|
1096
|
+
const text = serializeNote(meta, parsed.body);
|
|
1097
|
+
await atomicWrite(path, text);
|
|
1098
|
+
return { meta, body: parsed.body, text, resolvedScope: scope };
|
|
1099
|
+
});
|
|
1052
1100
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1101
|
+
async function edit(address, edits, options = {}) {
|
|
1102
|
+
const stableAddress = address;
|
|
1103
|
+
const operations = edits === void 0 ? [] : edits.map((operation) => ({ ...operation }));
|
|
1104
|
+
const stableOptions = { ...options };
|
|
1105
|
+
const destination = assertAddress(stableAddress);
|
|
1106
|
+
assertWritablePath(destination.path);
|
|
1107
|
+
const scope = assertScope(destination.scope);
|
|
1108
|
+
assertWritableHome(scope, destination.who, context);
|
|
1109
|
+
if (operations.length === 0 && stableOptions.origin === void 0 && stableOptions.stale === void 0) {
|
|
1110
|
+
throw new NoteError("nothing_to_do", "nothing to do: provide edits or at least one of origin, stale");
|
|
1111
|
+
}
|
|
1112
|
+
const path = physicalPath(scope, destination.path, context, destination.who);
|
|
1113
|
+
return withPathQueue(path, async () => {
|
|
1114
|
+
const raw = await readFileIfExists(path);
|
|
1115
|
+
if (raw === void 0) throw new NoteError("not_found", "note not found");
|
|
1116
|
+
const { meta, body } = parseNote(raw);
|
|
1066
1117
|
meta.scope = scope;
|
|
1118
|
+
const beforeMeta = { ...meta };
|
|
1119
|
+
let next = body;
|
|
1120
|
+
operations.forEach((operation, index) => {
|
|
1121
|
+
const oldText = operation?.oldText;
|
|
1122
|
+
const newText = operation?.newText;
|
|
1123
|
+
if (typeof oldText !== "string" || oldText.length === 0) throw new NoteError("no_match", `edit ${index}: oldText must be a non-empty string`, { editIndex: index });
|
|
1124
|
+
if (typeof newText !== "string") throw new NoteError("no_match", `edit ${index}: newText must be a string`, { editIndex: index });
|
|
1125
|
+
const lines = matchLineNumbers(next, oldText);
|
|
1126
|
+
if (lines.length === 0) throw new NoteError("no_match", `edit ${index}: oldText does not occur in the note body`, { editIndex: index });
|
|
1127
|
+
if (lines.length > 1 && !stableOptions.replaceAll) {
|
|
1128
|
+
throw new NoteError("ambiguous_edit", `edit ${index}: oldText occurs ${lines.length} times (lines ${lines.join(", ")}); pass replace_all to replace every occurrence`, { lineNumbers: lines, editIndex: index });
|
|
1129
|
+
}
|
|
1130
|
+
if (stableOptions.replaceAll) next = next.split(oldText).join(newText);
|
|
1131
|
+
else {
|
|
1132
|
+
const matchIndex = next.indexOf(oldText);
|
|
1133
|
+
next = next.substring(0, matchIndex) + newText + next.substring(matchIndex + oldText.length);
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
if (stableOptions.origin !== void 0) meta.origin = assertOrigin(stableOptions.origin);
|
|
1137
|
+
if (stableOptions.stale !== void 0) meta.stale = stableOptions.stale;
|
|
1138
|
+
meta.updatedAt = Date.now();
|
|
1139
|
+
const serialized = serializeNote(meta, next);
|
|
1140
|
+
assertSerializedSize(serialized);
|
|
1141
|
+
const bodyChanged = body !== next;
|
|
1142
|
+
const metadataChanged = beforeMeta.origin !== meta.origin || beforeMeta.stale !== meta.stale;
|
|
1143
|
+
let change;
|
|
1144
|
+
if (bodyChanged && metadataChanged) change = { kind: "file", before: raw, after: serialized };
|
|
1145
|
+
else if (bodyChanged) change = { kind: "body", before: body, after: next };
|
|
1146
|
+
else if (metadataChanged) change = { kind: "metadata", before: frontmatterOf(beforeMeta), after: frontmatterOf(meta) };
|
|
1147
|
+
else change = { kind: "none", before: "", after: "" };
|
|
1148
|
+
await atomicWrite(path, serialized);
|
|
1149
|
+
return { meta, applied: operations.length, resolvedScope: scope, change };
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
async function* scan(options) {
|
|
1153
|
+
const matcher = matcherFor(normalizePattern(options.pattern, context));
|
|
1154
|
+
const homes = await homesFor(context, options);
|
|
1155
|
+
for (const home of homes) {
|
|
1156
|
+
const scope = home.scope;
|
|
1157
|
+
const root = scopeDir(scope, context, home.who);
|
|
1158
|
+
for (const path of await walkMarkdown(root)) {
|
|
1159
|
+
const address = addressFor(context, scope, path, home.who);
|
|
1160
|
+
if (matcher && !matcher.test(address)) continue;
|
|
1161
|
+
const fullPath = join2(root, path);
|
|
1162
|
+
const raw = await withPathQueue(fullPath, () => readFile(fullPath, "utf8"));
|
|
1163
|
+
const { meta, body } = parseNote(raw);
|
|
1164
|
+
meta.scope = scope;
|
|
1165
|
+
yield { address, scope, path, meta, body };
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
async function list(options = {}) {
|
|
1170
|
+
const stableOptions = { ...options };
|
|
1171
|
+
const rows = [];
|
|
1172
|
+
for await (const row of scan(stableOptions)) {
|
|
1173
|
+
rows.push({ ...row, sizeBytes: Buffer.byteLength(row.body, "utf8") });
|
|
1174
|
+
}
|
|
1175
|
+
rows.sort((a, b) => b.meta.updatedAt - a.meta.updatedAt || a.address.localeCompare(b.address));
|
|
1176
|
+
return rows;
|
|
1177
|
+
}
|
|
1178
|
+
async function search(queries, options = {}) {
|
|
1179
|
+
const stableQueries = [...queries];
|
|
1180
|
+
const stableOptions = { ...options };
|
|
1181
|
+
const rows = [];
|
|
1182
|
+
for await (const note of scan(stableOptions)) {
|
|
1183
|
+
const { address, path, scope, meta, body } = note;
|
|
1067
1184
|
const serializedBodyOffset = Array.from(serializeNote(accessedMeta(meta, scope, Date.now()), "")).length;
|
|
1068
1185
|
let baseChars = 0;
|
|
1069
1186
|
const matches = [];
|
|
1070
1187
|
for (const [index, line] of body.split("\n").entries()) {
|
|
1071
|
-
if (
|
|
1072
|
-
matches.push({ line: index + 1, text: line, offsetChars: serializedBodyOffset + baseChars + earliestMatchOffsetChars(line,
|
|
1188
|
+
if (stableQueries.some((query) => line.includes(query))) {
|
|
1189
|
+
matches.push({ line: index + 1, text: line, offsetChars: serializedBodyOffset + baseChars + earliestMatchOffsetChars(line, stableQueries) });
|
|
1073
1190
|
}
|
|
1074
1191
|
baseChars += Array.from(line).length + 1;
|
|
1075
1192
|
}
|
|
1076
1193
|
if (matches.length > 0) rows.push({ address, path, scope, meta, matches });
|
|
1077
1194
|
}
|
|
1195
|
+
rows.sort((a, b) => a.address.localeCompare(b.address));
|
|
1196
|
+
return rows;
|
|
1078
1197
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1198
|
+
return { write, read, edit, list, search };
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// src/pi/notes/adapter.ts
|
|
1202
|
+
import { existsSync as existsSync2, renameSync } from "node:fs";
|
|
1203
|
+
import { homedir } from "node:os";
|
|
1204
|
+
import { join as join3, resolve as resolve4 } from "node:path";
|
|
1205
|
+
function notesRoot() {
|
|
1206
|
+
const override = process.env.PI_NOTES_HOME;
|
|
1207
|
+
return override && override.length > 0 ? resolve4(override) : join3(homedir(), ".agents", "notes");
|
|
1208
|
+
}
|
|
1209
|
+
function agentSlug(_ctx) {
|
|
1210
|
+
return slugify(process.env.PI_NOTES_AGENT ?? "root");
|
|
1211
|
+
}
|
|
1212
|
+
function modelSlug(ctx) {
|
|
1213
|
+
const id = ctx.model?.id;
|
|
1214
|
+
return id ? slugify(id.split("/").pop() ?? id) : "default";
|
|
1215
|
+
}
|
|
1216
|
+
function notesContextFromPi(ctx, home = notesRoot()) {
|
|
1217
|
+
return {
|
|
1218
|
+
home,
|
|
1219
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
1220
|
+
projectKey: projectKey(ctx.cwd),
|
|
1221
|
+
agent: agentSlug(ctx),
|
|
1222
|
+
model: modelSlug(ctx)
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
function migrateLegacyHomes(home = notesRoot()) {
|
|
1226
|
+
const legacy = join3(home, "personal");
|
|
1227
|
+
const modern = join3(home, "human");
|
|
1228
|
+
if (!existsSync2(legacy)) return void 0;
|
|
1229
|
+
if (existsSync2(modern)) return "both personal/ and human/ exist under the notes home; migrate by hand, no automatic merge";
|
|
1230
|
+
renameSync(legacy, modern);
|
|
1231
|
+
return void 0;
|
|
1081
1232
|
}
|
|
1082
1233
|
|
|
1083
|
-
// src/notes/tools.ts
|
|
1234
|
+
// src/pi/notes/tools.ts
|
|
1084
1235
|
var ORIGIN = Type3.Optional(Type3.Union([Type3.Literal("user"), Type3.Literal("self"), Type3.Literal("external")], {
|
|
1085
1236
|
description: "Where the note's content came from. user: written or dictated by the human. self: written by you, the agent (default). external: anything else \u2014 third-party text, tool output, fetched material."
|
|
1086
1237
|
}));
|
|
@@ -1088,24 +1239,27 @@ var ADDRESS_DESCRIPTION = "Address forms are bare `<vpath>` for this session, `@
|
|
|
1088
1239
|
function failure(error) {
|
|
1089
1240
|
if (error instanceof NoteError) {
|
|
1090
1241
|
const payload = { error: error.message };
|
|
1091
|
-
if (error.
|
|
1092
|
-
if (error.
|
|
1242
|
+
if (error.lineNumbers) payload.line_numbers = error.lineNumbers;
|
|
1243
|
+
if (error.editIndex !== void 0) payload.edit_index = error.editIndex;
|
|
1093
1244
|
return output(payload);
|
|
1094
1245
|
}
|
|
1095
1246
|
throw error;
|
|
1096
1247
|
}
|
|
1248
|
+
function renderDiff(change) {
|
|
1249
|
+
if (change.kind === "none") return "";
|
|
1250
|
+
return generateDiffString(change.before, change.after).diff;
|
|
1251
|
+
}
|
|
1097
1252
|
function registerNotesTools(pi) {
|
|
1098
1253
|
pi.registerTool(defineTool2({
|
|
1099
1254
|
name: "notes_write",
|
|
1100
1255
|
label: "Notes write",
|
|
1101
|
-
description: `Create or replace a note as a real markdown file, and name it for what it holds: a fresh window sees only an index entry, never the note itself. ${ADDRESS_DESCRIPTION} Keep notes small and split by topic \u2014 by what the note is about, never by who said it (authorship is origin's job); a rewrite replaces the body whole while preserving
|
|
1256
|
+
description: `Create or replace a note as a real markdown file, and name it for what it holds: a fresh window sees only an index entry, never the note itself. ${ADDRESS_DESCRIPTION} Keep notes small and split by topic \u2014 by what the note is about, never by who said it (authorship is origin's job); a rewrite replaces the body whole while preserving createdAt and every other frontmatter key. stale: true marks the note closed so it leaves the boot index but stays readable and searchable.`,
|
|
1102
1257
|
parameters: Type3.Object({ address: Type3.String(), content: Type3.String(), origin: ORIGIN, stale: Type3.Optional(Type3.Boolean()) }, { additionalProperties: false }),
|
|
1103
1258
|
executionMode: "sequential",
|
|
1104
1259
|
async execute(_id, params, _signal, _update, ctx) {
|
|
1105
1260
|
const content = params.content;
|
|
1106
1261
|
try {
|
|
1107
|
-
|
|
1108
|
-
writeNote(ctx, destination.path, content, { scope: destination.scope, who: destination.who, origin: params.origin ?? "self", stale: params.stale });
|
|
1262
|
+
await createNotesStore(notesContextFromPi(ctx)).write(params.address, content, { origin: params.origin ?? "self", stale: params.stale });
|
|
1109
1263
|
return output({ address: params.address, written: true });
|
|
1110
1264
|
} catch (error) {
|
|
1111
1265
|
return failure(error);
|
|
@@ -1120,8 +1274,8 @@ function registerNotesTools(pi) {
|
|
|
1120
1274
|
executionMode: "sequential",
|
|
1121
1275
|
async execute(_id, params, _signal, _update, ctx) {
|
|
1122
1276
|
try {
|
|
1123
|
-
const
|
|
1124
|
-
const
|
|
1277
|
+
const { applied, change } = await createNotesStore(notesContextFromPi(ctx)).edit(params.address, params.edits, { origin: params.origin, stale: params.stale, replaceAll: params.replace_all });
|
|
1278
|
+
const diff = renderDiff(change);
|
|
1125
1279
|
return output({ address: params.address, applied, diff });
|
|
1126
1280
|
} catch (error) {
|
|
1127
1281
|
return failure(error);
|
|
@@ -1136,8 +1290,7 @@ function registerNotesTools(pi) {
|
|
|
1136
1290
|
async execute(_id, params, _signal, _update, ctx) {
|
|
1137
1291
|
let note;
|
|
1138
1292
|
try {
|
|
1139
|
-
|
|
1140
|
-
note = readNote(ctx, destination.path, destination.scope, destination.who);
|
|
1293
|
+
note = await createNotesStore(notesContextFromPi(ctx)).read(params.address);
|
|
1141
1294
|
} catch (error) {
|
|
1142
1295
|
return failure(error);
|
|
1143
1296
|
}
|
|
@@ -1159,11 +1312,11 @@ function registerNotesTools(pi) {
|
|
|
1159
1312
|
async execute(_id, params, _signal, _update, ctx) {
|
|
1160
1313
|
let rows;
|
|
1161
1314
|
try {
|
|
1162
|
-
rows =
|
|
1315
|
+
rows = await createNotesStore(notesContextFromPi(ctx)).list({ pattern: params.pattern ?? void 0 });
|
|
1163
1316
|
} catch (error) {
|
|
1164
1317
|
return failure(error);
|
|
1165
1318
|
}
|
|
1166
|
-
const files = rows.map((row) => ({ address: row.address, stale: row.meta.stale, updated_at: localIso(row.meta.
|
|
1319
|
+
const files = rows.map((row) => ({ address: row.address, stale: row.meta.stale, updated_at: localIso(row.meta.updatedAt) }));
|
|
1167
1320
|
return output(page(files, params.cursor ?? 0, "files", params.max_results, (file, fits) => {
|
|
1168
1321
|
if (fits(file)) return file;
|
|
1169
1322
|
const address = middleTruncate(file.address, (candidate) => fits({ ...file, address: candidate, address_truncated: true }));
|
|
@@ -1180,14 +1333,14 @@ function registerNotesTools(pi) {
|
|
|
1180
1333
|
const queries = searchQueries(params.query);
|
|
1181
1334
|
let rows;
|
|
1182
1335
|
try {
|
|
1183
|
-
rows =
|
|
1336
|
+
rows = await createNotesStore(notesContextFromPi(ctx)).search(queries, { pattern: params.pattern ?? void 0 });
|
|
1184
1337
|
} catch (error) {
|
|
1185
1338
|
return failure(error);
|
|
1186
1339
|
}
|
|
1187
1340
|
const maxPerFile = params.max_matches_per_file ?? Number.POSITIVE_INFINITY;
|
|
1188
1341
|
const result = rows.map((row) => {
|
|
1189
1342
|
const matches = row.matches.map((match) => ({ line: match.line, text: match.text, truncated: false, offset_chars: match.offsetChars }));
|
|
1190
|
-
return { address: row.address, updated_at: localIso(row.meta.
|
|
1343
|
+
return { address: row.address, updated_at: localIso(row.meta.updatedAt), stale: row.meta.stale, matches_total: matches.length, matches: matches.slice(0, maxPerFile) };
|
|
1191
1344
|
});
|
|
1192
1345
|
const fitFile = (file, fits) => {
|
|
1193
1346
|
if (fits(file)) return file;
|
|
@@ -1243,7 +1396,7 @@ function deriveThresholds(reserveTokens, margins) {
|
|
|
1243
1396
|
else {
|
|
1244
1397
|
const parsed = validMargin(margins.reminderMarginTokens);
|
|
1245
1398
|
if (parsed === void 0) {
|
|
1246
|
-
warnings.push(`pi-context: ${reminderKey} must be a positive integer; using default
|
|
1399
|
+
warnings.push(`pi-context: ${reminderKey} must be a positive integer; using the default reminder margin.`);
|
|
1247
1400
|
reminderMargin = DEFAULT_REMINDER_MARGIN_TOKENS;
|
|
1248
1401
|
} else reminderMargin = parsed;
|
|
1249
1402
|
}
|
|
@@ -1330,7 +1483,7 @@ function notesIndex(snapshot) {
|
|
|
1330
1483
|
if (recentNotes.length > 0) {
|
|
1331
1484
|
const lines = [`You find ${recentNotes.length} crumpled note${recentNotes.length === 1 ? "" : "s"} in your pocket (by prefix, most recent first within each: up to ${POCKET_SESSION_LIMIT} from this session, ${POCKET_PROJECT_LIMIT} from @project, ${POCKET_HUMAN_LIMIT} from @human, ${POCKET_AGENT_LIMIT} from @self, ${POCKET_MODEL_LIMIT} from @model). A note's content never appears here, so its name has to say what the note is about:`];
|
|
1332
1485
|
for (const row of recentNotes) {
|
|
1333
|
-
lines.push(`- ${row.address} (${row.body.split("\n").length} lines, ${row.sizeBytes} UTF-8 bytes, updated ${relativeTime(row.meta.
|
|
1486
|
+
lines.push(`- ${row.address} (${row.body.split("\n").length} lines, ${row.sizeBytes} UTF-8 bytes, updated ${relativeTime(row.meta.updatedAt, snapshot.openedAt)})`);
|
|
1334
1487
|
}
|
|
1335
1488
|
sections.push(lines.join("\n"));
|
|
1336
1489
|
}
|
|
@@ -1361,7 +1514,8 @@ function remainingTokens(ctx) {
|
|
|
1361
1514
|
const usage = windowUsage(ctx);
|
|
1362
1515
|
return !usage || usage.tokens === null ? null : Math.max(0, usage.contextWindow - usage.tokens);
|
|
1363
1516
|
}
|
|
1364
|
-
function registerBudget(pi, isEnabled, settingsManager) {
|
|
1517
|
+
function registerBudget(pi, isEnabled, settingsManager, onCloseOut = () => {
|
|
1518
|
+
}) {
|
|
1365
1519
|
let cachedPolicy;
|
|
1366
1520
|
const notifiedWarnings = /* @__PURE__ */ new Set();
|
|
1367
1521
|
const resolvePolicy = (ctx) => {
|
|
@@ -1381,7 +1535,7 @@ function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
1381
1535
|
const automaticResetEnabled = (ctx) => {
|
|
1382
1536
|
return resolvePolicy(ctx).automatic;
|
|
1383
1537
|
};
|
|
1384
|
-
const
|
|
1538
|
+
const hardReserveDue = (ctx) => {
|
|
1385
1539
|
if (!automaticResetEnabled(ctx)) return false;
|
|
1386
1540
|
const usage = windowUsage(ctx);
|
|
1387
1541
|
return usage !== void 0 && usage.tokens !== null && usage.contextWindow - usage.tokens <= thresholdsFor(ctx).reserve;
|
|
@@ -1389,17 +1543,31 @@ function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
1389
1543
|
const invalidateThresholds = () => {
|
|
1390
1544
|
cachedPolicy = void 0;
|
|
1391
1545
|
};
|
|
1392
|
-
const formatRemaining = (remaining) => `${Math.max(0, Math.ceil(remaining / 1e3))}k`;
|
|
1393
1546
|
let pendingGuidance;
|
|
1394
1547
|
let pendingWarning;
|
|
1395
|
-
let pendingNotices =
|
|
1396
|
-
const
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1548
|
+
let pendingNotices = /* @__PURE__ */ new Map();
|
|
1549
|
+
const noticeKey = (sessionId, windowId) => `${sessionId}:${windowId}`;
|
|
1550
|
+
const warningCommittedInWindow = (ctx, notice) => {
|
|
1551
|
+
if (ctx.sessionManager.getSessionId() !== notice.sessionId) return false;
|
|
1552
|
+
let windowId = rootWindowId(notice.sessionId);
|
|
1553
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
1554
|
+
if (isWindowMarker(entry)) {
|
|
1555
|
+
windowId = entry.data.windowId;
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
if (windowId === notice.windowId && entry.type === "custom_message" && entry.customType === WARNING_TYPE) return true;
|
|
1559
|
+
}
|
|
1560
|
+
return false;
|
|
1561
|
+
};
|
|
1562
|
+
const notifyCommittedWarnings = (ctx, settled = false) => {
|
|
1563
|
+
for (const [key, notice] of pendingNotices) {
|
|
1564
|
+
if (warningCommittedInWindow(ctx, notice)) {
|
|
1565
|
+
pendingNotices.delete(key);
|
|
1566
|
+
ctx.ui.notify("pi-context: Context almost full; close out the current memory window.", "warning");
|
|
1567
|
+
} else if (settled) {
|
|
1568
|
+
pendingNotices.delete(key);
|
|
1569
|
+
}
|
|
1401
1570
|
}
|
|
1402
|
-
pendingNotices = [];
|
|
1403
1571
|
};
|
|
1404
1572
|
const clearStaged = () => {
|
|
1405
1573
|
pendingGuidance = void 0;
|
|
@@ -1407,7 +1575,7 @@ function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
1407
1575
|
};
|
|
1408
1576
|
const resetForTransition = () => {
|
|
1409
1577
|
clearStaged();
|
|
1410
|
-
pendingNotices
|
|
1578
|
+
pendingNotices.clear();
|
|
1411
1579
|
invalidateThresholds();
|
|
1412
1580
|
notifiedWarnings.clear();
|
|
1413
1581
|
};
|
|
@@ -1419,7 +1587,10 @@ function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
1419
1587
|
clearStaged();
|
|
1420
1588
|
const windowId = currentWindowId(ctx);
|
|
1421
1589
|
const drafts = staged.filter((draft) => draft !== void 0 && draft.windowId === windowId);
|
|
1422
|
-
|
|
1590
|
+
if (drafts.some((draft) => draft.customType === WARNING_TYPE)) {
|
|
1591
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
1592
|
+
pendingNotices.set(noticeKey(sessionId, windowId), { sessionId, windowId });
|
|
1593
|
+
}
|
|
1423
1594
|
return drafts.map((draft) => ({
|
|
1424
1595
|
type: "custom_message",
|
|
1425
1596
|
customType: draft.customType,
|
|
@@ -1434,8 +1605,9 @@ function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
1434
1605
|
pi.on("session_tree", resetForTransition);
|
|
1435
1606
|
pi.on("model_select", resetForTransition);
|
|
1436
1607
|
pi.on("session_shutdown", resetForTransition);
|
|
1608
|
+
pi.on("turn_start", (_event, ctx) => notifyCommittedWarnings(ctx));
|
|
1437
1609
|
pi.on("agent_settled", (_event, ctx) => {
|
|
1438
|
-
|
|
1610
|
+
notifyCommittedWarnings(ctx, true);
|
|
1439
1611
|
clearStaged();
|
|
1440
1612
|
});
|
|
1441
1613
|
pi.on("context", (_event, ctx) => {
|
|
@@ -1444,12 +1616,11 @@ function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
1444
1616
|
if (remaining === null) return void 0;
|
|
1445
1617
|
const windowId = currentWindowId(ctx);
|
|
1446
1618
|
const { reminder, warning } = thresholdsFor(ctx);
|
|
1447
|
-
if (
|
|
1448
|
-
|
|
1619
|
+
if (remaining <= warning && automaticResetEnabled(ctx)) {
|
|
1620
|
+
onCloseOut(windowId);
|
|
1621
|
+
if (hasWindowMessage(ctx, WARNING_TYPE) || pendingWarning?.windowId === windowId) return void 0;
|
|
1449
1622
|
pendingGuidance = void 0;
|
|
1450
|
-
const content =
|
|
1451
|
-
${WARNING_PROMPT}
|
|
1452
|
-
${GUIDANCE_CLOSE_TAG}`;
|
|
1623
|
+
const content = WARNING_CONTENT;
|
|
1453
1624
|
pendingWarning = { windowId, content, remaining };
|
|
1454
1625
|
const warningMessage = {
|
|
1455
1626
|
role: "custom",
|
|
@@ -1460,6 +1631,7 @@ ${GUIDANCE_CLOSE_TAG}`;
|
|
|
1460
1631
|
};
|
|
1461
1632
|
return { messages: [..._event.messages, warningMessage] };
|
|
1462
1633
|
}
|
|
1634
|
+
if (hasWindowMessage(ctx, WARNING_TYPE) || pendingWarning?.windowId === windowId) return void 0;
|
|
1463
1635
|
if (hasWindowMessage(ctx, GUIDANCE_TYPE) || pendingGuidance?.windowId === windowId) return void 0;
|
|
1464
1636
|
if (remaining <= reminder) {
|
|
1465
1637
|
const left = Math.max(0, remaining - warning);
|
|
@@ -1479,11 +1651,11 @@ ${GUIDANCE_CLOSE_TAG}`;
|
|
|
1479
1651
|
}));
|
|
1480
1652
|
return {
|
|
1481
1653
|
automaticResetEnabled,
|
|
1482
|
-
|
|
1654
|
+
hardReserveDue,
|
|
1483
1655
|
consumeTurnEnd,
|
|
1484
1656
|
clear: () => {
|
|
1485
1657
|
clearStaged();
|
|
1486
|
-
pendingNotices
|
|
1658
|
+
pendingNotices.clear();
|
|
1487
1659
|
}
|
|
1488
1660
|
};
|
|
1489
1661
|
}
|
|
@@ -1497,45 +1669,75 @@ function isOverflowLike(message, ctx) {
|
|
|
1497
1669
|
if (message.role !== "assistant") return false;
|
|
1498
1670
|
return isContextOverflow(message, ctx.model?.contextWindow) || ctx.model !== void 0 && isRecoverableLength(message, ctx.model.maxTokens);
|
|
1499
1671
|
}
|
|
1672
|
+
var NO_REQUEST = { phase: "none" };
|
|
1500
1673
|
function initialResetControl() {
|
|
1501
|
-
return { request:
|
|
1674
|
+
return { request: NO_REQUEST, overflow: "idle" };
|
|
1675
|
+
}
|
|
1676
|
+
function requestForWindow(request, windowId) {
|
|
1677
|
+
return request.phase !== "none" && request.windowId === windowId ? request : NO_REQUEST;
|
|
1502
1678
|
}
|
|
1503
1679
|
function reduceResetControl(state, event) {
|
|
1504
1680
|
switch (event.type) {
|
|
1505
|
-
case "
|
|
1506
|
-
|
|
1507
|
-
|
|
1681
|
+
case "close_out": {
|
|
1682
|
+
const request = state.request;
|
|
1683
|
+
if (request.phase === "tool-requested" && request.windowId === event.windowId) {
|
|
1684
|
+
return { state, effect: "already-pending" };
|
|
1685
|
+
}
|
|
1686
|
+
if (request.phase === "close-out" && request.windowId === event.windowId) {
|
|
1687
|
+
if (request.source === "manual" || event.source === "automatic") return { state, effect: "already-pending" };
|
|
1688
|
+
return { state: { ...state, request: { ...request, source: "manual" } }, effect: "close-out-armed" };
|
|
1689
|
+
}
|
|
1690
|
+
return { state: { ...state, request: { phase: "close-out", windowId: event.windowId, source: event.source } }, effect: "close-out-armed" };
|
|
1691
|
+
}
|
|
1692
|
+
case "tool_request": {
|
|
1693
|
+
const request = state.request;
|
|
1694
|
+
if (request.phase === "tool-requested" && request.windowId === event.windowId) return { state, effect: "already-pending" };
|
|
1695
|
+
return { state: { ...state, request: { phase: "tool-requested", windowId: event.windowId } }, effect: "close-out-armed" };
|
|
1508
1696
|
}
|
|
1509
1697
|
case "turn_end": {
|
|
1510
1698
|
const facts = event.facts;
|
|
1511
|
-
const
|
|
1512
|
-
|
|
1513
|
-
if (facts.aborted) {
|
|
1514
|
-
return { state: { request, overflow: "idle" }, effect: "none" };
|
|
1515
|
-
}
|
|
1699
|
+
const request = requestForWindow(state.request, facts.windowId);
|
|
1700
|
+
if (facts.aborted) return { state: initialResetControl(), effect: "none" };
|
|
1516
1701
|
if (facts.overflow) {
|
|
1517
1702
|
const pending = !facts.queued && facts.enabled && facts.automaticResetEnabled;
|
|
1518
1703
|
const spent = state.overflow === "pending-spent" || state.overflow === "spent";
|
|
1519
|
-
const
|
|
1520
|
-
return { state: { request, overflow
|
|
1704
|
+
const overflow = pending ? spent ? "pending-spent" : "pending" : spent ? "spent" : "idle";
|
|
1705
|
+
return { state: { request: NO_REQUEST, overflow }, effect: "none" };
|
|
1706
|
+
}
|
|
1707
|
+
if (facts.failed) return { state: { request: NO_REQUEST, overflow: state.overflow }, effect: "none" };
|
|
1708
|
+
if (!facts.enabled) {
|
|
1709
|
+
return { state: { request: NO_REQUEST, overflow: "idle" }, effect: "none" };
|
|
1710
|
+
}
|
|
1711
|
+
if (request.phase === "tool-requested" || facts.hardReserveDue) {
|
|
1712
|
+
return { state: { request: NO_REQUEST, overflow: "idle" }, effect: "commit-boundary" };
|
|
1521
1713
|
}
|
|
1522
|
-
|
|
1523
|
-
if (!facts.enabled || facts.failed) return { state: { request, overflow }, effect: "none" };
|
|
1524
|
-
const commit = requested || facts.thresholdDue;
|
|
1525
|
-
return { state: { request, overflow }, effect: commit ? "commit-boundary" : "none" };
|
|
1714
|
+
return { state: { request, overflow: "idle" }, effect: "none" };
|
|
1526
1715
|
}
|
|
1527
1716
|
case "before_settle": {
|
|
1528
1717
|
const facts = event.facts;
|
|
1529
|
-
if (
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1718
|
+
if (facts.aborted) return { state: initialResetControl(), effect: "none" };
|
|
1719
|
+
const request = requestForWindow(state.request, facts.windowId);
|
|
1720
|
+
if (state.overflow === "pending" || state.overflow === "pending-spent") {
|
|
1721
|
+
if (facts.queued) {
|
|
1722
|
+
return { state: { ...state, request: facts.failed ? NO_REQUEST : request }, effect: "none" };
|
|
1723
|
+
}
|
|
1724
|
+
const spent = state.overflow === "pending-spent";
|
|
1725
|
+
if (!facts.enabled || !facts.automaticResetEnabled) {
|
|
1726
|
+
return { state: { request: facts.failed ? NO_REQUEST : request, overflow: spent ? "spent" : "idle" }, effect: "none" };
|
|
1727
|
+
}
|
|
1728
|
+
return { state: { request: NO_REQUEST, overflow: "spent" }, effect: spent ? "none" : "recover-overflow" };
|
|
1729
|
+
}
|
|
1730
|
+
if (facts.failed || !facts.enabled || request.phase !== "close-out") {
|
|
1731
|
+
return { state: { ...state, request: NO_REQUEST }, effect: "none" };
|
|
1732
|
+
}
|
|
1733
|
+
if (facts.queued) return { state: { ...state, request }, effect: "none" };
|
|
1734
|
+
if (request.source === "automatic" && !facts.automaticResetEnabled) {
|
|
1735
|
+
return { state: { ...state, request: NO_REQUEST }, effect: "none" };
|
|
1534
1736
|
}
|
|
1535
|
-
return { state: {
|
|
1737
|
+
return { state: { request: NO_REQUEST, overflow: "idle" }, effect: "commit-boundary" };
|
|
1536
1738
|
}
|
|
1537
1739
|
case "settled":
|
|
1538
|
-
return { state:
|
|
1740
|
+
return { state: initialResetControl(), effect: "none" };
|
|
1539
1741
|
case "abort":
|
|
1540
1742
|
case "clear":
|
|
1541
1743
|
return { state: initialResetControl(), effect: "none" };
|
|
@@ -1543,27 +1745,38 @@ function reduceResetControl(state, event) {
|
|
|
1543
1745
|
}
|
|
1544
1746
|
function registerResetLifecycle(pi, options) {
|
|
1545
1747
|
let sessionActive = true;
|
|
1748
|
+
let lifecycleGeneration = 0;
|
|
1546
1749
|
let control = initialResetControl();
|
|
1547
1750
|
const clear = () => {
|
|
1548
1751
|
control = reduceResetControl(control, { type: "clear" }).state;
|
|
1549
1752
|
};
|
|
1550
|
-
const resetBoundaryResult = (entries, ctx) => {
|
|
1753
|
+
const resetBoundaryResult = async (entries, ctx) => {
|
|
1754
|
+
const generation = lifecycleGeneration;
|
|
1755
|
+
const outerGeneration = options.getLifecycleGeneration?.();
|
|
1756
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
1757
|
+
const windowId = currentWindowId(ctx);
|
|
1758
|
+
const isCurrent = () => sessionActive && lifecycleGeneration === generation && (outerGeneration === void 0 || options.getLifecycleGeneration?.() === outerGeneration) && options.isEnabled() && ctx.signal?.aborted !== true && ctx.sessionManager.getSessionId() === sessionId && currentWindowId(ctx) === windowId;
|
|
1759
|
+
let resetDrafts;
|
|
1551
1760
|
try {
|
|
1552
|
-
|
|
1761
|
+
resetDrafts = await options.buildReset(ctx, isCurrent);
|
|
1553
1762
|
} catch (error) {
|
|
1554
|
-
ctx.ui.notify(`pi-context: could not build reset (${String(error)}).`, "warning");
|
|
1763
|
+
if (isCurrent()) ctx.ui.notify(`pi-context: could not build reset (${String(error)}).`, "warning");
|
|
1555
1764
|
return entries.length > 0 ? { entries } : void 0;
|
|
1556
1765
|
}
|
|
1766
|
+
if (!isCurrent()) return entries.length > 0 ? { entries } : void 0;
|
|
1767
|
+
options.onResetReady?.(ctx, resetDrafts);
|
|
1768
|
+
return { entries: [...entries, ...resetDrafts], continue: true };
|
|
1557
1769
|
};
|
|
1558
|
-
pi.on("turn_end", (event, ctx) => {
|
|
1770
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
1559
1771
|
if (!sessionActive) return void 0;
|
|
1560
1772
|
const aborted = isAbort(event.message, event.outcome, ctx);
|
|
1561
1773
|
const stagedBudgetEntries = options.budget.consumeTurnEnd(ctx);
|
|
1562
|
-
const budgetEntries = options.isEnabled() && !aborted ? stagedBudgetEntries : [];
|
|
1774
|
+
const budgetEntries = options.isEnabled() && !aborted && event.outcome !== "error" ? stagedBudgetEntries : [];
|
|
1563
1775
|
const entries = [...event.entries ?? [], ...budgetEntries];
|
|
1564
1776
|
const decision = reduceResetControl(control, {
|
|
1565
1777
|
type: "turn_end",
|
|
1566
1778
|
facts: {
|
|
1779
|
+
windowId: currentWindowId(ctx),
|
|
1567
1780
|
aborted,
|
|
1568
1781
|
overflow: aborted ? false : isOverflowLike(event.message, ctx),
|
|
1569
1782
|
failed: event.outcome === "error",
|
|
@@ -1574,20 +1787,21 @@ function registerResetLifecycle(pi, options) {
|
|
|
1574
1787
|
get automaticResetEnabled() {
|
|
1575
1788
|
return options.budget.automaticResetEnabled(ctx);
|
|
1576
1789
|
},
|
|
1577
|
-
get
|
|
1578
|
-
return options.budget.
|
|
1790
|
+
get hardReserveDue() {
|
|
1791
|
+
return options.budget.hardReserveDue(ctx);
|
|
1579
1792
|
}
|
|
1580
1793
|
}
|
|
1581
1794
|
});
|
|
1582
1795
|
control = decision.state;
|
|
1583
|
-
if (decision.effect === "commit-boundary") return resetBoundaryResult(entries, ctx);
|
|
1796
|
+
if (decision.effect === "commit-boundary") return await resetBoundaryResult(entries, ctx);
|
|
1584
1797
|
return entries.length > 0 ? { entries } : void 0;
|
|
1585
1798
|
});
|
|
1586
|
-
pi.on("agent_before_settle", (event, ctx) => {
|
|
1799
|
+
pi.on("agent_before_settle", async (event, ctx) => {
|
|
1587
1800
|
if (!sessionActive) return void 0;
|
|
1588
1801
|
const decision = reduceResetControl(control, {
|
|
1589
1802
|
type: "before_settle",
|
|
1590
1803
|
facts: {
|
|
1804
|
+
windowId: currentWindowId(ctx),
|
|
1591
1805
|
get queued() {
|
|
1592
1806
|
return event.context.pendingMessages.length > 0 || ctx.hasPendingMessages();
|
|
1593
1807
|
},
|
|
@@ -1597,14 +1811,13 @@ function registerResetLifecycle(pi, options) {
|
|
|
1597
1811
|
get automaticResetEnabled() {
|
|
1598
1812
|
return options.budget.automaticResetEnabled(ctx);
|
|
1599
1813
|
},
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
}
|
|
1814
|
+
aborted: event.outcome === "aborted" || ctx.signal?.aborted === true,
|
|
1815
|
+
failed: event.outcome === "error"
|
|
1603
1816
|
}
|
|
1604
1817
|
});
|
|
1605
1818
|
control = decision.state;
|
|
1606
|
-
if (decision.effect !== "recover-overflow") return void 0;
|
|
1607
|
-
return resetBoundaryResult(event.entries, ctx);
|
|
1819
|
+
if (decision.effect !== "commit-boundary" && decision.effect !== "recover-overflow") return void 0;
|
|
1820
|
+
return await resetBoundaryResult(event.entries, ctx);
|
|
1608
1821
|
});
|
|
1609
1822
|
pi.on("session_before_compact", (event, ctx) => {
|
|
1610
1823
|
if (!sessionActive) return void 0;
|
|
@@ -1619,26 +1832,36 @@ function registerResetLifecycle(pi, options) {
|
|
|
1619
1832
|
return void 0;
|
|
1620
1833
|
});
|
|
1621
1834
|
pi.on("agent_end", (_event, ctx) => {
|
|
1622
|
-
if (ctx.signal?.aborted)
|
|
1835
|
+
if (ctx.signal?.aborted) control = reduceResetControl(control, { type: "abort" }).state;
|
|
1623
1836
|
});
|
|
1624
1837
|
pi.on("agent_settled", () => {
|
|
1625
1838
|
control = reduceResetControl(control, { type: "settled" }).state;
|
|
1626
1839
|
});
|
|
1627
1840
|
pi.on("session_start", () => {
|
|
1841
|
+
lifecycleGeneration++;
|
|
1628
1842
|
clear();
|
|
1629
1843
|
sessionActive = true;
|
|
1630
1844
|
});
|
|
1631
|
-
pi.on("session_tree",
|
|
1845
|
+
pi.on("session_tree", () => {
|
|
1846
|
+
lifecycleGeneration++;
|
|
1847
|
+
clear();
|
|
1848
|
+
});
|
|
1632
1849
|
pi.on("session_shutdown", () => {
|
|
1850
|
+
lifecycleGeneration++;
|
|
1633
1851
|
clear();
|
|
1634
1852
|
options.budget.clear();
|
|
1635
1853
|
sessionActive = false;
|
|
1636
1854
|
});
|
|
1637
1855
|
return {
|
|
1638
|
-
|
|
1639
|
-
const decision = reduceResetControl(control, { type: "
|
|
1856
|
+
closeOut(windowId, source) {
|
|
1857
|
+
const decision = reduceResetControl(control, { type: "close_out", windowId, source });
|
|
1858
|
+
control = decision.state;
|
|
1859
|
+
return decision.effect;
|
|
1860
|
+
},
|
|
1861
|
+
request(windowId) {
|
|
1862
|
+
const decision = reduceResetControl(control, { type: "tool_request", windowId });
|
|
1640
1863
|
control = decision.state;
|
|
1641
|
-
return decision.effect === "already-
|
|
1864
|
+
return decision.effect === "already-pending" ? "rollover_already_pending" : "rollover_requested";
|
|
1642
1865
|
},
|
|
1643
1866
|
clear
|
|
1644
1867
|
};
|
|
@@ -1647,7 +1870,7 @@ function registerResetLifecycle(pi, options) {
|
|
|
1647
1870
|
// src/context/reset-artifacts.ts
|
|
1648
1871
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1649
1872
|
|
|
1650
|
-
// src/notes/
|
|
1873
|
+
// src/pi/notes/snapshot.ts
|
|
1651
1874
|
var NOTES_HOMES = [
|
|
1652
1875
|
{ scope: "session", label: "this session" },
|
|
1653
1876
|
{ scope: "project", label: "@project" },
|
|
@@ -1655,13 +1878,19 @@ var NOTES_HOMES = [
|
|
|
1655
1878
|
{ scope: "agent", label: "@self" },
|
|
1656
1879
|
{ scope: "model", label: "@model" }
|
|
1657
1880
|
];
|
|
1658
|
-
function
|
|
1881
|
+
function queryForScope(scope) {
|
|
1882
|
+
if (scope === "agent" || scope === "model") return { scope };
|
|
1883
|
+
return { scope };
|
|
1884
|
+
}
|
|
1885
|
+
async function loadNotesSnapshot(ctx, loadHome, identity = notesContextFromPi(ctx)) {
|
|
1659
1886
|
const openedAt = Date.now();
|
|
1887
|
+
const store = createNotesStore(identity);
|
|
1888
|
+
const load = loadHome ?? ((_context, scope) => store.list(queryForScope(scope)));
|
|
1660
1889
|
const homes = /* @__PURE__ */ new Map();
|
|
1661
1890
|
const unavailable = [];
|
|
1662
1891
|
for (const home of NOTES_HOMES) {
|
|
1663
1892
|
try {
|
|
1664
|
-
homes.set(home.scope,
|
|
1893
|
+
homes.set(home.scope, await load(ctx, home.scope));
|
|
1665
1894
|
} catch (error) {
|
|
1666
1895
|
const code = typeof error === "object" && error !== null ? error.code : void 0;
|
|
1667
1896
|
if (typeof code !== "string" || !/^E[A-Z0-9_]+$/.test(code) || code.startsWith("ERR_")) throw error;
|
|
@@ -1673,20 +1902,19 @@ function loadNotesSnapshot(ctx, loadHome = (context, scope) => listNotes(context
|
|
|
1673
1902
|
}
|
|
1674
1903
|
|
|
1675
1904
|
// src/context/boot.ts
|
|
1676
|
-
function bootContent(
|
|
1677
|
-
return renderBootBlock({
|
|
1678
|
-
agentName: agentSlug(ctx),
|
|
1679
|
-
modelName: modelSlug(ctx),
|
|
1680
|
-
firstWindowId: rootWindowId(ctx.sessionManager.getSessionId()),
|
|
1681
|
-
currentWindowId: currentId,
|
|
1682
|
-
previousWindowId: previousId,
|
|
1683
|
-
notes
|
|
1684
|
-
});
|
|
1905
|
+
function bootContent(agentName, modelName, firstWindowId, currentId, previousId, notes) {
|
|
1906
|
+
return renderBootBlock({ agentName, modelName, firstWindowId, currentWindowId: currentId, previousWindowId: previousId, notes });
|
|
1685
1907
|
}
|
|
1686
|
-
function buildBootMessage(ctx, windowId, previousId, notifyIncompleteNotes) {
|
|
1687
|
-
const
|
|
1688
|
-
|
|
1689
|
-
|
|
1908
|
+
async function buildBootMessage(ctx, windowId, previousId, notifyIncompleteNotes, isCurrent = () => true) {
|
|
1909
|
+
const identity = notesContextFromPi(ctx);
|
|
1910
|
+
const notes = await loadNotesSnapshot(ctx, void 0, identity);
|
|
1911
|
+
if (isCurrent()) notifyIncompleteNotes?.(ctx, windowId, notes);
|
|
1912
|
+
return {
|
|
1913
|
+
customType: BOOT_TYPE,
|
|
1914
|
+
content: bootContent(identity.agent, identity.model, rootWindowId(identity.sessionId), windowId, previousId, notes),
|
|
1915
|
+
display: false,
|
|
1916
|
+
details: { windowId }
|
|
1917
|
+
};
|
|
1690
1918
|
}
|
|
1691
1919
|
function sendBoot(pi, boot) {
|
|
1692
1920
|
pi.sendMessage(
|
|
@@ -1694,15 +1922,20 @@ function sendBoot(pi, boot) {
|
|
|
1694
1922
|
{ triggerTurn: false }
|
|
1695
1923
|
);
|
|
1696
1924
|
}
|
|
1697
|
-
function ensureBoot(pi, ctx, notifyIncompleteNotes) {
|
|
1925
|
+
async function ensureBoot(pi, ctx, notifyIncompleteNotes, isCurrent = () => true) {
|
|
1926
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
1698
1927
|
const reset = currentReset(ctx);
|
|
1928
|
+
const windowId = reset?.data.windowId ?? rootWindowId(sessionId);
|
|
1929
|
+
const stillCurrent = () => isCurrent() && ctx.signal?.aborted !== true && ctx.sessionManager.getSessionId() === sessionId && currentWindowId(ctx) === windowId;
|
|
1699
1930
|
if (reset) {
|
|
1700
|
-
repairResetTail(pi, ctx, reset, notifyIncompleteNotes);
|
|
1931
|
+
await repairResetTail(pi, ctx, reset, notifyIncompleteNotes, stillCurrent);
|
|
1701
1932
|
return;
|
|
1702
1933
|
}
|
|
1703
|
-
const windowId = rootWindowId(ctx.sessionManager.getSessionId());
|
|
1704
1934
|
if (ctx.sessionManager.buildSessionProjection().messages.some((message) => isWindowBoot(message, windowId))) return;
|
|
1705
|
-
|
|
1935
|
+
const boot = await buildBootMessage(ctx, windowId, void 0, notifyIncompleteNotes, stillCurrent);
|
|
1936
|
+
if (!stillCurrent()) return;
|
|
1937
|
+
if (ctx.sessionManager.buildSessionProjection().messages.some((message) => isWindowBoot(message, windowId))) return;
|
|
1938
|
+
sendBoot(pi, boot);
|
|
1706
1939
|
}
|
|
1707
1940
|
|
|
1708
1941
|
// src/context/reset-artifacts.ts
|
|
@@ -1715,8 +1948,10 @@ function sendContinuation(pi) {
|
|
|
1715
1948
|
{ triggerTurn: false }
|
|
1716
1949
|
);
|
|
1717
1950
|
}
|
|
1718
|
-
function buildResetDrafts(ctx, notifyIncompleteNotes) {
|
|
1719
|
-
const
|
|
1951
|
+
async function buildResetDrafts(ctx, notifyIncompleteNotes, isCurrent = () => true) {
|
|
1952
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
1953
|
+
const previousId = currentWindowId(ctx);
|
|
1954
|
+
const sessionPrefix = sessionId.slice(0, 8);
|
|
1720
1955
|
const usedWindowIds = new Set(
|
|
1721
1956
|
ctx.sessionManager.getBranch().filter(isWindowMarker).map((entry) => entry.data.windowId)
|
|
1722
1957
|
);
|
|
@@ -1724,26 +1959,14 @@ function buildResetDrafts(ctx, notifyIncompleteNotes) {
|
|
|
1724
1959
|
do {
|
|
1725
1960
|
windowId = `pcw:${sessionPrefix}:${randomUUID2().slice(0, 8)}`;
|
|
1726
1961
|
} while (usedWindowIds.has(windowId));
|
|
1727
|
-
const boot = buildBootMessage(ctx, windowId,
|
|
1962
|
+
const boot = await buildBootMessage(ctx, windowId, previousId, notifyIncompleteNotes, isCurrent);
|
|
1728
1963
|
return [
|
|
1964
|
+
{ type: "compaction", summary: "", firstKeptEntryId: null },
|
|
1729
1965
|
{ type: "custom", customType: RESET_MARKER_TYPE, data: { windowId } },
|
|
1730
1966
|
{ type: "custom_message", customType: BOOT_TYPE, content: boot.content, display: false, details: { windowId } },
|
|
1731
1967
|
{ type: "custom_message", customType: CONTINUATION_TYPE, content: CONTINUATION, display: false }
|
|
1732
1968
|
];
|
|
1733
1969
|
}
|
|
1734
|
-
function persistManualReset(pi, ctx, notifyIncompleteNotes) {
|
|
1735
|
-
const [marker, boot, continuation] = buildResetDrafts(ctx, notifyIncompleteNotes);
|
|
1736
|
-
pi.appendEntry(marker.customType, marker.data);
|
|
1737
|
-
pi.sendMessage(
|
|
1738
|
-
{ customType: boot.customType, content: boot.content, display: boot.display, details: boot.details },
|
|
1739
|
-
{ triggerTurn: false }
|
|
1740
|
-
);
|
|
1741
|
-
pi.sendMessage(
|
|
1742
|
-
{ customType: continuation.customType, content: continuation.content, display: continuation.display },
|
|
1743
|
-
{ triggerTurn: false }
|
|
1744
|
-
);
|
|
1745
|
-
return boot.details.windowId;
|
|
1746
|
-
}
|
|
1747
1970
|
function inspectResetTail(ctx, markerId, windowId) {
|
|
1748
1971
|
const branch = ctx.sessionManager.getBranch();
|
|
1749
1972
|
const markerIndex = branch.findIndex((entry) => entry.id === markerId);
|
|
@@ -1771,11 +1994,15 @@ function resetTailCommitted(ctx, markerId, windowId) {
|
|
|
1771
1994
|
const tail = inspectResetTail(ctx, markerId, windowId);
|
|
1772
1995
|
return tail?.boot === true && tail.continuation === true;
|
|
1773
1996
|
}
|
|
1774
|
-
function repairResetTail(pi, ctx, marker, notifyIncompleteNotes) {
|
|
1997
|
+
async function repairResetTail(pi, ctx, marker, notifyIncompleteNotes, isCurrent = () => true) {
|
|
1775
1998
|
const tail = inspectResetTail(ctx, marker.id, marker.data.windowId);
|
|
1776
1999
|
if (!tail || tail.boot && tail.continuation) return;
|
|
1777
|
-
|
|
1778
|
-
if (!
|
|
2000
|
+
const boot = tail.boot ? void 0 : await buildBootMessage(ctx, marker.data.windowId, previousWindowId(ctx, marker.id), notifyIncompleteNotes, isCurrent);
|
|
2001
|
+
if (!isCurrent()) return;
|
|
2002
|
+
const latest = inspectResetTail(ctx, marker.id, marker.data.windowId);
|
|
2003
|
+
if (!latest) return;
|
|
2004
|
+
if (!latest.boot && boot) sendBoot(pi, boot);
|
|
2005
|
+
if (!latest.continuation && (latest.boot || boot)) sendContinuation(pi);
|
|
1779
2006
|
}
|
|
1780
2007
|
|
|
1781
2008
|
// src/context/runtime.ts
|
|
@@ -1785,6 +2012,7 @@ function branchHasWindowMarker(ctx, fromId) {
|
|
|
1785
2012
|
}
|
|
1786
2013
|
function registerContext(pi, settingsManager) {
|
|
1787
2014
|
let enabled = true;
|
|
2015
|
+
let lifecycleGeneration = 0;
|
|
1788
2016
|
let missingBootNotice;
|
|
1789
2017
|
const incompleteNotesNotified = /* @__PURE__ */ new Set();
|
|
1790
2018
|
const pendingResetNotices = /* @__PURE__ */ new Set();
|
|
@@ -1794,7 +2022,7 @@ function registerContext(pi, settingsManager) {
|
|
|
1794
2022
|
const branch = ctx.sessionManager.getBranch();
|
|
1795
2023
|
for (const windowId of pendingResetNotices) {
|
|
1796
2024
|
const marker = branch.find((entry) => isWindowMarker(entry) && entry.data.windowId === windowId);
|
|
1797
|
-
if (!marker || !resetTailCommitted(ctx, marker.id, windowId)) continue;
|
|
2025
|
+
if (!marker || !isCheckpointBackedReset(ctx, marker) || !resetTailCommitted(ctx, marker.id, windowId)) continue;
|
|
1798
2026
|
pendingResetNotices.delete(windowId);
|
|
1799
2027
|
ctx.ui.notify(`pi-context: memory cleared \xB7 ${windowId}`, "info");
|
|
1800
2028
|
}
|
|
@@ -1812,17 +2040,23 @@ function registerContext(pi, settingsManager) {
|
|
|
1812
2040
|
};
|
|
1813
2041
|
const migrationWarning = migrateLegacyHomes();
|
|
1814
2042
|
if (migrationWarning) console.warn(`pi-context: ${migrationWarning}`);
|
|
1815
|
-
const budget = registerBudget(pi, () => enabled, settingsManager);
|
|
1816
|
-
pi.on("session_start", (_event, ctx) => {
|
|
2043
|
+
const budget = registerBudget(pi, () => enabled, settingsManager, (windowId) => resets.closeOut(windowId, "automatic"));
|
|
2044
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
2045
|
+
const generation = ++lifecycleGeneration;
|
|
1817
2046
|
if (!enabled) return;
|
|
1818
2047
|
missingBootNotice = void 0;
|
|
1819
2048
|
pendingResetNotices.clear();
|
|
1820
|
-
ensureBoot(pi, ctx, notifyIncompleteNotes);
|
|
2049
|
+
await ensureBoot(pi, ctx, notifyIncompleteNotes, () => generation === lifecycleGeneration && enabled);
|
|
1821
2050
|
});
|
|
1822
|
-
pi.on("session_tree", (_event, ctx) => {
|
|
2051
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
2052
|
+
const generation = ++lifecycleGeneration;
|
|
1823
2053
|
missingBootNotice = void 0;
|
|
1824
2054
|
pendingResetNotices.clear();
|
|
1825
|
-
if (enabled) ensureBoot(pi, ctx, notifyIncompleteNotes);
|
|
2055
|
+
if (enabled) await ensureBoot(pi, ctx, notifyIncompleteNotes, () => generation === lifecycleGeneration && enabled);
|
|
2056
|
+
});
|
|
2057
|
+
pi.on("session_shutdown", () => {
|
|
2058
|
+
lifecycleGeneration++;
|
|
2059
|
+
pendingResetNotices.clear();
|
|
1826
2060
|
});
|
|
1827
2061
|
pi.on("session_before_tree", (event, ctx) => {
|
|
1828
2062
|
if (!event.preparation.userWantsSummary) return void 0;
|
|
@@ -1834,7 +2068,11 @@ function registerContext(pi, settingsManager) {
|
|
|
1834
2068
|
const reset = currentReset(ctx);
|
|
1835
2069
|
const windowId = reset?.data.windowId ?? rootWindowId(ctx.sessionManager.getSessionId());
|
|
1836
2070
|
try {
|
|
1837
|
-
|
|
2071
|
+
if (reset) {
|
|
2072
|
+
if (!event.messages.some((message) => isWindowBoot(message, windowId))) throw new Error(`Missing boot for context window ${windowId}`);
|
|
2073
|
+
return { messages: isCheckpointBackedReset(ctx, reset) ? event.messages : projectWindow(event.messages, windowId) };
|
|
2074
|
+
}
|
|
2075
|
+
return { messages: projectRootWindow(event.messages, windowId) };
|
|
1838
2076
|
} catch (error) {
|
|
1839
2077
|
if (missingBootNotice !== windowId) {
|
|
1840
2078
|
missingBootNotice = windowId;
|
|
@@ -1852,9 +2090,12 @@ function registerContext(pi, settingsManager) {
|
|
|
1852
2090
|
const arg = args.trim().toLowerCase();
|
|
1853
2091
|
if (arg === "on") {
|
|
1854
2092
|
enabled = true;
|
|
1855
|
-
|
|
2093
|
+
const generation = ++lifecycleGeneration;
|
|
2094
|
+
await ensureBoot(pi, cmdCtx, notifyIncompleteNotes, () => generation === lifecycleGeneration && enabled);
|
|
1856
2095
|
} else if (arg === "off") {
|
|
1857
2096
|
enabled = false;
|
|
2097
|
+
lifecycleGeneration++;
|
|
2098
|
+
pendingResetNotices.clear();
|
|
1858
2099
|
budget.clear();
|
|
1859
2100
|
resets.clear();
|
|
1860
2101
|
} else if (arg !== "") {
|
|
@@ -1865,16 +2106,25 @@ function registerContext(pi, settingsManager) {
|
|
|
1865
2106
|
}
|
|
1866
2107
|
});
|
|
1867
2108
|
pi.registerCommand("wipe-memory", {
|
|
1868
|
-
description: "
|
|
2109
|
+
description: "Ask the agent to close out its notes, then start a fresh context window",
|
|
1869
2110
|
handler: async (_args, cmdCtx) => {
|
|
1870
2111
|
if (!enabled) {
|
|
1871
2112
|
cmdCtx.ui.notify("pi-context: /wipe-memory requires /pi-context on.", "error");
|
|
1872
2113
|
return;
|
|
1873
2114
|
}
|
|
2115
|
+
const requestedWindowId = currentWindowId(cmdCtx);
|
|
2116
|
+
await cmdCtx.waitForIdle();
|
|
2117
|
+
if (!enabled || currentWindowId(cmdCtx) !== requestedWindowId) return;
|
|
2118
|
+
const armed = resets.closeOut(requestedWindowId, "manual");
|
|
2119
|
+
if (armed === "already-pending") return;
|
|
2120
|
+
try {
|
|
2121
|
+
pi.sendMessage({ customType: WARNING_TYPE, content: WARNING_CONTENT, display: false }, { triggerTurn: true });
|
|
2122
|
+
} catch (error) {
|
|
2123
|
+
resets.clear();
|
|
2124
|
+
cmdCtx.ui.notify(`pi-context: could not start manual close-out (${String(error)}).`, "error");
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
1874
2127
|
await cmdCtx.waitForIdle();
|
|
1875
|
-
if (!enabled) return;
|
|
1876
|
-
resets.clear();
|
|
1877
|
-
notifyCommittedResets(cmdCtx, persistManualReset(pi, cmdCtx, notifyIncompleteNotes));
|
|
1878
2128
|
}
|
|
1879
2129
|
});
|
|
1880
2130
|
pi.registerTool(defineTool4({
|
|
@@ -1882,23 +2132,27 @@ function registerContext(pi, settingsManager) {
|
|
|
1882
2132
|
label: "Wipe memory",
|
|
1883
2133
|
description: "Wipe your in-context memory and start a fresh context window. Your session, notes, and history survive.",
|
|
1884
2134
|
parameters: Type5.Object({}, { additionalProperties: false }),
|
|
1885
|
-
async execute() {
|
|
2135
|
+
async execute(_id, _params, _signal, _update, ctx) {
|
|
1886
2136
|
if (!enabled) return output({ error: "pi-context is off (/pi-context on to enable)" });
|
|
1887
|
-
return output({ status: resets.request() }, void 0, true);
|
|
2137
|
+
return output({ status: resets.request(currentWindowId(ctx)) }, void 0, true);
|
|
1888
2138
|
}
|
|
1889
2139
|
}));
|
|
1890
2140
|
const resets = registerResetLifecycle(pi, {
|
|
1891
2141
|
isEnabled: () => enabled,
|
|
1892
|
-
buildReset: (ctx) =>
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
2142
|
+
buildReset: (ctx, isCurrent) => buildResetDrafts(ctx, notifyIncompleteNotes, isCurrent),
|
|
2143
|
+
getLifecycleGeneration: () => lifecycleGeneration,
|
|
2144
|
+
onResetReady: (_ctx, drafts) => {
|
|
2145
|
+
const boot = drafts.find((draft) => draft.type === "custom_message" && draft.customType === BOOT_TYPE);
|
|
2146
|
+
if (boot?.type === "custom_message" && boot.details && typeof boot.details === "object") {
|
|
2147
|
+
const windowId = boot.details.windowId;
|
|
2148
|
+
if (typeof windowId === "string") pendingResetNotices.add(windowId);
|
|
2149
|
+
}
|
|
1896
2150
|
},
|
|
1897
2151
|
budget
|
|
1898
2152
|
});
|
|
1899
2153
|
}
|
|
1900
2154
|
|
|
1901
|
-
// src/notes/session-replay.ts
|
|
2155
|
+
// src/pi/notes/session-replay.ts
|
|
1902
2156
|
function isNoteOperation(data) {
|
|
1903
2157
|
if (typeof data !== "object" || data === null) return false;
|
|
1904
2158
|
const op = data;
|
|
@@ -1941,7 +2195,7 @@ function createPiContext(options = {}) {
|
|
|
1941
2195
|
function piContext(pi) {
|
|
1942
2196
|
registerPiContext(pi);
|
|
1943
2197
|
}
|
|
1944
|
-
var internal = { MAX_NOTE_BYTES, NOTE_TYPE, BOOT_TYPE, GUIDANCE_TYPE, WARNING_TYPE, CONTINUATION_TYPE, WARNING_PROMPT, WARNING_RUNWAY_TOKENS, RESET_MARKER_TYPE, CONTINUATION, CONTEXT_WINDOW_OPEN_TAG, CONTEXT_WINDOW_CLOSE_TAG, CONTEXT_WINDOW_PROTOCOL_OPEN_TAG, CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG, GUIDANCE_OPEN_TAG, PI_CONTEXT_SETTINGS_KEY, DEFAULT_RESERVE_TOKENS, DEFAULT_REMINDER_MARGIN_TOKENS, deriveThresholds, mergePiContextSettings, assertVirtualPath };
|
|
2198
|
+
var internal = { MAX_NOTE_BYTES, NOTE_TYPE, BOOT_TYPE, GUIDANCE_TYPE, WARNING_TYPE, CONTINUATION_TYPE, WARNING_PROMPT, WARNING_CONTENT, WARNING_RUNWAY_TOKENS, RESET_MARKER_TYPE, CONTINUATION, CONTEXT_WINDOW_OPEN_TAG, CONTEXT_WINDOW_CLOSE_TAG, CONTEXT_WINDOW_PROTOCOL_OPEN_TAG, CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG, GUIDANCE_OPEN_TAG, PI_CONTEXT_SETTINGS_KEY, DEFAULT_RESERVE_TOKENS, DEFAULT_REMINDER_MARGIN_TOKENS, deriveThresholds, mergePiContextSettings, assertVirtualPath };
|
|
1945
2199
|
export {
|
|
1946
2200
|
createPiContext,
|
|
1947
2201
|
piContext as default,
|