@howaboua/pi-notes-compaction 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/windows.ts ADDED
@@ -0,0 +1,282 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
+ import { getCurrentSystemMessage } from "@earendil-works/pi-ai";
4
+ import type {
5
+ CustomMessageEntryDraft,
6
+ ExtensionAPI,
7
+ ExtensionContext,
8
+ SessionEntry,
9
+ } from "@earendil-works/pi-coding-agent";
10
+ import { keyHint } from "@earendil-works/pi-coding-agent";
11
+ import { Box, Text, TruncatedText } from "@earendil-works/pi-tui";
12
+ import type { WindowIdentity } from "./bridge.js";
13
+
14
+ export const WINDOW_MESSAGE = "notes-compaction:window:v1";
15
+ export const SETTLEMENT_ENTRY = "notes-compaction:settled:v1";
16
+ export interface WindowDetails {
17
+ protocol: 1;
18
+ kind: "window" | "identity" | "reminder" | "urgent";
19
+ identity: WindowIdentity;
20
+ trim: boolean;
21
+ }
22
+ const GUIDANCE = `<context_window_guidance>
23
+ Keep one checkpoint per task at a stable path: request, constraints, decisions, progress, next steps, history IDs. Replace stale state; mark completion in place. Keep reusable findings and deferred ideas in separate topic notes; link, don't copy. Recording isn't permission to implement.
24
+
25
+ Save changed state after substantial work, before replying, and before new_context or handoff. Skip routine or unchanged state. Include checkpoint paths in handoffs. After rollover, read the checkpoint, then linked notes as needed; history only for missing details.
26
+ </context_window_guidance>`;
27
+
28
+ export function registerWindowRenderer(pi: ExtensionAPI): void {
29
+ pi.registerMessageRenderer(
30
+ WINDOW_MESSAGE,
31
+ (message, { expanded, outputPad }, theme) => {
32
+ const details = parseWindowDetails(message.details);
33
+ if (!details || typeof message.content !== "string") return undefined;
34
+ const title =
35
+ details.kind === "urgent"
36
+ ? "Context nearly full · Save notes and start a new window now"
37
+ : details.kind === "reminder"
38
+ ? "Context checkpoint reminder · Save notes and start a new window"
39
+ : `Context window ${details.identity.windowNumber + 1} · Notes and history`;
40
+ const box = new Box(outputPad, 1, (text) =>
41
+ theme.bg("customMessageBg", text),
42
+ );
43
+ box.addChild(
44
+ expanded
45
+ ? new Text(theme.fg("customMessageText", message.content), 0, 0)
46
+ : new TruncatedText(
47
+ theme.fg(
48
+ details.kind === "urgent" ? "warning" : "customMessageLabel",
49
+ title,
50
+ ) +
51
+ theme.fg(
52
+ "dim",
53
+ ` (${keyHint("app.tools.expand", "to expand")})`,
54
+ ),
55
+ 0,
56
+ 0,
57
+ ),
58
+ );
59
+ return box;
60
+ },
61
+ );
62
+ }
63
+
64
+ function identity(value: unknown): WindowIdentity | undefined {
65
+ if (!value || typeof value !== "object") return;
66
+ const data = value as Partial<WindowIdentity>;
67
+ if (
68
+ typeof data.firstWindowId !== "string" ||
69
+ !data.firstWindowId ||
70
+ typeof data.currentWindowId !== "string" ||
71
+ !data.currentWindowId ||
72
+ !Number.isInteger(data.windowNumber) ||
73
+ data.windowNumber! < 0 ||
74
+ (data.previousWindowId !== undefined &&
75
+ typeof data.previousWindowId !== "string")
76
+ )
77
+ return;
78
+ return data as WindowIdentity;
79
+ }
80
+
81
+ function parseWindowDetails(value: unknown): WindowDetails | undefined {
82
+ if (!value || typeof value !== "object") return;
83
+ const details = value as Partial<WindowDetails>;
84
+ const ids = identity(details.identity);
85
+ if (
86
+ details.protocol !== 1 ||
87
+ !ids ||
88
+ !["window", "identity", "reminder", "urgent"].includes(
89
+ details.kind ?? "",
90
+ ) ||
91
+ typeof details.trim !== "boolean"
92
+ )
93
+ return;
94
+ return {
95
+ protocol: 1,
96
+ kind: details.kind!,
97
+ identity: ids,
98
+ trim: details.trim,
99
+ };
100
+ }
101
+
102
+ /** Understand previous PCC window IDs for familiar history lookup, without importing its runtime. */
103
+ export function windowDetails(entry: SessionEntry): WindowDetails | undefined {
104
+ if (entry.type !== "custom_message") return;
105
+ return messageWindowDetails(entry.customType, entry.details);
106
+ }
107
+
108
+ function messageWindowDetails(
109
+ customType: string,
110
+ data: unknown,
111
+ ): WindowDetails | undefined {
112
+ if (customType === WINDOW_MESSAGE) return parseWindowDetails(data);
113
+ if (
114
+ customType !== "codex-context-window" ||
115
+ !data ||
116
+ typeof data !== "object" ||
117
+ !("contextManagement" in data)
118
+ )
119
+ return;
120
+ const details = data.contextManagement;
121
+ if (
122
+ !details ||
123
+ typeof details !== "object" ||
124
+ !("kind" in details) ||
125
+ details.kind !== "window"
126
+ )
127
+ return;
128
+ const ids = identity(details);
129
+ return ids
130
+ ? {
131
+ protocol: 1,
132
+ kind: "window",
133
+ identity: ids,
134
+ trim:
135
+ "trimPreviousWindow" in details &&
136
+ details.trimPreviousWindow === true,
137
+ }
138
+ : undefined;
139
+ }
140
+
141
+ export function windowMessage(
142
+ ids: WindowIdentity,
143
+ agent: string,
144
+ hint: string,
145
+ trim: boolean,
146
+ kind: "window" | "identity" = "window",
147
+ ): CustomMessageEntryDraft {
148
+ const lines = [
149
+ `Agent name: ${agent}`,
150
+ `First context window id: ${ids.firstWindowId}`,
151
+ `Current context window id: ${ids.currentWindowId}`,
152
+ ];
153
+ if (ids.previousWindowId)
154
+ lines.push(`Previous context window id: ${ids.previousWindowId}`);
155
+ if (hint) lines.push(hint);
156
+ return {
157
+ type: "custom_message",
158
+ customType: WINDOW_MESSAGE,
159
+ content: `${GUIDANCE}\n\n<context_window>\n${lines.join("\n")}\n</context_window>`,
160
+ display: true,
161
+ details: {
162
+ protocol: 1,
163
+ kind,
164
+ identity: ids,
165
+ trim,
166
+ } satisfies WindowDetails,
167
+ };
168
+ }
169
+
170
+ export class Windows {
171
+ current: WindowIdentity | undefined;
172
+ private compactedSinceBoundary = false;
173
+ private reminders = new Set<string>();
174
+ restore(entries: readonly SessionEntry[]): void {
175
+ this.current = undefined;
176
+ this.compactedSinceBoundary = false;
177
+ this.reminders.clear();
178
+ for (const entry of entries) {
179
+ const details = windowDetails(entry);
180
+ if (details?.kind === "window") {
181
+ this.current = details.identity;
182
+ this.compactedSinceBoundary = false;
183
+ } else if (
184
+ details &&
185
+ (details.kind === "reminder" || details.kind === "urgent")
186
+ )
187
+ this.reminders.add(
188
+ `${details.identity.currentWindowId}:${details.kind}`,
189
+ );
190
+ if (entry.type === "compaction") {
191
+ this.reminders.clear();
192
+ this.compactedSinceBoundary = true;
193
+ }
194
+ }
195
+ }
196
+ next(): WindowIdentity {
197
+ const id = randomUUID();
198
+ return this.current
199
+ ? {
200
+ firstWindowId: this.current.firstWindowId,
201
+ currentWindowId: id,
202
+ previousWindowId: this.current.currentWindowId,
203
+ windowNumber: this.current.windowNumber + 1,
204
+ }
205
+ : { firstWindowId: id, currentWindowId: id, windowNumber: 0 };
206
+ }
207
+ remaining(ctx: ExtensionContext) {
208
+ const usage = ctx.getContextUsage();
209
+ const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
210
+ const tokens = usage?.tokens;
211
+ const remainingTokens =
212
+ tokens === undefined ||
213
+ tokens === null ||
214
+ !Number.isFinite(tokens) ||
215
+ !Number.isFinite(contextWindow) ||
216
+ contextWindow <= 0
217
+ ? undefined
218
+ : Math.max(0, contextWindow - Math.max(0, tokens));
219
+ return {
220
+ contextWindow,
221
+ windowId: this.current?.currentWindowId,
222
+ remainingTokens,
223
+ remainingPercent:
224
+ remainingTokens === undefined
225
+ ? undefined
226
+ : Math.round((remainingTokens / contextWindow) * 1000) / 10,
227
+ };
228
+ }
229
+ reminder(ctx: ExtensionContext): CustomMessageEntryDraft | undefined {
230
+ const { remainingTokens, contextWindow, remainingPercent } =
231
+ this.remaining(ctx);
232
+ if (!this.current || remainingTokens === undefined) return;
233
+ const used = 100 * (1 - remainingTokens / contextWindow);
234
+ const kind = used >= 90 ? "urgent" : used >= 85 ? "reminder" : undefined;
235
+ if (!kind || this.reminders.has(`${this.current.currentWindowId}:${kind}`))
236
+ return;
237
+ this.reminders.add(`${this.current.currentWindowId}:reminder`);
238
+ if (kind === "urgent")
239
+ this.reminders.add(`${this.current.currentWindowId}:urgent`);
240
+ return {
241
+ type: "custom_message",
242
+ customType: WINDOW_MESSAGE,
243
+ content: `<context_window_reminder>\n${kind === "urgent" ? "Urgent: " : ""}${remainingPercent}% remaining. Checkpoint the active request, state and known history IDs in notes, then call new_context ${kind === "urgent" ? "now, before other work" : "before continuing work"}.\n</context_window_reminder>`,
244
+ display: true,
245
+ details: {
246
+ protocol: 1,
247
+ kind,
248
+ identity: this.current,
249
+ trim: false,
250
+ } satisfies WindowDetails,
251
+ };
252
+ }
253
+ projectMessages(messages: readonly AgentMessage[]): AgentMessage[] {
254
+ if (this.compactedSinceBoundary) return [...messages];
255
+ let boundary = -1;
256
+ for (let i = 0; i < messages.length; i++) {
257
+ const message = messages[i]!;
258
+ if (message.role !== "custom") continue;
259
+ const details = messageWindowDetails(message.customType, message.details);
260
+ if (!details && message.customType === WINDOW_MESSAGE)
261
+ throw new Error(
262
+ "Invalid saved context window. Open an intact session branch to continue.",
263
+ );
264
+ if (details?.kind === "window") boundary = details.trim ? i : -1;
265
+ }
266
+ if (boundary < 0) return [...messages];
267
+ const system = getCurrentSystemMessage(messages.slice(0, boundary));
268
+ return system
269
+ ? [system, ...messages.slice(boundary)]
270
+ : messages.slice(boundary);
271
+ }
272
+ projectBranch(entries: readonly SessionEntry[]): SessionEntry[] {
273
+ let boundary = -1;
274
+ for (let i = 0; i < entries.length; i++) {
275
+ const details = windowDetails(entries[i]!);
276
+ if (details?.kind === "window") boundary = details.trim ? i : -1;
277
+ // Normal Pi/PCC compaction has already chosen its own checkpoint.
278
+ if (entries[i]!.type === "compaction") boundary = -1;
279
+ }
280
+ return entries.slice(Math.max(0, boundary));
281
+ }
282
+ }