@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.
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "0.25.0",
3
- "sourceHash": "8a2495924871c544d2919a9301b6017c61ebe0c4bc38242b7e8c5b95d50f8df3"
2
+ "version": "0.25.2",
3
+ "sourceHash": "e240c1946d0f2b6d7b1d83ed658c348c7daed5fce5a374844215f1eebddcb25f"
4
4
  }
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.0", sourceHash: "8a2495924871c544d2919a9301b6017c61ebe0c4bc38242b7e8c5b95d50f8df3" };
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 RESET_SUMMARY = "You wake up. Your head is empty \u2014 no memories, the past a blank. The memory is gone for good. What outlived it: the notes you wrote, and the history that was recorded. They are not your memory \u2014 read them to rebuild what you need.";
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
- If <context_window> lists a Previous context window id, a reset just happened and the old conversation is not included. Read your note checkpoint first, then recover details through history_*: history_read directly when you know the window and item IDs, history_list or history_search to find them when you don't.
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/, @agents/<name>/, @model/, and @models/<name>/; bare names are the session home";
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 home, `@self/<vpath>` / `@agents/<name>/<vpath>` for agent homes, and `@model/<vpath>` / `@models/<name>/<vpath>` for model homes. `@self` and `@model` mean the current agent/model; the `<name>` forms name one absolutely. The word after `@` is always one of the reserved home names \u2014 names live at the second level, never `@faye/`. `@` means leaving home. Any other `@` prefix, or `@` inside a vpath, is a hard error. There is no cross-home fallback. Paths reject `..`, absolute paths, and backslashes. Homes you do not own (`@agents/<other>/`, `@models/<other>/`) are read-only.";
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 reachable homes: this session, @project/, @human/, your @self home, and the current @model home; other agents and models appear only under an explicit glob (@agents/<name>/**, @models/<name>/**, or a glob in the name segment to scan a whole namespace).`,
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 reachable homes as notes_list; explicit globs reach other agents and models. 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.`,
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, previousWindowId) {
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 (previousWindowId) lines.push(`Previous context window id: ${previousWindowId}`);
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
- const noun = snapshot.unavailable.length === 1 ? "home's index was" : "home indexes were";
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 home, most recent first within each: up to ${POCKET_SESSION_LIMIT} from this session, ${POCKET_PROJECT_LIMIT} from this project, ${POCKET_HUMAN_LIMIT} from @human, ${POCKET_AGENT_LIMIT} from your @self home, ${POCKET_MODEL_LIMIT} from the current @model home). A note's content never appears here, so its name has to say what the note is about:`];
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 "Notes_* addresses have five homes: bare <vpath> is this session, @project/<vpath> is this project, @human/<vpath> is the human's cross-project home, @self/<vpath> and @agents/<name>/<vpath> are agent homes (current vs named), and @model/<vpath> and @models/<name>/<vpath> are model homes. @self and @model resolve to who is running now; listings always show resolved names. @ means leaving home; there is no cross-home fallback. Anything else after @ \u2014 or @ inside a vpath \u2014 is a hard error. Any other note is a plain file \u2014 use the file tools.";
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 ? "pi-context: context budget critical \u2014 final checkpoint warning recorded for the model." : "pi-context: context budget low \u2014 checkpoint reminder recorded for the model, kept out of the chat view.", "warning");
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 explicitRequested = false;
1522
- let overflowPending = false;
1523
- let overflowRecoveryUsed = false;
1524
- let active = true;
1545
+ let sessionActive = true;
1546
+ let control = initialResetControl();
1525
1547
  const clear = () => {
1526
- explicitRequested = false;
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 (!active) return void 0;
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
- if (aborted) {
1547
- overflowPending = false;
1548
- overflowRecoveryUsed = false;
1549
- return entries.length > 0 ? { entries } : void 0;
1550
- }
1551
- if (isOverflowLike(event.message, ctx)) {
1552
- const queued = event.context.pendingMessages.length > 0 || ctx.hasPendingMessages();
1553
- overflowPending = !queued && options.isEnabled() && options.budget.automaticResetEnabled(ctx);
1554
- return entries.length > 0 ? { entries } : void 0;
1555
- }
1556
- if (event.outcome !== "error") {
1557
- overflowPending = false;
1558
- overflowRecoveryUsed = false;
1559
- }
1560
- if (!options.isEnabled() || event.outcome === "error") return entries.length > 0 ? { entries } : void 0;
1561
- const autoThreshold = options.budget.resetDue(ctx);
1562
- if (!requested && !autoThreshold) return entries.length > 0 ? { entries } : void 0;
1563
- return resetBoundaryResult(entries, ctx);
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 (!active || !overflowPending) return void 0;
1567
- if (event.context.pendingMessages.length > 0 || ctx.hasPendingMessages()) return void 0;
1568
- overflowPending = false;
1569
- if (!options.isEnabled() || !options.budget.automaticResetEnabled(ctx) || event.outcome === "aborted" || ctx.signal?.aborted) return void 0;
1570
- if (overflowRecoveryUsed) return void 0;
1571
- overflowRecoveryUsed = true;
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 (!active) return void 0;
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
- overflowPending = false;
1591
- overflowRecoveryUsed = false;
1625
+ control = reduceResetControl(control, { type: "settled" }).state;
1592
1626
  });
1593
1627
  pi.on("session_start", () => {
1594
1628
  clear();
1595
- active = true;
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
- active = false;
1635
+ sessionActive = false;
1602
1636
  });
1603
1637
  return {
1604
1638
  request() {
1605
- if (explicitRequested) return "rollover_already_pending";
1606
- explicitRequested = true;
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/runtime.ts
1614
- 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)}`;
1615
- function bootContent(ctx, currentId, previousId, resetLine, notes) {
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 notes = loadNotesSnapshot(ctx);
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
- type: "custom_message",
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 resetBootMayBeRepaired(ctx, markerId, windowId) {
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 false;
1687
- const afterMarker = branch.slice(markerIndex + 1);
1688
- if (afterMarker.some((entry) => isWindowBootEntry(entry, windowId))) return false;
1689
- return !afterMarker.some((entry) => entry.type === "message" || entry.type === "custom_message" || entry.type === "compaction" || entry.type === "branch_summary");
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 isWindowBootEntry(entry, windowId) {
1692
- return entry.type === "custom_message" && entry.customType === BOOT_TYPE && typeof entry.details === "object" && entry.details !== null && typeof entry.details.windowId === "string" && entry.details.windowId === windowId;
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
- if (!branch.some((entry) => isWindowMarker(entry) && entry.data.windowId === windowId) || !branch.some((entry) => isWindowBootEntry(entry, windowId))) continue;
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, RESET_SUMMARY, 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 };
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,