killeros 3.0.7 → 3.0.8
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 +755 -734
- package/Killeros.ts +56 -55
- package/LICENSE +21 -21
- package/README.md +381 -364
- package/killeros/activity.ts +111 -111
- package/killeros/auto-compaction.ts +384 -384
- package/killeros/bounded-text.ts +25 -25
- package/killeros/change-receipt.ts +676 -676
- package/killeros/codex-fast-state.ts +55 -55
- package/killeros/codex-fast.ts +37 -37
- package/killeros/commands.ts +259 -259
- package/killeros/display.ts +60 -60
- package/killeros/errors.ts +19 -19
- package/killeros/footer.ts +540 -540
- package/killeros/goal-command.ts +37 -26
- package/killeros/goal-interface.ts +666 -585
- package/killeros/goal-runtime.ts +519 -516
- package/killeros/goal-settlement.ts +368 -368
- package/killeros/goal-state.ts +753 -724
- package/killeros/handoff.ts +327 -327
- package/killeros/hooks.ts +443 -470
- package/killeros/notifications.ts +135 -135
- package/killeros/passive-git-status.ts +391 -408
- package/killeros/personal-instructions.ts +122 -122
- package/killeros/question-ui.ts +269 -269
- package/killeros/question.ts +242 -242
- package/killeros/runtime.ts +156 -155
- package/killeros/safe-terminal-text.ts +6 -6
- package/killeros/secret-detector.ts +19 -19
- package/killeros/settings.ts +145 -145
- package/killeros/shell-ui.ts +418 -418
- package/killeros/variants.ts +13 -13
- package/killeros/worked-for.ts +502 -502
- package/package.json +71 -68
- package/themes/killeros.json +87 -87
|
@@ -1,368 +1,368 @@
|
|
|
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 {
|
|
5
|
-
isGoalModeSupported,
|
|
6
|
-
isSavedSession,
|
|
7
|
-
pauseGoalAfterFailure,
|
|
8
|
-
pauseGoalAtTurnLimit,
|
|
9
|
-
resumeInterruptedGoalTurn,
|
|
10
|
-
scheduleGoalContinuation,
|
|
11
|
-
syncGoalUpdateTool,
|
|
12
|
-
transitionGoal,
|
|
13
|
-
} from "./goal-runtime.ts";
|
|
14
|
-
import { boundGoalText, pauseGoalState } from "./goal-state.ts";
|
|
15
|
-
import type { GoalRuntime, GoalState } from "./runtime.ts";
|
|
16
|
-
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
17
|
-
|
|
18
|
-
function pauseGoalForPossibleManualCompaction(
|
|
19
|
-
pi: ExtensionAPI,
|
|
20
|
-
runtime: GoalRuntime,
|
|
21
|
-
ctx: ExtensionContext,
|
|
22
|
-
reason: string,
|
|
23
|
-
): void {
|
|
24
|
-
if (runtime.state?.status !== "active") return;
|
|
25
|
-
const safeReason = boundGoalText(reason) || "the agent turn was aborted";
|
|
26
|
-
try {
|
|
27
|
-
transitionGoal(pi, runtime, "error", "paused", safeReason, {
|
|
28
|
-
resumeAfterManualCompaction: true,
|
|
29
|
-
keepTurnForRecovery: true,
|
|
30
|
-
preserveTurnAuthorization: true,
|
|
31
|
-
});
|
|
32
|
-
} catch {
|
|
33
|
-
const current = runtime.state;
|
|
34
|
-
runtime.state = current ? pauseGoalState(current, safeReason, Date.now(), true, true) : undefined;
|
|
35
|
-
syncGoalUpdateTool(pi, runtime);
|
|
36
|
-
runtime.persistenceRetryNeeded = true;
|
|
37
|
-
runtime.continuationScheduled = false;
|
|
38
|
-
runtime.automaticCompaction = undefined;
|
|
39
|
-
runtime.requestRender?.();
|
|
40
|
-
}
|
|
41
|
-
ctx.ui.notify(
|
|
42
|
-
"Goal paused because the turn was aborted. If /compact is running, KillerOS will resume the same goal turn after Pi saves the summary. Run /goal pause to keep it paused.",
|
|
43
|
-
"warning",
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function recoverGoalAfterManualCompaction(
|
|
48
|
-
pi: ExtensionAPI,
|
|
49
|
-
runtime: GoalRuntime,
|
|
50
|
-
ctx: ExtensionContext,
|
|
51
|
-
): boolean {
|
|
52
|
-
if (runtime.state?.status !== "paused"
|
|
53
|
-
|| runtime.state.resumeAfterManualCompaction !== true) return false;
|
|
54
|
-
const state = runtime.state;
|
|
55
|
-
const sameTurn = state.turnDecision === undefined;
|
|
56
|
-
const turn = runtime.goalTurn?.turn ?? state.turns;
|
|
57
|
-
const generation = runtime.lifecycleGeneration;
|
|
58
|
-
let resumed: GoalState;
|
|
59
|
-
try {
|
|
60
|
-
resumed = transitionGoal(pi, runtime, "resume", "active", undefined, {
|
|
61
|
-
...(sameTurn ? { resumeInterruptedTurn: true as const } : {}),
|
|
62
|
-
preserveTurnAuthorization: true,
|
|
63
|
-
});
|
|
64
|
-
} catch (error) {
|
|
65
|
-
runtime.persistenceRetryNeeded = true;
|
|
66
|
-
reportError(ctx, "Manual compaction succeeded, but the goal could not be resumed", error);
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
runtime.continuationScheduled = false;
|
|
70
|
-
runtime.automaticCompaction = undefined;
|
|
71
|
-
if (sameTurn) {
|
|
72
|
-
runtime.goalTurn = { turn, revision: runtime.state?.revision ?? state.revision };
|
|
73
|
-
runtime.goalTurnInFlight = true;
|
|
74
|
-
runtime.agentEndObserved = false;
|
|
75
|
-
if (ctx.mode !== "tui") ctx.ui.notify("Manual compaction complete. The interrupted goal turn resumed.", "info");
|
|
76
|
-
setImmediate(() => {
|
|
77
|
-
if (runtime.lifecycleGeneration !== generation || runtime.state !== resumed) return;
|
|
78
|
-
resumeInterruptedGoalTurn(pi, runtime, ctx);
|
|
79
|
-
});
|
|
80
|
-
} else {
|
|
81
|
-
runtime.goalTurn = undefined;
|
|
82
|
-
runtime.goalTurnInFlight = false;
|
|
83
|
-
if (scheduleGoalContinuation(pi, runtime, ctx) && ctx.mode !== "tui") ctx.ui.notify("Manual compaction complete. Goal resumed.", "info");
|
|
84
|
-
}
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Resumes the paused revision only after compaction and the interrupted turn have both settled. */
|
|
89
|
-
function finalizeAutomaticCompaction(
|
|
90
|
-
pi: ExtensionAPI,
|
|
91
|
-
runtime: GoalRuntime,
|
|
92
|
-
ctx: ExtensionContext,
|
|
93
|
-
): void {
|
|
94
|
-
const recovery = runtime.automaticCompaction;
|
|
95
|
-
if (!recovery || recovery.outcome === "pending" || !recovery.turnSettled) return;
|
|
96
|
-
if (runtime.state?.status !== "paused"
|
|
97
|
-
|| runtime.state.revision !== recovery.pausedRevision) {
|
|
98
|
-
runtime.automaticCompaction = undefined;
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
const state = runtime.state;
|
|
102
|
-
const sameTurn = recovery.resumeSameTurn;
|
|
103
|
-
const generation = runtime.lifecycleGeneration;
|
|
104
|
-
runtime.automaticCompaction = undefined;
|
|
105
|
-
let resumed: GoalState;
|
|
106
|
-
try {
|
|
107
|
-
resumed = transitionGoal(pi, runtime, "resume", "active", undefined, {
|
|
108
|
-
...(sameTurn ? { resumeInterruptedTurn: true as const } : {}),
|
|
109
|
-
...(!sameTurn && state.turnDecision !== undefined ? { preserveTurnAuthorization: true as const } : {}),
|
|
110
|
-
});
|
|
111
|
-
} catch (error) {
|
|
112
|
-
runtime.persistenceRetryNeeded = true;
|
|
113
|
-
reportError(ctx, sameTurn
|
|
114
|
-
? "Automatic compaction succeeded, but the interrupted goal turn could not be resumed"
|
|
115
|
-
: "Automatic compaction succeeded, but the goal could not be resumed", error);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
runtime.continuationScheduled = false;
|
|
119
|
-
if (sameTurn) {
|
|
120
|
-
runtime.goalTurn = { turn: recovery.turn, revision: runtime.state?.revision ?? state.revision };
|
|
121
|
-
runtime.goalTurnInFlight = false;
|
|
122
|
-
runtime.agentEndObserved = false;
|
|
123
|
-
setImmediate(() => {
|
|
124
|
-
if (runtime.lifecycleGeneration !== generation
|
|
125
|
-
|| runtime.state !== resumed
|
|
126
|
-
|| runtime.goalTurn?.turn !== recovery.turn) return;
|
|
127
|
-
runtime.goalTurnInFlight = true;
|
|
128
|
-
resumeInterruptedGoalTurn(pi, runtime, ctx);
|
|
129
|
-
});
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
runtime.goalTurn = undefined;
|
|
133
|
-
runtime.goalTurnInFlight = false;
|
|
134
|
-
if (pauseGoalAtTurnLimit(pi, runtime, ctx)) return;
|
|
135
|
-
setImmediate(() => scheduleGoalContinuation(pi, runtime, ctx, { generation, state: resumed }));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function completeAutomaticCompaction(
|
|
139
|
-
pi: ExtensionAPI,
|
|
140
|
-
runtime: GoalRuntime,
|
|
141
|
-
ctx: ExtensionContext,
|
|
142
|
-
): void {
|
|
143
|
-
if (!runtime.automaticCompaction) return;
|
|
144
|
-
runtime.automaticCompaction.outcome = "completed";
|
|
145
|
-
finalizeAutomaticCompaction(pi, runtime, ctx);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function skipAutomaticCompaction(
|
|
149
|
-
pi: ExtensionAPI,
|
|
150
|
-
runtime: GoalRuntime,
|
|
151
|
-
ctx: ExtensionContext,
|
|
152
|
-
): void {
|
|
153
|
-
if (!runtime.automaticCompaction) return;
|
|
154
|
-
runtime.automaticCompaction.outcome = "skipped";
|
|
155
|
-
finalizeAutomaticCompaction(pi, runtime, ctx);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function stopAutomaticCompactionRecovery(
|
|
159
|
-
pi: ExtensionAPI,
|
|
160
|
-
runtime: GoalRuntime,
|
|
161
|
-
ctx: ExtensionContext,
|
|
162
|
-
reason: string,
|
|
163
|
-
): void {
|
|
164
|
-
const recovery = runtime.automaticCompaction;
|
|
165
|
-
runtime.automaticCompaction = undefined;
|
|
166
|
-
const safeReason = boundGoalText(reason) || "automatic compaction failed";
|
|
167
|
-
if (runtime.state?.status !== "paused" || runtime.state.revision !== recovery?.pausedRevision) return;
|
|
168
|
-
try {
|
|
169
|
-
transitionGoal(pi, runtime, "error", "paused", safeReason);
|
|
170
|
-
} catch {
|
|
171
|
-
runtime.state = pauseGoalState(runtime.state, safeReason, Date.now());
|
|
172
|
-
syncGoalUpdateTool(pi, runtime);
|
|
173
|
-
runtime.persistenceRetryNeeded = true;
|
|
174
|
-
runtime.continuationScheduled = false;
|
|
175
|
-
runtime.goalTurn = undefined;
|
|
176
|
-
runtime.goalTurnInFlight = false;
|
|
177
|
-
runtime.requestRender?.();
|
|
178
|
-
}
|
|
179
|
-
ctx.ui.notify(
|
|
180
|
-
`Goal paused: ${safeReason}\nAutomatic continuation is stopped. Run /goal resume after resolving the compaction problem.`,
|
|
181
|
-
"error",
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function failAutomaticCompaction(
|
|
186
|
-
pi: ExtensionAPI,
|
|
187
|
-
runtime: GoalRuntime,
|
|
188
|
-
ctx: ExtensionContext,
|
|
189
|
-
error: unknown,
|
|
190
|
-
): void {
|
|
191
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
192
|
-
if (!runtime.automaticCompaction) {
|
|
193
|
-
if (runtime.persistenceRetryNeeded) {
|
|
194
|
-
ctx.ui.notify(`Automatic compaction did not start: ${safeTerminalText(reason)}`, "error");
|
|
195
|
-
}
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
stopAutomaticCompactionRecovery(pi, runtime, ctx, `automatic compaction failed: ${reason}`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function settleGoalTurn(
|
|
202
|
-
pi: ExtensionAPI,
|
|
203
|
-
runtime: GoalRuntime,
|
|
204
|
-
ctx: ExtensionContext,
|
|
205
|
-
settledTurn: number,
|
|
206
|
-
): void {
|
|
207
|
-
const state = runtime.state;
|
|
208
|
-
if (state?.status !== "active") return;
|
|
209
|
-
const decision = state.turnDecision;
|
|
210
|
-
if (!decision || decision.turn !== settledTurn) {
|
|
211
|
-
pauseGoalAfterFailure(pi, runtime, ctx, "no turn decision");
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
if (pauseGoalAtTurnLimit(pi, runtime, ctx)) return;
|
|
215
|
-
scheduleGoalContinuation(pi, runtime, ctx);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export function registerGoalSettlement(
|
|
219
|
-
pi: ExtensionAPI,
|
|
220
|
-
runtime: GoalRuntime,
|
|
221
|
-
): AutoCompactionGoalHandlers {
|
|
222
|
-
pi.on("agent_settled", (event, ctx) => {
|
|
223
|
-
runtime.blockedCompletion = undefined;
|
|
224
|
-
syncGoalUpdateTool(pi, runtime);
|
|
225
|
-
const wasGoalTurn = runtime.goalTurnInFlight;
|
|
226
|
-
const continuationWasScheduled = runtime.continuationScheduled;
|
|
227
|
-
const agentEndObserved = runtime.agentEndObserved;
|
|
228
|
-
const settledTurn = runtime.goalTurn?.turn;
|
|
229
|
-
runtime.goalTurnInFlight = false;
|
|
230
|
-
runtime.agentEndObserved = false;
|
|
231
|
-
runtime.continuationScheduled = false;
|
|
232
|
-
|
|
233
|
-
// ctx.compact() also aborts the request. An owned compaction must settle through
|
|
234
|
-
// its success/error callback; its cancellation cannot authorize recovery.
|
|
235
|
-
if ("aborted" in event && event.aborted === true && !runtime.automaticCompaction
|
|
236
|
-
&& runtime.lastStopReason !== "aborted") {
|
|
237
|
-
runtime.lastStopReason = undefined;
|
|
238
|
-
runtime.lastError = undefined;
|
|
239
|
-
if (runtime.state?.status === "active") {
|
|
240
|
-
pauseGoalAfterFailure(pi, runtime, ctx, "the agent request was cancelled");
|
|
241
|
-
}
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (runtime.automaticCompaction) {
|
|
246
|
-
const stopReason = runtime.lastStopReason;
|
|
247
|
-
const error = safeTerminalText(runtime.lastError ?? "");
|
|
248
|
-
runtime.lastStopReason = undefined;
|
|
249
|
-
runtime.lastError = undefined;
|
|
250
|
-
if (runtime.automaticCompaction.turnSettled
|
|
251
|
-
&& (settledTurn === undefined || settledTurn === runtime.automaticCompaction.turn)) {
|
|
252
|
-
// The interrupted turn already settled; a repeated settlement carries no new
|
|
253
|
-
// information, so keep waiting for compaction instead of failing the recovery.
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
const expectedInterruption = stopReason === "aborted"
|
|
257
|
-
|| stopReason === "error" && error === "This operation was aborted";
|
|
258
|
-
if (!wasGoalTurn || !agentEndObserved) {
|
|
259
|
-
stopAutomaticCompactionRecovery(pi, runtime, ctx, "the goal turn ended without an agent result");
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
if ((stopReason === "error" || stopReason === "aborted") && !expectedInterruption) {
|
|
263
|
-
stopAutomaticCompactionRecovery(
|
|
264
|
-
pi,
|
|
265
|
-
runtime,
|
|
266
|
-
ctx,
|
|
267
|
-
error || "the agent turn failed",
|
|
268
|
-
);
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
if (!expectedInterruption && runtime.automaticCompaction.resumeSameTurn) {
|
|
272
|
-
stopAutomaticCompactionRecovery(pi, runtime, ctx, "no turn decision");
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
if (settledTurn !== undefined && runtime.automaticCompaction.turn === settledTurn) {
|
|
276
|
-
runtime.automaticCompaction.turnSettled = true;
|
|
277
|
-
} else {
|
|
278
|
-
stopAutomaticCompactionRecovery(pi, runtime, ctx, "the goal turn identity changed during compaction");
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
finalizeAutomaticCompaction(pi, runtime, ctx);
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (!wasGoalTurn || runtime.state?.status !== "active") {
|
|
286
|
-
if (continuationWasScheduled && runtime.state?.status === "active") {
|
|
287
|
-
pauseGoalAfterFailure(pi, runtime, ctx, "the goal continuation ended before an agent turn started");
|
|
288
|
-
} else if (runtime.state?.status === "active"
|
|
289
|
-
&& (runtime.state.turnPhase === "ready" || runtime.state.turnPhase === "authorized")) {
|
|
290
|
-
scheduleGoalContinuation(pi, runtime, ctx);
|
|
291
|
-
}
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
if (settledTurn === undefined) {
|
|
295
|
-
pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn identity was lost");
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
if (!agentEndObserved) {
|
|
299
|
-
pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn ended without an agent result");
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
if (runtime.lastStopReason === "aborted") {
|
|
303
|
-
const reason = runtime.lastError || "the agent turn was aborted";
|
|
304
|
-
runtime.lastStopReason = undefined;
|
|
305
|
-
runtime.lastError = undefined;
|
|
306
|
-
pauseGoalForPossibleManualCompaction(pi, runtime, ctx, reason);
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
if (runtime.lastStopReason === "error") {
|
|
310
|
-
const reason = runtime.lastError || "the agent turn failed";
|
|
311
|
-
runtime.lastStopReason = undefined;
|
|
312
|
-
runtime.lastError = undefined;
|
|
313
|
-
pauseGoalAfterFailure(pi, runtime, ctx, reason);
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
runtime.lastStopReason = undefined;
|
|
317
|
-
runtime.lastError = undefined;
|
|
318
|
-
settleGoalTurn(pi, runtime, ctx, settledTurn);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
pi.on("session_compact", (event, ctx) => {
|
|
322
|
-
if (runtime.automaticCompaction !== undefined) return;
|
|
323
|
-
if (event.reason !== "manual") return;
|
|
324
|
-
recoverGoalAfterManualCompaction(pi, runtime, ctx);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
return {
|
|
328
|
-
hasAcceptedTerminalDecision: (): boolean => runtime.blockedCompletion?.kind === "accepted"
|
|
329
|
-
&& runtime.blockedCompletion.generation === runtime.lifecycleGeneration,
|
|
330
|
-
isActive: (ctx: ExtensionContext): boolean => isGoalModeSupported(ctx)
|
|
331
|
-
&& isSavedSession(ctx)
|
|
332
|
-
&& runtime.state?.status === "active",
|
|
333
|
-
onRequested: (): void => {
|
|
334
|
-
if (runtime.state?.status !== "active") return;
|
|
335
|
-
const current = runtime.state;
|
|
336
|
-
// A turn that already accepted continue or a blocker audit owns a pending
|
|
337
|
-
// authorization; recovery must take the next-turn path, never resume the decided turn.
|
|
338
|
-
const resumeSameTurn = runtime.goalTurnInFlight && current.turnDecision === undefined;
|
|
339
|
-
try {
|
|
340
|
-
const paused = transitionGoal(pi, runtime, "pause", "paused", undefined, {
|
|
341
|
-
keepTurnForRecovery: true,
|
|
342
|
-
preserveTurnAuthorization: true,
|
|
343
|
-
});
|
|
344
|
-
runtime.automaticCompaction = {
|
|
345
|
-
pausedRevision: paused.revision,
|
|
346
|
-
outcome: "pending",
|
|
347
|
-
turnSettled: false,
|
|
348
|
-
turn: current.turns,
|
|
349
|
-
resumeSameTurn,
|
|
350
|
-
};
|
|
351
|
-
} catch (error) {
|
|
352
|
-
const reason = boundGoalText(`automatic compaction pause could not be saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
353
|
-
runtime.state = current ? pauseGoalState(current, reason, Date.now()) : undefined;
|
|
354
|
-
syncGoalUpdateTool(pi, runtime);
|
|
355
|
-
runtime.persistenceRetryNeeded = true;
|
|
356
|
-
runtime.continuationScheduled = false;
|
|
357
|
-
runtime.automaticCompaction = undefined;
|
|
358
|
-
runtime.goalTurn = undefined;
|
|
359
|
-
runtime.goalTurnInFlight = false;
|
|
360
|
-
runtime.requestRender?.();
|
|
361
|
-
throw error;
|
|
362
|
-
}
|
|
363
|
-
},
|
|
364
|
-
onCompleted: (ctx: ExtensionContext): void => completeAutomaticCompaction(pi, runtime, ctx),
|
|
365
|
-
onFailed: (ctx: ExtensionContext, error: unknown): void => failAutomaticCompaction(pi, runtime, ctx, error),
|
|
366
|
-
onSkipped: (ctx: ExtensionContext): void => skipAutomaticCompaction(pi, runtime, ctx),
|
|
367
|
-
};
|
|
368
|
-
}
|
|
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 {
|
|
5
|
+
isGoalModeSupported,
|
|
6
|
+
isSavedSession,
|
|
7
|
+
pauseGoalAfterFailure,
|
|
8
|
+
pauseGoalAtTurnLimit,
|
|
9
|
+
resumeInterruptedGoalTurn,
|
|
10
|
+
scheduleGoalContinuation,
|
|
11
|
+
syncGoalUpdateTool,
|
|
12
|
+
transitionGoal,
|
|
13
|
+
} from "./goal-runtime.ts";
|
|
14
|
+
import { boundGoalText, pauseGoalState } from "./goal-state.ts";
|
|
15
|
+
import type { GoalRuntime, GoalState } from "./runtime.ts";
|
|
16
|
+
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
17
|
+
|
|
18
|
+
function pauseGoalForPossibleManualCompaction(
|
|
19
|
+
pi: ExtensionAPI,
|
|
20
|
+
runtime: GoalRuntime,
|
|
21
|
+
ctx: ExtensionContext,
|
|
22
|
+
reason: string,
|
|
23
|
+
): void {
|
|
24
|
+
if (runtime.state?.status !== "active") return;
|
|
25
|
+
const safeReason = boundGoalText(reason) || "the agent turn was aborted";
|
|
26
|
+
try {
|
|
27
|
+
transitionGoal(pi, runtime, "error", "paused", safeReason, {
|
|
28
|
+
resumeAfterManualCompaction: true,
|
|
29
|
+
keepTurnForRecovery: true,
|
|
30
|
+
preserveTurnAuthorization: true,
|
|
31
|
+
});
|
|
32
|
+
} catch {
|
|
33
|
+
const current = runtime.state;
|
|
34
|
+
runtime.state = current ? pauseGoalState(current, safeReason, Date.now(), true, true) : undefined;
|
|
35
|
+
syncGoalUpdateTool(pi, runtime);
|
|
36
|
+
runtime.persistenceRetryNeeded = true;
|
|
37
|
+
runtime.continuationScheduled = false;
|
|
38
|
+
runtime.automaticCompaction = undefined;
|
|
39
|
+
runtime.requestRender?.();
|
|
40
|
+
}
|
|
41
|
+
ctx.ui.notify(
|
|
42
|
+
"Goal paused because the turn was aborted. If /compact is running, KillerOS will resume the same goal turn after Pi saves the summary. Run /goal pause to keep it paused.",
|
|
43
|
+
"warning",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function recoverGoalAfterManualCompaction(
|
|
48
|
+
pi: ExtensionAPI,
|
|
49
|
+
runtime: GoalRuntime,
|
|
50
|
+
ctx: ExtensionContext,
|
|
51
|
+
): boolean {
|
|
52
|
+
if (runtime.state?.status !== "paused"
|
|
53
|
+
|| runtime.state.resumeAfterManualCompaction !== true) return false;
|
|
54
|
+
const state = runtime.state;
|
|
55
|
+
const sameTurn = state.turnDecision === undefined;
|
|
56
|
+
const turn = runtime.goalTurn?.turn ?? state.turns;
|
|
57
|
+
const generation = runtime.lifecycleGeneration;
|
|
58
|
+
let resumed: GoalState;
|
|
59
|
+
try {
|
|
60
|
+
resumed = transitionGoal(pi, runtime, "resume", "active", undefined, {
|
|
61
|
+
...(sameTurn ? { resumeInterruptedTurn: true as const } : {}),
|
|
62
|
+
preserveTurnAuthorization: true,
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
runtime.persistenceRetryNeeded = true;
|
|
66
|
+
reportError(ctx, "Manual compaction succeeded, but the goal could not be resumed", error);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
runtime.continuationScheduled = false;
|
|
70
|
+
runtime.automaticCompaction = undefined;
|
|
71
|
+
if (sameTurn) {
|
|
72
|
+
runtime.goalTurn = { turn, revision: runtime.state?.revision ?? state.revision };
|
|
73
|
+
runtime.goalTurnInFlight = true;
|
|
74
|
+
runtime.agentEndObserved = false;
|
|
75
|
+
if (ctx.mode !== "tui") ctx.ui.notify("Manual compaction complete. The interrupted goal turn resumed.", "info");
|
|
76
|
+
setImmediate(() => {
|
|
77
|
+
if (runtime.lifecycleGeneration !== generation || runtime.state !== resumed) return;
|
|
78
|
+
resumeInterruptedGoalTurn(pi, runtime, ctx);
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
runtime.goalTurn = undefined;
|
|
82
|
+
runtime.goalTurnInFlight = false;
|
|
83
|
+
if (scheduleGoalContinuation(pi, runtime, ctx) && ctx.mode !== "tui") ctx.ui.notify("Manual compaction complete. Goal resumed.", "info");
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Resumes the paused revision only after compaction and the interrupted turn have both settled. */
|
|
89
|
+
function finalizeAutomaticCompaction(
|
|
90
|
+
pi: ExtensionAPI,
|
|
91
|
+
runtime: GoalRuntime,
|
|
92
|
+
ctx: ExtensionContext,
|
|
93
|
+
): void {
|
|
94
|
+
const recovery = runtime.automaticCompaction;
|
|
95
|
+
if (!recovery || recovery.outcome === "pending" || !recovery.turnSettled) return;
|
|
96
|
+
if (runtime.state?.status !== "paused"
|
|
97
|
+
|| runtime.state.revision !== recovery.pausedRevision) {
|
|
98
|
+
runtime.automaticCompaction = undefined;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const state = runtime.state;
|
|
102
|
+
const sameTurn = recovery.resumeSameTurn;
|
|
103
|
+
const generation = runtime.lifecycleGeneration;
|
|
104
|
+
runtime.automaticCompaction = undefined;
|
|
105
|
+
let resumed: GoalState;
|
|
106
|
+
try {
|
|
107
|
+
resumed = transitionGoal(pi, runtime, "resume", "active", undefined, {
|
|
108
|
+
...(sameTurn ? { resumeInterruptedTurn: true as const } : {}),
|
|
109
|
+
...(!sameTurn && state.turnDecision !== undefined ? { preserveTurnAuthorization: true as const } : {}),
|
|
110
|
+
});
|
|
111
|
+
} catch (error) {
|
|
112
|
+
runtime.persistenceRetryNeeded = true;
|
|
113
|
+
reportError(ctx, sameTurn
|
|
114
|
+
? "Automatic compaction succeeded, but the interrupted goal turn could not be resumed"
|
|
115
|
+
: "Automatic compaction succeeded, but the goal could not be resumed", error);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
runtime.continuationScheduled = false;
|
|
119
|
+
if (sameTurn) {
|
|
120
|
+
runtime.goalTurn = { turn: recovery.turn, revision: runtime.state?.revision ?? state.revision };
|
|
121
|
+
runtime.goalTurnInFlight = false;
|
|
122
|
+
runtime.agentEndObserved = false;
|
|
123
|
+
setImmediate(() => {
|
|
124
|
+
if (runtime.lifecycleGeneration !== generation
|
|
125
|
+
|| runtime.state !== resumed
|
|
126
|
+
|| runtime.goalTurn?.turn !== recovery.turn) return;
|
|
127
|
+
runtime.goalTurnInFlight = true;
|
|
128
|
+
resumeInterruptedGoalTurn(pi, runtime, ctx);
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
runtime.goalTurn = undefined;
|
|
133
|
+
runtime.goalTurnInFlight = false;
|
|
134
|
+
if (pauseGoalAtTurnLimit(pi, runtime, ctx)) return;
|
|
135
|
+
setImmediate(() => scheduleGoalContinuation(pi, runtime, ctx, { generation, state: resumed }));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function completeAutomaticCompaction(
|
|
139
|
+
pi: ExtensionAPI,
|
|
140
|
+
runtime: GoalRuntime,
|
|
141
|
+
ctx: ExtensionContext,
|
|
142
|
+
): void {
|
|
143
|
+
if (!runtime.automaticCompaction) return;
|
|
144
|
+
runtime.automaticCompaction.outcome = "completed";
|
|
145
|
+
finalizeAutomaticCompaction(pi, runtime, ctx);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function skipAutomaticCompaction(
|
|
149
|
+
pi: ExtensionAPI,
|
|
150
|
+
runtime: GoalRuntime,
|
|
151
|
+
ctx: ExtensionContext,
|
|
152
|
+
): void {
|
|
153
|
+
if (!runtime.automaticCompaction) return;
|
|
154
|
+
runtime.automaticCompaction.outcome = "skipped";
|
|
155
|
+
finalizeAutomaticCompaction(pi, runtime, ctx);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function stopAutomaticCompactionRecovery(
|
|
159
|
+
pi: ExtensionAPI,
|
|
160
|
+
runtime: GoalRuntime,
|
|
161
|
+
ctx: ExtensionContext,
|
|
162
|
+
reason: string,
|
|
163
|
+
): void {
|
|
164
|
+
const recovery = runtime.automaticCompaction;
|
|
165
|
+
runtime.automaticCompaction = undefined;
|
|
166
|
+
const safeReason = boundGoalText(reason) || "automatic compaction failed";
|
|
167
|
+
if (runtime.state?.status !== "paused" || runtime.state.revision !== recovery?.pausedRevision) return;
|
|
168
|
+
try {
|
|
169
|
+
transitionGoal(pi, runtime, "error", "paused", safeReason);
|
|
170
|
+
} catch {
|
|
171
|
+
runtime.state = pauseGoalState(runtime.state, safeReason, Date.now());
|
|
172
|
+
syncGoalUpdateTool(pi, runtime);
|
|
173
|
+
runtime.persistenceRetryNeeded = true;
|
|
174
|
+
runtime.continuationScheduled = false;
|
|
175
|
+
runtime.goalTurn = undefined;
|
|
176
|
+
runtime.goalTurnInFlight = false;
|
|
177
|
+
runtime.requestRender?.();
|
|
178
|
+
}
|
|
179
|
+
ctx.ui.notify(
|
|
180
|
+
`Goal paused: ${safeReason}\nAutomatic continuation is stopped. Run /goal resume after resolving the compaction problem.`,
|
|
181
|
+
"error",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function failAutomaticCompaction(
|
|
186
|
+
pi: ExtensionAPI,
|
|
187
|
+
runtime: GoalRuntime,
|
|
188
|
+
ctx: ExtensionContext,
|
|
189
|
+
error: unknown,
|
|
190
|
+
): void {
|
|
191
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
192
|
+
if (!runtime.automaticCompaction) {
|
|
193
|
+
if (runtime.persistenceRetryNeeded) {
|
|
194
|
+
ctx.ui.notify(`Automatic compaction did not start: ${safeTerminalText(reason)}`, "error");
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
stopAutomaticCompactionRecovery(pi, runtime, ctx, `automatic compaction failed: ${reason}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function settleGoalTurn(
|
|
202
|
+
pi: ExtensionAPI,
|
|
203
|
+
runtime: GoalRuntime,
|
|
204
|
+
ctx: ExtensionContext,
|
|
205
|
+
settledTurn: number,
|
|
206
|
+
): void {
|
|
207
|
+
const state = runtime.state;
|
|
208
|
+
if (state?.status !== "active") return;
|
|
209
|
+
const decision = state.turnDecision;
|
|
210
|
+
if (!decision || decision.turn !== settledTurn) {
|
|
211
|
+
pauseGoalAfterFailure(pi, runtime, ctx, "no turn decision");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (pauseGoalAtTurnLimit(pi, runtime, ctx)) return;
|
|
215
|
+
scheduleGoalContinuation(pi, runtime, ctx);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function registerGoalSettlement(
|
|
219
|
+
pi: ExtensionAPI,
|
|
220
|
+
runtime: GoalRuntime,
|
|
221
|
+
): AutoCompactionGoalHandlers {
|
|
222
|
+
pi.on("agent_settled", (event, ctx) => {
|
|
223
|
+
runtime.blockedCompletion = undefined;
|
|
224
|
+
syncGoalUpdateTool(pi, runtime);
|
|
225
|
+
const wasGoalTurn = runtime.goalTurnInFlight;
|
|
226
|
+
const continuationWasScheduled = runtime.continuationScheduled;
|
|
227
|
+
const agentEndObserved = runtime.agentEndObserved;
|
|
228
|
+
const settledTurn = runtime.goalTurn?.turn;
|
|
229
|
+
runtime.goalTurnInFlight = false;
|
|
230
|
+
runtime.agentEndObserved = false;
|
|
231
|
+
runtime.continuationScheduled = false;
|
|
232
|
+
|
|
233
|
+
// ctx.compact() also aborts the request. An owned compaction must settle through
|
|
234
|
+
// its success/error callback; its cancellation cannot authorize recovery.
|
|
235
|
+
if ("aborted" in event && event.aborted === true && !runtime.automaticCompaction
|
|
236
|
+
&& runtime.lastStopReason !== "aborted") {
|
|
237
|
+
runtime.lastStopReason = undefined;
|
|
238
|
+
runtime.lastError = undefined;
|
|
239
|
+
if (runtime.state?.status === "active") {
|
|
240
|
+
pauseGoalAfterFailure(pi, runtime, ctx, "the agent request was cancelled");
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (runtime.automaticCompaction) {
|
|
246
|
+
const stopReason = runtime.lastStopReason;
|
|
247
|
+
const error = safeTerminalText(runtime.lastError ?? "");
|
|
248
|
+
runtime.lastStopReason = undefined;
|
|
249
|
+
runtime.lastError = undefined;
|
|
250
|
+
if (runtime.automaticCompaction.turnSettled
|
|
251
|
+
&& (settledTurn === undefined || settledTurn === runtime.automaticCompaction.turn)) {
|
|
252
|
+
// The interrupted turn already settled; a repeated settlement carries no new
|
|
253
|
+
// information, so keep waiting for compaction instead of failing the recovery.
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const expectedInterruption = stopReason === "aborted"
|
|
257
|
+
|| stopReason === "error" && error === "This operation was aborted";
|
|
258
|
+
if (!wasGoalTurn || !agentEndObserved) {
|
|
259
|
+
stopAutomaticCompactionRecovery(pi, runtime, ctx, "the goal turn ended without an agent result");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if ((stopReason === "error" || stopReason === "aborted") && !expectedInterruption) {
|
|
263
|
+
stopAutomaticCompactionRecovery(
|
|
264
|
+
pi,
|
|
265
|
+
runtime,
|
|
266
|
+
ctx,
|
|
267
|
+
error || "the agent turn failed",
|
|
268
|
+
);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (!expectedInterruption && runtime.automaticCompaction.resumeSameTurn) {
|
|
272
|
+
stopAutomaticCompactionRecovery(pi, runtime, ctx, "no turn decision");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (settledTurn !== undefined && runtime.automaticCompaction.turn === settledTurn) {
|
|
276
|
+
runtime.automaticCompaction.turnSettled = true;
|
|
277
|
+
} else {
|
|
278
|
+
stopAutomaticCompactionRecovery(pi, runtime, ctx, "the goal turn identity changed during compaction");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
finalizeAutomaticCompaction(pi, runtime, ctx);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!wasGoalTurn || runtime.state?.status !== "active") {
|
|
286
|
+
if (continuationWasScheduled && runtime.state?.status === "active") {
|
|
287
|
+
pauseGoalAfterFailure(pi, runtime, ctx, "the goal continuation ended before an agent turn started");
|
|
288
|
+
} else if (runtime.state?.status === "active"
|
|
289
|
+
&& (runtime.state.turnPhase === "ready" || runtime.state.turnPhase === "authorized")) {
|
|
290
|
+
scheduleGoalContinuation(pi, runtime, ctx);
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (settledTurn === undefined) {
|
|
295
|
+
pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn identity was lost");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (!agentEndObserved) {
|
|
299
|
+
pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn ended without an agent result");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (runtime.lastStopReason === "aborted") {
|
|
303
|
+
const reason = runtime.lastError || "the agent turn was aborted";
|
|
304
|
+
runtime.lastStopReason = undefined;
|
|
305
|
+
runtime.lastError = undefined;
|
|
306
|
+
pauseGoalForPossibleManualCompaction(pi, runtime, ctx, reason);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (runtime.lastStopReason === "error") {
|
|
310
|
+
const reason = runtime.lastError || "the agent turn failed";
|
|
311
|
+
runtime.lastStopReason = undefined;
|
|
312
|
+
runtime.lastError = undefined;
|
|
313
|
+
pauseGoalAfterFailure(pi, runtime, ctx, reason);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
runtime.lastStopReason = undefined;
|
|
317
|
+
runtime.lastError = undefined;
|
|
318
|
+
settleGoalTurn(pi, runtime, ctx, settledTurn);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
pi.on("session_compact", (event, ctx) => {
|
|
322
|
+
if (runtime.automaticCompaction !== undefined) return;
|
|
323
|
+
if (event.reason !== "manual") return;
|
|
324
|
+
recoverGoalAfterManualCompaction(pi, runtime, ctx);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
hasAcceptedTerminalDecision: (): boolean => runtime.blockedCompletion?.kind === "accepted"
|
|
329
|
+
&& runtime.blockedCompletion.generation === runtime.lifecycleGeneration,
|
|
330
|
+
isActive: (ctx: ExtensionContext): boolean => isGoalModeSupported(ctx)
|
|
331
|
+
&& isSavedSession(ctx)
|
|
332
|
+
&& runtime.state?.status === "active",
|
|
333
|
+
onRequested: (): void => {
|
|
334
|
+
if (runtime.state?.status !== "active") return;
|
|
335
|
+
const current = runtime.state;
|
|
336
|
+
// A turn that already accepted continue or a blocker audit owns a pending
|
|
337
|
+
// authorization; recovery must take the next-turn path, never resume the decided turn.
|
|
338
|
+
const resumeSameTurn = runtime.goalTurnInFlight && current.turnDecision === undefined;
|
|
339
|
+
try {
|
|
340
|
+
const paused = transitionGoal(pi, runtime, "pause", "paused", undefined, {
|
|
341
|
+
keepTurnForRecovery: true,
|
|
342
|
+
preserveTurnAuthorization: true,
|
|
343
|
+
});
|
|
344
|
+
runtime.automaticCompaction = {
|
|
345
|
+
pausedRevision: paused.revision,
|
|
346
|
+
outcome: "pending",
|
|
347
|
+
turnSettled: false,
|
|
348
|
+
turn: current.turns,
|
|
349
|
+
resumeSameTurn,
|
|
350
|
+
};
|
|
351
|
+
} catch (error) {
|
|
352
|
+
const reason = boundGoalText(`automatic compaction pause could not be saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
353
|
+
runtime.state = current ? pauseGoalState(current, reason, Date.now()) : undefined;
|
|
354
|
+
syncGoalUpdateTool(pi, runtime);
|
|
355
|
+
runtime.persistenceRetryNeeded = true;
|
|
356
|
+
runtime.continuationScheduled = false;
|
|
357
|
+
runtime.automaticCompaction = undefined;
|
|
358
|
+
runtime.goalTurn = undefined;
|
|
359
|
+
runtime.goalTurnInFlight = false;
|
|
360
|
+
runtime.requestRender?.();
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
onCompleted: (ctx: ExtensionContext): void => completeAutomaticCompaction(pi, runtime, ctx),
|
|
365
|
+
onFailed: (ctx: ExtensionContext, error: unknown): void => failAutomaticCompaction(pi, runtime, ctx, error),
|
|
366
|
+
onSkipped: (ctx: ExtensionContext): void => skipAutomaticCompaction(pi, runtime, ctx),
|
|
367
|
+
};
|
|
368
|
+
}
|