killeros 2.0.1 → 2.0.3
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 +24 -0
- package/Killeros.ts +13 -8
- package/README.md +14 -8
- package/killeros/footer.ts +46 -1
- package/killeros/goals.ts +118 -55
- package/killeros/notifications.ts +167 -0
- package/killeros/question.ts +16 -1
- package/killeros/runtime.ts +1 -25
- package/killeros/shell-ui.ts +15 -2
- package/package.json +2 -2
- package/killeros/context-compaction.ts +0 -614
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to KillerOS are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.0.3] - 2026-08-09
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added an opt-in global completion sound with `/notification` and an enabled-state Nerd Font bell in the terminal tab title.
|
|
12
|
+
- Moved active goal status to the footer's right side in warning yellow, replacing the path with `/goal is active (...)` and exact seconds.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Returned compaction timing, summary generation, manual instructions, retries, file tracking, and overflow recovery to Pi's public lifecycle.
|
|
17
|
+
- Continued active goals only after Pi reaches its settled boundary and preserved `/goal` as the sole durable objective and status owner.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Added fail-closed, revision-bound recovery after successful manual compaction, including reload and branch navigation recovery without reviving stale or explicitly paused goals.
|
|
22
|
+
- Rendered question help from the effective selector keybindings across supported Pi versions.
|
|
23
|
+
|
|
24
|
+
## [2.0.2] - 2026-08-08
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Replaced the static activity Spark with a 12-frame orange glyph loop at 120 ms per frame.
|
|
29
|
+
- Styled activity verbs orange and added the gray `(esc to interrupt · thinking)` status with bold `esc`.
|
|
30
|
+
|
|
7
31
|
## [2.0.1] - 2026-08-08
|
|
8
32
|
|
|
9
33
|
### Added
|
package/Killeros.ts
CHANGED
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { registerAliases, registerSlashAutocomplete } from "./killeros/commands.ts";
|
|
3
3
|
import { registerConcisePrompt } from "./killeros/concise.ts";
|
|
4
|
-
import { registerContextCompaction } from "./killeros/context-compaction.ts";
|
|
5
4
|
import { registerFooter } from "./killeros/footer.ts";
|
|
6
5
|
import { registerGoal, registerGoalSettlement } from "./killeros/goals.ts";
|
|
7
6
|
import { registerLifecycleHooks } from "./killeros/hooks.ts";
|
|
8
7
|
import { registerInitCommand, registerInitSettlement } from "./killeros/init.ts";
|
|
8
|
+
import {
|
|
9
|
+
registerCompletionNotifications,
|
|
10
|
+
type CompletionNotificationDependencies,
|
|
11
|
+
} from "./killeros/notifications.ts";
|
|
9
12
|
import { registerPersonalInstructions } from "./killeros/personal-instructions.ts";
|
|
10
13
|
import { registerQuestionTool } from "./killeros/question.ts";
|
|
11
|
-
import {
|
|
14
|
+
import { createGoalRuntime, createInitRuntime } from "./killeros/runtime.ts";
|
|
12
15
|
import { registerShellUi } from "./killeros/shell-ui.ts";
|
|
13
16
|
import { registerVariants } from "./killeros/variants.ts";
|
|
14
17
|
|
|
15
18
|
export { CONCISE_SYSTEM_PROMPT, isConcisedEnabled } from "./killeros/concise.ts";
|
|
16
|
-
export { contextPercentRemaining } from "./killeros/
|
|
17
|
-
export { formatCost, formatContextProgress } from "./killeros/footer.ts";
|
|
19
|
+
export { contextPercentRemaining, formatCost, formatContextProgress } from "./killeros/footer.ts";
|
|
18
20
|
export { executeHook } from "./killeros/hooks.ts";
|
|
19
21
|
export { INIT_WORKFLOW_PROMPT, writeInitAgentsFile } from "./killeros/init.ts";
|
|
20
22
|
|
|
21
|
-
export
|
|
23
|
+
export interface KillerosOptions {
|
|
24
|
+
completionNotifications?: CompletionNotificationDependencies;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}): void {
|
|
22
28
|
const initRuntime = createInitRuntime();
|
|
23
29
|
const goalRuntime = createGoalRuntime();
|
|
24
|
-
const compactionRuntime = createCompactionRuntime();
|
|
25
30
|
registerShellUi(pi);
|
|
26
31
|
registerConcisePrompt(pi);
|
|
27
32
|
registerGoal(pi, goalRuntime, initRuntime);
|
|
@@ -33,7 +38,7 @@ export default function Killeros(pi: ExtensionAPI): void {
|
|
|
33
38
|
registerVariants(pi);
|
|
34
39
|
registerInitCommand(pi, initRuntime, goalRuntime);
|
|
35
40
|
registerLifecycleHooks(pi);
|
|
36
|
-
|
|
37
|
-
registerGoalSettlement(pi, goalRuntime, initRuntime, compactionRuntime);
|
|
41
|
+
registerGoalSettlement(pi, goalRuntime, initRuntime);
|
|
38
42
|
registerInitSettlement(pi, initRuntime);
|
|
43
|
+
registerCompletionNotifications(pi, options.completionNotifications);
|
|
39
44
|
}
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ pi install git:github.com/KyrosHendrix/pi-KillerOS
|
|
|
31
31
|
Pin an install to a release:
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
pi install git:github.com/KyrosHendrix/pi-KillerOS@v2.0.
|
|
34
|
+
pi install git:github.com/KyrosHendrix/pi-KillerOS@v2.0.3
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Add `-l` to either command for a project-only install. Restart Pi after installing.
|
|
@@ -40,10 +40,11 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
|
|
|
40
40
|
|
|
41
41
|
- 52-column Compact startup card with inline version, polished model/provider identity, adjacent `/model`, directory, conditional Git branch, and a shuffled session-stable tip
|
|
42
42
|
- Cohesive dark theme with coral accents and neutral tool-call containers across pending, success, and error states
|
|
43
|
-
-
|
|
43
|
+
- Animated orange 12-frame activity glyph loop at 120 ms per frame, with orange shuffled Claude-adjacent verbs changing every 2.5 seconds, a gray `(esc to interrupt · thinking)` status with bold `esc`, and a quiet hidden-thinking label
|
|
44
44
|
- Framed multiline editor with Shift+Enter support and live command-blue highlighting for recognized slash command prefixes
|
|
45
45
|
- Responsive footer with polished model/provider identity, plain-language context, and active goal state remaining; reasoning, Git branch, elapsed time, cost, and path cut down by available width
|
|
46
|
-
-
|
|
46
|
+
- Pi-owned context compaction with active goals continuing from Pi's settled boundary after manual, threshold, and overflow compaction
|
|
47
|
+
- Optional completion sounds after successful or failed settled requests, excluding manual aborts
|
|
47
48
|
- `/variants` selector and direct reasoning-level arguments
|
|
48
49
|
- Codex-style `/goal` with an interactive status/action panel, durable objectives, pause, resume, edit, confirmed panel clearing, automatic continuation, and explicit completion
|
|
49
50
|
- Claude Code-style `/init` that scans the repository and generates a concise root `AGENTS.md` without setup questions
|
|
@@ -64,6 +65,7 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
|
|
|
64
65
|
/goal clear Remove the current goal
|
|
65
66
|
/variants Open the reasoning-level selector
|
|
66
67
|
/variants high Set a reasoning level directly
|
|
68
|
+
/notification Configure the completion sound
|
|
67
69
|
/clear Start a new session after confirmation
|
|
68
70
|
/exit Quit Pi gracefully
|
|
69
71
|
```
|
|
@@ -78,9 +80,13 @@ Supported reasoning levels are `off`, `minimal`, `low`, `medium`, `high`, `xhigh
|
|
|
78
80
|
|
|
79
81
|
KillerOS activates its packaged `killeros` theme when a TUI session starts. Tool-call backgrounds stay neutral across pending, successful, and failed states; restrained text and icons preserve status visibility.
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
The completion sound is a global user preference stored in Pi's agent directory and is off by default. Run `/notification` in TUI mode to enable or disable it; enabling does not play a preview. Enabled TUI tabs append ``, which requires a Nerd Font in the terminal tab UI. An unsupported font may show a box without affecting sound. KillerOS uses the terminal's audible bell and cannot produce sound when the terminal disables it.
|
|
82
84
|
|
|
83
|
-
KillerOS
|
|
85
|
+
KillerOS displays session costs in USD. The footer uses Pi's human-readable model name when available, keeps the provider visually secondary, and renders context as `percent left (tokens)` without a progress bar. An active goal replaces the right-side path with warning-yellow `/goal is active (...)` and keeps exact seconds in minute and hour formats. Paused and blocked goals retain their existing placement; completed goals remain in transcript history and `/goal` status rather than the footer. At narrow widths, context pressure and actionable goal state take priority.
|
|
86
|
+
|
|
87
|
+
Pi decides when compaction runs and Pi writes the summary, applies manual focus instructions, tracks files, retries summarization, and handles overflow recovery. KillerOS does not add a second threshold or replace Pi's summary. Active `/goal` work continues from Pi's settled boundary, after Pi finishes retries, compaction, and queued work.
|
|
88
|
+
|
|
89
|
+
Manual `/compact` aborts the current goal turn before summarization, so KillerOS records an honest temporary pause for that exact goal revision. After Pi saves the manual summary, KillerOS resumes that revision automatically. A failed or cancelled manual compaction stays paused; run `/goal pause` during the pause to cancel automatic recovery.
|
|
84
90
|
|
|
85
91
|
For trusted projects, KillerOS loads `AGENTS.local.md` after Pi's shared repository context. A one-line `@path` or `@~/path` file imports personal guidance from another location.
|
|
86
92
|
|
|
@@ -90,9 +96,9 @@ Lifecycle hooks are loaded from `.pi/killeros-hooks.json` at session start. Supp
|
|
|
90
96
|
|
|
91
97
|
| Mode | Behavior |
|
|
92
98
|
|---|---|
|
|
93
|
-
| TUI | All features are available |
|
|
94
|
-
| RPC | Goal set/view/pause/resume/clear and concise prompt guidance work; TUI components, `/goal edit`,
|
|
95
|
-
| Print/JSON | Concise prompt guidance works; interactive questions, `/goal`, and `/init` fail explicitly |
|
|
99
|
+
| TUI | All features are available, including the completion sound and tab-title indicator |
|
|
100
|
+
| RPC | Goal set/view/pause/resume/clear and concise prompt guidance work; TUI components, `/goal edit`, `/init`, completion sounds, and the title indicator are disabled |
|
|
101
|
+
| Print/JSON | Concise prompt guidance works; interactive questions, `/goal`, and `/init` fail explicitly; completion sounds and the title indicator are disabled |
|
|
96
102
|
|
|
97
103
|
## Validation
|
|
98
104
|
|
package/killeros/footer.ts
CHANGED
|
@@ -12,6 +12,20 @@ export function formatCost(usd: number): string {
|
|
|
12
12
|
return `$${usd.toFixed(2)}`;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export function contextPercentRemaining(ctx: ExtensionContext): number | null {
|
|
16
|
+
let usage: ReturnType<ExtensionContext["getContextUsage"]>;
|
|
17
|
+
try {
|
|
18
|
+
usage = ctx.getContextUsage();
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (!usage || !Number.isFinite(usage.contextWindow) || usage.contextWindow <= 0) return null;
|
|
23
|
+
if (usage.tokens === null || !Number.isFinite(usage.tokens)) return null;
|
|
24
|
+
|
|
25
|
+
const percentRemaining = ((usage.contextWindow - Math.max(0, usage.tokens)) / usage.contextWindow) * 100;
|
|
26
|
+
return Math.round(Math.max(0, Math.min(100, percentRemaining)));
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
export function formatContextProgress(tokensUsed: number | null, contextWindow: number, theme: Theme): string {
|
|
16
30
|
if (tokensUsed === null || !Number.isFinite(tokensUsed)) return theme.fg("dim", "—% left (—)");
|
|
17
31
|
const windowSize = Number.isFinite(contextWindow) && contextWindow > 0 ? contextWindow : 128_000;
|
|
@@ -111,9 +125,26 @@ function renderFooterRow(left: string, right: string, width: number): string {
|
|
|
111
125
|
return ` ${clippedLeft}${gap}${clippedRight} `;
|
|
112
126
|
}
|
|
113
127
|
|
|
128
|
+
function formatGoalElapsed(milliseconds: number): string {
|
|
129
|
+
const totalSeconds = Number.isFinite(milliseconds) ? Math.max(0, Math.floor(milliseconds / 1_000)) : 0;
|
|
130
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
131
|
+
|
|
132
|
+
const seconds = totalSeconds % 60;
|
|
133
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
134
|
+
if (totalMinutes < 60) return `${totalMinutes}m ${seconds.toString().padStart(2, "0")}s`;
|
|
135
|
+
|
|
136
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
137
|
+
const minutes = totalMinutes % 60;
|
|
138
|
+
return `${hours}h ${minutes.toString().padStart(2, "0")}m ${seconds.toString().padStart(2, "0")}s`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatActiveGoalFooter(state: GoalState | undefined, theme: Theme): string {
|
|
142
|
+
if (state?.status !== "active") return "";
|
|
143
|
+
return theme.fg("warning", `/goal is active (${formatGoalElapsed(goalElapsedMilliseconds(state))})`);
|
|
144
|
+
}
|
|
145
|
+
|
|
114
146
|
function formatGoalFooter(state: GoalState | undefined, theme: Theme): string {
|
|
115
147
|
if (!state) return "";
|
|
116
|
-
if (state.status === "active") return theme.fg("accent", `✻ goal · ${formatTime(goalElapsedMilliseconds(state))}`);
|
|
117
148
|
if (state.status === "paused") return theme.fg("warning", "Ⅱ goal paused");
|
|
118
149
|
if (state.status === "blocked") return theme.fg("error", "! goal blocked");
|
|
119
150
|
return "";
|
|
@@ -180,6 +211,7 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
180
211
|
const signature = formatModel(model, theme);
|
|
181
212
|
const fullDirectory = theme.fg("dim", cwd);
|
|
182
213
|
const focusedDirectory = theme.fg("dim", compactDirectory(cwd));
|
|
214
|
+
const activeGoal = formatActiveGoalFooter(goalRuntime.state, theme);
|
|
183
215
|
const goal = formatGoalFooter(goalRuntime.state, theme);
|
|
184
216
|
const rich = joinFooterParts([
|
|
185
217
|
signature,
|
|
@@ -192,6 +224,19 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
192
224
|
], theme);
|
|
193
225
|
const focused = joinFooterParts([signature, context, goal], theme);
|
|
194
226
|
|
|
227
|
+
if (activeGoal) {
|
|
228
|
+
if (footerRowFits(rich, activeGoal, width)) {
|
|
229
|
+
return [renderFooterRow(rich, activeGoal, width)];
|
|
230
|
+
}
|
|
231
|
+
if (footerRowFits(focused, activeGoal, width)) {
|
|
232
|
+
return [renderFooterRow(focused, activeGoal, width)];
|
|
233
|
+
}
|
|
234
|
+
if (footerRowFits(context, activeGoal, width)) {
|
|
235
|
+
return [renderFooterRow(context, activeGoal, width)];
|
|
236
|
+
}
|
|
237
|
+
return [renderFooterRow("", activeGoal, width)];
|
|
238
|
+
}
|
|
239
|
+
|
|
195
240
|
if (footerRowFits(rich, fullDirectory, width)) {
|
|
196
241
|
return [renderFooterRow(rich, fullDirectory, width)];
|
|
197
242
|
}
|
package/killeros/goals.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { CONCISE_SYSTEM_PROMPT } from "./concise.ts";
|
|
|
7
7
|
import { formatTime, formatTokens } from "./display.ts";
|
|
8
8
|
import { reportError } from "./errors.ts";
|
|
9
9
|
import { resolvePersonalInstructions } from "./personal-instructions.ts";
|
|
10
|
-
import type {
|
|
10
|
+
import type { GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
|
|
11
11
|
|
|
12
12
|
const GOAL_ENTRY_TYPE = "killeros-goal";
|
|
13
13
|
const GOAL_CONTINUATION_TYPE = "killeros-goal-continuation";
|
|
@@ -21,6 +21,16 @@ interface GoalEntryData {
|
|
|
21
21
|
state: GoalState | null;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
interface GoalTransitionOptions {
|
|
25
|
+
resetBlockedAudit?: boolean;
|
|
26
|
+
resumeAfterManualCompaction?: true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface RestoredGoalState {
|
|
30
|
+
state?: GoalState;
|
|
31
|
+
recoveryProven: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
const GoalUpdateParams = Type.Object({
|
|
25
35
|
status: Type.Union([Type.Literal("complete"), Type.Literal("blocked")], {
|
|
26
36
|
description: "Mark the active goal complete or blocked",
|
|
@@ -61,7 +71,9 @@ function parseGoalState(value: unknown): GoalState | undefined {
|
|
|
61
71
|
&& (!Number.isInteger(candidate.blockedAuditStartTurn) || candidate.blockedAuditStartTurn < 0 || candidate.blockedAuditStartTurn > candidate.turns!)
|
|
62
72
|
|| !finiteNonNegative(candidate.baselineTokens)
|
|
63
73
|
|| candidate.activeStartedAt !== undefined && !finiteNonNegative(candidate.activeStartedAt)
|
|
64
|
-
|| candidate.result !== undefined && typeof candidate.result !== "string"
|
|
74
|
+
|| candidate.result !== undefined && typeof candidate.result !== "string"
|
|
75
|
+
|| candidate.resumeAfterManualCompaction !== undefined && candidate.resumeAfterManualCompaction !== true
|
|
76
|
+
|| candidate.resumeAfterManualCompaction === true && candidate.status !== "paused") {
|
|
65
77
|
return undefined;
|
|
66
78
|
}
|
|
67
79
|
return {
|
|
@@ -77,6 +89,7 @@ function parseGoalState(value: unknown): GoalState | undefined {
|
|
|
77
89
|
blockedAuditStartTurn: candidate.blockedAuditStartTurn ?? 0,
|
|
78
90
|
baselineTokens: candidate.baselineTokens,
|
|
79
91
|
result: candidate.result,
|
|
92
|
+
resumeAfterManualCompaction: candidate.resumeAfterManualCompaction,
|
|
80
93
|
};
|
|
81
94
|
}
|
|
82
95
|
|
|
@@ -88,21 +101,26 @@ function goalBranchEntries(ctx: ExtensionContext): ReturnType<ExtensionContext["
|
|
|
88
101
|
}
|
|
89
102
|
}
|
|
90
103
|
|
|
91
|
-
function restoreGoalState(ctx: ExtensionContext):
|
|
104
|
+
function restoreGoalState(ctx: ExtensionContext): RestoredGoalState {
|
|
92
105
|
const entries = goalBranchEntries(ctx);
|
|
93
106
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
94
107
|
const entry = entries[index];
|
|
95
108
|
if (entry?.type !== "custom" || entry.customType !== GOAL_ENTRY_TYPE) continue;
|
|
96
109
|
const data = entry.data as Partial<GoalEntryData> | undefined;
|
|
97
|
-
if (!data || data.version !== GOAL_VERSION)
|
|
98
|
-
|
|
110
|
+
if (!data || data.version !== GOAL_VERSION || data.state === null) {
|
|
111
|
+
return { state: undefined, recoveryProven: false };
|
|
112
|
+
}
|
|
99
113
|
const restored = parseGoalState(data.state);
|
|
100
|
-
if (!restored) return undefined;
|
|
101
|
-
|
|
114
|
+
if (!restored) return { state: undefined, recoveryProven: false };
|
|
115
|
+
const state = restored.status === "active"
|
|
102
116
|
? { ...restored, activeStartedAt: Date.now() }
|
|
103
117
|
: { ...restored, activeStartedAt: undefined };
|
|
118
|
+
const recoveryProven = state.status === "paused"
|
|
119
|
+
&& state.resumeAfterManualCompaction === true
|
|
120
|
+
&& entries.slice(index + 1).some((candidate) => candidate.type === "compaction");
|
|
121
|
+
return { state, recoveryProven };
|
|
104
122
|
}
|
|
105
|
-
return undefined;
|
|
123
|
+
return { state: undefined, recoveryProven: false };
|
|
106
124
|
}
|
|
107
125
|
|
|
108
126
|
export function goalElapsedMilliseconds(state: GoalState, now = Date.now()): number {
|
|
@@ -152,7 +170,7 @@ function transitionGoal(
|
|
|
152
170
|
event: GoalEntryEvent,
|
|
153
171
|
status: GoalStatus,
|
|
154
172
|
result?: string,
|
|
155
|
-
|
|
173
|
+
options: GoalTransitionOptions = {},
|
|
156
174
|
): GoalState {
|
|
157
175
|
const current = runtime.state;
|
|
158
176
|
if (!current) throw new Error("No goal is set");
|
|
@@ -164,8 +182,9 @@ function transitionGoal(
|
|
|
164
182
|
status,
|
|
165
183
|
updatedAt: now,
|
|
166
184
|
activeStartedAt: status === "active" ? now : undefined,
|
|
167
|
-
blockedAuditStartTurn: resetBlockedAudit ? stopped.turns : stopped.blockedAuditStartTurn,
|
|
185
|
+
blockedAuditStartTurn: options.resetBlockedAudit ? stopped.turns : stopped.blockedAuditStartTurn,
|
|
168
186
|
result,
|
|
187
|
+
resumeAfterManualCompaction: options.resumeAfterManualCompaction,
|
|
169
188
|
};
|
|
170
189
|
persistGoalState(pi, runtime, event, next);
|
|
171
190
|
if (status !== "active") runtime.continuationScheduled = false;
|
|
@@ -209,7 +228,12 @@ export function pauseGoalAfterFailure(
|
|
|
209
228
|
try {
|
|
210
229
|
transitionGoal(pi, runtime, "error", "paused", reason);
|
|
211
230
|
} catch {
|
|
212
|
-
runtime.state = runtime.state ? {
|
|
231
|
+
runtime.state = runtime.state ? {
|
|
232
|
+
...stopGoalClock(runtime.state, Date.now()),
|
|
233
|
+
status: "paused",
|
|
234
|
+
result: reason,
|
|
235
|
+
resumeAfterManualCompaction: undefined,
|
|
236
|
+
} : undefined;
|
|
213
237
|
runtime.persistenceRetryNeeded = true;
|
|
214
238
|
runtime.continuationScheduled = false;
|
|
215
239
|
runtime.requestRender?.();
|
|
@@ -217,6 +241,56 @@ export function pauseGoalAfterFailure(
|
|
|
217
241
|
ctx.ui.notify(`Goal paused: ${reason}\n${recoveryInstruction}`, "error");
|
|
218
242
|
}
|
|
219
243
|
|
|
244
|
+
function pauseGoalForPossibleManualCompaction(
|
|
245
|
+
pi: ExtensionAPI,
|
|
246
|
+
runtime: GoalRuntime,
|
|
247
|
+
ctx: ExtensionContext,
|
|
248
|
+
reason: string,
|
|
249
|
+
): void {
|
|
250
|
+
if (runtime.state?.status !== "active") return;
|
|
251
|
+
try {
|
|
252
|
+
transitionGoal(pi, runtime, "error", "paused", reason, {
|
|
253
|
+
resumeAfterManualCompaction: true,
|
|
254
|
+
});
|
|
255
|
+
} catch {
|
|
256
|
+
runtime.state = runtime.state ? {
|
|
257
|
+
...stopGoalClock(runtime.state, Date.now()),
|
|
258
|
+
status: "paused",
|
|
259
|
+
result: reason,
|
|
260
|
+
resumeAfterManualCompaction: true,
|
|
261
|
+
} : undefined;
|
|
262
|
+
runtime.persistenceRetryNeeded = true;
|
|
263
|
+
runtime.continuationScheduled = false;
|
|
264
|
+
runtime.requestRender?.();
|
|
265
|
+
}
|
|
266
|
+
ctx.ui.notify(
|
|
267
|
+
"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.",
|
|
268
|
+
"warning",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function recoverGoalAfterManualCompaction(
|
|
273
|
+
pi: ExtensionAPI,
|
|
274
|
+
runtime: GoalRuntime,
|
|
275
|
+
initState: InitRuntime,
|
|
276
|
+
ctx: ExtensionContext,
|
|
277
|
+
): boolean {
|
|
278
|
+
if (runtime.state?.status !== "paused"
|
|
279
|
+
|| runtime.state.resumeAfterManualCompaction !== true
|
|
280
|
+
|| initState.active) return false;
|
|
281
|
+
try {
|
|
282
|
+
transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
|
|
283
|
+
} catch (error) {
|
|
284
|
+
runtime.persistenceRetryNeeded = true;
|
|
285
|
+
reportError(ctx, "Manual compaction succeeded, but the goal could not be resumed", error);
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
runtime.continuationScheduled = false;
|
|
289
|
+
ctx.ui.notify("Manual compaction complete. Goal resumed.", "info");
|
|
290
|
+
setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
220
294
|
function scheduleGoalContinuation(
|
|
221
295
|
pi: ExtensionAPI,
|
|
222
296
|
runtime: GoalRuntime,
|
|
@@ -259,6 +333,8 @@ function goalInstructions(state: GoalState, heading: string): string {
|
|
|
259
333
|
"Objective:",
|
|
260
334
|
state.objective,
|
|
261
335
|
"",
|
|
336
|
+
"Treat the exact objective above from /goal as authoritative; a compaction summary may describe it but does not replace it.",
|
|
337
|
+
"If the current context contains a compaction summary, take its first concrete next step after checking the current repository state.",
|
|
262
338
|
"Continue making concrete progress toward this unchanged objective. Re-check repository state and prior results instead of repeating work.",
|
|
263
339
|
"Do not stop merely because one response is complete: KillerOS will start another goal turn while the goal remains active.",
|
|
264
340
|
"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.",
|
|
@@ -354,37 +430,26 @@ export function registerGoal(
|
|
|
354
430
|
},
|
|
355
431
|
});
|
|
356
432
|
|
|
357
|
-
|
|
358
|
-
|
|
433
|
+
const restoreGoal = (ctx: ExtensionContext): void => {
|
|
434
|
+
const restored = restoreGoalState(ctx);
|
|
435
|
+
runtime.state = restored.state;
|
|
359
436
|
runtime.continuationScheduled = false;
|
|
360
437
|
runtime.continuationHeld = false;
|
|
361
|
-
runtime.continuationHeldForCompaction = false;
|
|
362
438
|
runtime.goalTurnInFlight = false;
|
|
363
439
|
runtime.agentEndObserved = false;
|
|
364
440
|
runtime.persistenceRetryNeeded = false;
|
|
365
441
|
runtime.lastStopReason = undefined;
|
|
366
442
|
runtime.lastError = undefined;
|
|
367
443
|
runtime.requestRender?.();
|
|
368
|
-
if (
|
|
444
|
+
if (restored.recoveryProven) {
|
|
445
|
+
recoverGoalAfterManualCompaction(pi, runtime, initState, ctx);
|
|
446
|
+
} else if (runtime.state?.status === "active") {
|
|
369
447
|
setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
|
|
370
448
|
}
|
|
371
|
-
}
|
|
449
|
+
};
|
|
372
450
|
|
|
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
|
-
});
|
|
451
|
+
pi.on("session_start", (_event, ctx) => restoreGoal(ctx));
|
|
452
|
+
pi.on("session_tree", (_event, ctx) => restoreGoal(ctx));
|
|
388
453
|
|
|
389
454
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
390
455
|
if (runtime.state?.status === "active") {
|
|
@@ -403,7 +468,6 @@ export function registerGoal(
|
|
|
403
468
|
runtime.state = undefined;
|
|
404
469
|
runtime.continuationScheduled = false;
|
|
405
470
|
runtime.continuationHeld = false;
|
|
406
|
-
runtime.continuationHeldForCompaction = false;
|
|
407
471
|
runtime.goalTurnInFlight = false;
|
|
408
472
|
runtime.agentEndObserved = false;
|
|
409
473
|
runtime.persistenceRetryNeeded = false;
|
|
@@ -423,6 +487,7 @@ export function registerGoal(
|
|
|
423
487
|
turns: current.turns + 1,
|
|
424
488
|
updatedAt: now,
|
|
425
489
|
activeStartedAt: current.activeStartedAt ?? now,
|
|
490
|
+
resumeAfterManualCompaction: undefined,
|
|
426
491
|
};
|
|
427
492
|
try {
|
|
428
493
|
persistGoalState(pi, runtime, "turn", next);
|
|
@@ -507,7 +572,7 @@ export function registerGoal(
|
|
|
507
572
|
return;
|
|
508
573
|
}
|
|
509
574
|
if (runtime.state.status === "paused") {
|
|
510
|
-
if (!runtime.persistenceRetryNeeded) {
|
|
575
|
+
if (!runtime.persistenceRetryNeeded && runtime.state.resumeAfterManualCompaction !== true) {
|
|
511
576
|
ctx.ui.notify("Goal is already paused", "info");
|
|
512
577
|
return;
|
|
513
578
|
}
|
|
@@ -516,11 +581,16 @@ export function registerGoal(
|
|
|
516
581
|
...runtime.state,
|
|
517
582
|
revision: runtime.state.revision + 1,
|
|
518
583
|
updatedAt: now,
|
|
584
|
+
resumeAfterManualCompaction: undefined,
|
|
519
585
|
};
|
|
520
586
|
try {
|
|
521
587
|
persistGoalState(pi, runtime, "pause", checkpoint);
|
|
522
|
-
ctx.ui.notify("Goal pause saved", "info");
|
|
588
|
+
ctx.ui.notify("Goal pause saved. Goal remains paused. Automatic compaction recovery is off.", "info");
|
|
523
589
|
} catch (error) {
|
|
590
|
+
runtime.state = checkpoint;
|
|
591
|
+
runtime.persistenceRetryNeeded = true;
|
|
592
|
+
runtime.continuationScheduled = false;
|
|
593
|
+
runtime.requestRender?.();
|
|
524
594
|
reportError(ctx, "Goal pause still could not be saved", error);
|
|
525
595
|
}
|
|
526
596
|
return;
|
|
@@ -562,7 +632,7 @@ export function registerGoal(
|
|
|
562
632
|
return;
|
|
563
633
|
}
|
|
564
634
|
try {
|
|
565
|
-
transitionGoal(pi, runtime, "resume", "active", undefined, true);
|
|
635
|
+
transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
|
|
566
636
|
runtime.continuationScheduled = false;
|
|
567
637
|
if (scheduleGoalContinuation(pi, runtime, initState, ctx)) ctx.ui.notify("Goal resumed", "info");
|
|
568
638
|
} catch (error) {
|
|
@@ -620,6 +690,7 @@ export function registerGoal(
|
|
|
620
690
|
activeStartedAt: now,
|
|
621
691
|
blockedAuditStartTurn: current.turns,
|
|
622
692
|
result: undefined,
|
|
693
|
+
resumeAfterManualCompaction: undefined,
|
|
623
694
|
};
|
|
624
695
|
try {
|
|
625
696
|
persistGoalState(pi, runtime, "edit", next);
|
|
@@ -720,7 +791,6 @@ export function registerGoalSettlement(
|
|
|
720
791
|
pi: ExtensionAPI,
|
|
721
792
|
runtime: GoalRuntime,
|
|
722
793
|
initState: InitRuntime,
|
|
723
|
-
compactionRuntime?: CompactionRuntime,
|
|
724
794
|
): void {
|
|
725
795
|
pi.on("agent_settled", (_event, ctx) => {
|
|
726
796
|
const wasGoalTurn = runtime.goalTurnInFlight;
|
|
@@ -739,17 +809,18 @@ export function registerGoalSettlement(
|
|
|
739
809
|
pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn ended without an agent result");
|
|
740
810
|
return;
|
|
741
811
|
}
|
|
742
|
-
if (runtime.lastStopReason === "
|
|
743
|
-
const reason = runtime.lastError ||
|
|
812
|
+
if (runtime.lastStopReason === "aborted") {
|
|
813
|
+
const reason = runtime.lastError || "the agent turn was aborted";
|
|
744
814
|
runtime.lastStopReason = undefined;
|
|
745
815
|
runtime.lastError = undefined;
|
|
746
|
-
|
|
816
|
+
pauseGoalForPossibleManualCompaction(pi, runtime, ctx, reason);
|
|
747
817
|
return;
|
|
748
818
|
}
|
|
749
|
-
if (
|
|
750
|
-
runtime.
|
|
751
|
-
runtime.
|
|
752
|
-
runtime.
|
|
819
|
+
if (runtime.lastStopReason === "error") {
|
|
820
|
+
const reason = runtime.lastError || "the agent turn failed";
|
|
821
|
+
runtime.lastStopReason = undefined;
|
|
822
|
+
runtime.lastError = undefined;
|
|
823
|
+
pauseGoalAfterFailure(pi, runtime, ctx, reason);
|
|
753
824
|
return;
|
|
754
825
|
}
|
|
755
826
|
runtime.lastStopReason = undefined;
|
|
@@ -757,16 +828,8 @@ export function registerGoalSettlement(
|
|
|
757
828
|
scheduleGoalContinuation(pi, runtime, initState, ctx);
|
|
758
829
|
});
|
|
759
830
|
|
|
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
|
-
}
|
|
831
|
+
pi.on("session_compact", (event, ctx) => {
|
|
832
|
+
if (event.reason !== "manual") return;
|
|
833
|
+
recoverGoalAfterManualCompaction(pi, runtime, initState, ctx);
|
|
834
|
+
});
|
|
772
835
|
}
|