killeros 2.0.9 → 2.0.11
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 +18 -0
- package/Killeros.ts +5 -22
- package/README.md +21 -20
- package/killeros/activity.ts +1 -1
- package/killeros/auto-compaction.ts +224 -0
- package/killeros/commands.ts +3 -3
- package/killeros/display.ts +2 -2
- package/killeros/footer.ts +2 -2
- package/killeros/goals.ts +79 -6
- package/killeros/init-evidence.ts +5 -5
- package/killeros/init.ts +1 -1
- package/killeros/notifications.ts +6 -36
- package/killeros/question.ts +7 -19
- package/killeros/runtime.ts +2 -0
- package/killeros/settings.ts +47 -0
- package/killeros/variants.ts +2 -2
- package/package.json +5 -5
- package/killeros/decision-gated-workflow.ts +0 -76
- package/killeros/workflow-gate.ts +0 -347
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ All notable changes to KillerOS are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.0.11] - 2026-08-17
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Made automated npm publication and GitHub release recovery idempotent, aligned the documented and tested Pi floor, and made machine-identifier casing locale-independent.
|
|
12
|
+
- Extended strict type checking to release scripts and typed test suites, while correctly identifying JavaScript-only suites as `.js`.
|
|
13
|
+
|
|
14
|
+
### Removed
|
|
15
|
+
|
|
16
|
+
- Removed the decision-gated workflow subsystem, its skill-specific policy, public extension interface, and YAML dependency. Skills now remain instruction-only.
|
|
17
|
+
|
|
18
|
+
## [2.0.10] - 2026-08-16
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Added proactive turn-boundary compaction with Pi-owned settings and safe continuation for ordinary prompts and active goals.
|
|
23
|
+
- Added reusable multi-activation decision-gated workflow registrations while preserving exact-match behavior.
|
|
24
|
+
|
|
7
25
|
## [2.0.9] - 2026-08-15
|
|
8
26
|
|
|
9
27
|
### Added
|
package/Killeros.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { registerRequestActivity } from "./killeros/activity.ts";
|
|
3
|
+
import { registerAutoCompaction } from "./killeros/auto-compaction.ts";
|
|
3
4
|
import {
|
|
4
5
|
createSlashCommandResolver,
|
|
5
6
|
registerAliases,
|
|
@@ -14,33 +15,19 @@ import {
|
|
|
14
15
|
type CompletionNotificationDependencies,
|
|
15
16
|
} from "./killeros/notifications.ts";
|
|
16
17
|
import { registerPersonalInstructions } from "./killeros/personal-instructions.ts";
|
|
17
|
-
import { registerQuestionTool
|
|
18
|
-
import { createDecisionGatedWorkflowAdapter } from "./killeros/decision-gated-workflow.ts";
|
|
18
|
+
import { registerQuestionTool } from "./killeros/question.ts";
|
|
19
19
|
import { createGoalRuntime, createInitRuntime } from "./killeros/runtime.ts";
|
|
20
20
|
import { registerShellUi } from "./killeros/shell-ui.ts";
|
|
21
21
|
import { registerVariants } from "./killeros/variants.ts";
|
|
22
22
|
import { registerWorkedFor } from "./killeros/worked-for.ts";
|
|
23
|
-
import { registerWorkflowGate, type WorkflowAdapter } from "./killeros/workflow-gate.ts";
|
|
24
23
|
|
|
25
24
|
export { contextPercentRemaining, formatCost, formatContextProgress } from "./killeros/footer.ts";
|
|
26
25
|
export { executeHook } from "./killeros/hooks.ts";
|
|
27
26
|
export { INIT_WORKFLOW_PROMPT } from "./killeros/init.ts";
|
|
28
27
|
export { buildInitEvidence, listInitEvidence, readInitEvidence } from "./killeros/init-evidence.ts";
|
|
29
28
|
export { captureInitTargetBaseline, installInitAgentsFile, validateGeneratedGuidance, writeInitAgentsFile } from "./killeros/init-target.ts";
|
|
30
|
-
export { createDecisionGatedWorkflowAdapter } from "./killeros/decision-gated-workflow.ts";
|
|
31
|
-
export { explicitSkillActivation, registerWorkflowGate } from "./killeros/workflow-gate.ts";
|
|
32
|
-
export type {
|
|
33
|
-
WorkflowAdapter,
|
|
34
|
-
WorkflowGateController,
|
|
35
|
-
WorkflowGateState,
|
|
36
|
-
WorkflowPolicy,
|
|
37
|
-
WorkflowTerminalReason,
|
|
38
|
-
WorkflowToolAuthorization,
|
|
39
|
-
} from "./killeros/workflow-gate.ts";
|
|
40
|
-
|
|
41
29
|
export interface KillerosOptions {
|
|
42
30
|
completionNotifications?: CompletionNotificationDependencies;
|
|
43
|
-
decisionGatedWorkflows?: readonly WorkflowAdapter[];
|
|
44
31
|
}
|
|
45
32
|
|
|
46
33
|
export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}): void {
|
|
@@ -50,19 +37,15 @@ export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}
|
|
|
50
37
|
registerShellUi(pi, commandResolver);
|
|
51
38
|
registerGoal(pi, goalRuntime, initRuntime);
|
|
52
39
|
registerPersonalInstructions(pi, initRuntime);
|
|
53
|
-
|
|
54
|
-
registerWorkflowGate(
|
|
55
|
-
pi,
|
|
56
|
-
questionRunner,
|
|
57
|
-
options.decisionGatedWorkflows ?? [createDecisionGatedWorkflowAdapter()],
|
|
58
|
-
);
|
|
40
|
+
registerQuestionTool(pi);
|
|
59
41
|
registerAliases(pi);
|
|
60
42
|
registerSlashAutocomplete(pi, commandResolver);
|
|
61
43
|
registerFooter(pi, goalRuntime);
|
|
62
44
|
registerVariants(pi);
|
|
63
45
|
registerInitCommand(pi, initRuntime, goalRuntime);
|
|
64
46
|
registerLifecycleHooks(pi);
|
|
65
|
-
registerGoalSettlement(pi, goalRuntime, initRuntime);
|
|
47
|
+
const goalCompaction = registerGoalSettlement(pi, goalRuntime, initRuntime);
|
|
48
|
+
registerAutoCompaction(pi, { goal: goalCompaction });
|
|
66
49
|
registerInitSettlement(pi, initRuntime);
|
|
67
50
|
registerRequestActivity(pi);
|
|
68
51
|
registerCompletionNotifications(pi, options.completionNotifications);
|
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.11
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Add `-l` to either command for a project-only install. Restart Pi after installing.
|
|
@@ -44,12 +44,11 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
|
|
|
44
44
|
- Frameless multiline editor with one focus-aware `❯`, a shuffled session-stable empty-state suggestion, overflow-only scroll indicators, Shift+Enter support, and slash-command autocomplete; KillerOS preserves an editor factory configured by another extension
|
|
45
45
|
- One compact TUI transcript line reporting truthful `Done`, `Stopped`, or `Failed` settlement with elapsed time while preserving older `✻ Worked for …` entries
|
|
46
46
|
- Compact two-deck footer with session state above workspace state; model, context, and active goals stay prioritized as reasoning, time, cost, branch, and path reduce by available width
|
|
47
|
-
-
|
|
47
|
+
- Proactive turn-boundary context compaction in TUI and RPC modes, with ordinary prompts and active goals continuing safely after a successful summary
|
|
48
48
|
- Optional completion sounds after successful or failed settled requests, excluding manual aborts
|
|
49
49
|
- `/variants` selector and direct reasoning-level arguments
|
|
50
50
|
- Codex-style `/goal` with an interactive status/action panel, durable objectives, immediate pause and clear cancellation, automatic continuation, explicit completion, and durable blocker audits
|
|
51
51
|
- Automatic `/init` guideline synthesis with a frozen safe evidence map, protected existing policy, and the four packaged behavioral sections adapted from `writing-great-guidelines`
|
|
52
|
-
- Opt-in decision-gated workflows that ask a structured policy question before explicit skill expansion, preserve the selected allowlist, and fail closed across tool and session boundaries
|
|
53
52
|
- `question` tool with single-select and opt-in bounded multi-select, height-bounded option windows, configured Pi keybindings, live option/input progress, proposal previews, custom answers, history, cancellation, and compact expandable transcript rendering
|
|
54
53
|
- Mid-prompt slash completion with current Pi `0.84.2` commands, extensions, prompts, and skills; paths, URLs, and invalid commands remain plain text
|
|
55
54
|
- Goal-aware `/clear` that confirms, aborts active work, waits for settlement, and starts a new session, plus `/exit` for graceful shutdown
|
|
@@ -79,10 +78,6 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
|
|
|
79
78
|
|
|
80
79
|
The generated file uses the four behavioral sections adapted from `writing-great-guidelines`; no external skill installation is required. `/init` asks no setup questions, starts no second model process, writes no other file, and reloads Pi resources only after a successful write.
|
|
81
80
|
|
|
82
|
-
### Decision-gated workflows
|
|
83
|
-
|
|
84
|
-
Explicit `/skill:decision-gated-workflow` activation opens the shared question UI before Pi expands the skill. `Normal` allows only interview and read-only tools; `With docs` additionally permits agreed glossary, context-map, and ADR paths. The selected policy remains active until the workflow is explicitly finished or cancelled, and lifecycle changes clear it safely. Extensions can supply additional adapters through `KillerosOptions.decisionGatedWorkflows`.
|
|
85
|
-
|
|
86
81
|
### Interactive questions
|
|
87
82
|
|
|
88
83
|
Single-select remains the default. Explicit `minSelections: 1` and `maxSelections: 1` are equivalent to omitting both bounds; other single-select bounds are rejected. An agent opts into multi-select with `mode: "multiple"` and may set `minSelections` and `maxSelections`; the custom answer counts as one selection.
|
|
@@ -97,9 +92,22 @@ KillerOS activates its packaged `killeros` theme when a TUI session starts. Tool
|
|
|
97
92
|
|
|
98
93
|
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.
|
|
99
94
|
|
|
95
|
+
Automatic compaction is enabled by default when Pi's effective `compaction.enabled` setting is true. After each completed assistant turn, including its tool execution, KillerOS reads the active model's current context usage and triggers the public Pi compaction API when `remainingTokens <= max(contextWindow * percentRemaining / 100, reserveTokens)`. The default `percentRemaining` is `15`; `reserveTokens` and `keepRecentTokens` remain Pi-owned. KillerOS reads those effective settings with Pi's public `SettingsManager` using `getAgentDir()` and the current project trust state. The KillerOS-only preference lives in the same global `killeros.json` file:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"autoCompaction": {
|
|
100
|
+
"enabled": true,
|
|
101
|
+
"percentRemaining": 15
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Missing context readings skip the check. Successful ordinary-prompt compaction queues one hidden continuation; active `/goal` runs use the existing session-compaction and goal-continuation path. A failed compaction does not automatically retry. Manual `/compact` behavior is unchanged.
|
|
107
|
+
|
|
100
108
|
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.
|
|
101
109
|
|
|
102
|
-
Pi
|
|
110
|
+
Pi writes the summary, applies manual focus instructions, tracks files, retries summarization, and handles overflow recovery. KillerOS owns only the proactive turn-boundary trigger and does not replace Pi's compaction implementation. Active `/goal` work continues from the settled compaction boundary, after Pi finishes retries, compaction, and queued work.
|
|
103
111
|
|
|
104
112
|
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.
|
|
105
113
|
|
|
@@ -111,9 +119,9 @@ Lifecycle hooks are loaded from `.pi/killeros-hooks.json` at session start. Supp
|
|
|
111
119
|
|
|
112
120
|
| Mode | Behavior |
|
|
113
121
|
|---|---|
|
|
114
|
-
| TUI | All features are available, including the completion sound and tab-title indicator |
|
|
115
|
-
| RPC |
|
|
116
|
-
| Print/JSON | Interactive questions, `/goal`,
|
|
122
|
+
| TUI | All features are available, including proactive compaction, the completion sound, and the tab-title indicator |
|
|
123
|
+
| RPC | Proactive compaction and goal set/view/pause/resume/clear work; TUI components, `/goal edit`, `/init`, completion sounds, and the title indicator are disabled |
|
|
124
|
+
| Print/JSON | Interactive questions, `/goal`, `/init`, and proactive compaction are disabled; completion sounds and the title indicator are disabled |
|
|
117
125
|
|
|
118
126
|
## Validation
|
|
119
127
|
|
|
@@ -131,16 +139,9 @@ The package manifest lists Pi’s built-in modules as peer dependencies, so npm
|
|
|
131
139
|
|
|
132
140
|
## Publish
|
|
133
141
|
|
|
134
|
-
To
|
|
135
|
-
|
|
136
|
-
Do not manually tag a normal release. If automation must recover a missing GitHub release, push the matching version tag; the same workflow validates the tag against the package and changelog before creating the release.
|
|
137
|
-
|
|
138
|
-
The [`pi-package`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) keyword makes a published npm release visible in Pi’s package catalog. GitHub release automation does not publish to npm. Publish there separately after validation:
|
|
142
|
+
To release KillerOS, update the version in `package.json` and `package-lock.json`, add the matching `CHANGELOG.md` section, and push the release commit to `main`. After the full CI workflow passes, the release workflow publishes the package to npm through trusted publishing, then creates the matching tag and GitHub release from that verified commit. The [`pi-package`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) keyword makes the npm release visible in Pi’s package catalog.
|
|
139
143
|
|
|
140
|
-
|
|
141
|
-
npm login
|
|
142
|
-
npm publish
|
|
143
|
-
```
|
|
144
|
+
Do not manually tag a normal release. If automation must recover a missing GitHub release, push the matching version tag. The workflow validates the tag against the package and changelog, skips npm publication when that version already exists, and creates only the missing release.
|
|
144
145
|
|
|
145
146
|
## Security
|
|
146
147
|
|
package/killeros/activity.ts
CHANGED
|
@@ -30,7 +30,7 @@ export function formatActivityMessage(message: ActivityMessage, theme: Theme): s
|
|
|
30
30
|
detail = "assembling the answer";
|
|
31
31
|
break;
|
|
32
32
|
case "tool":
|
|
33
|
-
switch (message.toolName.trim().
|
|
33
|
+
switch (message.toolName.trim().toLowerCase()) {
|
|
34
34
|
case "read":
|
|
35
35
|
case "grep":
|
|
36
36
|
case "find":
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAgentDir,
|
|
3
|
+
SettingsManager,
|
|
4
|
+
type CompactionSettings,
|
|
5
|
+
type ContextUsage,
|
|
6
|
+
type ExtensionAPI,
|
|
7
|
+
type ExtensionContext,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { createKillerosSettingsStore } from "./settings.ts";
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_AUTO_COMPACTION_PERCENT_REMAINING = 15;
|
|
12
|
+
export const AUTO_COMPACTION_MESSAGE_TYPE = "killeros-auto-compaction";
|
|
13
|
+
export const AUTO_COMPACTION_MESSAGE = "Continue the interrupted task from the compacted context.";
|
|
14
|
+
|
|
15
|
+
export interface AutoCompactionPreference {
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
percentRemaining: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AutoCompactionGoalHandlers {
|
|
21
|
+
isActive(ctx: ExtensionContext): boolean;
|
|
22
|
+
onRequested(): void;
|
|
23
|
+
onCompleted(ctx: ExtensionContext): void;
|
|
24
|
+
onFailed(ctx: ExtensionContext, error: unknown): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AutoCompactionDependencies {
|
|
28
|
+
loadPreference?: (ctx: ExtensionContext) => AutoCompactionPreference;
|
|
29
|
+
getCompactionSettings?: (ctx: ExtensionContext) => CompactionSettings;
|
|
30
|
+
goal?: AutoCompactionGoalHandlers;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface AutoCompactionRequest {
|
|
34
|
+
goal: boolean;
|
|
35
|
+
token: symbol;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
39
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function readAutoCompactionPreference(
|
|
43
|
+
settings: Readonly<Record<string, unknown>>,
|
|
44
|
+
): AutoCompactionPreference {
|
|
45
|
+
const raw = isRecord(settings.autoCompaction) ? settings.autoCompaction : {};
|
|
46
|
+
const percentRemaining = typeof raw.percentRemaining === "number"
|
|
47
|
+
&& Number.isFinite(raw.percentRemaining)
|
|
48
|
+
&& raw.percentRemaining >= 0
|
|
49
|
+
&& raw.percentRemaining <= 100
|
|
50
|
+
? raw.percentRemaining
|
|
51
|
+
: DEFAULT_AUTO_COMPACTION_PERCENT_REMAINING;
|
|
52
|
+
return {
|
|
53
|
+
enabled: typeof raw.enabled === "boolean" ? raw.enabled : true,
|
|
54
|
+
percentRemaining,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function defaultPreference(): AutoCompactionPreference {
|
|
59
|
+
return readAutoCompactionPreference(createKillerosSettingsStore().load());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function defaultCompactionSettings(ctx: ExtensionContext): CompactionSettings {
|
|
63
|
+
return SettingsManager.create(ctx.cwd, getAgentDir(), {
|
|
64
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
65
|
+
}).getCompactionSettings();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function reserveTokens(settings: Pick<CompactionSettings, "reserveTokens">): number {
|
|
69
|
+
return typeof settings.reserveTokens === "number" && Number.isFinite(settings.reserveTokens)
|
|
70
|
+
? Math.max(0, settings.reserveTokens)
|
|
71
|
+
: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function shouldTriggerAutoCompaction(
|
|
75
|
+
usage: Pick<ContextUsage, "tokens" | "contextWindow"> | undefined,
|
|
76
|
+
preference: AutoCompactionPreference,
|
|
77
|
+
compactionSettings: Pick<CompactionSettings, "enabled" | "reserveTokens">,
|
|
78
|
+
): boolean {
|
|
79
|
+
if (!preference.enabled || !compactionSettings.enabled || usage?.tokens === undefined || usage.tokens === null) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (!Number.isFinite(usage.tokens) || !Number.isFinite(usage.contextWindow) || usage.contextWindow <= 0) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const remainingTokens = usage.contextWindow - usage.tokens;
|
|
86
|
+
const threshold = Math.max(
|
|
87
|
+
usage.contextWindow * preference.percentRemaining / 100,
|
|
88
|
+
reserveTokens(compactionSettings),
|
|
89
|
+
);
|
|
90
|
+
return remainingTokens <= threshold;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function errorMessage(error: unknown): string {
|
|
94
|
+
return error instanceof Error ? error.message : String(error);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function supportedMode(ctx: ExtensionContext): boolean {
|
|
98
|
+
return ctx.mode === "tui" || ctx.mode === "rpc";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function registerAutoCompaction(
|
|
102
|
+
pi: ExtensionAPI,
|
|
103
|
+
dependencies: AutoCompactionDependencies = {},
|
|
104
|
+
): void {
|
|
105
|
+
const loadPreference = dependencies.loadPreference ?? (() => defaultPreference());
|
|
106
|
+
const getCompactionSettings = dependencies.getCompactionSettings ?? defaultCompactionSettings;
|
|
107
|
+
let request: AutoCompactionRequest | undefined;
|
|
108
|
+
let armed = true;
|
|
109
|
+
let settingsErrorReported = false;
|
|
110
|
+
|
|
111
|
+
const resetForLifecycle = (): void => {
|
|
112
|
+
request = undefined;
|
|
113
|
+
armed = true;
|
|
114
|
+
settingsErrorReported = false;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const notifyFailure = (ctx: ExtensionContext, error: unknown): void => {
|
|
118
|
+
ctx.ui.notify(`Automatic compaction failed: ${errorMessage(error)}`, "error");
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const finishFailure = (
|
|
122
|
+
ctx: ExtensionContext,
|
|
123
|
+
token: symbol,
|
|
124
|
+
goal: boolean,
|
|
125
|
+
error: unknown,
|
|
126
|
+
): void => {
|
|
127
|
+
if (!request || request.token !== token) return;
|
|
128
|
+
request = undefined;
|
|
129
|
+
if (goal && dependencies.goal) {
|
|
130
|
+
try {
|
|
131
|
+
dependencies.goal.onFailed(ctx, error);
|
|
132
|
+
} catch (callbackError) {
|
|
133
|
+
notifyFailure(ctx, callbackError);
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
notifyFailure(ctx, error);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
pi.on("turn_end", (_event, ctx) => {
|
|
141
|
+
if (!supportedMode(ctx) || request) return;
|
|
142
|
+
|
|
143
|
+
let preference: AutoCompactionPreference;
|
|
144
|
+
let compactionSettings: CompactionSettings;
|
|
145
|
+
let usage: ContextUsage | undefined;
|
|
146
|
+
try {
|
|
147
|
+
preference = loadPreference(ctx);
|
|
148
|
+
compactionSettings = getCompactionSettings(ctx);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (!settingsErrorReported) {
|
|
151
|
+
settingsErrorReported = true;
|
|
152
|
+
ctx.ui.notify(`Automatic compaction settings could not be read: ${errorMessage(error)}`, "error");
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
usage = ctx.getContextUsage();
|
|
158
|
+
} catch {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!usage) return;
|
|
162
|
+
if (usage.tokens !== null && usage.tokens !== undefined && Number.isFinite(usage.tokens)
|
|
163
|
+
&& Number.isFinite(usage.contextWindow) && usage.contextWindow > 0) {
|
|
164
|
+
const remainingTokens = usage.contextWindow - usage.tokens;
|
|
165
|
+
const threshold = Math.max(
|
|
166
|
+
usage.contextWindow * preference.percentRemaining / 100,
|
|
167
|
+
reserveTokens(compactionSettings),
|
|
168
|
+
);
|
|
169
|
+
if (remainingTokens > threshold) {
|
|
170
|
+
armed = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!armed) return;
|
|
174
|
+
if (!shouldTriggerAutoCompaction(usage, preference, compactionSettings)) return;
|
|
175
|
+
|
|
176
|
+
armed = false;
|
|
177
|
+
const token = Symbol();
|
|
178
|
+
const goal = dependencies.goal?.isActive(ctx) === true;
|
|
179
|
+
request = { goal, token };
|
|
180
|
+
if (goal && dependencies.goal) {
|
|
181
|
+
try {
|
|
182
|
+
dependencies.goal.onRequested();
|
|
183
|
+
} catch (error) {
|
|
184
|
+
finishFailure(ctx, token, true, error);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
ctx.compact({
|
|
191
|
+
onComplete: () => {
|
|
192
|
+
if (!request || request.token !== token) return;
|
|
193
|
+
request = undefined;
|
|
194
|
+
if (goal && dependencies.goal) {
|
|
195
|
+
try {
|
|
196
|
+
dependencies.goal.onCompleted(ctx);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
notifyFailure(ctx, error);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
pi.sendMessage({
|
|
204
|
+
customType: AUTO_COMPACTION_MESSAGE_TYPE,
|
|
205
|
+
content: AUTO_COMPACTION_MESSAGE,
|
|
206
|
+
display: false,
|
|
207
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
208
|
+
} catch (error) {
|
|
209
|
+
notifyFailure(ctx, error);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
onError: (error) => finishFailure(ctx, token, goal, error),
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
finishFailure(ctx, token, goal, error);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
pi.on("session_start", resetForLifecycle);
|
|
220
|
+
pi.on("session_shutdown", resetForLifecycle);
|
|
221
|
+
pi.on("session_tree", resetForLifecycle);
|
|
222
|
+
pi.on("session_before_switch", resetForLifecycle);
|
|
223
|
+
pi.on("session_before_fork", resetForLifecycle);
|
|
224
|
+
}
|
package/killeros/commands.ts
CHANGED
|
@@ -166,8 +166,8 @@ export function createSlashCommandResolver(
|
|
|
166
166
|
|
|
167
167
|
function scoreCommandMatch(name: string, prefix: string): number {
|
|
168
168
|
if (!prefix) return 1;
|
|
169
|
-
const normalizedName = name.
|
|
170
|
-
const normalizedPrefix = prefix.
|
|
169
|
+
const normalizedName = name.toLowerCase();
|
|
170
|
+
const normalizedPrefix = prefix.toLowerCase();
|
|
171
171
|
if (normalizedName.startsWith(normalizedPrefix)) return 100;
|
|
172
172
|
if (normalizedName.split(/[:\-_]/).some((token) => token.startsWith(normalizedPrefix))) return 80;
|
|
173
173
|
if (normalizedName.includes(normalizedPrefix)) return 50;
|
|
@@ -190,7 +190,7 @@ export function registerSlashAutocomplete(
|
|
|
190
190
|
const prefixMatch = getSlashCommandPrefix(beforeCursor);
|
|
191
191
|
if (!prefixMatch) return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
192
192
|
|
|
193
|
-
const prefix = prefixMatch.prefix.
|
|
193
|
+
const prefix = prefixMatch.prefix.toLowerCase();
|
|
194
194
|
const baseSuggestions = await current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
195
195
|
resolver.updateFallbackCommands(baseSuggestions?.items ?? []);
|
|
196
196
|
const commands = resolver.getCommandCatalog(baseSuggestions?.items ?? []);
|
package/killeros/display.ts
CHANGED
|
@@ -6,8 +6,8 @@ export function formatCwd(cwd: string): string {
|
|
|
6
6
|
if (!home) return cwd;
|
|
7
7
|
const normalizedHome = home.replace(/[\\/]+$/, "");
|
|
8
8
|
const normalizedCwd = cwd.replace(/[\\/]+$/, "");
|
|
9
|
-
const comparedHome = process.platform === "win32" ? normalizedHome.
|
|
10
|
-
const comparedCwd = process.platform === "win32" ? normalizedCwd.
|
|
9
|
+
const comparedHome = process.platform === "win32" ? normalizedHome.toLowerCase() : normalizedHome;
|
|
10
|
+
const comparedCwd = process.platform === "win32" ? normalizedCwd.toLowerCase() : normalizedCwd;
|
|
11
11
|
if (comparedCwd === comparedHome) return "~";
|
|
12
12
|
const separator = normalizedCwd.slice(normalizedHome.length, normalizedHome.length + 1);
|
|
13
13
|
return comparedCwd.startsWith(comparedHome) && (separator === "/" || separator === "\\")
|
package/killeros/footer.ts
CHANGED
|
@@ -75,12 +75,12 @@ const PROVIDER_WORDS: Readonly<Record<string, string>> = {
|
|
|
75
75
|
|
|
76
76
|
function formatProviderName(provider: string): string {
|
|
77
77
|
const normalized = provider.trim();
|
|
78
|
-
const known = PROVIDER_LABELS[normalized.
|
|
78
|
+
const known = PROVIDER_LABELS[normalized.toLowerCase()];
|
|
79
79
|
if (known) return known;
|
|
80
80
|
return normalized
|
|
81
81
|
.split(/[-_]+/u)
|
|
82
82
|
.filter(Boolean)
|
|
83
|
-
.map((word) => PROVIDER_WORDS[word.
|
|
83
|
+
.map((word) => PROVIDER_WORDS[word.toLowerCase()] ?? `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
|
|
84
84
|
.join(" ") || "Unknown provider";
|
|
85
85
|
}
|
|
86
86
|
|
package/killeros/goals.ts
CHANGED
|
@@ -264,7 +264,10 @@ function transitionGoal(
|
|
|
264
264
|
resumeAfterManualCompaction: options.resumeAfterManualCompaction,
|
|
265
265
|
};
|
|
266
266
|
persistGoalState(pi, runtime, event, next);
|
|
267
|
-
if (status !== "active")
|
|
267
|
+
if (status !== "active") {
|
|
268
|
+
runtime.continuationScheduled = false;
|
|
269
|
+
runtime.automaticCompaction = undefined;
|
|
270
|
+
}
|
|
268
271
|
return next;
|
|
269
272
|
}
|
|
270
273
|
|
|
@@ -272,6 +275,7 @@ function clearGoalExecutionFlags(runtime: GoalRuntime): void {
|
|
|
272
275
|
runtime.continuationScheduled = false;
|
|
273
276
|
runtime.goalTurnInFlight = false;
|
|
274
277
|
runtime.agentEndObserved = false;
|
|
278
|
+
runtime.automaticCompaction = undefined;
|
|
275
279
|
runtime.lastStopReason = undefined;
|
|
276
280
|
runtime.lastError = undefined;
|
|
277
281
|
}
|
|
@@ -287,7 +291,7 @@ async function stopGoalRun(runtime: GoalRuntime, ctx: ExtensionCommandContext, s
|
|
|
287
291
|
}
|
|
288
292
|
|
|
289
293
|
function goalStatusLabel(status: GoalStatus): string {
|
|
290
|
-
return `${status.charAt(0).
|
|
294
|
+
return `${status.charAt(0).toUpperCase()}${status.slice(1)}`;
|
|
291
295
|
}
|
|
292
296
|
|
|
293
297
|
function goalPanelActions(status: GoalStatus): Array<{ label: string; control: "pause" | "resume" | "edit" | "clear" }> {
|
|
@@ -305,7 +309,7 @@ function goalPanelActions(status: GoalStatus): Array<{ label: string; control: "
|
|
|
305
309
|
function goalStatusSummary(state: GoalState, ctx: ExtensionContext): string {
|
|
306
310
|
const usedTokens = Math.max(0, sumGoalTokens(ctx) - state.baselineTokens);
|
|
307
311
|
const lines = [
|
|
308
|
-
`Goal ${goalStatusLabel(state.status).
|
|
312
|
+
`Goal ${goalStatusLabel(state.status).toLowerCase()} · ${state.turns} turn${state.turns === 1 ? "" : "s"} · ${formatTime(goalElapsedMilliseconds(state))} · ${formatTokens(usedTokens)} tokens`,
|
|
309
313
|
state.objective,
|
|
310
314
|
];
|
|
311
315
|
if (state.result) lines.push(state.result);
|
|
@@ -333,6 +337,7 @@ export function pauseGoalAfterFailure(
|
|
|
333
337
|
syncGoalUpdateTool(pi, runtime);
|
|
334
338
|
runtime.persistenceRetryNeeded = true;
|
|
335
339
|
runtime.continuationScheduled = false;
|
|
340
|
+
runtime.automaticCompaction = undefined;
|
|
336
341
|
runtime.requestRender?.();
|
|
337
342
|
}
|
|
338
343
|
if (notify) ctx.ui.notify(`Goal paused: ${reason}\n${recoveryInstruction}`, "error");
|
|
@@ -359,6 +364,7 @@ function pauseGoalForPossibleManualCompaction(
|
|
|
359
364
|
syncGoalUpdateTool(pi, runtime);
|
|
360
365
|
runtime.persistenceRetryNeeded = true;
|
|
361
366
|
runtime.continuationScheduled = false;
|
|
367
|
+
runtime.automaticCompaction = undefined;
|
|
362
368
|
runtime.requestRender?.();
|
|
363
369
|
}
|
|
364
370
|
ctx.ui.notify(
|
|
@@ -384,6 +390,7 @@ function recoverGoalAfterManualCompaction(
|
|
|
384
390
|
return false;
|
|
385
391
|
}
|
|
386
392
|
runtime.continuationScheduled = false;
|
|
393
|
+
runtime.automaticCompaction = undefined;
|
|
387
394
|
ctx.ui.notify("Manual compaction complete. Goal resumed.", "info");
|
|
388
395
|
setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
|
|
389
396
|
return true;
|
|
@@ -450,6 +457,42 @@ function scheduleGoalContinuation(
|
|
|
450
457
|
}
|
|
451
458
|
}
|
|
452
459
|
|
|
460
|
+
function completeAutomaticCompaction(
|
|
461
|
+
pi: ExtensionAPI,
|
|
462
|
+
runtime: GoalRuntime,
|
|
463
|
+
initState: InitRuntime,
|
|
464
|
+
ctx: ExtensionContext,
|
|
465
|
+
): void {
|
|
466
|
+
if (runtime.automaticCompaction === undefined) return;
|
|
467
|
+
if (runtime.state?.status !== "active" || initState.active) {
|
|
468
|
+
runtime.automaticCompaction = undefined;
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
runtime.automaticCompaction = "completed";
|
|
472
|
+
if (runtime.goalTurnInFlight || !ctx.isIdle()) return;
|
|
473
|
+
runtime.automaticCompaction = undefined;
|
|
474
|
+
setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function failAutomaticCompaction(
|
|
478
|
+
pi: ExtensionAPI,
|
|
479
|
+
runtime: GoalRuntime,
|
|
480
|
+
ctx: ExtensionContext,
|
|
481
|
+
error: unknown,
|
|
482
|
+
): void {
|
|
483
|
+
if (runtime.automaticCompaction === undefined) return;
|
|
484
|
+
runtime.automaticCompaction = undefined;
|
|
485
|
+
if (runtime.state?.status !== "active") return;
|
|
486
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
487
|
+
pauseGoalAfterFailure(
|
|
488
|
+
pi,
|
|
489
|
+
runtime,
|
|
490
|
+
ctx,
|
|
491
|
+
`automatic compaction failed: ${reason}`,
|
|
492
|
+
"Automatic continuation is stopped. Run /goal resume after resolving the compaction problem.",
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
453
496
|
function goalInstructions(state: GoalState, heading: string): string {
|
|
454
497
|
return [
|
|
455
498
|
`# ${heading}`,
|
|
@@ -598,6 +641,7 @@ export function registerGoal(
|
|
|
598
641
|
runtime.continuationHeld = false;
|
|
599
642
|
runtime.goalTurnInFlight = false;
|
|
600
643
|
runtime.agentEndObserved = false;
|
|
644
|
+
runtime.automaticCompaction = undefined;
|
|
601
645
|
runtime.persistenceRetryNeeded = false;
|
|
602
646
|
runtime.lastStopReason = undefined;
|
|
603
647
|
runtime.lastError = undefined;
|
|
@@ -630,6 +674,7 @@ export function registerGoal(
|
|
|
630
674
|
runtime.continuationHeld = false;
|
|
631
675
|
runtime.goalTurnInFlight = false;
|
|
632
676
|
runtime.agentEndObserved = false;
|
|
677
|
+
runtime.automaticCompaction = undefined;
|
|
633
678
|
runtime.persistenceRetryNeeded = false;
|
|
634
679
|
runtime.lastStopReason = undefined;
|
|
635
680
|
runtime.lastError = undefined;
|
|
@@ -663,7 +708,7 @@ export function registerGoal(
|
|
|
663
708
|
return;
|
|
664
709
|
}
|
|
665
710
|
const input = args.trim();
|
|
666
|
-
const control = input.
|
|
711
|
+
const control = input.toLowerCase();
|
|
667
712
|
const isControl = control === "clear" || control === "edit" || control === "pause" || control === "resume";
|
|
668
713
|
|
|
669
714
|
if (!input) {
|
|
@@ -963,7 +1008,7 @@ export function registerGoal(
|
|
|
963
1008
|
pi.registerCommand("goal", {
|
|
964
1009
|
description: "Set or view the goal for a long-running task",
|
|
965
1010
|
getArgumentCompletions: (prefix) => {
|
|
966
|
-
const normalized = prefix.trimStart().
|
|
1011
|
+
const normalized = prefix.trimStart().toLowerCase();
|
|
967
1012
|
if (normalized.includes(" ")) return null;
|
|
968
1013
|
const actions = [
|
|
969
1014
|
{ value: "clear", description: "Remove the current goal" },
|
|
@@ -983,7 +1028,12 @@ export function registerGoalSettlement(
|
|
|
983
1028
|
pi: ExtensionAPI,
|
|
984
1029
|
runtime: GoalRuntime,
|
|
985
1030
|
initState: InitRuntime,
|
|
986
|
-
):
|
|
1031
|
+
): {
|
|
1032
|
+
isActive(ctx: ExtensionContext): boolean;
|
|
1033
|
+
onRequested(): void;
|
|
1034
|
+
onCompleted(ctx: ExtensionContext): void;
|
|
1035
|
+
onFailed(ctx: ExtensionContext, error: unknown): void;
|
|
1036
|
+
} {
|
|
987
1037
|
pi.on("agent_settled", (_event, ctx) => {
|
|
988
1038
|
const wasGoalTurn = runtime.goalTurnInFlight;
|
|
989
1039
|
const continuationWasScheduled = runtime.continuationScheduled;
|
|
@@ -1000,6 +1050,7 @@ export function registerGoalSettlement(
|
|
|
1000
1050
|
return;
|
|
1001
1051
|
}
|
|
1002
1052
|
if (!agentEndObserved) {
|
|
1053
|
+
if (runtime.automaticCompaction !== undefined) return;
|
|
1003
1054
|
pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn ended without an agent result");
|
|
1004
1055
|
return;
|
|
1005
1056
|
}
|
|
@@ -1007,6 +1058,12 @@ export function registerGoalSettlement(
|
|
|
1007
1058
|
const reason = runtime.lastError || "the agent turn was aborted";
|
|
1008
1059
|
runtime.lastStopReason = undefined;
|
|
1009
1060
|
runtime.lastError = undefined;
|
|
1061
|
+
if (runtime.automaticCompaction !== undefined) {
|
|
1062
|
+
if (runtime.automaticCompaction === "completed") {
|
|
1063
|
+
completeAutomaticCompaction(pi, runtime, initState, ctx);
|
|
1064
|
+
}
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1010
1067
|
pauseGoalForPossibleManualCompaction(pi, runtime, ctx, reason);
|
|
1011
1068
|
return;
|
|
1012
1069
|
}
|
|
@@ -1023,7 +1080,23 @@ export function registerGoalSettlement(
|
|
|
1023
1080
|
});
|
|
1024
1081
|
|
|
1025
1082
|
pi.on("session_compact", (event, ctx) => {
|
|
1083
|
+
if (runtime.automaticCompaction !== undefined) {
|
|
1084
|
+
completeAutomaticCompaction(pi, runtime, initState, ctx);
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1026
1087
|
if (event.reason !== "manual") return;
|
|
1027
1088
|
recoverGoalAfterManualCompaction(pi, runtime, initState, ctx);
|
|
1028
1089
|
});
|
|
1090
|
+
|
|
1091
|
+
return {
|
|
1092
|
+
isActive: (ctx: ExtensionContext): boolean => isGoalModeSupported(ctx)
|
|
1093
|
+
&& isSavedSession(ctx)
|
|
1094
|
+
&& runtime.state?.status === "active"
|
|
1095
|
+
&& !initState.active,
|
|
1096
|
+
onRequested: (): void => {
|
|
1097
|
+
if (runtime.state?.status === "active") runtime.automaticCompaction = "pending";
|
|
1098
|
+
},
|
|
1099
|
+
onCompleted: (ctx: ExtensionContext): void => completeAutomaticCompaction(pi, runtime, initState, ctx),
|
|
1100
|
+
onFailed: (ctx: ExtensionContext, error: unknown): void => failAutomaticCompaction(pi, runtime, ctx, error),
|
|
1101
|
+
};
|
|
1029
1102
|
}
|