killeros 2.0.19 → 2.0.21
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 +29 -0
- package/Killeros.ts +3 -1
- package/README.md +6 -3
- package/killeros/auto-compaction.ts +40 -10
- package/killeros/display.ts +8 -3
- package/killeros/footer.ts +120 -27
- package/killeros/goal-state.ts +407 -0
- package/killeros/goals.ts +43 -380
- package/killeros/handoff.ts +79 -26
- package/killeros/hooks.ts +1 -1
- package/killeros/init-evidence.ts +15 -3
- package/killeros/personal-instructions.ts +57 -6
- package/killeros/question-ui.ts +590 -0
- package/killeros/question.ts +12 -559
- package/killeros/runtime.ts +4 -1
- package/killeros/secret-detector.ts +19 -0
- package/killeros/shell-ui.ts +1 -2
- package/package.json +10 -7
package/killeros/goals.ts
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { closeSync, lstatSync, openSync, readSync } from "node:fs";
|
|
4
|
-
import path from "node:path";
|
|
5
2
|
import { type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
6
3
|
import { Text } from "@earendil-works/pi-tui";
|
|
7
4
|
import { Type } from "typebox";
|
|
8
5
|
import { BoundedText } from "./bounded-text.ts";
|
|
9
6
|
import { formatTime, formatTokens } from "./display.ts";
|
|
10
|
-
import {
|
|
7
|
+
import { beginGoalTurnState, checkpointActiveGoalState, checkpointPausedGoalState, createNewGoalState, editGoalState, goalElapsedMilliseconds, GOAL_VERSION, inferGoalVerification, parseGoalState, pauseGoalState, recordGoalBlockerAudit, transitionGoalState, validateGoalObjective, verifyGoalDeliverable, type GoalTransitionOptions } from "./goal-state.ts";
|
|
8
|
+
import { reportError } from "./errors.ts";
|
|
11
9
|
import { resolvePersonalInstructions } from "./personal-instructions.ts";
|
|
12
|
-
import type {
|
|
10
|
+
import type { GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
|
|
13
11
|
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
14
12
|
|
|
13
|
+
export { goalElapsedMilliseconds };
|
|
14
|
+
|
|
15
15
|
const GOAL_ENTRY_TYPE = "killeros-goal";
|
|
16
16
|
const GOAL_CONTINUATION_TYPE = "killeros-goal-continuation";
|
|
17
17
|
const GOAL_UPDATE_TOOL = "killeros_goal_update";
|
|
18
|
-
const GOAL_OBJECTIVE_LIMIT = 4_000;
|
|
19
|
-
const GOAL_VERSION = 1;
|
|
20
|
-
const FILE_HASH_CHUNK_SIZE = 64 * 1024;
|
|
21
18
|
|
|
22
19
|
type GoalEntryEvent = "set" | "replace" | "edit" | "turn" | "pause" | "resume" | "blocked" | "complete" | "error" | "clear" | "checkpoint" | "blocker-audit";
|
|
23
20
|
interface GoalEntryData {
|
|
@@ -26,10 +23,8 @@ interface GoalEntryData {
|
|
|
26
23
|
state: GoalState | null;
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
resumeAfterManualCompaction?: true;
|
|
32
|
-
blockerAudit?: GoalBlockerAudit;
|
|
26
|
+
function isUnknownRecord(value: unknown): value is Record<string, unknown> {
|
|
27
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
28
|
}
|
|
34
29
|
|
|
35
30
|
interface RestoredGoalState {
|
|
@@ -61,218 +56,6 @@ interface GoalUpdateDetails {
|
|
|
61
56
|
streak?: number;
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
function isGoalStatus(value: unknown): value is GoalStatus {
|
|
65
|
-
return value === "active" || value === "paused" || value === "blocked" || value === "complete";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function finiteNonNegative(value: unknown): value is number {
|
|
69
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function isUnknownRecord(value: unknown): value is Record<string, unknown> {
|
|
73
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function isGoalFileBaseline(value: unknown): value is GoalFileBaseline {
|
|
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));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function isGoalFileVerification(value: unknown): value is GoalFileVerification {
|
|
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);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function isAbsoluteFilePath(value: string): boolean {
|
|
99
|
-
if (!value || /^(?:https?|file):\/\//iu.test(value) || /[\\\/]$/u.test(value)) return false;
|
|
100
|
-
return path.isAbsolute(value) || path.win32.isAbsolute(value);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Hash a deliverable in bounded memory for baseline and completion checks. */
|
|
104
|
-
function hashFileContent(filePath: string): string {
|
|
105
|
-
const descriptor = openSync(filePath, "r");
|
|
106
|
-
try {
|
|
107
|
-
const hash = createHash("sha256");
|
|
108
|
-
const buffer = Buffer.allocUnsafe(FILE_HASH_CHUNK_SIZE);
|
|
109
|
-
let position = 0;
|
|
110
|
-
while (true) {
|
|
111
|
-
const bytesRead = readSync(descriptor, buffer, 0, buffer.length, position);
|
|
112
|
-
if (bytesRead === 0) return hash.digest("hex");
|
|
113
|
-
hash.update(buffer.subarray(0, bytesRead));
|
|
114
|
-
position += bytesRead;
|
|
115
|
-
}
|
|
116
|
-
} finally {
|
|
117
|
-
closeSync(descriptor);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function captureGoalFileBaseline(filePath: string): GoalFileBaseline {
|
|
122
|
-
let artifact: ReturnType<typeof lstatSync>;
|
|
123
|
-
try {
|
|
124
|
-
artifact = lstatSync(filePath);
|
|
125
|
-
} catch (error) {
|
|
126
|
-
if (hasErrorCode(error, "ENOENT")) return { exists: false };
|
|
127
|
-
throw error;
|
|
128
|
-
}
|
|
129
|
-
const baseline = { exists: true as const, size: artifact.size, mtimeMs: artifact.mtimeMs };
|
|
130
|
-
if (!artifact.isFile()) return baseline;
|
|
131
|
-
try {
|
|
132
|
-
return { ...baseline, contentHash: hashFileContent(filePath) };
|
|
133
|
-
} catch {
|
|
134
|
-
return { ...baseline, contentHash: null };
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Captures one explicit absolute output path so goal completion can verify its creation or modification. */
|
|
139
|
-
function inferGoalVerification(objective: string): GoalFileVerification | undefined {
|
|
140
|
-
const destination = /\b(?:create|write|save|generate)\b[^\r\n]{0,160}?\b(?:file|document|markdown|report|spreadsheet|presentation|image)\b\s+(?:to|at|as|destination(?:\s+is)?|output(?:\s+(?:to|at))?)\b\s*(?:`([^`\r\n]+)`|"([^"\r\n]+)"|'([^'\r\n]+)'|([A-Za-z]:\\[^\s,;]+|\/[^\s,;]+))/giu;
|
|
141
|
-
const paths = [...objective.matchAll(destination)]
|
|
142
|
-
.map((match) => (match[1] ?? match[2] ?? match[3] ?? match[4] ?? "").trim())
|
|
143
|
-
.filter(isAbsoluteFilePath);
|
|
144
|
-
const unique = [...new Set(paths)];
|
|
145
|
-
const filePath = unique.length === 1 ? unique[0] : undefined;
|
|
146
|
-
return filePath ? { kind: "file", path: filePath, baseline: captureGoalFileBaseline(filePath) } : undefined;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function verifyGoalDeliverable(verification: GoalFileVerification): void {
|
|
150
|
-
let artifact: ReturnType<typeof lstatSync>;
|
|
151
|
-
try {
|
|
152
|
-
artifact = lstatSync(verification.path);
|
|
153
|
-
} catch {
|
|
154
|
-
throw new Error(`Goal deliverable is not a regular file at the required path: ${verification.path}`);
|
|
155
|
-
}
|
|
156
|
-
if (!artifact.isFile()) {
|
|
157
|
-
throw new Error(`Goal deliverable is not a regular file at the required path: ${verification.path}`);
|
|
158
|
-
}
|
|
159
|
-
if (!verification.baseline.exists) return;
|
|
160
|
-
if (verification.baseline.contentHash === null) {
|
|
161
|
-
throw new Error(`Goal deliverable content cannot be verified: ${verification.path}`);
|
|
162
|
-
}
|
|
163
|
-
if (verification.baseline.contentHash !== undefined) {
|
|
164
|
-
let contentHash: string;
|
|
165
|
-
try {
|
|
166
|
-
contentHash = hashFileContent(verification.path);
|
|
167
|
-
} catch {
|
|
168
|
-
throw new Error(`Goal deliverable content cannot be verified: ${verification.path}`);
|
|
169
|
-
}
|
|
170
|
-
if (contentHash === verification.baseline.contentHash) {
|
|
171
|
-
throw new Error(`Goal deliverable has not changed since the goal started: ${verification.path}`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
if (verification.baseline.contentHash === undefined
|
|
175
|
-
&& artifact.size === verification.baseline.size
|
|
176
|
-
&& artifact.mtimeMs === verification.baseline.mtimeMs) {
|
|
177
|
-
throw new Error(`Goal deliverable has not changed since the goal started: ${verification.path}`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function isGoalBlockerAudit(value: unknown, turns: number, status: GoalStatus): value is GoalBlockerAudit {
|
|
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
|
-
}
|
|
189
|
-
if (status === "complete") return false;
|
|
190
|
-
return status === "blocked" ? value.streak === 3 : value.streak < 3;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function parseGoalState(value: unknown): GoalState | undefined {
|
|
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)) {
|
|
228
|
-
return undefined;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const common: GoalStateCommon = {
|
|
232
|
-
version: GOAL_VERSION,
|
|
233
|
-
revision,
|
|
234
|
-
objective: objective.trim(),
|
|
235
|
-
createdAt,
|
|
236
|
-
updatedAt,
|
|
237
|
-
activeMilliseconds,
|
|
238
|
-
turns,
|
|
239
|
-
blockedAuditStartTurn: blockedAuditStartTurn ?? 0,
|
|
240
|
-
baselineTokens,
|
|
241
|
-
...(verification === undefined ? {} : { verification }),
|
|
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
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
59
|
function goalBranchEntries(ctx: ExtensionContext): ReturnType<ExtensionContext["sessionManager"]["getEntries"]> {
|
|
277
60
|
try {
|
|
278
61
|
return ctx.sessionManager.getBranch();
|
|
@@ -311,35 +94,6 @@ function restoreGoalState(ctx: ExtensionContext): RestoredGoalState {
|
|
|
311
94
|
return { state: undefined };
|
|
312
95
|
}
|
|
313
96
|
|
|
314
|
-
export function goalElapsedMilliseconds(state: GoalState, now = Date.now()): number {
|
|
315
|
-
const activeInterval = state.status === "active"
|
|
316
|
-
? Math.max(0, now - state.activeStartedAt)
|
|
317
|
-
: 0;
|
|
318
|
-
return state.activeMilliseconds + activeInterval;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function commonGoalState(state: GoalState): GoalStateCommon {
|
|
322
|
-
return {
|
|
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 }),
|
|
333
|
-
};
|
|
334
|
-
}
|
|
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
|
-
|
|
343
97
|
function sumGoalTokens(ctx: ExtensionContext): number {
|
|
344
98
|
let total = 0;
|
|
345
99
|
for (const entry of goalBranchEntries(ctx)) {
|
|
@@ -390,42 +144,7 @@ function transitionGoal(
|
|
|
390
144
|
): GoalState {
|
|
391
145
|
const current = runtime.state;
|
|
392
146
|
if (!current) throw new Error("No goal is set");
|
|
393
|
-
const
|
|
394
|
-
const stopped = stopGoalClock(current, now);
|
|
395
|
-
const common: GoalStateCommon = {
|
|
396
|
-
...stopped,
|
|
397
|
-
revision: stopped.revision + 1,
|
|
398
|
-
updatedAt: now,
|
|
399
|
-
blockedAuditStartTurn: options.resetBlockedAudit ? stopped.turns : stopped.blockedAuditStartTurn,
|
|
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
|
-
}
|
|
147
|
+
const next = transitionGoalState(current, status, result, options, Date.now());
|
|
429
148
|
persistGoalState(pi, runtime, event, next);
|
|
430
149
|
if (status !== "active") {
|
|
431
150
|
runtime.continuationScheduled = false;
|
|
@@ -472,7 +191,7 @@ function goalPanelActions(status: GoalStatus): Array<{ label: string; control: "
|
|
|
472
191
|
function goalStatusSummary(state: GoalState, ctx: ExtensionContext): string {
|
|
473
192
|
const usedTokens = Math.max(0, sumGoalTokens(ctx) - state.baselineTokens);
|
|
474
193
|
const lines = [
|
|
475
|
-
`Goal ${goalStatusLabel(state.status).toLowerCase()} · ${state.turns} turn${state.turns === 1 ? "" : "s"} · ${formatTime(goalElapsedMilliseconds(state))} · ${formatTokens(usedTokens)} tokens`,
|
|
194
|
+
`Goal ${goalStatusLabel(state.status).toLowerCase()} · ${state.turns} turn${state.turns === 1 ? "" : "s"} · ${formatTime(goalElapsedMilliseconds(state, Date.now()))} · ${formatTokens(usedTokens)} tokens`,
|
|
476
195
|
state.objective,
|
|
477
196
|
];
|
|
478
197
|
if (state.result) lines.push(state.result);
|
|
@@ -493,12 +212,7 @@ export function pauseGoalAfterFailure(
|
|
|
493
212
|
transitionGoal(pi, runtime, "error", "paused", safeReason);
|
|
494
213
|
} catch {
|
|
495
214
|
const current = runtime.state;
|
|
496
|
-
runtime.state = current ?
|
|
497
|
-
...stopGoalClock(current, Date.now()),
|
|
498
|
-
status: "paused",
|
|
499
|
-
result: safeReason,
|
|
500
|
-
...(current.blockerAudit === undefined ? {} : { blockerAudit: current.blockerAudit }),
|
|
501
|
-
} : undefined;
|
|
215
|
+
runtime.state = current ? pauseGoalState(current, safeReason, Date.now()) : undefined;
|
|
502
216
|
syncGoalUpdateTool(pi, runtime);
|
|
503
217
|
runtime.persistenceRetryNeeded = true;
|
|
504
218
|
runtime.continuationScheduled = false;
|
|
@@ -522,13 +236,7 @@ function pauseGoalForPossibleManualCompaction(
|
|
|
522
236
|
});
|
|
523
237
|
} catch {
|
|
524
238
|
const current = runtime.state;
|
|
525
|
-
runtime.state = current ?
|
|
526
|
-
...stopGoalClock(current, Date.now()),
|
|
527
|
-
status: "paused",
|
|
528
|
-
result: safeReason,
|
|
529
|
-
resumeAfterManualCompaction: true,
|
|
530
|
-
...(current.blockerAudit === undefined ? {} : { blockerAudit: current.blockerAudit }),
|
|
531
|
-
} : undefined;
|
|
239
|
+
runtime.state = current ? pauseGoalState(current, safeReason, Date.now(), true) : undefined;
|
|
532
240
|
syncGoalUpdateTool(pi, runtime);
|
|
533
241
|
runtime.persistenceRetryNeeded = true;
|
|
534
242
|
runtime.continuationScheduled = false;
|
|
@@ -570,13 +278,7 @@ function beginGoalTurn(
|
|
|
570
278
|
ctx: ExtensionContext,
|
|
571
279
|
current: Extract<GoalState, { status: "active" }>,
|
|
572
280
|
): GoalState | undefined {
|
|
573
|
-
const
|
|
574
|
-
const next: GoalState = {
|
|
575
|
-
...current,
|
|
576
|
-
revision: current.revision + 1,
|
|
577
|
-
turns: current.turns + 1,
|
|
578
|
-
updatedAt: now,
|
|
579
|
-
};
|
|
281
|
+
const next = beginGoalTurnState(current, Date.now());
|
|
580
282
|
try {
|
|
581
283
|
persistGoalState(pi, runtime, "turn", next);
|
|
582
284
|
} catch (error) {
|
|
@@ -624,7 +326,7 @@ function scheduleGoalContinuation(
|
|
|
624
326
|
}
|
|
625
327
|
}
|
|
626
328
|
|
|
627
|
-
/** Resumes the revision paused by automatic compaction
|
|
329
|
+
/** Resumes the revision paused by automatic compaction once both host callbacks settle and Pi reports an outcome. */
|
|
628
330
|
function finalizeAutomaticCompaction(
|
|
629
331
|
pi: ExtensionAPI,
|
|
630
332
|
runtime: GoalRuntime,
|
|
@@ -632,7 +334,8 @@ function finalizeAutomaticCompaction(
|
|
|
632
334
|
ctx: ExtensionContext,
|
|
633
335
|
): void {
|
|
634
336
|
const recovery = runtime.automaticCompaction;
|
|
635
|
-
if (!recovery
|
|
337
|
+
if (!recovery || recovery.outcome === "pending" || !recovery.turnSettled) return;
|
|
338
|
+
const skipped = recovery.outcome === "skipped";
|
|
636
339
|
runtime.automaticCompaction = undefined;
|
|
637
340
|
if (runtime.state?.status !== "paused"
|
|
638
341
|
|| runtime.state.revision !== recovery.pausedRevision
|
|
@@ -641,7 +344,9 @@ function finalizeAutomaticCompaction(
|
|
|
641
344
|
transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
|
|
642
345
|
} catch (error) {
|
|
643
346
|
runtime.persistenceRetryNeeded = true;
|
|
644
|
-
reportError(ctx,
|
|
347
|
+
reportError(ctx, skipped
|
|
348
|
+
? "Automatic compaction was skipped, but the goal could not be resumed"
|
|
349
|
+
: "Automatic compaction succeeded, but the goal could not be resumed", error);
|
|
645
350
|
return;
|
|
646
351
|
}
|
|
647
352
|
runtime.continuationScheduled = false;
|
|
@@ -656,7 +361,19 @@ function completeAutomaticCompaction(
|
|
|
656
361
|
ctx: ExtensionContext,
|
|
657
362
|
): void {
|
|
658
363
|
if (!runtime.automaticCompaction) return;
|
|
659
|
-
runtime.automaticCompaction.
|
|
364
|
+
runtime.automaticCompaction.outcome = "completed";
|
|
365
|
+
finalizeAutomaticCompaction(pi, runtime, initState, ctx);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Records Pi's expected session-too-small rejection and resumes without claiming compaction succeeded. */
|
|
369
|
+
function skipAutomaticCompaction(
|
|
370
|
+
pi: ExtensionAPI,
|
|
371
|
+
runtime: GoalRuntime,
|
|
372
|
+
initState: InitRuntime,
|
|
373
|
+
ctx: ExtensionContext,
|
|
374
|
+
): void {
|
|
375
|
+
if (!runtime.automaticCompaction) return;
|
|
376
|
+
runtime.automaticCompaction.outcome = "skipped";
|
|
660
377
|
finalizeAutomaticCompaction(pi, runtime, initState, ctx);
|
|
661
378
|
}
|
|
662
379
|
|
|
@@ -745,12 +462,6 @@ function isSavedSession(ctx: ExtensionContext): boolean {
|
|
|
745
462
|
}
|
|
746
463
|
}
|
|
747
464
|
|
|
748
|
-
function validateGoalObjective(input: string): string | undefined {
|
|
749
|
-
const objective = input.trim();
|
|
750
|
-
if (!objective) return undefined;
|
|
751
|
-
return [...objective].length <= GOAL_OBJECTIVE_LIMIT ? objective : undefined;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
465
|
export function registerGoal(
|
|
755
466
|
pi: ExtensionAPI,
|
|
756
467
|
runtime: GoalRuntime,
|
|
@@ -786,7 +497,7 @@ export function registerGoal(
|
|
|
786
497
|
const evidence = params.evidence.trim();
|
|
787
498
|
if (!evidence) throw new Error("Goal evidence must not be empty");
|
|
788
499
|
if (params.status === "complete") {
|
|
789
|
-
if (state.verification) verifyGoalDeliverable(state.verification);
|
|
500
|
+
if (state.verification) await verifyGoalDeliverable(state.verification);
|
|
790
501
|
const verification = state.verification ? "file" : "model-reported";
|
|
791
502
|
transitionGoal(pi, runtime, "complete", "complete", evidence, { resetBlockedAudit: true });
|
|
792
503
|
return {
|
|
@@ -807,12 +518,7 @@ export function registerGoal(
|
|
|
807
518
|
const streak = sameTurn ? previous.streak : consecutive ? previous.streak + 1 : 1;
|
|
808
519
|
const blockerAudit = { key: blockerKey, streak, lastTurn: state.turns };
|
|
809
520
|
if (streak < 3) {
|
|
810
|
-
const next
|
|
811
|
-
...state,
|
|
812
|
-
revision: state.revision + 1,
|
|
813
|
-
updatedAt: Date.now(),
|
|
814
|
-
blockerAudit,
|
|
815
|
-
};
|
|
521
|
+
const next = recordGoalBlockerAudit(state, blockerAudit, Date.now());
|
|
816
522
|
persistGoalState(pi, runtime, "blocker-audit", next);
|
|
817
523
|
return {
|
|
818
524
|
content: [{ type: "text", text: `Blocker audit ${streak}/3 recorded; the goal remains active: ${evidence}` }],
|
|
@@ -865,16 +571,7 @@ export function registerGoal(
|
|
|
865
571
|
|
|
866
572
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
867
573
|
if (runtime.state?.status === "active") {
|
|
868
|
-
const
|
|
869
|
-
const checkpoint: GoalState = {
|
|
870
|
-
...stopGoalClock(runtime.state, now),
|
|
871
|
-
revision: runtime.state.revision + 1,
|
|
872
|
-
status: "active",
|
|
873
|
-
updatedAt: now,
|
|
874
|
-
activeStartedAt: now,
|
|
875
|
-
...(runtime.state.result === undefined ? {} : { result: runtime.state.result }),
|
|
876
|
-
...(runtime.state.blockerAudit === undefined ? {} : { blockerAudit: runtime.state.blockerAudit }),
|
|
877
|
-
};
|
|
574
|
+
const checkpoint = checkpointActiveGoalState(runtime.state, Date.now());
|
|
878
575
|
try {
|
|
879
576
|
persistGoalState(pi, runtime, "checkpoint", checkpoint);
|
|
880
577
|
} catch (error) {
|
|
@@ -993,13 +690,7 @@ export function registerGoal(
|
|
|
993
690
|
ctx.ui.notify("Goal is already paused", "info");
|
|
994
691
|
return;
|
|
995
692
|
}
|
|
996
|
-
const
|
|
997
|
-
const { resumeAfterManualCompaction: _resume, ...paused } = runtime.state;
|
|
998
|
-
const checkpoint: GoalState = {
|
|
999
|
-
...paused,
|
|
1000
|
-
revision: paused.revision + 1,
|
|
1001
|
-
updatedAt: now,
|
|
1002
|
-
};
|
|
693
|
+
const checkpoint = checkpointPausedGoalState(runtime.state, Date.now());
|
|
1003
694
|
try {
|
|
1004
695
|
persistGoalState(pi, runtime, "pause", checkpoint);
|
|
1005
696
|
ctx.ui.notify("Goal pause saved. Goal remains paused. Automatic compaction recovery is off.", "info");
|
|
@@ -1113,20 +804,8 @@ export function registerGoal(
|
|
|
1113
804
|
scheduleGoalContinuation(pi, runtime, initState, ctx);
|
|
1114
805
|
return;
|
|
1115
806
|
}
|
|
1116
|
-
const
|
|
1117
|
-
const
|
|
1118
|
-
const { verification: _previousVerification, ...currentWithoutVerification } = current;
|
|
1119
|
-
const verification = inferGoalVerification(objective);
|
|
1120
|
-
const next: GoalState = {
|
|
1121
|
-
...currentWithoutVerification,
|
|
1122
|
-
revision: current.revision + 1,
|
|
1123
|
-
objective,
|
|
1124
|
-
status: "active",
|
|
1125
|
-
updatedAt: now,
|
|
1126
|
-
activeStartedAt: now,
|
|
1127
|
-
blockedAuditStartTurn: current.turns,
|
|
1128
|
-
...(verification === undefined ? {} : { verification }),
|
|
1129
|
-
};
|
|
807
|
+
const verification = await inferGoalVerification(objective);
|
|
808
|
+
const next = editGoalState(runtime.state, objective, verification, Date.now());
|
|
1130
809
|
try {
|
|
1131
810
|
persistGoalState(pi, runtime, "edit", next);
|
|
1132
811
|
runtime.continuationScheduled = false;
|
|
@@ -1182,22 +861,9 @@ export function registerGoal(
|
|
|
1182
861
|
scheduleGoalContinuation(pi, runtime, initState, ctx);
|
|
1183
862
|
return;
|
|
1184
863
|
}
|
|
1185
|
-
const now = Date.now();
|
|
1186
864
|
try {
|
|
1187
|
-
const
|
|
1188
|
-
|
|
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
|
-
};
|
|
865
|
+
const verification = await inferGoalVerification(objective);
|
|
866
|
+
const state = createNewGoalState(objective, sumGoalTokens(ctx), verification, Date.now());
|
|
1201
867
|
persistGoalState(pi, runtime, unfinished ? "replace" : "set", state);
|
|
1202
868
|
if (scheduleGoalContinuation(pi, runtime, initState, ctx)) {
|
|
1203
869
|
ctx.ui.notify("Goal active. KillerOS will continue until completion, a repeated blocker, or pause.", "info");
|
|
@@ -1247,6 +913,7 @@ export function registerGoalSettlement(
|
|
|
1247
913
|
onRequested(): void;
|
|
1248
914
|
onCompleted(ctx: ExtensionContext): void;
|
|
1249
915
|
onFailed(ctx: ExtensionContext, error: unknown): void;
|
|
916
|
+
onSkipped(ctx: ExtensionContext): void;
|
|
1250
917
|
} {
|
|
1251
918
|
pi.on("agent_settled", (_event, ctx) => {
|
|
1252
919
|
const wasGoalTurn = runtime.goalTurnInFlight;
|
|
@@ -1333,18 +1000,13 @@ export function registerGoalSettlement(
|
|
|
1333
1000
|
const paused = transitionGoal(pi, runtime, "pause", "paused");
|
|
1334
1001
|
runtime.automaticCompaction = {
|
|
1335
1002
|
pausedRevision: paused.revision,
|
|
1336
|
-
|
|
1003
|
+
outcome: "pending",
|
|
1337
1004
|
turnSettled: false,
|
|
1338
1005
|
};
|
|
1339
1006
|
} catch (error) {
|
|
1340
1007
|
const current = runtime.state;
|
|
1341
1008
|
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;
|
|
1009
|
+
runtime.state = current ? pauseGoalState(current, reason, Date.now()) : undefined;
|
|
1348
1010
|
syncGoalUpdateTool(pi, runtime);
|
|
1349
1011
|
runtime.persistenceRetryNeeded = true;
|
|
1350
1012
|
runtime.continuationScheduled = false;
|
|
@@ -1355,5 +1017,6 @@ export function registerGoalSettlement(
|
|
|
1355
1017
|
},
|
|
1356
1018
|
onCompleted: (ctx: ExtensionContext): void => completeAutomaticCompaction(pi, runtime, initState, ctx),
|
|
1357
1019
|
onFailed: (ctx: ExtensionContext, error: unknown): void => failAutomaticCompaction(pi, runtime, ctx, error),
|
|
1020
|
+
onSkipped: (ctx: ExtensionContext): void => skipAutomaticCompaction(pi, runtime, initState, ctx),
|
|
1358
1021
|
};
|
|
1359
1022
|
}
|