killeros 2.0.18 → 2.0.20
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 +24 -0
- package/Killeros.ts +3 -1
- package/README.md +5 -2
- package/killeros/activity.ts +4 -0
- package/killeros/auto-compaction.ts +40 -10
- package/killeros/codex-fast-state.ts +5 -7
- package/killeros/commands.ts +5 -3
- package/killeros/errors.ts +8 -0
- package/killeros/footer.ts +3 -3
- package/killeros/goals.ts +326 -116
- package/killeros/handoff.ts +40 -10
- package/killeros/hooks.ts +121 -48
- 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 +3 -4
- package/killeros/question.ts +15 -8
- package/killeros/runtime.ts +42 -7
- package/killeros/settings.ts +8 -5
- package/killeros/shell-ui.ts +14 -6
- package/package.json +6 -3
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,96 @@ function scheduleGoalContinuation(
|
|
|
530
624
|
}
|
|
531
625
|
}
|
|
532
626
|
|
|
533
|
-
|
|
627
|
+
/** Resumes the revision paused by automatic compaction once both host callbacks settle and Pi reports an outcome. */
|
|
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 || recovery.outcome === "pending" || !recovery.turnSettled) return;
|
|
636
|
+
const skipped = recovery.outcome === "skipped";
|
|
637
|
+
runtime.automaticCompaction = undefined;
|
|
638
|
+
if (runtime.state?.status !== "paused"
|
|
639
|
+
|| runtime.state.revision !== recovery.pausedRevision
|
|
640
|
+
|| initState.active) return;
|
|
641
|
+
try {
|
|
642
|
+
transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
|
|
643
|
+
} catch (error) {
|
|
644
|
+
runtime.persistenceRetryNeeded = true;
|
|
645
|
+
reportError(ctx, skipped
|
|
646
|
+
? "Automatic compaction was skipped, but the goal could not be resumed"
|
|
647
|
+
: "Automatic compaction succeeded, but the goal could not be resumed", error);
|
|
542
648
|
return;
|
|
543
649
|
}
|
|
544
|
-
runtime.
|
|
650
|
+
runtime.continuationScheduled = false;
|
|
545
651
|
setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
|
|
546
652
|
}
|
|
547
653
|
|
|
654
|
+
/** Records Pi's successful compaction callback and attempts guarded recovery. */
|
|
655
|
+
function completeAutomaticCompaction(
|
|
656
|
+
pi: ExtensionAPI,
|
|
657
|
+
runtime: GoalRuntime,
|
|
658
|
+
initState: InitRuntime,
|
|
659
|
+
ctx: ExtensionContext,
|
|
660
|
+
): void {
|
|
661
|
+
if (!runtime.automaticCompaction) return;
|
|
662
|
+
runtime.automaticCompaction.outcome = "completed";
|
|
663
|
+
finalizeAutomaticCompaction(pi, runtime, initState, ctx);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** Records Pi's expected session-too-small rejection and resumes without claiming compaction succeeded. */
|
|
667
|
+
function skipAutomaticCompaction(
|
|
668
|
+
pi: ExtensionAPI,
|
|
669
|
+
runtime: GoalRuntime,
|
|
670
|
+
initState: InitRuntime,
|
|
671
|
+
ctx: ExtensionContext,
|
|
672
|
+
): void {
|
|
673
|
+
if (!runtime.automaticCompaction) return;
|
|
674
|
+
runtime.automaticCompaction.outcome = "skipped";
|
|
675
|
+
finalizeAutomaticCompaction(pi, runtime, initState, ctx);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** Consumes automatic recovery and records its failure on the eligible paused goal. */
|
|
679
|
+
function stopAutomaticCompactionRecovery(
|
|
680
|
+
pi: ExtensionAPI,
|
|
681
|
+
runtime: GoalRuntime,
|
|
682
|
+
ctx: ExtensionContext,
|
|
683
|
+
reason: string,
|
|
684
|
+
): void {
|
|
685
|
+
const recovery = runtime.automaticCompaction;
|
|
686
|
+
runtime.automaticCompaction = undefined;
|
|
687
|
+
const safeReason = safeTerminalText(reason);
|
|
688
|
+
if (runtime.state?.status !== "paused" || runtime.state.revision !== recovery?.pausedRevision) return;
|
|
689
|
+
try {
|
|
690
|
+
transitionGoal(pi, runtime, "error", "paused", safeReason);
|
|
691
|
+
} catch {
|
|
692
|
+
runtime.state = { ...runtime.state, result: safeReason };
|
|
693
|
+
runtime.persistenceRetryNeeded = true;
|
|
694
|
+
runtime.requestRender?.();
|
|
695
|
+
}
|
|
696
|
+
ctx.ui.notify(
|
|
697
|
+
`Goal paused: ${safeReason}\nAutomatic continuation is stopped. Run /goal resume after resolving the compaction problem.`,
|
|
698
|
+
"error",
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** Leaves the goal paused when Pi rejects automatic compaction. */
|
|
548
703
|
function failAutomaticCompaction(
|
|
549
704
|
pi: ExtensionAPI,
|
|
550
705
|
runtime: GoalRuntime,
|
|
551
706
|
ctx: ExtensionContext,
|
|
552
707
|
error: unknown,
|
|
553
708
|
): void {
|
|
554
|
-
if (runtime.automaticCompaction === undefined) return;
|
|
555
|
-
runtime.automaticCompaction = undefined;
|
|
556
|
-
if (runtime.state?.status !== "active") return;
|
|
557
709
|
const reason = error instanceof Error ? error.message : String(error);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
);
|
|
710
|
+
if (!runtime.automaticCompaction) {
|
|
711
|
+
if (runtime.persistenceRetryNeeded) {
|
|
712
|
+
ctx.ui.notify(`Automatic compaction did not start: ${safeTerminalText(reason)}`, "error");
|
|
713
|
+
}
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
stopAutomaticCompactionRecovery(pi, runtime, ctx, `automatic compaction failed: ${reason}`);
|
|
565
717
|
}
|
|
566
718
|
|
|
567
719
|
function goalInstructions(state: GoalState, heading: string): string {
|
|
@@ -732,7 +884,11 @@ export function registerGoal(
|
|
|
732
884
|
const checkpoint: GoalState = {
|
|
733
885
|
...stopGoalClock(runtime.state, now),
|
|
734
886
|
revision: runtime.state.revision + 1,
|
|
887
|
+
status: "active",
|
|
735
888
|
updatedAt: now,
|
|
889
|
+
activeStartedAt: now,
|
|
890
|
+
...(runtime.state.result === undefined ? {} : { result: runtime.state.result }),
|
|
891
|
+
...(runtime.state.blockerAudit === undefined ? {} : { blockerAudit: runtime.state.blockerAudit }),
|
|
736
892
|
};
|
|
737
893
|
try {
|
|
738
894
|
persistGoalState(pi, runtime, "checkpoint", checkpoint);
|
|
@@ -846,16 +1002,18 @@ export function registerGoal(
|
|
|
846
1002
|
return;
|
|
847
1003
|
}
|
|
848
1004
|
if (runtime.state.status === "paused") {
|
|
849
|
-
if (!runtime.persistenceRetryNeeded
|
|
1005
|
+
if (!runtime.persistenceRetryNeeded
|
|
1006
|
+
&& runtime.state.resumeAfterManualCompaction !== true
|
|
1007
|
+
&& runtime.automaticCompaction === undefined) {
|
|
850
1008
|
ctx.ui.notify("Goal is already paused", "info");
|
|
851
1009
|
return;
|
|
852
1010
|
}
|
|
853
1011
|
const now = Date.now();
|
|
1012
|
+
const { resumeAfterManualCompaction: _resume, ...paused } = runtime.state;
|
|
854
1013
|
const checkpoint: GoalState = {
|
|
855
|
-
...
|
|
856
|
-
revision:
|
|
1014
|
+
...paused,
|
|
1015
|
+
revision: paused.revision + 1,
|
|
857
1016
|
updatedAt: now,
|
|
858
|
-
resumeAfterManualCompaction: undefined,
|
|
859
1017
|
};
|
|
860
1018
|
try {
|
|
861
1019
|
persistGoalState(pi, runtime, "pause", checkpoint);
|
|
@@ -972,18 +1130,17 @@ export function registerGoal(
|
|
|
972
1130
|
}
|
|
973
1131
|
const now = Date.now();
|
|
974
1132
|
const current = stopGoalClock(runtime.state, now);
|
|
1133
|
+
const { verification: _previousVerification, ...currentWithoutVerification } = current;
|
|
1134
|
+
const verification = inferGoalVerification(objective);
|
|
975
1135
|
const next: GoalState = {
|
|
976
|
-
...
|
|
1136
|
+
...currentWithoutVerification,
|
|
977
1137
|
revision: current.revision + 1,
|
|
978
1138
|
objective,
|
|
979
1139
|
status: "active",
|
|
980
1140
|
updatedAt: now,
|
|
981
1141
|
activeStartedAt: now,
|
|
982
1142
|
blockedAuditStartTurn: current.turns,
|
|
983
|
-
|
|
984
|
-
verification: inferGoalVerification(objective),
|
|
985
|
-
result: undefined,
|
|
986
|
-
resumeAfterManualCompaction: undefined,
|
|
1143
|
+
...(verification === undefined ? {} : { verification }),
|
|
987
1144
|
};
|
|
988
1145
|
try {
|
|
989
1146
|
persistGoalState(pi, runtime, "edit", next);
|
|
@@ -1105,6 +1262,7 @@ export function registerGoalSettlement(
|
|
|
1105
1262
|
onRequested(): void;
|
|
1106
1263
|
onCompleted(ctx: ExtensionContext): void;
|
|
1107
1264
|
onFailed(ctx: ExtensionContext, error: unknown): void;
|
|
1265
|
+
onSkipped(ctx: ExtensionContext): void;
|
|
1108
1266
|
} {
|
|
1109
1267
|
pi.on("agent_settled", (_event, ctx) => {
|
|
1110
1268
|
const wasGoalTurn = runtime.goalTurnInFlight;
|
|
@@ -1113,6 +1271,32 @@ export function registerGoalSettlement(
|
|
|
1113
1271
|
runtime.goalTurnInFlight = false;
|
|
1114
1272
|
runtime.agentEndObserved = false;
|
|
1115
1273
|
runtime.continuationScheduled = false;
|
|
1274
|
+
|
|
1275
|
+
if (runtime.automaticCompaction) {
|
|
1276
|
+
const stopReason = runtime.lastStopReason;
|
|
1277
|
+
const error = safeTerminalText(runtime.lastError ?? "");
|
|
1278
|
+
runtime.lastStopReason = undefined;
|
|
1279
|
+
runtime.lastError = undefined;
|
|
1280
|
+
const expectedInterruption = stopReason === "aborted"
|
|
1281
|
+
|| stopReason === "error" && error === "This operation was aborted";
|
|
1282
|
+
if (!wasGoalTurn || !agentEndObserved) {
|
|
1283
|
+
stopAutomaticCompactionRecovery(pi, runtime, ctx, "the goal turn ended without an agent result");
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
if ((stopReason === "error" || stopReason === "aborted") && !expectedInterruption) {
|
|
1287
|
+
stopAutomaticCompactionRecovery(
|
|
1288
|
+
pi,
|
|
1289
|
+
runtime,
|
|
1290
|
+
ctx,
|
|
1291
|
+
error || "the agent turn failed",
|
|
1292
|
+
);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
runtime.automaticCompaction.turnSettled = true;
|
|
1296
|
+
finalizeAutomaticCompaction(pi, runtime, initState, ctx);
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1116
1300
|
if (!wasGoalTurn || runtime.state?.status !== "active" || initState.active) {
|
|
1117
1301
|
if (continuationWasScheduled && runtime.state?.status === "active" && !initState.active) {
|
|
1118
1302
|
pauseGoalAfterFailure(pi, runtime, ctx, "the goal continuation ended before an agent turn started");
|
|
@@ -1122,7 +1306,6 @@ export function registerGoalSettlement(
|
|
|
1122
1306
|
return;
|
|
1123
1307
|
}
|
|
1124
1308
|
if (!agentEndObserved) {
|
|
1125
|
-
if (runtime.automaticCompaction !== undefined) return;
|
|
1126
1309
|
pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn ended without an agent result");
|
|
1127
1310
|
return;
|
|
1128
1311
|
}
|
|
@@ -1130,7 +1313,6 @@ export function registerGoalSettlement(
|
|
|
1130
1313
|
const reason = runtime.lastError || "the agent turn was aborted";
|
|
1131
1314
|
runtime.lastStopReason = undefined;
|
|
1132
1315
|
runtime.lastError = undefined;
|
|
1133
|
-
if (runtime.automaticCompaction !== undefined) return;
|
|
1134
1316
|
pauseGoalForPossibleManualCompaction(pi, runtime, ctx, reason);
|
|
1135
1317
|
return;
|
|
1136
1318
|
}
|
|
@@ -1152,15 +1334,43 @@ export function registerGoalSettlement(
|
|
|
1152
1334
|
recoverGoalAfterManualCompaction(pi, runtime, initState, ctx);
|
|
1153
1335
|
});
|
|
1154
1336
|
|
|
1337
|
+
const resetAutomaticRecovery = (): void => { runtime.automaticCompaction = undefined; };
|
|
1338
|
+
pi.on("session_before_switch", resetAutomaticRecovery);
|
|
1339
|
+
pi.on("session_before_fork", resetAutomaticRecovery);
|
|
1340
|
+
|
|
1155
1341
|
return {
|
|
1156
1342
|
isActive: (ctx: ExtensionContext): boolean => isGoalModeSupported(ctx)
|
|
1157
1343
|
&& isSavedSession(ctx)
|
|
1158
1344
|
&& runtime.state?.status === "active"
|
|
1159
1345
|
&& !initState.active,
|
|
1160
1346
|
onRequested: (): void => {
|
|
1161
|
-
if (runtime.state?.status
|
|
1347
|
+
if (runtime.state?.status !== "active") return;
|
|
1348
|
+
try {
|
|
1349
|
+
const paused = transitionGoal(pi, runtime, "pause", "paused");
|
|
1350
|
+
runtime.automaticCompaction = {
|
|
1351
|
+
pausedRevision: paused.revision,
|
|
1352
|
+
outcome: "pending",
|
|
1353
|
+
turnSettled: false,
|
|
1354
|
+
};
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
const current = runtime.state;
|
|
1357
|
+
const reason = safeTerminalText(`automatic compaction pause could not be saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
1358
|
+
runtime.state = current ? {
|
|
1359
|
+
...stopGoalClock(current, Date.now()),
|
|
1360
|
+
status: "paused",
|
|
1361
|
+
result: reason,
|
|
1362
|
+
...(current.blockerAudit === undefined ? {} : { blockerAudit: current.blockerAudit }),
|
|
1363
|
+
} : undefined;
|
|
1364
|
+
syncGoalUpdateTool(pi, runtime);
|
|
1365
|
+
runtime.persistenceRetryNeeded = true;
|
|
1366
|
+
runtime.continuationScheduled = false;
|
|
1367
|
+
runtime.automaticCompaction = undefined;
|
|
1368
|
+
runtime.requestRender?.();
|
|
1369
|
+
throw error;
|
|
1370
|
+
}
|
|
1162
1371
|
},
|
|
1163
1372
|
onCompleted: (ctx: ExtensionContext): void => completeAutomaticCompaction(pi, runtime, initState, ctx),
|
|
1164
1373
|
onFailed: (ctx: ExtensionContext, error: unknown): void => failAutomaticCompaction(pi, runtime, ctx, error),
|
|
1374
|
+
onSkipped: (ctx: ExtensionContext): void => skipAutomaticCompaction(pi, runtime, initState, ctx),
|
|
1165
1375
|
};
|
|
1166
1376
|
}
|