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