@aragon-agent/cli 0.6.15 → 0.6.17
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/README.md +108 -71
- package/dist/agent/controller.d.ts +10 -0
- package/dist/agent/controller.js +52 -47
- package/dist/agent/reducer.js +0 -4
- package/dist/cli.js +49 -15
- package/dist/commands/builtins.js +27 -43
- package/dist/compaction/child.d.ts +10 -8
- package/dist/compaction/child.js +23 -25
- package/dist/compaction/context-command.js +14 -3
- package/dist/compaction/limits.d.ts +0 -2
- package/dist/compaction/limits.js +0 -2
- package/dist/compaction/types.d.ts +6 -1
- package/dist/config/app-paths.d.ts +10 -0
- package/dist/config/app-paths.js +12 -0
- package/dist/config/cli-commands.d.ts +1 -1
- package/dist/config/cli-commands.js +16 -15
- package/dist/config/env.d.ts +2 -1
- package/dist/config/env.js +13 -1
- package/dist/config/load.d.ts +9 -17
- package/dist/config/load.js +14 -6
- package/dist/config/model-windows.d.ts +91 -0
- package/dist/config/model-windows.js +181 -0
- package/dist/config/schema.d.ts +34 -5
- package/dist/config/schema.js +15 -11
- package/dist/exec/events.d.ts +12 -8
- package/dist/exec/events.js +2 -2
- package/dist/exec/runner.js +4 -1
- package/dist/fast/limits.d.ts +2 -2
- package/dist/fast/limits.js +2 -2
- package/dist/fast/prompt.d.ts +10 -11
- package/dist/fast/prompt.js +19 -20
- package/dist/fast/reviewer.d.ts +1 -1
- package/dist/fast/reviewer.js +1 -1
- package/dist/fast/types.d.ts +0 -2
- package/dist/fast/wiring.d.ts +18 -34
- package/dist/fast/wiring.js +20 -51
- package/dist/input/csiu-keys.d.ts +66 -0
- package/dist/input/csiu-keys.js +170 -0
- package/dist/input/keyboard-enhancement.d.ts +42 -0
- package/dist/input/keyboard-enhancement.js +65 -0
- package/dist/input/stdin-filter.d.ts +9 -0
- package/dist/input/stdin-filter.js +45 -9
- package/dist/input/win32-input-mode.d.ts +61 -0
- package/dist/input/win32-input-mode.js +251 -0
- package/dist/session/validate-session.js +3 -1
- package/dist/team/child-reviewer.d.ts +105 -0
- package/dist/team/child-reviewer.js +117 -0
- package/dist/team/normalize.d.ts +7 -26
- package/dist/team/normalize.js +9 -22
- package/dist/team/prompt.d.ts +0 -11
- package/dist/team/prompt.js +3 -12
- package/dist/team/report.d.ts +4 -19
- package/dist/team/report.js +1 -27
- package/dist/team/runtime.d.ts +5 -10
- package/dist/team/runtime.js +35 -39
- package/dist/team/subagent.d.ts +18 -18
- package/dist/team/subagent.js +80 -30
- package/dist/team/task-tool.d.ts +6 -26
- package/dist/team/task-tool.js +13 -47
- package/dist/team/types.d.ts +3 -42
- package/dist/ui/App.js +3 -4
- package/dist/ui/BottomStatusRow.js +2 -1
- package/dist/ui/StatusBar.js +2 -1
- package/dist/ui/TeamPanel.js +1 -1
- package/dist/ui/entries/TeamCard.js +1 -6
- package/dist/ui/layout/status-detail-layout.js +12 -2
- package/dist/ui/layout/status-layout.d.ts +4 -2
- package/dist/ui/layout/status-layout.js +24 -1
- package/dist/ui/overlays/HelpOverlay.js +1 -2
- package/package.json +2 -2
- package/dist/ui/ModeChip.d.ts +0 -18
- package/dist/ui/ModeChip.js +0 -24
package/dist/config/load.d.ts
CHANGED
|
@@ -59,6 +59,15 @@ export interface CliFlags {
|
|
|
59
59
|
* `false` on every run that passed no flag at all.
|
|
60
60
|
*/
|
|
61
61
|
paste?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* `--keyboard-enhancement` ⇒ true, `--no-keyboard-enhancement` ⇒ false,
|
|
64
|
+
* absent ⇒ undefined.
|
|
65
|
+
*
|
|
66
|
+
* DECLARED AS A PAIR and carried through `toFlags`, for the reason the
|
|
67
|
+
* `paste` note above records: the key is persisted and defaults to
|
|
68
|
+
* `true`, so a lone negative flag must not read as silence.
|
|
69
|
+
*/
|
|
70
|
+
keyboardEnhancement?: boolean;
|
|
62
71
|
/** `--no-skills` ⇒ false. Turns the whole skill subsystem off (§7.5). */
|
|
63
72
|
skills?: boolean;
|
|
64
73
|
/** `--skill <name>` ×N — force Level 2 injection for this run. */
|
|
@@ -184,23 +193,6 @@ export interface CliFlags {
|
|
|
184
193
|
* to keep in step with `toFlags`.
|
|
185
194
|
*/
|
|
186
195
|
fastReview?: string;
|
|
187
|
-
/**
|
|
188
|
-
* `--fast-delegate` ⇒ true, `--no-fast-delegate` ⇒ false, absent ⇒ undefined
|
|
189
|
-
* (web-use-tier-cooperation-and-control-closure §4.1.5).
|
|
190
|
-
*
|
|
191
|
-
* DECLARED AS A PAIR — MANDATORY, for the reason `--team`, `--todo-panel`,
|
|
192
|
-
* `--render-governor`, `--retry` and `--fast` each record in turn, and here
|
|
193
|
-
* with the SAME polarity as `--update`: `fast.delegate` is PERSISTED and
|
|
194
|
-
* defaults to `true`, so a lone `--no-fast-delegate` would make commander
|
|
195
|
-
* default `opts.fastDelegate` to `true` and silently overwrite a stored
|
|
196
|
-
* `false` on every run that passed no flag at all.
|
|
197
|
-
*
|
|
198
|
-
* It exists because it is the ONLY `fast.*` sub-switch with no channel a
|
|
199
|
-
* wrapper can reach: `--fast-review <n|off>` already covers `review` and
|
|
200
|
-
* `reviewEveryTurns`, and `enabled` / `model` / `provider` have both a flag
|
|
201
|
-
* and an environment variable.
|
|
202
|
-
*/
|
|
203
|
-
fastDelegate?: boolean;
|
|
204
196
|
/**
|
|
205
197
|
* `--update` ⇒ true, `--no-update` ⇒ false, absent ⇒ undefined
|
|
206
198
|
* (cli-auto-update §4.2).
|
package/dist/config/load.js
CHANGED
|
@@ -78,6 +78,19 @@ function resolvePaste(flags, env, file) {
|
|
|
78
78
|
return file.paste;
|
|
79
79
|
return DEFAULT_CONFIG.paste;
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Keyboard enhancement: flag > env > file > default `true`. Shaped exactly
|
|
83
|
+
* like `resolvePaste` above, for the reason its own note gives.
|
|
84
|
+
*/
|
|
85
|
+
function resolveKeyboardEnhancement(flags, env, file) {
|
|
86
|
+
if (flags.keyboardEnhancement !== undefined)
|
|
87
|
+
return flags.keyboardEnhancement;
|
|
88
|
+
if (env.keyboardEnhancement !== undefined)
|
|
89
|
+
return env.keyboardEnhancement;
|
|
90
|
+
if (file.keyboardEnhancement !== undefined)
|
|
91
|
+
return file.keyboardEnhancement;
|
|
92
|
+
return DEFAULT_CONFIG.keyboardEnhancement;
|
|
93
|
+
}
|
|
81
94
|
/**
|
|
82
95
|
* Idle scroll-resume delay: env › file › default 5000, clamped to [0, 120000].
|
|
83
96
|
*
|
|
@@ -230,12 +243,6 @@ function resolveFastConfig(flags, env, file) {
|
|
|
230
243
|
...(flags.fastProvider !== undefined && flags.fastProvider !== ''
|
|
231
244
|
? { provider: flags.fastProvider }
|
|
232
245
|
: {}),
|
|
233
|
-
// `!== undefined`, for the reason stated on `flags.fast` above: commander
|
|
234
|
-
// materialises a lone `--no-fast-delegate` as `opts.fastDelegate = true`
|
|
235
|
-
// when the flag is absent, and `delegate` defaults to TRUE — so a truthiness
|
|
236
|
-
// check would force it on for every run that passed no flag at all,
|
|
237
|
-
// overwriting a stored `false` with nothing to show for it.
|
|
238
|
-
...(flags.fastDelegate !== undefined ? { delegate: flags.fastDelegate } : {}),
|
|
239
246
|
...reviewPatch,
|
|
240
247
|
});
|
|
241
248
|
}
|
|
@@ -585,6 +592,7 @@ export function loadConfig(flags = {}, options = {}) {
|
|
|
585
592
|
mouse: resolveMouse(flags, env.partial, file),
|
|
586
593
|
mouseSelect: resolveMouseSelect(flags, env.partial, file),
|
|
587
594
|
paste: resolvePaste(flags, env.partial, file),
|
|
595
|
+
keyboardEnhancement: resolveKeyboardEnhancement(flags, env.partial, file),
|
|
588
596
|
scrollResumeMs: resolveScrollResumeMs(env.partial, file),
|
|
589
597
|
// From `<home>/state.json`, not the config file — see `ui-state.ts`.
|
|
590
598
|
submitCount: readSubmitCount(),
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-declared context windows, read from `~/.aragon-agent/model-windows.json`
|
|
3
|
+
* (path owned by `app-paths.ts`).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS. `ModelRegistry.getContextWindow` resolves a window from
|
|
6
|
+
* API discovery, the builtin model lists and the ten-entry catalog - and for
|
|
7
|
+
* every other id (a gateway alias, a self-hosted model, a release too new for
|
|
8
|
+
* the table) it returns the explicitly-untrusted 128k `fallback`. The gauge
|
|
9
|
+
* then renders `?` forever, by design: it refuses to divide by a guessed
|
|
10
|
+
* denominator. The one number that can make the denominator real is the user's
|
|
11
|
+
* own, and `config.json`'s `contextWindow` key can only hold ONE of it. This
|
|
12
|
+
* file is the per-model form of the same assertion.
|
|
13
|
+
*
|
|
14
|
+
* THE APPLICATION NEVER WRITES IT. A file the user owns by hand cannot be
|
|
15
|
+
* clobbered by an upgrade, and its absence is the ordinary case, not an error.
|
|
16
|
+
*
|
|
17
|
+
* ASCII ONLY: `src/config/**` is inside the glyph scanner's scope
|
|
18
|
+
* (`glyphs.test.ts::inScope`).
|
|
19
|
+
*/
|
|
20
|
+
/** Structural bounds only; there is no policy surface to tune here. */
|
|
21
|
+
export interface ModelWindowsLimits {
|
|
22
|
+
/** Entries beyond this many are dropped, in file order. */
|
|
23
|
+
readonly maxEntries: number;
|
|
24
|
+
/** Keys longer than this are dropped rather than trusted as ids. */
|
|
25
|
+
readonly maxKeyLength: number;
|
|
26
|
+
/** A window below the smallest real context (1k) is a typo, not a limit. */
|
|
27
|
+
readonly minWindow: number;
|
|
28
|
+
/** A window above 1e9 tokens is also a typo; no model ships that today. */
|
|
29
|
+
readonly maxWindow: number;
|
|
30
|
+
}
|
|
31
|
+
export declare const MODEL_WINDOWS_LIMITS: ModelWindowsLimits;
|
|
32
|
+
/** Normalized model id -> declared window. Keys are stored ALREADY normalized. */
|
|
33
|
+
export type ModelWindowTable = ReadonlyMap<string, number>;
|
|
34
|
+
/**
|
|
35
|
+
* Same normalization core's `catalogModelId` applies, restated here because it
|
|
36
|
+
* is not part of Core's public surface and a second exporter would be a
|
|
37
|
+
* dependency on an internal. Keep the two in step: a date-stamped id
|
|
38
|
+
* (`kimi-k3-20260716`) and its bare form must resolve to one entry.
|
|
39
|
+
*/
|
|
40
|
+
export declare function normalizeModelKey(modelId: string): string;
|
|
41
|
+
/** What a parse produced, and whether the FILE itself was at fault. */
|
|
42
|
+
export interface ModelWindowsParse {
|
|
43
|
+
table: ModelWindowTable;
|
|
44
|
+
/**
|
|
45
|
+
* True only when the text could not be understood at all (bad JSON, wrong
|
|
46
|
+
* top-level type). A valid file with zero USABLE entries is `false`: the
|
|
47
|
+
* user wrote JSON, the values were the problem, and every dead value was
|
|
48
|
+
* already dropped rather than trusted.
|
|
49
|
+
*/
|
|
50
|
+
malformed: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Parse already-decoded text into a table. NEVER THROWS - a hand-edited file
|
|
54
|
+
* is the rule here, not the exception, and a typo must cost the user one dead
|
|
55
|
+
* entry, not the session. Two accepted shapes:
|
|
56
|
+
*
|
|
57
|
+
* { "version": 1, "windows": { "kimi-k3": 1048576 } }
|
|
58
|
+
* { "kimi-k3": 1048576 } (bare map, same effect)
|
|
59
|
+
*
|
|
60
|
+
* The wrapper exists so the format can grow fields without a migration; the
|
|
61
|
+
* bare map is accepted because the first shape a user types is the simple one.
|
|
62
|
+
* A leading UTF-8 BOM is tolerated: `readFileSync(..., 'utf8')` does not strip
|
|
63
|
+
* it, `JSON.parse` cannot parse it, and Notepad writes it by default.
|
|
64
|
+
*/
|
|
65
|
+
export declare function parseModelWindowsResult(text: string): ModelWindowsParse;
|
|
66
|
+
/** The table alone, for callers that do not care why it might be empty. */
|
|
67
|
+
export declare function parseModelWindows(text: string): ModelWindowTable;
|
|
68
|
+
/**
|
|
69
|
+
* mtime-cached lookup over the file.
|
|
70
|
+
*
|
|
71
|
+
* WHY A STAT PER LOOKUP RATHER THAN A LOAD AT STARTUP: `getModelInfoFor` is
|
|
72
|
+
* called on every measurement, and the meter's `refreshValidity` re-reads the
|
|
73
|
+
* window precisely so a moved denominator (a `/model` switch, a live config
|
|
74
|
+
* edit) re-measures without a relaunch. A user saving this file mid-session
|
|
75
|
+
* gets the same treatment - `statSync` is microseconds, and the read itself
|
|
76
|
+
* only happens when the stamp actually changes.
|
|
77
|
+
*
|
|
78
|
+
* THE STAMP IS mtimeMs+size, so a rewrite that lands in the SAME millisecond
|
|
79
|
+
* with the SAME byte count serves the previous table once. Squeezing that
|
|
80
|
+
* window shut would need a content hash per lookup, which is exactly the cost
|
|
81
|
+
* the stamp exists to avoid; the next differing write clears it regardless.
|
|
82
|
+
*/
|
|
83
|
+
export declare class ModelWindows {
|
|
84
|
+
private readonly path;
|
|
85
|
+
private table;
|
|
86
|
+
private stamp;
|
|
87
|
+
constructor(path?: string);
|
|
88
|
+
/** The declared window for `modelId`, or `undefined` when nobody declared one. */
|
|
89
|
+
lookup(modelId: string): number | undefined;
|
|
90
|
+
private refresh;
|
|
91
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-declared context windows, read from `~/.aragon-agent/model-windows.json`
|
|
3
|
+
* (path owned by `app-paths.ts`).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS. `ModelRegistry.getContextWindow` resolves a window from
|
|
6
|
+
* API discovery, the builtin model lists and the ten-entry catalog - and for
|
|
7
|
+
* every other id (a gateway alias, a self-hosted model, a release too new for
|
|
8
|
+
* the table) it returns the explicitly-untrusted 128k `fallback`. The gauge
|
|
9
|
+
* then renders `?` forever, by design: it refuses to divide by a guessed
|
|
10
|
+
* denominator. The one number that can make the denominator real is the user's
|
|
11
|
+
* own, and `config.json`'s `contextWindow` key can only hold ONE of it. This
|
|
12
|
+
* file is the per-model form of the same assertion.
|
|
13
|
+
*
|
|
14
|
+
* THE APPLICATION NEVER WRITES IT. A file the user owns by hand cannot be
|
|
15
|
+
* clobbered by an upgrade, and its absence is the ordinary case, not an error.
|
|
16
|
+
*
|
|
17
|
+
* ASCII ONLY: `src/config/**` is inside the glyph scanner's scope
|
|
18
|
+
* (`glyphs.test.ts::inScope`).
|
|
19
|
+
*/
|
|
20
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
21
|
+
import { getLogger } from '../logging/logger.js';
|
|
22
|
+
import { getModelWindowsPath } from './app-paths.js';
|
|
23
|
+
export const MODEL_WINDOWS_LIMITS = {
|
|
24
|
+
maxEntries: 512,
|
|
25
|
+
maxKeyLength: 200,
|
|
26
|
+
minWindow: 1_024,
|
|
27
|
+
maxWindow: 1_000_000_000,
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Same normalization core's `catalogModelId` applies, restated here because it
|
|
31
|
+
* is not part of Core's public surface and a second exporter would be a
|
|
32
|
+
* dependency on an internal. Keep the two in step: a date-stamped id
|
|
33
|
+
* (`kimi-k3-20260716`) and its bare form must resolve to one entry.
|
|
34
|
+
*/
|
|
35
|
+
export function normalizeModelKey(modelId) {
|
|
36
|
+
return modelId.trim().toLowerCase()
|
|
37
|
+
.replace(/^(models|openai|anthropic|google)\//, '')
|
|
38
|
+
.replace(/-(?:\d{4}-\d{2}-\d{2}|\d{8})$/, '');
|
|
39
|
+
}
|
|
40
|
+
function isWindowValue(value) {
|
|
41
|
+
return typeof value === 'number' && Number.isSafeInteger(value) &&
|
|
42
|
+
value >= MODEL_WINDOWS_LIMITS.minWindow && value <= MODEL_WINDOWS_LIMITS.maxWindow;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parse already-decoded text into a table. NEVER THROWS - a hand-edited file
|
|
46
|
+
* is the rule here, not the exception, and a typo must cost the user one dead
|
|
47
|
+
* entry, not the session. Two accepted shapes:
|
|
48
|
+
*
|
|
49
|
+
* { "version": 1, "windows": { "kimi-k3": 1048576 } }
|
|
50
|
+
* { "kimi-k3": 1048576 } (bare map, same effect)
|
|
51
|
+
*
|
|
52
|
+
* The wrapper exists so the format can grow fields without a migration; the
|
|
53
|
+
* bare map is accepted because the first shape a user types is the simple one.
|
|
54
|
+
* A leading UTF-8 BOM is tolerated: `readFileSync(..., 'utf8')` does not strip
|
|
55
|
+
* it, `JSON.parse` cannot parse it, and Notepad writes it by default.
|
|
56
|
+
*/
|
|
57
|
+
export function parseModelWindowsResult(text) {
|
|
58
|
+
const body = text.replace(/^\uFEFF/, '');
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(body);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return { table: new Map(), malformed: true };
|
|
65
|
+
}
|
|
66
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
67
|
+
return { table: new Map(), malformed: true };
|
|
68
|
+
}
|
|
69
|
+
const record = parsed;
|
|
70
|
+
const source = record.windows !== undefined && record.windows !== null &&
|
|
71
|
+
typeof record.windows === 'object' && !Array.isArray(record.windows)
|
|
72
|
+
? record.windows
|
|
73
|
+
: record;
|
|
74
|
+
const table = new Map();
|
|
75
|
+
for (const [rawKey, rawValue] of Object.entries(source)) {
|
|
76
|
+
const key = normalizeModelKey(rawKey);
|
|
77
|
+
if (key.length === 0 || key.length > MODEL_WINDOWS_LIMITS.maxKeyLength)
|
|
78
|
+
continue;
|
|
79
|
+
if (!isWindowValue(rawValue))
|
|
80
|
+
continue;
|
|
81
|
+
table.set(key, rawValue);
|
|
82
|
+
if (table.size >= MODEL_WINDOWS_LIMITS.maxEntries)
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
return { table, malformed: false };
|
|
86
|
+
}
|
|
87
|
+
/** The table alone, for callers that do not care why it might be empty. */
|
|
88
|
+
export function parseModelWindows(text) {
|
|
89
|
+
return parseModelWindowsResult(text).table;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Decode the raw bytes the way a Windows text editor may have saved them.
|
|
93
|
+
*
|
|
94
|
+
* `readFileSync(path, 'utf8')` alone fails THREE ways on this machine's most
|
|
95
|
+
* common editors: a UTF-8 BOM survives into the string and kills `JSON.parse`,
|
|
96
|
+
* and Notepad's "Unicode" saves are UTF-16LE (or BE), which decode as NUL-laden
|
|
97
|
+
* mojibake. All three are silent feature-death for exactly the user who just
|
|
98
|
+
* tried to configure it, so the sniff happens before any parse.
|
|
99
|
+
*/
|
|
100
|
+
function decodeModelWindowsFile(bytes) {
|
|
101
|
+
if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
|
|
102
|
+
return bytes.subarray(2).toString('utf16le');
|
|
103
|
+
}
|
|
104
|
+
if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
|
|
105
|
+
// Big-endian UTF-16: Node has no utf16be decoder, so swap pairs in place
|
|
106
|
+
// on a COPY and reuse the little-endian decoder.
|
|
107
|
+
const swapped = Buffer.from(bytes.subarray(2));
|
|
108
|
+
for (let i = 0; i + 1 < swapped.length; i += 2) {
|
|
109
|
+
const high = swapped[i];
|
|
110
|
+
swapped[i] = swapped[i + 1];
|
|
111
|
+
swapped[i + 1] = high;
|
|
112
|
+
}
|
|
113
|
+
return swapped.toString('utf16le');
|
|
114
|
+
}
|
|
115
|
+
if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
|
|
116
|
+
return bytes.subarray(3).toString('utf8');
|
|
117
|
+
}
|
|
118
|
+
return bytes.toString('utf8');
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* mtime-cached lookup over the file.
|
|
122
|
+
*
|
|
123
|
+
* WHY A STAT PER LOOKUP RATHER THAN A LOAD AT STARTUP: `getModelInfoFor` is
|
|
124
|
+
* called on every measurement, and the meter's `refreshValidity` re-reads the
|
|
125
|
+
* window precisely so a moved denominator (a `/model` switch, a live config
|
|
126
|
+
* edit) re-measures without a relaunch. A user saving this file mid-session
|
|
127
|
+
* gets the same treatment - `statSync` is microseconds, and the read itself
|
|
128
|
+
* only happens when the stamp actually changes.
|
|
129
|
+
*
|
|
130
|
+
* THE STAMP IS mtimeMs+size, so a rewrite that lands in the SAME millisecond
|
|
131
|
+
* with the SAME byte count serves the previous table once. Squeezing that
|
|
132
|
+
* window shut would need a content hash per lookup, which is exactly the cost
|
|
133
|
+
* the stamp exists to avoid; the next differing write clears it regardless.
|
|
134
|
+
*/
|
|
135
|
+
export class ModelWindows {
|
|
136
|
+
path;
|
|
137
|
+
table = new Map();
|
|
138
|
+
stamp = null;
|
|
139
|
+
constructor(path = getModelWindowsPath()) {
|
|
140
|
+
this.path = path;
|
|
141
|
+
}
|
|
142
|
+
/** The declared window for `modelId`, or `undefined` when nobody declared one. */
|
|
143
|
+
lookup(modelId) {
|
|
144
|
+
this.refresh();
|
|
145
|
+
return this.table.get(normalizeModelKey(modelId));
|
|
146
|
+
}
|
|
147
|
+
refresh() {
|
|
148
|
+
let stamp;
|
|
149
|
+
try {
|
|
150
|
+
const stats = statSync(this.path);
|
|
151
|
+
stamp = { mtimeMs: stats.mtimeMs, size: stats.size };
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
// A deleted file (ENOENT) is a decision: drop the entries. Any OTHER
|
|
155
|
+
// failure (EACCES, an antivirus lock) is transient, and blanking every
|
|
156
|
+
// declared window because a scanner held the file open for 50 ms would
|
|
157
|
+
// swing the gauge to `?` for a reason the user cannot see - keep the
|
|
158
|
+
// last good table and try again on the next lookup.
|
|
159
|
+
if (error.code === 'ENOENT') {
|
|
160
|
+
this.stamp = null;
|
|
161
|
+
this.table = new Map();
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (stamp.mtimeMs === this.stamp?.mtimeMs && stamp.size === this.stamp.size)
|
|
166
|
+
return;
|
|
167
|
+
let bytes;
|
|
168
|
+
try {
|
|
169
|
+
bytes = readFileSync(this.path);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return; // Appeared in the stat, gone in the read: try again next lookup.
|
|
173
|
+
}
|
|
174
|
+
const result = parseModelWindowsResult(decodeModelWindowsFile(bytes));
|
|
175
|
+
if (result.malformed) {
|
|
176
|
+
getLogger().warn('config', 'model-windows.json could not be parsed; ignoring it until it is fixed', { path: this.path });
|
|
177
|
+
}
|
|
178
|
+
this.table = result.table;
|
|
179
|
+
this.stamp = stamp;
|
|
180
|
+
}
|
|
181
|
+
}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -303,11 +303,25 @@ export interface TeamConfig {
|
|
|
303
303
|
* failed subagent.
|
|
304
304
|
*/
|
|
305
305
|
maxConcurrent: number;
|
|
306
|
-
/**
|
|
306
|
+
/**
|
|
307
|
+
* One child's wall clock before its own abort. `0` (the default) means NO
|
|
308
|
+
* ceiling: a subagent runs until it finishes or is aborted, exactly like
|
|
309
|
+
* the lead, whose only in-run ceiling is the event-silence idle watchdog
|
|
310
|
+
* a child already keeps. A wedged child still dies on its OWN watchdog.
|
|
311
|
+
*/
|
|
307
312
|
subagentTimeoutMs: number;
|
|
308
|
-
/**
|
|
313
|
+
/**
|
|
314
|
+
* The whole dispatch's wall clock, and the `task` tool-timeout override.
|
|
315
|
+
* `0` (the default) means NO ceiling: the executor override of `0` arms no
|
|
316
|
+
* timer (see Core `ToolExecutor`), so the fan-out lasts as long as its
|
|
317
|
+
* slowest child and only an Esc ends it early.
|
|
318
|
+
*/
|
|
309
319
|
dispatchTimeoutMs: number;
|
|
310
|
-
/**
|
|
320
|
+
/**
|
|
321
|
+
* Runaway-loop cap: turns one child may take before it is stopped.
|
|
322
|
+
* `0` (the default) means NO cap - the same exposure the lead has, and
|
|
323
|
+
* the deliberate trade of "subagents as capable as the main agent".
|
|
324
|
+
*/
|
|
311
325
|
maxTurnsPerSubagent: number;
|
|
312
326
|
}
|
|
313
327
|
/**
|
|
@@ -458,8 +472,6 @@ export interface FastConfig {
|
|
|
458
472
|
/** Applied to fast-tier children and to the review call. Defaults to `'off'`:
|
|
459
473
|
* a "fast" model asked to think for 32 768 tokens is not fast (D-13). */
|
|
460
474
|
thinkingLevel: ThinkingLevel;
|
|
461
|
-
/** Allow `model: "fast"` on `task`. */
|
|
462
|
-
delegate: boolean;
|
|
463
475
|
/** Run the periodic asynchronous review. */
|
|
464
476
|
review: boolean;
|
|
465
477
|
/** Completed turns between reviews. */
|
|
@@ -786,6 +798,14 @@ export interface PersistedConfig {
|
|
|
786
798
|
* asked for; a kill switch is what a bug report needs.
|
|
787
799
|
*/
|
|
788
800
|
paste: boolean;
|
|
801
|
+
/**
|
|
802
|
+
* Ask the terminal to report modified keys distinctly so Shift+Enter can
|
|
803
|
+
* insert a newline (win32-input-mode on Windows, kitty disambiguate +
|
|
804
|
+
* modifyOtherKeys elsewhere). A KILL SWITCH in the exact sense `paste`
|
|
805
|
+
* above records: the encoding tables are structural and live in
|
|
806
|
+
* `input/win32-input-mode.ts` / `input/csiu-keys.ts`.
|
|
807
|
+
*/
|
|
808
|
+
keyboardEnhancement: boolean;
|
|
789
809
|
/** Idle ms before a paused viewport returns to the newest line; `0` disables. */
|
|
790
810
|
scrollResumeMs: number;
|
|
791
811
|
/** Start every session in PLAN mode (plan-mode §4.4). */
|
|
@@ -971,6 +991,15 @@ export interface CliConfig {
|
|
|
971
991
|
* reach none of them.
|
|
972
992
|
*/
|
|
973
993
|
paste: boolean;
|
|
994
|
+
/**
|
|
995
|
+
* Whether this session may push the keyboard-enhancement modes.
|
|
996
|
+
*
|
|
997
|
+
* IT MUST EXIST HERE AND NOT ONLY IN `PersistedConfig`, for the reason
|
|
998
|
+
* `paste` above records: `runInteractive` reads it -- where `config`
|
|
999
|
+
* is a `CliConfig` -- to decide the filter's `enhancedKeys` feature
|
|
1000
|
+
* and the `enableKeyboardEnhancement` push.
|
|
1001
|
+
*/
|
|
1002
|
+
keyboardEnhancement: boolean;
|
|
974
1003
|
/** Idle ms before a paused viewport returns to the newest line; `0` disables. */
|
|
975
1004
|
scrollResumeMs: number;
|
|
976
1005
|
/**
|
package/dist/config/schema.js
CHANGED
|
@@ -447,15 +447,20 @@ export const DEFAULT_TEAM_CONFIG = {
|
|
|
447
447
|
enabled: true,
|
|
448
448
|
maxSubagents: 5,
|
|
449
449
|
maxConcurrent: 3,
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
450
|
+
// 0 = NO LIMIT. Subagents run under the same regime as the lead: no
|
|
451
|
+
// wall clock, no turn cap; only the idle watchdog and the user's Esc.
|
|
452
|
+
subagentTimeoutMs: 0,
|
|
453
|
+
dispatchTimeoutMs: 0,
|
|
454
|
+
maxTurnsPerSubagent: 0,
|
|
453
455
|
};
|
|
454
456
|
const SUBAGENTS_RANGE = { min: 1, max: HARD_MAX_SUBAGENTS };
|
|
455
457
|
const CONCURRENT_RANGE = { min: 1, max: HARD_MAX_SUBAGENTS };
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
458
|
+
// `min: 0` is the documented OFF SWITCH for each bound, so these MUST use
|
|
459
|
+
// `clampIntAllowingZero`: plain `clampInt` folds `0` back to the fallback
|
|
460
|
+
// ("my setting won't stick"), the exact trap `scrollResumeMs` records above.
|
|
461
|
+
const SUBAGENT_TIMEOUT_RANGE = { min: 0, max: 1_800_000 };
|
|
462
|
+
const DISPATCH_TIMEOUT_RANGE = { min: 0, max: 3_600_000 };
|
|
463
|
+
const TURNS_RANGE = { min: 0, max: 100 };
|
|
459
464
|
/**
|
|
460
465
|
* THE single gate for every team-config read AND write, mirroring
|
|
461
466
|
* `clampSkillsConfig` / `clampLogConfig` for the reason those two record:
|
|
@@ -474,9 +479,9 @@ export function clampTeamConfig(raw) {
|
|
|
474
479
|
enabled: bool(src.enabled, DEFAULT_TEAM_CONFIG.enabled),
|
|
475
480
|
maxSubagents,
|
|
476
481
|
maxConcurrent: Math.min(maxSubagents, clampInt(src.maxConcurrent, DEFAULT_TEAM_CONFIG.maxConcurrent, CONCURRENT_RANGE)),
|
|
477
|
-
subagentTimeoutMs:
|
|
478
|
-
dispatchTimeoutMs:
|
|
479
|
-
maxTurnsPerSubagent:
|
|
482
|
+
subagentTimeoutMs: clampIntAllowingZero(src.subagentTimeoutMs, DEFAULT_TEAM_CONFIG.subagentTimeoutMs, SUBAGENT_TIMEOUT_RANGE),
|
|
483
|
+
dispatchTimeoutMs: clampIntAllowingZero(src.dispatchTimeoutMs, DEFAULT_TEAM_CONFIG.dispatchTimeoutMs, DISPATCH_TIMEOUT_RANGE),
|
|
484
|
+
maxTurnsPerSubagent: clampIntAllowingZero(src.maxTurnsPerSubagent, DEFAULT_TEAM_CONFIG.maxTurnsPerSubagent, TURNS_RANGE),
|
|
480
485
|
};
|
|
481
486
|
}
|
|
482
487
|
export const DEFAULT_TODO_CONFIG = {
|
|
@@ -615,7 +620,6 @@ export const DEFAULT_FAST_CONFIG = {
|
|
|
615
620
|
model: '',
|
|
616
621
|
baseUrl: '',
|
|
617
622
|
thinkingLevel: 'off',
|
|
618
|
-
delegate: true,
|
|
619
623
|
review: true,
|
|
620
624
|
reviewEveryTurns: 5,
|
|
621
625
|
reviewContextTurns: 3,
|
|
@@ -655,7 +659,6 @@ export function clampFastConfig(raw) {
|
|
|
655
659
|
model: text(src.model, DEFAULT_FAST_CONFIG.model),
|
|
656
660
|
baseUrl: text(src.baseUrl, DEFAULT_FAST_CONFIG.baseUrl),
|
|
657
661
|
thinkingLevel: clampThinkingLevel(src.thinkingLevel, DEFAULT_FAST_CONFIG.thinkingLevel),
|
|
658
|
-
delegate: bool(src.delegate, DEFAULT_FAST_CONFIG.delegate),
|
|
659
662
|
review: bool(src.review, DEFAULT_FAST_CONFIG.review),
|
|
660
663
|
reviewEveryTurns: clampInt(src.reviewEveryTurns, DEFAULT_FAST_CONFIG.reviewEveryTurns, REVIEW_EVERY_RANGE),
|
|
661
664
|
reviewContextTurns: clampInt(src.reviewContextTurns, DEFAULT_FAST_CONFIG.reviewContextTurns, REVIEW_CONTEXT_RANGE),
|
|
@@ -896,6 +899,7 @@ export const DEFAULT_CONFIG = {
|
|
|
896
899
|
mouse: true,
|
|
897
900
|
mouseSelect: DEFAULT_MOUSE_SELECT,
|
|
898
901
|
paste: true,
|
|
902
|
+
keyboardEnhancement: true,
|
|
899
903
|
scrollResumeMs: DEFAULT_SCROLL_RESUME_MS,
|
|
900
904
|
planModeDefault: false,
|
|
901
905
|
planModeMaxAskRounds: DEFAULT_PLAN_MAX_ASK_ROUNDS,
|
package/dist/exec/events.d.ts
CHANGED
|
@@ -219,11 +219,11 @@ export interface ExecFastReviewEvent {
|
|
|
219
219
|
* not know is unaffected.
|
|
220
220
|
*
|
|
221
221
|
* THE PROBLEM IT SOLVES. `fast_review` only fires when a review produced TEXT,
|
|
222
|
-
* so every other outcome — a review that failed, a tier that switched itself
|
|
223
|
-
* after three failures, a budget that ran out
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
222
|
+
* so every other outcome — a review that failed, a tier that switched itself
|
|
223
|
+
* off after three failures, or a budget that ran out — was invisible to a
|
|
224
|
+
* wrapper. The single most likely and least self-diagnosable failure ("it
|
|
225
|
+
* runs but not one review card ever arrives") had no signal at all on this
|
|
226
|
+
* stream.
|
|
227
227
|
*
|
|
228
228
|
* IT IS A STATE EVENT, NOT A LOG LINE. It fires on `tier_changed` and on every
|
|
229
229
|
* `review_end`, which in a session with reviews is a handful per hundred turns.
|
|
@@ -249,7 +249,11 @@ export interface ExecFastTierEvent {
|
|
|
249
249
|
reviews: number;
|
|
250
250
|
reviewBudget: number;
|
|
251
251
|
budgetReached: boolean;
|
|
252
|
-
/**
|
|
252
|
+
/**
|
|
253
|
+
* DEPRECATED, ALWAYS `0`. Fast-tier children no longer exist (every `task`
|
|
254
|
+
* child runs the lead's model); the field is kept so schema-v1 consumers
|
|
255
|
+
* keep parsing. Do not build new readers on it.
|
|
256
|
+
*/
|
|
253
257
|
delegated: number;
|
|
254
258
|
inFlight: boolean;
|
|
255
259
|
/** Present only on the `review_end` branch. */
|
|
@@ -399,8 +403,8 @@ export interface ExecInitParams {
|
|
|
399
403
|
*
|
|
400
404
|
* · `'interrupt'` — `{"type":"interrupt"}` on stdin settles the turn
|
|
401
405
|
* without killing the process.
|
|
402
|
-
* · `'fast-policy'` — this build understands `--fast-
|
|
403
|
-
*
|
|
406
|
+
* · `'fast-policy'` — this build understands `--fast-review`.
|
|
407
|
+
* FOR AFTER-THE-FACT DISPLAY ONLY:
|
|
404
408
|
* argv is fixed before `system/init` arrives, so it
|
|
405
409
|
* cannot gate whether the flags are passed.
|
|
406
410
|
* · `'fast-tier-events'` — this build emits `fast_tier`. Its ABSENCE is what
|
package/dist/exec/events.js
CHANGED
|
@@ -34,8 +34,8 @@ export const EXEC_SCHEMA_VERSION = 1;
|
|
|
34
34
|
*
|
|
35
35
|
* · `'interrupt'` — `{"type":"interrupt"}` on stdin settles the turn
|
|
36
36
|
* without killing the process.
|
|
37
|
-
* · `'fast-policy'` — this build understands `--fast-
|
|
38
|
-
*
|
|
37
|
+
* · `'fast-policy'` — this build understands `--fast-review`.
|
|
38
|
+
* FOR AFTER-THE-FACT DISPLAY ONLY:
|
|
39
39
|
* argv is fixed before `system/init` arrives, so it
|
|
40
40
|
* cannot gate whether the flags are passed.
|
|
41
41
|
* · `'fast-tier-events'` — this build emits `fast_tier`. Its ABSENCE is what
|
package/dist/exec/runner.js
CHANGED
|
@@ -153,7 +153,10 @@ export class ExecRunner {
|
|
|
153
153
|
reviews: snapshot.reviews,
|
|
154
154
|
reviewBudget: snapshot.reviewBudget,
|
|
155
155
|
budgetReached: snapshot.budgetReached,
|
|
156
|
-
|
|
156
|
+
// Deprecated in schema v1: always 0 since fast-tier children were
|
|
157
|
+
// removed (every task child runs the lead's model). Emitted so
|
|
158
|
+
// schema-v1 consumers keep parsing.
|
|
159
|
+
delegated: 0,
|
|
157
160
|
inFlight: snapshot.inFlight,
|
|
158
161
|
...(event.type === 'review_end'
|
|
159
162
|
? {
|
package/dist/fast/limits.d.ts
CHANGED
|
@@ -76,7 +76,7 @@ export declare const FAST_LIMITS: {
|
|
|
76
76
|
* A critique waiting for its injection window goes stale (RV-12).
|
|
77
77
|
*
|
|
78
78
|
* The window is not guaranteed to be prompt: one `task` dispatch is a single
|
|
79
|
-
* tool call that can run for
|
|
79
|
+
* tool call that can run for as long as its children need, so a critique triggered
|
|
80
80
|
* just before one arrives at the far side describing a state five children
|
|
81
81
|
* have since rewritten. Advice that is merely late is worse than no advice,
|
|
82
82
|
* because the lead cannot tell that it is late.
|
|
@@ -95,4 +95,4 @@ export declare const FAST_LIMITS: {
|
|
|
95
95
|
* changes, so a behaviour report can be tied to a block revision with one grep -
|
|
96
96
|
* exactly what `TEAM_BLOCK_VERSION` and `TODO_BLOCK_VERSION` are for.
|
|
97
97
|
*/
|
|
98
|
-
export declare const FAST_BLOCK_VERSION = "
|
|
98
|
+
export declare const FAST_BLOCK_VERSION = "v2-2026-10";
|
package/dist/fast/limits.js
CHANGED
|
@@ -76,7 +76,7 @@ export const FAST_LIMITS = {
|
|
|
76
76
|
* A critique waiting for its injection window goes stale (RV-12).
|
|
77
77
|
*
|
|
78
78
|
* The window is not guaranteed to be prompt: one `task` dispatch is a single
|
|
79
|
-
* tool call that can run for
|
|
79
|
+
* tool call that can run for as long as its children need, so a critique triggered
|
|
80
80
|
* just before one arrives at the far side describing a state five children
|
|
81
81
|
* have since rewritten. Advice that is merely late is worse than no advice,
|
|
82
82
|
* because the lead cannot tell that it is late.
|
|
@@ -95,4 +95,4 @@ export const FAST_LIMITS = {
|
|
|
95
95
|
* changes, so a behaviour report can be tied to a block revision with one grep -
|
|
96
96
|
* exactly what `TEAM_BLOCK_VERSION` and `TODO_BLOCK_VERSION` are for.
|
|
97
97
|
*/
|
|
98
|
-
export const FAST_BLOCK_VERSION = '
|
|
98
|
+
export const FAST_BLOCK_VERSION = 'v2-2026-10';
|
package/dist/fast/prompt.d.ts
CHANGED
|
@@ -19,23 +19,22 @@ export interface FastBlockParams {
|
|
|
19
19
|
/** The resolved fast model id, interpolated so the prompt never names a model
|
|
20
20
|
* the tier will not actually use (§3.9 / RV-3). */
|
|
21
21
|
model: string;
|
|
22
|
-
/** `fast.delegate` — whether `model: "fast"` is honoured on `task`. */
|
|
23
|
-
delegate: boolean;
|
|
24
22
|
/** `fast.review` — whether `<fast_review>` blocks can appear at all. */
|
|
25
23
|
review: boolean;
|
|
26
24
|
}
|
|
27
25
|
/**
|
|
28
|
-
* The
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* The `<fast_tier>` block, spliced for the lead AND for task children running
|
|
27
|
+
* under a per-child reviewer. Under 1100 characters on purpose: it is paid for
|
|
28
|
+
* on EVERY turn of every fast-enabled session, so anything that does not change
|
|
29
|
+
* a decision does not belong in it (R-11).
|
|
31
30
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
31
|
+
* REVIEW-ONLY SIN MAIN-AGENT PARITY: every `task` child runs the lead's own
|
|
32
|
+
* model, so the block no longer advertises a per-child `model:"fast"`. Telling
|
|
33
|
+
* a model about a field that no longer exists is how you get a model that keeps
|
|
34
|
+
* asking for something it can never have.
|
|
36
35
|
*
|
|
37
|
-
* Returns `''` when
|
|
38
|
-
*
|
|
36
|
+
* Returns `''` when the review is off, which the splice treats as "no block"
|
|
37
|
+
* and which keeps the byte-identity branch intact.
|
|
39
38
|
*/
|
|
40
39
|
export declare function buildFastBlock(params: FastBlockParams): string;
|
|
41
40
|
export interface ReviewSystemPromptParams {
|