killeros 2.0.21 → 2.1.22
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 +35 -0
- package/Killeros.ts +5 -2
- package/README.md +25 -3
- package/killeros/auto-compaction.ts +56 -15
- package/killeros/change-receipt.ts +727 -0
- package/killeros/commands.ts +1 -1
- package/killeros/footer.ts +157 -32
- package/killeros/goal-command.ts +120 -0
- package/killeros/goal-history.ts +71 -0
- package/killeros/goal-interface.ts +599 -0
- package/killeros/goal-runtime.ts +349 -0
- package/killeros/goal-settlement.ts +261 -0
- package/killeros/goal-state.ts +95 -13
- package/killeros/hooks.ts +144 -20
- package/killeros/personal-instructions.ts +5 -0
- package/killeros/runtime.ts +9 -0
- package/killeros/worked-for.ts +271 -61
- package/package.json +3 -2
- package/killeros/goals.ts +0 -1022
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { reportError } from "./errors.ts";
|
|
3
|
+
import { beginGoalTurnState, checkpointActiveGoalState, GOAL_VERSION, parseGoalState, pauseGoalState, transitionGoalState, type GoalTransitionOptions } from "./goal-state.ts";
|
|
4
|
+
import { resolvePersonalInstructions } from "./personal-instructions.ts";
|
|
5
|
+
import type { GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
|
|
6
|
+
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
7
|
+
|
|
8
|
+
export const GOAL_ENTRY_TYPE = "killeros-goal";
|
|
9
|
+
const GOAL_CONTINUATION_TYPE = "killeros-goal-continuation";
|
|
10
|
+
export const GOAL_UPDATE_TOOL = "killeros_goal_update";
|
|
11
|
+
|
|
12
|
+
export type GoalEntryEvent = "set" | "replace" | "edit" | "check" | "limit" | "turn" | "pause" | "resume" | "blocked" | "complete" | "error" | "clear" | "checkpoint" | "blocker-audit";
|
|
13
|
+
export interface GoalEntryData {
|
|
14
|
+
version: 1;
|
|
15
|
+
event: GoalEntryEvent;
|
|
16
|
+
state: GoalState | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isUnknownRecord(value: unknown): value is Record<string, unknown> {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface RestoredGoalState {
|
|
24
|
+
state?: GoalState;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function goalBranchEntries(ctx: ExtensionContext): ReturnType<ExtensionContext["sessionManager"]["getEntries"]> {
|
|
28
|
+
try {
|
|
29
|
+
return ctx.sessionManager.getBranch();
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function restoreGoalState(ctx: ExtensionContext): RestoredGoalState {
|
|
36
|
+
const entries = goalBranchEntries(ctx);
|
|
37
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
38
|
+
const entry = entries[index];
|
|
39
|
+
if (entry?.type !== "custom" || entry.customType !== GOAL_ENTRY_TYPE) continue;
|
|
40
|
+
const data: unknown = entry.data;
|
|
41
|
+
if (!isUnknownRecord(data) || data.version !== GOAL_VERSION || data.state === null) {
|
|
42
|
+
return { state: undefined };
|
|
43
|
+
}
|
|
44
|
+
// v2.0.18 shutdown checkpoints stopped active clocks by omitting activeStartedAt.
|
|
45
|
+
const savedState = data.event === "checkpoint"
|
|
46
|
+
&& isUnknownRecord(data.state)
|
|
47
|
+
&& data.state.status === "active"
|
|
48
|
+
&& data.state.activeStartedAt === undefined
|
|
49
|
+
? { ...data.state, activeStartedAt: Date.now() }
|
|
50
|
+
: data.state;
|
|
51
|
+
const restored = parseGoalState(savedState);
|
|
52
|
+
if (!restored) return { state: undefined };
|
|
53
|
+
if (restored.status === "active") {
|
|
54
|
+
return { state: { ...restored, activeStartedAt: Date.now() } };
|
|
55
|
+
}
|
|
56
|
+
if (restored.status === "paused") {
|
|
57
|
+
const { resumeAfterManualCompaction: _resume, ...state } = restored;
|
|
58
|
+
return { state };
|
|
59
|
+
}
|
|
60
|
+
return { state: restored };
|
|
61
|
+
}
|
|
62
|
+
return { state: undefined };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function sumGoalTokens(ctx: ExtensionContext): number {
|
|
66
|
+
let total = 0;
|
|
67
|
+
for (const entry of goalBranchEntries(ctx)) {
|
|
68
|
+
if (entry.type === "message" && (entry.message.role === "assistant" || entry.message.role === "toolResult")) {
|
|
69
|
+
total += entry.message.usage?.totalTokens ?? 0;
|
|
70
|
+
} else if ((entry.type === "compaction" || entry.type === "branch_summary") && entry.usage) {
|
|
71
|
+
total += entry.usage.totalTokens;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return total;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function setGoalUpdateToolActive(pi: ExtensionAPI, active: boolean): void {
|
|
78
|
+
const activeTools = pi.getActiveTools();
|
|
79
|
+
const isActive = activeTools.includes(GOAL_UPDATE_TOOL);
|
|
80
|
+
if (active === isActive) return;
|
|
81
|
+
pi.setActiveTools(active
|
|
82
|
+
? [...activeTools, GOAL_UPDATE_TOOL]
|
|
83
|
+
: activeTools.filter((name) => name !== GOAL_UPDATE_TOOL));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function syncGoalUpdateTool(pi: ExtensionAPI, runtime: GoalRuntime): void {
|
|
87
|
+
setGoalUpdateToolActive(pi, runtime.state?.status === "active");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function persistGoalState(
|
|
91
|
+
pi: ExtensionAPI,
|
|
92
|
+
runtime: GoalRuntime,
|
|
93
|
+
event: GoalEntryEvent,
|
|
94
|
+
state: GoalState | undefined,
|
|
95
|
+
): void {
|
|
96
|
+
const data: GoalEntryData = { version: GOAL_VERSION, event, state: state ?? null };
|
|
97
|
+
runtime.automaticCompaction = undefined;
|
|
98
|
+
pi.appendEntry(GOAL_ENTRY_TYPE, data);
|
|
99
|
+
runtime.state = state;
|
|
100
|
+
syncGoalUpdateTool(pi, runtime);
|
|
101
|
+
runtime.persistenceRetryNeeded = false;
|
|
102
|
+
runtime.requestRender?.();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function transitionGoal(
|
|
106
|
+
pi: ExtensionAPI,
|
|
107
|
+
runtime: GoalRuntime,
|
|
108
|
+
event: GoalEntryEvent,
|
|
109
|
+
status: GoalStatus,
|
|
110
|
+
result?: string,
|
|
111
|
+
options: GoalTransitionOptions = {},
|
|
112
|
+
): GoalState {
|
|
113
|
+
const current = runtime.state;
|
|
114
|
+
if (!current) throw new Error("No goal is set");
|
|
115
|
+
const next = transitionGoalState(current, status, result, options, Date.now());
|
|
116
|
+
persistGoalState(pi, runtime, event, next);
|
|
117
|
+
if (status !== "active") {
|
|
118
|
+
runtime.continuationScheduled = false;
|
|
119
|
+
runtime.automaticCompaction = undefined;
|
|
120
|
+
}
|
|
121
|
+
return next;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function pauseGoalAtTurnLimit(
|
|
125
|
+
pi: ExtensionAPI,
|
|
126
|
+
runtime: GoalRuntime,
|
|
127
|
+
ctx: ExtensionContext,
|
|
128
|
+
notify = true,
|
|
129
|
+
): boolean {
|
|
130
|
+
const state = runtime.state;
|
|
131
|
+
if (state?.status !== "active" || state.maxTurns === undefined || state.turns < state.maxTurns) return false;
|
|
132
|
+
const result = `Turn limit reached (${state.turns}/${state.maxTurns}).`;
|
|
133
|
+
try {
|
|
134
|
+
transitionGoal(pi, runtime, "limit", "paused", result);
|
|
135
|
+
if (notify) ctx.ui.notify(`Goal paused: turn limit reached (${state.turns}/${state.maxTurns})`, "warning");
|
|
136
|
+
} catch (error) {
|
|
137
|
+
pauseGoalAfterFailure(pi, runtime, ctx, `turn limit pause could not be saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function clearGoalExecutionFlags(runtime: GoalRuntime): void {
|
|
143
|
+
runtime.continuationScheduled = false;
|
|
144
|
+
runtime.goalTurnInFlight = false;
|
|
145
|
+
runtime.agentEndObserved = false;
|
|
146
|
+
runtime.automaticCompaction = undefined;
|
|
147
|
+
runtime.lastStopReason = undefined;
|
|
148
|
+
runtime.lastError = undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function stopGoalRun(runtime: GoalRuntime, ctx: ExtensionCommandContext, shouldStop: boolean): Promise<void> {
|
|
152
|
+
if (!shouldStop) return;
|
|
153
|
+
try {
|
|
154
|
+
ctx.abort();
|
|
155
|
+
await ctx.waitForIdle();
|
|
156
|
+
} finally {
|
|
157
|
+
clearGoalExecutionFlags(runtime);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function pauseGoalAfterFailure(
|
|
162
|
+
pi: ExtensionAPI,
|
|
163
|
+
runtime: GoalRuntime,
|
|
164
|
+
ctx: ExtensionContext,
|
|
165
|
+
reason: string,
|
|
166
|
+
recoveryInstruction = "Run /goal resume after resolving the problem.",
|
|
167
|
+
notify = true,
|
|
168
|
+
): void {
|
|
169
|
+
if (runtime.state?.status !== "active") return;
|
|
170
|
+
const safeReason = safeTerminalText(reason);
|
|
171
|
+
try {
|
|
172
|
+
transitionGoal(pi, runtime, "error", "paused", safeReason);
|
|
173
|
+
} catch {
|
|
174
|
+
const current = runtime.state;
|
|
175
|
+
runtime.state = current ? pauseGoalState(current, safeReason, Date.now()) : undefined;
|
|
176
|
+
syncGoalUpdateTool(pi, runtime);
|
|
177
|
+
runtime.persistenceRetryNeeded = true;
|
|
178
|
+
runtime.continuationScheduled = false;
|
|
179
|
+
runtime.automaticCompaction = undefined;
|
|
180
|
+
runtime.requestRender?.();
|
|
181
|
+
}
|
|
182
|
+
if (notify) ctx.ui.notify(`Goal paused: ${safeReason}\n${recoveryInstruction}`, "error");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function beginGoalTurn(
|
|
186
|
+
pi: ExtensionAPI,
|
|
187
|
+
runtime: GoalRuntime,
|
|
188
|
+
ctx: ExtensionContext,
|
|
189
|
+
current: Extract<GoalState, { status: "active" }>,
|
|
190
|
+
): GoalState | undefined {
|
|
191
|
+
const next = beginGoalTurnState(current, Date.now());
|
|
192
|
+
try {
|
|
193
|
+
persistGoalState(pi, runtime, "turn", next);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
pauseGoalAfterFailure(pi, runtime, ctx, `turn state could not be saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
runtime.goalTurnInFlight = true;
|
|
199
|
+
runtime.agentEndObserved = false;
|
|
200
|
+
runtime.lastStopReason = undefined;
|
|
201
|
+
runtime.lastError = undefined;
|
|
202
|
+
return next;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Starts one goal turn only after Pi is idle and all competing workflow gates are clear. */
|
|
206
|
+
export function scheduleGoalContinuation(
|
|
207
|
+
pi: ExtensionAPI,
|
|
208
|
+
runtime: GoalRuntime,
|
|
209
|
+
initState: InitRuntime,
|
|
210
|
+
ctx: ExtensionContext,
|
|
211
|
+
): boolean {
|
|
212
|
+
if (isGoalModeSupported(ctx) && isSavedSession(ctx) && pauseGoalAtTurnLimit(pi, runtime, ctx)) return false;
|
|
213
|
+
if (!isGoalModeSupported(ctx)
|
|
214
|
+
|| !isSavedSession(ctx)
|
|
215
|
+
|| runtime.state?.status !== "active"
|
|
216
|
+
|| runtime.continuationScheduled
|
|
217
|
+
|| runtime.continuationHeld
|
|
218
|
+
|| runtime.goalTurnInFlight
|
|
219
|
+
|| initState.active
|
|
220
|
+
|| !ctx.isIdle()
|
|
221
|
+
|| ctx.hasPendingMessages()) return false;
|
|
222
|
+
runtime.continuationScheduled = true;
|
|
223
|
+
const next = beginGoalTurn(pi, runtime, ctx, runtime.state);
|
|
224
|
+
if (!next) return false;
|
|
225
|
+
try {
|
|
226
|
+
pi.sendMessage({
|
|
227
|
+
customType: GOAL_CONTINUATION_TYPE,
|
|
228
|
+
content: goalContinuationMessage(next, ctx),
|
|
229
|
+
display: false,
|
|
230
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
231
|
+
return true;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
runtime.continuationScheduled = false;
|
|
234
|
+
runtime.goalTurnInFlight = false;
|
|
235
|
+
pauseGoalAfterFailure(pi, runtime, ctx, `continuation could not start: ${error instanceof Error ? error.message : String(error)}`);
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function goalInstructions(state: GoalState, heading: string): string {
|
|
241
|
+
return [
|
|
242
|
+
`# ${heading}`,
|
|
243
|
+
`Status: active · Turn: ${state.turns}`,
|
|
244
|
+
"Objective:",
|
|
245
|
+
state.objective,
|
|
246
|
+
"",
|
|
247
|
+
"Treat the exact objective above from /goal as authoritative; a compaction summary may describe it but does not replace it.",
|
|
248
|
+
"If the current context contains a compaction summary, take its first concrete next step after checking the current repository state.",
|
|
249
|
+
"Continue making concrete progress toward this unchanged objective. Re-check repository state and prior results instead of repeating work.",
|
|
250
|
+
"Do not stop merely because one response is complete: KillerOS will start another goal turn while the goal remains active.",
|
|
251
|
+
"Before declaring completion, audit every part of the objective and verify the relevant results. Then call killeros_goal_update with status complete and concise evidence.",
|
|
252
|
+
"Call killeros_goal_update with status blocked and the same lowercase blockerKey on each turn where one external impasse persists; attempts one and two record the audit, and attempt three marks the goal blocked.",
|
|
253
|
+
"Never use the goal tool to pause, resume, edit, replace, or clear the objective. Those transitions belong to the user.",
|
|
254
|
+
].join("\n");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function goalSystemPrompt(state: GoalState): string {
|
|
258
|
+
return goalInstructions(state, "Active KillerOS goal");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function goalContinuationMessage(state: GoalState, ctx: ExtensionContext): string {
|
|
262
|
+
const sections = [goalInstructions(state, "KillerOS long-running goal turn")];
|
|
263
|
+
if (ctx.isProjectTrusted()) {
|
|
264
|
+
const personal = resolvePersonalInstructions(ctx.cwd);
|
|
265
|
+
if (personal) {
|
|
266
|
+
sections.push(personal);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return sections.join("\n\n");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function isGoalModeSupported(ctx: ExtensionContext): boolean {
|
|
273
|
+
return ctx.mode === "tui" || ctx.mode === "rpc";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function isSavedSession(ctx: ExtensionContext): boolean {
|
|
277
|
+
try {
|
|
278
|
+
return Boolean(ctx.sessionManager.getSessionFile());
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function registerGoalRuntime(
|
|
285
|
+
pi: ExtensionAPI,
|
|
286
|
+
runtime: GoalRuntime,
|
|
287
|
+
initState: InitRuntime,
|
|
288
|
+
): void {
|
|
289
|
+
const restoreGoal = (ctx: ExtensionContext): void => {
|
|
290
|
+
const restored = restoreGoalState(ctx);
|
|
291
|
+
runtime.state = isGoalModeSupported(ctx) ? restored.state : undefined;
|
|
292
|
+
syncGoalUpdateTool(pi, runtime);
|
|
293
|
+
runtime.continuationScheduled = false;
|
|
294
|
+
runtime.continuationHeld = false;
|
|
295
|
+
runtime.goalTurnInFlight = false;
|
|
296
|
+
runtime.agentEndObserved = false;
|
|
297
|
+
runtime.automaticCompaction = undefined;
|
|
298
|
+
runtime.persistenceRetryNeeded = false;
|
|
299
|
+
runtime.lastStopReason = undefined;
|
|
300
|
+
runtime.lastError = undefined;
|
|
301
|
+
runtime.requestRender?.();
|
|
302
|
+
if (runtime.state?.status === "active") {
|
|
303
|
+
setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
pi.on("session_start", (_event, ctx) => restoreGoal(ctx));
|
|
308
|
+
pi.on("session_tree", (_event, ctx) => restoreGoal(ctx));
|
|
309
|
+
|
|
310
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
311
|
+
if (runtime.state?.status === "active") {
|
|
312
|
+
const checkpoint = checkpointActiveGoalState(runtime.state, Date.now());
|
|
313
|
+
try {
|
|
314
|
+
persistGoalState(pi, runtime, "checkpoint", checkpoint);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
reportError(ctx, "Goal state could not be checkpointed", error);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
runtime.state = undefined;
|
|
320
|
+
syncGoalUpdateTool(pi, runtime);
|
|
321
|
+
runtime.continuationScheduled = false;
|
|
322
|
+
runtime.continuationHeld = false;
|
|
323
|
+
runtime.goalTurnInFlight = false;
|
|
324
|
+
runtime.agentEndObserved = false;
|
|
325
|
+
runtime.automaticCompaction = undefined;
|
|
326
|
+
runtime.persistenceRetryNeeded = false;
|
|
327
|
+
runtime.lastStopReason = undefined;
|
|
328
|
+
runtime.lastError = undefined;
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
332
|
+
runtime.continuationScheduled = false;
|
|
333
|
+
if (!runtime.goalTurnInFlight && isGoalModeSupported(ctx) && isSavedSession(ctx) && pauseGoalAtTurnLimit(pi, runtime, ctx)) return;
|
|
334
|
+
const current = runtime.state;
|
|
335
|
+
if (!isGoalModeSupported(ctx) || !isSavedSession(ctx) || !current || current.status !== "active" || initState.active) return;
|
|
336
|
+
if (runtime.goalTurnInFlight) return { systemPrompt: `${event.systemPrompt}\n\n${goalSystemPrompt(current)}` };
|
|
337
|
+
const next = beginGoalTurn(pi, runtime, ctx, current);
|
|
338
|
+
if (!next) return;
|
|
339
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${goalSystemPrompt(next)}` };
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
pi.on("agent_end", (event) => {
|
|
343
|
+
if (!runtime.goalTurnInFlight) return;
|
|
344
|
+
const finalAssistant = [...event.messages].reverse().find((message) => message.role === "assistant");
|
|
345
|
+
runtime.agentEndObserved = finalAssistant !== undefined;
|
|
346
|
+
runtime.lastStopReason = finalAssistant?.stopReason;
|
|
347
|
+
runtime.lastError = finalAssistant?.errorMessage;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AutoCompactionGoalHandlers } from "./auto-compaction.ts";
|
|
3
|
+
import { reportError } from "./errors.ts";
|
|
4
|
+
import { isGoalModeSupported, isSavedSession, pauseGoalAfterFailure, pauseGoalAtTurnLimit, scheduleGoalContinuation, syncGoalUpdateTool, transitionGoal } from "./goal-runtime.ts";
|
|
5
|
+
import { pauseGoalState } from "./goal-state.ts";
|
|
6
|
+
import type { GoalRuntime, InitRuntime } from "./runtime.ts";
|
|
7
|
+
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
8
|
+
|
|
9
|
+
function pauseGoalForPossibleManualCompaction(
|
|
10
|
+
pi: ExtensionAPI,
|
|
11
|
+
runtime: GoalRuntime,
|
|
12
|
+
ctx: ExtensionContext,
|
|
13
|
+
reason: string,
|
|
14
|
+
): void {
|
|
15
|
+
if (runtime.state?.status !== "active") return;
|
|
16
|
+
const safeReason = safeTerminalText(reason);
|
|
17
|
+
try {
|
|
18
|
+
transitionGoal(pi, runtime, "error", "paused", safeReason, {
|
|
19
|
+
resumeAfterManualCompaction: true,
|
|
20
|
+
});
|
|
21
|
+
} catch {
|
|
22
|
+
const current = runtime.state;
|
|
23
|
+
runtime.state = current ? pauseGoalState(current, safeReason, Date.now(), true) : undefined;
|
|
24
|
+
syncGoalUpdateTool(pi, runtime);
|
|
25
|
+
runtime.persistenceRetryNeeded = true;
|
|
26
|
+
runtime.continuationScheduled = false;
|
|
27
|
+
runtime.automaticCompaction = undefined;
|
|
28
|
+
runtime.requestRender?.();
|
|
29
|
+
}
|
|
30
|
+
ctx.ui.notify(
|
|
31
|
+
"Goal paused because the turn was aborted. If /compact is running, KillerOS will resume after Pi saves the summary. Run /goal pause to keep it paused.",
|
|
32
|
+
"warning",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function recoverGoalAfterManualCompaction(
|
|
37
|
+
pi: ExtensionAPI,
|
|
38
|
+
runtime: GoalRuntime,
|
|
39
|
+
initState: InitRuntime,
|
|
40
|
+
ctx: ExtensionContext,
|
|
41
|
+
): boolean {
|
|
42
|
+
if (runtime.state?.status !== "paused"
|
|
43
|
+
|| runtime.state.resumeAfterManualCompaction !== true
|
|
44
|
+
|| initState.active) return false;
|
|
45
|
+
try {
|
|
46
|
+
transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
|
|
47
|
+
} catch (error) {
|
|
48
|
+
runtime.persistenceRetryNeeded = true;
|
|
49
|
+
reportError(ctx, "Manual compaction succeeded, but the goal could not be resumed", error);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
runtime.continuationScheduled = false;
|
|
53
|
+
runtime.automaticCompaction = undefined;
|
|
54
|
+
ctx.ui.notify("Manual compaction complete. Goal resumed.", "info");
|
|
55
|
+
setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Resumes the paused revision after both compaction and goal-turn settlement report an outcome. */
|
|
60
|
+
function finalizeAutomaticCompaction(
|
|
61
|
+
pi: ExtensionAPI,
|
|
62
|
+
runtime: GoalRuntime,
|
|
63
|
+
initState: InitRuntime,
|
|
64
|
+
ctx: ExtensionContext,
|
|
65
|
+
): void {
|
|
66
|
+
const recovery = runtime.automaticCompaction;
|
|
67
|
+
if (!recovery || recovery.outcome === "pending" || !recovery.turnSettled) return;
|
|
68
|
+
const skipped = recovery.outcome === "skipped";
|
|
69
|
+
runtime.automaticCompaction = undefined;
|
|
70
|
+
if (runtime.state?.status !== "paused"
|
|
71
|
+
|| runtime.state.revision !== recovery.pausedRevision
|
|
72
|
+
|| initState.active) return;
|
|
73
|
+
try {
|
|
74
|
+
transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
runtime.persistenceRetryNeeded = true;
|
|
77
|
+
reportError(ctx, skipped
|
|
78
|
+
? "Automatic compaction was skipped, but the goal could not be resumed"
|
|
79
|
+
: "Automatic compaction succeeded, but the goal could not be resumed", error);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
runtime.continuationScheduled = false;
|
|
83
|
+
setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Records Pi's successful compaction callback and attempts guarded recovery. */
|
|
87
|
+
function completeAutomaticCompaction(
|
|
88
|
+
pi: ExtensionAPI,
|
|
89
|
+
runtime: GoalRuntime,
|
|
90
|
+
initState: InitRuntime,
|
|
91
|
+
ctx: ExtensionContext,
|
|
92
|
+
): void {
|
|
93
|
+
if (!runtime.automaticCompaction) return;
|
|
94
|
+
runtime.automaticCompaction.outcome = "completed";
|
|
95
|
+
finalizeAutomaticCompaction(pi, runtime, initState, ctx);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Records Pi's expected session-too-small rejection and resumes without claiming compaction succeeded. */
|
|
99
|
+
function skipAutomaticCompaction(
|
|
100
|
+
pi: ExtensionAPI,
|
|
101
|
+
runtime: GoalRuntime,
|
|
102
|
+
initState: InitRuntime,
|
|
103
|
+
ctx: ExtensionContext,
|
|
104
|
+
): void {
|
|
105
|
+
if (!runtime.automaticCompaction) return;
|
|
106
|
+
runtime.automaticCompaction.outcome = "skipped";
|
|
107
|
+
finalizeAutomaticCompaction(pi, runtime, initState, ctx);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Consumes automatic recovery and records its failure on the eligible paused goal. */
|
|
111
|
+
function stopAutomaticCompactionRecovery(
|
|
112
|
+
pi: ExtensionAPI,
|
|
113
|
+
runtime: GoalRuntime,
|
|
114
|
+
ctx: ExtensionContext,
|
|
115
|
+
reason: string,
|
|
116
|
+
): void {
|
|
117
|
+
const recovery = runtime.automaticCompaction;
|
|
118
|
+
runtime.automaticCompaction = undefined;
|
|
119
|
+
const safeReason = safeTerminalText(reason);
|
|
120
|
+
if (runtime.state?.status !== "paused" || runtime.state.revision !== recovery?.pausedRevision) return;
|
|
121
|
+
try {
|
|
122
|
+
transitionGoal(pi, runtime, "error", "paused", safeReason);
|
|
123
|
+
} catch {
|
|
124
|
+
runtime.state = { ...runtime.state, result: safeReason };
|
|
125
|
+
runtime.persistenceRetryNeeded = true;
|
|
126
|
+
runtime.requestRender?.();
|
|
127
|
+
}
|
|
128
|
+
ctx.ui.notify(
|
|
129
|
+
`Goal paused: ${safeReason}\nAutomatic continuation is stopped. Run /goal resume after resolving the compaction problem.`,
|
|
130
|
+
"error",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Leaves the goal paused when Pi rejects automatic compaction. */
|
|
135
|
+
function failAutomaticCompaction(
|
|
136
|
+
pi: ExtensionAPI,
|
|
137
|
+
runtime: GoalRuntime,
|
|
138
|
+
ctx: ExtensionContext,
|
|
139
|
+
error: unknown,
|
|
140
|
+
): void {
|
|
141
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
142
|
+
if (!runtime.automaticCompaction) {
|
|
143
|
+
if (runtime.persistenceRetryNeeded) {
|
|
144
|
+
ctx.ui.notify(`Automatic compaction did not start: ${safeTerminalText(reason)}`, "error");
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
stopAutomaticCompactionRecovery(pi, runtime, ctx, `automatic compaction failed: ${reason}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function registerGoalSettlement(
|
|
152
|
+
pi: ExtensionAPI,
|
|
153
|
+
runtime: GoalRuntime,
|
|
154
|
+
initState: InitRuntime,
|
|
155
|
+
): AutoCompactionGoalHandlers {
|
|
156
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
157
|
+
const wasGoalTurn = runtime.goalTurnInFlight;
|
|
158
|
+
const continuationWasScheduled = runtime.continuationScheduled;
|
|
159
|
+
const agentEndObserved = runtime.agentEndObserved;
|
|
160
|
+
runtime.goalTurnInFlight = false;
|
|
161
|
+
runtime.agentEndObserved = false;
|
|
162
|
+
runtime.continuationScheduled = false;
|
|
163
|
+
|
|
164
|
+
if (runtime.automaticCompaction) {
|
|
165
|
+
const stopReason = runtime.lastStopReason;
|
|
166
|
+
const error = safeTerminalText(runtime.lastError ?? "");
|
|
167
|
+
runtime.lastStopReason = undefined;
|
|
168
|
+
runtime.lastError = undefined;
|
|
169
|
+
const expectedInterruption = stopReason === "aborted"
|
|
170
|
+
|| stopReason === "error" && error === "This operation was aborted";
|
|
171
|
+
if (!wasGoalTurn || !agentEndObserved) {
|
|
172
|
+
stopAutomaticCompactionRecovery(pi, runtime, ctx, "the goal turn ended without an agent result");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if ((stopReason === "error" || stopReason === "aborted") && !expectedInterruption) {
|
|
176
|
+
stopAutomaticCompactionRecovery(
|
|
177
|
+
pi,
|
|
178
|
+
runtime,
|
|
179
|
+
ctx,
|
|
180
|
+
error || "the agent turn failed",
|
|
181
|
+
);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
runtime.automaticCompaction.turnSettled = true;
|
|
185
|
+
finalizeAutomaticCompaction(pi, runtime, initState, ctx);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!wasGoalTurn || runtime.state?.status !== "active" || initState.active) {
|
|
190
|
+
if (continuationWasScheduled && runtime.state?.status === "active" && !initState.active) {
|
|
191
|
+
pauseGoalAfterFailure(pi, runtime, ctx, "the goal continuation ended before an agent turn started");
|
|
192
|
+
} else if (runtime.state?.status === "active" && !initState.active) {
|
|
193
|
+
scheduleGoalContinuation(pi, runtime, initState, ctx);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!agentEndObserved) {
|
|
198
|
+
pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn ended without an agent result");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (runtime.lastStopReason === "aborted") {
|
|
202
|
+
const reason = runtime.lastError || "the agent turn was aborted";
|
|
203
|
+
runtime.lastStopReason = undefined;
|
|
204
|
+
runtime.lastError = undefined;
|
|
205
|
+
pauseGoalForPossibleManualCompaction(pi, runtime, ctx, reason);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (runtime.lastStopReason === "error") {
|
|
209
|
+
const reason = runtime.lastError || "the agent turn failed";
|
|
210
|
+
runtime.lastStopReason = undefined;
|
|
211
|
+
runtime.lastError = undefined;
|
|
212
|
+
pauseGoalAfterFailure(pi, runtime, ctx, reason);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
runtime.lastStopReason = undefined;
|
|
216
|
+
runtime.lastError = undefined;
|
|
217
|
+
if (pauseGoalAtTurnLimit(pi, runtime, ctx)) return;
|
|
218
|
+
scheduleGoalContinuation(pi, runtime, initState, ctx);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
pi.on("session_compact", (event, ctx) => {
|
|
222
|
+
if (runtime.automaticCompaction !== undefined) return;
|
|
223
|
+
if (event.reason !== "manual") return;
|
|
224
|
+
recoverGoalAfterManualCompaction(pi, runtime, initState, ctx);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const resetAutomaticRecovery = (): void => { runtime.automaticCompaction = undefined; };
|
|
228
|
+
pi.on("session_before_switch", resetAutomaticRecovery);
|
|
229
|
+
pi.on("session_before_fork", resetAutomaticRecovery);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
isActive: (ctx: ExtensionContext): boolean => isGoalModeSupported(ctx)
|
|
233
|
+
&& isSavedSession(ctx)
|
|
234
|
+
&& runtime.state?.status === "active"
|
|
235
|
+
&& !initState.active,
|
|
236
|
+
onRequested: (): void => {
|
|
237
|
+
if (runtime.state?.status !== "active") return;
|
|
238
|
+
try {
|
|
239
|
+
const paused = transitionGoal(pi, runtime, "pause", "paused");
|
|
240
|
+
runtime.automaticCompaction = {
|
|
241
|
+
pausedRevision: paused.revision,
|
|
242
|
+
outcome: "pending",
|
|
243
|
+
turnSettled: false,
|
|
244
|
+
};
|
|
245
|
+
} catch (error) {
|
|
246
|
+
const current = runtime.state;
|
|
247
|
+
const reason = safeTerminalText(`automatic compaction pause could not be saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
248
|
+
runtime.state = current ? pauseGoalState(current, reason, Date.now()) : undefined;
|
|
249
|
+
syncGoalUpdateTool(pi, runtime);
|
|
250
|
+
runtime.persistenceRetryNeeded = true;
|
|
251
|
+
runtime.continuationScheduled = false;
|
|
252
|
+
runtime.automaticCompaction = undefined;
|
|
253
|
+
runtime.requestRender?.();
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
onCompleted: (ctx: ExtensionContext): void => completeAutomaticCompaction(pi, runtime, initState, ctx),
|
|
258
|
+
onFailed: (ctx: ExtensionContext, error: unknown): void => failAutomaticCompaction(pi, runtime, ctx, error),
|
|
259
|
+
onSkipped: (ctx: ExtensionContext): void => skipAutomaticCompaction(pi, runtime, initState, ctx),
|
|
260
|
+
};
|
|
261
|
+
}
|