killeros 2.0.18 → 2.0.19
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 +12 -0
- package/README.md +1 -1
- package/killeros/activity.ts +4 -0
- package/killeros/codex-fast-state.ts +5 -7
- package/killeros/commands.ts +5 -3
- package/killeros/errors.ts +8 -0
- package/killeros/footer.ts +1 -1
- package/killeros/goals.ts +309 -116
- package/killeros/hooks.ts +120 -47
- package/killeros/init-evidence.ts +4 -2
- package/killeros/init-target.ts +28 -20
- package/killeros/init.ts +12 -5
- package/killeros/personal-instructions.ts +1 -1
- package/killeros/question.ts +15 -7
- package/killeros/runtime.ts +39 -7
- package/killeros/settings.ts +8 -5
- package/killeros/shell-ui.ts +14 -5
- package/package.json +1 -1
package/killeros/goals.ts
CHANGED
|
@@ -7,9 +7,9 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
7
7
|
import { Type } from "typebox";
|
|
8
8
|
import { BoundedText } from "./bounded-text.ts";
|
|
9
9
|
import { formatTime, formatTokens } from "./display.ts";
|
|
10
|
-
import { reportError } from "./errors.ts";
|
|
10
|
+
import { hasErrorCode, reportError } from "./errors.ts";
|
|
11
11
|
import { resolvePersonalInstructions } from "./personal-instructions.ts";
|
|
12
|
-
import type { GoalBlockerAudit, GoalFileBaseline, GoalFileVerification, GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
|
|
12
|
+
import type { GoalBlockerAudit, GoalFileBaseline, GoalFileVerification, GoalRuntime, GoalState, GoalStateCommon, GoalStatus, InitRuntime } from "./runtime.ts";
|
|
13
13
|
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
14
14
|
|
|
15
15
|
const GOAL_ENTRY_TYPE = "killeros-goal";
|
|
@@ -69,26 +69,30 @@ function finiteNonNegative(value: unknown): value is number {
|
|
|
69
69
|
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
function isUnknownRecord(value: unknown): value is Record<string, unknown> {
|
|
73
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
72
76
|
function isGoalFileBaseline(value: unknown): value is GoalFileBaseline {
|
|
73
|
-
if (!value
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
&& finiteNonNegative(
|
|
79
|
-
&& (
|
|
80
|
-
|
|
81
|
-
||
|
|
77
|
+
if (!isUnknownRecord(value)) return false;
|
|
78
|
+
if (value.exists === false) {
|
|
79
|
+
return value.size === undefined && value.mtimeMs === undefined && value.contentHash === undefined;
|
|
80
|
+
}
|
|
81
|
+
return value.exists === true
|
|
82
|
+
&& finiteNonNegative(value.size)
|
|
83
|
+
&& finiteNonNegative(value.mtimeMs)
|
|
84
|
+
&& (value.contentHash === undefined
|
|
85
|
+
|| value.contentHash === null
|
|
86
|
+
|| typeof value.contentHash === "string" && /^[a-f0-9]{64}$/u.test(value.contentHash));
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
function isGoalFileVerification(value: unknown): value is GoalFileVerification {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
&&
|
|
89
|
-
&&
|
|
90
|
-
&&
|
|
91
|
-
&& isGoalFileBaseline(candidate.baseline);
|
|
90
|
+
return isUnknownRecord(value)
|
|
91
|
+
&& value.kind === "file"
|
|
92
|
+
&& typeof value.path === "string"
|
|
93
|
+
&& value.path === value.path.trim()
|
|
94
|
+
&& isAbsoluteFilePath(value.path)
|
|
95
|
+
&& isGoalFileBaseline(value.baseline);
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
function isAbsoluteFilePath(value: string): boolean {
|
|
@@ -119,7 +123,7 @@ function captureGoalFileBaseline(filePath: string): GoalFileBaseline {
|
|
|
119
123
|
try {
|
|
120
124
|
artifact = lstatSync(filePath);
|
|
121
125
|
} catch (error) {
|
|
122
|
-
if ((error
|
|
126
|
+
if (hasErrorCode(error, "ENOENT")) return { exists: false };
|
|
123
127
|
throw error;
|
|
124
128
|
}
|
|
125
129
|
const baseline = { exists: true as const, size: artifact.size, mtimeMs: artifact.mtimeMs };
|
|
@@ -175,56 +179,98 @@ function verifyGoalDeliverable(verification: GoalFileVerification): void {
|
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
function isGoalBlockerAudit(value: unknown, turns: number, status: GoalStatus): value is GoalBlockerAudit {
|
|
178
|
-
if (!value
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
||
|
|
182
|
-
|| !Number.isInteger(
|
|
183
|
-
|
|
182
|
+
if (!isUnknownRecord(value)
|
|
183
|
+
|| typeof value.key !== "string"
|
|
184
|
+
|| !/^[a-z0-9][a-z0-9._-]{0,119}$/u.test(value.key)
|
|
185
|
+
|| typeof value.streak !== "number" || !Number.isInteger(value.streak) || value.streak < 1 || value.streak > 3
|
|
186
|
+
|| typeof value.lastTurn !== "number" || !Number.isInteger(value.lastTurn) || value.lastTurn < 1 || value.lastTurn > turns) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
184
189
|
if (status === "complete") return false;
|
|
185
|
-
return status === "blocked" ?
|
|
190
|
+
return status === "blocked" ? value.streak === 3 : value.streak < 3;
|
|
186
191
|
}
|
|
187
192
|
|
|
188
193
|
function parseGoalState(value: unknown): GoalState | undefined {
|
|
189
|
-
if (!value
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
||
|
|
194
|
+
if (!isUnknownRecord(value)) return undefined;
|
|
195
|
+
const {
|
|
196
|
+
version,
|
|
197
|
+
revision,
|
|
198
|
+
objective,
|
|
199
|
+
status,
|
|
200
|
+
createdAt,
|
|
201
|
+
updatedAt,
|
|
202
|
+
activeMilliseconds,
|
|
203
|
+
activeStartedAt,
|
|
204
|
+
turns,
|
|
205
|
+
blockedAuditStartTurn,
|
|
206
|
+
baselineTokens,
|
|
207
|
+
result,
|
|
208
|
+
resumeAfterManualCompaction,
|
|
209
|
+
blockerAudit,
|
|
210
|
+
verification,
|
|
211
|
+
} = value;
|
|
212
|
+
if (version !== GOAL_VERSION
|
|
213
|
+
|| typeof revision !== "number" || !Number.isInteger(revision) || revision < 1
|
|
214
|
+
|| typeof objective !== "string" || !objective.trim() || [...objective].length > GOAL_OBJECTIVE_LIMIT
|
|
215
|
+
|| !isGoalStatus(status)
|
|
216
|
+
|| !finiteNonNegative(createdAt)
|
|
217
|
+
|| !finiteNonNegative(updatedAt)
|
|
218
|
+
|| !finiteNonNegative(activeMilliseconds)
|
|
219
|
+
|| typeof turns !== "number" || !Number.isInteger(turns) || turns < 0
|
|
220
|
+
|| blockedAuditStartTurn !== undefined
|
|
221
|
+
&& (typeof blockedAuditStartTurn !== "number" || !Number.isInteger(blockedAuditStartTurn)
|
|
222
|
+
|| blockedAuditStartTurn < 0 || blockedAuditStartTurn > turns)
|
|
223
|
+
|| !finiteNonNegative(baselineTokens)
|
|
224
|
+
|| result !== undefined && typeof result !== "string"
|
|
225
|
+
|| verification !== undefined && !isGoalFileVerification(verification)
|
|
226
|
+
|| resumeAfterManualCompaction !== undefined && resumeAfterManualCompaction !== true
|
|
227
|
+
|| blockerAudit !== undefined && !isGoalBlockerAudit(blockerAudit, turns, status)) {
|
|
209
228
|
return undefined;
|
|
210
229
|
}
|
|
211
|
-
|
|
230
|
+
|
|
231
|
+
const common: GoalStateCommon = {
|
|
212
232
|
version: GOAL_VERSION,
|
|
213
|
-
revision
|
|
214
|
-
objective:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
baselineTokens: candidate.baselineTokens,
|
|
223
|
-
result: candidate.result,
|
|
224
|
-
resumeAfterManualCompaction: candidate.resumeAfterManualCompaction,
|
|
225
|
-
blockerAudit: candidate.blockerAudit,
|
|
226
|
-
verification: candidate.verification,
|
|
233
|
+
revision,
|
|
234
|
+
objective: objective.trim(),
|
|
235
|
+
createdAt,
|
|
236
|
+
updatedAt,
|
|
237
|
+
activeMilliseconds,
|
|
238
|
+
turns,
|
|
239
|
+
blockedAuditStartTurn: blockedAuditStartTurn ?? 0,
|
|
240
|
+
baselineTokens,
|
|
241
|
+
...(verification === undefined ? {} : { verification }),
|
|
227
242
|
};
|
|
243
|
+
switch (status) {
|
|
244
|
+
case "active":
|
|
245
|
+
if (!finiteNonNegative(activeStartedAt) || resumeAfterManualCompaction !== undefined) return undefined;
|
|
246
|
+
return {
|
|
247
|
+
...common,
|
|
248
|
+
status,
|
|
249
|
+
activeStartedAt,
|
|
250
|
+
...(result === undefined ? {} : { result }),
|
|
251
|
+
...(blockerAudit === undefined ? {} : { blockerAudit }),
|
|
252
|
+
};
|
|
253
|
+
case "paused":
|
|
254
|
+
if (activeStartedAt !== undefined) return undefined;
|
|
255
|
+
return {
|
|
256
|
+
...common,
|
|
257
|
+
status,
|
|
258
|
+
...(result === undefined ? {} : { result }),
|
|
259
|
+
...(blockerAudit === undefined ? {} : { blockerAudit }),
|
|
260
|
+
...(resumeAfterManualCompaction === undefined ? {} : { resumeAfterManualCompaction }),
|
|
261
|
+
};
|
|
262
|
+
case "blocked":
|
|
263
|
+
if (activeStartedAt !== undefined || resumeAfterManualCompaction !== undefined || typeof result !== "string") return undefined;
|
|
264
|
+
return { ...common, status, result, ...(blockerAudit === undefined ? {} : { blockerAudit }) };
|
|
265
|
+
case "complete":
|
|
266
|
+
if (activeStartedAt !== undefined || resumeAfterManualCompaction !== undefined
|
|
267
|
+
|| typeof result !== "string" || blockerAudit !== undefined) return undefined;
|
|
268
|
+
return { ...common, status, result };
|
|
269
|
+
default: {
|
|
270
|
+
const exhaustive: never = status;
|
|
271
|
+
return exhaustive;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
228
274
|
}
|
|
229
275
|
|
|
230
276
|
function goalBranchEntries(ctx: ExtensionContext): ReturnType<ExtensionContext["sessionManager"]["getEntries"]> {
|
|
@@ -240,37 +286,60 @@ function restoreGoalState(ctx: ExtensionContext): RestoredGoalState {
|
|
|
240
286
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
241
287
|
const entry = entries[index];
|
|
242
288
|
if (entry?.type !== "custom" || entry.customType !== GOAL_ENTRY_TYPE) continue;
|
|
243
|
-
const data = entry.data
|
|
244
|
-
if (!data || data.version !== GOAL_VERSION || data.state === null) {
|
|
289
|
+
const data: unknown = entry.data;
|
|
290
|
+
if (!isUnknownRecord(data) || data.version !== GOAL_VERSION || data.state === null) {
|
|
245
291
|
return { state: undefined };
|
|
246
292
|
}
|
|
247
|
-
|
|
293
|
+
// v2.0.18 shutdown checkpoints stopped active clocks by omitting activeStartedAt.
|
|
294
|
+
const savedState = data.event === "checkpoint"
|
|
295
|
+
&& isUnknownRecord(data.state)
|
|
296
|
+
&& data.state.status === "active"
|
|
297
|
+
&& data.state.activeStartedAt === undefined
|
|
298
|
+
? { ...data.state, activeStartedAt: Date.now() }
|
|
299
|
+
: data.state;
|
|
300
|
+
const restored = parseGoalState(savedState);
|
|
248
301
|
if (!restored) return { state: undefined };
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
|
|
302
|
+
if (restored.status === "active") {
|
|
303
|
+
return { state: { ...restored, activeStartedAt: Date.now() } };
|
|
304
|
+
}
|
|
305
|
+
if (restored.status === "paused") {
|
|
306
|
+
const { resumeAfterManualCompaction: _resume, ...state } = restored;
|
|
307
|
+
return { state };
|
|
308
|
+
}
|
|
309
|
+
return { state: restored };
|
|
254
310
|
}
|
|
255
311
|
return { state: undefined };
|
|
256
312
|
}
|
|
257
313
|
|
|
258
314
|
export function goalElapsedMilliseconds(state: GoalState, now = Date.now()): number {
|
|
259
|
-
const activeInterval = state.status === "active"
|
|
315
|
+
const activeInterval = state.status === "active"
|
|
260
316
|
? Math.max(0, now - state.activeStartedAt)
|
|
261
317
|
: 0;
|
|
262
318
|
return state.activeMilliseconds + activeInterval;
|
|
263
319
|
}
|
|
264
320
|
|
|
265
|
-
function
|
|
266
|
-
if (state.status !== "active" || state.activeStartedAt === undefined) return state;
|
|
321
|
+
function commonGoalState(state: GoalState): GoalStateCommon {
|
|
267
322
|
return {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
323
|
+
version: state.version,
|
|
324
|
+
revision: state.revision,
|
|
325
|
+
objective: state.objective,
|
|
326
|
+
createdAt: state.createdAt,
|
|
327
|
+
updatedAt: state.updatedAt,
|
|
328
|
+
activeMilliseconds: state.activeMilliseconds,
|
|
329
|
+
turns: state.turns,
|
|
330
|
+
blockedAuditStartTurn: state.blockedAuditStartTurn,
|
|
331
|
+
baselineTokens: state.baselineTokens,
|
|
332
|
+
...(state.verification === undefined ? {} : { verification: state.verification }),
|
|
271
333
|
};
|
|
272
334
|
}
|
|
273
335
|
|
|
336
|
+
function stopGoalClock(state: GoalState, now: number): GoalStateCommon {
|
|
337
|
+
const common = commonGoalState(state);
|
|
338
|
+
return state.status === "active"
|
|
339
|
+
? { ...common, activeMilliseconds: common.activeMilliseconds + Math.max(0, now - state.activeStartedAt) }
|
|
340
|
+
: common;
|
|
341
|
+
}
|
|
342
|
+
|
|
274
343
|
function sumGoalTokens(ctx: ExtensionContext): number {
|
|
275
344
|
let total = 0;
|
|
276
345
|
for (const entry of goalBranchEntries(ctx)) {
|
|
@@ -303,6 +372,7 @@ function persistGoalState(
|
|
|
303
372
|
state: GoalState | undefined,
|
|
304
373
|
): void {
|
|
305
374
|
const data: GoalEntryData = { version: GOAL_VERSION, event, state: state ?? null };
|
|
375
|
+
runtime.automaticCompaction = undefined;
|
|
306
376
|
pi.appendEntry(GOAL_ENTRY_TYPE, data);
|
|
307
377
|
runtime.state = state;
|
|
308
378
|
syncGoalUpdateTool(pi, runtime);
|
|
@@ -322,17 +392,40 @@ function transitionGoal(
|
|
|
322
392
|
if (!current) throw new Error("No goal is set");
|
|
323
393
|
const now = Date.now();
|
|
324
394
|
const stopped = stopGoalClock(current, now);
|
|
325
|
-
const
|
|
395
|
+
const common: GoalStateCommon = {
|
|
326
396
|
...stopped,
|
|
327
397
|
revision: stopped.revision + 1,
|
|
328
|
-
status,
|
|
329
398
|
updatedAt: now,
|
|
330
|
-
activeStartedAt: status === "active" ? now : undefined,
|
|
331
399
|
blockedAuditStartTurn: options.resetBlockedAudit ? stopped.turns : stopped.blockedAuditStartTurn,
|
|
332
|
-
blockerAudit: options.resetBlockedAudit ? undefined : options.blockerAudit ?? stopped.blockerAudit,
|
|
333
|
-
result,
|
|
334
|
-
resumeAfterManualCompaction: options.resumeAfterManualCompaction,
|
|
335
400
|
};
|
|
401
|
+
const blockerAudit = options.resetBlockedAudit ? undefined : options.blockerAudit ?? current.blockerAudit;
|
|
402
|
+
let next: GoalState;
|
|
403
|
+
switch (status) {
|
|
404
|
+
case "active":
|
|
405
|
+
next = { ...common, status, activeStartedAt: now, ...(blockerAudit === undefined ? {} : { blockerAudit }) };
|
|
406
|
+
break;
|
|
407
|
+
case "paused":
|
|
408
|
+
next = {
|
|
409
|
+
...common,
|
|
410
|
+
status,
|
|
411
|
+
...(result === undefined ? {} : { result }),
|
|
412
|
+
...(blockerAudit === undefined ? {} : { blockerAudit }),
|
|
413
|
+
...(options.resumeAfterManualCompaction === undefined ? {} : { resumeAfterManualCompaction: true }),
|
|
414
|
+
};
|
|
415
|
+
break;
|
|
416
|
+
case "blocked":
|
|
417
|
+
if (result === undefined) throw new Error("A blocked goal requires a result");
|
|
418
|
+
next = { ...common, status, result, ...(blockerAudit === undefined ? {} : { blockerAudit }) };
|
|
419
|
+
break;
|
|
420
|
+
case "complete":
|
|
421
|
+
if (result === undefined) throw new Error("A complete goal requires a result");
|
|
422
|
+
next = { ...common, status, result };
|
|
423
|
+
break;
|
|
424
|
+
default: {
|
|
425
|
+
const exhaustive: never = status;
|
|
426
|
+
return exhaustive;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
336
429
|
persistGoalState(pi, runtime, event, next);
|
|
337
430
|
if (status !== "active") {
|
|
338
431
|
runtime.continuationScheduled = false;
|
|
@@ -399,11 +492,12 @@ export function pauseGoalAfterFailure(
|
|
|
399
492
|
try {
|
|
400
493
|
transitionGoal(pi, runtime, "error", "paused", safeReason);
|
|
401
494
|
} catch {
|
|
402
|
-
|
|
403
|
-
|
|
495
|
+
const current = runtime.state;
|
|
496
|
+
runtime.state = current ? {
|
|
497
|
+
...stopGoalClock(current, Date.now()),
|
|
404
498
|
status: "paused",
|
|
405
499
|
result: safeReason,
|
|
406
|
-
|
|
500
|
+
...(current.blockerAudit === undefined ? {} : { blockerAudit: current.blockerAudit }),
|
|
407
501
|
} : undefined;
|
|
408
502
|
syncGoalUpdateTool(pi, runtime);
|
|
409
503
|
runtime.persistenceRetryNeeded = true;
|
|
@@ -427,11 +521,13 @@ function pauseGoalForPossibleManualCompaction(
|
|
|
427
521
|
resumeAfterManualCompaction: true,
|
|
428
522
|
});
|
|
429
523
|
} catch {
|
|
430
|
-
|
|
431
|
-
|
|
524
|
+
const current = runtime.state;
|
|
525
|
+
runtime.state = current ? {
|
|
526
|
+
...stopGoalClock(current, Date.now()),
|
|
432
527
|
status: "paused",
|
|
433
528
|
result: safeReason,
|
|
434
529
|
resumeAfterManualCompaction: true,
|
|
530
|
+
...(current.blockerAudit === undefined ? {} : { blockerAudit: current.blockerAudit }),
|
|
435
531
|
} : undefined;
|
|
436
532
|
syncGoalUpdateTool(pi, runtime);
|
|
437
533
|
runtime.persistenceRetryNeeded = true;
|
|
@@ -472,7 +568,7 @@ function beginGoalTurn(
|
|
|
472
568
|
pi: ExtensionAPI,
|
|
473
569
|
runtime: GoalRuntime,
|
|
474
570
|
ctx: ExtensionContext,
|
|
475
|
-
current: GoalState,
|
|
571
|
+
current: Extract<GoalState, { status: "active" }>,
|
|
476
572
|
): GoalState | undefined {
|
|
477
573
|
const now = Date.now();
|
|
478
574
|
const next: GoalState = {
|
|
@@ -480,8 +576,6 @@ function beginGoalTurn(
|
|
|
480
576
|
revision: current.revision + 1,
|
|
481
577
|
turns: current.turns + 1,
|
|
482
578
|
updatedAt: now,
|
|
483
|
-
activeStartedAt: current.activeStartedAt ?? now,
|
|
484
|
-
resumeAfterManualCompaction: undefined,
|
|
485
579
|
};
|
|
486
580
|
try {
|
|
487
581
|
persistGoalState(pi, runtime, "turn", next);
|
|
@@ -530,38 +624,81 @@ function scheduleGoalContinuation(
|
|
|
530
624
|
}
|
|
531
625
|
}
|
|
532
626
|
|
|
533
|
-
|
|
627
|
+
/** Resumes the revision paused by automatic compaction after both host callbacks settle. */
|
|
628
|
+
function finalizeAutomaticCompaction(
|
|
534
629
|
pi: ExtensionAPI,
|
|
535
630
|
runtime: GoalRuntime,
|
|
536
631
|
initState: InitRuntime,
|
|
537
632
|
ctx: ExtensionContext,
|
|
538
633
|
): void {
|
|
539
|
-
|
|
540
|
-
if (
|
|
541
|
-
|
|
634
|
+
const recovery = runtime.automaticCompaction;
|
|
635
|
+
if (!recovery?.compactionSucceeded || !recovery.turnSettled) return;
|
|
636
|
+
runtime.automaticCompaction = undefined;
|
|
637
|
+
if (runtime.state?.status !== "paused"
|
|
638
|
+
|| runtime.state.revision !== recovery.pausedRevision
|
|
639
|
+
|| initState.active) return;
|
|
640
|
+
try {
|
|
641
|
+
transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
|
|
642
|
+
} catch (error) {
|
|
643
|
+
runtime.persistenceRetryNeeded = true;
|
|
644
|
+
reportError(ctx, "Automatic compaction succeeded, but the goal could not be resumed", error);
|
|
542
645
|
return;
|
|
543
646
|
}
|
|
544
|
-
runtime.
|
|
647
|
+
runtime.continuationScheduled = false;
|
|
545
648
|
setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
|
|
546
649
|
}
|
|
547
650
|
|
|
651
|
+
/** Records Pi's successful compaction callback and attempts guarded recovery. */
|
|
652
|
+
function completeAutomaticCompaction(
|
|
653
|
+
pi: ExtensionAPI,
|
|
654
|
+
runtime: GoalRuntime,
|
|
655
|
+
initState: InitRuntime,
|
|
656
|
+
ctx: ExtensionContext,
|
|
657
|
+
): void {
|
|
658
|
+
if (!runtime.automaticCompaction) return;
|
|
659
|
+
runtime.automaticCompaction.compactionSucceeded = true;
|
|
660
|
+
finalizeAutomaticCompaction(pi, runtime, initState, ctx);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** Consumes automatic recovery and records its failure on the eligible paused goal. */
|
|
664
|
+
function stopAutomaticCompactionRecovery(
|
|
665
|
+
pi: ExtensionAPI,
|
|
666
|
+
runtime: GoalRuntime,
|
|
667
|
+
ctx: ExtensionContext,
|
|
668
|
+
reason: string,
|
|
669
|
+
): void {
|
|
670
|
+
const recovery = runtime.automaticCompaction;
|
|
671
|
+
runtime.automaticCompaction = undefined;
|
|
672
|
+
const safeReason = safeTerminalText(reason);
|
|
673
|
+
if (runtime.state?.status !== "paused" || runtime.state.revision !== recovery?.pausedRevision) return;
|
|
674
|
+
try {
|
|
675
|
+
transitionGoal(pi, runtime, "error", "paused", safeReason);
|
|
676
|
+
} catch {
|
|
677
|
+
runtime.state = { ...runtime.state, result: safeReason };
|
|
678
|
+
runtime.persistenceRetryNeeded = true;
|
|
679
|
+
runtime.requestRender?.();
|
|
680
|
+
}
|
|
681
|
+
ctx.ui.notify(
|
|
682
|
+
`Goal paused: ${safeReason}\nAutomatic continuation is stopped. Run /goal resume after resolving the compaction problem.`,
|
|
683
|
+
"error",
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Leaves the goal paused when Pi rejects automatic compaction. */
|
|
548
688
|
function failAutomaticCompaction(
|
|
549
689
|
pi: ExtensionAPI,
|
|
550
690
|
runtime: GoalRuntime,
|
|
551
691
|
ctx: ExtensionContext,
|
|
552
692
|
error: unknown,
|
|
553
693
|
): void {
|
|
554
|
-
if (runtime.automaticCompaction === undefined) return;
|
|
555
|
-
runtime.automaticCompaction = undefined;
|
|
556
|
-
if (runtime.state?.status !== "active") return;
|
|
557
694
|
const reason = error instanceof Error ? error.message : String(error);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
);
|
|
695
|
+
if (!runtime.automaticCompaction) {
|
|
696
|
+
if (runtime.persistenceRetryNeeded) {
|
|
697
|
+
ctx.ui.notify(`Automatic compaction did not start: ${safeTerminalText(reason)}`, "error");
|
|
698
|
+
}
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
stopAutomaticCompactionRecovery(pi, runtime, ctx, `automatic compaction failed: ${reason}`);
|
|
565
702
|
}
|
|
566
703
|
|
|
567
704
|
function goalInstructions(state: GoalState, heading: string): string {
|
|
@@ -732,7 +869,11 @@ export function registerGoal(
|
|
|
732
869
|
const checkpoint: GoalState = {
|
|
733
870
|
...stopGoalClock(runtime.state, now),
|
|
734
871
|
revision: runtime.state.revision + 1,
|
|
872
|
+
status: "active",
|
|
735
873
|
updatedAt: now,
|
|
874
|
+
activeStartedAt: now,
|
|
875
|
+
...(runtime.state.result === undefined ? {} : { result: runtime.state.result }),
|
|
876
|
+
...(runtime.state.blockerAudit === undefined ? {} : { blockerAudit: runtime.state.blockerAudit }),
|
|
736
877
|
};
|
|
737
878
|
try {
|
|
738
879
|
persistGoalState(pi, runtime, "checkpoint", checkpoint);
|
|
@@ -846,16 +987,18 @@ export function registerGoal(
|
|
|
846
987
|
return;
|
|
847
988
|
}
|
|
848
989
|
if (runtime.state.status === "paused") {
|
|
849
|
-
if (!runtime.persistenceRetryNeeded
|
|
990
|
+
if (!runtime.persistenceRetryNeeded
|
|
991
|
+
&& runtime.state.resumeAfterManualCompaction !== true
|
|
992
|
+
&& runtime.automaticCompaction === undefined) {
|
|
850
993
|
ctx.ui.notify("Goal is already paused", "info");
|
|
851
994
|
return;
|
|
852
995
|
}
|
|
853
996
|
const now = Date.now();
|
|
997
|
+
const { resumeAfterManualCompaction: _resume, ...paused } = runtime.state;
|
|
854
998
|
const checkpoint: GoalState = {
|
|
855
|
-
...
|
|
856
|
-
revision:
|
|
999
|
+
...paused,
|
|
1000
|
+
revision: paused.revision + 1,
|
|
857
1001
|
updatedAt: now,
|
|
858
|
-
resumeAfterManualCompaction: undefined,
|
|
859
1002
|
};
|
|
860
1003
|
try {
|
|
861
1004
|
persistGoalState(pi, runtime, "pause", checkpoint);
|
|
@@ -972,18 +1115,17 @@ export function registerGoal(
|
|
|
972
1115
|
}
|
|
973
1116
|
const now = Date.now();
|
|
974
1117
|
const current = stopGoalClock(runtime.state, now);
|
|
1118
|
+
const { verification: _previousVerification, ...currentWithoutVerification } = current;
|
|
1119
|
+
const verification = inferGoalVerification(objective);
|
|
975
1120
|
const next: GoalState = {
|
|
976
|
-
...
|
|
1121
|
+
...currentWithoutVerification,
|
|
977
1122
|
revision: current.revision + 1,
|
|
978
1123
|
objective,
|
|
979
1124
|
status: "active",
|
|
980
1125
|
updatedAt: now,
|
|
981
1126
|
activeStartedAt: now,
|
|
982
1127
|
blockedAuditStartTurn: current.turns,
|
|
983
|
-
|
|
984
|
-
verification: inferGoalVerification(objective),
|
|
985
|
-
result: undefined,
|
|
986
|
-
resumeAfterManualCompaction: undefined,
|
|
1128
|
+
...(verification === undefined ? {} : { verification }),
|
|
987
1129
|
};
|
|
988
1130
|
try {
|
|
989
1131
|
persistGoalState(pi, runtime, "edit", next);
|
|
@@ -1113,6 +1255,32 @@ export function registerGoalSettlement(
|
|
|
1113
1255
|
runtime.goalTurnInFlight = false;
|
|
1114
1256
|
runtime.agentEndObserved = false;
|
|
1115
1257
|
runtime.continuationScheduled = false;
|
|
1258
|
+
|
|
1259
|
+
if (runtime.automaticCompaction) {
|
|
1260
|
+
const stopReason = runtime.lastStopReason;
|
|
1261
|
+
const error = safeTerminalText(runtime.lastError ?? "");
|
|
1262
|
+
runtime.lastStopReason = undefined;
|
|
1263
|
+
runtime.lastError = undefined;
|
|
1264
|
+
const expectedInterruption = stopReason === "aborted"
|
|
1265
|
+
|| stopReason === "error" && error === "This operation was aborted";
|
|
1266
|
+
if (!wasGoalTurn || !agentEndObserved) {
|
|
1267
|
+
stopAutomaticCompactionRecovery(pi, runtime, ctx, "the goal turn ended without an agent result");
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
if ((stopReason === "error" || stopReason === "aborted") && !expectedInterruption) {
|
|
1271
|
+
stopAutomaticCompactionRecovery(
|
|
1272
|
+
pi,
|
|
1273
|
+
runtime,
|
|
1274
|
+
ctx,
|
|
1275
|
+
error || "the agent turn failed",
|
|
1276
|
+
);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
runtime.automaticCompaction.turnSettled = true;
|
|
1280
|
+
finalizeAutomaticCompaction(pi, runtime, initState, ctx);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1116
1284
|
if (!wasGoalTurn || runtime.state?.status !== "active" || initState.active) {
|
|
1117
1285
|
if (continuationWasScheduled && runtime.state?.status === "active" && !initState.active) {
|
|
1118
1286
|
pauseGoalAfterFailure(pi, runtime, ctx, "the goal continuation ended before an agent turn started");
|
|
@@ -1122,7 +1290,6 @@ export function registerGoalSettlement(
|
|
|
1122
1290
|
return;
|
|
1123
1291
|
}
|
|
1124
1292
|
if (!agentEndObserved) {
|
|
1125
|
-
if (runtime.automaticCompaction !== undefined) return;
|
|
1126
1293
|
pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn ended without an agent result");
|
|
1127
1294
|
return;
|
|
1128
1295
|
}
|
|
@@ -1130,7 +1297,6 @@ export function registerGoalSettlement(
|
|
|
1130
1297
|
const reason = runtime.lastError || "the agent turn was aborted";
|
|
1131
1298
|
runtime.lastStopReason = undefined;
|
|
1132
1299
|
runtime.lastError = undefined;
|
|
1133
|
-
if (runtime.automaticCompaction !== undefined) return;
|
|
1134
1300
|
pauseGoalForPossibleManualCompaction(pi, runtime, ctx, reason);
|
|
1135
1301
|
return;
|
|
1136
1302
|
}
|
|
@@ -1152,13 +1318,40 @@ export function registerGoalSettlement(
|
|
|
1152
1318
|
recoverGoalAfterManualCompaction(pi, runtime, initState, ctx);
|
|
1153
1319
|
});
|
|
1154
1320
|
|
|
1321
|
+
const resetAutomaticRecovery = (): void => { runtime.automaticCompaction = undefined; };
|
|
1322
|
+
pi.on("session_before_switch", resetAutomaticRecovery);
|
|
1323
|
+
pi.on("session_before_fork", resetAutomaticRecovery);
|
|
1324
|
+
|
|
1155
1325
|
return {
|
|
1156
1326
|
isActive: (ctx: ExtensionContext): boolean => isGoalModeSupported(ctx)
|
|
1157
1327
|
&& isSavedSession(ctx)
|
|
1158
1328
|
&& runtime.state?.status === "active"
|
|
1159
1329
|
&& !initState.active,
|
|
1160
1330
|
onRequested: (): void => {
|
|
1161
|
-
if (runtime.state?.status
|
|
1331
|
+
if (runtime.state?.status !== "active") return;
|
|
1332
|
+
try {
|
|
1333
|
+
const paused = transitionGoal(pi, runtime, "pause", "paused");
|
|
1334
|
+
runtime.automaticCompaction = {
|
|
1335
|
+
pausedRevision: paused.revision,
|
|
1336
|
+
compactionSucceeded: false,
|
|
1337
|
+
turnSettled: false,
|
|
1338
|
+
};
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
const current = runtime.state;
|
|
1341
|
+
const reason = safeTerminalText(`automatic compaction pause could not be saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
1342
|
+
runtime.state = current ? {
|
|
1343
|
+
...stopGoalClock(current, Date.now()),
|
|
1344
|
+
status: "paused",
|
|
1345
|
+
result: reason,
|
|
1346
|
+
...(current.blockerAudit === undefined ? {} : { blockerAudit: current.blockerAudit }),
|
|
1347
|
+
} : undefined;
|
|
1348
|
+
syncGoalUpdateTool(pi, runtime);
|
|
1349
|
+
runtime.persistenceRetryNeeded = true;
|
|
1350
|
+
runtime.continuationScheduled = false;
|
|
1351
|
+
runtime.automaticCompaction = undefined;
|
|
1352
|
+
runtime.requestRender?.();
|
|
1353
|
+
throw error;
|
|
1354
|
+
}
|
|
1162
1355
|
},
|
|
1163
1356
|
onCompleted: (ctx: ExtensionContext): void => completeAutomaticCompaction(pi, runtime, initState, ctx),
|
|
1164
1357
|
onFailed: (ctx: ExtensionContext, error: unknown): void => failAutomaticCompaction(pi, runtime, ctx, error),
|