killeros 2.1.26 → 2.1.28

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.
@@ -1,9 +1,18 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { AutoCompactionGoalHandlers } from "./auto-compaction.ts";
3
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";
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";
7
16
  import { safeTerminalText } from "./safe-terminal-text.ts";
8
17
 
9
18
  function pauseGoalForPossibleManualCompaction(
@@ -13,14 +22,16 @@ function pauseGoalForPossibleManualCompaction(
13
22
  reason: string,
14
23
  ): void {
15
24
  if (runtime.state?.status !== "active") return;
16
- const safeReason = safeTerminalText(reason);
25
+ const safeReason = boundGoalText(reason) || "the agent turn was aborted";
17
26
  try {
18
27
  transitionGoal(pi, runtime, "error", "paused", safeReason, {
19
28
  resumeAfterManualCompaction: true,
29
+ keepTurnForRecovery: true,
30
+ preserveTurnAuthorization: true,
20
31
  });
21
32
  } catch {
22
33
  const current = runtime.state;
23
- runtime.state = current ? pauseGoalState(current, safeReason, Date.now(), true) : undefined;
34
+ runtime.state = current ? pauseGoalState(current, safeReason, Date.now(), true, true) : undefined;
24
35
  syncGoalUpdateTool(pi, runtime);
25
36
  runtime.persistenceRetryNeeded = true;
26
37
  runtime.continuationScheduled = false;
@@ -28,7 +39,7 @@ function pauseGoalForPossibleManualCompaction(
28
39
  runtime.requestRender?.();
29
40
  }
30
41
  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.",
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.",
32
43
  "warning",
33
44
  );
34
45
  }
@@ -36,14 +47,20 @@ function pauseGoalForPossibleManualCompaction(
36
47
  function recoverGoalAfterManualCompaction(
37
48
  pi: ExtensionAPI,
38
49
  runtime: GoalRuntime,
39
- initState: InitRuntime,
40
50
  ctx: ExtensionContext,
41
51
  ): boolean {
42
52
  if (runtime.state?.status !== "paused"
43
- || runtime.state.resumeAfterManualCompaction !== true
44
- || initState.active) return false;
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;
45
59
  try {
46
- transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
60
+ resumed = transitionGoal(pi, runtime, "resume", "active", undefined, {
61
+ ...(sameTurn ? { resumeInterruptedTurn: true as const } : {}),
62
+ preserveTurnAuthorization: true,
63
+ });
47
64
  } catch (error) {
48
65
  runtime.persistenceRetryNeeded = true;
49
66
  reportError(ctx, "Manual compaction succeeded, but the goal could not be resumed", error);
@@ -51,63 +68,93 @@ function recoverGoalAfterManualCompaction(
51
68
  }
52
69
  runtime.continuationScheduled = false;
53
70
  runtime.automaticCompaction = undefined;
54
- ctx.ui.notify("Manual compaction complete. Goal resumed.", "info");
55
- setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
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
+ }
56
85
  return true;
57
86
  }
58
87
 
59
- /** Resumes the paused revision after both compaction and goal-turn settlement report an outcome. */
88
+ /** Resumes the paused revision only after compaction and the interrupted turn have both settled. */
60
89
  function finalizeAutomaticCompaction(
61
90
  pi: ExtensionAPI,
62
91
  runtime: GoalRuntime,
63
- initState: InitRuntime,
64
92
  ctx: ExtensionContext,
65
93
  ): void {
66
94
  const recovery = runtime.automaticCompaction;
67
95
  if (!recovery || recovery.outcome === "pending" || !recovery.turnSettled) return;
68
- const skipped = recovery.outcome === "skipped";
69
- runtime.automaticCompaction = undefined;
70
96
  if (runtime.state?.status !== "paused"
71
- || runtime.state.revision !== recovery.pausedRevision
72
- || initState.active) return;
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;
73
106
  try {
74
- transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
107
+ resumed = transitionGoal(pi, runtime, "resume", "active", undefined, {
108
+ ...(sameTurn ? { resumeInterruptedTurn: true as const } : {}),
109
+ ...(!sameTurn && state.turnDecision !== undefined ? { preserveTurnAuthorization: true as const } : {}),
110
+ });
75
111
  } catch (error) {
76
112
  runtime.persistenceRetryNeeded = true;
77
- reportError(ctx, skipped
78
- ? "Automatic compaction was skipped, but the goal could not be resumed"
113
+ reportError(ctx, sameTurn
114
+ ? "Automatic compaction succeeded, but the interrupted goal turn could not be resumed"
79
115
  : "Automatic compaction succeeded, but the goal could not be resumed", error);
80
116
  return;
81
117
  }
82
118
  runtime.continuationScheduled = false;
83
- setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
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 }));
84
136
  }
85
137
 
86
- /** Records Pi's successful compaction callback and attempts guarded recovery. */
87
138
  function completeAutomaticCompaction(
88
139
  pi: ExtensionAPI,
89
140
  runtime: GoalRuntime,
90
- initState: InitRuntime,
91
141
  ctx: ExtensionContext,
92
142
  ): void {
93
143
  if (!runtime.automaticCompaction) return;
94
144
  runtime.automaticCompaction.outcome = "completed";
95
- finalizeAutomaticCompaction(pi, runtime, initState, ctx);
145
+ finalizeAutomaticCompaction(pi, runtime, ctx);
96
146
  }
97
147
 
98
- /** Records Pi's expected session-too-small rejection and resumes without claiming compaction succeeded. */
99
148
  function skipAutomaticCompaction(
100
149
  pi: ExtensionAPI,
101
150
  runtime: GoalRuntime,
102
- initState: InitRuntime,
103
151
  ctx: ExtensionContext,
104
152
  ): void {
105
153
  if (!runtime.automaticCompaction) return;
106
154
  runtime.automaticCompaction.outcome = "skipped";
107
- finalizeAutomaticCompaction(pi, runtime, initState, ctx);
155
+ finalizeAutomaticCompaction(pi, runtime, ctx);
108
156
  }
109
157
 
110
- /** Consumes automatic recovery and records its failure on the eligible paused goal. */
111
158
  function stopAutomaticCompactionRecovery(
112
159
  pi: ExtensionAPI,
113
160
  runtime: GoalRuntime,
@@ -116,13 +163,17 @@ function stopAutomaticCompactionRecovery(
116
163
  ): void {
117
164
  const recovery = runtime.automaticCompaction;
118
165
  runtime.automaticCompaction = undefined;
119
- const safeReason = safeTerminalText(reason);
166
+ const safeReason = boundGoalText(reason) || "automatic compaction failed";
120
167
  if (runtime.state?.status !== "paused" || runtime.state.revision !== recovery?.pausedRevision) return;
121
168
  try {
122
169
  transitionGoal(pi, runtime, "error", "paused", safeReason);
123
170
  } catch {
124
- runtime.state = { ...runtime.state, result: safeReason };
171
+ runtime.state = pauseGoalState(runtime.state, safeReason, Date.now());
172
+ syncGoalUpdateTool(pi, runtime);
125
173
  runtime.persistenceRetryNeeded = true;
174
+ runtime.continuationScheduled = false;
175
+ runtime.goalTurn = undefined;
176
+ runtime.goalTurnInFlight = false;
126
177
  runtime.requestRender?.();
127
178
  }
128
179
  ctx.ui.notify(
@@ -131,7 +182,6 @@ function stopAutomaticCompactionRecovery(
131
182
  );
132
183
  }
133
184
 
134
- /** Leaves the goal paused when Pi rejects automatic compaction. */
135
185
  function failAutomaticCompaction(
136
186
  pi: ExtensionAPI,
137
187
  runtime: GoalRuntime,
@@ -148,15 +198,32 @@ function failAutomaticCompaction(
148
198
  stopAutomaticCompactionRecovery(pi, runtime, ctx, `automatic compaction failed: ${reason}`);
149
199
  }
150
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
+
151
218
  export function registerGoalSettlement(
152
219
  pi: ExtensionAPI,
153
220
  runtime: GoalRuntime,
154
- initState: InitRuntime,
155
221
  ): AutoCompactionGoalHandlers {
156
222
  pi.on("agent_settled", (_event, ctx) => {
157
223
  const wasGoalTurn = runtime.goalTurnInFlight;
158
224
  const continuationWasScheduled = runtime.continuationScheduled;
159
225
  const agentEndObserved = runtime.agentEndObserved;
226
+ const settledTurn = runtime.goalTurn?.turn;
160
227
  runtime.goalTurnInFlight = false;
161
228
  runtime.agentEndObserved = false;
162
229
  runtime.continuationScheduled = false;
@@ -166,6 +233,12 @@ export function registerGoalSettlement(
166
233
  const error = safeTerminalText(runtime.lastError ?? "");
167
234
  runtime.lastStopReason = undefined;
168
235
  runtime.lastError = undefined;
236
+ if (runtime.automaticCompaction.turnSettled
237
+ && (settledTurn === undefined || settledTurn === runtime.automaticCompaction.turn)) {
238
+ // The interrupted turn already settled; a repeated settlement carries no new
239
+ // information, so keep waiting for compaction instead of failing the recovery.
240
+ return;
241
+ }
169
242
  const expectedInterruption = stopReason === "aborted"
170
243
  || stopReason === "error" && error === "This operation was aborted";
171
244
  if (!wasGoalTurn || !agentEndObserved) {
@@ -181,19 +254,29 @@ export function registerGoalSettlement(
181
254
  );
182
255
  return;
183
256
  }
184
- runtime.automaticCompaction.turnSettled = true;
185
- finalizeAutomaticCompaction(pi, runtime, initState, ctx);
257
+ if (settledTurn !== undefined && runtime.automaticCompaction.turn === settledTurn) {
258
+ runtime.automaticCompaction.turnSettled = true;
259
+ } else {
260
+ stopAutomaticCompactionRecovery(pi, runtime, ctx, "the goal turn identity changed during compaction");
261
+ return;
262
+ }
263
+ finalizeAutomaticCompaction(pi, runtime, ctx);
186
264
  return;
187
265
  }
188
266
 
189
- if (!wasGoalTurn || runtime.state?.status !== "active" || initState.active) {
190
- if (continuationWasScheduled && runtime.state?.status === "active" && !initState.active) {
267
+ if (!wasGoalTurn || runtime.state?.status !== "active") {
268
+ if (continuationWasScheduled && runtime.state?.status === "active") {
191
269
  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);
270
+ } else if (runtime.state?.status === "active"
271
+ && (runtime.state.turnPhase === "ready" || runtime.state.turnPhase === "authorized")) {
272
+ scheduleGoalContinuation(pi, runtime, ctx);
194
273
  }
195
274
  return;
196
275
  }
276
+ if (settledTurn === undefined) {
277
+ pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn identity was lost");
278
+ return;
279
+ }
197
280
  if (!agentEndObserved) {
198
281
  pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn ended without an agent result");
199
282
  return;
@@ -214,44 +297,52 @@ export function registerGoalSettlement(
214
297
  }
215
298
  runtime.lastStopReason = undefined;
216
299
  runtime.lastError = undefined;
217
- if (pauseGoalAtTurnLimit(pi, runtime, ctx)) return;
218
- scheduleGoalContinuation(pi, runtime, initState, ctx);
300
+ settleGoalTurn(pi, runtime, ctx, settledTurn);
219
301
  });
220
302
 
221
303
  pi.on("session_compact", (event, ctx) => {
222
304
  if (runtime.automaticCompaction !== undefined) return;
223
305
  if (event.reason !== "manual") return;
224
- recoverGoalAfterManualCompaction(pi, runtime, initState, ctx);
306
+ recoverGoalAfterManualCompaction(pi, runtime, ctx);
225
307
  });
226
308
 
227
309
  return {
228
310
  isActive: (ctx: ExtensionContext): boolean => isGoalModeSupported(ctx)
229
311
  && isSavedSession(ctx)
230
- && runtime.state?.status === "active"
231
- && !initState.active,
312
+ && runtime.state?.status === "active",
232
313
  onRequested: (): void => {
233
314
  if (runtime.state?.status !== "active") return;
315
+ const current = runtime.state;
316
+ // A turn that already accepted continue or a blocker audit owns a pending
317
+ // authorization; recovery must take the next-turn path, never resume the decided turn.
318
+ const resumeSameTurn = runtime.goalTurnInFlight && current.turnDecision === undefined;
234
319
  try {
235
- const paused = transitionGoal(pi, runtime, "pause", "paused");
320
+ const paused = transitionGoal(pi, runtime, "pause", "paused", undefined, {
321
+ keepTurnForRecovery: true,
322
+ preserveTurnAuthorization: true,
323
+ });
236
324
  runtime.automaticCompaction = {
237
325
  pausedRevision: paused.revision,
238
326
  outcome: "pending",
239
327
  turnSettled: false,
328
+ turn: current.turns,
329
+ resumeSameTurn,
240
330
  };
241
331
  } catch (error) {
242
- const current = runtime.state;
243
- const reason = safeTerminalText(`automatic compaction pause could not be saved: ${error instanceof Error ? error.message : String(error)}`);
332
+ const reason = boundGoalText(`automatic compaction pause could not be saved: ${error instanceof Error ? error.message : String(error)}`);
244
333
  runtime.state = current ? pauseGoalState(current, reason, Date.now()) : undefined;
245
334
  syncGoalUpdateTool(pi, runtime);
246
335
  runtime.persistenceRetryNeeded = true;
247
336
  runtime.continuationScheduled = false;
248
337
  runtime.automaticCompaction = undefined;
338
+ runtime.goalTurn = undefined;
339
+ runtime.goalTurnInFlight = false;
249
340
  runtime.requestRender?.();
250
341
  throw error;
251
342
  }
252
343
  },
253
- onCompleted: (ctx: ExtensionContext): void => completeAutomaticCompaction(pi, runtime, initState, ctx),
344
+ onCompleted: (ctx: ExtensionContext): void => completeAutomaticCompaction(pi, runtime, ctx),
254
345
  onFailed: (ctx: ExtensionContext, error: unknown): void => failAutomaticCompaction(pi, runtime, ctx, error),
255
- onSkipped: (ctx: ExtensionContext): void => skipAutomaticCompaction(pi, runtime, initState, ctx),
346
+ onSkipped: (ctx: ExtensionContext): void => skipAutomaticCompaction(pi, runtime, ctx),
256
347
  };
257
348
  }