killeros 1.5.2 → 1.5.4
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 +25 -0
- package/Killeros.ts +30 -4
- package/README.md +38 -14
- package/killeros/commands.ts +242 -2
- package/killeros/context-compaction.ts +566 -0
- package/killeros/goals.ts +25 -2
- package/killeros/runtime.ts +25 -0
- package/killeros/subagent-lifecycle.ts +237 -8
- package/killeros/subagent-persistence.ts +572 -0
- package/killeros/subagent-process.ts +583 -565
- package/killeros/subagent-ui.ts +17 -1
- package/killeros/subagents.ts +2579 -1581
- package/package.json +1 -1
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
SessionBeforeCompactEvent,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { pauseGoalAfterFailure } from "./goals.ts";
|
|
7
|
+
import type { CompactionRuntime, GoalRuntime } from "./runtime.ts";
|
|
8
|
+
|
|
9
|
+
type CompactionPreparation = SessionBeforeCompactEvent["preparation"];
|
|
10
|
+
type CompactionMessage = CompactionPreparation["messagesToSummarize"][number];
|
|
11
|
+
type MessageRecord = Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
const warnedRuntimes = new WeakSet<CompactionRuntime>();
|
|
14
|
+
const inFlightTimers = new WeakMap<CompactionRuntime, NodeJS.Timeout>();
|
|
15
|
+
const abortCleanups = new WeakMap<CompactionRuntime, () => void>();
|
|
16
|
+
const compactionFailureHandlers = new WeakMap<CompactionRuntime, () => void>();
|
|
17
|
+
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;
|
|
18
|
+
const PATH_KEYS = new Set(["path", "file", "filepath", "file_path", "filename", "target"]);
|
|
19
|
+
const MAX_MESSAGE_TEXT = 1_000;
|
|
20
|
+
const MAX_CONTEXT_ITEMS = 80;
|
|
21
|
+
const MAX_FILES = 40;
|
|
22
|
+
const MAX_PREVIOUS_SUMMARY = 6_000;
|
|
23
|
+
const MAX_CUSTOM_INSTRUCTIONS = 4_000;
|
|
24
|
+
const COMPACTION_STATE_TIMEOUT_MS = 60_000;
|
|
25
|
+
|
|
26
|
+
type NotificationLevel = Parameters<ExtensionContext["ui"]["notify"]>[1];
|
|
27
|
+
|
|
28
|
+
function asRecord(value: unknown): MessageRecord | undefined {
|
|
29
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
30
|
+
return value as MessageRecord;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function compactText(value: string, limit = MAX_MESSAGE_TEXT): string {
|
|
34
|
+
return value.replace(/\s+/gu, " ").trim().slice(0, limit);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function textFromContent(content: unknown): string[] {
|
|
38
|
+
if (typeof content === "string") return [content];
|
|
39
|
+
if (!Array.isArray(content)) return [];
|
|
40
|
+
|
|
41
|
+
const text: string[] = [];
|
|
42
|
+
for (const part of content) {
|
|
43
|
+
const record = asRecord(part);
|
|
44
|
+
if (!record) continue;
|
|
45
|
+
if (typeof record.text === "string") text.push(record.text);
|
|
46
|
+
if (record.type === "toolCall" && typeof record.name === "string") {
|
|
47
|
+
text.push(`Called ${record.name}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return text;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function messageText(message: CompactionMessage): string {
|
|
54
|
+
const record = asRecord(message);
|
|
55
|
+
if (!record) return "";
|
|
56
|
+
|
|
57
|
+
const text = textFromContent(record.content);
|
|
58
|
+
if (typeof record.summary === "string") text.unshift(record.summary);
|
|
59
|
+
return compactText(text.join(" "));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function messageRole(message: CompactionMessage): string {
|
|
63
|
+
const role = asRecord(message)?.role;
|
|
64
|
+
return typeof role === "string" && role.trim() ? role : "context";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function extractMessages(preparation: CompactionPreparation): Array<{ role: string; text: string }> {
|
|
68
|
+
const messages = [...preparation.messagesToSummarize, ...preparation.turnPrefixMessages];
|
|
69
|
+
const extracted: Array<{ role: string; text: string }> = [];
|
|
70
|
+
for (const message of messages) {
|
|
71
|
+
const text = messageText(message);
|
|
72
|
+
if (text) extracted.push({ role: messageRole(message), text });
|
|
73
|
+
if (extracted.length >= MAX_CONTEXT_ITEMS) break;
|
|
74
|
+
}
|
|
75
|
+
return extracted;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function addPath(paths: Set<string>, value: string): void {
|
|
79
|
+
const path = value.trim().replace(/^['"`([{<]+|['"`.,;:!?)}\]>]+$/gu, "");
|
|
80
|
+
if (!path || path.includes("://") || path.length > 512 || paths.size >= MAX_FILES) return;
|
|
81
|
+
paths.add(path);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function extractFilePaths(text: string, paths: Set<string>): void {
|
|
85
|
+
FILE_PATH_PATTERN.lastIndex = 0;
|
|
86
|
+
let match: RegExpExecArray | null;
|
|
87
|
+
while ((match = FILE_PATH_PATTERN.exec(text)) !== null) {
|
|
88
|
+
addPath(paths, match[0]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function collectPaths(value: unknown, paths: Set<string>, depth = 0): void {
|
|
93
|
+
if (paths.size >= MAX_FILES || depth > 4) return;
|
|
94
|
+
if (typeof value === "string") {
|
|
95
|
+
extractFilePaths(value, paths);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
for (const item of value) collectPaths(item, paths, depth + 1);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const record = asRecord(value);
|
|
104
|
+
if (!record) return;
|
|
105
|
+
for (const [key, item] of Object.entries(record)) {
|
|
106
|
+
const normalizedKey = key.toLocaleLowerCase();
|
|
107
|
+
if (PATH_KEYS.has(normalizedKey) || normalizedKey === "content" || normalizedKey === "text" || normalizedKey === "summary" || normalizedKey === "arguments" || normalizedKey === "input") {
|
|
108
|
+
collectPaths(item, paths, depth + 1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function collectMessagePaths(preparation: CompactionPreparation): Set<string> {
|
|
114
|
+
const paths = new Set<string>();
|
|
115
|
+
const messages = [...preparation.messagesToSummarize, ...preparation.turnPrefixMessages];
|
|
116
|
+
for (const message of messages) collectPaths(message, paths);
|
|
117
|
+
return paths;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function collectFileOperationPaths(preparation: CompactionPreparation): Set<string> {
|
|
121
|
+
const paths = new Set<string>();
|
|
122
|
+
const fileOps = asRecord(preparation.fileOps);
|
|
123
|
+
if (!fileOps) return paths;
|
|
124
|
+
|
|
125
|
+
for (const key of ["written", "edited"]) {
|
|
126
|
+
const values = fileOps[key];
|
|
127
|
+
if (values instanceof Set) {
|
|
128
|
+
for (const value of values) {
|
|
129
|
+
if (typeof value === "string") addPath(paths, value);
|
|
130
|
+
}
|
|
131
|
+
} else if (Array.isArray(values)) {
|
|
132
|
+
for (const value of values) {
|
|
133
|
+
if (typeof value === "string") addPath(paths, value);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return paths;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function quoteText(value: string, limit: number): string {
|
|
141
|
+
const text = value.trim().slice(0, limit);
|
|
142
|
+
return text
|
|
143
|
+
.split(/\r?\n/gu)
|
|
144
|
+
.map((line) => `> ${line}`)
|
|
145
|
+
.join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function retainSummaryEdges(value: string, limit: number): string {
|
|
149
|
+
if (value.length <= limit) return value;
|
|
150
|
+
const marker = "\n...[previous summary truncated]...\n";
|
|
151
|
+
const available = Math.max(0, limit - marker.length);
|
|
152
|
+
const headLength = Math.ceil(available * 0.6);
|
|
153
|
+
return `${value.slice(0, headLength)}${marker}${value.slice(-(available - headLength))}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function evidenceLine(item: { role: string; text: string }): string {
|
|
157
|
+
return `- [${item.role}] ${compactText(item.text, 320)}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function recentMatches(
|
|
161
|
+
messages: Array<{ role: string; text: string }>,
|
|
162
|
+
predicate: (item: { role: string; text: string }) => boolean,
|
|
163
|
+
limit: number,
|
|
164
|
+
): string[] {
|
|
165
|
+
return messages.filter(predicate).slice(-limit).map(evidenceLine);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildProgress(messages: Array<{ role: string; text: string }>): string[] {
|
|
169
|
+
const done = recentMatches(
|
|
170
|
+
messages,
|
|
171
|
+
(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),
|
|
172
|
+
4,
|
|
173
|
+
);
|
|
174
|
+
const inProgress = recentMatches(
|
|
175
|
+
messages,
|
|
176
|
+
(item) => !done.includes(evidenceLine(item)),
|
|
177
|
+
3,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const lines = ["### Done"];
|
|
181
|
+
lines.push(...(done.length ? done : ["- No completed work was clearly reported in the retained context."]));
|
|
182
|
+
lines.push("", "### In Progress", ...(inProgress.length ? inProgress : ["- The retained context does not state the current work clearly."]));
|
|
183
|
+
return lines;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildKeyDecisions(messages: Array<{ role: string; text: string }>): string[] {
|
|
187
|
+
const decisions = recentMatches(
|
|
188
|
+
messages,
|
|
189
|
+
(item) => /\b(?:decid(?:e|ed)|chose|choose|instead|must|should|will|avoid|required|prefer|keep)\b/iu.test(item.text),
|
|
190
|
+
4,
|
|
191
|
+
);
|
|
192
|
+
return ["- Extracted from conversation context.", ...(decisions.length ? decisions : ["- No explicit decisions were found in the retained context."])];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildModifiedFiles(preparation: CompactionPreparation, messages: Array<{ role: string; text: string }>): string[] {
|
|
196
|
+
const exact = collectFileOperationPaths(preparation);
|
|
197
|
+
if (exact.size) return [...exact].map((path) => `- ${path}`);
|
|
198
|
+
|
|
199
|
+
const mentioned = collectMessagePaths(preparation);
|
|
200
|
+
if (!mentioned.size) return ["- No modified files were recorded in the retained context."];
|
|
201
|
+
|
|
202
|
+
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));
|
|
203
|
+
return [
|
|
204
|
+
evidence
|
|
205
|
+
? "- Files mentioned with a change action (no structured file-operation record was retained):"
|
|
206
|
+
: "- Files mentioned in the retained context (no structured file-operation record was retained):",
|
|
207
|
+
...[...mentioned].map((path) => `- ${path}`),
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildStructuredSummary(
|
|
212
|
+
preparation: CompactionPreparation,
|
|
213
|
+
goalObjective: string | undefined,
|
|
214
|
+
customInstructions: string | undefined,
|
|
215
|
+
): string {
|
|
216
|
+
const messages = extractMessages(preparation);
|
|
217
|
+
const firstUserMessage = messages.find((item) => item.role === "user");
|
|
218
|
+
const goal = goalObjective?.trim()
|
|
219
|
+
? compactText(goalObjective, 4_000)
|
|
220
|
+
: firstUserMessage
|
|
221
|
+
? compactText(firstUserMessage.text, 4_000)
|
|
222
|
+
: "No explicit goal was retained; continue from the latest context.";
|
|
223
|
+
|
|
224
|
+
const nextSteps = [
|
|
225
|
+
"- Continue the task from where it was interrupted by compaction.",
|
|
226
|
+
"- Re-read any files that were being edited to verify current state.",
|
|
227
|
+
];
|
|
228
|
+
if (goalObjective?.trim()) nextSteps.push("- Keep the active goal moving until it is complete or clearly blocked.");
|
|
229
|
+
if (customInstructions?.trim()) {
|
|
230
|
+
nextSteps.push("", "Custom Instructions:", quoteText(customInstructions, MAX_CUSTOM_INSTRUCTIONS));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return [
|
|
234
|
+
"# KillerOS Compaction Summary",
|
|
235
|
+
"",
|
|
236
|
+
...(preparation.previousSummary?.trim()
|
|
237
|
+
? ["## Previous Summary", quoteText(retainSummaryEdges(preparation.previousSummary, MAX_PREVIOUS_SUMMARY), MAX_PREVIOUS_SUMMARY), ""]
|
|
238
|
+
: []),
|
|
239
|
+
"## Goal",
|
|
240
|
+
goal,
|
|
241
|
+
"",
|
|
242
|
+
"## Progress",
|
|
243
|
+
...buildProgress(messages),
|
|
244
|
+
"",
|
|
245
|
+
"## Key Decisions",
|
|
246
|
+
...buildKeyDecisions(messages),
|
|
247
|
+
"",
|
|
248
|
+
"## Next Steps",
|
|
249
|
+
...nextSteps,
|
|
250
|
+
"",
|
|
251
|
+
"## Modified Files",
|
|
252
|
+
...buildModifiedFiles(preparation, messages),
|
|
253
|
+
...(customInstructions?.trim()
|
|
254
|
+
? ["", "## Custom Instructions", quoteText(customInstructions, MAX_CUSTOM_INSTRUCTIONS)]
|
|
255
|
+
: []),
|
|
256
|
+
].join("\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function exactPercentRemaining(ctx: ExtensionContext): number | null {
|
|
260
|
+
let usage: ReturnType<ExtensionContext["getContextUsage"]>;
|
|
261
|
+
try {
|
|
262
|
+
usage = ctx.getContextUsage();
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
if (!usage || !Number.isFinite(usage.contextWindow) || usage.contextWindow <= 0) return null;
|
|
267
|
+
if (usage.tokens === null || !Number.isFinite(usage.tokens)) return null;
|
|
268
|
+
|
|
269
|
+
const percentRemaining = ((usage.contextWindow - usage.tokens) / usage.contextWindow) * 100;
|
|
270
|
+
return Math.max(0, Math.min(100, percentRemaining));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function contextPercentRemaining(ctx: ExtensionContext): number | null {
|
|
274
|
+
const percentRemaining = exactPercentRemaining(ctx);
|
|
275
|
+
return percentRemaining === null ? null : Math.round(percentRemaining);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function resetCompactionState(runtime: CompactionRuntime, clearTimestamp = false): void {
|
|
279
|
+
runtime.compactionInFlight = false;
|
|
280
|
+
runtime.automaticCompactionAwaitingHook = false;
|
|
281
|
+
runtime.automaticCompactionPending = false;
|
|
282
|
+
warnedRuntimes.delete(runtime);
|
|
283
|
+
const timer = inFlightTimers.get(runtime);
|
|
284
|
+
if (timer) clearTimeout(timer);
|
|
285
|
+
inFlightTimers.delete(runtime);
|
|
286
|
+
abortCleanups.get(runtime)?.();
|
|
287
|
+
abortCleanups.delete(runtime);
|
|
288
|
+
if (clearTimestamp) runtime.lastCompactionAt = undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function isCurrentCompaction(runtime: CompactionRuntime, operationId: number): boolean {
|
|
292
|
+
return runtime.compactionInFlight && runtime.compactionOperationId === operationId;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function armCompactionTimeout(runtime: CompactionRuntime, operationId: number): void {
|
|
296
|
+
// ponytail: recover stale state after 60s because Pi exposes no failed-compaction extension event; replace with that event if Pi adds one.
|
|
297
|
+
const timer = setTimeout(() => {
|
|
298
|
+
if (!isCurrentCompaction(runtime, operationId)) return;
|
|
299
|
+
const onFailure = compactionFailureHandlers.get(runtime);
|
|
300
|
+
resetCompactionState(runtime);
|
|
301
|
+
compactionFailureHandlers.delete(runtime);
|
|
302
|
+
onFailure?.();
|
|
303
|
+
}, COMPACTION_STATE_TIMEOUT_MS);
|
|
304
|
+
timer.unref();
|
|
305
|
+
inFlightTimers.set(runtime, timer);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function markCompactionInFlight(
|
|
309
|
+
runtime: CompactionRuntime,
|
|
310
|
+
signal: AbortSignal,
|
|
311
|
+
onFailure?: () => void,
|
|
312
|
+
): number | null {
|
|
313
|
+
const expectedAutomaticHook = runtime.automaticCompactionAwaitingHook;
|
|
314
|
+
if (runtime.compactionInFlight && !expectedAutomaticHook) return null;
|
|
315
|
+
const operationId = expectedAutomaticHook
|
|
316
|
+
? runtime.compactionOperationId
|
|
317
|
+
: runtime.compactionOperationId + 1;
|
|
318
|
+
resetCompactionState(runtime);
|
|
319
|
+
if (!expectedAutomaticHook) {
|
|
320
|
+
compactionFailureHandlers.delete(runtime);
|
|
321
|
+
runtime.compactionOperationId = operationId;
|
|
322
|
+
if (onFailure) compactionFailureHandlers.set(runtime, onFailure);
|
|
323
|
+
}
|
|
324
|
+
runtime.compactionInFlight = true;
|
|
325
|
+
const onAbort = (): void => {
|
|
326
|
+
if (!isCurrentCompaction(runtime, operationId)) return;
|
|
327
|
+
const failure = compactionFailureHandlers.get(runtime);
|
|
328
|
+
resetCompactionState(runtime);
|
|
329
|
+
compactionFailureHandlers.delete(runtime);
|
|
330
|
+
failure?.();
|
|
331
|
+
};
|
|
332
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
333
|
+
abortCleanups.set(runtime, () => signal.removeEventListener("abort", onAbort));
|
|
334
|
+
armCompactionTimeout(runtime, operationId);
|
|
335
|
+
if (signal.aborted) {
|
|
336
|
+
onAbort();
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
return operationId;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function markAutomaticCompactionInFlight(runtime: CompactionRuntime): number {
|
|
343
|
+
resetCompactionState(runtime);
|
|
344
|
+
compactionFailureHandlers.delete(runtime);
|
|
345
|
+
const operationId = runtime.compactionOperationId + 1;
|
|
346
|
+
runtime.compactionOperationId = operationId;
|
|
347
|
+
runtime.compactionInFlight = true;
|
|
348
|
+
runtime.automaticCompactionAwaitingHook = true;
|
|
349
|
+
armCompactionTimeout(runtime, operationId);
|
|
350
|
+
return operationId;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function errorMessage(error: unknown): string {
|
|
354
|
+
return error instanceof Error ? error.message : String(error);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function notify(ctx: ExtensionContext, message: string, level: NotificationLevel): void {
|
|
358
|
+
try {
|
|
359
|
+
ctx.ui.notify(message, level);
|
|
360
|
+
} catch {
|
|
361
|
+
// The compaction callback can run after session replacement; stale UI must not escape the detached task.
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function notifyCompactionFailure(
|
|
366
|
+
pi: ExtensionAPI,
|
|
367
|
+
ctx: ExtensionContext,
|
|
368
|
+
compactionRuntime: CompactionRuntime,
|
|
369
|
+
goalRuntime: GoalRuntime,
|
|
370
|
+
error: unknown,
|
|
371
|
+
): void {
|
|
372
|
+
resetCompactionState(compactionRuntime);
|
|
373
|
+
compactionRuntime.automaticCompactionArmed = true;
|
|
374
|
+
compactionFailureHandlers.delete(compactionRuntime);
|
|
375
|
+
let goalWasPaused = false;
|
|
376
|
+
if (goalRuntime.continuationHeldForCompaction) {
|
|
377
|
+
goalRuntime.continuationHeldForCompaction = false;
|
|
378
|
+
goalRuntime.continuationHeld = false;
|
|
379
|
+
pauseGoalAfterFailure(
|
|
380
|
+
pi,
|
|
381
|
+
goalRuntime,
|
|
382
|
+
ctx,
|
|
383
|
+
`automatic context compaction failed: ${errorMessage(error)}`,
|
|
384
|
+
"Run /compact to retry context compaction, then /goal resume.",
|
|
385
|
+
);
|
|
386
|
+
goalWasPaused = true;
|
|
387
|
+
goalRuntime.requestRender?.();
|
|
388
|
+
}
|
|
389
|
+
if (!goalWasPaused) {
|
|
390
|
+
notify(ctx, `Automatic context compaction failed: ${errorMessage(error)}. Run /compact to try again.`, "error");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function notifyPiCompactionFailure(
|
|
395
|
+
pi: ExtensionAPI,
|
|
396
|
+
ctx: ExtensionContext,
|
|
397
|
+
compactionRuntime: CompactionRuntime,
|
|
398
|
+
goalRuntime: GoalRuntime,
|
|
399
|
+
error: unknown,
|
|
400
|
+
): void {
|
|
401
|
+
resetCompactionState(compactionRuntime);
|
|
402
|
+
compactionRuntime.automaticCompactionArmed = true;
|
|
403
|
+
compactionFailureHandlers.delete(compactionRuntime);
|
|
404
|
+
if (goalRuntime.continuationHeldForCompaction) {
|
|
405
|
+
goalRuntime.continuationHeldForCompaction = false;
|
|
406
|
+
goalRuntime.continuationHeld = false;
|
|
407
|
+
pauseGoalAfterFailure(
|
|
408
|
+
pi,
|
|
409
|
+
goalRuntime,
|
|
410
|
+
ctx,
|
|
411
|
+
`context compaction failed: ${errorMessage(error)}`,
|
|
412
|
+
"Run /compact to retry context compaction, then /goal resume.",
|
|
413
|
+
);
|
|
414
|
+
goalRuntime.requestRender?.();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function requestAutomaticCompaction(
|
|
419
|
+
pi: ExtensionAPI,
|
|
420
|
+
ctx: ExtensionContext,
|
|
421
|
+
compactionRuntime: CompactionRuntime,
|
|
422
|
+
goalRuntime: GoalRuntime,
|
|
423
|
+
percentRemaining: number | null,
|
|
424
|
+
): void {
|
|
425
|
+
const sessionGeneration = compactionRuntime.sessionGeneration;
|
|
426
|
+
compactionRuntime.automaticCompactionArmed = false;
|
|
427
|
+
const operationId = markAutomaticCompactionInFlight(compactionRuntime);
|
|
428
|
+
const handleFailure = (error: unknown, fromCleanup = false): void => {
|
|
429
|
+
if (sessionGeneration !== compactionRuntime.sessionGeneration
|
|
430
|
+
|| compactionRuntime.compactionOperationId !== operationId
|
|
431
|
+
|| (!fromCleanup && !compactionRuntime.compactionInFlight)) return;
|
|
432
|
+
try {
|
|
433
|
+
notifyCompactionFailure(pi, ctx, compactionRuntime, goalRuntime, error);
|
|
434
|
+
} catch {
|
|
435
|
+
resetCompactionState(compactionRuntime);
|
|
436
|
+
compactionFailureHandlers.delete(compactionRuntime);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
compactionFailureHandlers.set(compactionRuntime, () => handleFailure(new Error("compaction timed out or was cancelled"), true));
|
|
440
|
+
try {
|
|
441
|
+
ctx.compact({
|
|
442
|
+
onError: handleFailure,
|
|
443
|
+
});
|
|
444
|
+
} catch (error) {
|
|
445
|
+
handleFailure(error);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const percent = percentRemaining === null ? "the threshold" : `${percentRemaining}% remaining`;
|
|
450
|
+
notify(ctx, `Context ${percent}. Compacting automatically.`, "info");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function resetForSessionBoundary(compactionRuntime: CompactionRuntime): void {
|
|
454
|
+
compactionRuntime.sessionGeneration += 1;
|
|
455
|
+
resetCompactionState(compactionRuntime, true);
|
|
456
|
+
compactionRuntime.automaticCompactionArmed = true;
|
|
457
|
+
compactionFailureHandlers.delete(compactionRuntime);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function registerContextCompaction(
|
|
461
|
+
pi: ExtensionAPI,
|
|
462
|
+
compactionRuntime: CompactionRuntime,
|
|
463
|
+
goalRuntime: GoalRuntime,
|
|
464
|
+
): void {
|
|
465
|
+
pi.on("session_start", () => {
|
|
466
|
+
resetForSessionBoundary(compactionRuntime);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
pi.on("session_tree", () => {
|
|
470
|
+
resetForSessionBoundary(compactionRuntime);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
pi.on("session_shutdown", () => {
|
|
474
|
+
resetForSessionBoundary(compactionRuntime);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
pi.on("turn_end", (_event, ctx) => {
|
|
478
|
+
const exactRemaining = exactPercentRemaining(ctx);
|
|
479
|
+
if (exactRemaining === null) return;
|
|
480
|
+
const percentRemaining = Math.round(exactRemaining);
|
|
481
|
+
if (exactRemaining > compactionRuntime.thresholdPercent) {
|
|
482
|
+
compactionRuntime.automaticCompactionArmed = true;
|
|
483
|
+
compactionRuntime.automaticCompactionPending = false;
|
|
484
|
+
warnedRuntimes.delete(compactionRuntime);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (compactionRuntime.compactionInFlight) return;
|
|
488
|
+
if (!compactionRuntime.automaticCompactionArmed) return;
|
|
489
|
+
|
|
490
|
+
compactionRuntime.automaticCompactionPending = true;
|
|
491
|
+
if (warnedRuntimes.has(compactionRuntime)) return;
|
|
492
|
+
warnedRuntimes.add(compactionRuntime);
|
|
493
|
+
notify(
|
|
494
|
+
ctx,
|
|
495
|
+
`Context ${percentRemaining}% remaining. Automatic compaction will start when this run settles.`,
|
|
496
|
+
"warning",
|
|
497
|
+
);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
501
|
+
if (compactionRuntime.compactionInFlight) return;
|
|
502
|
+
if (goalRuntime.goalTurnInFlight
|
|
503
|
+
&& (goalRuntime.lastStopReason === "error" || goalRuntime.lastStopReason === "aborted")) return;
|
|
504
|
+
const exactRemaining = exactPercentRemaining(ctx);
|
|
505
|
+
const percentRemaining = exactRemaining === null ? null : Math.round(exactRemaining);
|
|
506
|
+
if (exactRemaining !== null && exactRemaining > compactionRuntime.thresholdPercent) {
|
|
507
|
+
compactionRuntime.automaticCompactionArmed = true;
|
|
508
|
+
compactionRuntime.automaticCompactionPending = false;
|
|
509
|
+
warnedRuntimes.delete(compactionRuntime);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (percentRemaining === null && !compactionRuntime.automaticCompactionPending) return;
|
|
513
|
+
if (!compactionRuntime.automaticCompactionArmed) return;
|
|
514
|
+
if (exactRemaining !== null && exactRemaining <= compactionRuntime.thresholdPercent) {
|
|
515
|
+
compactionRuntime.automaticCompactionPending = true;
|
|
516
|
+
}
|
|
517
|
+
if (!compactionRuntime.automaticCompactionPending) return;
|
|
518
|
+
|
|
519
|
+
requestAutomaticCompaction(pi, ctx, compactionRuntime, goalRuntime, percentRemaining);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
pi.on("session_before_compact", (event, ctx) => {
|
|
523
|
+
const expectedAutomaticHook = compactionRuntime.automaticCompactionAwaitingHook;
|
|
524
|
+
const sessionGeneration = compactionRuntime.sessionGeneration;
|
|
525
|
+
const operationId = markCompactionInFlight(
|
|
526
|
+
compactionRuntime,
|
|
527
|
+
event.signal,
|
|
528
|
+
expectedAutomaticHook
|
|
529
|
+
? undefined
|
|
530
|
+
: () => {
|
|
531
|
+
if (sessionGeneration !== compactionRuntime.sessionGeneration) return;
|
|
532
|
+
try {
|
|
533
|
+
notifyPiCompactionFailure(
|
|
534
|
+
pi,
|
|
535
|
+
ctx,
|
|
536
|
+
compactionRuntime,
|
|
537
|
+
goalRuntime,
|
|
538
|
+
new Error("compaction timed out or was cancelled"),
|
|
539
|
+
);
|
|
540
|
+
} catch {
|
|
541
|
+
resetCompactionState(compactionRuntime);
|
|
542
|
+
compactionFailureHandlers.delete(compactionRuntime);
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
);
|
|
546
|
+
if (operationId === null) return { cancel: true };
|
|
547
|
+
const goalObjective = goalRuntime.state?.status === "active"
|
|
548
|
+
? goalRuntime.state.objective
|
|
549
|
+
: undefined;
|
|
550
|
+
const summary = buildStructuredSummary(event.preparation, goalObjective, event.customInstructions);
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
compaction: {
|
|
554
|
+
summary,
|
|
555
|
+
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
556
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
pi.on("session_compact", () => {
|
|
562
|
+
resetCompactionState(compactionRuntime);
|
|
563
|
+
compactionFailureHandlers.delete(compactionRuntime);
|
|
564
|
+
compactionRuntime.lastCompactionAt = Date.now();
|
|
565
|
+
});
|
|
566
|
+
}
|
package/killeros/goals.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { CONCISE_SYSTEM_PROMPT } from "./concise.ts";
|
|
|
6
6
|
import { formatTime, formatTokens } from "./display.ts";
|
|
7
7
|
import { reportError } from "./errors.ts";
|
|
8
8
|
import { resolvePersonalInstructions } from "./personal-instructions.ts";
|
|
9
|
-
import type { GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
|
|
9
|
+
import type { CompactionRuntime, GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
|
|
10
10
|
|
|
11
11
|
const GOAL_ENTRY_TYPE = "killeros-goal";
|
|
12
12
|
const GOAL_CONTINUATION_TYPE = "killeros-goal-continuation";
|
|
@@ -185,7 +185,7 @@ function goalStatusSummary(state: GoalState, ctx: ExtensionContext): string {
|
|
|
185
185
|
return lines.join("\n");
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
function pauseGoalAfterFailure(
|
|
188
|
+
export function pauseGoalAfterFailure(
|
|
189
189
|
pi: ExtensionAPI,
|
|
190
190
|
runtime: GoalRuntime,
|
|
191
191
|
ctx: ExtensionContext,
|
|
@@ -339,6 +339,7 @@ export function registerGoal(
|
|
|
339
339
|
runtime.state = restoreGoalState(ctx);
|
|
340
340
|
runtime.continuationScheduled = false;
|
|
341
341
|
runtime.continuationHeld = false;
|
|
342
|
+
runtime.continuationHeldForCompaction = false;
|
|
342
343
|
runtime.goalTurnInFlight = false;
|
|
343
344
|
runtime.agentEndObserved = false;
|
|
344
345
|
runtime.persistenceRetryNeeded = false;
|
|
@@ -354,6 +355,7 @@ export function registerGoal(
|
|
|
354
355
|
runtime.state = restoreGoalState(ctx);
|
|
355
356
|
runtime.continuationScheduled = false;
|
|
356
357
|
runtime.continuationHeld = false;
|
|
358
|
+
runtime.continuationHeldForCompaction = false;
|
|
357
359
|
runtime.goalTurnInFlight = false;
|
|
358
360
|
runtime.agentEndObserved = false;
|
|
359
361
|
runtime.persistenceRetryNeeded = false;
|
|
@@ -382,6 +384,7 @@ export function registerGoal(
|
|
|
382
384
|
runtime.state = undefined;
|
|
383
385
|
runtime.continuationScheduled = false;
|
|
384
386
|
runtime.continuationHeld = false;
|
|
387
|
+
runtime.continuationHeldForCompaction = false;
|
|
385
388
|
runtime.goalTurnInFlight = false;
|
|
386
389
|
runtime.agentEndObserved = false;
|
|
387
390
|
runtime.persistenceRetryNeeded = false;
|
|
@@ -688,6 +691,7 @@ export function registerGoalSettlement(
|
|
|
688
691
|
pi: ExtensionAPI,
|
|
689
692
|
runtime: GoalRuntime,
|
|
690
693
|
initState: InitRuntime,
|
|
694
|
+
compactionRuntime?: CompactionRuntime,
|
|
691
695
|
): void {
|
|
692
696
|
pi.on("agent_settled", (_event, ctx) => {
|
|
693
697
|
const wasGoalTurn = runtime.goalTurnInFlight;
|
|
@@ -713,8 +717,27 @@ export function registerGoalSettlement(
|
|
|
713
717
|
pauseGoalAfterFailure(pi, runtime, ctx, reason);
|
|
714
718
|
return;
|
|
715
719
|
}
|
|
720
|
+
if (compactionRuntime?.compactionInFlight) {
|
|
721
|
+
runtime.continuationHeld = true;
|
|
722
|
+
runtime.continuationHeldForCompaction = true;
|
|
723
|
+
runtime.requestRender?.();
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
716
726
|
runtime.lastStopReason = undefined;
|
|
717
727
|
runtime.lastError = undefined;
|
|
718
728
|
scheduleGoalContinuation(pi, runtime, initState, ctx);
|
|
719
729
|
});
|
|
730
|
+
|
|
731
|
+
if (compactionRuntime) {
|
|
732
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
733
|
+
if (!runtime.continuationHeldForCompaction) return;
|
|
734
|
+
runtime.continuationHeldForCompaction = false;
|
|
735
|
+
if (!runtime.continuationHeld || runtime.state?.status !== "active" || initState.active) {
|
|
736
|
+
runtime.continuationHeld = false;
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
runtime.continuationHeld = false;
|
|
740
|
+
setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
|
|
741
|
+
});
|
|
742
|
+
}
|
|
720
743
|
}
|
package/killeros/runtime.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface GoalRuntime {
|
|
|
29
29
|
state?: GoalState;
|
|
30
30
|
continuationScheduled: boolean;
|
|
31
31
|
continuationHeld: boolean;
|
|
32
|
+
continuationHeldForCompaction: boolean;
|
|
32
33
|
goalTurnInFlight: boolean;
|
|
33
34
|
agentEndObserved: boolean;
|
|
34
35
|
persistenceRetryNeeded: boolean;
|
|
@@ -45,6 +46,7 @@ export function createGoalRuntime(): GoalRuntime {
|
|
|
45
46
|
return {
|
|
46
47
|
continuationScheduled: false,
|
|
47
48
|
continuationHeld: false,
|
|
49
|
+
continuationHeldForCompaction: false,
|
|
48
50
|
goalTurnInFlight: false,
|
|
49
51
|
agentEndObserved: false,
|
|
50
52
|
persistenceRetryNeeded: false,
|
|
@@ -59,3 +61,26 @@ export function resetInitRuntime(state: InitRuntime): void {
|
|
|
59
61
|
state.projectRoot = undefined;
|
|
60
62
|
state.activeTools = undefined;
|
|
61
63
|
}
|
|
64
|
+
|
|
65
|
+
export interface CompactionRuntime {
|
|
66
|
+
compactionInFlight: boolean;
|
|
67
|
+
automaticCompactionArmed: boolean;
|
|
68
|
+
automaticCompactionAwaitingHook: boolean;
|
|
69
|
+
automaticCompactionPending: boolean;
|
|
70
|
+
compactionOperationId: number;
|
|
71
|
+
sessionGeneration: number;
|
|
72
|
+
lastCompactionAt?: number;
|
|
73
|
+
thresholdPercent: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createCompactionRuntime(): CompactionRuntime {
|
|
77
|
+
return {
|
|
78
|
+
compactionInFlight: false,
|
|
79
|
+
automaticCompactionArmed: true,
|
|
80
|
+
automaticCompactionAwaitingHook: false,
|
|
81
|
+
automaticCompactionPending: false,
|
|
82
|
+
compactionOperationId: 0,
|
|
83
|
+
sessionGeneration: 0,
|
|
84
|
+
thresholdPercent: 30,
|
|
85
|
+
};
|
|
86
|
+
}
|