killeros 3.0.7 → 3.0.8

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.
@@ -1,516 +1,519 @@
1
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { reportError } from "./errors.ts";
3
- import {
4
- beginGoalTurnState,
5
- boundGoalText,
6
- checkpointActiveGoalState,
7
- GOAL_VERSION,
8
- parseGoalState,
9
- pauseGoalState,
10
- transitionGoalState,
11
- type GoalTransitionOptions,
12
- } from "./goal-state.ts";
13
- import type { GoalRuntime, GoalState, GoalStatus } from "./runtime.ts";
14
- import { safeTerminalText } from "./safe-terminal-text.ts";
15
-
16
- export const GOAL_ENTRY_TYPE = "killeros-goal";
17
- export const GOAL_CONTINUATION_TYPE = "killeros-goal-continuation";
18
- export const GOAL_UPDATE_TOOL = "killeros_goal_update";
19
-
20
- export type GoalEntryEvent = "set" | "replace" | "limit" | "turn" | "continue" | "pause" | "resume" | "blocked" | "complete" | "error" | "clear" | "checkpoint" | "blocker-audit";
21
- export interface GoalEntryData {
22
- version: 1;
23
- event: GoalEntryEvent;
24
- state: GoalState | null;
25
- }
26
-
27
- function isUnknownRecord(value: unknown): value is Record<string, unknown> {
28
- return typeof value === "object" && value !== null && !Array.isArray(value);
29
- }
30
-
31
- interface RestoredGoalState {
32
- state?: GoalState;
33
- }
34
-
35
- export function goalBranchEntries(ctx: ExtensionContext): ReturnType<ExtensionContext["sessionManager"]["getEntries"]> {
36
- try {
37
- return ctx.sessionManager.getBranch();
38
- } catch {
39
- return [];
40
- }
41
- }
42
-
43
- function restoreGoalState(ctx: ExtensionContext): RestoredGoalState {
44
- const entries = goalBranchEntries(ctx);
45
- for (let index = entries.length - 1; index >= 0; index -= 1) {
46
- const entry = entries[index];
47
- if (entry?.type !== "custom" || entry.customType !== GOAL_ENTRY_TYPE) continue;
48
- const data: unknown = entry.data;
49
- if (!isUnknownRecord(data) || data.version !== GOAL_VERSION || data.state === null) {
50
- return { state: undefined };
51
- }
52
- // v2.0.18 shutdown checkpoints stopped active clocks by omitting activeStartedAt.
53
- const savedState = data.event === "checkpoint"
54
- && isUnknownRecord(data.state)
55
- && data.state.status === "active"
56
- && data.state.activeStartedAt === undefined
57
- ? { ...data.state, activeStartedAt: Date.now() }
58
- : data.state;
59
- const restored = parseGoalState(savedState);
60
- if (!restored) return { state: undefined };
61
- if (restored.status === "active") {
62
- return { state: { ...restored, activeStartedAt: Date.now() } };
63
- }
64
- if (restored.status === "paused") {
65
- const { resumeAfterManualCompaction: _resume, ...state } = restored;
66
- return { state };
67
- }
68
- return { state: restored };
69
- }
70
- return { state: undefined };
71
- }
72
-
73
- export function sumGoalTokens(ctx: ExtensionContext): number {
74
- let total = 0;
75
- for (const entry of goalBranchEntries(ctx)) {
76
- if (entry.type === "message" && (entry.message.role === "assistant" || entry.message.role === "toolResult")) {
77
- total += entry.message.usage?.totalTokens ?? 0;
78
- } else if ((entry.type === "compaction" || entry.type === "branch_summary") && entry.usage) {
79
- total += entry.usage.totalTokens;
80
- }
81
- }
82
- return total;
83
- }
84
-
85
- function setGoalUpdateToolActive(pi: ExtensionAPI, active: boolean): void {
86
- try {
87
- const activeTools = pi.getActiveTools();
88
- const isActive = activeTools.includes(GOAL_UPDATE_TOOL);
89
- if (active === isActive) return;
90
- pi.setActiveTools(active
91
- ? [...activeTools, GOAL_UPDATE_TOOL]
92
- : activeTools.filter((name) => name !== GOAL_UPDATE_TOOL));
93
- } catch {
94
- // Availability is checked again immediately before a goal request.
95
- }
96
- }
97
-
98
- export function syncGoalUpdateTool(pi: ExtensionAPI, runtime: GoalRuntime): void {
99
- const permission = runtime.blockedCompletion;
100
- setGoalUpdateToolActive(pi, runtime.state?.status === "active"
101
- || permission?.kind === "eligible"
102
- && permission.state === runtime.state
103
- && permission.generation === runtime.lifecycleGeneration);
104
- }
105
-
106
- function goalUpdateToolIsAvailable(pi: ExtensionAPI): boolean {
107
- try {
108
- const active = pi.getActiveTools();
109
- if (!active.includes(GOAL_UPDATE_TOOL)) pi.setActiveTools([...active, GOAL_UPDATE_TOOL]);
110
- return pi.getActiveTools().includes(GOAL_UPDATE_TOOL);
111
- } catch {
112
- return false;
113
- }
114
- }
115
-
116
- export function persistGoalState(
117
- pi: ExtensionAPI,
118
- runtime: GoalRuntime,
119
- event: GoalEntryEvent,
120
- state: GoalState | undefined,
121
- ): void {
122
- const data: GoalEntryData = { version: GOAL_VERSION, event, state: state ?? null };
123
- pi.appendEntry(GOAL_ENTRY_TYPE, data);
124
- runtime.state = state;
125
- runtime.blockedCompletion = undefined;
126
- if (event === "clear") {
127
- runtime.lifecycleGeneration += 1;
128
- runtime.continuationHeld = undefined;
129
- }
130
- syncGoalUpdateTool(pi, runtime);
131
- runtime.persistenceRetryNeeded = false;
132
- runtime.requestRender?.();
133
- }
134
-
135
- export function transitionGoal(
136
- pi: ExtensionAPI,
137
- runtime: GoalRuntime,
138
- event: GoalEntryEvent,
139
- status: GoalStatus,
140
- result?: string,
141
- options: GoalTransitionOptions = {},
142
- ): GoalState {
143
- const current = runtime.state;
144
- if (!current) throw new Error("No goal is set");
145
- const next = transitionGoalState(current, status, result, options, Date.now());
146
- persistGoalState(pi, runtime, event, next);
147
- if (status !== "active") {
148
- runtime.continuationScheduled = false;
149
- runtime.automaticCompaction = undefined;
150
- if (!options.keepTurnForRecovery) {
151
- runtime.goalTurn = undefined;
152
- runtime.goalTurnInFlight = false;
153
- runtime.agentEndObserved = false;
154
- }
155
- } else if (!options.resumeInterruptedTurn) {
156
- runtime.goalTurn = undefined;
157
- runtime.goalTurnInFlight = false;
158
- runtime.agentEndObserved = false;
159
- }
160
- return next;
161
- }
162
-
163
- export function pauseGoalAtTurnLimit(
164
- pi: ExtensionAPI,
165
- runtime: GoalRuntime,
166
- ctx: ExtensionContext,
167
- ): boolean {
168
- const state = runtime.state;
169
- if (state?.status !== "active" || state.maxTurns === undefined || state.turns < state.maxTurns) return false;
170
- const result = `Turn limit reached (${state.turns}/${state.maxTurns}).`;
171
- try {
172
- transitionGoal(pi, runtime, "limit", "paused", result);
173
- } catch (error) {
174
- pauseGoalAfterFailure(pi, runtime, ctx, `turn limit pause could not be saved: ${error instanceof Error ? error.message : String(error)}`);
175
- }
176
- return true;
177
- }
178
-
179
- function clearGoalExecutionFlags(runtime: GoalRuntime): void {
180
- runtime.continuationScheduled = false;
181
- runtime.goalTurnInFlight = false;
182
- runtime.agentEndObserved = false;
183
- runtime.goalTurn = undefined;
184
- runtime.automaticCompaction = undefined;
185
- runtime.lastStopReason = undefined;
186
- runtime.lastError = undefined;
187
- }
188
-
189
- export async function stopGoalRun(runtime: GoalRuntime, ctx: ExtensionCommandContext, shouldStop: boolean): Promise<void> {
190
- if (!shouldStop) return;
191
- try {
192
- ctx.abort();
193
- await ctx.waitForIdle();
194
- } finally {
195
- clearGoalExecutionFlags(runtime);
196
- }
197
- }
198
-
199
- function safeFailureReason(reason: string): string {
200
- return boundGoalText(reason) || "an unspecified goal failure";
201
- }
202
-
203
- export function pauseGoalAfterFailure(
204
- pi: ExtensionAPI,
205
- runtime: GoalRuntime,
206
- ctx: ExtensionContext,
207
- reason: string,
208
- recoveryInstruction = "Run /goal resume after resolving the problem.",
209
- notify = true,
210
- ): void {
211
- if (runtime.state?.status !== "active") return;
212
- const safeReason = safeFailureReason(reason);
213
- const safeRecoveryInstruction = safeReason === "no turn decision"
214
- && recoveryInstruction === "Run /goal resume after resolving the problem."
215
- ? "The agent ended without choosing continue, complete, or blocked. Run /goal resume only after choosing to continue."
216
- : safeTerminalText(recoveryInstruction);
217
- try {
218
- transitionGoal(pi, runtime, "error", "paused", safeReason);
219
- } catch {
220
- const current = runtime.state;
221
- runtime.state = current ? pauseGoalState(current, safeReason, Date.now()) : undefined;
222
- syncGoalUpdateTool(pi, runtime);
223
- runtime.persistenceRetryNeeded = true;
224
- clearGoalExecutionFlags(runtime);
225
- runtime.requestRender?.();
226
- }
227
- if (notify) ctx.ui.notify(`Goal paused: ${safeReason}\n${safeRecoveryInstruction}`, "error");
228
- }
229
-
230
- function pauseGoalBeforeTurn(
231
- pi: ExtensionAPI,
232
- runtime: GoalRuntime,
233
- ctx: ExtensionContext,
234
- ): void {
235
- const state = runtime.state;
236
- if (state?.status !== "active") return;
237
- const nextTurn = state.turns + 1;
238
- const reason = "killeros_goal_update is unavailable";
239
- try {
240
- transitionGoal(pi, runtime, "error", "paused", reason);
241
- } catch {
242
- runtime.state = pauseGoalState(state, reason, Date.now());
243
- syncGoalUpdateTool(pi, runtime);
244
- runtime.persistenceRetryNeeded = true;
245
- clearGoalExecutionFlags(runtime);
246
- runtime.requestRender?.();
247
- }
248
- ctx.ui.notify(
249
- `Goal paused before turn ${nextTurn}: ${reason}\nEnable the goal tool, then run /goal resume.`,
250
- "error",
251
- );
252
- }
253
-
254
- function beginGoalTurn(
255
- pi: ExtensionAPI,
256
- runtime: GoalRuntime,
257
- ctx: ExtensionContext,
258
- current: Extract<GoalState, { status: "active" }>,
259
- ): GoalState | undefined {
260
- let next: GoalState;
261
- try {
262
- next = beginGoalTurnState(current, Date.now());
263
- persistGoalState(pi, runtime, "turn", next);
264
- } catch (error) {
265
- pauseGoalAfterFailure(pi, runtime, ctx, `turn state could not be saved: ${error instanceof Error ? error.message : String(error)}`);
266
- return undefined;
267
- }
268
- runtime.goalTurn = { revision: next.revision, turn: next.turns };
269
- runtime.goalTurnInFlight = true;
270
- runtime.agentEndObserved = false;
271
- runtime.lastStopReason = undefined;
272
- runtime.lastError = undefined;
273
- return next;
274
- }
275
-
276
- function sendGoalTurn(
277
- pi: ExtensionAPI,
278
- runtime: GoalRuntime,
279
- ctx: ExtensionContext,
280
- state: Extract<GoalState, { status: "active" }>,
281
- ): boolean {
282
- runtime.continuationScheduled = true;
283
- try {
284
- pi.sendMessage({
285
- customType: GOAL_CONTINUATION_TYPE,
286
- content: goalContinuationMessage(state),
287
- display: false,
288
- }, { triggerTurn: true, deliverAs: "followUp" });
289
- return true;
290
- } catch (error) {
291
- runtime.continuationScheduled = false;
292
- runtime.goalTurnInFlight = false;
293
- runtime.goalTurn = undefined;
294
- pauseGoalAfterFailure(pi, runtime, ctx, `continuation could not start: ${error instanceof Error ? error.message : String(error)}`);
295
- return false;
296
- }
297
- }
298
-
299
- export function resumeInterruptedGoalTurn(
300
- pi: ExtensionAPI,
301
- runtime: GoalRuntime,
302
- ctx: ExtensionContext,
303
- ): boolean {
304
- const state = runtime.state;
305
- if (state?.status !== "active"
306
- || (state.turnPhase !== "in-flight" && state.turnPhase !== "authorized")
307
- || !runtime.goalTurnInFlight) return false;
308
- if (!goalUpdateToolIsAvailable(pi)) {
309
- pauseGoalBeforeTurn(pi, runtime, ctx);
310
- return false;
311
- }
312
- return sendGoalTurn(pi, runtime, ctx, state);
313
- }
314
-
315
- /** Starts one goal turn only after Pi is idle and all competing workflow gates are clear. */
316
- export function scheduleGoalContinuation(
317
- pi: ExtensionAPI,
318
- runtime: GoalRuntime,
319
- ctx: ExtensionContext,
320
- guard?: { generation: number; state: GoalState },
321
- ): boolean {
322
- if (guard && (guard.generation !== runtime.lifecycleGeneration || runtime.state !== guard.state)) return false;
323
- if (isGoalModeSupported(ctx) && isSavedSession(ctx) && pauseGoalAtTurnLimit(pi, runtime, ctx)) return false;
324
- if (!isGoalModeSupported(ctx)
325
- || !isSavedSession(ctx)
326
- || runtime.state?.status !== "active"
327
- || runtime.continuationScheduled
328
- || runtime.continuationHeld
329
- || runtime.goalTurnInFlight
330
- || !ctx.isIdle()
331
- || ctx.hasPendingMessages()) return false;
332
-
333
- const current = runtime.state;
334
- const phase = current.turnPhase;
335
- const canStart = phase === "ready"
336
- || phase === "authorized"
337
- || phase === undefined && current.turns === 0;
338
- if (!canStart || phase === "authorized" && current.turnDecision === undefined) return false;
339
- if (!goalUpdateToolIsAvailable(pi)) {
340
- pauseGoalBeforeTurn(pi, runtime, ctx);
341
- return false;
342
- }
343
- const next = beginGoalTurn(pi, runtime, ctx, current);
344
- return next !== undefined && sendGoalTurn(pi, runtime, ctx, next as Extract<GoalState, { status: "active" }>);
345
- }
346
-
347
- function goalInstructions(state: GoalState, heading: string): string {
348
- const previous = state.lastDecision;
349
- const continuation = previous?.kind === "continue"
350
- ? [
351
- "The previous turn's accepted next action was model-reported (not independently verified):",
352
- previous.nextAction,
353
- ]
354
- : [];
355
- return [
356
- `# ${heading}`,
357
- `Status: active · Turn: ${state.turns}`,
358
- "Objective:",
359
- state.objective,
360
- "",
361
- "Treat the exact objective above from /goal as authoritative; a compaction summary may describe it but does not replace it.",
362
- "If the current context contains a compaction summary, take its first concrete next step after checking the current repository state.",
363
- ...continuation,
364
- "Continue making concrete progress toward this unchanged objective. Re-check repository state and prior results instead of repeating work.",
365
- ...(state.turnDecision === undefined
366
- ? [
367
- "Before ending this turn, choose exactly one accepted goal decision.",
368
- "After auditing and verifying the whole objective, call killeros_goal_update with status complete and concise evidence.",
369
- "If useful work remains, call killeros_goal_update with status continue, concrete evidence from this turn, and one concrete nextAction toward the unchanged objective.",
370
- "If one external impasse persists, call killeros_goal_update with status blocked, the same lowercase blockerKey, and current evidence; attempts one and two record audits and attempt three marks the goal blocked.",
371
- ]
372
- : ["This logical turn already has one accepted decision. Do not call killeros_goal_update again; finish the interrupted request, then let KillerOS start the authorized next turn after settlement."]),
373
- "A normal response without one accepted decision pauses the goal. Do not restate a previous result as progress.",
374
- "Goal evidence and nextAction are model-reported; KillerOS does not verify progress from prose or tool activity.",
375
- "Never use the goal tool to pause, resume, edit, replace, or clear the objective. Those transitions belong to the user.",
376
- ].join("\n");
377
- }
378
-
379
- function blockedGoalSystemPrompt(state: Extract<GoalState, { status: "blocked" }>): string {
380
- return [
381
- "# Blocked KillerOS goal",
382
- "Status: blocked",
383
- "Objective:",
384
- state.objective,
385
- "",
386
- "Treat the exact saved objective above as authoritative. Automatic goal continuation remains stopped; this ordinary request grants no goal turns.",
387
- "If killeros_goal_update is available, you may call it only with status complete and current evidence after auditing and verifying the whole unchanged objective.",
388
- "File proof checks only the persisted deliverable against its original baseline, not every acceptance criterion. General-objective completion is model-reported.",
389
- "The disappearance of a blocker alone does not prove completion. If useful work remains or this request is unrelated, finish normally without a goal update. No decision is mandatory.",
390
- "Never use the goal tool to continue, audit a blocker, pause, resume, edit, replace, or clear this goal. User controls remain unchanged.",
391
- ].join("\n");
392
- }
393
-
394
- function goalSystemPrompt(state: GoalState): string {
395
- return goalInstructions(state, "Active KillerOS goal");
396
- }
397
-
398
- function goalContinuationMessage(state: GoalState): string {
399
- return goalInstructions(state, "KillerOS long-running goal turn");
400
- }
401
-
402
- export function isGoalModeSupported(ctx: ExtensionContext): boolean {
403
- return ctx.mode === "tui" || ctx.mode === "rpc";
404
- }
405
-
406
- export function isSavedSession(ctx: ExtensionContext): boolean {
407
- try {
408
- return Boolean(ctx.sessionManager.getSessionFile());
409
- } catch {
410
- return false;
411
- }
412
- }
413
-
414
- export function registerGoalRuntime(
415
- pi: ExtensionAPI,
416
- runtime: GoalRuntime,
417
- ): void {
418
- const restoreGoal = (ctx: ExtensionContext): void => {
419
- runtime.lifecycleGeneration += 1;
420
- const generation = runtime.lifecycleGeneration;
421
- const restored = restoreGoalState(ctx);
422
- runtime.state = isGoalModeSupported(ctx) ? restored.state : undefined;
423
- runtime.blockedCompletion = undefined;
424
- syncGoalUpdateTool(pi, runtime);
425
- runtime.continuationScheduled = false;
426
- runtime.continuationHeld = undefined;
427
- runtime.goalTurnInFlight = false;
428
- runtime.goalTurn = undefined;
429
- runtime.agentEndObserved = false;
430
- runtime.automaticCompaction = undefined;
431
- runtime.persistenceRetryNeeded = false;
432
- runtime.lastStopReason = undefined;
433
- runtime.lastError = undefined;
434
- runtime.requestRender?.();
435
- const state = runtime.state;
436
- if (state?.status !== "active") return;
437
- if ((state.turnPhase === "in-flight") || (state.turnPhase === undefined && state.turns > 0)) {
438
- pauseGoalAfterFailure(pi, runtime, ctx, "no turn decision");
439
- return;
440
- }
441
- setImmediate(() => {
442
- if (runtime.lifecycleGeneration !== generation || runtime.state !== state) return;
443
- scheduleGoalContinuation(pi, runtime, ctx, { generation, state });
444
- });
445
- };
446
-
447
- pi.on("session_start", (_event, ctx) => restoreGoal(ctx));
448
- pi.on("session_tree", (_event, ctx) => restoreGoal(ctx));
449
-
450
- pi.on("session_shutdown", (_event, ctx) => {
451
- runtime.lifecycleGeneration += 1;
452
- runtime.blockedCompletion = undefined;
453
- if (runtime.state?.status === "active") {
454
- const checkpoint = checkpointActiveGoalState(runtime.state, Date.now());
455
- try {
456
- persistGoalState(pi, runtime, "checkpoint", checkpoint);
457
- } catch (error) {
458
- reportError(ctx, "Goal state could not be checkpointed", error);
459
- }
460
- }
461
- runtime.state = undefined;
462
- syncGoalUpdateTool(pi, runtime);
463
- clearGoalExecutionFlags(runtime);
464
- runtime.continuationHeld = undefined;
465
- runtime.persistenceRetryNeeded = false;
466
- });
467
-
468
- pi.on("before_agent_start", (event, ctx) => {
469
- runtime.continuationScheduled = false;
470
- if (!runtime.goalTurnInFlight && isGoalModeSupported(ctx) && isSavedSession(ctx) && pauseGoalAtTurnLimit(pi, runtime, ctx)) return;
471
- const current = runtime.state;
472
- if (!isGoalModeSupported(ctx) || !isSavedSession(ctx) || !current) return;
473
- if (current.status === "blocked") {
474
- runtime.blockedCompletion ??= { kind: "eligible", state: current, generation: runtime.lifecycleGeneration };
475
- syncGoalUpdateTool(pi, runtime);
476
- return { systemPrompt: `${event.systemPrompt}\n\n${blockedGoalSystemPrompt(current)}` };
477
- }
478
- if (current.status !== "active") return;
479
- if (runtime.goalTurnInFlight) {
480
- if (!goalUpdateToolIsAvailable(pi)) {
481
- pauseGoalBeforeTurn(pi, runtime, ctx);
482
- try {
483
- ctx.abort();
484
- } catch {
485
- // The state is already paused; a host abort may be unavailable during startup.
486
- }
487
- return;
488
- }
489
- return { systemPrompt: `${event.systemPrompt}\n\n${goalSystemPrompt(current)}` };
490
- }
491
- if (current.turnPhase === "in-flight") {
492
- pauseGoalAfterFailure(pi, runtime, ctx, "no turn decision");
493
- return;
494
- }
495
- if (!goalUpdateToolIsAvailable(pi)) {
496
- pauseGoalBeforeTurn(pi, runtime, ctx);
497
- try {
498
- ctx.abort();
499
- } catch {
500
- // See the in-flight branch above.
501
- }
502
- return;
503
- }
504
- const next = beginGoalTurn(pi, runtime, ctx, current);
505
- if (!next) return;
506
- return { systemPrompt: `${event.systemPrompt}\n\n${goalSystemPrompt(next)}` };
507
- });
508
-
509
- pi.on("agent_end", (event) => {
510
- if (!runtime.goalTurnInFlight) return;
511
- const finalAssistant = [...event.messages].reverse().find((message) => message.role === "assistant");
512
- runtime.agentEndObserved = finalAssistant !== undefined;
513
- runtime.lastStopReason = finalAssistant?.stopReason;
514
- runtime.lastError = finalAssistant?.errorMessage;
515
- });
516
- }
1
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { reportError } from "./errors.ts";
3
+ import {
4
+ beginGoalTurnState,
5
+ boundGoalText,
6
+ checkpointActiveGoalState,
7
+ GOAL_VERSION,
8
+ parseGoalState,
9
+ pauseGoalState,
10
+ transitionGoalState,
11
+ type GoalTransitionOptions,
12
+ } from "./goal-state.ts";
13
+ import type { GoalRuntime, GoalState, GoalStatus } from "./runtime.ts";
14
+ import { safeTerminalText } from "./safe-terminal-text.ts";
15
+
16
+ export const GOAL_ENTRY_TYPE = "killeros-goal";
17
+ export const GOAL_CONTINUATION_TYPE = "killeros-goal-continuation";
18
+ export const GOAL_UPDATE_TOOL = "killeros_goal_update";
19
+
20
+ export type GoalEntryEvent = "set" | "replace" | "edit" | "limit" | "turn" | "continue" | "pause" | "resume" | "blocked" | "complete" | "error" | "clear" | "checkpoint" | "blocker-audit";
21
+ export interface GoalEntryData {
22
+ version: 1;
23
+ event: GoalEntryEvent;
24
+ state: GoalState | null;
25
+ }
26
+
27
+ function isUnknownRecord(value: unknown): value is Record<string, unknown> {
28
+ return typeof value === "object" && value !== null && !Array.isArray(value);
29
+ }
30
+
31
+ interface RestoredGoalState {
32
+ state?: GoalState;
33
+ }
34
+
35
+ export function goalBranchEntries(ctx: ExtensionContext): ReturnType<ExtensionContext["sessionManager"]["getEntries"]> {
36
+ try {
37
+ return ctx.sessionManager.getBranch();
38
+ } catch {
39
+ return [];
40
+ }
41
+ }
42
+
43
+ function restoreGoalState(ctx: ExtensionContext): RestoredGoalState {
44
+ const entries = goalBranchEntries(ctx);
45
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
46
+ const entry = entries[index];
47
+ if (entry?.type !== "custom" || entry.customType !== GOAL_ENTRY_TYPE) continue;
48
+ const data: unknown = entry.data;
49
+ if (!isUnknownRecord(data) || data.version !== GOAL_VERSION || data.state === null) {
50
+ return { state: undefined };
51
+ }
52
+ // v2.0.18 shutdown checkpoints stopped active clocks by omitting activeStartedAt.
53
+ const savedState = data.event === "checkpoint"
54
+ && isUnknownRecord(data.state)
55
+ && data.state.status === "active"
56
+ && data.state.activeStartedAt === undefined
57
+ ? { ...data.state, activeStartedAt: Date.now() }
58
+ : data.state;
59
+ const restored = parseGoalState(savedState);
60
+ if (!restored) return { state: undefined };
61
+ if (restored.status === "active") {
62
+ return { state: { ...restored, activeStartedAt: Date.now() } };
63
+ }
64
+ if (restored.status === "paused") {
65
+ const { resumeAfterManualCompaction: _resume, ...state } = restored;
66
+ return { state };
67
+ }
68
+ return { state: restored };
69
+ }
70
+ return { state: undefined };
71
+ }
72
+
73
+ export function sumGoalTokens(ctx: ExtensionContext): number {
74
+ let total = 0;
75
+ for (const entry of goalBranchEntries(ctx)) {
76
+ if (entry.type === "message" && (entry.message.role === "assistant" || entry.message.role === "toolResult")) {
77
+ total += entry.message.usage?.totalTokens ?? 0;
78
+ } else if ((entry.type === "compaction" || entry.type === "branch_summary") && entry.usage) {
79
+ total += entry.usage.totalTokens;
80
+ }
81
+ }
82
+ return total;
83
+ }
84
+
85
+ function setGoalUpdateToolActive(pi: ExtensionAPI, active: boolean): void {
86
+ try {
87
+ const activeTools = pi.getActiveTools();
88
+ const isActive = activeTools.includes(GOAL_UPDATE_TOOL);
89
+ if (active === isActive) return;
90
+ pi.setActiveTools(active
91
+ ? [...activeTools, GOAL_UPDATE_TOOL]
92
+ : activeTools.filter((name) => name !== GOAL_UPDATE_TOOL));
93
+ } catch {
94
+ // Availability is checked again immediately before a goal request.
95
+ }
96
+ }
97
+
98
+ export function syncGoalUpdateTool(pi: ExtensionAPI, runtime: GoalRuntime): void {
99
+ const permission = runtime.blockedCompletion;
100
+ setGoalUpdateToolActive(pi, runtime.state?.status === "active"
101
+ || permission?.kind === "eligible"
102
+ && permission.state === runtime.state
103
+ && permission.generation === runtime.lifecycleGeneration);
104
+ }
105
+
106
+ function goalUpdateToolIsAvailable(pi: ExtensionAPI): boolean {
107
+ try {
108
+ const active = pi.getActiveTools();
109
+ if (!active.includes(GOAL_UPDATE_TOOL)) pi.setActiveTools([...active, GOAL_UPDATE_TOOL]);
110
+ return pi.getActiveTools().includes(GOAL_UPDATE_TOOL);
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ export function persistGoalState(
117
+ pi: ExtensionAPI,
118
+ runtime: GoalRuntime,
119
+ event: GoalEntryEvent,
120
+ state: GoalState | undefined,
121
+ ): void {
122
+ const data: GoalEntryData = { version: GOAL_VERSION, event, state: state ?? null };
123
+ pi.appendEntry(GOAL_ENTRY_TYPE, data);
124
+ runtime.state = state;
125
+ // Accepted progress must not invalidate an edit waiting for the current turn.
126
+ if (event !== "continue" && event !== "blocker-audit") runtime.mutation = Symbol();
127
+ runtime.blockedCompletion = undefined;
128
+ if (event === "clear") {
129
+ runtime.lifecycleGeneration += 1;
130
+ runtime.continuationHeld = undefined;
131
+ }
132
+ syncGoalUpdateTool(pi, runtime);
133
+ runtime.persistenceRetryNeeded = false;
134
+ runtime.requestRender?.();
135
+ }
136
+
137
+ export function transitionGoal(
138
+ pi: ExtensionAPI,
139
+ runtime: GoalRuntime,
140
+ event: GoalEntryEvent,
141
+ status: GoalStatus,
142
+ result?: string,
143
+ options: GoalTransitionOptions = {},
144
+ ): GoalState {
145
+ const current = runtime.state;
146
+ if (!current) throw new Error("No goal is set");
147
+ const next = transitionGoalState(current, status, result, options, Date.now());
148
+ persistGoalState(pi, runtime, event, next);
149
+ if (status !== "active") {
150
+ runtime.continuationScheduled = false;
151
+ runtime.automaticCompaction = undefined;
152
+ if (!options.keepTurnForRecovery) {
153
+ runtime.goalTurn = undefined;
154
+ runtime.goalTurnInFlight = false;
155
+ runtime.agentEndObserved = false;
156
+ }
157
+ } else if (!options.resumeInterruptedTurn) {
158
+ runtime.goalTurn = undefined;
159
+ runtime.goalTurnInFlight = false;
160
+ runtime.agentEndObserved = false;
161
+ }
162
+ return next;
163
+ }
164
+
165
+ export function pauseGoalAtTurnLimit(
166
+ pi: ExtensionAPI,
167
+ runtime: GoalRuntime,
168
+ ctx: ExtensionContext,
169
+ ): boolean {
170
+ const state = runtime.state;
171
+ if (state?.status !== "active" || state.maxTurns === undefined || state.turns < state.maxTurns) return false;
172
+ const result = `Turn limit reached (${state.turns}/${state.maxTurns}).`;
173
+ try {
174
+ transitionGoal(pi, runtime, "limit", "paused", result);
175
+ } catch (error) {
176
+ pauseGoalAfterFailure(pi, runtime, ctx, `turn limit pause could not be saved: ${error instanceof Error ? error.message : String(error)}`);
177
+ }
178
+ return true;
179
+ }
180
+
181
+ function clearGoalExecutionFlags(runtime: GoalRuntime): void {
182
+ runtime.continuationScheduled = false;
183
+ runtime.goalTurnInFlight = false;
184
+ runtime.agentEndObserved = false;
185
+ runtime.goalTurn = undefined;
186
+ runtime.automaticCompaction = undefined;
187
+ runtime.lastStopReason = undefined;
188
+ runtime.lastError = undefined;
189
+ }
190
+
191
+ export async function stopGoalRun(runtime: GoalRuntime, ctx: ExtensionCommandContext, shouldStop: boolean): Promise<void> {
192
+ if (!shouldStop) return;
193
+ try {
194
+ ctx.abort();
195
+ await ctx.waitForIdle();
196
+ } finally {
197
+ clearGoalExecutionFlags(runtime);
198
+ }
199
+ }
200
+
201
+ function safeFailureReason(reason: string): string {
202
+ return boundGoalText(reason) || "an unspecified goal failure";
203
+ }
204
+
205
+ export function pauseGoalAfterFailure(
206
+ pi: ExtensionAPI,
207
+ runtime: GoalRuntime,
208
+ ctx: ExtensionContext,
209
+ reason: string,
210
+ recoveryInstruction = "Run /goal resume after resolving the problem.",
211
+ notify = true,
212
+ ): void {
213
+ if (runtime.state?.status !== "active") return;
214
+ const safeReason = safeFailureReason(reason);
215
+ const safeRecoveryInstruction = safeReason === "no turn decision"
216
+ && recoveryInstruction === "Run /goal resume after resolving the problem."
217
+ ? "The agent ended without choosing continue, complete, or blocked. Run /goal resume only after choosing to continue."
218
+ : safeTerminalText(recoveryInstruction);
219
+ try {
220
+ transitionGoal(pi, runtime, "error", "paused", safeReason);
221
+ } catch {
222
+ const current = runtime.state;
223
+ runtime.state = current ? pauseGoalState(current, safeReason, Date.now()) : undefined;
224
+ syncGoalUpdateTool(pi, runtime);
225
+ runtime.persistenceRetryNeeded = true;
226
+ clearGoalExecutionFlags(runtime);
227
+ runtime.requestRender?.();
228
+ }
229
+ if (notify) ctx.ui.notify(`Goal paused: ${safeReason}\n${safeRecoveryInstruction}`, "error");
230
+ }
231
+
232
+ function pauseGoalBeforeTurn(
233
+ pi: ExtensionAPI,
234
+ runtime: GoalRuntime,
235
+ ctx: ExtensionContext,
236
+ ): void {
237
+ const state = runtime.state;
238
+ if (state?.status !== "active") return;
239
+ const nextTurn = state.turns + 1;
240
+ const reason = "killeros_goal_update is unavailable";
241
+ try {
242
+ transitionGoal(pi, runtime, "error", "paused", reason);
243
+ } catch {
244
+ runtime.state = pauseGoalState(state, reason, Date.now());
245
+ syncGoalUpdateTool(pi, runtime);
246
+ runtime.persistenceRetryNeeded = true;
247
+ clearGoalExecutionFlags(runtime);
248
+ runtime.requestRender?.();
249
+ }
250
+ ctx.ui.notify(
251
+ `Goal paused before turn ${nextTurn}: ${reason}\nEnable the goal tool, then run /goal resume.`,
252
+ "error",
253
+ );
254
+ }
255
+
256
+ function beginGoalTurn(
257
+ pi: ExtensionAPI,
258
+ runtime: GoalRuntime,
259
+ ctx: ExtensionContext,
260
+ current: Extract<GoalState, { status: "active" }>,
261
+ ): GoalState | undefined {
262
+ let next: GoalState;
263
+ try {
264
+ next = beginGoalTurnState(current, Date.now());
265
+ persistGoalState(pi, runtime, "turn", next);
266
+ } catch (error) {
267
+ pauseGoalAfterFailure(pi, runtime, ctx, `turn state could not be saved: ${error instanceof Error ? error.message : String(error)}`);
268
+ return undefined;
269
+ }
270
+ runtime.goalTurn = { revision: next.revision, turn: next.turns };
271
+ runtime.goalTurnInFlight = true;
272
+ runtime.agentEndObserved = false;
273
+ runtime.lastStopReason = undefined;
274
+ runtime.lastError = undefined;
275
+ return next;
276
+ }
277
+
278
+ function sendGoalTurn(
279
+ pi: ExtensionAPI,
280
+ runtime: GoalRuntime,
281
+ ctx: ExtensionContext,
282
+ state: Extract<GoalState, { status: "active" }>,
283
+ ): boolean {
284
+ runtime.continuationScheduled = true;
285
+ try {
286
+ pi.sendMessage({
287
+ customType: GOAL_CONTINUATION_TYPE,
288
+ content: goalContinuationMessage(state),
289
+ display: false,
290
+ }, { triggerTurn: true, deliverAs: "followUp" });
291
+ return true;
292
+ } catch (error) {
293
+ runtime.continuationScheduled = false;
294
+ runtime.goalTurnInFlight = false;
295
+ runtime.goalTurn = undefined;
296
+ pauseGoalAfterFailure(pi, runtime, ctx, `continuation could not start: ${error instanceof Error ? error.message : String(error)}`);
297
+ return false;
298
+ }
299
+ }
300
+
301
+ export function resumeInterruptedGoalTurn(
302
+ pi: ExtensionAPI,
303
+ runtime: GoalRuntime,
304
+ ctx: ExtensionContext,
305
+ ): boolean {
306
+ const state = runtime.state;
307
+ if (state?.status !== "active"
308
+ || (state.turnPhase !== "in-flight" && state.turnPhase !== "authorized")
309
+ || !runtime.goalTurnInFlight) return false;
310
+ if (!goalUpdateToolIsAvailable(pi)) {
311
+ pauseGoalBeforeTurn(pi, runtime, ctx);
312
+ return false;
313
+ }
314
+ return sendGoalTurn(pi, runtime, ctx, state);
315
+ }
316
+
317
+ /** Starts one goal turn only after Pi is idle and all competing workflow gates are clear. */
318
+ export function scheduleGoalContinuation(
319
+ pi: ExtensionAPI,
320
+ runtime: GoalRuntime,
321
+ ctx: ExtensionContext,
322
+ guard?: { generation: number; state: GoalState },
323
+ ): boolean {
324
+ if (guard && (guard.generation !== runtime.lifecycleGeneration || runtime.state !== guard.state)) return false;
325
+ if (isGoalModeSupported(ctx) && isSavedSession(ctx) && pauseGoalAtTurnLimit(pi, runtime, ctx)) return false;
326
+ if (!isGoalModeSupported(ctx)
327
+ || !isSavedSession(ctx)
328
+ || runtime.state?.status !== "active"
329
+ || runtime.continuationScheduled
330
+ || runtime.continuationHeld
331
+ || runtime.goalTurnInFlight
332
+ || !ctx.isIdle()
333
+ || ctx.hasPendingMessages()) return false;
334
+
335
+ const current = runtime.state;
336
+ const phase = current.turnPhase;
337
+ const canStart = phase === "ready"
338
+ || phase === "authorized"
339
+ || phase === undefined && current.turns === 0;
340
+ if (!canStart || phase === "authorized" && current.turnDecision === undefined) return false;
341
+ if (!goalUpdateToolIsAvailable(pi)) {
342
+ pauseGoalBeforeTurn(pi, runtime, ctx);
343
+ return false;
344
+ }
345
+ const next = beginGoalTurn(pi, runtime, ctx, current);
346
+ return next !== undefined && sendGoalTurn(pi, runtime, ctx, next as Extract<GoalState, { status: "active" }>);
347
+ }
348
+
349
+ function goalInstructions(state: GoalState, heading: string): string {
350
+ const previous = state.lastDecision;
351
+ const continuation = previous?.kind === "continue"
352
+ ? [
353
+ "The previous turn's accepted next action was model-reported (not independently verified):",
354
+ previous.nextAction,
355
+ ]
356
+ : [];
357
+ return [
358
+ `# ${heading}`,
359
+ `Status: active · Turn: ${state.turns}`,
360
+ "Objective:",
361
+ state.objective,
362
+ "",
363
+ "Treat the exact objective above from /goal as authoritative; a compaction summary may describe it but does not replace it.",
364
+ "If earlier messages contain a different objective, the objective above replaces them.",
365
+ "If the current context contains a compaction summary, take its first concrete next step after checking the current repository state.",
366
+ ...continuation,
367
+ "Continue making concrete progress toward this unchanged objective. Re-check repository state and prior results instead of repeating work.",
368
+ ...(state.turnDecision === undefined
369
+ ? [
370
+ "Before ending this turn, choose exactly one accepted goal decision.",
371
+ "After auditing and verifying the whole objective, call killeros_goal_update with status complete and concise evidence.",
372
+ "If useful work remains, call killeros_goal_update with status continue, concrete evidence from this turn, and one concrete nextAction toward the unchanged objective.",
373
+ "If one external impasse persists, call killeros_goal_update with status blocked, the same lowercase blockerKey, and current evidence; attempts one and two record audits and attempt three marks the goal blocked.",
374
+ ]
375
+ : ["This logical turn already has one accepted decision. Do not call killeros_goal_update again; finish the interrupted request, then let KillerOS start the authorized next turn after settlement."]),
376
+ "A normal response without one accepted decision pauses the goal. Do not restate a previous result as progress.",
377
+ "Goal evidence and nextAction are model-reported; KillerOS does not verify progress from prose or tool activity.",
378
+ "Never use the goal tool to pause, resume, edit, replace, or clear the objective. Those transitions belong to the user.",
379
+ ].join("\n");
380
+ }
381
+
382
+ function blockedGoalSystemPrompt(state: Extract<GoalState, { status: "blocked" }>): string {
383
+ return [
384
+ "# Blocked KillerOS goal",
385
+ "Status: blocked",
386
+ "Objective:",
387
+ state.objective,
388
+ "",
389
+ "Treat the exact saved objective above as authoritative. Automatic goal continuation remains stopped; this ordinary request grants no goal turns.",
390
+ "If killeros_goal_update is available, you may call it only with status complete and current evidence after auditing and verifying the whole unchanged objective.",
391
+ "File proof checks only the persisted deliverable against its original baseline, not every acceptance criterion. General-objective completion is model-reported.",
392
+ "The disappearance of a blocker alone does not prove completion. If useful work remains or this request is unrelated, finish normally without a goal update. No decision is mandatory.",
393
+ "Never use the goal tool to continue, audit a blocker, pause, resume, edit, replace, or clear this goal. User controls remain unchanged.",
394
+ ].join("\n");
395
+ }
396
+
397
+ function goalSystemPrompt(state: GoalState): string {
398
+ return goalInstructions(state, "Active KillerOS goal");
399
+ }
400
+
401
+ function goalContinuationMessage(state: GoalState): string {
402
+ return goalInstructions(state, "KillerOS long-running goal turn");
403
+ }
404
+
405
+ export function isGoalModeSupported(ctx: ExtensionContext): boolean {
406
+ return ctx.mode === "tui" || ctx.mode === "rpc";
407
+ }
408
+
409
+ export function isSavedSession(ctx: ExtensionContext): boolean {
410
+ try {
411
+ return Boolean(ctx.sessionManager.getSessionFile());
412
+ } catch {
413
+ return false;
414
+ }
415
+ }
416
+
417
+ export function registerGoalRuntime(
418
+ pi: ExtensionAPI,
419
+ runtime: GoalRuntime,
420
+ ): void {
421
+ const restoreGoal = (ctx: ExtensionContext): void => {
422
+ runtime.lifecycleGeneration += 1;
423
+ const generation = runtime.lifecycleGeneration;
424
+ const restored = restoreGoalState(ctx);
425
+ runtime.state = isGoalModeSupported(ctx) ? restored.state : undefined;
426
+ runtime.blockedCompletion = undefined;
427
+ syncGoalUpdateTool(pi, runtime);
428
+ runtime.continuationScheduled = false;
429
+ runtime.continuationHeld = undefined;
430
+ runtime.goalTurnInFlight = false;
431
+ runtime.goalTurn = undefined;
432
+ runtime.agentEndObserved = false;
433
+ runtime.automaticCompaction = undefined;
434
+ runtime.persistenceRetryNeeded = false;
435
+ runtime.lastStopReason = undefined;
436
+ runtime.lastError = undefined;
437
+ runtime.requestRender?.();
438
+ const state = runtime.state;
439
+ if (state?.status !== "active") return;
440
+ if ((state.turnPhase === "in-flight") || (state.turnPhase === undefined && state.turns > 0)) {
441
+ pauseGoalAfterFailure(pi, runtime, ctx, "no turn decision");
442
+ return;
443
+ }
444
+ setImmediate(() => {
445
+ if (runtime.lifecycleGeneration !== generation || runtime.state !== state) return;
446
+ scheduleGoalContinuation(pi, runtime, ctx, { generation, state });
447
+ });
448
+ };
449
+
450
+ pi.on("session_start", (_event, ctx) => restoreGoal(ctx));
451
+ pi.on("session_tree", (_event, ctx) => restoreGoal(ctx));
452
+
453
+ pi.on("session_shutdown", (_event, ctx) => {
454
+ runtime.lifecycleGeneration += 1;
455
+ runtime.blockedCompletion = undefined;
456
+ if (runtime.state?.status === "active") {
457
+ const checkpoint = checkpointActiveGoalState(runtime.state, Date.now());
458
+ try {
459
+ persistGoalState(pi, runtime, "checkpoint", checkpoint);
460
+ } catch (error) {
461
+ reportError(ctx, "Goal state could not be checkpointed", error);
462
+ }
463
+ }
464
+ runtime.state = undefined;
465
+ syncGoalUpdateTool(pi, runtime);
466
+ clearGoalExecutionFlags(runtime);
467
+ runtime.continuationHeld = undefined;
468
+ runtime.persistenceRetryNeeded = false;
469
+ });
470
+
471
+ pi.on("before_agent_start", (event, ctx) => {
472
+ runtime.continuationScheduled = false;
473
+ if (!runtime.goalTurnInFlight && isGoalModeSupported(ctx) && isSavedSession(ctx) && pauseGoalAtTurnLimit(pi, runtime, ctx)) return;
474
+ const current = runtime.state;
475
+ if (!isGoalModeSupported(ctx) || !isSavedSession(ctx) || !current) return;
476
+ if (current.status === "blocked") {
477
+ runtime.blockedCompletion ??= { kind: "eligible", state: current, generation: runtime.lifecycleGeneration };
478
+ syncGoalUpdateTool(pi, runtime);
479
+ return { systemPrompt: `${event.systemPrompt}\n\n${blockedGoalSystemPrompt(current)}` };
480
+ }
481
+ if (current.status !== "active") return;
482
+ if (runtime.goalTurnInFlight) {
483
+ if (!goalUpdateToolIsAvailable(pi)) {
484
+ pauseGoalBeforeTurn(pi, runtime, ctx);
485
+ try {
486
+ ctx.abort();
487
+ } catch {
488
+ // The state is already paused; a host abort may be unavailable during startup.
489
+ }
490
+ return;
491
+ }
492
+ return { systemPrompt: `${event.systemPrompt}\n\n${goalSystemPrompt(current)}` };
493
+ }
494
+ if (current.turnPhase === "in-flight") {
495
+ pauseGoalAfterFailure(pi, runtime, ctx, "no turn decision");
496
+ return;
497
+ }
498
+ if (!goalUpdateToolIsAvailable(pi)) {
499
+ pauseGoalBeforeTurn(pi, runtime, ctx);
500
+ try {
501
+ ctx.abort();
502
+ } catch {
503
+ // See the in-flight branch above.
504
+ }
505
+ return;
506
+ }
507
+ const next = beginGoalTurn(pi, runtime, ctx, current);
508
+ if (!next) return;
509
+ return { systemPrompt: `${event.systemPrompt}\n\n${goalSystemPrompt(next)}` };
510
+ });
511
+
512
+ pi.on("agent_end", (event) => {
513
+ if (!runtime.goalTurnInFlight) return;
514
+ const finalAssistant = [...event.messages].reverse().find((message) => message.role === "assistant");
515
+ runtime.agentEndObserved = finalAssistant !== undefined;
516
+ runtime.lastStopReason = finalAssistant?.stopReason;
517
+ runtime.lastError = finalAssistant?.errorMessage;
518
+ });
519
+ }