@astrosheep/pi-context 0.25.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +88 -7
  2. package/dist/build-info.json +2 -2
  3. package/dist/extension.js +616 -370
  4. package/dist/src/context/boot.d.ts +24 -0
  5. package/dist/src/context/boot.js +33 -24
  6. package/dist/src/context/budget.d.ts +9 -0
  7. package/dist/src/context/budget.js +19 -15
  8. package/dist/src/context/context-window.d.ts +41 -0
  9. package/dist/src/context/context-window.js +16 -1
  10. package/dist/src/context/prompts.d.ts +20 -0
  11. package/dist/src/context/prompts.js +1 -1
  12. package/dist/src/context/reset-artifacts.d.ts +26 -0
  13. package/dist/src/context/reset-artifacts.js +18 -17
  14. package/dist/src/context/reset-lifecycle.d.ts +89 -0
  15. package/dist/src/context/reset-lifecycle.js +103 -75
  16. package/dist/src/context/runtime.d.ts +3 -0
  17. package/dist/src/context/runtime.js +53 -21
  18. package/dist/src/context/thresholds.d.ts +33 -0
  19. package/dist/src/context/thresholds.js +1 -1
  20. package/dist/src/dream/cli.d.ts +10 -0
  21. package/dist/src/dream/cli.js +1 -1
  22. package/dist/src/dream/doctor.d.ts +2 -0
  23. package/dist/src/dream/doctor.js +6 -2
  24. package/dist/src/dream/gates.d.ts +10 -0
  25. package/dist/src/dream/git.d.ts +21 -0
  26. package/dist/src/dream/lock.d.ts +31 -0
  27. package/dist/src/dream/runner.d.ts +30 -0
  28. package/dist/src/dream/settings.d.ts +16 -0
  29. package/dist/src/history/history-tools.d.ts +2 -0
  30. package/dist/src/history/history.d.ts +57 -0
  31. package/dist/src/index.d.ts +39 -0
  32. package/dist/src/index.js +4 -4
  33. package/dist/src/notes/address.d.ts +26 -0
  34. package/dist/src/notes/address.js +8 -14
  35. package/dist/src/notes/constants.d.ts +3 -0
  36. package/dist/src/notes/constants.js +3 -0
  37. package/dist/src/notes/context.d.ts +10 -0
  38. package/dist/src/notes/context.js +33 -0
  39. package/dist/src/notes/frontmatter.d.ts +46 -0
  40. package/dist/src/notes/frontmatter.js +10 -5
  41. package/dist/src/notes/index.d.ts +4 -0
  42. package/dist/src/notes/index.js +2 -0
  43. package/dist/src/notes/paths.d.ts +21 -0
  44. package/dist/src/notes/paths.js +72 -76
  45. package/dist/src/notes/store.d.ts +94 -0
  46. package/dist/src/notes/store.js +298 -242
  47. package/dist/src/pi/notes/adapter.d.ts +12 -0
  48. package/dist/src/pi/notes/adapter.js +39 -0
  49. package/dist/src/pi/notes/session-replay.d.ts +16 -0
  50. package/dist/src/{notes → pi/notes}/session-replay.js +2 -2
  51. package/dist/src/pi/notes/snapshot.d.ts +33 -0
  52. package/dist/src/{notes/notes-snapshot.js → pi/notes/snapshot.js} +11 -3
  53. package/dist/src/pi/notes/tools.d.ts +2 -0
  54. package/dist/src/{notes → pi/notes}/tools.js +24 -21
  55. package/dist/src/protocol.d.ts +41 -0
  56. package/dist/src/protocol.js +4 -6
  57. package/dist/src/session-reader.d.ts +5 -0
  58. package/dist/src/settings.d.ts +6 -0
  59. package/dist/src/tool-output.d.ts +101 -0
  60. package/dist/src/tool-schema.d.ts +17 -0
  61. package/dist/test/agent-loop.test.d.ts +1 -0
  62. package/dist/test/agent-loop.test.js +318 -19
  63. package/dist/test/boot.integration.test.d.ts +1 -0
  64. package/dist/test/boot.integration.test.js +55 -29
  65. package/dist/test/budget-settings.integration.test.d.ts +1 -0
  66. package/dist/test/budget-settings.integration.test.js +8 -7
  67. package/dist/test/doctor.test.d.ts +1 -0
  68. package/dist/test/doctor.test.js +10 -2
  69. package/dist/test/dream-skill.test.d.ts +1 -0
  70. package/dist/test/dream-skill.test.js +69 -0
  71. package/dist/test/dream.test.d.ts +1 -0
  72. package/dist/test/helpers/extension.d.ts +115 -0
  73. package/dist/test/helpers/extension.js +6 -6
  74. package/dist/test/helpers/notes.d.ts +6 -0
  75. package/dist/test/helpers/notes.js +13 -0
  76. package/dist/test/history.integration.test.d.ts +1 -0
  77. package/dist/test/notes-library.test.d.ts +1 -0
  78. package/dist/test/notes-library.test.js +111 -0
  79. package/dist/test/notes.integration.test.d.ts +1 -0
  80. package/dist/test/notes.integration.test.js +22 -24
  81. package/dist/test/notes.test.d.ts +1 -0
  82. package/dist/test/notes.test.js +137 -7
  83. package/dist/test/reset-lifecycle.test.d.ts +1 -0
  84. package/dist/test/reset-lifecycle.test.js +142 -85
  85. package/docs/architecture.md +8 -8
  86. package/docs/reset-lifecycle.md +63 -79
  87. package/package.json +35 -2
  88. package/playbook.md +33 -32
  89. package/skills/dream/SKILL.md +12 -0
  90. package/src/context/boot.ts +44 -25
  91. package/src/context/budget.ts +25 -17
  92. package/src/context/context-window.ts +16 -1
  93. package/src/context/prompts.ts +2 -2
  94. package/src/context/reset-artifacts.ts +26 -24
  95. package/src/context/reset-lifecycle.ts +117 -111
  96. package/src/context/runtime.ts +50 -22
  97. package/src/context/thresholds.ts +1 -1
  98. package/src/dream/cli.ts +1 -1
  99. package/src/dream/doctor.ts +5 -2
  100. package/src/index.ts +4 -4
  101. package/src/notes/address.ts +9 -15
  102. package/src/notes/constants.ts +3 -0
  103. package/src/notes/context.ts +40 -0
  104. package/src/notes/frontmatter.ts +18 -12
  105. package/src/notes/index.ts +22 -0
  106. package/src/notes/paths.ts +64 -78
  107. package/src/notes/store.ts +308 -244
  108. package/src/pi/notes/adapter.ts +44 -0
  109. package/src/{notes → pi/notes}/session-replay.ts +3 -3
  110. package/src/{notes/notes-snapshot.ts → pi/notes/snapshot.ts} +13 -4
  111. package/src/{notes → pi/notes}/tools.ts +25 -23
  112. package/src/protocol.ts +5 -6
@@ -0,0 +1,24 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { type NotesSnapshot } from "../pi/notes/snapshot.js";
3
+ export type IncompleteNotesNotifier = (ctx: ExtensionContext, windowId: string, snapshot: NotesSnapshot) => void;
4
+ /** The boot custom message: identity, notes snapshot, and static protocol; never reset prose. */
5
+ export type BootMessage = {
6
+ readonly customType: string;
7
+ readonly content: string;
8
+ readonly display: false;
9
+ readonly details: {
10
+ windowId: string;
11
+ };
12
+ };
13
+ /**
14
+ * Acquire one notes snapshot and build the boot custom message for a window. All identity
15
+ * fields are captured synchronously before the first filesystem await.
16
+ */
17
+ export declare function buildBootMessage(ctx: ExtensionContext, windowId: string, previousId: string | undefined, notifyIncompleteNotes?: IncompleteNotesNotifier, isCurrent?: () => boolean): Promise<BootMessage>;
18
+ /** Persist one hidden boot message without triggering a model turn. */
19
+ export declare function sendBoot(pi: ExtensionAPI, boot: BootMessage): void;
20
+ /**
21
+ * Boot entry point for `session_start` / `session_tree`. Ordinary startup ensures one root
22
+ * boot; a reset marker instead asks reset-artifact repair to complete its persisted tail.
23
+ */
24
+ export declare function ensureBoot(pi: ExtensionAPI, ctx: ExtensionContext, notifyIncompleteNotes?: IncompleteNotesNotifier, isCurrent?: () => boolean): Promise<void>;
@@ -1,28 +1,28 @@
1
- import { loadNotesSnapshot } from "../notes/notes-snapshot.js";
2
- import { agentSlug, modelSlug } from "../notes/paths.js";
1
+ import { notesContextFromPi } from "../pi/notes/adapter.js";
2
+ import { loadNotesSnapshot } from "../pi/notes/snapshot.js";
3
3
  import { BOOT_TYPE } from "../protocol.js";
4
4
  import { renderBootBlock } from "./prompts.js";
5
- import { currentReset, isWindowBoot, rootWindowId } from "./context-window.js";
5
+ import { currentReset, currentWindowId, isWindowBoot, rootWindowId } from "./context-window.js";
6
6
  import { repairResetTail } from "./reset-artifacts.js";
7
- /** Render the boot block from the live context; acquisition stays with loadNotesSnapshot. */
8
- function bootContent(ctx, currentId, previousId, notes) {
9
- return renderBootBlock({
10
- agentName: agentSlug(ctx),
11
- modelName: modelSlug(ctx),
12
- firstWindowId: rootWindowId(ctx.sessionManager.getSessionId()),
13
- currentWindowId: currentId,
14
- previousWindowId: previousId,
15
- notes,
16
- });
7
+ /** Render the boot block from one pre-await identity and acquired notes snapshot. */
8
+ function bootContent(agentName, modelName, firstWindowId, currentId, previousId, notes) {
9
+ return renderBootBlock({ agentName, modelName, firstWindowId, currentWindowId: currentId, previousWindowId: previousId, notes });
17
10
  }
18
11
  /**
19
- * Acquire one notes snapshot and build the boot custom message for a window. The caller
20
- * supplies `previousId` only when a reset boundary needs the prior window identity.
12
+ * Acquire one notes snapshot and build the boot custom message for a window. All identity
13
+ * fields are captured synchronously before the first filesystem await.
21
14
  */
22
- export function buildBootMessage(ctx, windowId, previousId, notifyIncompleteNotes) {
23
- const notes = loadNotesSnapshot(ctx);
24
- notifyIncompleteNotes?.(ctx, windowId, notes);
25
- return { customType: BOOT_TYPE, content: bootContent(ctx, windowId, previousId, notes), display: false, details: { windowId } };
15
+ export async function buildBootMessage(ctx, windowId, previousId, notifyIncompleteNotes, isCurrent = () => true) {
16
+ const identity = notesContextFromPi(ctx);
17
+ const notes = await loadNotesSnapshot(ctx, undefined, identity);
18
+ if (isCurrent())
19
+ notifyIncompleteNotes?.(ctx, windowId, notes);
20
+ return {
21
+ customType: BOOT_TYPE,
22
+ content: bootContent(identity.agent, identity.model, rootWindowId(identity.sessionId), windowId, previousId, notes),
23
+ display: false,
24
+ details: { windowId },
25
+ };
26
26
  }
27
27
  /** Persist one hidden boot message without triggering a model turn. */
28
28
  export function sendBoot(pi, boot) {
@@ -31,16 +31,25 @@ export function sendBoot(pi, boot) {
31
31
  /**
32
32
  * Boot entry point for `session_start` / `session_tree`. Ordinary startup ensures one root
33
33
  * boot; a reset marker instead asks reset-artifact repair to complete its persisted tail.
34
- * Boot idempotence lives here: an already-projected root boot or a complete reset tail emits nothing.
35
34
  */
36
- export function ensureBoot(pi, ctx, notifyIncompleteNotes) {
35
+ export async function ensureBoot(pi, ctx, notifyIncompleteNotes, isCurrent = () => true) {
36
+ const sessionId = ctx.sessionManager.getSessionId();
37
37
  const reset = currentReset(ctx);
38
+ const windowId = reset?.data.windowId ?? rootWindowId(sessionId);
39
+ const stillCurrent = () => isCurrent() &&
40
+ ctx.signal?.aborted !== true &&
41
+ ctx.sessionManager.getSessionId() === sessionId &&
42
+ currentWindowId(ctx) === windowId;
38
43
  if (reset) {
39
- repairResetTail(pi, ctx, reset, notifyIncompleteNotes);
44
+ await repairResetTail(pi, ctx, reset, notifyIncompleteNotes, stillCurrent);
40
45
  return;
41
46
  }
42
- const windowId = rootWindowId(ctx.sessionManager.getSessionId());
43
47
  if (ctx.sessionManager.buildSessionProjection().messages.some((message) => isWindowBoot(message, windowId)))
44
48
  return;
45
- sendBoot(pi, buildBootMessage(ctx, windowId, undefined, notifyIncompleteNotes));
49
+ const boot = await buildBootMessage(ctx, windowId, undefined, notifyIncompleteNotes, stillCurrent);
50
+ if (!stillCurrent())
51
+ return;
52
+ if (ctx.sessionManager.buildSessionProjection().messages.some((message) => isWindowBoot(message, windowId)))
53
+ return;
54
+ sendBoot(pi, boot);
46
55
  }
@@ -0,0 +1,9 @@
1
+ import { type ExtensionAPI, type ExtensionContext, type SessionBoundaryDraft, type SettingsManager } from "@earendil-works/pi-coding-agent";
2
+ /** Remaining tokens in the provider's active window, or null without a usable estimate. */
3
+ export declare function remainingTokens(ctx: Pick<ExtensionContext, "sessionManager" | "getContextUsage" | "model">): number | null;
4
+ export declare function registerBudget(pi: ExtensionAPI, isEnabled: () => boolean, settingsManager?: SettingsManager, onCloseOut?: (windowId: string) => void): {
5
+ automaticResetEnabled: (ctx: ExtensionContext) => boolean;
6
+ hardReserveDue: (ctx: ExtensionContext) => boolean;
7
+ consumeTurnEnd: (ctx: ExtensionContext) => SessionBoundaryDraft[];
8
+ clear: () => void;
9
+ };
@@ -1,6 +1,6 @@
1
1
  import { Type } from "@earendil-works/pi-ai";
2
2
  import { defineTool } from "@earendil-works/pi-coding-agent";
3
- import { GUIDANCE_CLOSE_TAG, GUIDANCE_OPEN_TAG, GUIDANCE_TYPE, WARNING_PROMPT, WARNING_TYPE } from "../protocol.js";
3
+ import { GUIDANCE_TYPE, WARNING_CONTENT, WARNING_TYPE } from "../protocol.js";
4
4
  import { readThresholdSettings } from "./thresholds.js";
5
5
  import { currentWindowId, hasWindowMessage, windowUsage } from "./context-window.js";
6
6
  import { tokenBudgetGuidance } from "./prompts.js";
@@ -10,7 +10,7 @@ export function remainingTokens(ctx) {
10
10
  const usage = windowUsage(ctx);
11
11
  return !usage || usage.tokens === null ? null : Math.max(0, usage.contextWindow - usage.tokens);
12
12
  }
13
- export function registerBudget(pi, isEnabled, settingsManager) {
13
+ export function registerBudget(pi, isEnabled, settingsManager, onCloseOut = () => { }) {
14
14
  let cachedPolicy;
15
15
  const notifiedWarnings = new Set();
16
16
  const resolvePolicy = (ctx) => {
@@ -33,7 +33,7 @@ export function registerBudget(pi, isEnabled, settingsManager) {
33
33
  const automaticResetEnabled = (ctx) => {
34
34
  return resolvePolicy(ctx).automatic;
35
35
  };
36
- const resetDue = (ctx) => {
36
+ const hardReserveDue = (ctx) => {
37
37
  if (!automaticResetEnabled(ctx))
38
38
  return false;
39
39
  const usage = windowUsage(ctx);
@@ -49,8 +49,8 @@ export function registerBudget(pi, isEnabled, settingsManager) {
49
49
  if (notice.windowId !== windowId || !hasWindowMessage(ctx, notice.customType))
50
50
  continue;
51
51
  ctx.ui.notify(notice.customType === WARNING_TYPE
52
- ? "pi-context: context budget critical final checkpoint warning recorded for the model."
53
- : "pi-context: context budget low checkpoint reminder recorded for the model, kept out of the chat view.", "warning");
52
+ ? "pi-context: Context almost full; close out the current memory window."
53
+ : "pi-context: Context running low; checkpoint your notes soon.", "warning");
54
54
  }
55
55
  pendingNotices = [];
56
56
  };
@@ -72,7 +72,7 @@ export function registerBudget(pi, isEnabled, settingsManager) {
72
72
  clearStaged();
73
73
  const windowId = currentWindowId(ctx);
74
74
  const drafts = staged.filter((draft) => draft !== undefined && draft.windowId === windowId);
75
- pendingNotices = drafts.map(({ windowId, customType }) => ({ windowId, customType }));
75
+ pendingNotices = drafts.map(({ windowId, customType, remaining }) => ({ windowId, customType, remaining }));
76
76
  return drafts.map((draft) => ({
77
77
  type: "custom_message",
78
78
  customType: draft.customType,
@@ -88,7 +88,6 @@ export function registerBudget(pi, isEnabled, settingsManager) {
88
88
  // lifecycle point that must discard an uncommitted draft before the next prompt.
89
89
  // UI notices follow committed reminders. Aborted requests can retry their drafts
90
90
  // without showing the same low-budget notification twice.
91
- pi.on("turn_start", (_event, ctx) => notifyCommittedReminders(ctx));
92
91
  pi.on("agent_settled", (_event, ctx) => {
93
92
  notifyCommittedReminders(ctx);
94
93
  clearStaged();
@@ -103,13 +102,16 @@ export function registerBudget(pi, isEnabled, settingsManager) {
103
102
  return undefined;
104
103
  const windowId = currentWindowId(ctx);
105
104
  const { reminder, warning } = thresholdsFor(ctx);
106
- if (hasWindowMessage(ctx, WARNING_TYPE) || pendingWarning?.windowId === windowId)
107
- return undefined;
108
- if (remaining <= warning) {
109
- // A not-yet-committed shallow reminder is superseded by the final warning.
105
+ if (remaining <= warning && automaticResetEnabled(ctx)) {
106
+ // Re-arm close-out on each eligible request. A prior request may have aborted
107
+ // after persisting the warning, so warning deduplication must not own this state.
108
+ onCloseOut(windowId);
109
+ if (hasWindowMessage(ctx, WARNING_TYPE) || pendingWarning?.windowId === windowId)
110
+ return undefined;
111
+ // The critical close-out starts at reserve + runway, not at the hard reserve.
110
112
  pendingGuidance = undefined;
111
- const content = `${GUIDANCE_OPEN_TAG}\n${WARNING_PROMPT}\n${GUIDANCE_CLOSE_TAG}`;
112
- pendingWarning = { windowId, content };
113
+ const content = WARNING_CONTENT;
114
+ pendingWarning = { windowId, content, remaining };
113
115
  const warningMessage = {
114
116
  role: "custom",
115
117
  customType: WARNING_TYPE,
@@ -119,13 +121,15 @@ export function registerBudget(pi, isEnabled, settingsManager) {
119
121
  };
120
122
  return { messages: [..._event.messages, warningMessage] };
121
123
  }
124
+ if (hasWindowMessage(ctx, WARNING_TYPE) || pendingWarning?.windowId === windowId)
125
+ return undefined;
122
126
  if (hasWindowMessage(ctx, GUIDANCE_TYPE) || pendingGuidance?.windowId === windowId)
123
127
  return undefined;
124
128
  if (remaining <= reminder) {
125
129
  // Persist at turn_end, before any reset drafts. A queued sendMessage could
126
130
  // otherwise cross the marker and leak the old window's reminder forward.
127
131
  const left = Math.max(0, remaining - warning);
128
- pendingGuidance = { windowId, content: tokenBudgetGuidance(left) };
132
+ pendingGuidance = { windowId, content: tokenBudgetGuidance(left), remaining };
129
133
  }
130
134
  return undefined;
131
135
  });
@@ -143,7 +147,7 @@ export function registerBudget(pi, isEnabled, settingsManager) {
143
147
  }));
144
148
  return {
145
149
  automaticResetEnabled,
146
- resetDue,
150
+ hardReserveDue,
147
151
  consumeTurnEnd,
148
152
  clear: () => { clearStaged(); pendingNotices = []; },
149
153
  };
@@ -0,0 +1,41 @@
1
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
+ import { type CustomEntry, type ExtensionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
3
+ import type { SessionReader } from "../session-reader.js";
4
+ export type WindowMarker = CustomEntry<{
5
+ windowId: string;
6
+ }> & {
7
+ data: {
8
+ windowId: string;
9
+ };
10
+ };
11
+ export declare function isWindowMarker(entry: SessionEntry): entry is WindowMarker;
12
+ /** Only the active branch can supply a window boundary. */
13
+ export declare function currentReset(ctx: SessionReader): WindowMarker | undefined;
14
+ /** A reset is checkpoint-backed only when its marker directly follows a native compaction. */
15
+ export declare function isCheckpointBackedReset(ctx: SessionReader, marker?: WindowMarker | undefined): boolean;
16
+ /** Mint the durable identity of a session's root history window. */
17
+ export declare function rootWindowId(sessionId: string): string;
18
+ /** Persisted messages in the active window, excluding earlier windows on this branch. */
19
+ export declare function hasWindowMessage(ctx: SessionReader, customType: string): boolean;
20
+ /** The root or latest durable marker on the active branch. */
21
+ export declare function currentWindowId(ctx: SessionReader): string;
22
+ /** The durable window active just before the marker: the last earlier marker, else the root window. */
23
+ export declare function previousWindowId(ctx: SessionReader, markerId: string): string;
24
+ /** Match a provider-facing boot message, optionally by window identity. */
25
+ export declare function isWindowBoot(message: AgentMessage, windowId?: string): boolean;
26
+ /** Match a persisted boot entry by raw identity, even when a later edit hides it from projection. */
27
+ export declare function isWindowBootEntry(entry: SessionEntry, windowId: string): boolean;
28
+ /**
29
+ * The durable marker selects a boot message by identity, never by wall-clock time.
30
+ * The boot is the first conversation message of the window. Folding only its prefix
31
+ * preserves later prompt/tool patches in place, including their cacheable ordering.
32
+ */
33
+ export declare function projectWindow(messages: AgentMessage[], windowId: string): AgentMessage[];
34
+ /**
35
+ * Root windows are not reset boundaries. A forked session can copy a root boot whose
36
+ * details name the source session; refresh that boot in-place in the provider projection
37
+ * while retaining every user/assistant/tool message from the copied root transcript.
38
+ */
39
+ export declare function projectRootWindow(messages: AgentMessage[], windowId: string): AgentMessage[];
40
+ /** Usage for the selected window, excluding provider usage recorded before its marker. */
41
+ export declare function windowUsage(ctx: Pick<ExtensionContext, "sessionManager" | "getContextUsage" | "model">): import("@earendil-works/pi-coding-agent").ContextUsage | undefined;
@@ -18,6 +18,16 @@ export function currentReset(ctx) {
18
18
  }
19
19
  return undefined;
20
20
  }
21
+ /** A reset is checkpoint-backed only when its marker directly follows a native compaction. */
22
+ export function isCheckpointBackedReset(ctx, marker = currentReset(ctx)) {
23
+ if (!marker?.parentId)
24
+ return false;
25
+ const branch = ctx.sessionManager.getBranch();
26
+ const markerIndex = branch.findIndex((entry) => entry.id === marker.id);
27
+ const checkpoint = branch[markerIndex - 1];
28
+ return markerIndex > 0 && checkpoint?.id === marker.parentId && checkpoint.type === "compaction" &&
29
+ checkpoint.summary === "" && checkpoint.firstKeptEntryId === checkpoint.id;
30
+ }
21
31
  /** Mint the durable identity of a session's root history window. */
22
32
  export function rootWindowId(sessionId) {
23
33
  return `pcw:${sessionId.slice(0, 8)}:root`;
@@ -99,7 +109,12 @@ export function windowUsage(ctx) {
99
109
  return undefined;
100
110
  const windowId = reset.data.windowId;
101
111
  try {
102
- const messages = projectWindow(ctx.sessionManager.buildSessionProjection().messages, windowId);
112
+ const canonicalMessages = ctx.sessionManager.buildSessionProjection().messages;
113
+ // New resets already cut the durable Pi projection. Marker slicing is retained only
114
+ // for sessions written before native reset checkpoints existed.
115
+ const messages = isCheckpointBackedReset(ctx, reset)
116
+ ? canonicalMessages
117
+ : projectWindow(canonicalMessages, windowId);
103
118
  const { tokens } = estimateContextTokens(convertToLlm(messages));
104
119
  return { tokens, contextWindow, percent: tokens / contextWindow * 100 };
105
120
  }
@@ -0,0 +1,20 @@
1
+ import type { NotesSnapshot } from "../pi/notes/snapshot.js";
2
+ /**
3
+ * Render a static, once-per-window boot block from explicit data. This function does not read
4
+ * notes or call runtime UI APIs; acquisition belongs to loadNotesSnapshot and its caller.
5
+ */
6
+ export type BootRenderData = {
7
+ readonly agentName: string;
8
+ readonly modelName: string;
9
+ readonly firstWindowId: string;
10
+ readonly currentWindowId: string;
11
+ readonly previousWindowId?: string;
12
+ readonly notes: NotesSnapshot;
13
+ };
14
+ export declare function renderBootBlock(data: BootRenderData): string;
15
+ /**
16
+ * Codex-equivalent low-budget reminder. The measured remaining count is frozen into
17
+ * the text at the crossing that fires it, so each persisted copy is a snapshot true
18
+ * at write time; get_context_remaining remains the live source for the current figure.
19
+ */
20
+ export declare function tokenBudgetGuidance(remaining: number): string;
@@ -59,7 +59,7 @@ function notesIndex(snapshot) {
59
59
  if (recentNotes.length > 0) {
60
60
  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:`];
61
61
  for (const row of recentNotes) {
62
- lines.push(`- ${row.address} (${row.body.split("\n").length} lines, ${row.sizeBytes} UTF-8 bytes, updated ${relativeTime(row.meta.updated_at, snapshot.openedAt)})`);
62
+ lines.push(`- ${row.address} (${row.body.split("\n").length} lines, ${row.sizeBytes} UTF-8 bytes, updated ${relativeTime(row.meta.updatedAt, snapshot.openedAt)})`);
63
63
  }
64
64
  sections.push(lines.join("\n"));
65
65
  }
@@ -0,0 +1,26 @@
1
+ import type { ExtensionAPI, ExtensionContext, SessionBoundaryDraft, SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ import { type WindowMarker } from "./context-window.js";
3
+ import { type IncompleteNotesNotifier } from "./boot.js";
4
+ export type ResetTailState = {
5
+ readonly boot: boolean;
6
+ readonly continuation: boolean;
7
+ };
8
+ /** Match the hidden continuation entry that carries the one reset message. */
9
+ export declare function isWindowContinuationEntry(entry: SessionEntry): boolean;
10
+ /** The single continuation sender: the only reset prose persisted for a window. */
11
+ export declare function sendContinuation(pi: ExtensionAPI): void;
12
+ /**
13
+ * The closed, ordered reset shape: retain-none native checkpoint, marker, matching boot,
14
+ * continuation. The boot snapshot is acquired asynchronously after capturing this identity.
15
+ */
16
+ export declare function buildResetDrafts(ctx: ExtensionContext, notifyIncompleteNotes?: IncompleteNotesNotifier, isCurrent?: () => boolean): Promise<[SessionBoundaryDraft, SessionBoundaryDraft, SessionBoundaryDraft, SessionBoundaryDraft]>;
17
+ /**
18
+ * Inspect the persisted tail of a reset marker. It reports which reset messages are present
19
+ * only while the tail stays repairable: metadata may follow the marker, but real conversation,
20
+ * a foreign message, a later marker, or a misordered/duplicate reset artifact refuses repair.
21
+ */
22
+ export declare function inspectResetTail(ctx: ExtensionContext, markerId: string, windowId: string): ResetTailState | undefined;
23
+ /** True once the marker's tail already carries its boot and continuation in a valid order. */
24
+ export declare function resetTailCommitted(ctx: ExtensionContext, markerId: string, windowId: string): boolean;
25
+ /** Emit only the reset artifacts an incomplete tail is missing, in the closed order. */
26
+ export declare function repairResetTail(pi: ExtensionAPI, ctx: ExtensionContext, marker: WindowMarker, notifyIncompleteNotes?: IncompleteNotesNotifier, isCurrent?: () => boolean): Promise<void>;
@@ -11,31 +11,26 @@ export function sendContinuation(pi) {
11
11
  pi.sendMessage({ customType: CONTINUATION_TYPE, content: CONTINUATION, display: false }, { triggerTurn: false });
12
12
  }
13
13
  /**
14
- * The closed, ordered reset shape: marker, matching boot, continuation. This is the one
15
- * source of the reset message and the one place that mints the new window identity.
14
+ * The closed, ordered reset shape: retain-none native checkpoint, marker, matching boot,
15
+ * continuation. The boot snapshot is acquired asynchronously after capturing this identity.
16
16
  */
17
- export function buildResetDrafts(ctx, notifyIncompleteNotes) {
18
- const sessionPrefix = ctx.sessionManager.getSessionId().slice(0, 8);
17
+ export async function buildResetDrafts(ctx, notifyIncompleteNotes, isCurrent = () => true) {
18
+ const sessionId = ctx.sessionManager.getSessionId();
19
+ const previousId = currentWindowId(ctx);
20
+ const sessionPrefix = sessionId.slice(0, 8);
19
21
  const usedWindowIds = new Set(ctx.sessionManager.getBranch().filter(isWindowMarker).map((entry) => entry.data.windowId));
20
22
  let windowId;
21
23
  do {
22
24
  windowId = `pcw:${sessionPrefix}:${randomUUID().slice(0, 8)}`;
23
25
  } while (usedWindowIds.has(windowId));
24
- const boot = buildBootMessage(ctx, windowId, currentWindowId(ctx), notifyIncompleteNotes);
26
+ const boot = await buildBootMessage(ctx, windowId, previousId, notifyIncompleteNotes, isCurrent);
25
27
  return [
28
+ { type: "compaction", summary: "", firstKeptEntryId: null },
26
29
  { type: "custom", customType: RESET_MARKER_TYPE, data: { windowId } },
27
30
  { type: "custom_message", customType: BOOT_TYPE, content: boot.content, display: false, details: { windowId } },
28
31
  { type: "custom_message", customType: CONTINUATION_TYPE, content: CONTINUATION, display: false },
29
32
  ];
30
33
  }
31
- /** Persist the marker and send both hidden reset messages; returns the new window id. */
32
- export function persistManualReset(pi, ctx, notifyIncompleteNotes) {
33
- const [marker, boot, continuation] = buildResetDrafts(ctx, notifyIncompleteNotes);
34
- pi.appendEntry(marker.customType, marker.data);
35
- pi.sendMessage({ customType: boot.customType, content: boot.content, display: boot.display, details: boot.details }, { triggerTurn: false });
36
- pi.sendMessage({ customType: continuation.customType, content: continuation.content, display: continuation.display }, { triggerTurn: false });
37
- return boot.details.windowId;
38
- }
39
34
  /**
40
35
  * Inspect the persisted tail of a reset marker. It reports which reset messages are present
41
36
  * only while the tail stays repairable: metadata may follow the marker, but real conversation,
@@ -75,12 +70,18 @@ export function resetTailCommitted(ctx, markerId, windowId) {
75
70
  return tail?.boot === true && tail.continuation === true;
76
71
  }
77
72
  /** Emit only the reset artifacts an incomplete tail is missing, in the closed order. */
78
- export function repairResetTail(pi, ctx, marker, notifyIncompleteNotes) {
73
+ export async function repairResetTail(pi, ctx, marker, notifyIncompleteNotes, isCurrent = () => true) {
79
74
  const tail = inspectResetTail(ctx, marker.id, marker.data.windowId);
80
75
  if (!tail || (tail.boot && tail.continuation))
81
76
  return;
82
- if (!tail.boot)
83
- sendBoot(pi, buildBootMessage(ctx, marker.data.windowId, previousWindowId(ctx, marker.id), notifyIncompleteNotes));
84
- if (!tail.continuation)
77
+ const boot = tail.boot ? undefined : await buildBootMessage(ctx, marker.data.windowId, previousWindowId(ctx, marker.id), notifyIncompleteNotes, isCurrent);
78
+ if (!isCurrent())
79
+ return;
80
+ const latest = inspectResetTail(ctx, marker.id, marker.data.windowId);
81
+ if (!latest)
82
+ return;
83
+ if (!latest.boot && boot)
84
+ sendBoot(pi, boot);
85
+ if (!latest.continuation && (latest.boot || boot))
85
86
  sendContinuation(pi);
86
87
  }
@@ -0,0 +1,89 @@
1
+ import type { ExtensionAPI, ExtensionContext, SessionBoundaryDraft } from "@earendil-works/pi-coding-agent";
2
+ type BudgetOwner = {
3
+ automaticResetEnabled: (ctx: ExtensionContext) => boolean;
4
+ hardReserveDue: (ctx: ExtensionContext) => boolean;
5
+ consumeTurnEnd: (ctx: ExtensionContext) => SessionBoundaryDraft[];
6
+ clear: () => void;
7
+ };
8
+ type ResetOptions = {
9
+ isEnabled: () => boolean;
10
+ budget: BudgetOwner;
11
+ buildReset: (ctx: ExtensionContext, isCurrent: () => boolean) => SessionBoundaryDraft[] | Promise<SessionBoundaryDraft[]>;
12
+ getLifecycleGeneration?: () => number;
13
+ onResetReady?: (ctx: ExtensionContext, drafts: readonly SessionBoundaryDraft[]) => void;
14
+ };
15
+ export type ResetRequestSource = "manual" | "automatic";
16
+ export type ResetRequest = {
17
+ readonly phase: "none";
18
+ } | {
19
+ readonly phase: "close-out";
20
+ readonly windowId: string;
21
+ readonly source: ResetRequestSource;
22
+ } | {
23
+ readonly phase: "tool-requested";
24
+ readonly windowId: string;
25
+ };
26
+ export type ResetOverflowPhase = "idle" | "pending" | "pending-spent" | "spent";
27
+ export interface ResetControlState {
28
+ readonly request: ResetRequest;
29
+ readonly overflow: ResetOverflowPhase;
30
+ }
31
+ export declare function initialResetControl(): ResetControlState;
32
+ /** Facts for a completed turn. Guard values stay lazy to preserve policy resolution order. */
33
+ export interface ResetTurnEndFacts {
34
+ readonly windowId: string;
35
+ readonly aborted: boolean;
36
+ readonly overflow: boolean;
37
+ readonly failed: boolean;
38
+ readonly enabled: boolean;
39
+ readonly queued: boolean;
40
+ readonly automaticResetEnabled: boolean;
41
+ readonly hardReserveDue: boolean;
42
+ }
43
+ /** Facts for the pre-settlement boundary; policy guards stay lazy for the same reason. */
44
+ export interface ResetBeforeSettleFacts {
45
+ readonly windowId: string;
46
+ readonly queued: boolean;
47
+ readonly enabled: boolean;
48
+ readonly automaticResetEnabled: boolean;
49
+ readonly aborted: boolean;
50
+ readonly failed: boolean;
51
+ }
52
+ export type ResetControlEvent = {
53
+ readonly type: "close_out";
54
+ readonly windowId: string;
55
+ readonly source: ResetRequestSource;
56
+ } | {
57
+ readonly type: "tool_request";
58
+ readonly windowId: string;
59
+ } | {
60
+ readonly type: "turn_end";
61
+ readonly facts: ResetTurnEndFacts;
62
+ } | {
63
+ readonly type: "before_settle";
64
+ readonly facts: ResetBeforeSettleFacts;
65
+ } | {
66
+ readonly type: "settled";
67
+ } | {
68
+ readonly type: "abort";
69
+ } | {
70
+ readonly type: "clear";
71
+ };
72
+ export type ResetControlEffect = "none" | "close-out-armed" | "already-pending" | "commit-boundary" | "recover-overflow";
73
+ export interface ResetControlResult {
74
+ readonly state: ResetControlState;
75
+ readonly effect: ResetControlEffect;
76
+ }
77
+ /** Pure reset-control transitions: request phases own close-out, tool commit, and fallback. */
78
+ export declare function reduceResetControl(state: ResetControlState, event: ResetControlEvent): ResetControlResult;
79
+ /**
80
+ * Own close-out requests at Pi's public turn and pre-settlement boundaries. Tool-requested
81
+ * resets commit at turn_end; manual and budget close-outs remain armed across note/tool turns
82
+ * and fall back at a successful agent_before_settle. Overflow recovery remains bounded.
83
+ */
84
+ export declare function registerResetLifecycle(pi: ExtensionAPI, options: ResetOptions): {
85
+ closeOut(windowId: string, source: ResetRequestSource): ResetControlEffect;
86
+ request(windowId: string): "rollover_already_pending" | "rollover_requested";
87
+ clear: () => void;
88
+ };
89
+ export {};