@aragon-agent/cli 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +321 -0
- package/README.md +297 -2
- package/dist/agent/controller.d.ts +193 -1
- package/dist/agent/controller.js +475 -2
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +47 -0
- package/dist/agent/reducer.d.ts +189 -0
- package/dist/agent/reducer.js +301 -2
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +297 -16
- package/dist/commands/builtins.js +165 -24
- package/dist/commands/registry.d.ts +27 -0
- package/dist/compaction/archive.d.ts +175 -0
- package/dist/compaction/archive.js +267 -0
- package/dist/compaction/child.d.ts +97 -0
- package/dist/compaction/child.js +118 -0
- package/dist/compaction/command.d.ts +41 -0
- package/dist/compaction/command.js +355 -0
- package/dist/compaction/compactor.d.ts +302 -0
- package/dist/compaction/compactor.js +810 -0
- package/dist/compaction/digest.d.ts +62 -0
- package/dist/compaction/digest.js +164 -0
- package/dist/compaction/limits.d.ts +165 -0
- package/dist/compaction/limits.js +172 -0
- package/dist/compaction/pressure.d.ts +135 -0
- package/dist/compaction/pressure.js +167 -0
- package/dist/compaction/summarize-call.d.ts +70 -0
- package/dist/compaction/summarize-call.js +86 -0
- package/dist/compaction/summary-prompt.d.ts +85 -0
- package/dist/compaction/summary-prompt.js +197 -0
- package/dist/compaction/tail-budget.d.ts +52 -0
- package/dist/compaction/tail-budget.js +67 -0
- package/dist/compaction/types.d.ts +215 -0
- package/dist/compaction/types.js +22 -0
- package/dist/compaction/wiring.d.ts +264 -0
- package/dist/compaction/wiring.js +577 -0
- package/dist/config/app-paths.d.ts +14 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/cli-commands.d.ts +41 -0
- package/dist/config/cli-commands.js +77 -1
- package/dist/config/env.d.ts +5 -2
- package/dist/config/env.js +140 -3
- package/dist/config/load.d.ts +42 -0
- package/dist/config/load.js +101 -1
- package/dist/config/schema.d.ts +244 -0
- package/dist/config/schema.js +170 -0
- package/dist/config/store.js +37 -1
- package/dist/config/ui-state.d.ts +28 -1
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +50 -1
- package/dist/exec/runner.js +42 -0
- package/dist/input/limits.d.ts +72 -0
- package/dist/input/limits.js +63 -0
- package/dist/input/mouse-events.d.ts +31 -5
- package/dist/input/mouse-events.js +86 -27
- package/dist/input/paste-parse.d.ts +71 -0
- package/dist/input/paste-parse.js +95 -0
- package/dist/input/stdin-filter.d.ts +81 -0
- package/dist/input/stdin-filter.js +379 -0
- package/dist/logging/install.d.ts +53 -1
- package/dist/logging/install.js +81 -1
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/classify.d.ts +52 -0
- package/dist/proc/classify.js +143 -0
- package/dist/proc/kill-tree.d.ts +42 -0
- package/dist/proc/kill-tree.js +119 -0
- package/dist/proc/limits.d.ts +84 -0
- package/dist/proc/limits.js +84 -0
- package/dist/proc/log-ring.d.ts +68 -0
- package/dist/proc/log-ring.js +102 -0
- package/dist/proc/prompt.d.ts +24 -0
- package/dist/proc/prompt.js +49 -0
- package/dist/proc/readiness.d.ts +102 -0
- package/dist/proc/readiness.js +219 -0
- package/dist/proc/supervisor.d.ts +103 -0
- package/dist/proc/supervisor.js +465 -0
- package/dist/proc/types.d.ts +175 -0
- package/dist/proc/types.js +15 -0
- package/dist/session/persist.js +55 -1
- package/dist/team/report.js +7 -1
- package/dist/team/runtime.d.ts +10 -0
- package/dist/team/runtime.js +7 -0
- package/dist/team/subagent.d.ts +31 -1
- package/dist/team/subagent.js +42 -15
- package/dist/team/types.d.ts +8 -0
- package/dist/tools/bash-tool.d.ts +51 -2
- package/dist/tools/bash-tool.js +321 -51
- package/dist/tools/fs-tools.d.ts +17 -0
- package/dist/tools/index.d.ts +55 -2
- package/dist/tools/index.js +39 -1
- package/dist/tools/proc-tools.d.ts +25 -0
- package/dist/tools/proc-tools.js +102 -0
- package/dist/ui/ActivityLine.d.ts +19 -1
- package/dist/ui/ActivityLine.js +10 -2
- package/dist/ui/App.d.ts +62 -2
- package/dist/ui/App.js +546 -16
- package/dist/ui/Composer.d.ts +66 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +82 -3
- package/dist/ui/PromptInput.js +232 -49
- package/dist/ui/StatusBar.d.ts +59 -0
- package/dist/ui/StatusBar.js +16 -4
- package/dist/ui/TeamPanel.js +7 -1
- package/dist/ui/Transcript.d.ts +21 -1
- package/dist/ui/Transcript.js +61 -3
- package/dist/ui/clipboard.d.ts +52 -0
- package/dist/ui/clipboard.js +91 -0
- package/dist/ui/composer-limits.d.ts +31 -0
- package/dist/ui/composer-limits.js +50 -0
- package/dist/ui/composer-rows.d.ts +73 -0
- package/dist/ui/composer-rows.js +201 -0
- package/dist/ui/editor-reducer.d.ts +37 -0
- package/dist/ui/editor-reducer.js +88 -12
- package/dist/ui/entries/CompactionCard.d.ts +75 -0
- package/dist/ui/entries/CompactionCard.js +158 -0
- package/dist/ui/entries/ServiceCard.d.ts +66 -0
- package/dist/ui/entries/ServiceCard.js +116 -0
- package/dist/ui/entries/UserEntry.d.ts +17 -3
- package/dist/ui/entries/UserEntry.js +34 -5
- package/dist/ui/frame-differ.d.ts +46 -0
- package/dist/ui/frame-differ.js +94 -10
- package/dist/ui/gauge.d.ts +16 -1
- package/dist/ui/gauge.js +25 -6
- package/dist/ui/glyphs.d.ts +11 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/ScrollViewport.d.ts +43 -6
- package/dist/ui/layout/ScrollViewport.js +128 -21
- package/dist/ui/layout/budget.d.ts +44 -9
- package/dist/ui/layout/budget.js +42 -10
- package/dist/ui/layout/follow-state.d.ts +137 -0
- package/dist/ui/layout/follow-state.js +150 -0
- package/dist/ui/layout/virtual-window.d.ts +11 -0
- package/dist/ui/layout/virtual-window.js +85 -1
- package/dist/ui/overlays/HelpOverlay.js +9 -1
- package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
- package/dist/ui/overlays/QuestionOverlay.js +5 -1
- package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
- package/dist/ui/overlays/SettingsScreen.js +74 -2
- package/dist/ui/palettes.d.ts +16 -0
- package/dist/ui/palettes.js +6 -0
- package/dist/ui/paste-frames.d.ts +87 -0
- package/dist/ui/paste-frames.js +112 -0
- package/dist/ui/paste-tokens.d.ts +90 -0
- package/dist/ui/paste-tokens.js +147 -0
- package/dist/ui/screen.d.ts +38 -0
- package/dist/ui/screen.js +113 -8
- package/dist/ui/selection/highlight.d.ts +39 -0
- package/dist/ui/selection/highlight.js +64 -0
- package/dist/ui/selection/screen-mirror.d.ts +24 -0
- package/dist/ui/selection/screen-mirror.js +35 -0
- package/dist/ui/selection/selection-controller.d.ts +88 -0
- package/dist/ui/selection/selection-controller.js +282 -0
- package/dist/ui/selection/selection.d.ts +90 -0
- package/dist/ui/selection/selection.js +223 -0
- package/dist/ui/stdout-frame-writer.d.ts +33 -0
- package/dist/ui/stdout-frame-writer.js +15 -0
- package/dist/ui/theme.d.ts +11 -0
- package/dist/ui/theme.js +2 -0
- package/dist/ui/transcript-text.js +72 -0
- package/dist/ui/use-startup-notices.d.ts +27 -1
- package/dist/ui/use-startup-notices.js +43 -11
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/dist/ui/use-wheel-routing.js +8 -0
- package/package.json +4 -2
- package/dist/input/stdin-mouse-filter.d.ts +0 -42
- package/dist/input/stdin-mouse-filter.js +0 -153
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The compaction archive - a compaction stops being irreversible
|
|
3
|
+
* (context-auto-compaction-hardening §3.5 / W4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope
|
|
6
|
+
* (`glyphs.test.ts::inScope`), and that INCLUDES the field names below and every
|
|
7
|
+
* string `/compact history` and `/compact show` print.
|
|
8
|
+
*
|
|
9
|
+
* WHAT THIS IS FOR. Round 1's non-goal was stated plainly: "the dropped messages
|
|
10
|
+
* are gone". Combined with R-1 - "the summary is wrong or lossy, and the agent
|
|
11
|
+
* proceeds confidently on a false record... this is the feature's defining risk
|
|
12
|
+
* and it cannot be eliminated" - the product was asking the user to accept an
|
|
13
|
+
* unverifiable, unrecoverable transformation of their session. That is the wrong
|
|
14
|
+
* default for a tool that runs unattended. This does not add undo (§12: restoring
|
|
15
|
+
* an over-full history re-creates the condition that triggered compaction). It
|
|
16
|
+
* adds FIDELITY ON DISK, so R-1 becomes something a human can check.
|
|
17
|
+
*
|
|
18
|
+
* NO IMPORTS FROM `ui/`. This module writes files and formats two report blocks;
|
|
19
|
+
* it has no terminal, no theme and no glyphs.
|
|
20
|
+
*/
|
|
21
|
+
import { mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { getCompactionArchiveDir } from '../config/app-paths.js';
|
|
24
|
+
import { getLogger } from '../logging/logger.js';
|
|
25
|
+
import { COMPACTION_BLOCK_VERSION, COMPACTION_LIMITS } from './limits.js';
|
|
26
|
+
const FILE_PREFIX = 'compaction-';
|
|
27
|
+
const FILE_SUFFIX = '.json';
|
|
28
|
+
/** Bumped only when the SHAPE changes; the reader rejects anything else. */
|
|
29
|
+
export const ARCHIVE_FORMAT_VERSION = 1;
|
|
30
|
+
/**
|
|
31
|
+
* `compaction-<YYYY-MM-DD>-<hhmmss>-<runId>-<index>.json`.
|
|
32
|
+
*
|
|
33
|
+
* THE RUN ID IS IN THE NAME BECAUSE THE DIRECTORY IS SHARED (RV-6).
|
|
34
|
+
* `file-sink.ts` makes log retention best-effort precisely because "on Windows
|
|
35
|
+
* another `aragon` process may hold a handle on a file we would like to remove",
|
|
36
|
+
* so concurrent CLI processes are an acknowledged in-tree condition rather than
|
|
37
|
+
* an edge case. Without this segment `/compact history` presents another
|
|
38
|
+
* session's compactions as this one's and `/compact show 2` is ambiguous the
|
|
39
|
+
* moment two runs both reach their second compaction.
|
|
40
|
+
*/
|
|
41
|
+
export function archiveFileName(runId, index, at) {
|
|
42
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
43
|
+
const date = `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}`;
|
|
44
|
+
const time = `${pad(at.getHours())}${pad(at.getMinutes())}${pad(at.getSeconds())}`;
|
|
45
|
+
return `${FILE_PREFIX}${date}-${time}-${runId}-${index}${FILE_SUFFIX}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The inverse of `archiveFileName`, or `null` when the name is not one of ours.
|
|
49
|
+
*
|
|
50
|
+
* AN EXACT PARSE RATHER THAN `name.includes('-' + runId + '-')`, because that
|
|
51
|
+
* substring test is not the question either caller is asking. A run id is a pid
|
|
52
|
+
* in base 36 followed by four base-36 characters of the clock, so a six-character
|
|
53
|
+
* all-digit one matches the `hhmmss` segment of EVERY other run's file written at
|
|
54
|
+
* that second - and the two places this attribution is used are precisely the two
|
|
55
|
+
* RV-6 exists to protect: retention deleting a neighbour's archive, and
|
|
56
|
+
* `/compact history` presenting one as this run's.
|
|
57
|
+
*
|
|
58
|
+
* KEEP IT IN STEP WITH `archiveFileName` ABOVE. The two are one format, written
|
|
59
|
+
* adjacently so a change to either is visible against the other.
|
|
60
|
+
*/
|
|
61
|
+
function parseArchiveName(name) {
|
|
62
|
+
const match = /^compaction-\d{4}-\d{2}-\d{2}-\d{6}-(.+)-(\d+)\.json$/.exec(name);
|
|
63
|
+
if (!match)
|
|
64
|
+
return null;
|
|
65
|
+
return { runId: match[1], index: Number(match[2]) };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Mint this process's archive key.
|
|
69
|
+
*
|
|
70
|
+
* NOT `SessionMeta.id`: that exists only once a session has been SAVED, and the
|
|
71
|
+
* unattended long run this feature serves is exactly the one nobody saved. Short,
|
|
72
|
+
* filename-safe, and only ever compared for equality - a grouping key, not an
|
|
73
|
+
* identity.
|
|
74
|
+
*/
|
|
75
|
+
export function mintRunId(pid = process.pid, now = Date.now()) {
|
|
76
|
+
return `${pid.toString(36)}${now.toString(36).slice(-4)}`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Serialize, clipping the dropped bodies from the OLDEST end if the document
|
|
80
|
+
* exceeds `archiveMaxBytes`.
|
|
81
|
+
*
|
|
82
|
+
* THE METADATA AND THE SUMMARY ARE NEVER CLIPPED. A truncated archive that still
|
|
83
|
+
* says what happened, when, and with which model beats no archive at all - and
|
|
84
|
+
* the oldest dropped messages are the least likely to be the ones a human came
|
|
85
|
+
* looking for.
|
|
86
|
+
*/
|
|
87
|
+
export function serializeArchive(doc, maxBytes) {
|
|
88
|
+
let dropped = doc.dropped;
|
|
89
|
+
let clipped = doc.clipped;
|
|
90
|
+
let text = JSON.stringify({ ...doc, dropped, clipped }, null, 2);
|
|
91
|
+
while (Buffer.byteLength(text, 'utf8') > maxBytes && dropped.length > 0) {
|
|
92
|
+
// HALVED RATHER THAN SHIFTED ONE AT A TIME: a 100 MB archive of 4 000
|
|
93
|
+
// messages would otherwise re-serialize 4 000 times, inside a write that is
|
|
94
|
+
// supposed to be invisible to the run.
|
|
95
|
+
dropped = dropped.slice(Math.max(1, Math.ceil(dropped.length / 2)));
|
|
96
|
+
clipped = true;
|
|
97
|
+
text = JSON.stringify({ ...doc, dropped, clipped }, null, 2);
|
|
98
|
+
}
|
|
99
|
+
return text;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Write one archive, then prune. Returns the path written, or `null`.
|
|
103
|
+
*
|
|
104
|
+
* NEVER THROWS. The caller runs inside an event handler on the compaction path;
|
|
105
|
+
* a filesystem is not allowed to affect whether a session survives its context
|
|
106
|
+
* window. Every failure is a `warn` with the path and nothing else - a toast for
|
|
107
|
+
* a best-effort audit file trains users to ignore toasts.
|
|
108
|
+
*/
|
|
109
|
+
export function writeArchive(input) {
|
|
110
|
+
const dir = input.dir ?? getCompactionArchiveDir();
|
|
111
|
+
const now = input.now ?? Date.now();
|
|
112
|
+
const path = join(dir, archiveFileName(input.runId, input.index, new Date(now)));
|
|
113
|
+
try {
|
|
114
|
+
mkdirSync(dir, { recursive: true });
|
|
115
|
+
const doc = {
|
|
116
|
+
version: ARCHIVE_FORMAT_VERSION,
|
|
117
|
+
blockVersion: COMPACTION_BLOCK_VERSION,
|
|
118
|
+
createdAt: now,
|
|
119
|
+
runId: input.runId,
|
|
120
|
+
index: input.index,
|
|
121
|
+
trigger: input.trigger,
|
|
122
|
+
mode: input.mode,
|
|
123
|
+
model: input.model,
|
|
124
|
+
generation: input.generation,
|
|
125
|
+
tokensBefore: input.tokensBefore,
|
|
126
|
+
tokensAfter: input.tokensAfter,
|
|
127
|
+
messagesBefore: input.messagesBefore,
|
|
128
|
+
messagesAfter: input.messagesAfter,
|
|
129
|
+
droppedCount: input.dropped.length,
|
|
130
|
+
...(input.summary !== undefined ? { summary: input.summary } : {}),
|
|
131
|
+
...(input.tailRelief ? { tailRelief: input.tailRelief } : {}),
|
|
132
|
+
clipped: false,
|
|
133
|
+
dropped: input.dropped,
|
|
134
|
+
};
|
|
135
|
+
writeFileSync(path, serializeArchive(doc, COMPACTION_LIMITS.archiveMaxBytes), 'utf8');
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
getLogger()
|
|
139
|
+
.child('compaction')
|
|
140
|
+
.warn('compaction_archive_failed', { path, error: errorText(err) });
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
pruneArchives(input.runId, dir, now);
|
|
144
|
+
return path;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Two-tier retention, and the ORDER MATTERS (§3.5.2 / RV-6).
|
|
148
|
+
*
|
|
149
|
+
* 1. WITHIN THIS RUN: keep the newest `archiveMaxFiles` files carrying THIS
|
|
150
|
+
* `runId`. This is the bound that protects the user from their own long
|
|
151
|
+
* session, and it can never touch another run.
|
|
152
|
+
* 2. ACROSS RUNS: delete files older than `archiveMaxAgeMs`, regardless of
|
|
153
|
+
* `runId`, so an abandoned run's archives do not accumulate forever. AGE, not
|
|
154
|
+
* count - a count-based global prune is exactly the rule that deletes a live
|
|
155
|
+
* session's work while it is still running.
|
|
156
|
+
*
|
|
157
|
+
* Best-effort throughout, in the `enforceRetention` shape: every `rmSync` in its
|
|
158
|
+
* own `try`, a locked file skipped rather than fatal.
|
|
159
|
+
*/
|
|
160
|
+
export function pruneArchives(runId, dir, now) {
|
|
161
|
+
let files;
|
|
162
|
+
try {
|
|
163
|
+
files = readdirSync(dir)
|
|
164
|
+
.filter((name) => name.startsWith(FILE_PREFIX) && name.endsWith(FILE_SUFFIX))
|
|
165
|
+
.map((name) => {
|
|
166
|
+
const path = join(dir, name);
|
|
167
|
+
try {
|
|
168
|
+
return { path, name, mtime: statSync(path).mtimeMs };
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
.filter((e) => e !== null);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return; // The directory does not exist yet, or is unreadable.
|
|
178
|
+
}
|
|
179
|
+
const mine = files
|
|
180
|
+
.filter((f) => parseArchiveName(f.name)?.runId === runId)
|
|
181
|
+
.sort((a, b) => a.mtime - b.mtime);
|
|
182
|
+
for (const f of mine.slice(0, Math.max(0, mine.length - COMPACTION_LIMITS.archiveMaxFiles))) {
|
|
183
|
+
remove(f.path);
|
|
184
|
+
}
|
|
185
|
+
for (const f of files) {
|
|
186
|
+
if (now - f.mtime > COMPACTION_LIMITS.archiveMaxAgeMs)
|
|
187
|
+
remove(f.path);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function remove(path) {
|
|
191
|
+
try {
|
|
192
|
+
rmSync(path, { force: true });
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Held by another instance. Retention is best-effort by design.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* This run's archives, newest first, plus a COUNT of everyone else's.
|
|
200
|
+
*
|
|
201
|
+
* THE COUNT IS REPORTED RATHER THAN THE FILES, because the alternative is a user
|
|
202
|
+
* concluding their own compactions went missing when the directory holds a
|
|
203
|
+
* neighbour's.
|
|
204
|
+
*/
|
|
205
|
+
export function listArchives(runId, dirOverride) {
|
|
206
|
+
const dir = dirOverride ?? getCompactionArchiveDir();
|
|
207
|
+
const entries = [];
|
|
208
|
+
let otherRuns = 0;
|
|
209
|
+
let names;
|
|
210
|
+
try {
|
|
211
|
+
names = readdirSync(dir).filter((name) => name.startsWith(FILE_PREFIX) && name.endsWith(FILE_SUFFIX));
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return { entries: [], otherRuns: 0, dir };
|
|
215
|
+
}
|
|
216
|
+
for (const name of names) {
|
|
217
|
+
if (parseArchiveName(name)?.runId !== runId) {
|
|
218
|
+
otherRuns += 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const entry = readArchiveEntry(join(dir, name), name);
|
|
222
|
+
if (entry)
|
|
223
|
+
entries.push(entry);
|
|
224
|
+
}
|
|
225
|
+
entries.sort((a, b) => b.index - a.index);
|
|
226
|
+
return { entries, otherRuns, dir };
|
|
227
|
+
}
|
|
228
|
+
/** One archive by its `#n`, or `null`. `n` is the index within THIS run. */
|
|
229
|
+
export function findArchive(runId, index, dirOverride) {
|
|
230
|
+
return listArchives(runId, dirOverride).entries.find((e) => e.index === index) ?? null;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Read one document's METADATA. The `dropped` array is never returned.
|
|
234
|
+
*
|
|
235
|
+
* `/compact show` prints no message bodies (DH-9): a single archive can be
|
|
236
|
+
* megabytes and the transcript is not a pager. The path is the useful output.
|
|
237
|
+
*/
|
|
238
|
+
function readArchiveEntry(path, fileName) {
|
|
239
|
+
try {
|
|
240
|
+
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
241
|
+
if (raw.version !== ARCHIVE_FORMAT_VERSION)
|
|
242
|
+
return null;
|
|
243
|
+
return {
|
|
244
|
+
path,
|
|
245
|
+
fileName,
|
|
246
|
+
index: Number(raw.index ?? 0),
|
|
247
|
+
createdAt: Number(raw.createdAt ?? 0),
|
|
248
|
+
trigger: (raw.trigger ?? 'pressure'),
|
|
249
|
+
mode: (raw.mode ?? 'none'),
|
|
250
|
+
model: String(raw.model ?? ''),
|
|
251
|
+
tokensBefore: Number(raw.tokensBefore ?? 0),
|
|
252
|
+
tokensAfter: Number(raw.tokensAfter ?? 0),
|
|
253
|
+
droppedCount: Number(raw.droppedCount ?? 0),
|
|
254
|
+
...(typeof raw.summary === 'string' ? { summary: raw.summary } : {}),
|
|
255
|
+
...(raw.tailRelief ? { tailRelief: raw.tailRelief } : {}),
|
|
256
|
+
clipped: raw.clipped === true,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// A half-written file from a process that was killed mid-write, or a format
|
|
261
|
+
// from a future version. Skipping it is the only sensible reading.
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function errorText(err) {
|
|
266
|
+
return err instanceof Error ? err.message : String(err);
|
|
267
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-child context compaction (context-auto-compaction-hardening §3.4 / W3).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS REVERSES D-15. That decision said children are bounded by their
|
|
7
|
+
* dispatch timeout and turn cap, so giving them a manager only makes a dispatch's
|
|
8
|
+
* cost unpredictable. The premise is measurable and false at the configured
|
|
9
|
+
* bounds: `team.maxTurnsPerSubagent` is 24 and `team.dispatchTimeoutMs` is
|
|
10
|
+
* 900 000, which is 24 turns over fifteen minutes - the same profile as the lead
|
|
11
|
+
* run this feature exists for. The cost argument also inverts once the
|
|
12
|
+
* alternative is priced: an overflow ENDS the dispatch, the lead pays for every
|
|
13
|
+
* turn the child already ran, and receives one partial sentence for all of it.
|
|
14
|
+
* Two fast-model summarizations is the cheap side of that trade.
|
|
15
|
+
*
|
|
16
|
+
* A CHILD MANAGER IS THE SAME `Compactor` UNDER A DIFFERENT POLICY, and the
|
|
17
|
+
* overlay needs TWO mechanisms rather than one (RV-2):
|
|
18
|
+
*
|
|
19
|
+
* A. A CONFIG VIEW, for `keepRecentTurns` / `onFailure` / `useFastTier`, which
|
|
20
|
+
* `Compactor` reads through `deps.getConfig()`.
|
|
21
|
+
* B. AN INSTANCE BOUND, for `maxPerRun`, which is STRUCTURAL and has no config
|
|
22
|
+
* path by design (`limits.ts` header). Without it the child silently
|
|
23
|
+
* inherits the lead's 5 - two and a half times the bound the reversal above
|
|
24
|
+
* rests on.
|
|
25
|
+
*
|
|
26
|
+
* NO UI. A child's compaction produces no transcript card (DH-7): the lead's
|
|
27
|
+
* transcript describes the LEAD's context, and a card there would claim the
|
|
28
|
+
* lead's own history had been compacted. The team panel row and the dispatch
|
|
29
|
+
* report are where a child's internals belong.
|
|
30
|
+
*/
|
|
31
|
+
import type { AssistantMessage, ContextManager, LLMRequest, Message, ModelInfo, ModelRef, TokenUsage } from '@aragon-agent/core';
|
|
32
|
+
import type { CliConfig } from '../config/schema.js';
|
|
33
|
+
export interface ChildCompactionDeps {
|
|
34
|
+
/** Read LIVE on every call - see `childConfig` below. */
|
|
35
|
+
getConfig: () => CliConfig;
|
|
36
|
+
hasKey: (providerId: string) => boolean;
|
|
37
|
+
getApiKey: (providerId: string) => string | undefined;
|
|
38
|
+
getModelInfoFor: (ref: Pick<ModelRef, 'providerId' | 'modelId'>) => ModelInfo;
|
|
39
|
+
isPricedModel: (ref: Pick<ModelRef, 'providerId' | 'modelId'>) => boolean;
|
|
40
|
+
/** The lead wiring's own transport, reused (D-10 stays true). */
|
|
41
|
+
complete: (providerId: string, request: LLMRequest) => Promise<AssistantMessage>;
|
|
42
|
+
/** Child compaction spend joins the session totals through the lead's sink. */
|
|
43
|
+
onUsage: (usage: TokenUsage) => void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Everything one child manager needs about its child.
|
|
47
|
+
*
|
|
48
|
+
* AN OPTIONS OBJECT RATHER THAN FIVE POSITIONAL PARAMETERS - the rule
|
|
49
|
+
* `TeamRuntime.RunOneCtx` states one package over, and this signature would sit
|
|
50
|
+
* exactly on the cap.
|
|
51
|
+
*/
|
|
52
|
+
export interface ChildContextManagerRequest {
|
|
53
|
+
label: string;
|
|
54
|
+
/**
|
|
55
|
+
* The model the child actually runs on.
|
|
56
|
+
*
|
|
57
|
+
* CARRIED RATHER THAN READ FROM CONFIG, because a `tier: 'fast'` child runs on
|
|
58
|
+
* a model whose context window is usually SMALLER than the lead's. Measuring a
|
|
59
|
+
* child against the lead's window would put the trigger in the wrong place in
|
|
60
|
+
* the one direction that matters - late.
|
|
61
|
+
*/
|
|
62
|
+
model: ModelRef;
|
|
63
|
+
/**
|
|
64
|
+
* LAZY, AND IT MUST BE. `agentRef` is assigned AFTER the factory call returns,
|
|
65
|
+
* so a non-lazy read here is `undefined` at construction and the child's
|
|
66
|
+
* estimate branch would silently measure an empty history - CR-1 reproduced one
|
|
67
|
+
* module over.
|
|
68
|
+
*/
|
|
69
|
+
getMessages: () => readonly Message[];
|
|
70
|
+
getSystemPrompt: () => string;
|
|
71
|
+
onCompacted: () => void;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A child manager, plus the one hook the port does not carry.
|
|
75
|
+
*
|
|
76
|
+
* `onTurnEnd` IS NOT PART OF `ContextManager` and is called by `subagent.ts` from
|
|
77
|
+
* its existing `turn_end` case. Without it the child never records the history
|
|
78
|
+
* length its last measurement covered, so W1's delta is permanently `undefined`
|
|
79
|
+
* and the child reads the same systematically-short number the lead used to.
|
|
80
|
+
*/
|
|
81
|
+
export interface ChildContextManager extends ContextManager {
|
|
82
|
+
onTurnEnd(usage: TokenUsage, messages: readonly Message[], systemPrompt: string): void;
|
|
83
|
+
}
|
|
84
|
+
export type ChildContextManagerFactory = (req: ChildContextManagerRequest) => ChildContextManager;
|
|
85
|
+
/**
|
|
86
|
+
* The shape `subagent.ts` is handed, which may DECLINE.
|
|
87
|
+
*
|
|
88
|
+
* `undefined` RATHER THAN A THROW OR A NO-OP MANAGER, and the difference is
|
|
89
|
+
* AC-H10: the caller spreads the result conditionally, so a session with
|
|
90
|
+
* `compaction.subagents: false` builds a child whose options bag has no
|
|
91
|
+
* `contextManager` KEY AT ALL - the loop's gate then tests a genuinely absent
|
|
92
|
+
* field. The decision is re-taken per child rather than per dispatch, so a
|
|
93
|
+
* settings-screen edit mid-dispatch reaches the next one.
|
|
94
|
+
*/
|
|
95
|
+
export type ChildContextManagerProvider = (req: ChildContextManagerRequest) => ChildContextManager | undefined;
|
|
96
|
+
/** One manager per child. State is per-history and must not be shared. */
|
|
97
|
+
export declare function createChildContextManager(req: ChildContextManagerRequest, deps: ChildCompactionDeps): ChildContextManager;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-child context compaction (context-auto-compaction-hardening §3.4 / W3).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS REVERSES D-15. That decision said children are bounded by their
|
|
7
|
+
* dispatch timeout and turn cap, so giving them a manager only makes a dispatch's
|
|
8
|
+
* cost unpredictable. The premise is measurable and false at the configured
|
|
9
|
+
* bounds: `team.maxTurnsPerSubagent` is 24 and `team.dispatchTimeoutMs` is
|
|
10
|
+
* 900 000, which is 24 turns over fifteen minutes - the same profile as the lead
|
|
11
|
+
* run this feature exists for. The cost argument also inverts once the
|
|
12
|
+
* alternative is priced: an overflow ENDS the dispatch, the lead pays for every
|
|
13
|
+
* turn the child already ran, and receives one partial sentence for all of it.
|
|
14
|
+
* Two fast-model summarizations is the cheap side of that trade.
|
|
15
|
+
*
|
|
16
|
+
* A CHILD MANAGER IS THE SAME `Compactor` UNDER A DIFFERENT POLICY, and the
|
|
17
|
+
* overlay needs TWO mechanisms rather than one (RV-2):
|
|
18
|
+
*
|
|
19
|
+
* A. A CONFIG VIEW, for `keepRecentTurns` / `onFailure` / `useFastTier`, which
|
|
20
|
+
* `Compactor` reads through `deps.getConfig()`.
|
|
21
|
+
* B. AN INSTANCE BOUND, for `maxPerRun`, which is STRUCTURAL and has no config
|
|
22
|
+
* path by design (`limits.ts` header). Without it the child silently
|
|
23
|
+
* inherits the lead's 5 - two and a half times the bound the reversal above
|
|
24
|
+
* rests on.
|
|
25
|
+
*
|
|
26
|
+
* NO UI. A child's compaction produces no transcript card (DH-7): the lead's
|
|
27
|
+
* transcript describes the LEAD's context, and a card there would claim the
|
|
28
|
+
* lead's own history had been compacted. The team panel row and the dispatch
|
|
29
|
+
* report are where a child's internals belong.
|
|
30
|
+
*/
|
|
31
|
+
import { getLogger } from '../logging/logger.js';
|
|
32
|
+
import { Compactor } from './compactor.js';
|
|
33
|
+
import { COMPACTION_LIMITS } from './limits.js';
|
|
34
|
+
/** One manager per child. State is per-history and must not be shared. */
|
|
35
|
+
export function createChildContextManager(req, deps) {
|
|
36
|
+
const log = getLogger().child('compaction');
|
|
37
|
+
/**
|
|
38
|
+
* MECHANISM A - the config view.
|
|
39
|
+
*
|
|
40
|
+
* IT RE-READS `deps.getConfig()` ON EVERY CALL and never closes over one
|
|
41
|
+
* snapshot: a settings-screen edit mid-dispatch has to reach a running child,
|
|
42
|
+
* which is the same reason `subagent.ts` reuses the lead's `getApiKey` closure
|
|
43
|
+
* rather than the resolved key.
|
|
44
|
+
*
|
|
45
|
+
* `useFastTier: true` EXPRESSES A PREFERENCE, NOT AN OUTCOME. `resolveFastTier`
|
|
46
|
+
* still decides, and when nothing resolves the child falls back exactly as the
|
|
47
|
+
* lead does - to its OWN model, because `provider` / `model` are overlaid here
|
|
48
|
+
* too.
|
|
49
|
+
*
|
|
50
|
+
* `onFailure: 'truncate'` IS FORCED because a child that stops is a failed
|
|
51
|
+
* dispatch and the user is not there to intervene.
|
|
52
|
+
*/
|
|
53
|
+
const childConfig = () => {
|
|
54
|
+
const base = deps.getConfig();
|
|
55
|
+
return {
|
|
56
|
+
...base,
|
|
57
|
+
provider: req.model.providerId,
|
|
58
|
+
model: req.model.modelId,
|
|
59
|
+
...(req.model.baseUrl !== undefined ? { baseUrl: req.model.baseUrl } : {}),
|
|
60
|
+
compaction: {
|
|
61
|
+
...base.compaction,
|
|
62
|
+
keepRecentTurns: COMPACTION_LIMITS.childKeepRecentTurns,
|
|
63
|
+
onFailure: 'truncate',
|
|
64
|
+
useFastTier: true,
|
|
65
|
+
// ONE ARCHIVE PER LEAD COMPACTION IS AUDITABLE; TWENTY PER DISPATCH IS
|
|
66
|
+
// NOISE. The child's dropped slice is therefore never retained either -
|
|
67
|
+
// `runCompaction` reads this key before it stashes anything.
|
|
68
|
+
archive: false,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const compactor = new Compactor({
|
|
73
|
+
getConfig: childConfig,
|
|
74
|
+
hasKey: deps.hasKey,
|
|
75
|
+
getApiKey: deps.getApiKey,
|
|
76
|
+
getModelInfoFor: deps.getModelInfoFor,
|
|
77
|
+
isPricedModel: deps.isPricedModel,
|
|
78
|
+
getMessages: req.getMessages,
|
|
79
|
+
getSystemPrompt: req.getSystemPrompt,
|
|
80
|
+
complete: deps.complete,
|
|
81
|
+
emit: (event) => {
|
|
82
|
+
if (event.type === 'usage') {
|
|
83
|
+
deps.onUsage(event.usage);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (event.type !== 'compaction_end')
|
|
87
|
+
return;
|
|
88
|
+
// DEFENCE IN DEPTH SINCE quiet-noop D-8: a child compaction that declines
|
|
89
|
+
// before committing to work no longer emits this event at all, so this
|
|
90
|
+
// filter is now unreachable on that path. It is retained deliberately -
|
|
91
|
+
// it is this tree's own record that a no-op compaction is a non-event, and
|
|
92
|
+
// deleting it would make the child depend on that rule holding forever.
|
|
93
|
+
if (!event.record.applied && event.record.mode === 'none')
|
|
94
|
+
return;
|
|
95
|
+
req.onCompacted();
|
|
96
|
+
// W3 HAS NO CARD, so this line is the whole record of a child compaction.
|
|
97
|
+
log.info('compaction_child', {
|
|
98
|
+
label: req.label,
|
|
99
|
+
index: event.record.index,
|
|
100
|
+
tokensBefore: event.record.tokensBefore,
|
|
101
|
+
tokensAfter: event.record.tokensAfter,
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
// A CHILD HAS NO TRANSCRIPT AND NOBODY WATCHING IT. `notify` on the lead's
|
|
105
|
+
// channel would put a warning about a background worker's context in the
|
|
106
|
+
// user's transcript with no card to explain which agent it is about.
|
|
107
|
+
notify: () => { },
|
|
108
|
+
},
|
|
109
|
+
// MECHANISM B - the instance bound (RV-2 / DH-16).
|
|
110
|
+
{ maxPerRun: COMPACTION_LIMITS.childMaxPerRun });
|
|
111
|
+
return {
|
|
112
|
+
// THERE IS NO `/compact` FOR A CHILD, so nothing is ever queued and the
|
|
113
|
+
// manual branch of `shouldCompact` is unreachable by construction.
|
|
114
|
+
shouldCompact: (probe) => compactor.shouldCompact(probe),
|
|
115
|
+
compact: (ctx) => compactor.compact(ctx),
|
|
116
|
+
onTurnEnd: (usage, messages, systemPrompt) => compactor.onTurnEnd(usage, messages, systemPrompt),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/compact` slash command and its status report
|
|
3
|
+
* (context-auto-compaction §4.4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope, and this
|
|
6
|
+
* text lands in a transcript `notice` — produced outside `src/ui/**`, where no
|
|
7
|
+
* terminal capabilities are available, so it must be safe on a legacy `cmd.exe`.
|
|
8
|
+
*
|
|
9
|
+
* IT LIVES HERE RATHER THAN IN `builtins.ts`, AND THAT IS A BUDGET DECISION
|
|
10
|
+
* RATHER THAN TASTE (C-16 / P1-12). `CLAUDE.md` caps a source file at 1000 lines,
|
|
11
|
+
* `builtins.ts` is already past it, and `/compact` has eight forms plus the
|
|
12
|
+
* longest status report in the package. `builtins.ts` gets the registration and nothing
|
|
13
|
+
* else — the same split `controller.ts` argues for `FastWiring`, one layer out.
|
|
14
|
+
*/
|
|
15
|
+
import type { CommandContext } from '../commands/registry.js';
|
|
16
|
+
/**
|
|
17
|
+
* `/compact status` — THE GUARANTEED REPORTING SURFACE (§6.5 / AC-17).
|
|
18
|
+
*
|
|
19
|
+
* The chip drops on a narrow terminal, the activity row is suppressed under an
|
|
20
|
+
* overlay, and the card scrolls away. This command answers every question about
|
|
21
|
+
* the feature's state at any width, in any mode, including after the fact.
|
|
22
|
+
*
|
|
23
|
+
* IT READS THE SAME NUMBERS THE STATUS BAR DOES, through one `occupiedTokens`
|
|
24
|
+
* (§3.4.1 / R-11): a user watching 78 % on the bar who is told 91 % here would
|
|
25
|
+
* conclude one of the two is lying, and they would be right.
|
|
26
|
+
*
|
|
27
|
+
* `unknown (no price table for <model>)` RATHER THAN A CURRENCY AMOUNT when the
|
|
28
|
+
* static table has never seen the summarizer (the C-11 / RV-4 lesson from the
|
|
29
|
+
* fast tier). `$0.00` on a feature that is spending money is the same class of
|
|
30
|
+
* lie as pricing a Haiku child at Sonnet rates — and it matters MORE here,
|
|
31
|
+
* because the DEFAULT summarizer is the session's own frontier model.
|
|
32
|
+
*/
|
|
33
|
+
export declare function formatCompactionStatus(ctx: CommandContext): string;
|
|
34
|
+
/**
|
|
35
|
+
* The `/compact` handler.
|
|
36
|
+
*
|
|
37
|
+
* `/compact <free text>` is the Claude Code affordance and is therefore the
|
|
38
|
+
* FALL-THROUGH rather than a subcommand: anything that is not one of the seven
|
|
39
|
+
* verbs is instructions for the summarizer.
|
|
40
|
+
*/
|
|
41
|
+
export declare function runCompactCommand(ctx: CommandContext): Promise<void>;
|