killeros 1.4.8 → 1.5.0

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.
@@ -0,0 +1,720 @@
1
+ import { type ExtensionAPI, type ExtensionContext, type ThemeColor } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
+ import { Type } from "typebox";
4
+ import { MAX_NODE_TIMER_MS } from "./subagent-process.ts";
5
+ import { CONCISE_SYSTEM_PROMPT } from "./concise.ts";
6
+ import { formatTime, formatTokens } from "./display.ts";
7
+ import { reportError } from "./errors.ts";
8
+ import { resolvePersonalInstructions } from "./personal-instructions.ts";
9
+ import type { GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
10
+
11
+ const GOAL_ENTRY_TYPE = "killeros-goal";
12
+ const GOAL_CONTINUATION_TYPE = "killeros-goal-continuation";
13
+ const GOAL_OBJECTIVE_LIMIT = 4_000;
14
+ const GOAL_VERSION = 1;
15
+
16
+ type GoalEntryEvent = "set" | "replace" | "edit" | "turn" | "pause" | "resume" | "blocked" | "complete" | "error" | "clear" | "checkpoint";
17
+ interface GoalEntryData {
18
+ version: 1;
19
+ event: GoalEntryEvent;
20
+ state: GoalState | null;
21
+ }
22
+
23
+ const GoalUpdateParams = Type.Object({
24
+ status: Type.Union([Type.Literal("complete"), Type.Literal("blocked")], {
25
+ description: "Mark the active goal complete or blocked",
26
+ }),
27
+ evidence: Type.String({
28
+ minLength: 1,
29
+ maxLength: 2_000,
30
+ description: "Concise evidence that the objective is complete, or the repeated blocker and attempted workarounds",
31
+ }),
32
+ });
33
+
34
+ interface GoalUpdateDetails {
35
+ status: "complete" | "blocked";
36
+ evidence: string;
37
+ }
38
+
39
+ function isGoalStatus(value: unknown): value is GoalStatus {
40
+ return value === "active" || value === "paused" || value === "blocked" || value === "complete";
41
+ }
42
+
43
+ function finiteNonNegative(value: unknown): value is number {
44
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
45
+ }
46
+
47
+ function parseGoalState(value: unknown): GoalState | undefined {
48
+ if (!value || typeof value !== "object") return undefined;
49
+ const candidate = value as Partial<GoalState>;
50
+ if (candidate.version !== GOAL_VERSION
51
+ || !Number.isInteger(candidate.revision) || (candidate.revision ?? 0) < 1
52
+ || typeof candidate.objective !== "string" || !candidate.objective.trim()
53
+ || [...candidate.objective].length > GOAL_OBJECTIVE_LIMIT
54
+ || !isGoalStatus(candidate.status)
55
+ || !finiteNonNegative(candidate.createdAt)
56
+ || !finiteNonNegative(candidate.updatedAt)
57
+ || !finiteNonNegative(candidate.activeMilliseconds)
58
+ || !Number.isInteger(candidate.turns) || (candidate.turns ?? -1) < 0
59
+ || candidate.blockedAuditStartTurn !== undefined
60
+ && (!Number.isInteger(candidate.blockedAuditStartTurn) || candidate.blockedAuditStartTurn < 0 || candidate.blockedAuditStartTurn > candidate.turns!)
61
+ || !finiteNonNegative(candidate.baselineTokens)
62
+ || candidate.activeStartedAt !== undefined && !finiteNonNegative(candidate.activeStartedAt)
63
+ || candidate.result !== undefined && typeof candidate.result !== "string") {
64
+ return undefined;
65
+ }
66
+ return {
67
+ version: GOAL_VERSION,
68
+ revision: candidate.revision!,
69
+ objective: candidate.objective.trim(),
70
+ status: candidate.status,
71
+ createdAt: candidate.createdAt,
72
+ updatedAt: candidate.updatedAt,
73
+ activeMilliseconds: candidate.activeMilliseconds,
74
+ activeStartedAt: candidate.activeStartedAt,
75
+ turns: candidate.turns!,
76
+ blockedAuditStartTurn: candidate.blockedAuditStartTurn ?? 0,
77
+ baselineTokens: candidate.baselineTokens,
78
+ result: candidate.result,
79
+ };
80
+ }
81
+
82
+ function goalBranchEntries(ctx: ExtensionContext): ReturnType<ExtensionContext["sessionManager"]["getEntries"]> {
83
+ try {
84
+ return ctx.sessionManager.getBranch();
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
89
+
90
+ function restoreGoalState(ctx: ExtensionContext): GoalState | undefined {
91
+ const entries = goalBranchEntries(ctx);
92
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
93
+ const entry = entries[index];
94
+ if (entry?.type !== "custom" || entry.customType !== GOAL_ENTRY_TYPE) continue;
95
+ const data = entry.data as Partial<GoalEntryData> | undefined;
96
+ if (!data || data.version !== GOAL_VERSION) return undefined;
97
+ if (data.state === null) return undefined;
98
+ const restored = parseGoalState(data.state);
99
+ if (!restored) return undefined;
100
+ return restored.status === "active"
101
+ ? { ...restored, activeStartedAt: Date.now() }
102
+ : { ...restored, activeStartedAt: undefined };
103
+ }
104
+ return undefined;
105
+ }
106
+
107
+ export function goalElapsedMilliseconds(state: GoalState, now = Date.now()): number {
108
+ const activeInterval = state.status === "active" && state.activeStartedAt !== undefined
109
+ ? Math.max(0, now - state.activeStartedAt)
110
+ : 0;
111
+ return state.activeMilliseconds + activeInterval;
112
+ }
113
+
114
+ function stopGoalClock(state: GoalState, now: number): GoalState {
115
+ if (state.status !== "active" || state.activeStartedAt === undefined) return state;
116
+ return {
117
+ ...state,
118
+ activeMilliseconds: state.activeMilliseconds + Math.max(0, now - state.activeStartedAt),
119
+ activeStartedAt: undefined,
120
+ };
121
+ }
122
+
123
+ function sumGoalTokens(ctx: ExtensionContext): number {
124
+ let total = 0;
125
+ for (const entry of goalBranchEntries(ctx)) {
126
+ if (entry.type === "message" && (entry.message.role === "assistant" || entry.message.role === "toolResult")) {
127
+ total += entry.message.usage?.totalTokens ?? 0;
128
+ } else if ((entry.type === "compaction" || entry.type === "branch_summary") && entry.usage) {
129
+ total += entry.usage.totalTokens;
130
+ }
131
+ }
132
+ return total;
133
+ }
134
+
135
+ function persistGoalState(
136
+ pi: ExtensionAPI,
137
+ runtime: GoalRuntime,
138
+ event: GoalEntryEvent,
139
+ state: GoalState | undefined,
140
+ ): void {
141
+ const data: GoalEntryData = { version: GOAL_VERSION, event, state: state ?? null };
142
+ pi.appendEntry(GOAL_ENTRY_TYPE, data);
143
+ runtime.state = state;
144
+ runtime.persistenceRetryNeeded = false;
145
+ runtime.requestRender?.();
146
+ }
147
+
148
+ function transitionGoal(
149
+ pi: ExtensionAPI,
150
+ runtime: GoalRuntime,
151
+ event: GoalEntryEvent,
152
+ status: GoalStatus,
153
+ result?: string,
154
+ resetBlockedAudit = false,
155
+ ): GoalState {
156
+ const current = runtime.state;
157
+ if (!current) throw new Error("No goal is set");
158
+ const now = Date.now();
159
+ const stopped = stopGoalClock(current, now);
160
+ const next: GoalState = {
161
+ ...stopped,
162
+ revision: stopped.revision + 1,
163
+ status,
164
+ updatedAt: now,
165
+ activeStartedAt: status === "active" ? now : undefined,
166
+ blockedAuditStartTurn: resetBlockedAudit ? stopped.turns : stopped.blockedAuditStartTurn,
167
+ result,
168
+ };
169
+ persistGoalState(pi, runtime, event, next);
170
+ if (status !== "active") runtime.continuationScheduled = false;
171
+ return next;
172
+ }
173
+
174
+ function goalStatusLabel(status: GoalStatus): string {
175
+ return `${status.charAt(0).toLocaleUpperCase()}${status.slice(1)}`;
176
+ }
177
+
178
+ function goalStatusSummary(state: GoalState, ctx: ExtensionContext): string {
179
+ const usedTokens = Math.max(0, sumGoalTokens(ctx) - state.baselineTokens);
180
+ const lines = [
181
+ `Goal ${goalStatusLabel(state.status).toLocaleLowerCase()} · ${state.turns} turn${state.turns === 1 ? "" : "s"} · ${formatTime(goalElapsedMilliseconds(state))} · ${formatTokens(usedTokens)} tokens`,
182
+ state.objective,
183
+ ];
184
+ if (state.result) lines.push(state.result);
185
+ return lines.join("\n");
186
+ }
187
+
188
+ function pauseGoalAfterFailure(
189
+ pi: ExtensionAPI,
190
+ runtime: GoalRuntime,
191
+ ctx: ExtensionContext,
192
+ reason: string,
193
+ recoveryInstruction = "Run /goal resume after resolving the problem.",
194
+ ): void {
195
+ if (runtime.state?.status !== "active") return;
196
+ try {
197
+ transitionGoal(pi, runtime, "error", "paused", reason);
198
+ } catch {
199
+ runtime.state = runtime.state ? { ...stopGoalClock(runtime.state, Date.now()), status: "paused", result: reason } : undefined;
200
+ runtime.persistenceRetryNeeded = true;
201
+ runtime.continuationScheduled = false;
202
+ runtime.requestRender?.();
203
+ }
204
+ ctx.ui.notify(`Goal paused: ${reason}\n${recoveryInstruction}`, "error");
205
+ }
206
+
207
+ function scheduleGoalContinuation(
208
+ pi: ExtensionAPI,
209
+ runtime: GoalRuntime,
210
+ initState: InitRuntime,
211
+ ctx: ExtensionContext,
212
+ ): void {
213
+ if (!isGoalModeSupported(ctx)
214
+ || !isSavedSession(ctx)
215
+ || runtime.state?.status !== "active"
216
+ || runtime.continuationScheduled
217
+ || runtime.continuationHeld
218
+ || runtime.goalTurnInFlight
219
+ || initState.active
220
+ || ctx.hasPendingMessages()) return;
221
+ const current = runtime.state;
222
+ runtime.continuationScheduled = true;
223
+ runtime.goalTurnInFlight = false;
224
+ runtime.agentEndObserved = false;
225
+ runtime.lastStopReason = undefined;
226
+ runtime.lastError = undefined;
227
+ try {
228
+ pi.sendMessage({
229
+ customType: GOAL_CONTINUATION_TYPE,
230
+ content: goalContinuationMessage(current, ctx),
231
+ display: false,
232
+ }, { triggerTurn: true, deliverAs: "followUp" });
233
+ } catch (error) {
234
+ runtime.continuationScheduled = false;
235
+ runtime.goalTurnInFlight = false;
236
+ pauseGoalAfterFailure(pi, runtime, ctx, `continuation could not start: ${error instanceof Error ? error.message : String(error)}`);
237
+ }
238
+ }
239
+
240
+ function goalInstructions(state: GoalState, heading: string): string {
241
+ return [
242
+ `# ${heading}`,
243
+ `Status: active · Turn: ${state.turns}`,
244
+ "Objective:",
245
+ state.objective,
246
+ "",
247
+ "Continue making concrete progress toward this unchanged objective. Re-check repository state and prior results instead of repeating work.",
248
+ "Do not stop merely because one response is complete: KillerOS will start another goal turn while the goal remains active.",
249
+ "Before declaring completion, audit every part of the objective and verify the relevant results. Then call killeros_goal_update with status complete and concise evidence.",
250
+ "Call killeros_goal_update with status blocked only when the same external impasse has prevented progress for three consecutive goal turns; name the blocker and attempted workarounds.",
251
+ "Never use the goal tool to pause, resume, edit, replace, or clear the objective. Those transitions belong to the user.",
252
+ ].join("\n");
253
+ }
254
+
255
+ function goalSystemPrompt(state: GoalState): string {
256
+ return goalInstructions(state, "Active KillerOS goal");
257
+ }
258
+
259
+ function goalContinuationMessage(state: GoalState, ctx: ExtensionContext): string {
260
+ const sections = [goalInstructions(state, "KillerOS long-running goal turn")];
261
+ if (ctx.isProjectTrusted()) {
262
+ const personal = resolvePersonalInstructions(ctx.cwd);
263
+ if (personal) {
264
+ sections.push(`<personal_instructions source=${JSON.stringify(personal.source)}>\n${personal.content}\n</personal_instructions>`);
265
+ }
266
+ }
267
+ sections.push(CONCISE_SYSTEM_PROMPT);
268
+ return sections.join("\n\n");
269
+ }
270
+
271
+ function isGoalModeSupported(ctx: ExtensionContext): boolean {
272
+ return ctx.mode === "tui" || ctx.mode === "rpc";
273
+ }
274
+
275
+ function isSavedSession(ctx: ExtensionContext): boolean {
276
+ try {
277
+ return Boolean(ctx.sessionManager.getSessionFile());
278
+ } catch {
279
+ return false;
280
+ }
281
+ }
282
+
283
+ function validateGoalObjective(input: string): string | undefined {
284
+ const objective = input.trim();
285
+ if (!objective) return undefined;
286
+ return [...objective].length <= GOAL_OBJECTIVE_LIMIT ? objective : undefined;
287
+ }
288
+
289
+ export function registerGoal(
290
+ pi: ExtensionAPI,
291
+ runtime: GoalRuntime,
292
+ initState: InitRuntime,
293
+ ): void {
294
+ pi.registerEntryRenderer<GoalEntryData>(GOAL_ENTRY_TYPE, (entry, _options, theme) => {
295
+ const data = entry.data;
296
+ if (!data || data.version !== GOAL_VERSION || data.event === "turn" || data.event === "checkpoint") return undefined;
297
+ if (data.event === "clear" || data.state === null) return new Text(theme.fg("dim", "Goal cleared"), 0, 0);
298
+ const state = parseGoalState(data.state);
299
+ if (!state) return undefined;
300
+ const icon = state.status === "active" ? "✻" : state.status === "paused" ? "Ⅱ" : state.status === "blocked" ? "!" : "✓";
301
+ const color: ThemeColor = state.status === "active" ? "accent" : state.status === "paused" ? "warning" : state.status === "blocked" ? "error" : "success";
302
+ return new Text(`${theme.fg(color, `${icon} Goal ${state.status}`)}${theme.fg("dim", ` · ${state.objective}`)}`, 0, 0);
303
+ });
304
+
305
+ pi.registerTool<typeof GoalUpdateParams, GoalUpdateDetails>({
306
+ name: "killeros_goal_update",
307
+ label: "Goal update",
308
+ description: "Mark the active KillerOS long-running goal complete after verification, or blocked after the same impasse persists for three consecutive goal turns.",
309
+ parameters: GoalUpdateParams,
310
+ executionMode: "sequential",
311
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
312
+ if (!isGoalModeSupported(ctx)) throw new Error("KillerOS goals require TUI or RPC mode");
313
+ if (!isSavedSession(ctx)) throw new Error("KillerOS goals require a saved session");
314
+ const state = runtime.state;
315
+ if (!state || state.status !== "active") throw new Error("There is no active KillerOS goal to update");
316
+ const evidence = params.evidence.trim();
317
+ if (!evidence) throw new Error("Goal evidence must not be empty");
318
+ if (params.status === "blocked" && state.turns - state.blockedAuditStartTurn < 3) {
319
+ throw new Error("A goal cannot be marked blocked before three goal turns in the current audit; keep working and audit the same blocker again");
320
+ }
321
+ transitionGoal(pi, runtime, params.status, params.status, evidence);
322
+ return {
323
+ content: [{ type: "text", text: `Goal marked ${params.status}: ${evidence}` }],
324
+ details: { status: params.status, evidence },
325
+ };
326
+ },
327
+ renderCall(args, theme) {
328
+ return new Text(`${theme.fg("toolTitle", theme.bold("goal "))}${theme.fg("muted", args.status)}`, 0, 0);
329
+ },
330
+ renderResult(result, _options, theme) {
331
+ const details = result.details;
332
+ return new Text(details
333
+ ? `${theme.fg(details.status === "complete" ? "success" : "warning", details.status === "complete" ? "✓ Complete" : "! Blocked")}${theme.fg("dim", ` · ${details.evidence}`)}`
334
+ : theme.fg("dim", "Goal updated"), 0, 0);
335
+ },
336
+ });
337
+
338
+ pi.on("session_start", (_event, ctx) => {
339
+ runtime.state = restoreGoalState(ctx);
340
+ runtime.continuationScheduled = false;
341
+ runtime.continuationHeld = false;
342
+ runtime.goalTurnInFlight = false;
343
+ runtime.agentEndObserved = false;
344
+ runtime.persistenceRetryNeeded = false;
345
+ runtime.lastStopReason = undefined;
346
+ runtime.lastError = undefined;
347
+ runtime.requestRender?.();
348
+ if (runtime.state?.status === "active") {
349
+ setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
350
+ }
351
+ });
352
+
353
+ pi.on("session_tree", (_event, ctx) => {
354
+ runtime.state = restoreGoalState(ctx);
355
+ runtime.continuationScheduled = false;
356
+ runtime.continuationHeld = false;
357
+ runtime.goalTurnInFlight = false;
358
+ runtime.agentEndObserved = false;
359
+ runtime.persistenceRetryNeeded = false;
360
+ runtime.lastStopReason = undefined;
361
+ runtime.lastError = undefined;
362
+ runtime.requestRender?.();
363
+ if (runtime.state?.status === "active") {
364
+ setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
365
+ }
366
+ });
367
+
368
+ pi.on("session_shutdown", (_event, ctx) => {
369
+ if (runtime.state?.status === "active") {
370
+ const now = Date.now();
371
+ const checkpoint: GoalState = {
372
+ ...stopGoalClock(runtime.state, now),
373
+ revision: runtime.state.revision + 1,
374
+ updatedAt: now,
375
+ };
376
+ try {
377
+ persistGoalState(pi, runtime, "checkpoint", checkpoint);
378
+ } catch (error) {
379
+ reportError(ctx, "Goal state could not be checkpointed", error);
380
+ }
381
+ }
382
+ runtime.state = undefined;
383
+ runtime.continuationScheduled = false;
384
+ runtime.continuationHeld = false;
385
+ runtime.goalTurnInFlight = false;
386
+ runtime.agentEndObserved = false;
387
+ runtime.persistenceRetryNeeded = false;
388
+ runtime.lastStopReason = undefined;
389
+ runtime.lastError = undefined;
390
+ });
391
+
392
+ pi.on("before_agent_start", (event, ctx) => {
393
+ runtime.continuationScheduled = false;
394
+ const current = runtime.state;
395
+ if (!isGoalModeSupported(ctx) || !isSavedSession(ctx) || !current || current.status !== "active" || initState.active) return;
396
+ if (runtime.goalTurnInFlight) return { systemPrompt: `${event.systemPrompt}\n\n${goalSystemPrompt(current)}` };
397
+ const now = Date.now();
398
+ const next: GoalState = {
399
+ ...current,
400
+ revision: current.revision + 1,
401
+ turns: current.turns + 1,
402
+ updatedAt: now,
403
+ activeStartedAt: current.activeStartedAt ?? now,
404
+ };
405
+ try {
406
+ persistGoalState(pi, runtime, "turn", next);
407
+ } catch (error) {
408
+ pauseGoalAfterFailure(pi, runtime, ctx, `turn state could not be saved: ${error instanceof Error ? error.message : String(error)}`);
409
+ return;
410
+ }
411
+ runtime.goalTurnInFlight = true;
412
+ runtime.agentEndObserved = false;
413
+ runtime.lastStopReason = undefined;
414
+ runtime.lastError = undefined;
415
+ return { systemPrompt: `${event.systemPrompt}\n\n${goalSystemPrompt(next)}` };
416
+ });
417
+
418
+ pi.on("agent_end", (event) => {
419
+ if (!runtime.goalTurnInFlight) return;
420
+ const finalAssistant = [...event.messages].reverse().find((message) => message.role === "assistant");
421
+ runtime.agentEndObserved = finalAssistant !== undefined;
422
+ runtime.lastStopReason = finalAssistant?.stopReason;
423
+ runtime.lastError = finalAssistant?.errorMessage;
424
+ });
425
+
426
+ pi.registerCommand("goal", {
427
+ description: "Set or view the goal for a long-running task",
428
+ getArgumentCompletions: (prefix) => {
429
+ const normalized = prefix.trimStart().toLocaleLowerCase();
430
+ if (normalized.includes(" ")) return null;
431
+ const actions = [
432
+ { value: "clear", description: "Remove the current goal" },
433
+ { value: "edit", description: "Edit and reactivate the current goal" },
434
+ { value: "pause", description: "Stop automatic continuation" },
435
+ { value: "resume", description: "Resume automatic continuation" },
436
+ ];
437
+ return actions
438
+ .filter((action) => action.value.startsWith(normalized))
439
+ .map((action) => ({ ...action, label: action.value }));
440
+ },
441
+ handler: async (args, ctx) => {
442
+ if (ctx.mode === "print" || ctx.mode === "json") {
443
+ ctx.ui.notify("/goal requires TUI or RPC mode", "error");
444
+ return;
445
+ }
446
+ if (!isSavedSession(ctx)) {
447
+ ctx.ui.notify("/goal requires a saved session", "error");
448
+ return;
449
+ }
450
+ const input = args.trim();
451
+ const control = input.toLocaleLowerCase();
452
+ const isControl = control === "clear" || control === "edit" || control === "pause" || control === "resume";
453
+
454
+ if (!input) {
455
+ if (!runtime.state) {
456
+ ctx.ui.notify("No goal is set. Use /goal <objective> to start a long-running task.", "info");
457
+ return;
458
+ }
459
+ ctx.ui.notify(goalStatusSummary(runtime.state, ctx), "info");
460
+ return;
461
+ }
462
+
463
+ if (control === "clear") {
464
+ if (!runtime.state) {
465
+ ctx.ui.notify("No goal is set", "info");
466
+ return;
467
+ }
468
+ try {
469
+ persistGoalState(pi, runtime, "clear", undefined);
470
+ runtime.continuationScheduled = false;
471
+ ctx.ui.notify("Goal cleared", "info");
472
+ } catch (error) {
473
+ if (runtime.state?.status === "active") {
474
+ pauseGoalAfterFailure(
475
+ pi,
476
+ runtime,
477
+ ctx,
478
+ `the requested clear could not be saved: ${error instanceof Error ? error.message : String(error)}`,
479
+ "Automatic continuation is stopped. Retry /goal clear to remove the goal.",
480
+ );
481
+ } else {
482
+ reportError(ctx, "Goal could not be cleared", error);
483
+ }
484
+ }
485
+ return;
486
+ }
487
+
488
+ if (control === "pause") {
489
+ if (!runtime.state) {
490
+ ctx.ui.notify("No goal is set", "info");
491
+ return;
492
+ }
493
+ if (runtime.state.status === "paused") {
494
+ if (!runtime.persistenceRetryNeeded) {
495
+ ctx.ui.notify("Goal is already paused", "info");
496
+ return;
497
+ }
498
+ const now = Date.now();
499
+ const checkpoint: GoalState = {
500
+ ...runtime.state,
501
+ revision: runtime.state.revision + 1,
502
+ updatedAt: now,
503
+ };
504
+ try {
505
+ persistGoalState(pi, runtime, "pause", checkpoint);
506
+ ctx.ui.notify("Goal pause saved", "info");
507
+ } catch (error) {
508
+ reportError(ctx, "Goal pause still could not be saved", error);
509
+ }
510
+ return;
511
+ }
512
+ if (runtime.state.status !== "active") {
513
+ ctx.ui.notify(`Goal is ${runtime.state.status}; only an active goal can be paused`, "warning");
514
+ return;
515
+ }
516
+ try {
517
+ transitionGoal(pi, runtime, "pause", "paused");
518
+ ctx.ui.notify("Goal paused. Run /goal resume to continue.", "info");
519
+ } catch (error) {
520
+ pauseGoalAfterFailure(
521
+ pi,
522
+ runtime,
523
+ ctx,
524
+ `the requested pause could not be saved: ${error instanceof Error ? error.message : String(error)}`,
525
+ "Automatic continuation is stopped. If session storage is still unavailable, retry /goal pause after it recovers.",
526
+ );
527
+ }
528
+ return;
529
+ }
530
+
531
+ if (control === "resume") {
532
+ if (initState.active) {
533
+ ctx.ui.notify("Wait for /init to finish before resuming a goal", "error");
534
+ return;
535
+ }
536
+ if (!runtime.state) {
537
+ ctx.ui.notify("No goal is set", "info");
538
+ return;
539
+ }
540
+ if (runtime.state.status === "active") {
541
+ ctx.ui.notify("Goal is already active", "info");
542
+ return;
543
+ }
544
+ if (runtime.state.status === "complete") {
545
+ ctx.ui.notify("The goal is complete. Set a new objective or use /goal edit.", "info");
546
+ return;
547
+ }
548
+ try {
549
+ transitionGoal(pi, runtime, "resume", "active", undefined, true);
550
+ runtime.continuationScheduled = false;
551
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
552
+ ctx.ui.notify("Goal resumed", "info");
553
+ } catch (error) {
554
+ reportError(ctx, "Goal could not be resumed", error);
555
+ }
556
+ return;
557
+ }
558
+
559
+ if (control === "edit") {
560
+ if (initState.active) {
561
+ ctx.ui.notify("Wait for /init to finish before editing a goal", "error");
562
+ return;
563
+ }
564
+ if (!runtime.state) {
565
+ ctx.ui.notify("No goal is set", "info");
566
+ return;
567
+ }
568
+ if (ctx.mode !== "tui") {
569
+ ctx.ui.notify("/goal edit requires interactive TUI mode", "error");
570
+ return;
571
+ }
572
+ runtime.continuationHeld = true;
573
+ let waitError: unknown;
574
+ try {
575
+ await ctx.waitForIdle();
576
+ } catch (error) {
577
+ waitError = error;
578
+ } finally {
579
+ runtime.continuationHeld = false;
580
+ }
581
+ if (waitError) {
582
+ reportError(ctx, "Goal could not wait for the active turn", waitError);
583
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
584
+ return;
585
+ }
586
+ const edited = await ctx.ui.editor("Edit long-running goal", runtime.state.objective);
587
+ if (edited === undefined) {
588
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
589
+ return;
590
+ }
591
+ const objective = validateGoalObjective(edited);
592
+ if (!objective) {
593
+ ctx.ui.notify(edited.trim() ? "A goal objective may not exceed 4,000 characters" : "A goal objective may not be empty", "error");
594
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
595
+ return;
596
+ }
597
+ const now = Date.now();
598
+ const current = stopGoalClock(runtime.state, now);
599
+ const next: GoalState = {
600
+ ...current,
601
+ revision: current.revision + 1,
602
+ objective,
603
+ status: "active",
604
+ updatedAt: now,
605
+ activeStartedAt: now,
606
+ blockedAuditStartTurn: current.turns,
607
+ result: undefined,
608
+ };
609
+ try {
610
+ persistGoalState(pi, runtime, "edit", next);
611
+ runtime.continuationScheduled = false;
612
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
613
+ ctx.ui.notify("Goal updated and active", "info");
614
+ } catch (error) {
615
+ pauseGoalAfterFailure(
616
+ pi,
617
+ runtime,
618
+ ctx,
619
+ `Goal could not be edited: ${error instanceof Error ? error.message : String(error)}`,
620
+ "Automatic continuation is stopped. Retry /goal edit after session storage recovers.",
621
+ );
622
+ }
623
+ return;
624
+ }
625
+
626
+ if (isControl) return;
627
+ if (initState.active) {
628
+ ctx.ui.notify("Wait for /init to finish before starting a goal", "error");
629
+ return;
630
+ }
631
+ const objective = validateGoalObjective(input);
632
+ if (!objective) {
633
+ ctx.ui.notify(input ? "A goal objective may not exceed 4,000 characters" : "A goal objective may not be empty", "error");
634
+ return;
635
+ }
636
+
637
+ const unfinished = runtime.state && runtime.state.status !== "complete";
638
+ if (unfinished) {
639
+ if (!ctx.hasUI) {
640
+ ctx.ui.notify("Clear the current goal before replacing it outside TUI mode", "error");
641
+ return;
642
+ }
643
+ const replace = await ctx.ui.confirm("Replace active goal", "Replace the current unfinished goal and discard its continuation state?");
644
+ if (!replace) return;
645
+ }
646
+
647
+ runtime.continuationHeld = true;
648
+ let waitError: unknown;
649
+ try {
650
+ await ctx.waitForIdle();
651
+ } catch (error) {
652
+ waitError = error;
653
+ } finally {
654
+ runtime.continuationHeld = false;
655
+ }
656
+ if (waitError) {
657
+ reportError(ctx, "Goal could not wait for the active turn", waitError);
658
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
659
+ return;
660
+ }
661
+ const now = Date.now();
662
+ const state: GoalState = {
663
+ version: GOAL_VERSION,
664
+ revision: 1,
665
+ objective,
666
+ status: "active",
667
+ createdAt: now,
668
+ updatedAt: now,
669
+ activeMilliseconds: 0,
670
+ activeStartedAt: now,
671
+ turns: 0,
672
+ blockedAuditStartTurn: 0,
673
+ baselineTokens: sumGoalTokens(ctx),
674
+ };
675
+ try {
676
+ persistGoalState(pi, runtime, unfinished ? "replace" : "set", state);
677
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
678
+ ctx.ui.notify("Goal active. KillerOS will continue until completion, a repeated blocker, or pause.", "info");
679
+ } catch (error) {
680
+ reportError(ctx, "Goal could not be started", error);
681
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
682
+ }
683
+ },
684
+ });
685
+ }
686
+
687
+ export function registerGoalSettlement(
688
+ pi: ExtensionAPI,
689
+ runtime: GoalRuntime,
690
+ initState: InitRuntime,
691
+ ): void {
692
+ pi.on("agent_settled", (_event, ctx) => {
693
+ const wasGoalTurn = runtime.goalTurnInFlight;
694
+ const continuationWasScheduled = runtime.continuationScheduled;
695
+ const agentEndObserved = runtime.agentEndObserved;
696
+ runtime.goalTurnInFlight = false;
697
+ runtime.agentEndObserved = false;
698
+ runtime.continuationScheduled = false;
699
+ if (!wasGoalTurn || runtime.state?.status !== "active" || initState.active) {
700
+ if (continuationWasScheduled && runtime.state?.status === "active" && !initState.active) {
701
+ pauseGoalAfterFailure(pi, runtime, ctx, "the goal continuation ended before an agent turn started");
702
+ }
703
+ return;
704
+ }
705
+ if (!agentEndObserved) {
706
+ pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn ended without an agent result");
707
+ return;
708
+ }
709
+ if (runtime.lastStopReason === "error" || runtime.lastStopReason === "aborted") {
710
+ const reason = runtime.lastError || (runtime.lastStopReason === "aborted" ? "the agent turn was aborted" : "the agent turn failed");
711
+ runtime.lastStopReason = undefined;
712
+ runtime.lastError = undefined;
713
+ pauseGoalAfterFailure(pi, runtime, ctx, reason);
714
+ return;
715
+ }
716
+ runtime.lastStopReason = undefined;
717
+ runtime.lastError = undefined;
718
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
719
+ });
720
+ }