@astrosheep/pi-context 0.25.0 → 0.25.2
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/dist/build-info.json +2 -2
- package/dist/extension.js +229 -139
- package/dist/src/context/boot.js +46 -0
- package/dist/src/context/budget.js +6 -6
- package/dist/src/context/context-window.js +15 -0
- package/dist/src/context/prompts.js +4 -7
- package/dist/src/context/reset-artifacts.js +86 -0
- package/dist/src/context/reset-lifecycle.js +108 -60
- package/dist/src/context/runtime.js +8 -93
- package/dist/src/index.js +2 -2
- package/dist/src/notes/address.js +1 -1
- package/dist/src/notes/tools.js +3 -3
- package/dist/src/protocol.js +3 -5
- package/dist/test/agent-loop.test.js +12 -10
- package/dist/test/boot.integration.test.js +56 -4
- package/dist/test/helpers/extension.js +1 -2
- package/dist/test/notes.integration.test.js +1 -4
- package/dist/test/notes.test.js +2 -2
- package/dist/test/reset-lifecycle.test.js +199 -2
- package/docs/reset-lifecycle.md +57 -0
- package/package.json +1 -1
- package/src/context/boot.ts +68 -0
- package/src/context/budget.ts +9 -9
- package/src/context/context-window.ts +15 -0
- package/src/context/prompts.ts +4 -7
- package/src/context/reset-artifacts.ts +101 -0
- package/src/context/reset-lifecycle.ts +183 -56
- package/src/context/runtime.ts +9 -104
- package/src/index.ts +2 -2
- package/src/notes/address.ts +1 -1
- package/src/notes/tools.ts +3 -3
- package/src/protocol.ts +3 -6
package/dist/build-info.json
CHANGED
package/dist/extension.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// <define:__PI_CONTEXT_BUILD__>
|
|
2
|
-
var define_PI_CONTEXT_BUILD_default = { version: "0.25.
|
|
2
|
+
var define_PI_CONTEXT_BUILD_default = { version: "0.25.2", sourceHash: "e240c1946d0f2b6d7b1d83ed658c348c7daed5fce5a374844215f1eebddcb25f" };
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
5
|
import { VERSION as VERSION2 } from "@earendil-works/pi-coding-agent";
|
|
@@ -259,8 +259,7 @@ var PI_CONTEXT_SETTINGS_KEY = "pi-context";
|
|
|
259
259
|
var DEFAULT_RESERVE_TOKENS = 16384;
|
|
260
260
|
var DEFAULT_REMINDER_MARGIN_TOKENS = 24576;
|
|
261
261
|
var WARNING_RUNWAY_TOKENS = 12288;
|
|
262
|
-
var
|
|
263
|
-
var CONTINUATION = "Your memory was just erased. Pull only the details you need from history_* and notes_*, then get back to work.";
|
|
262
|
+
var CONTINUATION = "Your memory was just erased. Your head is blank. Good news: your notes are still here, and history remains... searchable. Do try to keep up.";
|
|
264
263
|
var PROTOCOL_BLOCK = `${CONTEXT_WINDOW_PROTOCOL_OPEN_TAG}
|
|
265
264
|
Your memory resets whenever the context window fills; only what you wrote down survives. Two things outlive every window in this session: the notes you wrote, and the history that was recorded. Neither is memory \u2014 both are record. Write notes with notes_write, revise them with notes_edit, and read them back with notes_read / notes_search / notes_list; history is read-only through the history_* tools. Everything else wakes blank.
|
|
266
265
|
Mark outdated or unneeded notes stale \u2014 leave them, and they will keep misleading you.
|
|
@@ -269,9 +268,7 @@ Keep a running checkpoint while you work, not at the last minute \u2014 the next
|
|
|
269
268
|
|
|
270
269
|
Use get_context_remaining to see how much of the window is left. When it runs out, this window is gone \u2014 with no final turn at the limit \u2014 and you continue in a fresh one, recovering only through notes_* and history_*. Once your checkpoint is written, you can call wipe_memory yourself instead of waiting for the erase. Do not let a window die undocumented.
|
|
271
270
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
Notes live in five homes, and the word after @ is always one of their reserved names \u2014 your own name and other people's names live at the second level (@agents/faye/, never @faye/). Bare names are this session; @project/<vpath> is this project's workspace; @human/<vpath> is the human's cross-project home; @self/<vpath> and @agents/<name>/<vpath> are agent homes; @model/<vpath> and @models/<name>/<vpath> are model homes. @self and @model are the only relative forms \u2014 the current agent, the current model \u2014 and listings never show them, only the resolved name. There is no cross-home fallback.
|
|
271
|
+
Note addresses take five prefixes: bare <vpath> is this session; @project/<vpath> is this project; @human/<vpath> is the human's cross-project notes; @self/<vpath> is your own, as the current agent; @model/<vpath> is the current model's. @self and @model resolve to who is running now; listings always show resolved names. Nothing else is legal \u2014 any other @ prefix, or @ inside a vpath, is a hard error, with no fallback across prefixes.
|
|
275
272
|
Session notes belong to this trip \u2014 the goal, the progress, the loose ends. The next window of THIS trip wakes to them; once the trip is over, nobody does.
|
|
276
273
|
@project notes hold facts about this project \u2014 architecture, conventions, workflows, deployment and environment details \u2014 for whoever works here next.
|
|
277
274
|
@human notes hold the human's durable preferences and standing rules, plus lessons that apply across projects \u2014 for every agent that serves this human, whoever is running. You write there as the human's scribe; what the human dictates carries origin: user. When the intended scope is unclear, keep the note in the narrowest stated scope rather than widening it.
|
|
@@ -307,12 +304,23 @@ function hasWindowMessage(ctx, customType) {
|
|
|
307
304
|
function currentWindowId(ctx) {
|
|
308
305
|
return currentReset(ctx)?.data.windowId ?? rootWindowId(ctx.sessionManager.getSessionId());
|
|
309
306
|
}
|
|
307
|
+
function previousWindowId(ctx, markerId) {
|
|
308
|
+
let previousId = rootWindowId(ctx.sessionManager.getSessionId());
|
|
309
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
310
|
+
if (entry.id === markerId) break;
|
|
311
|
+
if (isWindowMarker(entry)) previousId = entry.data.windowId;
|
|
312
|
+
}
|
|
313
|
+
return previousId;
|
|
314
|
+
}
|
|
310
315
|
function hasWindowId(details, windowId) {
|
|
311
316
|
return typeof details === "object" && details !== null && typeof details.windowId === "string" && details.windowId === windowId;
|
|
312
317
|
}
|
|
313
318
|
function isWindowBoot(message, windowId) {
|
|
314
319
|
return message.role === "custom" && message.customType === BOOT_TYPE && (windowId === void 0 || hasWindowId(message.details, windowId));
|
|
315
320
|
}
|
|
321
|
+
function isWindowBootEntry(entry, windowId) {
|
|
322
|
+
return entry.type === "custom_message" && entry.customType === BOOT_TYPE && hasWindowId(entry.details, windowId);
|
|
323
|
+
}
|
|
316
324
|
function projectWindow(messages, windowId) {
|
|
317
325
|
const cut = messages.findIndex((message) => isWindowBoot(message, windowId));
|
|
318
326
|
if (cut < 0) throw new Error(`Missing boot for context window ${windowId}`);
|
|
@@ -756,7 +764,7 @@ function physicalPath(scope, vpath, ctx, who) {
|
|
|
756
764
|
}
|
|
757
765
|
|
|
758
766
|
// src/notes/address.ts
|
|
759
|
-
var ADDRESS_FORMS = "legal prefixes are @project/, @human/, @self/,
|
|
767
|
+
var ADDRESS_FORMS = "legal prefixes are @project/, @human/, @self/, and @model/; bare names are this session";
|
|
760
768
|
function assertVirtualPath(value) {
|
|
761
769
|
if (typeof value !== "string" || value.length === 0) throw new Error("path must be a non-empty virtual relative path");
|
|
762
770
|
if (value.includes("\0") || value.includes("\\") || value.startsWith("/")) throw new Error("path must be a safe virtual relative path");
|
|
@@ -1076,7 +1084,7 @@ function searchNotes(ctx, queries, opts = {}) {
|
|
|
1076
1084
|
var ORIGIN = Type3.Optional(Type3.Union([Type3.Literal("user"), Type3.Literal("self"), Type3.Literal("external")], {
|
|
1077
1085
|
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."
|
|
1078
1086
|
}));
|
|
1079
|
-
var ADDRESS_DESCRIPTION = "Address forms are bare `<vpath>` for this session, `@project/<vpath>` for this project, `@human/<vpath>` for the human's cross-project
|
|
1087
|
+
var ADDRESS_DESCRIPTION = "Address forms are bare `<vpath>` for this session, `@project/<vpath>` for this project, `@human/<vpath>` for the human's cross-project notes, `@self/<vpath>` for your own, and `@model/<vpath>` for the current model's. `@self` and `@model` mean whoever is running now. Any other `@` prefix, or `@` inside a vpath, is a hard error. There is no fallback across prefixes. Paths reject `..`, absolute paths, and backslashes.";
|
|
1080
1088
|
function failure(error) {
|
|
1081
1089
|
if (error instanceof NoteError) {
|
|
1082
1090
|
const payload = { error: error.message };
|
|
@@ -1146,7 +1154,7 @@ function registerNotesTools(pi) {
|
|
|
1146
1154
|
pi.registerTool(defineTool2({
|
|
1147
1155
|
name: "notes_list",
|
|
1148
1156
|
label: "Notes list",
|
|
1149
|
-
description: `List note files as rows carrying address, updated_at, and stale, most recently updated first. ${ADDRESS_DESCRIPTION} Listings merge your five
|
|
1157
|
+
description: `List note files as rows carrying address, updated_at, and stale, most recently updated first. ${ADDRESS_DESCRIPTION} Listings merge your five prefixes: this session, @project/, @human/, @self/, and @model/.`,
|
|
1150
1158
|
parameters: Type3.Object({ pattern: nullableString(), cursor: cursor(), max_results: positiveInteger() }, { additionalProperties: false }),
|
|
1151
1159
|
async execute(_id, params, _signal, _update, ctx) {
|
|
1152
1160
|
let rows;
|
|
@@ -1166,7 +1174,7 @@ function registerNotesTools(pi) {
|
|
|
1166
1174
|
pi.registerTool(defineTool2({
|
|
1167
1175
|
name: "notes_search",
|
|
1168
1176
|
label: "Notes search",
|
|
1169
|
-
description: `Case-sensitive literal substring search over note bodies; query is one string or several (OR), each matched line appears once. ${ADDRESS_DESCRIPTION} Search merges the same five
|
|
1177
|
+
description: `Case-sensitive literal substring search over note bodies; query is one string or several (OR), each matched line appears once. ${ADDRESS_DESCRIPTION} Search merges the same five prefixes as notes_list. Patterns glob over full address strings. Each file entry carries matches_total, its full match count before capping. Each match carries line, text, offset_chars (a code-point offset into the serialized note returned by notes_read, at the earliest query match), and truncated.`,
|
|
1170
1178
|
parameters: Type3.Object({ query: searchQuery(), pattern: nullableString(), cursor: cursor(), max_matches_per_file: positiveInteger(), max_files: positiveInteger() }, { additionalProperties: false }),
|
|
1171
1179
|
async execute(_id, params, _signal, _update, ctx) {
|
|
1172
1180
|
const queries = searchQueries(params.query);
|
|
@@ -1273,20 +1281,19 @@ function readThresholdSettings(ctx, settingsManager) {
|
|
|
1273
1281
|
// src/context/runtime.ts
|
|
1274
1282
|
import { getCurrentSystemMessage as getCurrentSystemMessage2, Type as Type5 } from "@earendil-works/pi-ai";
|
|
1275
1283
|
import { VERSION, defineTool as defineTool4 } from "@earendil-works/pi-coding-agent";
|
|
1276
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1277
1284
|
|
|
1278
1285
|
// src/context/budget.ts
|
|
1279
1286
|
import { Type as Type4 } from "@earendil-works/pi-ai";
|
|
1280
1287
|
import { defineTool as defineTool3 } from "@earendil-works/pi-coding-agent";
|
|
1281
1288
|
|
|
1282
1289
|
// src/context/prompts.ts
|
|
1283
|
-
function identityBlock(agentName, modelName, firstWindowId, currentWindowId2,
|
|
1290
|
+
function identityBlock(agentName, modelName, firstWindowId, currentWindowId2, previousWindowId2) {
|
|
1284
1291
|
const lines = [
|
|
1285
1292
|
`Agent name: ${agentName} (brain: ${modelName})`,
|
|
1286
1293
|
`First context window id: ${firstWindowId}`,
|
|
1287
1294
|
`Current context window id: ${currentWindowId2}`
|
|
1288
1295
|
];
|
|
1289
|
-
if (
|
|
1296
|
+
if (previousWindowId2) lines.push(`Previous context window id: ${previousWindowId2}`);
|
|
1290
1297
|
return `${CONTEXT_WINDOW_OPEN_TAG}
|
|
1291
1298
|
${lines.join("\n")}
|
|
1292
1299
|
${CONTEXT_WINDOW_CLOSE_TAG}`;
|
|
@@ -1303,8 +1310,7 @@ function rowsFor(snapshot, scope) {
|
|
|
1303
1310
|
function notesUnavailableNotice(snapshot) {
|
|
1304
1311
|
if (snapshot.unavailable.length === 0) return void 0;
|
|
1305
1312
|
const homes = snapshot.unavailable.map((home) => home.label).join(", ");
|
|
1306
|
-
|
|
1307
|
-
return `Notes index incomplete: ${homes} ${noun} unavailable during boot; notes_list can retry after recovery.`;
|
|
1313
|
+
return `Notes index incomplete: index for ${homes} unavailable during boot; notes_list can retry after recovery.`;
|
|
1308
1314
|
}
|
|
1309
1315
|
function notesIndex(snapshot) {
|
|
1310
1316
|
const sections = [];
|
|
@@ -1322,7 +1328,7 @@ function notesIndex(snapshot) {
|
|
|
1322
1328
|
...rowsFor(snapshot, "model").filter((row) => !row.meta.stale && row.path !== "MAP.md").slice(0, POCKET_MODEL_LIMIT)
|
|
1323
1329
|
];
|
|
1324
1330
|
if (recentNotes.length > 0) {
|
|
1325
|
-
const lines = [`You find ${recentNotes.length} crumpled note${recentNotes.length === 1 ? "" : "s"} in your pocket (by
|
|
1331
|
+
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:`];
|
|
1326
1332
|
for (const row of recentNotes) {
|
|
1327
1333
|
lines.push(`- ${row.address} (${row.body.split("\n").length} lines, ${row.sizeBytes} UTF-8 bytes, updated ${relativeTime(row.meta.updated_at, snapshot.openedAt)})`);
|
|
1328
1334
|
}
|
|
@@ -1331,11 +1337,10 @@ function notesIndex(snapshot) {
|
|
|
1331
1337
|
return sections.join("\n\n");
|
|
1332
1338
|
}
|
|
1333
1339
|
function notesHomeBlock() {
|
|
1334
|
-
return "
|
|
1340
|
+
return "Note addresses: bare <vpath> is this session; @project/<vpath> is this project; @human/<vpath> is the human's cross-project notes; @self/<vpath> is your own (current agent); @model/<vpath> is the current model's. @self and @model resolve to who is running now. Any other @ prefix, or @ inside a vpath, is a hard error; there is no fallback across prefixes. Anything not matching these is a plain file \u2014 use the file tools.";
|
|
1335
1341
|
}
|
|
1336
1342
|
function renderBootBlock(data) {
|
|
1337
1343
|
const parts = [];
|
|
1338
|
-
if (data.resetLine) parts.push(RESET_SUMMARY);
|
|
1339
1344
|
parts.push(identityBlock(data.agentName, data.modelName, data.firstWindowId, data.currentWindowId, data.previousWindowId));
|
|
1340
1345
|
parts.push(notesHomeBlock());
|
|
1341
1346
|
const incomplete = notesUnavailableNotice(data.notes);
|
|
@@ -1384,6 +1389,7 @@ function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
1384
1389
|
const invalidateThresholds = () => {
|
|
1385
1390
|
cachedPolicy = void 0;
|
|
1386
1391
|
};
|
|
1392
|
+
const formatRemaining = (remaining) => `${Math.max(0, Math.ceil(remaining / 1e3))}k`;
|
|
1387
1393
|
let pendingGuidance;
|
|
1388
1394
|
let pendingWarning;
|
|
1389
1395
|
let pendingNotices = [];
|
|
@@ -1391,7 +1397,7 @@ function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
1391
1397
|
const windowId = currentWindowId(ctx);
|
|
1392
1398
|
for (const notice of pendingNotices) {
|
|
1393
1399
|
if (notice.windowId !== windowId || !hasWindowMessage(ctx, notice.customType)) continue;
|
|
1394
|
-
ctx.ui.notify(notice.customType === WARNING_TYPE ?
|
|
1400
|
+
ctx.ui.notify(notice.customType === WARNING_TYPE ? `pi-context: Context almost full \u2014 ${formatRemaining(notice.remaining)} remaining` : `pi-context: Context running low \u2014 ${formatRemaining(notice.remaining)} remaining`, "warning");
|
|
1395
1401
|
}
|
|
1396
1402
|
pendingNotices = [];
|
|
1397
1403
|
};
|
|
@@ -1413,7 +1419,7 @@ function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
1413
1419
|
clearStaged();
|
|
1414
1420
|
const windowId = currentWindowId(ctx);
|
|
1415
1421
|
const drafts = staged.filter((draft) => draft !== void 0 && draft.windowId === windowId);
|
|
1416
|
-
pendingNotices = drafts.map(({ windowId: windowId2, customType }) => ({ windowId: windowId2, customType }));
|
|
1422
|
+
pendingNotices = drafts.map(({ windowId: windowId2, customType, remaining }) => ({ windowId: windowId2, customType, remaining }));
|
|
1417
1423
|
return drafts.map((draft) => ({
|
|
1418
1424
|
type: "custom_message",
|
|
1419
1425
|
customType: draft.customType,
|
|
@@ -1428,7 +1434,6 @@ function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
1428
1434
|
pi.on("session_tree", resetForTransition);
|
|
1429
1435
|
pi.on("model_select", resetForTransition);
|
|
1430
1436
|
pi.on("session_shutdown", resetForTransition);
|
|
1431
|
-
pi.on("turn_start", (_event, ctx) => notifyCommittedReminders(ctx));
|
|
1432
1437
|
pi.on("agent_settled", (_event, ctx) => {
|
|
1433
1438
|
notifyCommittedReminders(ctx);
|
|
1434
1439
|
clearStaged();
|
|
@@ -1445,7 +1450,7 @@ function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
1445
1450
|
const content = `${GUIDANCE_OPEN_TAG}
|
|
1446
1451
|
${WARNING_PROMPT}
|
|
1447
1452
|
${GUIDANCE_CLOSE_TAG}`;
|
|
1448
|
-
pendingWarning = { windowId, content };
|
|
1453
|
+
pendingWarning = { windowId, content, remaining };
|
|
1449
1454
|
const warningMessage = {
|
|
1450
1455
|
role: "custom",
|
|
1451
1456
|
customType: WARNING_TYPE,
|
|
@@ -1458,7 +1463,7 @@ ${GUIDANCE_CLOSE_TAG}`;
|
|
|
1458
1463
|
if (hasWindowMessage(ctx, GUIDANCE_TYPE) || pendingGuidance?.windowId === windowId) return void 0;
|
|
1459
1464
|
if (remaining <= reminder) {
|
|
1460
1465
|
const left = Math.max(0, remaining - warning);
|
|
1461
|
-
pendingGuidance = { windowId, content: tokenBudgetGuidance(left) };
|
|
1466
|
+
pendingGuidance = { windowId, content: tokenBudgetGuidance(left), remaining };
|
|
1462
1467
|
}
|
|
1463
1468
|
return void 0;
|
|
1464
1469
|
});
|
|
@@ -1483,31 +1488,6 @@ ${GUIDANCE_CLOSE_TAG}`;
|
|
|
1483
1488
|
};
|
|
1484
1489
|
}
|
|
1485
1490
|
|
|
1486
|
-
// src/notes/notes-snapshot.ts
|
|
1487
|
-
var NOTES_HOMES = [
|
|
1488
|
-
{ scope: "session", label: "this session" },
|
|
1489
|
-
{ scope: "project", label: "@project" },
|
|
1490
|
-
{ scope: "human", label: "@human" },
|
|
1491
|
-
{ scope: "agent", label: "@self" },
|
|
1492
|
-
{ scope: "model", label: "@model" }
|
|
1493
|
-
];
|
|
1494
|
-
function loadNotesSnapshot(ctx, loadHome = (context, scope) => listNotes(context, { scope })) {
|
|
1495
|
-
const openedAt = Date.now();
|
|
1496
|
-
const homes = /* @__PURE__ */ new Map();
|
|
1497
|
-
const unavailable = [];
|
|
1498
|
-
for (const home of NOTES_HOMES) {
|
|
1499
|
-
try {
|
|
1500
|
-
homes.set(home.scope, loadHome(ctx, home.scope));
|
|
1501
|
-
} catch (error) {
|
|
1502
|
-
const code = typeof error === "object" && error !== null ? error.code : void 0;
|
|
1503
|
-
if (typeof code !== "string" || !/^E[A-Z0-9_]+$/.test(code) || code.startsWith("ERR_")) throw error;
|
|
1504
|
-
homes.set(home.scope, []);
|
|
1505
|
-
unavailable.push(home);
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
return { openedAt, homes, unavailable };
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
1491
|
// src/context/reset-lifecycle.ts
|
|
1512
1492
|
import { isContextOverflow, isRecoverableLength } from "@earendil-works/pi-ai";
|
|
1513
1493
|
function isAbort(message, outcome, ctx) {
|
|
@@ -1517,15 +1497,55 @@ function isOverflowLike(message, ctx) {
|
|
|
1517
1497
|
if (message.role !== "assistant") return false;
|
|
1518
1498
|
return isContextOverflow(message, ctx.model?.contextWindow) || ctx.model !== void 0 && isRecoverableLength(message, ctx.model.maxTokens);
|
|
1519
1499
|
}
|
|
1500
|
+
function initialResetControl() {
|
|
1501
|
+
return { request: "none", overflow: "idle" };
|
|
1502
|
+
}
|
|
1503
|
+
function reduceResetControl(state, event) {
|
|
1504
|
+
switch (event.type) {
|
|
1505
|
+
case "request": {
|
|
1506
|
+
if (state.request === "explicit") return { state, effect: "already-requested" };
|
|
1507
|
+
return { state: { ...state, request: "explicit" }, effect: "requested" };
|
|
1508
|
+
}
|
|
1509
|
+
case "turn_end": {
|
|
1510
|
+
const facts = event.facts;
|
|
1511
|
+
const requested = state.request === "explicit";
|
|
1512
|
+
const request = "none";
|
|
1513
|
+
if (facts.aborted) {
|
|
1514
|
+
return { state: { request, overflow: "idle" }, effect: "none" };
|
|
1515
|
+
}
|
|
1516
|
+
if (facts.overflow) {
|
|
1517
|
+
const pending = !facts.queued && facts.enabled && facts.automaticResetEnabled;
|
|
1518
|
+
const spent = state.overflow === "pending-spent" || state.overflow === "spent";
|
|
1519
|
+
const overflow2 = pending ? spent ? "pending-spent" : "pending" : spent ? "spent" : "idle";
|
|
1520
|
+
return { state: { request, overflow: overflow2 }, effect: "none" };
|
|
1521
|
+
}
|
|
1522
|
+
const overflow = facts.failed ? state.overflow : "idle";
|
|
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" };
|
|
1526
|
+
}
|
|
1527
|
+
case "before_settle": {
|
|
1528
|
+
const facts = event.facts;
|
|
1529
|
+
if (state.overflow !== "pending" && state.overflow !== "pending-spent") return { state, effect: "none" };
|
|
1530
|
+
if (facts.queued) return { state, effect: "none" };
|
|
1531
|
+
const spent = state.overflow === "pending-spent";
|
|
1532
|
+
if (!facts.enabled || !facts.automaticResetEnabled || facts.aborted) {
|
|
1533
|
+
return { state: { ...state, overflow: spent ? "spent" : "idle" }, effect: "none" };
|
|
1534
|
+
}
|
|
1535
|
+
return { state: { ...state, overflow: "spent" }, effect: spent ? "none" : "recover-overflow" };
|
|
1536
|
+
}
|
|
1537
|
+
case "settled":
|
|
1538
|
+
return { state: { ...state, overflow: "idle" }, effect: "none" };
|
|
1539
|
+
case "abort":
|
|
1540
|
+
case "clear":
|
|
1541
|
+
return { state: initialResetControl(), effect: "none" };
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1520
1544
|
function registerResetLifecycle(pi, options) {
|
|
1521
|
-
let
|
|
1522
|
-
let
|
|
1523
|
-
let overflowRecoveryUsed = false;
|
|
1524
|
-
let active = true;
|
|
1545
|
+
let sessionActive = true;
|
|
1546
|
+
let control = initialResetControl();
|
|
1525
1547
|
const clear = () => {
|
|
1526
|
-
|
|
1527
|
-
overflowPending = false;
|
|
1528
|
-
overflowRecoveryUsed = false;
|
|
1548
|
+
control = reduceResetControl(control, { type: "clear" }).state;
|
|
1529
1549
|
};
|
|
1530
1550
|
const resetBoundaryResult = (entries, ctx) => {
|
|
1531
1551
|
try {
|
|
@@ -1536,43 +1556,58 @@ function registerResetLifecycle(pi, options) {
|
|
|
1536
1556
|
}
|
|
1537
1557
|
};
|
|
1538
1558
|
pi.on("turn_end", (event, ctx) => {
|
|
1539
|
-
if (!
|
|
1540
|
-
const requested = explicitRequested;
|
|
1541
|
-
explicitRequested = false;
|
|
1559
|
+
if (!sessionActive) return void 0;
|
|
1542
1560
|
const aborted = isAbort(event.message, event.outcome, ctx);
|
|
1543
1561
|
const stagedBudgetEntries = options.budget.consumeTurnEnd(ctx);
|
|
1544
1562
|
const budgetEntries = options.isEnabled() && !aborted ? stagedBudgetEntries : [];
|
|
1545
1563
|
const entries = [...event.entries ?? [], ...budgetEntries];
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
+
const decision = reduceResetControl(control, {
|
|
1565
|
+
type: "turn_end",
|
|
1566
|
+
facts: {
|
|
1567
|
+
aborted,
|
|
1568
|
+
overflow: aborted ? false : isOverflowLike(event.message, ctx),
|
|
1569
|
+
failed: event.outcome === "error",
|
|
1570
|
+
enabled: options.isEnabled(),
|
|
1571
|
+
get queued() {
|
|
1572
|
+
return event.context.pendingMessages.length > 0 || ctx.hasPendingMessages();
|
|
1573
|
+
},
|
|
1574
|
+
get automaticResetEnabled() {
|
|
1575
|
+
return options.budget.automaticResetEnabled(ctx);
|
|
1576
|
+
},
|
|
1577
|
+
get thresholdDue() {
|
|
1578
|
+
return options.budget.resetDue(ctx);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
control = decision.state;
|
|
1583
|
+
if (decision.effect === "commit-boundary") return resetBoundaryResult(entries, ctx);
|
|
1584
|
+
return entries.length > 0 ? { entries } : void 0;
|
|
1564
1585
|
});
|
|
1565
1586
|
pi.on("agent_before_settle", (event, ctx) => {
|
|
1566
|
-
if (!
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1587
|
+
if (!sessionActive) return void 0;
|
|
1588
|
+
const decision = reduceResetControl(control, {
|
|
1589
|
+
type: "before_settle",
|
|
1590
|
+
facts: {
|
|
1591
|
+
get queued() {
|
|
1592
|
+
return event.context.pendingMessages.length > 0 || ctx.hasPendingMessages();
|
|
1593
|
+
},
|
|
1594
|
+
get enabled() {
|
|
1595
|
+
return options.isEnabled();
|
|
1596
|
+
},
|
|
1597
|
+
get automaticResetEnabled() {
|
|
1598
|
+
return options.budget.automaticResetEnabled(ctx);
|
|
1599
|
+
},
|
|
1600
|
+
get aborted() {
|
|
1601
|
+
return event.outcome === "aborted" || ctx.signal?.aborted === true;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
control = decision.state;
|
|
1606
|
+
if (decision.effect !== "recover-overflow") return void 0;
|
|
1572
1607
|
return resetBoundaryResult(event.entries, ctx);
|
|
1573
1608
|
});
|
|
1574
1609
|
pi.on("session_before_compact", (event, ctx) => {
|
|
1575
|
-
if (!
|
|
1610
|
+
if (!sessionActive) return void 0;
|
|
1576
1611
|
if (event.signal.aborted) return { cancel: true };
|
|
1577
1612
|
const markerExists = currentReset(ctx) !== void 0;
|
|
1578
1613
|
if (options.isEnabled() || markerExists) {
|
|
@@ -1587,42 +1622,99 @@ function registerResetLifecycle(pi, options) {
|
|
|
1587
1622
|
if (ctx.signal?.aborted) clear();
|
|
1588
1623
|
});
|
|
1589
1624
|
pi.on("agent_settled", () => {
|
|
1590
|
-
|
|
1591
|
-
overflowRecoveryUsed = false;
|
|
1625
|
+
control = reduceResetControl(control, { type: "settled" }).state;
|
|
1592
1626
|
});
|
|
1593
1627
|
pi.on("session_start", () => {
|
|
1594
1628
|
clear();
|
|
1595
|
-
|
|
1629
|
+
sessionActive = true;
|
|
1596
1630
|
});
|
|
1597
1631
|
pi.on("session_tree", clear);
|
|
1598
1632
|
pi.on("session_shutdown", () => {
|
|
1599
1633
|
clear();
|
|
1600
1634
|
options.budget.clear();
|
|
1601
|
-
|
|
1635
|
+
sessionActive = false;
|
|
1602
1636
|
});
|
|
1603
1637
|
return {
|
|
1604
1638
|
request() {
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
return "rollover_requested";
|
|
1639
|
+
const decision = reduceResetControl(control, { type: "request" });
|
|
1640
|
+
control = decision.state;
|
|
1641
|
+
return decision.effect === "already-requested" ? "rollover_already_pending" : "rollover_requested";
|
|
1608
1642
|
},
|
|
1609
1643
|
clear
|
|
1610
1644
|
};
|
|
1611
1645
|
}
|
|
1612
1646
|
|
|
1613
|
-
// src/context/
|
|
1614
|
-
|
|
1615
|
-
|
|
1647
|
+
// src/context/reset-artifacts.ts
|
|
1648
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1649
|
+
|
|
1650
|
+
// src/notes/notes-snapshot.ts
|
|
1651
|
+
var NOTES_HOMES = [
|
|
1652
|
+
{ scope: "session", label: "this session" },
|
|
1653
|
+
{ scope: "project", label: "@project" },
|
|
1654
|
+
{ scope: "human", label: "@human" },
|
|
1655
|
+
{ scope: "agent", label: "@self" },
|
|
1656
|
+
{ scope: "model", label: "@model" }
|
|
1657
|
+
];
|
|
1658
|
+
function loadNotesSnapshot(ctx, loadHome = (context, scope) => listNotes(context, { scope })) {
|
|
1659
|
+
const openedAt = Date.now();
|
|
1660
|
+
const homes = /* @__PURE__ */ new Map();
|
|
1661
|
+
const unavailable = [];
|
|
1662
|
+
for (const home of NOTES_HOMES) {
|
|
1663
|
+
try {
|
|
1664
|
+
homes.set(home.scope, loadHome(ctx, home.scope));
|
|
1665
|
+
} catch (error) {
|
|
1666
|
+
const code = typeof error === "object" && error !== null ? error.code : void 0;
|
|
1667
|
+
if (typeof code !== "string" || !/^E[A-Z0-9_]+$/.test(code) || code.startsWith("ERR_")) throw error;
|
|
1668
|
+
homes.set(home.scope, []);
|
|
1669
|
+
unavailable.push(home);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
return { openedAt, homes, unavailable };
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// src/context/boot.ts
|
|
1676
|
+
function bootContent(ctx, currentId, previousId, notes) {
|
|
1616
1677
|
return renderBootBlock({
|
|
1617
1678
|
agentName: agentSlug(ctx),
|
|
1618
1679
|
modelName: modelSlug(ctx),
|
|
1619
1680
|
firstWindowId: rootWindowId(ctx.sessionManager.getSessionId()),
|
|
1620
1681
|
currentWindowId: currentId,
|
|
1621
1682
|
previousWindowId: previousId,
|
|
1622
|
-
resetLine,
|
|
1623
1683
|
notes
|
|
1624
1684
|
});
|
|
1625
1685
|
}
|
|
1686
|
+
function buildBootMessage(ctx, windowId, previousId, notifyIncompleteNotes) {
|
|
1687
|
+
const notes = loadNotesSnapshot(ctx);
|
|
1688
|
+
notifyIncompleteNotes?.(ctx, windowId, notes);
|
|
1689
|
+
return { customType: BOOT_TYPE, content: bootContent(ctx, windowId, previousId, notes), display: false, details: { windowId } };
|
|
1690
|
+
}
|
|
1691
|
+
function sendBoot(pi, boot) {
|
|
1692
|
+
pi.sendMessage(
|
|
1693
|
+
{ customType: boot.customType, content: boot.content, display: boot.display, details: boot.details },
|
|
1694
|
+
{ triggerTurn: false }
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
function ensureBoot(pi, ctx, notifyIncompleteNotes) {
|
|
1698
|
+
const reset = currentReset(ctx);
|
|
1699
|
+
if (reset) {
|
|
1700
|
+
repairResetTail(pi, ctx, reset, notifyIncompleteNotes);
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
const windowId = rootWindowId(ctx.sessionManager.getSessionId());
|
|
1704
|
+
if (ctx.sessionManager.buildSessionProjection().messages.some((message) => isWindowBoot(message, windowId))) return;
|
|
1705
|
+
sendBoot(pi, buildBootMessage(ctx, windowId, void 0, notifyIncompleteNotes));
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// src/context/reset-artifacts.ts
|
|
1709
|
+
function isWindowContinuationEntry(entry) {
|
|
1710
|
+
return entry.type === "custom_message" && entry.customType === CONTINUATION_TYPE;
|
|
1711
|
+
}
|
|
1712
|
+
function sendContinuation(pi) {
|
|
1713
|
+
pi.sendMessage(
|
|
1714
|
+
{ customType: CONTINUATION_TYPE, content: CONTINUATION, display: false },
|
|
1715
|
+
{ triggerTurn: false }
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1626
1718
|
function buildResetDrafts(ctx, notifyIncompleteNotes) {
|
|
1627
1719
|
const sessionPrefix = ctx.sessionManager.getSessionId().slice(0, 8);
|
|
1628
1720
|
const usedWindowIds = new Set(
|
|
@@ -1632,65 +1724,62 @@ function buildResetDrafts(ctx, notifyIncompleteNotes) {
|
|
|
1632
1724
|
do {
|
|
1633
1725
|
windowId = `pcw:${sessionPrefix}:${randomUUID2().slice(0, 8)}`;
|
|
1634
1726
|
} while (usedWindowIds.has(windowId));
|
|
1635
|
-
const
|
|
1636
|
-
notifyIncompleteNotes?.(ctx, windowId, notes);
|
|
1727
|
+
const boot = buildBootMessage(ctx, windowId, currentWindowId(ctx), notifyIncompleteNotes);
|
|
1637
1728
|
return [
|
|
1638
1729
|
{ type: "custom", customType: RESET_MARKER_TYPE, data: { windowId } },
|
|
1639
|
-
{
|
|
1640
|
-
|
|
1641
|
-
customType: BOOT_TYPE,
|
|
1642
|
-
content: bootContent(ctx, windowId, currentWindowId(ctx), true, notes),
|
|
1643
|
-
display: false,
|
|
1644
|
-
details: { windowId }
|
|
1645
|
-
},
|
|
1646
|
-
{
|
|
1647
|
-
type: "custom_message",
|
|
1648
|
-
customType: CONTINUATION_TYPE,
|
|
1649
|
-
content: CONTINUATION,
|
|
1650
|
-
display: false
|
|
1651
|
-
}
|
|
1730
|
+
{ type: "custom_message", customType: BOOT_TYPE, content: boot.content, display: false, details: { windowId } },
|
|
1731
|
+
{ type: "custom_message", customType: CONTINUATION_TYPE, content: CONTINUATION, display: false }
|
|
1652
1732
|
];
|
|
1653
1733
|
}
|
|
1654
|
-
function ensureBoot(pi, ctx, notifyIncompleteNotes) {
|
|
1655
|
-
const reset = currentReset(ctx);
|
|
1656
|
-
const sessionId2 = ctx.sessionManager.getSessionId();
|
|
1657
|
-
const windowId = reset?.data?.windowId ?? rootWindowId(sessionId2);
|
|
1658
|
-
if (ctx.sessionManager.buildSessionProjection().messages.some((message) => isWindowBoot(message, windowId))) return;
|
|
1659
|
-
if (reset && !resetBootMayBeRepaired(ctx, reset.id, windowId)) return;
|
|
1660
|
-
let previousId = reset ? rootWindowId(sessionId2) : void 0;
|
|
1661
|
-
if (reset) {
|
|
1662
|
-
for (const entry of ctx.sessionManager.getBranch()) {
|
|
1663
|
-
if (entry.id === reset.id) break;
|
|
1664
|
-
if (isWindowMarker(entry)) previousId = entry.data.windowId;
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
const notes = loadNotesSnapshot(ctx);
|
|
1668
|
-
notifyIncompleteNotes?.(ctx, windowId, notes);
|
|
1669
|
-
pi.sendMessage(
|
|
1670
|
-
{ customType: BOOT_TYPE, content: bootContent(ctx, windowId, previousId, reset !== void 0, notes), display: false, details: { windowId } },
|
|
1671
|
-
{ triggerTurn: false }
|
|
1672
|
-
);
|
|
1673
|
-
}
|
|
1674
1734
|
function persistManualReset(pi, ctx, notifyIncompleteNotes) {
|
|
1675
|
-
const [marker, boot] = buildResetDrafts(ctx, notifyIncompleteNotes);
|
|
1735
|
+
const [marker, boot, continuation] = buildResetDrafts(ctx, notifyIncompleteNotes);
|
|
1676
1736
|
pi.appendEntry(marker.customType, marker.data);
|
|
1677
1737
|
pi.sendMessage(
|
|
1678
1738
|
{ customType: boot.customType, content: boot.content, display: boot.display, details: boot.details },
|
|
1679
1739
|
{ triggerTurn: false }
|
|
1680
1740
|
);
|
|
1741
|
+
pi.sendMessage(
|
|
1742
|
+
{ customType: continuation.customType, content: continuation.content, display: continuation.display },
|
|
1743
|
+
{ triggerTurn: false }
|
|
1744
|
+
);
|
|
1681
1745
|
return boot.details.windowId;
|
|
1682
1746
|
}
|
|
1683
|
-
function
|
|
1747
|
+
function inspectResetTail(ctx, markerId, windowId) {
|
|
1684
1748
|
const branch = ctx.sessionManager.getBranch();
|
|
1685
1749
|
const markerIndex = branch.findIndex((entry) => entry.id === markerId);
|
|
1686
|
-
if (markerIndex < 0) return
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1750
|
+
if (markerIndex < 0) return void 0;
|
|
1751
|
+
let boot = false;
|
|
1752
|
+
let continuation = false;
|
|
1753
|
+
for (const entry of branch.slice(markerIndex + 1)) {
|
|
1754
|
+
if (isWindowBootEntry(entry, windowId)) {
|
|
1755
|
+
if (boot || continuation) return void 0;
|
|
1756
|
+
boot = true;
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
if (isWindowContinuationEntry(entry)) {
|
|
1760
|
+
if (continuation || !boot) return void 0;
|
|
1761
|
+
continuation = true;
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
if (isWindowMarker(entry) || entry.type === "message" || entry.type === "custom_message" || entry.type === "compaction" || entry.type === "branch_summary") {
|
|
1765
|
+
return void 0;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
return { boot, continuation };
|
|
1690
1769
|
}
|
|
1691
|
-
function
|
|
1692
|
-
|
|
1770
|
+
function resetTailCommitted(ctx, markerId, windowId) {
|
|
1771
|
+
const tail = inspectResetTail(ctx, markerId, windowId);
|
|
1772
|
+
return tail?.boot === true && tail.continuation === true;
|
|
1693
1773
|
}
|
|
1774
|
+
function repairResetTail(pi, ctx, marker, notifyIncompleteNotes) {
|
|
1775
|
+
const tail = inspectResetTail(ctx, marker.id, marker.data.windowId);
|
|
1776
|
+
if (!tail || tail.boot && tail.continuation) return;
|
|
1777
|
+
if (!tail.boot) sendBoot(pi, buildBootMessage(ctx, marker.data.windowId, previousWindowId(ctx, marker.id), notifyIncompleteNotes));
|
|
1778
|
+
if (!tail.continuation) sendContinuation(pi);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// src/context/runtime.ts
|
|
1782
|
+
var buildLabel = typeof define_PI_CONTEXT_BUILD_default === "undefined" ? "unbundled source (build unknown)" : `${define_PI_CONTEXT_BUILD_default.version} \xB7 build ${define_PI_CONTEXT_BUILD_default.sourceHash.slice(0, 12)}`;
|
|
1694
1783
|
function branchHasWindowMarker(ctx, fromId) {
|
|
1695
1784
|
return ctx.sessionManager.getBranch(fromId).some((entry) => isWindowMarker(entry));
|
|
1696
1785
|
}
|
|
@@ -1704,7 +1793,8 @@ function registerContext(pi, settingsManager) {
|
|
|
1704
1793
|
if (pendingResetNotices.size === 0) return;
|
|
1705
1794
|
const branch = ctx.sessionManager.getBranch();
|
|
1706
1795
|
for (const windowId of pendingResetNotices) {
|
|
1707
|
-
|
|
1796
|
+
const marker = branch.find((entry) => isWindowMarker(entry) && entry.data.windowId === windowId);
|
|
1797
|
+
if (!marker || !resetTailCommitted(ctx, marker.id, windowId)) continue;
|
|
1708
1798
|
pendingResetNotices.delete(windowId);
|
|
1709
1799
|
ctx.ui.notify(`pi-context: memory cleared \xB7 ${windowId}`, "info");
|
|
1710
1800
|
}
|
|
@@ -1851,7 +1941,7 @@ function createPiContext(options = {}) {
|
|
|
1851
1941
|
function piContext(pi) {
|
|
1852
1942
|
registerPiContext(pi);
|
|
1853
1943
|
}
|
|
1854
|
-
var internal = { MAX_NOTE_BYTES, NOTE_TYPE, BOOT_TYPE, GUIDANCE_TYPE, WARNING_TYPE, CONTINUATION_TYPE, WARNING_PROMPT, WARNING_RUNWAY_TOKENS, RESET_MARKER_TYPE,
|
|
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 };
|
|
1855
1945
|
export {
|
|
1856
1946
|
createPiContext,
|
|
1857
1947
|
piContext as default,
|