killeros 2.0.2 → 2.0.3
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/CHANGELOG.md +17 -0
- package/Killeros.ts +13 -8
- package/README.md +13 -7
- package/killeros/footer.ts +46 -1
- package/killeros/goals.ts +118 -55
- package/killeros/notifications.ts +167 -0
- package/killeros/question.ts +16 -1
- package/killeros/runtime.ts +1 -25
- package/killeros/shell-ui.ts +1 -0
- package/package.json +2 -2
- package/killeros/context-compaction.ts +0 -614
|
@@ -1,614 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionAPI,
|
|
3
|
-
ExtensionContext,
|
|
4
|
-
SessionBeforeCompactEvent,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { compact, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import { pauseGoalAfterFailure } from "./goals.ts";
|
|
8
|
-
import type { CompactionRuntime, GoalRuntime } from "./runtime.ts";
|
|
9
|
-
|
|
10
|
-
type CompactionPreparation = SessionBeforeCompactEvent["preparation"];
|
|
11
|
-
type CompactionMessage = CompactionPreparation["messagesToSummarize"][number];
|
|
12
|
-
type MessageRecord = Record<string, unknown>;
|
|
13
|
-
|
|
14
|
-
const warnedRuntimes = new WeakSet<CompactionRuntime>();
|
|
15
|
-
const inFlightTimers = new WeakMap<CompactionRuntime, NodeJS.Timeout>();
|
|
16
|
-
const abortCleanups = new WeakMap<CompactionRuntime, () => void>();
|
|
17
|
-
const compactionFailureHandlers = new WeakMap<CompactionRuntime, () => void>();
|
|
18
|
-
const FILE_PATH_PATTERN = /(?:[A-Za-z]:[\\/]|[\\/]|\.{1,2}[\\/])?[A-Za-z0-9_@.-]+(?:[\\/][A-Za-z0-9_@.-]+)*\.(?:ts|tsx|js|jsx|mjs|cjs|json|md|css|html|py|rs|go|java|kt|rb|php|vue|svelte|yaml|yml|toml|sh|sql)\b/giu;
|
|
19
|
-
const PATH_KEYS = new Set(["path", "file", "filepath", "file_path", "filename", "target"]);
|
|
20
|
-
const MAX_MESSAGE_TEXT = 1_000;
|
|
21
|
-
const MAX_CONTEXT_ITEMS = 80;
|
|
22
|
-
const MAX_FILES = 40;
|
|
23
|
-
const MAX_PREVIOUS_SUMMARY = 6_000;
|
|
24
|
-
const MAX_CUSTOM_INSTRUCTIONS = 4_000;
|
|
25
|
-
const COMPACTION_STATE_TIMEOUT_MS = 5 * 60_000;
|
|
26
|
-
const DETERMINISTIC_FALLBACK_PREFIX = "This summary was produced deterministically without model understanding. Verify its details before relying on it.";
|
|
27
|
-
const COMPACTION_ACCURACY_WARNING = "Long threads and multiple compactions can cause the model to be less accurate. Start a new thread when possible to keep threads small and targeted.";
|
|
28
|
-
|
|
29
|
-
type NotificationLevel = Parameters<ExtensionContext["ui"]["notify"]>[1];
|
|
30
|
-
|
|
31
|
-
function asRecord(value: unknown): MessageRecord | undefined {
|
|
32
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
33
|
-
return value as MessageRecord;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function compactText(value: string, limit = MAX_MESSAGE_TEXT): string {
|
|
37
|
-
return value.replace(/\s+/gu, " ").trim().slice(0, limit);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function textFromContent(content: unknown): string[] {
|
|
41
|
-
if (typeof content === "string") return [content];
|
|
42
|
-
if (!Array.isArray(content)) return [];
|
|
43
|
-
|
|
44
|
-
const text: string[] = [];
|
|
45
|
-
for (const part of content) {
|
|
46
|
-
const record = asRecord(part);
|
|
47
|
-
if (!record) continue;
|
|
48
|
-
if (typeof record.text === "string") text.push(record.text);
|
|
49
|
-
if (record.type === "toolCall" && typeof record.name === "string") {
|
|
50
|
-
text.push(`Called ${record.name}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return text;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function messageText(message: CompactionMessage): string {
|
|
57
|
-
const record = asRecord(message);
|
|
58
|
-
if (!record) return "";
|
|
59
|
-
|
|
60
|
-
const text = textFromContent(record.content);
|
|
61
|
-
if (typeof record.summary === "string") text.unshift(record.summary);
|
|
62
|
-
return compactText(text.join(" "));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function messageRole(message: CompactionMessage): string {
|
|
66
|
-
const role = asRecord(message)?.role;
|
|
67
|
-
return typeof role === "string" && role.trim() ? role : "context";
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function extractMessages(preparation: CompactionPreparation): Array<{ role: string; text: string }> {
|
|
71
|
-
const messages = [...preparation.messagesToSummarize, ...preparation.turnPrefixMessages];
|
|
72
|
-
const extracted: Array<{ role: string; text: string }> = [];
|
|
73
|
-
for (const message of messages) {
|
|
74
|
-
const text = messageText(message);
|
|
75
|
-
if (text) extracted.push({ role: messageRole(message), text });
|
|
76
|
-
if (extracted.length >= MAX_CONTEXT_ITEMS) break;
|
|
77
|
-
}
|
|
78
|
-
return extracted;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function addPath(paths: Set<string>, value: string): void {
|
|
82
|
-
const path = value.trim().replace(/^['"`([{<]+|['"`.,;:!?)}\]>]+$/gu, "");
|
|
83
|
-
if (!path || path.includes("://") || path.length > 512 || paths.size >= MAX_FILES) return;
|
|
84
|
-
paths.add(path);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function extractFilePaths(text: string, paths: Set<string>): void {
|
|
88
|
-
FILE_PATH_PATTERN.lastIndex = 0;
|
|
89
|
-
let match: RegExpExecArray | null;
|
|
90
|
-
while ((match = FILE_PATH_PATTERN.exec(text)) !== null) {
|
|
91
|
-
addPath(paths, match[0]);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function collectPaths(value: unknown, paths: Set<string>, depth = 0): void {
|
|
96
|
-
if (paths.size >= MAX_FILES || depth > 4) return;
|
|
97
|
-
if (typeof value === "string") {
|
|
98
|
-
extractFilePaths(value, paths);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (Array.isArray(value)) {
|
|
102
|
-
for (const item of value) collectPaths(item, paths, depth + 1);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const record = asRecord(value);
|
|
107
|
-
if (!record) return;
|
|
108
|
-
for (const [key, item] of Object.entries(record)) {
|
|
109
|
-
const normalizedKey = key.toLocaleLowerCase();
|
|
110
|
-
if (PATH_KEYS.has(normalizedKey) || normalizedKey === "content" || normalizedKey === "text" || normalizedKey === "summary" || normalizedKey === "arguments" || normalizedKey === "input") {
|
|
111
|
-
collectPaths(item, paths, depth + 1);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function collectMessagePaths(preparation: CompactionPreparation): Set<string> {
|
|
117
|
-
const paths = new Set<string>();
|
|
118
|
-
const messages = [...preparation.messagesToSummarize, ...preparation.turnPrefixMessages];
|
|
119
|
-
for (const message of messages) collectPaths(message, paths);
|
|
120
|
-
return paths;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function collectFileOperationPaths(preparation: CompactionPreparation): Set<string> {
|
|
124
|
-
const paths = new Set<string>();
|
|
125
|
-
const fileOps = asRecord(preparation.fileOps);
|
|
126
|
-
if (!fileOps) return paths;
|
|
127
|
-
|
|
128
|
-
for (const key of ["written", "edited"]) {
|
|
129
|
-
const values = fileOps[key];
|
|
130
|
-
if (values instanceof Set) {
|
|
131
|
-
for (const value of values) {
|
|
132
|
-
if (typeof value === "string") addPath(paths, value);
|
|
133
|
-
}
|
|
134
|
-
} else if (Array.isArray(values)) {
|
|
135
|
-
for (const value of values) {
|
|
136
|
-
if (typeof value === "string") addPath(paths, value);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return paths;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function quoteText(value: string, limit: number): string {
|
|
144
|
-
const text = value.trim().slice(0, limit);
|
|
145
|
-
return text
|
|
146
|
-
.split(/\r?\n/gu)
|
|
147
|
-
.map((line) => `> ${line}`)
|
|
148
|
-
.join("\n");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function retainSummaryEdges(value: string, limit: number): string {
|
|
152
|
-
if (value.length <= limit) return value;
|
|
153
|
-
const marker = "\n...[previous summary truncated]...\n";
|
|
154
|
-
const available = Math.max(0, limit - marker.length);
|
|
155
|
-
const headLength = Math.ceil(available * 0.6);
|
|
156
|
-
return `${value.slice(0, headLength)}${marker}${value.slice(-(available - headLength))}`;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function evidenceLine(item: { role: string; text: string }): string {
|
|
160
|
-
return `- [${item.role}] ${compactText(item.text, 320)}`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function recentMatches(
|
|
164
|
-
messages: Array<{ role: string; text: string }>,
|
|
165
|
-
predicate: (item: { role: string; text: string }) => boolean,
|
|
166
|
-
limit: number,
|
|
167
|
-
): string[] {
|
|
168
|
-
return messages.filter(predicate).slice(-limit).map(evidenceLine);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function buildProgress(messages: Array<{ role: string; text: string }>): string[] {
|
|
172
|
-
const done = recentMatches(
|
|
173
|
-
messages,
|
|
174
|
-
(item) => item.role === "assistant" && /\b(?:added|built|changed|completed|created|done|fixed|implemented|modified|removed|resolved|updated|verified|passed|finished)\b/iu.test(item.text),
|
|
175
|
-
4,
|
|
176
|
-
);
|
|
177
|
-
const inProgress = recentMatches(
|
|
178
|
-
messages,
|
|
179
|
-
(item) => !done.includes(evidenceLine(item)),
|
|
180
|
-
3,
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
const lines = ["### Done"];
|
|
184
|
-
lines.push(...(done.length ? done : ["- No completed work was clearly reported in the retained context."]));
|
|
185
|
-
lines.push("", "### In Progress", ...(inProgress.length ? inProgress : ["- The retained context does not state the current work clearly."]));
|
|
186
|
-
return lines;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function buildKeyDecisions(messages: Array<{ role: string; text: string }>): string[] {
|
|
190
|
-
const decisions = recentMatches(
|
|
191
|
-
messages,
|
|
192
|
-
(item) => /\b(?:decid(?:e|ed)|chose|choose|instead|must|should|will|avoid|required|prefer|keep)\b/iu.test(item.text),
|
|
193
|
-
4,
|
|
194
|
-
);
|
|
195
|
-
return ["- Extracted from conversation context.", ...(decisions.length ? decisions : ["- No explicit decisions were found in the retained context."])];
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function buildModifiedFiles(preparation: CompactionPreparation, messages: Array<{ role: string; text: string }>): string[] {
|
|
199
|
-
const exact = collectFileOperationPaths(preparation);
|
|
200
|
-
if (exact.size) return [...exact].map((path) => `- ${path}`);
|
|
201
|
-
|
|
202
|
-
const mentioned = collectMessagePaths(preparation);
|
|
203
|
-
if (!mentioned.size) return ["- No modified files were recorded in the retained context."];
|
|
204
|
-
|
|
205
|
-
const evidence = messages.some((item) => /\b(?:add(?:ed)?|chang(?:ed|ing)|creat(?:ed|ing)|edit(?:ed|ing)|fix(?:ed|ing)|modif(?:ied|y|ying)|updat(?:ed|ing)|writ(?:e|ten|ing)|remov(?:e|ed|ing))\b/iu.test(item.text));
|
|
206
|
-
return [
|
|
207
|
-
evidence
|
|
208
|
-
? "- Files mentioned with a change action (no structured file-operation record was retained):"
|
|
209
|
-
: "- Files mentioned in the retained context (no structured file-operation record was retained):",
|
|
210
|
-
...[...mentioned].map((path) => `- ${path}`),
|
|
211
|
-
];
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function buildStructuredSummary(
|
|
215
|
-
preparation: CompactionPreparation,
|
|
216
|
-
goalObjective: string | undefined,
|
|
217
|
-
customInstructions: string | undefined,
|
|
218
|
-
): string {
|
|
219
|
-
const messages = extractMessages(preparation);
|
|
220
|
-
const firstUserMessage = messages.find((item) => item.role === "user");
|
|
221
|
-
const goal = goalObjective?.trim()
|
|
222
|
-
? compactText(goalObjective, 4_000)
|
|
223
|
-
: firstUserMessage
|
|
224
|
-
? compactText(firstUserMessage.text, 4_000)
|
|
225
|
-
: "No explicit goal was retained; continue from the latest context.";
|
|
226
|
-
|
|
227
|
-
const nextSteps = [
|
|
228
|
-
"- Continue the task from where it was interrupted by compaction.",
|
|
229
|
-
"- Re-read any files that were being edited to verify current state.",
|
|
230
|
-
];
|
|
231
|
-
if (goalObjective?.trim()) nextSteps.push("- Keep the active goal moving until it is complete or clearly blocked.");
|
|
232
|
-
if (customInstructions?.trim()) {
|
|
233
|
-
nextSteps.push("", "Custom Instructions:", quoteText(customInstructions, MAX_CUSTOM_INSTRUCTIONS));
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return [
|
|
237
|
-
DETERMINISTIC_FALLBACK_PREFIX,
|
|
238
|
-
"",
|
|
239
|
-
"# KillerOS Compaction Summary",
|
|
240
|
-
"",
|
|
241
|
-
...(preparation.previousSummary?.trim()
|
|
242
|
-
? ["## Previous Summary", quoteText(retainSummaryEdges(preparation.previousSummary, MAX_PREVIOUS_SUMMARY), MAX_PREVIOUS_SUMMARY), ""]
|
|
243
|
-
: []),
|
|
244
|
-
"## Goal",
|
|
245
|
-
goal,
|
|
246
|
-
"",
|
|
247
|
-
"## Progress",
|
|
248
|
-
...buildProgress(messages),
|
|
249
|
-
"",
|
|
250
|
-
"## Key Decisions",
|
|
251
|
-
...buildKeyDecisions(messages),
|
|
252
|
-
"",
|
|
253
|
-
"## Next Steps",
|
|
254
|
-
...nextSteps,
|
|
255
|
-
"",
|
|
256
|
-
"## Modified Files",
|
|
257
|
-
...buildModifiedFiles(preparation, messages),
|
|
258
|
-
...(customInstructions?.trim()
|
|
259
|
-
? ["", "## Custom Instructions", quoteText(customInstructions, MAX_CUSTOM_INSTRUCTIONS)]
|
|
260
|
-
: []),
|
|
261
|
-
].join("\n");
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
async function buildModelSummary(
|
|
265
|
-
event: SessionBeforeCompactEvent,
|
|
266
|
-
ctx: ExtensionContext,
|
|
267
|
-
) {
|
|
268
|
-
const model = ctx.model;
|
|
269
|
-
if (!model) throw new Error("No model is available for compaction");
|
|
270
|
-
|
|
271
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
272
|
-
if (!auth.ok) throw new Error(auth.error);
|
|
273
|
-
|
|
274
|
-
const provider = ctx.modelRegistry.getProvider(model.provider);
|
|
275
|
-
const streamFn = provider
|
|
276
|
-
? provider.streamSimple.bind(provider)
|
|
277
|
-
: undefined;
|
|
278
|
-
const retry = SettingsManager.create(ctx.cwd, undefined, {
|
|
279
|
-
projectTrusted: ctx.isProjectTrusted(),
|
|
280
|
-
}).getRetrySettings();
|
|
281
|
-
|
|
282
|
-
return compact(
|
|
283
|
-
event.preparation,
|
|
284
|
-
model,
|
|
285
|
-
auth.apiKey,
|
|
286
|
-
auth.headers,
|
|
287
|
-
event.customInstructions,
|
|
288
|
-
event.signal,
|
|
289
|
-
ctx.thinkingLevel,
|
|
290
|
-
streamFn,
|
|
291
|
-
auth.env,
|
|
292
|
-
retry,
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function exactPercentRemaining(ctx: ExtensionContext): number | null {
|
|
297
|
-
let usage: ReturnType<ExtensionContext["getContextUsage"]>;
|
|
298
|
-
try {
|
|
299
|
-
usage = ctx.getContextUsage();
|
|
300
|
-
} catch {
|
|
301
|
-
return null;
|
|
302
|
-
}
|
|
303
|
-
if (!usage || !Number.isFinite(usage.contextWindow) || usage.contextWindow <= 0) return null;
|
|
304
|
-
if (usage.tokens === null || !Number.isFinite(usage.tokens)) return null;
|
|
305
|
-
|
|
306
|
-
const percentRemaining = ((usage.contextWindow - usage.tokens) / usage.contextWindow) * 100;
|
|
307
|
-
return Math.max(0, Math.min(100, percentRemaining));
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export function contextPercentRemaining(ctx: ExtensionContext): number | null {
|
|
311
|
-
const percentRemaining = exactPercentRemaining(ctx);
|
|
312
|
-
return percentRemaining === null ? null : Math.round(percentRemaining);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function resetCompactionState(runtime: CompactionRuntime, clearTimestamp = false): void {
|
|
316
|
-
runtime.compactionInFlight = false;
|
|
317
|
-
runtime.automaticCompactionAwaitingHook = false;
|
|
318
|
-
runtime.automaticCompactionPending = false;
|
|
319
|
-
warnedRuntimes.delete(runtime);
|
|
320
|
-
const timer = inFlightTimers.get(runtime);
|
|
321
|
-
if (timer) clearTimeout(timer);
|
|
322
|
-
inFlightTimers.delete(runtime);
|
|
323
|
-
abortCleanups.get(runtime)?.();
|
|
324
|
-
abortCleanups.delete(runtime);
|
|
325
|
-
if (clearTimestamp) runtime.lastCompactionAt = undefined;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function isCurrentCompaction(runtime: CompactionRuntime, operationId: number): boolean {
|
|
329
|
-
return runtime.compactionInFlight && runtime.compactionOperationId === operationId;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function armCompactionTimeout(runtime: CompactionRuntime, operationId: number): void {
|
|
333
|
-
// ponytail: recover stale state after five minutes because Pi exposes no failed-compaction extension event; replace with that event if Pi adds one.
|
|
334
|
-
const timer = setTimeout(() => {
|
|
335
|
-
if (!isCurrentCompaction(runtime, operationId)) return;
|
|
336
|
-
const onFailure = compactionFailureHandlers.get(runtime);
|
|
337
|
-
resetCompactionState(runtime);
|
|
338
|
-
compactionFailureHandlers.delete(runtime);
|
|
339
|
-
onFailure?.();
|
|
340
|
-
}, COMPACTION_STATE_TIMEOUT_MS);
|
|
341
|
-
timer.unref();
|
|
342
|
-
inFlightTimers.set(runtime, timer);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function markCompactionInFlight(
|
|
346
|
-
runtime: CompactionRuntime,
|
|
347
|
-
signal: AbortSignal,
|
|
348
|
-
onFailure?: () => void,
|
|
349
|
-
): number | null {
|
|
350
|
-
const expectedAutomaticHook = runtime.automaticCompactionAwaitingHook;
|
|
351
|
-
if (runtime.compactionInFlight && !expectedAutomaticHook) return null;
|
|
352
|
-
const operationId = expectedAutomaticHook
|
|
353
|
-
? runtime.compactionOperationId
|
|
354
|
-
: runtime.compactionOperationId + 1;
|
|
355
|
-
resetCompactionState(runtime);
|
|
356
|
-
if (!expectedAutomaticHook) {
|
|
357
|
-
compactionFailureHandlers.delete(runtime);
|
|
358
|
-
runtime.compactionOperationId = operationId;
|
|
359
|
-
if (onFailure) compactionFailureHandlers.set(runtime, onFailure);
|
|
360
|
-
}
|
|
361
|
-
runtime.compactionInFlight = true;
|
|
362
|
-
const onAbort = (): void => {
|
|
363
|
-
if (!isCurrentCompaction(runtime, operationId)) return;
|
|
364
|
-
const failure = compactionFailureHandlers.get(runtime);
|
|
365
|
-
resetCompactionState(runtime);
|
|
366
|
-
compactionFailureHandlers.delete(runtime);
|
|
367
|
-
failure?.();
|
|
368
|
-
};
|
|
369
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
370
|
-
abortCleanups.set(runtime, () => signal.removeEventListener("abort", onAbort));
|
|
371
|
-
armCompactionTimeout(runtime, operationId);
|
|
372
|
-
if (signal.aborted) {
|
|
373
|
-
onAbort();
|
|
374
|
-
return null;
|
|
375
|
-
}
|
|
376
|
-
return operationId;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function markAutomaticCompactionInFlight(runtime: CompactionRuntime): number {
|
|
380
|
-
resetCompactionState(runtime);
|
|
381
|
-
compactionFailureHandlers.delete(runtime);
|
|
382
|
-
const operationId = runtime.compactionOperationId + 1;
|
|
383
|
-
runtime.compactionOperationId = operationId;
|
|
384
|
-
runtime.compactionInFlight = true;
|
|
385
|
-
runtime.automaticCompactionAwaitingHook = true;
|
|
386
|
-
armCompactionTimeout(runtime, operationId);
|
|
387
|
-
return operationId;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function errorMessage(error: unknown): string {
|
|
391
|
-
return error instanceof Error ? error.message : String(error);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function notify(ctx: ExtensionContext, message: string, level: NotificationLevel): void {
|
|
395
|
-
try {
|
|
396
|
-
ctx.ui.notify(message, level);
|
|
397
|
-
} catch {
|
|
398
|
-
// The compaction callback can run after session replacement; stale UI must not escape the detached task.
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function notifyCompactionFailure(
|
|
403
|
-
pi: ExtensionAPI,
|
|
404
|
-
ctx: ExtensionContext,
|
|
405
|
-
compactionRuntime: CompactionRuntime,
|
|
406
|
-
goalRuntime: GoalRuntime,
|
|
407
|
-
error: unknown,
|
|
408
|
-
): void {
|
|
409
|
-
resetCompactionState(compactionRuntime);
|
|
410
|
-
compactionRuntime.automaticCompactionArmed = true;
|
|
411
|
-
compactionFailureHandlers.delete(compactionRuntime);
|
|
412
|
-
let goalWasPaused = false;
|
|
413
|
-
if (goalRuntime.continuationHeldForCompaction) {
|
|
414
|
-
goalRuntime.continuationHeldForCompaction = false;
|
|
415
|
-
goalRuntime.continuationHeld = false;
|
|
416
|
-
pauseGoalAfterFailure(
|
|
417
|
-
pi,
|
|
418
|
-
goalRuntime,
|
|
419
|
-
ctx,
|
|
420
|
-
`automatic context compaction failed: ${errorMessage(error)}`,
|
|
421
|
-
"Run /compact to retry context compaction, then /goal resume.",
|
|
422
|
-
);
|
|
423
|
-
goalWasPaused = true;
|
|
424
|
-
goalRuntime.requestRender?.();
|
|
425
|
-
}
|
|
426
|
-
if (!goalWasPaused) {
|
|
427
|
-
notify(ctx, `Automatic context compaction failed: ${errorMessage(error)}. Run /compact to try again.`, "error");
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function notifyPiCompactionFailure(
|
|
432
|
-
pi: ExtensionAPI,
|
|
433
|
-
ctx: ExtensionContext,
|
|
434
|
-
compactionRuntime: CompactionRuntime,
|
|
435
|
-
goalRuntime: GoalRuntime,
|
|
436
|
-
error: unknown,
|
|
437
|
-
): void {
|
|
438
|
-
resetCompactionState(compactionRuntime);
|
|
439
|
-
compactionRuntime.automaticCompactionArmed = true;
|
|
440
|
-
compactionFailureHandlers.delete(compactionRuntime);
|
|
441
|
-
if (goalRuntime.continuationHeldForCompaction) {
|
|
442
|
-
goalRuntime.continuationHeldForCompaction = false;
|
|
443
|
-
goalRuntime.continuationHeld = false;
|
|
444
|
-
pauseGoalAfterFailure(
|
|
445
|
-
pi,
|
|
446
|
-
goalRuntime,
|
|
447
|
-
ctx,
|
|
448
|
-
`context compaction failed: ${errorMessage(error)}`,
|
|
449
|
-
"Run /compact to retry context compaction, then /goal resume.",
|
|
450
|
-
);
|
|
451
|
-
goalRuntime.requestRender?.();
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function requestAutomaticCompaction(
|
|
456
|
-
pi: ExtensionAPI,
|
|
457
|
-
ctx: ExtensionContext,
|
|
458
|
-
compactionRuntime: CompactionRuntime,
|
|
459
|
-
goalRuntime: GoalRuntime,
|
|
460
|
-
percentRemaining: number | null,
|
|
461
|
-
): void {
|
|
462
|
-
const sessionGeneration = compactionRuntime.sessionGeneration;
|
|
463
|
-
compactionRuntime.automaticCompactionArmed = false;
|
|
464
|
-
const operationId = markAutomaticCompactionInFlight(compactionRuntime);
|
|
465
|
-
const handleFailure = (error: unknown, fromCleanup = false): void => {
|
|
466
|
-
if (sessionGeneration !== compactionRuntime.sessionGeneration
|
|
467
|
-
|| compactionRuntime.compactionOperationId !== operationId
|
|
468
|
-
|| (!fromCleanup && !compactionRuntime.compactionInFlight)) return;
|
|
469
|
-
try {
|
|
470
|
-
notifyCompactionFailure(pi, ctx, compactionRuntime, goalRuntime, error);
|
|
471
|
-
} catch {
|
|
472
|
-
resetCompactionState(compactionRuntime);
|
|
473
|
-
compactionFailureHandlers.delete(compactionRuntime);
|
|
474
|
-
}
|
|
475
|
-
};
|
|
476
|
-
compactionFailureHandlers.set(compactionRuntime, () => handleFailure(new Error("compaction timed out or was cancelled"), true));
|
|
477
|
-
try {
|
|
478
|
-
ctx.compact({
|
|
479
|
-
onError: handleFailure,
|
|
480
|
-
});
|
|
481
|
-
} catch (error) {
|
|
482
|
-
handleFailure(error);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const percent = percentRemaining === null ? "the threshold" : `${percentRemaining}% remaining`;
|
|
487
|
-
notify(ctx, `Context ${percent}. Compacting automatically.`, "info");
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function resetForSessionBoundary(compactionRuntime: CompactionRuntime): void {
|
|
491
|
-
compactionRuntime.sessionGeneration += 1;
|
|
492
|
-
resetCompactionState(compactionRuntime, true);
|
|
493
|
-
compactionRuntime.automaticCompactionArmed = true;
|
|
494
|
-
compactionFailureHandlers.delete(compactionRuntime);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
export function registerContextCompaction(
|
|
498
|
-
pi: ExtensionAPI,
|
|
499
|
-
compactionRuntime: CompactionRuntime,
|
|
500
|
-
goalRuntime: GoalRuntime,
|
|
501
|
-
): void {
|
|
502
|
-
pi.on("session_start", () => {
|
|
503
|
-
resetForSessionBoundary(compactionRuntime);
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
pi.on("session_tree", () => {
|
|
507
|
-
resetForSessionBoundary(compactionRuntime);
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
pi.on("session_shutdown", () => {
|
|
511
|
-
resetForSessionBoundary(compactionRuntime);
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
pi.on("turn_end", (_event, ctx) => {
|
|
515
|
-
const exactRemaining = exactPercentRemaining(ctx);
|
|
516
|
-
if (exactRemaining === null) return;
|
|
517
|
-
const percentRemaining = Math.round(exactRemaining);
|
|
518
|
-
if (exactRemaining > compactionRuntime.thresholdPercent) {
|
|
519
|
-
compactionRuntime.automaticCompactionArmed = true;
|
|
520
|
-
compactionRuntime.automaticCompactionPending = false;
|
|
521
|
-
warnedRuntimes.delete(compactionRuntime);
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
if (compactionRuntime.compactionInFlight) return;
|
|
525
|
-
if (!compactionRuntime.automaticCompactionArmed) return;
|
|
526
|
-
|
|
527
|
-
compactionRuntime.automaticCompactionPending = true;
|
|
528
|
-
if (warnedRuntimes.has(compactionRuntime)) return;
|
|
529
|
-
warnedRuntimes.add(compactionRuntime);
|
|
530
|
-
notify(
|
|
531
|
-
ctx,
|
|
532
|
-
`Context ${percentRemaining}% remaining. Automatic compaction will start when this run settles.`,
|
|
533
|
-
"warning",
|
|
534
|
-
);
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
pi.on("agent_settled", (_event, ctx) => {
|
|
538
|
-
if (compactionRuntime.compactionInFlight) return;
|
|
539
|
-
if (goalRuntime.goalTurnInFlight
|
|
540
|
-
&& (goalRuntime.lastStopReason === "error" || goalRuntime.lastStopReason === "aborted")) return;
|
|
541
|
-
const exactRemaining = exactPercentRemaining(ctx);
|
|
542
|
-
const percentRemaining = exactRemaining === null ? null : Math.round(exactRemaining);
|
|
543
|
-
if (exactRemaining !== null && exactRemaining > compactionRuntime.thresholdPercent) {
|
|
544
|
-
compactionRuntime.automaticCompactionArmed = true;
|
|
545
|
-
compactionRuntime.automaticCompactionPending = false;
|
|
546
|
-
warnedRuntimes.delete(compactionRuntime);
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
if (percentRemaining === null && !compactionRuntime.automaticCompactionPending) return;
|
|
550
|
-
if (!compactionRuntime.automaticCompactionArmed) return;
|
|
551
|
-
if (exactRemaining !== null && exactRemaining <= compactionRuntime.thresholdPercent) {
|
|
552
|
-
compactionRuntime.automaticCompactionPending = true;
|
|
553
|
-
}
|
|
554
|
-
if (!compactionRuntime.automaticCompactionPending) return;
|
|
555
|
-
|
|
556
|
-
requestAutomaticCompaction(pi, ctx, compactionRuntime, goalRuntime, percentRemaining);
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
pi.on("session_before_compact", async (event, ctx) => {
|
|
560
|
-
const expectedAutomaticHook = compactionRuntime.automaticCompactionAwaitingHook;
|
|
561
|
-
const sessionGeneration = compactionRuntime.sessionGeneration;
|
|
562
|
-
const operationId = markCompactionInFlight(
|
|
563
|
-
compactionRuntime,
|
|
564
|
-
event.signal,
|
|
565
|
-
expectedAutomaticHook
|
|
566
|
-
? undefined
|
|
567
|
-
: () => {
|
|
568
|
-
if (sessionGeneration !== compactionRuntime.sessionGeneration) return;
|
|
569
|
-
try {
|
|
570
|
-
notifyPiCompactionFailure(
|
|
571
|
-
pi,
|
|
572
|
-
ctx,
|
|
573
|
-
compactionRuntime,
|
|
574
|
-
goalRuntime,
|
|
575
|
-
new Error("compaction timed out or was cancelled"),
|
|
576
|
-
);
|
|
577
|
-
} catch {
|
|
578
|
-
resetCompactionState(compactionRuntime);
|
|
579
|
-
compactionFailureHandlers.delete(compactionRuntime);
|
|
580
|
-
}
|
|
581
|
-
},
|
|
582
|
-
);
|
|
583
|
-
if (operationId === null) return { cancel: true };
|
|
584
|
-
|
|
585
|
-
try {
|
|
586
|
-
return { compaction: await buildModelSummary(event, ctx) };
|
|
587
|
-
} catch (error) {
|
|
588
|
-
if (event.signal.aborted) return { cancel: true };
|
|
589
|
-
notify(ctx, `Model compaction failed: ${errorMessage(error)}. Using the deterministic fallback.`, "warning");
|
|
590
|
-
const goalObjective = goalRuntime.state?.status === "active"
|
|
591
|
-
? goalRuntime.state.objective
|
|
592
|
-
: undefined;
|
|
593
|
-
const summary = buildStructuredSummary(event.preparation, goalObjective, event.customInstructions);
|
|
594
|
-
|
|
595
|
-
return {
|
|
596
|
-
compaction: {
|
|
597
|
-
summary,
|
|
598
|
-
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
599
|
-
tokensBefore: event.preparation.tokensBefore,
|
|
600
|
-
details: { killerosDeterministicFallback: true },
|
|
601
|
-
},
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
pi.on("session_compact", (event, ctx) => {
|
|
607
|
-
resetCompactionState(compactionRuntime);
|
|
608
|
-
compactionFailureHandlers.delete(compactionRuntime);
|
|
609
|
-
compactionRuntime.lastCompactionAt = Date.now();
|
|
610
|
-
if (asRecord(event.compactionEntry.details)?.killerosDeterministicFallback === true) {
|
|
611
|
-
notify(ctx, COMPACTION_ACCURACY_WARNING, "warning");
|
|
612
|
-
}
|
|
613
|
-
});
|
|
614
|
-
}
|