@aragon-agent/cli 0.5.1 → 0.5.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 +117 -0
- package/README.md +155 -5
- package/dist/agent/controller.d.ts +51 -1
- package/dist/agent/controller.js +177 -2
- package/dist/agent/headless.d.ts +12 -0
- package/dist/agent/headless.js +41 -1
- package/dist/agent/reducer.d.ts +48 -0
- package/dist/agent/reducer.js +91 -2
- package/dist/agent/system-prompt.d.ts +17 -0
- package/dist/agent/system-prompt.js +4 -0
- package/dist/cli.js +131 -6
- package/dist/commands/builtins.js +82 -0
- package/dist/config/app-paths.d.ts +17 -0
- package/dist/config/app-paths.js +21 -0
- package/dist/config/cli-commands.d.ts +26 -2
- package/dist/config/cli-commands.js +63 -4
- package/dist/config/env.d.ts +2 -2
- package/dist/config/env.js +36 -2
- package/dist/config/history-commands.d.ts +20 -0
- package/dist/config/history-commands.js +71 -0
- package/dist/config/load.d.ts +15 -0
- package/dist/config/load.js +88 -5
- package/dist/config/migrate-state-out-of-config.d.ts +32 -0
- package/dist/config/migrate-state-out-of-config.js +85 -0
- package/dist/config/prompt-history.d.ts +86 -0
- package/dist/config/prompt-history.js +332 -0
- package/dist/config/schema.d.ts +125 -9
- package/dist/config/schema.js +90 -5
- package/dist/config/store.js +13 -4
- package/dist/config/ui-state.d.ts +52 -0
- package/dist/config/ui-state.js +149 -0
- package/dist/input/mouse-events.d.ts +61 -0
- package/dist/input/mouse-events.js +153 -0
- package/dist/input/stdin-mouse-filter.d.ts +42 -0
- package/dist/input/stdin-mouse-filter.js +153 -0
- package/dist/logging/install.d.ts +85 -0
- package/dist/logging/install.js +94 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.d.ts +16 -0
- package/dist/session/persist.js +19 -1
- package/dist/team/activity.d.ts +67 -0
- package/dist/team/activity.js +130 -0
- package/dist/team/bus.d.ts +190 -0
- package/dist/team/bus.js +366 -0
- package/dist/team/comm-tools.d.ts +61 -0
- package/dist/team/comm-tools.js +221 -0
- package/dist/team/human-queue.d.ts +55 -0
- package/dist/team/human-queue.js +72 -0
- package/dist/team/limits.d.ts +123 -0
- package/dist/team/limits.js +128 -0
- package/dist/team/normalize.d.ts +39 -0
- package/dist/team/normalize.js +106 -0
- package/dist/team/panel-rows.d.ts +48 -0
- package/dist/team/panel-rows.js +75 -0
- package/dist/team/prompt.d.ts +49 -0
- package/dist/team/prompt.js +87 -0
- package/dist/team/report.d.ts +58 -0
- package/dist/team/report.js +205 -0
- package/dist/team/retry.d.ts +48 -0
- package/dist/team/retry.js +56 -0
- package/dist/team/runtime.d.ts +122 -0
- package/dist/team/runtime.js +406 -0
- package/dist/team/subagent.d.ts +90 -0
- package/dist/team/subagent.js +294 -0
- package/dist/team/task-tool.d.ts +47 -0
- package/dist/team/task-tool.js +118 -0
- package/dist/team/types.d.ts +172 -0
- package/dist/team/types.js +13 -0
- package/dist/tools/index.d.ts +48 -2
- package/dist/tools/index.js +26 -1
- package/dist/ui/App.d.ts +9 -1
- package/dist/ui/App.js +140 -21
- package/dist/ui/Composer.d.ts +6 -1
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/PromptInput.d.ts +15 -1
- package/dist/ui/PromptInput.js +38 -1
- package/dist/ui/StatusBar.d.ts +21 -0
- package/dist/ui/StatusBar.js +5 -2
- package/dist/ui/TeamPanel.d.ts +57 -0
- package/dist/ui/TeamPanel.js +134 -0
- package/dist/ui/Transcript.js +10 -0
- package/dist/ui/entries/TeamCard.d.ts +31 -0
- package/dist/ui/entries/TeamCard.js +39 -0
- package/dist/ui/glyphs.d.ts +22 -0
- package/dist/ui/glyphs.js +10 -0
- package/dist/ui/layout/AppShell.d.ts +20 -1
- package/dist/ui/layout/AppShell.js +39 -4
- package/dist/ui/layout/ScrollIndicator.d.ts +31 -0
- package/dist/ui/layout/ScrollIndicator.js +15 -0
- package/dist/ui/layout/ScrollViewport.d.ts +8 -2
- package/dist/ui/layout/ScrollViewport.js +24 -4
- package/dist/ui/layout/regions.d.ts +55 -0
- package/dist/ui/layout/regions.js +52 -0
- package/dist/ui/layout/scroll-indicator.d.ts +27 -0
- package/dist/ui/layout/scroll-indicator.js +35 -0
- package/dist/ui/layout/scroll.d.ts +9 -0
- package/dist/ui/layout/scroll.js +23 -0
- package/dist/ui/overlays/HelpOverlay.js +14 -0
- package/dist/ui/screen.d.ts +14 -1
- package/dist/ui/screen.js +37 -3
- package/dist/ui/transcript-text.js +20 -0
- package/dist/ui/use-startup-notices.d.ts +11 -1
- package/dist/ui/use-startup-notices.js +29 -2
- package/dist/ui/use-wheel-routing.d.ts +68 -0
- package/dist/ui/use-wheel-routing.js +139 -0
- package/package.json +2 -2
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prompts the user submitted, as an append-only JSON Lines file at
|
|
3
|
+
* `<home>/prompt-history.jsonl` (config-state-separation §4.3).
|
|
4
|
+
*
|
|
5
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
* WHY THIS IS NOT IN config.json ANY MORE: it used to be a `promptHistory`
|
|
7
|
+
* array inside the config file, which meant every single submit rewrote the
|
|
8
|
+
* whole file — API key section included — and left a user who opened
|
|
9
|
+
* `config.json` to change their model scrolling past their own typing. A
|
|
10
|
+
* config file holds what the user DECLARED; this holds what they DID.
|
|
11
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
12
|
+
*
|
|
13
|
+
* APPEND, never read-modify-write. Two `aragon` windows are an ordinary setup,
|
|
14
|
+
* and `O_APPEND` + one `writeSync` per entry means they interleave instead of
|
|
15
|
+
* overwriting each other's snapshots. It also means a process killed mid-write
|
|
16
|
+
* costs one broken line rather than a truncated array that reads back as "your
|
|
17
|
+
* history is gone" — hence the per-line tolerance in `parseLine`.
|
|
18
|
+
*
|
|
19
|
+
* FAILURE POLICY (C2), the same one `skills/usage.ts` documents: every function
|
|
20
|
+
* here swallows its errors. This is recall convenience; a read-only home
|
|
21
|
+
* directory must degrade to "no history", never to a CLI that will not start.
|
|
22
|
+
* The one thing that policy makes dangerous is a MISSING WRITE — it would be
|
|
23
|
+
* invisible — which is why the write path creates `<home>` first and why a
|
|
24
|
+
* failed append is logged rather than merely ignored.
|
|
25
|
+
*/
|
|
26
|
+
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, writeSync, } from 'node:fs';
|
|
27
|
+
import process from 'node:process';
|
|
28
|
+
import { getHomeRoot, getPromptHistoryPath } from './app-paths.js';
|
|
29
|
+
import { PROMPT_HISTORY_CAP } from './schema.js';
|
|
30
|
+
import { getLogger } from '../logging/logger.js';
|
|
31
|
+
export const PROMPT_HISTORY_FILENAME = 'prompt-history.jsonl';
|
|
32
|
+
export const PROMPT_HISTORY_LINE_VERSION = 1;
|
|
33
|
+
/**
|
|
34
|
+
* Longer submissions are not recorded at all (D-6).
|
|
35
|
+
*
|
|
36
|
+
* Storing a TRUNCATED copy would be worse than storing nothing: the user
|
|
37
|
+
* presses `↑`, gets back something that looks complete, and sends half a
|
|
38
|
+
* prompt. 16 KiB is far past anything typed by hand.
|
|
39
|
+
*/
|
|
40
|
+
export const PROMPT_ENTRY_MAX_CHARS = 16_384;
|
|
41
|
+
/** Physical lines tolerated before the file is rewritten down to the cap. */
|
|
42
|
+
export const HISTORY_COMPACT_AT_LINES = 400;
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// In-memory state
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
let memory = null;
|
|
47
|
+
let loadedLines = 0;
|
|
48
|
+
let appendedLines = 0;
|
|
49
|
+
/**
|
|
50
|
+
* Starts `true`, and that initial value is load-bearing (RV-5).
|
|
51
|
+
*
|
|
52
|
+
* `loadConfig()` calls `setHistoryEnabled()`, but the startup migration runs
|
|
53
|
+
* BEFORE any `loadConfig()`. Something has to be true in that window, and
|
|
54
|
+
* `true` is what the config key itself defaults to. The one case in that window
|
|
55
|
+
* that must honour an explicit opt-out — importing a legacy `promptHistory` —
|
|
56
|
+
* reads the value straight off the config file instead of trusting this flag
|
|
57
|
+
* (see `migrate-state-out-of-config.ts`).
|
|
58
|
+
*/
|
|
59
|
+
let historyEnabled = true;
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Parsing
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
function parseLine(line) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(line);
|
|
66
|
+
if (!parsed || typeof parsed !== 'object')
|
|
67
|
+
return null;
|
|
68
|
+
// Unknown versions are skipped rather than coerced: that is what lets a
|
|
69
|
+
// future field be added without an older CLI reading the file as garbage.
|
|
70
|
+
if (parsed.v !== PROMPT_HISTORY_LINE_VERSION)
|
|
71
|
+
return null;
|
|
72
|
+
if (typeof parsed.text !== 'string' || parsed.text.length === 0)
|
|
73
|
+
return null;
|
|
74
|
+
const ts = typeof parsed.ts === 'number' && Number.isFinite(parsed.ts) ? parsed.ts : 0;
|
|
75
|
+
return { v: PROMPT_HISTORY_LINE_VERSION, ts, text: parsed.text };
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Read and parse the file. `null` means the read itself failed — distinct from
|
|
83
|
+
* "no file yet", because compaction must not rewrite a file it could not read.
|
|
84
|
+
*/
|
|
85
|
+
function readFromDisk() {
|
|
86
|
+
const path = getPromptHistoryPath();
|
|
87
|
+
if (!existsSync(path))
|
|
88
|
+
return { entries: [], lines: 0 };
|
|
89
|
+
let raw;
|
|
90
|
+
try {
|
|
91
|
+
raw = readFileSync(path, 'utf-8');
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const entries = [];
|
|
97
|
+
let lines = 0;
|
|
98
|
+
for (const line of raw.split('\n')) {
|
|
99
|
+
if (line.trim().length === 0)
|
|
100
|
+
continue;
|
|
101
|
+
lines += 1;
|
|
102
|
+
const entry = parseLine(line);
|
|
103
|
+
if (entry)
|
|
104
|
+
entries.push(entry);
|
|
105
|
+
}
|
|
106
|
+
return { entries, lines };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Drop repeats, keeping the LAST occurrence, then keep the newest `cap`.
|
|
110
|
+
*
|
|
111
|
+
* Oldest-first ordering is the contract `PromptInput` walks with `↑`/`↓`, and
|
|
112
|
+
* "last occurrence wins" reproduces what the old in-config implementation did
|
|
113
|
+
* with `filter(p => p !== text)` followed by an append.
|
|
114
|
+
*/
|
|
115
|
+
function dedupeEntries(entries) {
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
const reversed = [];
|
|
118
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
119
|
+
const entry = entries[i];
|
|
120
|
+
if (seen.has(entry.text))
|
|
121
|
+
continue;
|
|
122
|
+
seen.add(entry.text);
|
|
123
|
+
reversed.push(entry);
|
|
124
|
+
}
|
|
125
|
+
reversed.reverse();
|
|
126
|
+
return reversed.slice(-PROMPT_HISTORY_CAP);
|
|
127
|
+
}
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Read
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
/**
|
|
132
|
+
* The recall list: oldest first, de-duplicated, capped. Cached for the life of
|
|
133
|
+
* the process so `/reload` — which calls `loadConfig()` again — cannot discard
|
|
134
|
+
* entries added during this session.
|
|
135
|
+
*
|
|
136
|
+
* Reads even when `historyEnabled` is false (RV-12): turning recording off
|
|
137
|
+
* stops new writes, but someone who wants to see what is already stored before
|
|
138
|
+
* deleting it must not be told "nothing here" while the file still exists.
|
|
139
|
+
*/
|
|
140
|
+
export function loadPromptHistory() {
|
|
141
|
+
if (memory !== null)
|
|
142
|
+
return memory;
|
|
143
|
+
const read = readFromDisk() ?? { entries: [], lines: 0 };
|
|
144
|
+
memory = dedupeEntries(read.entries).map((entry) => entry.text);
|
|
145
|
+
loadedLines = read.lines;
|
|
146
|
+
appendedLines = 0;
|
|
147
|
+
return memory;
|
|
148
|
+
}
|
|
149
|
+
/** Every entry on disk with its timestamp, newest first. For `history list`. */
|
|
150
|
+
export function listPromptHistory() {
|
|
151
|
+
const read = readFromDisk();
|
|
152
|
+
if (!read)
|
|
153
|
+
return [];
|
|
154
|
+
return [...read.entries].reverse();
|
|
155
|
+
}
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Write
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
/**
|
|
160
|
+
* Append one line. Returns whether it reached the disk.
|
|
161
|
+
*
|
|
162
|
+
* `mkdirSync` IS NOT DEFENSIVE NOISE — deleting it breaks a real user. `<home>`
|
|
163
|
+
* is created by the logger (only when `log.toFile` is on) or by the first
|
|
164
|
+
* config write (only when the user changes a setting); someone on a fresh
|
|
165
|
+
* install who did neither has no directory here, `openSync` throws `ENOENT`,
|
|
166
|
+
* the swallow below hides it, and their history silently never persists.
|
|
167
|
+
*
|
|
168
|
+
* A short-lived descriptor rather than a kept-open one: this is a once-per-
|
|
169
|
+
* submit write, and a descriptor held for the life of the process would make
|
|
170
|
+
* `aragon history clear` unable to delete the file on Windows.
|
|
171
|
+
*/
|
|
172
|
+
function appendLine(entry) {
|
|
173
|
+
try {
|
|
174
|
+
mkdirSync(getHomeRoot(), { recursive: true });
|
|
175
|
+
const fd = openSync(getPromptHistoryPath(), 'a', 0o600);
|
|
176
|
+
try {
|
|
177
|
+
writeSync(fd, `${JSON.stringify(entry)}\n`);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
closeSync(fd);
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
getLogger().warn('history', 'history_append_failed', {
|
|
186
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
187
|
+
});
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Record one submitted prompt and return THE UPDATED LIST.
|
|
193
|
+
*
|
|
194
|
+
* The return value is the whole point of the signature (RV-1). `App.tsx` writes
|
|
195
|
+
* `setPromptHistory(appendPrompt(text))`: the recall list rendered by the
|
|
196
|
+
* composer is React state, and a module-level array updated in here re-renders
|
|
197
|
+
* nothing. Drop the return value and `↑` stops recalling what was just
|
|
198
|
+
* submitted — but only until the next restart, which makes it about as hard to
|
|
199
|
+
* diagnose as a bug in this area gets.
|
|
200
|
+
*
|
|
201
|
+
* Returns the current list unchanged when recording is off, the text is blank,
|
|
202
|
+
* or the text is oversized, so callers never need a branch.
|
|
203
|
+
*/
|
|
204
|
+
export function appendPrompt(text, now = Date.now()) {
|
|
205
|
+
const current = loadPromptHistory();
|
|
206
|
+
if (!historyEnabled)
|
|
207
|
+
return current;
|
|
208
|
+
if (text.trim().length === 0)
|
|
209
|
+
return current;
|
|
210
|
+
if (text.length > PROMPT_ENTRY_MAX_CHARS) {
|
|
211
|
+
getLogger().debug('history', 'prompt_history_entry_too_large', {
|
|
212
|
+
chars: text.length,
|
|
213
|
+
max: PROMPT_ENTRY_MAX_CHARS,
|
|
214
|
+
});
|
|
215
|
+
return current;
|
|
216
|
+
}
|
|
217
|
+
memory = [...current.filter((prompt) => prompt !== text), text].slice(-PROMPT_HISTORY_CAP);
|
|
218
|
+
if (appendLine({ v: PROMPT_HISTORY_LINE_VERSION, ts: now, text })) {
|
|
219
|
+
appendedLines += 1;
|
|
220
|
+
if (loadedLines + appendedLines > HISTORY_COMPACT_AT_LINES)
|
|
221
|
+
compact();
|
|
222
|
+
}
|
|
223
|
+
return memory;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Rewrite the file down to the cap.
|
|
227
|
+
*
|
|
228
|
+
* THE RE-READ ON THE FIRST LINE IS THE WHOLE POINT (RV-3). Writing this
|
|
229
|
+
* process's in-memory array over the file would reintroduce exactly the
|
|
230
|
+
* multi-instance clobbering that append-only exists to avoid — and not "a few
|
|
231
|
+
* trailing entries" either, but everything every other instance wrote since
|
|
232
|
+
* this one last read. A failed read therefore skips compaction entirely: the
|
|
233
|
+
* file keeps growing and the next append tries again, which is strictly better
|
|
234
|
+
* than replacing it with a stale snapshot.
|
|
235
|
+
*/
|
|
236
|
+
function compact() {
|
|
237
|
+
const disk = readFromDisk();
|
|
238
|
+
if (!disk)
|
|
239
|
+
return;
|
|
240
|
+
// Entries this session has but the file does not — normally none, since the
|
|
241
|
+
// appends above are already on disk. It only matters if a read raced a write.
|
|
242
|
+
const onDisk = new Set(disk.entries.map((entry) => entry.text));
|
|
243
|
+
const carried = (memory ?? [])
|
|
244
|
+
.filter((text) => !onDisk.has(text))
|
|
245
|
+
.map((text) => ({ v: PROMPT_HISTORY_LINE_VERSION, ts: Date.now(), text }));
|
|
246
|
+
const kept = dedupeEntries([...disk.entries, ...carried]);
|
|
247
|
+
const path = getPromptHistoryPath();
|
|
248
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
249
|
+
try {
|
|
250
|
+
mkdirSync(getHomeRoot(), { recursive: true });
|
|
251
|
+
const payload = kept.map((entry) => `${JSON.stringify(entry)}\n`).join('');
|
|
252
|
+
writeFileSync(tmp, payload, { encoding: 'utf-8', mode: 0o600 });
|
|
253
|
+
renameSync(tmp, path);
|
|
254
|
+
// rename does not preserve mode everywhere, and this file holds the user's
|
|
255
|
+
// prompts verbatim — re-apply it rather than hope (RV-6, as `store.ts` does).
|
|
256
|
+
if (process.platform !== 'win32')
|
|
257
|
+
chmodSync(path, 0o600);
|
|
258
|
+
memory = kept.map((entry) => entry.text);
|
|
259
|
+
loadedLines = kept.length;
|
|
260
|
+
appendedLines = 0;
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
try {
|
|
264
|
+
rmSync(tmp, { force: true });
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Nothing further to try; the original file is untouched either way.
|
|
268
|
+
}
|
|
269
|
+
getLogger().warn('history', 'history_compact_failed', {
|
|
270
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Migration-only: append the texts that are not in the file yet, returning how
|
|
276
|
+
* many were written.
|
|
277
|
+
*
|
|
278
|
+
* Ignores `historyEnabled` on purpose — the caller decides, because it is the
|
|
279
|
+
* only code that can see an explicit `historyEnabled: false` in the config file
|
|
280
|
+
* before `loadConfig()` has run (§4.6 step 3).
|
|
281
|
+
*/
|
|
282
|
+
export function importLegacyPromptHistory(entries) {
|
|
283
|
+
const read = readFromDisk();
|
|
284
|
+
if (!read)
|
|
285
|
+
return 0;
|
|
286
|
+
const known = new Set(read.entries.map((entry) => entry.text));
|
|
287
|
+
let written = 0;
|
|
288
|
+
for (const text of entries) {
|
|
289
|
+
if (typeof text !== 'string')
|
|
290
|
+
continue;
|
|
291
|
+
if (text.trim().length === 0)
|
|
292
|
+
continue;
|
|
293
|
+
if (text.length > PROMPT_ENTRY_MAX_CHARS)
|
|
294
|
+
continue;
|
|
295
|
+
if (known.has(text))
|
|
296
|
+
continue;
|
|
297
|
+
if (!appendLine({ v: PROMPT_HISTORY_LINE_VERSION, ts: Date.now(), text }))
|
|
298
|
+
break;
|
|
299
|
+
known.add(text);
|
|
300
|
+
written += 1;
|
|
301
|
+
}
|
|
302
|
+
// The cache, if anything already built one, no longer matches the file.
|
|
303
|
+
if (written > 0)
|
|
304
|
+
memory = null;
|
|
305
|
+
return written;
|
|
306
|
+
}
|
|
307
|
+
/** Delete the file and empty the cache. Returns how many entries were removed. */
|
|
308
|
+
export function clearPromptHistory() {
|
|
309
|
+
const removed = readFromDisk()?.entries.length ?? 0;
|
|
310
|
+
try {
|
|
311
|
+
rmSync(getPromptHistoryPath(), { force: true });
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
// Same policy as every other failure here: a file we could not delete is
|
|
315
|
+
// not worth an error the user can do nothing about.
|
|
316
|
+
}
|
|
317
|
+
memory = [];
|
|
318
|
+
loadedLines = 0;
|
|
319
|
+
appendedLines = 0;
|
|
320
|
+
return removed;
|
|
321
|
+
}
|
|
322
|
+
/** Called once by `loadConfig()` after `historyEnabled` has been resolved. */
|
|
323
|
+
export function setHistoryEnabled(enabled) {
|
|
324
|
+
historyEnabled = enabled;
|
|
325
|
+
}
|
|
326
|
+
/** Test-only: forget the cache, the line counters and the switch. */
|
|
327
|
+
export function resetPromptHistoryForTests() {
|
|
328
|
+
memory = null;
|
|
329
|
+
loadedLines = 0;
|
|
330
|
+
appendedLines = 0;
|
|
331
|
+
historyEnabled = true;
|
|
332
|
+
}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -55,9 +55,13 @@ export declare const DEFAULT_TOOL_TIMEOUT_MS = 180000;
|
|
|
55
55
|
export declare const IDLE_TIMEOUT_MARGIN_MS = 30000;
|
|
56
56
|
/** Watchdog idle timeout (§3.8) — must stay ≥ toolTimeout so long tools survive. */
|
|
57
57
|
export declare const DEFAULT_IDLE_TIMEOUT_MS: number;
|
|
58
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Cap on the recall list.
|
|
60
|
+
*
|
|
61
|
+
* The list itself now lives in `<home>/prompt-history.jsonl`; this constant is
|
|
62
|
+
* consumed by `config/prompt-history.ts`, not by the UI.
|
|
63
|
+
*/
|
|
59
64
|
export declare const PROMPT_HISTORY_CAP = 100;
|
|
60
|
-
export declare const RECENT_MODELS_CAP = 10;
|
|
61
65
|
/**
|
|
62
66
|
* How many trailing entries the full-screen viewport renders (invariant I-3).
|
|
63
67
|
* Yoga lays out every child even when `overflow: hidden` clips it, so the cost
|
|
@@ -190,6 +194,51 @@ export declare const DEFAULT_LOG_CONFIG: LogConfig;
|
|
|
190
194
|
* but none of it".
|
|
191
195
|
*/
|
|
192
196
|
export declare function clampLogConfig(raw: unknown): LogConfig;
|
|
197
|
+
export interface TeamConfig {
|
|
198
|
+
/** Register the `task` tool at all. Defaults to TRUE (R-b). */
|
|
199
|
+
enabled: boolean;
|
|
200
|
+
/** Children per dispatch. Clamped to [1, HARD_MAX_SUBAGENTS] (R-g). */
|
|
201
|
+
maxSubagents: number;
|
|
202
|
+
/**
|
|
203
|
+
* Children in flight at once.
|
|
204
|
+
*
|
|
205
|
+
* A SEPARATE KNOB FROM `maxSubagents` on purpose (D-6): provider rate limits
|
|
206
|
+
* are a different constraint from context economics, and folding them into one
|
|
207
|
+
* number makes both wrong. Five simultaneous streams against one API key is a
|
|
208
|
+
* reliable way to collect HTTP 429s, each of which lands in the report as a
|
|
209
|
+
* failed subagent.
|
|
210
|
+
*/
|
|
211
|
+
maxConcurrent: number;
|
|
212
|
+
/** One child's wall clock before its own abort. */
|
|
213
|
+
subagentTimeoutMs: number;
|
|
214
|
+
/** The whole dispatch's wall clock. Also the `task` tool-timeout override. */
|
|
215
|
+
dispatchTimeoutMs: number;
|
|
216
|
+
/** Runaway-loop cap: turns one child may take before it is stopped. */
|
|
217
|
+
maxTurnsPerSubagent: number;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The requirement's ceiling on the fan-out (R-g).
|
|
221
|
+
*
|
|
222
|
+
* Re-exported from `TEAM_LIMITS` rather than re-spelled, because it is enforced
|
|
223
|
+
* in TWO places — here on the config value, and again in
|
|
224
|
+
* `normalizeSubagentSpecs` on what the model asked for — and two copies of the
|
|
225
|
+
* same 10 is exactly how a hand-edited `config.json` ends up being able to raise
|
|
226
|
+
* a ceiling the requirement fixed.
|
|
227
|
+
*/
|
|
228
|
+
export declare const HARD_MAX_SUBAGENTS: 10;
|
|
229
|
+
export declare const DEFAULT_TEAM_CONFIG: TeamConfig;
|
|
230
|
+
/**
|
|
231
|
+
* THE single gate for every team-config read AND write, mirroring
|
|
232
|
+
* `clampSkillsConfig` / `clampLogConfig` for the reason those two record:
|
|
233
|
+
* hardening only the read path leaves a bad value on disk that reverts to the
|
|
234
|
+
* default on every launch, which presents to the user as "my setting won't
|
|
235
|
+
* stick".
|
|
236
|
+
*
|
|
237
|
+
* `maxConcurrent` is additionally capped at `maxSubagents`: a pool wider than
|
|
238
|
+
* the population it draws from is not wrong so much as meaningless, and letting
|
|
239
|
+
* the two disagree would make the panel's `n/m` readout confusing for no gain.
|
|
240
|
+
*/
|
|
241
|
+
export declare function clampTeamConfig(raw: unknown): TeamConfig;
|
|
193
242
|
export interface PersistedConfig {
|
|
194
243
|
version: number;
|
|
195
244
|
provider: string;
|
|
@@ -211,14 +260,27 @@ export interface PersistedConfig {
|
|
|
211
260
|
idleTimeoutMs: number;
|
|
212
261
|
/** provider -> key. Masked in the UI; the file is written 0600 on POSIX. */
|
|
213
262
|
apiKeys: Record<string, string | null>;
|
|
214
|
-
|
|
215
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Record submitted prompts to `<home>/prompt-history.jsonl` at all (D-9).
|
|
265
|
+
*
|
|
266
|
+
* A real preference, and the reason it belongs here rather than in
|
|
267
|
+
* `state.json`: the user declares it, and switching it off is a decision they
|
|
268
|
+
* expect to survive. Command-line only, like `log.redactSecrets` — there is
|
|
269
|
+
* no settings-screen entry, because it is set once and then forgotten.
|
|
270
|
+
*/
|
|
271
|
+
historyEnabled: boolean;
|
|
216
272
|
/** Entry spacing: `comfortable` keeps a blank line at turn boundaries (§4.3). */
|
|
217
273
|
density: DensityMode;
|
|
218
274
|
/** Show the composer hint row at all. */
|
|
219
275
|
hints: boolean;
|
|
220
|
-
/**
|
|
221
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Enable SGR mouse reporting in full-screen mode so the wheel scrolls the
|
|
278
|
+
* region under the pointer (mouse-wheel-region-routing §5.7).
|
|
279
|
+
*
|
|
280
|
+
* A PLAIN BOOLEAN, not a `fullscreen`-style tri-state: there are no
|
|
281
|
+
* heuristics for an explicit `true` to override (D-10).
|
|
282
|
+
*/
|
|
283
|
+
mouse: boolean;
|
|
222
284
|
/** Start every session in PLAN mode (plan-mode §4.4). */
|
|
223
285
|
planModeDefault: boolean;
|
|
224
286
|
/** `ask_user` rounds allowed per user turn; clamped to [1, 10]. */
|
|
@@ -229,8 +291,34 @@ export interface PersistedConfig {
|
|
|
229
291
|
skills: SkillsConfig;
|
|
230
292
|
/** Logging settings. The THIRD nested object; scalars only (see LogConfig). */
|
|
231
293
|
log: LogConfig;
|
|
294
|
+
/** Team mode. The FOURTH nested object; scalars only (see TeamConfig). */
|
|
295
|
+
team: TeamConfig;
|
|
232
296
|
}
|
|
233
297
|
export declare const DEFAULT_CONFIG: PersistedConfig;
|
|
298
|
+
/**
|
|
299
|
+
* Runtime state that used to be stored in `config.json` by mistake
|
|
300
|
+
* (config-state-separation §4.5). Each one now lives in its own file:
|
|
301
|
+
* `promptHistory` → `prompt-history.jsonl`, `submitCount` / `mouseNoticeSeen`
|
|
302
|
+
* → `state.json`, and `recentModels` was dead — nothing ever wrote it.
|
|
303
|
+
*/
|
|
304
|
+
export declare const LEGACY_STATE_KEYS: readonly ["promptHistory", "submitCount", "mouseNoticeSeen", "recentModels"];
|
|
305
|
+
/**
|
|
306
|
+
* THE single gate that keeps those keys out of `config.json`, and the one thing
|
|
307
|
+
* in this change that cannot be skipped.
|
|
308
|
+
*
|
|
309
|
+
* `loadPersistedConfig()` merges `{ ...DEFAULT_CONFIG, ...partial }` where
|
|
310
|
+
* `partial` is the raw parse of whatever is on disk. Removing the fields from
|
|
311
|
+
* the interface therefore removes NOTHING from the file: the keys ride through
|
|
312
|
+
* the spread and `updatePersistedConfig` writes them straight back. The user
|
|
313
|
+
* deletes `promptHistory` by hand and watches it grow back.
|
|
314
|
+
*
|
|
315
|
+
* Applied on the READ side on purpose, which is what makes it self-healing:
|
|
316
|
+
* every write goes read-strip-merge-write, so a hand-edited file, a downgrade
|
|
317
|
+
* to 0.5.x and back, or a half-finished migration all converge on a clean file
|
|
318
|
+
* at the next write. Same shape as `clampSkillsConfig` / `clampLogConfig` —
|
|
319
|
+
* one door for both directions.
|
|
320
|
+
*/
|
|
321
|
+
export declare function stripLegacyStateKeys<T extends object>(raw: T): T;
|
|
234
322
|
export interface CliConfig {
|
|
235
323
|
provider: string;
|
|
236
324
|
model: string;
|
|
@@ -259,11 +347,22 @@ export interface CliConfig {
|
|
|
259
347
|
idleTimeoutMs: number;
|
|
260
348
|
/** provider -> resolved key (config-file key ?? provider env var). */
|
|
261
349
|
apiKeys: Record<string, string | undefined>;
|
|
262
|
-
|
|
263
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Whether submitted prompts are recorded (D-9).
|
|
352
|
+
*
|
|
353
|
+
* The recall LIST is deliberately not here: its only consumer is one
|
|
354
|
+
* `useState` seed in `App.tsx`, and carrying it on `CliConfig` would make
|
|
355
|
+
* every `loadConfig()` — `aragon logs tail` and `aragon skills list`
|
|
356
|
+
* included — read a file it has no use for (D-4).
|
|
357
|
+
*/
|
|
358
|
+
historyEnabled: boolean;
|
|
264
359
|
density: DensityMode;
|
|
265
360
|
hints: boolean;
|
|
266
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Whether this session may enable SGR mouse reporting (full-screen + TTY
|
|
363
|
+
* only). Resolved by `resolveMouse` in `load.ts`.
|
|
364
|
+
*/
|
|
365
|
+
mouse: boolean;
|
|
267
366
|
/**
|
|
268
367
|
* Whether this session opens in PLAN mode.
|
|
269
368
|
*
|
|
@@ -279,6 +378,23 @@ export interface CliConfig {
|
|
|
279
378
|
skillsRuntime: SkillsRuntimeOptions;
|
|
280
379
|
/** Resolved logging settings; the authoritative input to `Logger.reconfigure`. */
|
|
281
380
|
log: LogConfig;
|
|
381
|
+
/**
|
|
382
|
+
* Resolved team-mode settings (team-subagents §3.10).
|
|
383
|
+
*
|
|
384
|
+
* `enabled` here decides whether `task` is REGISTERED, which happens once at
|
|
385
|
+
* construction and can never be undone (§4.5 / D-17). The live on/off switch
|
|
386
|
+
* `/team` flips is a separate flag on the controller.
|
|
387
|
+
*/
|
|
388
|
+
team: TeamConfig;
|
|
389
|
+
/**
|
|
390
|
+
* Lifetime submit count driving the composer hint fade (`HINT_FADE_AFTER`).
|
|
391
|
+
*
|
|
392
|
+
* Read from `<home>/state.json` rather than the config file. It stays on
|
|
393
|
+
* `CliConfig` — unlike the recall list — because it is one scalar that is
|
|
394
|
+
* already plumbed through `controller.getConfig()` / `setSubmitCount()` and
|
|
395
|
+
* read during render (D-4).
|
|
396
|
+
*/
|
|
397
|
+
submitCount: number;
|
|
282
398
|
/** Working directory for tools (default `process.cwd()`, overridable). */
|
|
283
399
|
cwd: string;
|
|
284
400
|
/** Whether ANSI color is enabled. */
|
package/dist/config/schema.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { isAbsolute } from 'node:path';
|
|
9
9
|
import { clampLogLevel } from '../logging/levels.js';
|
|
10
|
+
import { TEAM_LIMITS } from '../team/limits.js';
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
12
|
// Provider set — only the adapter-backed providers are usable at run time (R5).
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
@@ -88,9 +89,13 @@ export const DEFAULT_TOOL_TIMEOUT_MS = 180_000;
|
|
|
88
89
|
export const IDLE_TIMEOUT_MARGIN_MS = 30_000;
|
|
89
90
|
/** Watchdog idle timeout (§3.8) — must stay ≥ toolTimeout so long tools survive. */
|
|
90
91
|
export const DEFAULT_IDLE_TIMEOUT_MS = DEFAULT_TOOL_TIMEOUT_MS + IDLE_TIMEOUT_MARGIN_MS;
|
|
91
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Cap on the recall list.
|
|
94
|
+
*
|
|
95
|
+
* The list itself now lives in `<home>/prompt-history.jsonl`; this constant is
|
|
96
|
+
* consumed by `config/prompt-history.ts`, not by the UI.
|
|
97
|
+
*/
|
|
92
98
|
export const PROMPT_HISTORY_CAP = 100;
|
|
93
|
-
export const RECENT_MODELS_CAP = 10;
|
|
94
99
|
/**
|
|
95
100
|
* How many trailing entries the full-screen viewport renders (invariant I-3).
|
|
96
101
|
* Yoga lays out every child even when `overflow: hidden` clips it, so the cost
|
|
@@ -260,6 +265,52 @@ export function clampLogConfig(raw) {
|
|
|
260
265
|
: DEFAULT_LOG_CONFIG.previewChars,
|
|
261
266
|
};
|
|
262
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* The requirement's ceiling on the fan-out (R-g).
|
|
270
|
+
*
|
|
271
|
+
* Re-exported from `TEAM_LIMITS` rather than re-spelled, because it is enforced
|
|
272
|
+
* in TWO places — here on the config value, and again in
|
|
273
|
+
* `normalizeSubagentSpecs` on what the model asked for — and two copies of the
|
|
274
|
+
* same 10 is exactly how a hand-edited `config.json` ends up being able to raise
|
|
275
|
+
* a ceiling the requirement fixed.
|
|
276
|
+
*/
|
|
277
|
+
export const HARD_MAX_SUBAGENTS = TEAM_LIMITS.hardMaxSubagents;
|
|
278
|
+
export const DEFAULT_TEAM_CONFIG = {
|
|
279
|
+
enabled: true,
|
|
280
|
+
maxSubagents: 5,
|
|
281
|
+
maxConcurrent: 3,
|
|
282
|
+
subagentTimeoutMs: 300_000,
|
|
283
|
+
dispatchTimeoutMs: 900_000,
|
|
284
|
+
maxTurnsPerSubagent: 24,
|
|
285
|
+
};
|
|
286
|
+
const SUBAGENTS_RANGE = { min: 1, max: HARD_MAX_SUBAGENTS };
|
|
287
|
+
const CONCURRENT_RANGE = { min: 1, max: HARD_MAX_SUBAGENTS };
|
|
288
|
+
const SUBAGENT_TIMEOUT_RANGE = { min: 30_000, max: 1_800_000 };
|
|
289
|
+
const DISPATCH_TIMEOUT_RANGE = { min: 60_000, max: 3_600_000 };
|
|
290
|
+
const TURNS_RANGE = { min: 4, max: 100 };
|
|
291
|
+
/**
|
|
292
|
+
* THE single gate for every team-config read AND write, mirroring
|
|
293
|
+
* `clampSkillsConfig` / `clampLogConfig` for the reason those two record:
|
|
294
|
+
* hardening only the read path leaves a bad value on disk that reverts to the
|
|
295
|
+
* default on every launch, which presents to the user as "my setting won't
|
|
296
|
+
* stick".
|
|
297
|
+
*
|
|
298
|
+
* `maxConcurrent` is additionally capped at `maxSubagents`: a pool wider than
|
|
299
|
+
* the population it draws from is not wrong so much as meaningless, and letting
|
|
300
|
+
* the two disagree would make the panel's `n/m` readout confusing for no gain.
|
|
301
|
+
*/
|
|
302
|
+
export function clampTeamConfig(raw) {
|
|
303
|
+
const src = (raw && typeof raw === 'object' ? raw : {});
|
|
304
|
+
const maxSubagents = clampInt(src.maxSubagents, DEFAULT_TEAM_CONFIG.maxSubagents, SUBAGENTS_RANGE);
|
|
305
|
+
return {
|
|
306
|
+
enabled: bool(src.enabled, DEFAULT_TEAM_CONFIG.enabled),
|
|
307
|
+
maxSubagents,
|
|
308
|
+
maxConcurrent: Math.min(maxSubagents, clampInt(src.maxConcurrent, DEFAULT_TEAM_CONFIG.maxConcurrent, CONCURRENT_RANGE)),
|
|
309
|
+
subagentTimeoutMs: clampInt(src.subagentTimeoutMs, DEFAULT_TEAM_CONFIG.subagentTimeoutMs, SUBAGENT_TIMEOUT_RANGE),
|
|
310
|
+
dispatchTimeoutMs: clampInt(src.dispatchTimeoutMs, DEFAULT_TEAM_CONFIG.dispatchTimeoutMs, DISPATCH_TIMEOUT_RANGE),
|
|
311
|
+
maxTurnsPerSubagent: clampInt(src.maxTurnsPerSubagent, DEFAULT_TEAM_CONFIG.maxTurnsPerSubagent, TURNS_RANGE),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
263
314
|
export const DEFAULT_CONFIG = {
|
|
264
315
|
version: CONFIG_VERSION,
|
|
265
316
|
provider: 'anthropic',
|
|
@@ -276,17 +327,51 @@ export const DEFAULT_CONFIG = {
|
|
|
276
327
|
toolTimeoutMs: DEFAULT_TOOL_TIMEOUT_MS,
|
|
277
328
|
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
278
329
|
apiKeys: {},
|
|
279
|
-
|
|
280
|
-
promptHistory: [],
|
|
330
|
+
historyEnabled: true,
|
|
281
331
|
density: 'comfortable',
|
|
282
332
|
hints: true,
|
|
283
|
-
|
|
333
|
+
mouse: true,
|
|
284
334
|
planModeDefault: false,
|
|
285
335
|
planModeMaxAskRounds: DEFAULT_PLAN_MAX_ASK_ROUNDS,
|
|
286
336
|
planModeHumanTimeoutMs: DEFAULT_PLAN_HUMAN_TIMEOUT_MS,
|
|
287
337
|
skills: DEFAULT_SKILLS_CONFIG,
|
|
288
338
|
log: DEFAULT_LOG_CONFIG,
|
|
339
|
+
team: DEFAULT_TEAM_CONFIG,
|
|
289
340
|
};
|
|
341
|
+
/**
|
|
342
|
+
* Runtime state that used to be stored in `config.json` by mistake
|
|
343
|
+
* (config-state-separation §4.5). Each one now lives in its own file:
|
|
344
|
+
* `promptHistory` → `prompt-history.jsonl`, `submitCount` / `mouseNoticeSeen`
|
|
345
|
+
* → `state.json`, and `recentModels` was dead — nothing ever wrote it.
|
|
346
|
+
*/
|
|
347
|
+
export const LEGACY_STATE_KEYS = [
|
|
348
|
+
'promptHistory',
|
|
349
|
+
'submitCount',
|
|
350
|
+
'mouseNoticeSeen',
|
|
351
|
+
'recentModels',
|
|
352
|
+
];
|
|
353
|
+
/**
|
|
354
|
+
* THE single gate that keeps those keys out of `config.json`, and the one thing
|
|
355
|
+
* in this change that cannot be skipped.
|
|
356
|
+
*
|
|
357
|
+
* `loadPersistedConfig()` merges `{ ...DEFAULT_CONFIG, ...partial }` where
|
|
358
|
+
* `partial` is the raw parse of whatever is on disk. Removing the fields from
|
|
359
|
+
* the interface therefore removes NOTHING from the file: the keys ride through
|
|
360
|
+
* the spread and `updatePersistedConfig` writes them straight back. The user
|
|
361
|
+
* deletes `promptHistory` by hand and watches it grow back.
|
|
362
|
+
*
|
|
363
|
+
* Applied on the READ side on purpose, which is what makes it self-healing:
|
|
364
|
+
* every write goes read-strip-merge-write, so a hand-edited file, a downgrade
|
|
365
|
+
* to 0.5.x and back, or a half-finished migration all converge on a clean file
|
|
366
|
+
* at the next write. Same shape as `clampSkillsConfig` / `clampLogConfig` —
|
|
367
|
+
* one door for both directions.
|
|
368
|
+
*/
|
|
369
|
+
export function stripLegacyStateKeys(raw) {
|
|
370
|
+
const out = { ...raw };
|
|
371
|
+
for (const key of LEGACY_STATE_KEYS)
|
|
372
|
+
delete out[key];
|
|
373
|
+
return out;
|
|
374
|
+
}
|
|
290
375
|
// ---------------------------------------------------------------------------
|
|
291
376
|
// Coercion / validation helpers
|
|
292
377
|
// ---------------------------------------------------------------------------
|