killeros 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/Killeros.ts +17 -10
- package/README.md +21 -13
- package/killeros/commands.ts +2 -8
- package/killeros/concise.ts +12 -8
- package/killeros/footer.ts +46 -1
- package/killeros/goals.ts +118 -57
- package/killeros/hooks.ts +49 -17
- package/killeros/init-evidence.ts +240 -0
- package/killeros/init-target.ts +289 -0
- package/killeros/init.ts +139 -356
- package/killeros/notifications.ts +167 -0
- package/killeros/question.ts +16 -1
- package/killeros/runtime.ts +19 -31
- package/killeros/shell-ui.ts +18 -141
- package/package.json +2 -2
- package/killeros/context-compaction.ts +0 -614
package/killeros/goals.ts
CHANGED
|
@@ -3,11 +3,10 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { MAX_NODE_TIMER_MS } from "./limits.ts";
|
|
5
5
|
import { BoundedText } from "./bounded-text.ts";
|
|
6
|
-
import { CONCISE_SYSTEM_PROMPT } from "./concise.ts";
|
|
7
6
|
import { formatTime, formatTokens } from "./display.ts";
|
|
8
7
|
import { reportError } from "./errors.ts";
|
|
9
8
|
import { resolvePersonalInstructions } from "./personal-instructions.ts";
|
|
10
|
-
import type {
|
|
9
|
+
import type { GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
|
|
11
10
|
|
|
12
11
|
const GOAL_ENTRY_TYPE = "killeros-goal";
|
|
13
12
|
const GOAL_CONTINUATION_TYPE = "killeros-goal-continuation";
|
|
@@ -21,6 +20,16 @@ interface GoalEntryData {
|
|
|
21
20
|
state: GoalState | null;
|
|
22
21
|
}
|
|
23
22
|
|
|
23
|
+
interface GoalTransitionOptions {
|
|
24
|
+
resetBlockedAudit?: boolean;
|
|
25
|
+
resumeAfterManualCompaction?: true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface RestoredGoalState {
|
|
29
|
+
state?: GoalState;
|
|
30
|
+
recoveryProven: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
const GoalUpdateParams = Type.Object({
|
|
25
34
|
status: Type.Union([Type.Literal("complete"), Type.Literal("blocked")], {
|
|
26
35
|
description: "Mark the active goal complete or blocked",
|
|
@@ -61,7 +70,9 @@ function parseGoalState(value: unknown): GoalState | undefined {
|
|
|
61
70
|
&& (!Number.isInteger(candidate.blockedAuditStartTurn) || candidate.blockedAuditStartTurn < 0 || candidate.blockedAuditStartTurn > candidate.turns!)
|
|
62
71
|
|| !finiteNonNegative(candidate.baselineTokens)
|
|
63
72
|
|| candidate.activeStartedAt !== undefined && !finiteNonNegative(candidate.activeStartedAt)
|
|
64
|
-
|| candidate.result !== undefined && typeof candidate.result !== "string"
|
|
73
|
+
|| candidate.result !== undefined && typeof candidate.result !== "string"
|
|
74
|
+
|| candidate.resumeAfterManualCompaction !== undefined && candidate.resumeAfterManualCompaction !== true
|
|
75
|
+
|| candidate.resumeAfterManualCompaction === true && candidate.status !== "paused") {
|
|
65
76
|
return undefined;
|
|
66
77
|
}
|
|
67
78
|
return {
|
|
@@ -77,6 +88,7 @@ function parseGoalState(value: unknown): GoalState | undefined {
|
|
|
77
88
|
blockedAuditStartTurn: candidate.blockedAuditStartTurn ?? 0,
|
|
78
89
|
baselineTokens: candidate.baselineTokens,
|
|
79
90
|
result: candidate.result,
|
|
91
|
+
resumeAfterManualCompaction: candidate.resumeAfterManualCompaction,
|
|
80
92
|
};
|
|
81
93
|
}
|
|
82
94
|
|
|
@@ -88,21 +100,26 @@ function goalBranchEntries(ctx: ExtensionContext): ReturnType<ExtensionContext["
|
|
|
88
100
|
}
|
|
89
101
|
}
|
|
90
102
|
|
|
91
|
-
function restoreGoalState(ctx: ExtensionContext):
|
|
103
|
+
function restoreGoalState(ctx: ExtensionContext): RestoredGoalState {
|
|
92
104
|
const entries = goalBranchEntries(ctx);
|
|
93
105
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
94
106
|
const entry = entries[index];
|
|
95
107
|
if (entry?.type !== "custom" || entry.customType !== GOAL_ENTRY_TYPE) continue;
|
|
96
108
|
const data = entry.data as Partial<GoalEntryData> | undefined;
|
|
97
|
-
if (!data || data.version !== GOAL_VERSION)
|
|
98
|
-
|
|
109
|
+
if (!data || data.version !== GOAL_VERSION || data.state === null) {
|
|
110
|
+
return { state: undefined, recoveryProven: false };
|
|
111
|
+
}
|
|
99
112
|
const restored = parseGoalState(data.state);
|
|
100
|
-
if (!restored) return undefined;
|
|
101
|
-
|
|
113
|
+
if (!restored) return { state: undefined, recoveryProven: false };
|
|
114
|
+
const state = restored.status === "active"
|
|
102
115
|
? { ...restored, activeStartedAt: Date.now() }
|
|
103
116
|
: { ...restored, activeStartedAt: undefined };
|
|
117
|
+
const recoveryProven = state.status === "paused"
|
|
118
|
+
&& state.resumeAfterManualCompaction === true
|
|
119
|
+
&& entries.slice(index + 1).some((candidate) => candidate.type === "compaction");
|
|
120
|
+
return { state, recoveryProven };
|
|
104
121
|
}
|
|
105
|
-
return undefined;
|
|
122
|
+
return { state: undefined, recoveryProven: false };
|
|
106
123
|
}
|
|
107
124
|
|
|
108
125
|
export function goalElapsedMilliseconds(state: GoalState, now = Date.now()): number {
|
|
@@ -152,7 +169,7 @@ function transitionGoal(
|
|
|
152
169
|
event: GoalEntryEvent,
|
|
153
170
|
status: GoalStatus,
|
|
154
171
|
result?: string,
|
|
155
|
-
|
|
172
|
+
options: GoalTransitionOptions = {},
|
|
156
173
|
): GoalState {
|
|
157
174
|
const current = runtime.state;
|
|
158
175
|
if (!current) throw new Error("No goal is set");
|
|
@@ -164,8 +181,9 @@ function transitionGoal(
|
|
|
164
181
|
status,
|
|
165
182
|
updatedAt: now,
|
|
166
183
|
activeStartedAt: status === "active" ? now : undefined,
|
|
167
|
-
blockedAuditStartTurn: resetBlockedAudit ? stopped.turns : stopped.blockedAuditStartTurn,
|
|
184
|
+
blockedAuditStartTurn: options.resetBlockedAudit ? stopped.turns : stopped.blockedAuditStartTurn,
|
|
168
185
|
result,
|
|
186
|
+
resumeAfterManualCompaction: options.resumeAfterManualCompaction,
|
|
169
187
|
};
|
|
170
188
|
persistGoalState(pi, runtime, event, next);
|
|
171
189
|
if (status !== "active") runtime.continuationScheduled = false;
|
|
@@ -209,7 +227,12 @@ export function pauseGoalAfterFailure(
|
|
|
209
227
|
try {
|
|
210
228
|
transitionGoal(pi, runtime, "error", "paused", reason);
|
|
211
229
|
} catch {
|
|
212
|
-
runtime.state = runtime.state ? {
|
|
230
|
+
runtime.state = runtime.state ? {
|
|
231
|
+
...stopGoalClock(runtime.state, Date.now()),
|
|
232
|
+
status: "paused",
|
|
233
|
+
result: reason,
|
|
234
|
+
resumeAfterManualCompaction: undefined,
|
|
235
|
+
} : undefined;
|
|
213
236
|
runtime.persistenceRetryNeeded = true;
|
|
214
237
|
runtime.continuationScheduled = false;
|
|
215
238
|
runtime.requestRender?.();
|
|
@@ -217,6 +240,56 @@ export function pauseGoalAfterFailure(
|
|
|
217
240
|
ctx.ui.notify(`Goal paused: ${reason}\n${recoveryInstruction}`, "error");
|
|
218
241
|
}
|
|
219
242
|
|
|
243
|
+
function pauseGoalForPossibleManualCompaction(
|
|
244
|
+
pi: ExtensionAPI,
|
|
245
|
+
runtime: GoalRuntime,
|
|
246
|
+
ctx: ExtensionContext,
|
|
247
|
+
reason: string,
|
|
248
|
+
): void {
|
|
249
|
+
if (runtime.state?.status !== "active") return;
|
|
250
|
+
try {
|
|
251
|
+
transitionGoal(pi, runtime, "error", "paused", reason, {
|
|
252
|
+
resumeAfterManualCompaction: true,
|
|
253
|
+
});
|
|
254
|
+
} catch {
|
|
255
|
+
runtime.state = runtime.state ? {
|
|
256
|
+
...stopGoalClock(runtime.state, Date.now()),
|
|
257
|
+
status: "paused",
|
|
258
|
+
result: reason,
|
|
259
|
+
resumeAfterManualCompaction: true,
|
|
260
|
+
} : undefined;
|
|
261
|
+
runtime.persistenceRetryNeeded = true;
|
|
262
|
+
runtime.continuationScheduled = false;
|
|
263
|
+
runtime.requestRender?.();
|
|
264
|
+
}
|
|
265
|
+
ctx.ui.notify(
|
|
266
|
+
"Goal paused because the turn was aborted. If /compact is running, KillerOS will resume after Pi saves the summary. Run /goal pause to keep it paused.",
|
|
267
|
+
"warning",
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function recoverGoalAfterManualCompaction(
|
|
272
|
+
pi: ExtensionAPI,
|
|
273
|
+
runtime: GoalRuntime,
|
|
274
|
+
initState: InitRuntime,
|
|
275
|
+
ctx: ExtensionContext,
|
|
276
|
+
): boolean {
|
|
277
|
+
if (runtime.state?.status !== "paused"
|
|
278
|
+
|| runtime.state.resumeAfterManualCompaction !== true
|
|
279
|
+
|| initState.active) return false;
|
|
280
|
+
try {
|
|
281
|
+
transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
|
|
282
|
+
} catch (error) {
|
|
283
|
+
runtime.persistenceRetryNeeded = true;
|
|
284
|
+
reportError(ctx, "Manual compaction succeeded, but the goal could not be resumed", error);
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
runtime.continuationScheduled = false;
|
|
288
|
+
ctx.ui.notify("Manual compaction complete. Goal resumed.", "info");
|
|
289
|
+
setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
220
293
|
function scheduleGoalContinuation(
|
|
221
294
|
pi: ExtensionAPI,
|
|
222
295
|
runtime: GoalRuntime,
|
|
@@ -259,6 +332,8 @@ function goalInstructions(state: GoalState, heading: string): string {
|
|
|
259
332
|
"Objective:",
|
|
260
333
|
state.objective,
|
|
261
334
|
"",
|
|
335
|
+
"Treat the exact objective above from /goal as authoritative; a compaction summary may describe it but does not replace it.",
|
|
336
|
+
"If the current context contains a compaction summary, take its first concrete next step after checking the current repository state.",
|
|
262
337
|
"Continue making concrete progress toward this unchanged objective. Re-check repository state and prior results instead of repeating work.",
|
|
263
338
|
"Do not stop merely because one response is complete: KillerOS will start another goal turn while the goal remains active.",
|
|
264
339
|
"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.",
|
|
@@ -279,7 +354,6 @@ function goalContinuationMessage(state: GoalState, ctx: ExtensionContext): strin
|
|
|
279
354
|
sections.push(`<personal_instructions source=${JSON.stringify(personal.source)}>\n${personal.content}\n</personal_instructions>`);
|
|
280
355
|
}
|
|
281
356
|
}
|
|
282
|
-
sections.push(CONCISE_SYSTEM_PROMPT);
|
|
283
357
|
return sections.join("\n\n");
|
|
284
358
|
}
|
|
285
359
|
|
|
@@ -354,37 +428,26 @@ export function registerGoal(
|
|
|
354
428
|
},
|
|
355
429
|
});
|
|
356
430
|
|
|
357
|
-
|
|
358
|
-
|
|
431
|
+
const restoreGoal = (ctx: ExtensionContext): void => {
|
|
432
|
+
const restored = restoreGoalState(ctx);
|
|
433
|
+
runtime.state = restored.state;
|
|
359
434
|
runtime.continuationScheduled = false;
|
|
360
435
|
runtime.continuationHeld = false;
|
|
361
|
-
runtime.continuationHeldForCompaction = false;
|
|
362
436
|
runtime.goalTurnInFlight = false;
|
|
363
437
|
runtime.agentEndObserved = false;
|
|
364
438
|
runtime.persistenceRetryNeeded = false;
|
|
365
439
|
runtime.lastStopReason = undefined;
|
|
366
440
|
runtime.lastError = undefined;
|
|
367
441
|
runtime.requestRender?.();
|
|
368
|
-
if (
|
|
442
|
+
if (restored.recoveryProven) {
|
|
443
|
+
recoverGoalAfterManualCompaction(pi, runtime, initState, ctx);
|
|
444
|
+
} else if (runtime.state?.status === "active") {
|
|
369
445
|
setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
|
|
370
446
|
}
|
|
371
|
-
}
|
|
447
|
+
};
|
|
372
448
|
|
|
373
|
-
pi.on("
|
|
374
|
-
|
|
375
|
-
runtime.continuationScheduled = false;
|
|
376
|
-
runtime.continuationHeld = false;
|
|
377
|
-
runtime.continuationHeldForCompaction = false;
|
|
378
|
-
runtime.goalTurnInFlight = false;
|
|
379
|
-
runtime.agentEndObserved = false;
|
|
380
|
-
runtime.persistenceRetryNeeded = false;
|
|
381
|
-
runtime.lastStopReason = undefined;
|
|
382
|
-
runtime.lastError = undefined;
|
|
383
|
-
runtime.requestRender?.();
|
|
384
|
-
if (runtime.state?.status === "active") {
|
|
385
|
-
setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
|
|
386
|
-
}
|
|
387
|
-
});
|
|
449
|
+
pi.on("session_start", (_event, ctx) => restoreGoal(ctx));
|
|
450
|
+
pi.on("session_tree", (_event, ctx) => restoreGoal(ctx));
|
|
388
451
|
|
|
389
452
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
390
453
|
if (runtime.state?.status === "active") {
|
|
@@ -403,7 +466,6 @@ export function registerGoal(
|
|
|
403
466
|
runtime.state = undefined;
|
|
404
467
|
runtime.continuationScheduled = false;
|
|
405
468
|
runtime.continuationHeld = false;
|
|
406
|
-
runtime.continuationHeldForCompaction = false;
|
|
407
469
|
runtime.goalTurnInFlight = false;
|
|
408
470
|
runtime.agentEndObserved = false;
|
|
409
471
|
runtime.persistenceRetryNeeded = false;
|
|
@@ -423,6 +485,7 @@ export function registerGoal(
|
|
|
423
485
|
turns: current.turns + 1,
|
|
424
486
|
updatedAt: now,
|
|
425
487
|
activeStartedAt: current.activeStartedAt ?? now,
|
|
488
|
+
resumeAfterManualCompaction: undefined,
|
|
426
489
|
};
|
|
427
490
|
try {
|
|
428
491
|
persistGoalState(pi, runtime, "turn", next);
|
|
@@ -507,7 +570,7 @@ export function registerGoal(
|
|
|
507
570
|
return;
|
|
508
571
|
}
|
|
509
572
|
if (runtime.state.status === "paused") {
|
|
510
|
-
if (!runtime.persistenceRetryNeeded) {
|
|
573
|
+
if (!runtime.persistenceRetryNeeded && runtime.state.resumeAfterManualCompaction !== true) {
|
|
511
574
|
ctx.ui.notify("Goal is already paused", "info");
|
|
512
575
|
return;
|
|
513
576
|
}
|
|
@@ -516,11 +579,16 @@ export function registerGoal(
|
|
|
516
579
|
...runtime.state,
|
|
517
580
|
revision: runtime.state.revision + 1,
|
|
518
581
|
updatedAt: now,
|
|
582
|
+
resumeAfterManualCompaction: undefined,
|
|
519
583
|
};
|
|
520
584
|
try {
|
|
521
585
|
persistGoalState(pi, runtime, "pause", checkpoint);
|
|
522
|
-
ctx.ui.notify("Goal pause saved", "info");
|
|
586
|
+
ctx.ui.notify("Goal pause saved. Goal remains paused. Automatic compaction recovery is off.", "info");
|
|
523
587
|
} catch (error) {
|
|
588
|
+
runtime.state = checkpoint;
|
|
589
|
+
runtime.persistenceRetryNeeded = true;
|
|
590
|
+
runtime.continuationScheduled = false;
|
|
591
|
+
runtime.requestRender?.();
|
|
524
592
|
reportError(ctx, "Goal pause still could not be saved", error);
|
|
525
593
|
}
|
|
526
594
|
return;
|
|
@@ -562,7 +630,7 @@ export function registerGoal(
|
|
|
562
630
|
return;
|
|
563
631
|
}
|
|
564
632
|
try {
|
|
565
|
-
transitionGoal(pi, runtime, "resume", "active", undefined, true);
|
|
633
|
+
transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
|
|
566
634
|
runtime.continuationScheduled = false;
|
|
567
635
|
if (scheduleGoalContinuation(pi, runtime, initState, ctx)) ctx.ui.notify("Goal resumed", "info");
|
|
568
636
|
} catch (error) {
|
|
@@ -620,6 +688,7 @@ export function registerGoal(
|
|
|
620
688
|
activeStartedAt: now,
|
|
621
689
|
blockedAuditStartTurn: current.turns,
|
|
622
690
|
result: undefined,
|
|
691
|
+
resumeAfterManualCompaction: undefined,
|
|
623
692
|
};
|
|
624
693
|
try {
|
|
625
694
|
persistGoalState(pi, runtime, "edit", next);
|
|
@@ -720,7 +789,6 @@ export function registerGoalSettlement(
|
|
|
720
789
|
pi: ExtensionAPI,
|
|
721
790
|
runtime: GoalRuntime,
|
|
722
791
|
initState: InitRuntime,
|
|
723
|
-
compactionRuntime?: CompactionRuntime,
|
|
724
792
|
): void {
|
|
725
793
|
pi.on("agent_settled", (_event, ctx) => {
|
|
726
794
|
const wasGoalTurn = runtime.goalTurnInFlight;
|
|
@@ -739,17 +807,18 @@ export function registerGoalSettlement(
|
|
|
739
807
|
pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn ended without an agent result");
|
|
740
808
|
return;
|
|
741
809
|
}
|
|
742
|
-
if (runtime.lastStopReason === "
|
|
743
|
-
const reason = runtime.lastError ||
|
|
810
|
+
if (runtime.lastStopReason === "aborted") {
|
|
811
|
+
const reason = runtime.lastError || "the agent turn was aborted";
|
|
744
812
|
runtime.lastStopReason = undefined;
|
|
745
813
|
runtime.lastError = undefined;
|
|
746
|
-
|
|
814
|
+
pauseGoalForPossibleManualCompaction(pi, runtime, ctx, reason);
|
|
747
815
|
return;
|
|
748
816
|
}
|
|
749
|
-
if (
|
|
750
|
-
runtime.
|
|
751
|
-
runtime.
|
|
752
|
-
runtime.
|
|
817
|
+
if (runtime.lastStopReason === "error") {
|
|
818
|
+
const reason = runtime.lastError || "the agent turn failed";
|
|
819
|
+
runtime.lastStopReason = undefined;
|
|
820
|
+
runtime.lastError = undefined;
|
|
821
|
+
pauseGoalAfterFailure(pi, runtime, ctx, reason);
|
|
753
822
|
return;
|
|
754
823
|
}
|
|
755
824
|
runtime.lastStopReason = undefined;
|
|
@@ -757,16 +826,8 @@ export function registerGoalSettlement(
|
|
|
757
826
|
scheduleGoalContinuation(pi, runtime, initState, ctx);
|
|
758
827
|
});
|
|
759
828
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
if (!runtime.continuationHeld || runtime.state?.status !== "active" || initState.active) {
|
|
765
|
-
runtime.continuationHeld = false;
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
runtime.continuationHeld = false;
|
|
769
|
-
setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
|
|
770
|
-
});
|
|
771
|
-
}
|
|
829
|
+
pi.on("session_compact", (event, ctx) => {
|
|
830
|
+
if (event.reason !== "manual") return;
|
|
831
|
+
recoverGoalAfterManualCompaction(pi, runtime, initState, ctx);
|
|
832
|
+
});
|
|
772
833
|
}
|
package/killeros/hooks.ts
CHANGED
|
@@ -22,6 +22,7 @@ interface HookExecutionResult {
|
|
|
22
22
|
stdout: string;
|
|
23
23
|
stderr: string;
|
|
24
24
|
timedOut: boolean;
|
|
25
|
+
cancelled: boolean;
|
|
25
26
|
exitUnconfirmed: boolean;
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -109,7 +110,17 @@ function terminateHookProcess(child: ReturnType<typeof spawn>, force: boolean):
|
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
export function executeHook(
|
|
113
|
+
export function executeHook(
|
|
114
|
+
command: string,
|
|
115
|
+
cwd: string,
|
|
116
|
+
environment: Record<string, string>,
|
|
117
|
+
timeoutMs = 30_000,
|
|
118
|
+
spawnProcess: typeof spawn = spawn,
|
|
119
|
+
signal?: AbortSignal,
|
|
120
|
+
): Promise<HookExecutionResult> {
|
|
121
|
+
if (signal?.aborted) {
|
|
122
|
+
return Promise.resolve({ code: 130, stdout: "", stderr: "", timedOut: false, cancelled: true, exitUnconfirmed: false });
|
|
123
|
+
}
|
|
113
124
|
return new Promise((resolve) => {
|
|
114
125
|
const child = spawnProcess(command, {
|
|
115
126
|
cwd,
|
|
@@ -122,36 +133,48 @@ export function executeHook(command: string, cwd: string, environment: Record<st
|
|
|
122
133
|
let stdout = "";
|
|
123
134
|
let stderr = "";
|
|
124
135
|
let completed = false;
|
|
125
|
-
let
|
|
126
|
-
let exitUnconfirmed = false;
|
|
136
|
+
let termination: "timeout" | "cancelled" | undefined;
|
|
127
137
|
let timer: NodeJS.Timeout | undefined;
|
|
128
138
|
let forceTimer: NodeJS.Timeout | undefined;
|
|
129
139
|
let settleTimer: NodeJS.Timeout | undefined;
|
|
130
|
-
const finish = (code: number,
|
|
140
|
+
const finish = (code: number, exitUnconfirmed = false): void => {
|
|
131
141
|
if (completed) return;
|
|
132
142
|
completed = true;
|
|
133
|
-
exitUnconfirmed = unconfirmed;
|
|
134
143
|
if (timer) clearTimeout(timer);
|
|
135
144
|
if (forceTimer) clearTimeout(forceTimer);
|
|
136
145
|
if (settleTimer) clearTimeout(settleTimer);
|
|
137
|
-
|
|
146
|
+
signal?.removeEventListener("abort", abort);
|
|
147
|
+
resolve({
|
|
148
|
+
code,
|
|
149
|
+
stdout,
|
|
150
|
+
stderr,
|
|
151
|
+
timedOut: termination === "timeout",
|
|
152
|
+
cancelled: termination === "cancelled",
|
|
153
|
+
exitUnconfirmed,
|
|
154
|
+
});
|
|
138
155
|
};
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
finish(timedOut ? 124 : 1);
|
|
144
|
-
});
|
|
145
|
-
child.once("close", (code) => finish(timedOut ? 124 : code ?? 1));
|
|
146
|
-
timer = setTimeout(() => {
|
|
147
|
-
timedOut = true;
|
|
156
|
+
const terminationCode = (): number => termination === "cancelled" ? 130 : 124;
|
|
157
|
+
const beginTermination = (reason: "timeout" | "cancelled"): void => {
|
|
158
|
+
if (completed || termination) return;
|
|
159
|
+
termination = reason;
|
|
148
160
|
terminateHookProcess(child, false);
|
|
149
161
|
forceTimer = setTimeout(() => {
|
|
150
162
|
if (completed) return;
|
|
151
163
|
terminateHookProcess(child, true);
|
|
152
|
-
settleTimer = setTimeout(() => finish(
|
|
164
|
+
settleTimer = setTimeout(() => finish(terminationCode(), true), 1_000);
|
|
153
165
|
}, 1_000);
|
|
154
|
-
}
|
|
166
|
+
};
|
|
167
|
+
const abort = (): void => beginTermination("cancelled");
|
|
168
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
169
|
+
if (signal?.aborted) beginTermination("cancelled");
|
|
170
|
+
child.stdout.on("data", (chunk) => { stdout = appendBounded(stdout, chunk); });
|
|
171
|
+
child.stderr.on("data", (chunk) => { stderr = appendBounded(stderr, chunk); });
|
|
172
|
+
child.on("error", (error) => {
|
|
173
|
+
stderr = appendBounded(stderr, error.message);
|
|
174
|
+
finish(termination ? terminationCode() : 1);
|
|
175
|
+
});
|
|
176
|
+
child.once("close", (code) => finish(termination ? terminationCode() : code ?? 1));
|
|
177
|
+
timer = setTimeout(() => beginTermination("timeout"), Math.max(1_000, Math.min(timeoutMs, 300_000)));
|
|
155
178
|
});
|
|
156
179
|
}
|
|
157
180
|
|
|
@@ -180,7 +203,10 @@ export function registerLifecycleHooks(pi: ExtensionAPI): void {
|
|
|
180
203
|
ctx.cwd,
|
|
181
204
|
hookEnvironment("tool_call", event.toolName, event.input),
|
|
182
205
|
hook.timeoutMs,
|
|
206
|
+
spawn,
|
|
207
|
+
ctx.signal,
|
|
183
208
|
);
|
|
209
|
+
if (result.cancelled) return { block: true, reason: "Hook cancelled because the parent request was aborted" };
|
|
184
210
|
if (result.code !== 0) {
|
|
185
211
|
const reason = hookFailureMessage(hook, result);
|
|
186
212
|
ctx.ui.notify(reason, "error");
|
|
@@ -200,7 +226,10 @@ export function registerLifecycleHooks(pi: ExtensionAPI): void {
|
|
|
200
226
|
isError: event.isError,
|
|
201
227
|
}),
|
|
202
228
|
hook.timeoutMs,
|
|
229
|
+
spawn,
|
|
230
|
+
ctx.signal,
|
|
203
231
|
);
|
|
232
|
+
if (result.cancelled) break;
|
|
204
233
|
if (result.code !== 0) ctx.ui.notify(hookFailureMessage(hook, result), "error");
|
|
205
234
|
}
|
|
206
235
|
});
|
|
@@ -212,7 +241,10 @@ export function registerLifecycleHooks(pi: ExtensionAPI): void {
|
|
|
212
241
|
ctx.cwd,
|
|
213
242
|
hookEnvironment("agent_settled"),
|
|
214
243
|
hook.timeoutMs,
|
|
244
|
+
spawn,
|
|
245
|
+
ctx.signal,
|
|
215
246
|
);
|
|
247
|
+
if (result.cancelled) break;
|
|
216
248
|
if (result.code !== 0) ctx.ui.notify(hookFailureMessage(hook, result), "error");
|
|
217
249
|
}
|
|
218
250
|
});
|