killeros 2.0.17 → 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 +50 -0
- package/README.md +3 -3
- package/killeros/activity.ts +4 -0
- package/killeros/codex-fast-state.ts +16 -7
- package/killeros/commands.ts +26 -9
- package/killeros/display.ts +7 -4
- package/killeros/errors.ts +10 -1
- package/killeros/footer.ts +5 -3
- package/killeros/goals.ts +336 -140
- package/killeros/handoff.ts +4 -14
- package/killeros/hooks.ts +186 -58
- package/killeros/init-evidence.ts +4 -2
- package/killeros/init-target.ts +28 -20
- package/killeros/init.ts +16 -7
- package/killeros/notifications.ts +2 -1
- package/killeros/personal-instructions.ts +11 -12
- 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/killeros/variants.ts +3 -2
- package/package.json +7 -7
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 {
|
|
@@ -118,8 +122,9 @@ function captureGoalFileBaseline(filePath: string): GoalFileBaseline {
|
|
|
118
122
|
let artifact: ReturnType<typeof lstatSync>;
|
|
119
123
|
try {
|
|
120
124
|
artifact = lstatSync(filePath);
|
|
121
|
-
} catch {
|
|
122
|
-
return { exists: false };
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (hasErrorCode(error, "ENOENT")) return { exists: false };
|
|
127
|
+
throw error;
|
|
123
128
|
}
|
|
124
129
|
const baseline = { exists: true as const, size: artifact.size, mtimeMs: artifact.mtimeMs };
|
|
125
130
|
if (!artifact.isFile()) return baseline;
|
|
@@ -174,56 +179,98 @@ function verifyGoalDeliverable(verification: GoalFileVerification): void {
|
|
|
174
179
|
}
|
|
175
180
|
|
|
176
181
|
function isGoalBlockerAudit(value: unknown, turns: number, status: GoalStatus): value is GoalBlockerAudit {
|
|
177
|
-
if (!value
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
||
|
|
181
|
-
|| !Number.isInteger(
|
|
182
|
-
|
|
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
|
+
}
|
|
183
189
|
if (status === "complete") return false;
|
|
184
|
-
return status === "blocked" ?
|
|
190
|
+
return status === "blocked" ? value.streak === 3 : value.streak < 3;
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
function parseGoalState(value: unknown): GoalState | undefined {
|
|
188
|
-
if (!value
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
||
|
|
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)) {
|
|
208
228
|
return undefined;
|
|
209
229
|
}
|
|
210
|
-
|
|
230
|
+
|
|
231
|
+
const common: GoalStateCommon = {
|
|
211
232
|
version: GOAL_VERSION,
|
|
212
|
-
revision
|
|
213
|
-
objective:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
baselineTokens: candidate.baselineTokens,
|
|
222
|
-
result: candidate.result,
|
|
223
|
-
resumeAfterManualCompaction: candidate.resumeAfterManualCompaction,
|
|
224
|
-
blockerAudit: candidate.blockerAudit,
|
|
225
|
-
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 }),
|
|
226
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
|
+
}
|
|
227
274
|
}
|
|
228
275
|
|
|
229
276
|
function goalBranchEntries(ctx: ExtensionContext): ReturnType<ExtensionContext["sessionManager"]["getEntries"]> {
|
|
@@ -239,37 +286,60 @@ function restoreGoalState(ctx: ExtensionContext): RestoredGoalState {
|
|
|
239
286
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
240
287
|
const entry = entries[index];
|
|
241
288
|
if (entry?.type !== "custom" || entry.customType !== GOAL_ENTRY_TYPE) continue;
|
|
242
|
-
const data = entry.data
|
|
243
|
-
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) {
|
|
244
291
|
return { state: undefined };
|
|
245
292
|
}
|
|
246
|
-
|
|
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);
|
|
247
301
|
if (!restored) return { state: undefined };
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (
|
|
252
|
-
|
|
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 };
|
|
253
310
|
}
|
|
254
311
|
return { state: undefined };
|
|
255
312
|
}
|
|
256
313
|
|
|
257
314
|
export function goalElapsedMilliseconds(state: GoalState, now = Date.now()): number {
|
|
258
|
-
const activeInterval = state.status === "active"
|
|
315
|
+
const activeInterval = state.status === "active"
|
|
259
316
|
? Math.max(0, now - state.activeStartedAt)
|
|
260
317
|
: 0;
|
|
261
318
|
return state.activeMilliseconds + activeInterval;
|
|
262
319
|
}
|
|
263
320
|
|
|
264
|
-
function
|
|
265
|
-
if (state.status !== "active" || state.activeStartedAt === undefined) return state;
|
|
321
|
+
function commonGoalState(state: GoalState): GoalStateCommon {
|
|
266
322
|
return {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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 }),
|
|
270
333
|
};
|
|
271
334
|
}
|
|
272
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
|
+
|
|
273
343
|
function sumGoalTokens(ctx: ExtensionContext): number {
|
|
274
344
|
let total = 0;
|
|
275
345
|
for (const entry of goalBranchEntries(ctx)) {
|
|
@@ -302,6 +372,7 @@ function persistGoalState(
|
|
|
302
372
|
state: GoalState | undefined,
|
|
303
373
|
): void {
|
|
304
374
|
const data: GoalEntryData = { version: GOAL_VERSION, event, state: state ?? null };
|
|
375
|
+
runtime.automaticCompaction = undefined;
|
|
305
376
|
pi.appendEntry(GOAL_ENTRY_TYPE, data);
|
|
306
377
|
runtime.state = state;
|
|
307
378
|
syncGoalUpdateTool(pi, runtime);
|
|
@@ -321,17 +392,40 @@ function transitionGoal(
|
|
|
321
392
|
if (!current) throw new Error("No goal is set");
|
|
322
393
|
const now = Date.now();
|
|
323
394
|
const stopped = stopGoalClock(current, now);
|
|
324
|
-
const
|
|
395
|
+
const common: GoalStateCommon = {
|
|
325
396
|
...stopped,
|
|
326
397
|
revision: stopped.revision + 1,
|
|
327
|
-
status,
|
|
328
398
|
updatedAt: now,
|
|
329
|
-
activeStartedAt: status === "active" ? now : undefined,
|
|
330
399
|
blockedAuditStartTurn: options.resetBlockedAudit ? stopped.turns : stopped.blockedAuditStartTurn,
|
|
331
|
-
blockerAudit: options.resetBlockedAudit ? undefined : options.blockerAudit ?? stopped.blockerAudit,
|
|
332
|
-
result,
|
|
333
|
-
resumeAfterManualCompaction: options.resumeAfterManualCompaction,
|
|
334
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
|
+
}
|
|
335
429
|
persistGoalState(pi, runtime, event, next);
|
|
336
430
|
if (status !== "active") {
|
|
337
431
|
runtime.continuationScheduled = false;
|
|
@@ -382,7 +476,7 @@ function goalStatusSummary(state: GoalState, ctx: ExtensionContext): string {
|
|
|
382
476
|
state.objective,
|
|
383
477
|
];
|
|
384
478
|
if (state.result) lines.push(state.result);
|
|
385
|
-
return lines.join("\n");
|
|
479
|
+
return safeTerminalText(lines.join("\n"));
|
|
386
480
|
}
|
|
387
481
|
|
|
388
482
|
export function pauseGoalAfterFailure(
|
|
@@ -394,14 +488,16 @@ export function pauseGoalAfterFailure(
|
|
|
394
488
|
notify = true,
|
|
395
489
|
): void {
|
|
396
490
|
if (runtime.state?.status !== "active") return;
|
|
491
|
+
const safeReason = safeTerminalText(reason);
|
|
397
492
|
try {
|
|
398
|
-
transitionGoal(pi, runtime, "error", "paused",
|
|
493
|
+
transitionGoal(pi, runtime, "error", "paused", safeReason);
|
|
399
494
|
} catch {
|
|
400
|
-
|
|
401
|
-
|
|
495
|
+
const current = runtime.state;
|
|
496
|
+
runtime.state = current ? {
|
|
497
|
+
...stopGoalClock(current, Date.now()),
|
|
402
498
|
status: "paused",
|
|
403
|
-
result:
|
|
404
|
-
|
|
499
|
+
result: safeReason,
|
|
500
|
+
...(current.blockerAudit === undefined ? {} : { blockerAudit: current.blockerAudit }),
|
|
405
501
|
} : undefined;
|
|
406
502
|
syncGoalUpdateTool(pi, runtime);
|
|
407
503
|
runtime.persistenceRetryNeeded = true;
|
|
@@ -409,7 +505,7 @@ export function pauseGoalAfterFailure(
|
|
|
409
505
|
runtime.automaticCompaction = undefined;
|
|
410
506
|
runtime.requestRender?.();
|
|
411
507
|
}
|
|
412
|
-
if (notify) ctx.ui.notify(`Goal paused: ${
|
|
508
|
+
if (notify) ctx.ui.notify(`Goal paused: ${safeReason}\n${recoveryInstruction}`, "error");
|
|
413
509
|
}
|
|
414
510
|
|
|
415
511
|
function pauseGoalForPossibleManualCompaction(
|
|
@@ -419,16 +515,19 @@ function pauseGoalForPossibleManualCompaction(
|
|
|
419
515
|
reason: string,
|
|
420
516
|
): void {
|
|
421
517
|
if (runtime.state?.status !== "active") return;
|
|
518
|
+
const safeReason = safeTerminalText(reason);
|
|
422
519
|
try {
|
|
423
|
-
transitionGoal(pi, runtime, "error", "paused",
|
|
520
|
+
transitionGoal(pi, runtime, "error", "paused", safeReason, {
|
|
424
521
|
resumeAfterManualCompaction: true,
|
|
425
522
|
});
|
|
426
523
|
} catch {
|
|
427
|
-
|
|
428
|
-
|
|
524
|
+
const current = runtime.state;
|
|
525
|
+
runtime.state = current ? {
|
|
526
|
+
...stopGoalClock(current, Date.now()),
|
|
429
527
|
status: "paused",
|
|
430
|
-
result:
|
|
528
|
+
result: safeReason,
|
|
431
529
|
resumeAfterManualCompaction: true,
|
|
530
|
+
...(current.blockerAudit === undefined ? {} : { blockerAudit: current.blockerAudit }),
|
|
432
531
|
} : undefined;
|
|
433
532
|
syncGoalUpdateTool(pi, runtime);
|
|
434
533
|
runtime.persistenceRetryNeeded = true;
|
|
@@ -469,7 +568,7 @@ function beginGoalTurn(
|
|
|
469
568
|
pi: ExtensionAPI,
|
|
470
569
|
runtime: GoalRuntime,
|
|
471
570
|
ctx: ExtensionContext,
|
|
472
|
-
current: GoalState,
|
|
571
|
+
current: Extract<GoalState, { status: "active" }>,
|
|
473
572
|
): GoalState | undefined {
|
|
474
573
|
const now = Date.now();
|
|
475
574
|
const next: GoalState = {
|
|
@@ -477,8 +576,6 @@ function beginGoalTurn(
|
|
|
477
576
|
revision: current.revision + 1,
|
|
478
577
|
turns: current.turns + 1,
|
|
479
578
|
updatedAt: now,
|
|
480
|
-
activeStartedAt: current.activeStartedAt ?? now,
|
|
481
|
-
resumeAfterManualCompaction: undefined,
|
|
482
579
|
};
|
|
483
580
|
try {
|
|
484
581
|
persistGoalState(pi, runtime, "turn", next);
|
|
@@ -527,38 +624,81 @@ function scheduleGoalContinuation(
|
|
|
527
624
|
}
|
|
528
625
|
}
|
|
529
626
|
|
|
530
|
-
|
|
627
|
+
/** Resumes the revision paused by automatic compaction after both host callbacks settle. */
|
|
628
|
+
function finalizeAutomaticCompaction(
|
|
531
629
|
pi: ExtensionAPI,
|
|
532
630
|
runtime: GoalRuntime,
|
|
533
631
|
initState: InitRuntime,
|
|
534
632
|
ctx: ExtensionContext,
|
|
535
633
|
): void {
|
|
536
|
-
|
|
537
|
-
if (
|
|
538
|
-
|
|
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);
|
|
539
645
|
return;
|
|
540
646
|
}
|
|
541
|
-
runtime.
|
|
647
|
+
runtime.continuationScheduled = false;
|
|
542
648
|
setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
|
|
543
649
|
}
|
|
544
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. */
|
|
545
688
|
function failAutomaticCompaction(
|
|
546
689
|
pi: ExtensionAPI,
|
|
547
690
|
runtime: GoalRuntime,
|
|
548
691
|
ctx: ExtensionContext,
|
|
549
692
|
error: unknown,
|
|
550
693
|
): void {
|
|
551
|
-
if (runtime.automaticCompaction === undefined) return;
|
|
552
|
-
runtime.automaticCompaction = undefined;
|
|
553
|
-
if (runtime.state?.status !== "active") return;
|
|
554
694
|
const reason = error instanceof Error ? error.message : String(error);
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
);
|
|
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}`);
|
|
562
702
|
}
|
|
563
703
|
|
|
564
704
|
function goalInstructions(state: GoalState, heading: string): string {
|
|
@@ -587,7 +727,7 @@ function goalContinuationMessage(state: GoalState, ctx: ExtensionContext): strin
|
|
|
587
727
|
if (ctx.isProjectTrusted()) {
|
|
588
728
|
const personal = resolvePersonalInstructions(ctx.cwd);
|
|
589
729
|
if (personal) {
|
|
590
|
-
sections.push(
|
|
730
|
+
sections.push(personal);
|
|
591
731
|
}
|
|
592
732
|
}
|
|
593
733
|
return sections.join("\n\n");
|
|
@@ -729,7 +869,11 @@ export function registerGoal(
|
|
|
729
869
|
const checkpoint: GoalState = {
|
|
730
870
|
...stopGoalClock(runtime.state, now),
|
|
731
871
|
revision: runtime.state.revision + 1,
|
|
872
|
+
status: "active",
|
|
732
873
|
updatedAt: now,
|
|
874
|
+
activeStartedAt: now,
|
|
875
|
+
...(runtime.state.result === undefined ? {} : { result: runtime.state.result }),
|
|
876
|
+
...(runtime.state.blockerAudit === undefined ? {} : { blockerAudit: runtime.state.blockerAudit }),
|
|
733
877
|
};
|
|
734
878
|
try {
|
|
735
879
|
persistGoalState(pi, runtime, "checkpoint", checkpoint);
|
|
@@ -793,7 +937,7 @@ export function registerGoal(
|
|
|
793
937
|
const selected = await ctx.ui.select(goalStatusSummary(runtime.state, ctx), actions.map((action) => action.label));
|
|
794
938
|
const action = actions.find((candidate) => candidate.label === selected);
|
|
795
939
|
if (!action) return;
|
|
796
|
-
if (action.control === "clear" && !await ctx.ui.confirm("Clear goal?", runtime.state.objective)) return;
|
|
940
|
+
if (action.control === "clear" && !await ctx.ui.confirm("Clear goal?", safeTerminalText(runtime.state.objective))) return;
|
|
797
941
|
await handleGoalCommand(action.control, ctx);
|
|
798
942
|
return;
|
|
799
943
|
}
|
|
@@ -843,16 +987,18 @@ export function registerGoal(
|
|
|
843
987
|
return;
|
|
844
988
|
}
|
|
845
989
|
if (runtime.state.status === "paused") {
|
|
846
|
-
if (!runtime.persistenceRetryNeeded
|
|
990
|
+
if (!runtime.persistenceRetryNeeded
|
|
991
|
+
&& runtime.state.resumeAfterManualCompaction !== true
|
|
992
|
+
&& runtime.automaticCompaction === undefined) {
|
|
847
993
|
ctx.ui.notify("Goal is already paused", "info");
|
|
848
994
|
return;
|
|
849
995
|
}
|
|
850
996
|
const now = Date.now();
|
|
997
|
+
const { resumeAfterManualCompaction: _resume, ...paused } = runtime.state;
|
|
851
998
|
const checkpoint: GoalState = {
|
|
852
|
-
...
|
|
853
|
-
revision:
|
|
999
|
+
...paused,
|
|
1000
|
+
revision: paused.revision + 1,
|
|
854
1001
|
updatedAt: now,
|
|
855
|
-
resumeAfterManualCompaction: undefined,
|
|
856
1002
|
};
|
|
857
1003
|
try {
|
|
858
1004
|
persistGoalState(pi, runtime, "pause", checkpoint);
|
|
@@ -878,7 +1024,7 @@ export function registerGoal(
|
|
|
878
1024
|
transitionGoal(pi, runtime, "pause", "paused");
|
|
879
1025
|
saved = true;
|
|
880
1026
|
} catch (error) {
|
|
881
|
-
failureReason = `the requested pause could not be saved: ${error instanceof Error ? error.message : String(error)}
|
|
1027
|
+
failureReason = safeTerminalText(`the requested pause could not be saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
882
1028
|
pauseGoalAfterFailure(
|
|
883
1029
|
pi,
|
|
884
1030
|
runtime,
|
|
@@ -969,18 +1115,17 @@ export function registerGoal(
|
|
|
969
1115
|
}
|
|
970
1116
|
const now = Date.now();
|
|
971
1117
|
const current = stopGoalClock(runtime.state, now);
|
|
1118
|
+
const { verification: _previousVerification, ...currentWithoutVerification } = current;
|
|
1119
|
+
const verification = inferGoalVerification(objective);
|
|
972
1120
|
const next: GoalState = {
|
|
973
|
-
...
|
|
1121
|
+
...currentWithoutVerification,
|
|
974
1122
|
revision: current.revision + 1,
|
|
975
1123
|
objective,
|
|
976
1124
|
status: "active",
|
|
977
1125
|
updatedAt: now,
|
|
978
1126
|
activeStartedAt: now,
|
|
979
1127
|
blockedAuditStartTurn: current.turns,
|
|
980
|
-
|
|
981
|
-
verification: inferGoalVerification(objective),
|
|
982
|
-
result: undefined,
|
|
983
|
-
resumeAfterManualCompaction: undefined,
|
|
1128
|
+
...(verification === undefined ? {} : { verification }),
|
|
984
1129
|
};
|
|
985
1130
|
try {
|
|
986
1131
|
persistGoalState(pi, runtime, "edit", next);
|
|
@@ -1038,21 +1183,21 @@ export function registerGoal(
|
|
|
1038
1183
|
return;
|
|
1039
1184
|
}
|
|
1040
1185
|
const now = Date.now();
|
|
1041
|
-
const state: GoalState = {
|
|
1042
|
-
version: GOAL_VERSION,
|
|
1043
|
-
revision: 1,
|
|
1044
|
-
objective,
|
|
1045
|
-
status: "active",
|
|
1046
|
-
createdAt: now,
|
|
1047
|
-
updatedAt: now,
|
|
1048
|
-
activeMilliseconds: 0,
|
|
1049
|
-
activeStartedAt: now,
|
|
1050
|
-
turns: 0,
|
|
1051
|
-
blockedAuditStartTurn: 0,
|
|
1052
|
-
baselineTokens: sumGoalTokens(ctx),
|
|
1053
|
-
verification: inferGoalVerification(objective),
|
|
1054
|
-
};
|
|
1055
1186
|
try {
|
|
1187
|
+
const state: GoalState = {
|
|
1188
|
+
version: GOAL_VERSION,
|
|
1189
|
+
revision: 1,
|
|
1190
|
+
objective,
|
|
1191
|
+
status: "active",
|
|
1192
|
+
createdAt: now,
|
|
1193
|
+
updatedAt: now,
|
|
1194
|
+
activeMilliseconds: 0,
|
|
1195
|
+
activeStartedAt: now,
|
|
1196
|
+
turns: 0,
|
|
1197
|
+
blockedAuditStartTurn: 0,
|
|
1198
|
+
baselineTokens: sumGoalTokens(ctx),
|
|
1199
|
+
verification: inferGoalVerification(objective),
|
|
1200
|
+
};
|
|
1056
1201
|
persistGoalState(pi, runtime, unfinished ? "replace" : "set", state);
|
|
1057
1202
|
if (scheduleGoalContinuation(pi, runtime, initState, ctx)) {
|
|
1058
1203
|
ctx.ui.notify("Goal active. KillerOS will continue until completion, a repeated blocker, or pause.", "info");
|
|
@@ -1110,6 +1255,32 @@ export function registerGoalSettlement(
|
|
|
1110
1255
|
runtime.goalTurnInFlight = false;
|
|
1111
1256
|
runtime.agentEndObserved = false;
|
|
1112
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
|
+
|
|
1113
1284
|
if (!wasGoalTurn || runtime.state?.status !== "active" || initState.active) {
|
|
1114
1285
|
if (continuationWasScheduled && runtime.state?.status === "active" && !initState.active) {
|
|
1115
1286
|
pauseGoalAfterFailure(pi, runtime, ctx, "the goal continuation ended before an agent turn started");
|
|
@@ -1119,7 +1290,6 @@ export function registerGoalSettlement(
|
|
|
1119
1290
|
return;
|
|
1120
1291
|
}
|
|
1121
1292
|
if (!agentEndObserved) {
|
|
1122
|
-
if (runtime.automaticCompaction !== undefined) return;
|
|
1123
1293
|
pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn ended without an agent result");
|
|
1124
1294
|
return;
|
|
1125
1295
|
}
|
|
@@ -1127,7 +1297,6 @@ export function registerGoalSettlement(
|
|
|
1127
1297
|
const reason = runtime.lastError || "the agent turn was aborted";
|
|
1128
1298
|
runtime.lastStopReason = undefined;
|
|
1129
1299
|
runtime.lastError = undefined;
|
|
1130
|
-
if (runtime.automaticCompaction !== undefined) return;
|
|
1131
1300
|
pauseGoalForPossibleManualCompaction(pi, runtime, ctx, reason);
|
|
1132
1301
|
return;
|
|
1133
1302
|
}
|
|
@@ -1149,13 +1318,40 @@ export function registerGoalSettlement(
|
|
|
1149
1318
|
recoverGoalAfterManualCompaction(pi, runtime, initState, ctx);
|
|
1150
1319
|
});
|
|
1151
1320
|
|
|
1321
|
+
const resetAutomaticRecovery = (): void => { runtime.automaticCompaction = undefined; };
|
|
1322
|
+
pi.on("session_before_switch", resetAutomaticRecovery);
|
|
1323
|
+
pi.on("session_before_fork", resetAutomaticRecovery);
|
|
1324
|
+
|
|
1152
1325
|
return {
|
|
1153
1326
|
isActive: (ctx: ExtensionContext): boolean => isGoalModeSupported(ctx)
|
|
1154
1327
|
&& isSavedSession(ctx)
|
|
1155
1328
|
&& runtime.state?.status === "active"
|
|
1156
1329
|
&& !initState.active,
|
|
1157
1330
|
onRequested: (): void => {
|
|
1158
|
-
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
|
+
}
|
|
1159
1355
|
},
|
|
1160
1356
|
onCompleted: (ctx: ExtensionContext): void => completeAutomaticCompaction(pi, runtime, initState, ctx),
|
|
1161
1357
|
onFailed: (ctx: ExtensionContext, error: unknown): void => failAutomaticCompaction(pi, runtime, ctx, error),
|