killeros 2.1.27 → 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 } 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
  }
@@ -40,8 +51,16 @@ function recoverGoalAfterManualCompaction(
40
51
  ): boolean {
41
52
  if (runtime.state?.status !== "paused"
42
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;
43
59
  try {
44
- 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
+ });
45
64
  } catch (error) {
46
65
  runtime.persistenceRetryNeeded = true;
47
66
  reportError(ctx, "Manual compaction succeeded, but the goal could not be resumed", error);
@@ -49,12 +68,24 @@ function recoverGoalAfterManualCompaction(
49
68
  }
50
69
  runtime.continuationScheduled = false;
51
70
  runtime.automaticCompaction = undefined;
52
- ctx.ui.notify("Manual compaction complete. Goal resumed.", "info");
53
- setImmediate(() => scheduleGoalContinuation(pi, runtime, 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
+ }
54
85
  return true;
55
86
  }
56
87
 
57
- /** 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. */
58
89
  function finalizeAutomaticCompaction(
59
90
  pi: ExtensionAPI,
60
91
  runtime: GoalRuntime,
@@ -62,24 +93,48 @@ function finalizeAutomaticCompaction(
62
93
  ): void {
63
94
  const recovery = runtime.automaticCompaction;
64
95
  if (!recovery || recovery.outcome === "pending" || !recovery.turnSettled) return;
65
- const skipped = recovery.outcome === "skipped";
66
- runtime.automaticCompaction = undefined;
67
96
  if (runtime.state?.status !== "paused"
68
- || runtime.state.revision !== recovery.pausedRevision) 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;
69
106
  try {
70
- 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
+ });
71
111
  } catch (error) {
72
112
  runtime.persistenceRetryNeeded = true;
73
- reportError(ctx, skipped
74
- ? "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"
75
115
  : "Automatic compaction succeeded, but the goal could not be resumed", error);
76
116
  return;
77
117
  }
78
118
  runtime.continuationScheduled = false;
79
- setImmediate(() => scheduleGoalContinuation(pi, runtime, 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 }));
80
136
  }
81
137
 
82
- /** Records Pi's successful compaction callback and attempts guarded recovery. */
83
138
  function completeAutomaticCompaction(
84
139
  pi: ExtensionAPI,
85
140
  runtime: GoalRuntime,
@@ -90,7 +145,6 @@ function completeAutomaticCompaction(
90
145
  finalizeAutomaticCompaction(pi, runtime, ctx);
91
146
  }
92
147
 
93
- /** Records Pi's expected session-too-small rejection and resumes without claiming compaction succeeded. */
94
148
  function skipAutomaticCompaction(
95
149
  pi: ExtensionAPI,
96
150
  runtime: GoalRuntime,
@@ -101,7 +155,6 @@ function skipAutomaticCompaction(
101
155
  finalizeAutomaticCompaction(pi, runtime, ctx);
102
156
  }
103
157
 
104
- /** Consumes automatic recovery and records its failure on the eligible paused goal. */
105
158
  function stopAutomaticCompactionRecovery(
106
159
  pi: ExtensionAPI,
107
160
  runtime: GoalRuntime,
@@ -110,13 +163,17 @@ function stopAutomaticCompactionRecovery(
110
163
  ): void {
111
164
  const recovery = runtime.automaticCompaction;
112
165
  runtime.automaticCompaction = undefined;
113
- const safeReason = safeTerminalText(reason);
166
+ const safeReason = boundGoalText(reason) || "automatic compaction failed";
114
167
  if (runtime.state?.status !== "paused" || runtime.state.revision !== recovery?.pausedRevision) return;
115
168
  try {
116
169
  transitionGoal(pi, runtime, "error", "paused", safeReason);
117
170
  } catch {
118
- runtime.state = { ...runtime.state, result: safeReason };
171
+ runtime.state = pauseGoalState(runtime.state, safeReason, Date.now());
172
+ syncGoalUpdateTool(pi, runtime);
119
173
  runtime.persistenceRetryNeeded = true;
174
+ runtime.continuationScheduled = false;
175
+ runtime.goalTurn = undefined;
176
+ runtime.goalTurnInFlight = false;
120
177
  runtime.requestRender?.();
121
178
  }
122
179
  ctx.ui.notify(
@@ -125,7 +182,6 @@ function stopAutomaticCompactionRecovery(
125
182
  );
126
183
  }
127
184
 
128
- /** Leaves the goal paused when Pi rejects automatic compaction. */
129
185
  function failAutomaticCompaction(
130
186
  pi: ExtensionAPI,
131
187
  runtime: GoalRuntime,
@@ -142,6 +198,23 @@ function failAutomaticCompaction(
142
198
  stopAutomaticCompactionRecovery(pi, runtime, ctx, `automatic compaction failed: ${reason}`);
143
199
  }
144
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
+
145
218
  export function registerGoalSettlement(
146
219
  pi: ExtensionAPI,
147
220
  runtime: GoalRuntime,
@@ -150,6 +223,7 @@ export function registerGoalSettlement(
150
223
  const wasGoalTurn = runtime.goalTurnInFlight;
151
224
  const continuationWasScheduled = runtime.continuationScheduled;
152
225
  const agentEndObserved = runtime.agentEndObserved;
226
+ const settledTurn = runtime.goalTurn?.turn;
153
227
  runtime.goalTurnInFlight = false;
154
228
  runtime.agentEndObserved = false;
155
229
  runtime.continuationScheduled = false;
@@ -159,6 +233,12 @@ export function registerGoalSettlement(
159
233
  const error = safeTerminalText(runtime.lastError ?? "");
160
234
  runtime.lastStopReason = undefined;
161
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
+ }
162
242
  const expectedInterruption = stopReason === "aborted"
163
243
  || stopReason === "error" && error === "This operation was aborted";
164
244
  if (!wasGoalTurn || !agentEndObserved) {
@@ -174,7 +254,12 @@ export function registerGoalSettlement(
174
254
  );
175
255
  return;
176
256
  }
177
- runtime.automaticCompaction.turnSettled = true;
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
+ }
178
263
  finalizeAutomaticCompaction(pi, runtime, ctx);
179
264
  return;
180
265
  }
@@ -182,11 +267,16 @@ export function registerGoalSettlement(
182
267
  if (!wasGoalTurn || runtime.state?.status !== "active") {
183
268
  if (continuationWasScheduled && runtime.state?.status === "active") {
184
269
  pauseGoalAfterFailure(pi, runtime, ctx, "the goal continuation ended before an agent turn started");
185
- } else if (runtime.state?.status === "active") {
270
+ } else if (runtime.state?.status === "active"
271
+ && (runtime.state.turnPhase === "ready" || runtime.state.turnPhase === "authorized")) {
186
272
  scheduleGoalContinuation(pi, runtime, ctx);
187
273
  }
188
274
  return;
189
275
  }
276
+ if (settledTurn === undefined) {
277
+ pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn identity was lost");
278
+ return;
279
+ }
190
280
  if (!agentEndObserved) {
191
281
  pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn ended without an agent result");
192
282
  return;
@@ -207,8 +297,7 @@ export function registerGoalSettlement(
207
297
  }
208
298
  runtime.lastStopReason = undefined;
209
299
  runtime.lastError = undefined;
210
- if (pauseGoalAtTurnLimit(pi, runtime, ctx)) return;
211
- scheduleGoalContinuation(pi, runtime, ctx);
300
+ settleGoalTurn(pi, runtime, ctx, settledTurn);
212
301
  });
213
302
 
214
303
  pi.on("session_compact", (event, ctx) => {
@@ -223,21 +312,31 @@ export function registerGoalSettlement(
223
312
  && runtime.state?.status === "active",
224
313
  onRequested: (): void => {
225
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;
226
319
  try {
227
- const paused = transitionGoal(pi, runtime, "pause", "paused");
320
+ const paused = transitionGoal(pi, runtime, "pause", "paused", undefined, {
321
+ keepTurnForRecovery: true,
322
+ preserveTurnAuthorization: true,
323
+ });
228
324
  runtime.automaticCompaction = {
229
325
  pausedRevision: paused.revision,
230
326
  outcome: "pending",
231
327
  turnSettled: false,
328
+ turn: current.turns,
329
+ resumeSameTurn,
232
330
  };
233
331
  } catch (error) {
234
- const current = runtime.state;
235
- 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)}`);
236
333
  runtime.state = current ? pauseGoalState(current, reason, Date.now()) : undefined;
237
334
  syncGoalUpdateTool(pi, runtime);
238
335
  runtime.persistenceRetryNeeded = true;
239
336
  runtime.continuationScheduled = false;
240
337
  runtime.automaticCompaction = undefined;
338
+ runtime.goalTurn = undefined;
339
+ runtime.goalTurnInFlight = false;
241
340
  runtime.requestRender?.();
242
341
  throw error;
243
342
  }