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/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 { hasErrorCode, reportError } from "./errors.ts";
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 { GoalBlockerAudit, GoalFileBaseline, GoalFileVerification, GoalRuntime, GoalState, GoalStateCommon, GoalStatus, InitRuntime } from "./runtime.ts";
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
- interface GoalTransitionOptions {
30
- resetBlockedAudit?: boolean;
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 now = Date.now();
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 now = Date.now();
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 after both host callbacks settle. */
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?.compactionSucceeded || !recovery.turnSettled) return;
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, "Automatic compaction succeeded, but the goal could not be resumed", error);
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.compactionSucceeded = true;
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: GoalState = {
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 now = Date.now();
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 now = Date.now();
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 now = Date.now();
1117
- const current = stopGoalClock(runtime.state, now);
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 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
- };
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
- compactionSucceeded: false,
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
  }