@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.
Files changed (167) hide show
  1. package/CHANGELOG.md +321 -0
  2. package/README.md +297 -2
  3. package/dist/agent/controller.d.ts +193 -1
  4. package/dist/agent/controller.js +475 -2
  5. package/dist/agent/headless.d.ts +8 -0
  6. package/dist/agent/headless.js +47 -0
  7. package/dist/agent/reducer.d.ts +189 -0
  8. package/dist/agent/reducer.js +301 -2
  9. package/dist/agent/system-prompt.d.ts +14 -0
  10. package/dist/agent/system-prompt.js +2 -0
  11. package/dist/cli.js +297 -16
  12. package/dist/commands/builtins.js +165 -24
  13. package/dist/commands/registry.d.ts +27 -0
  14. package/dist/compaction/archive.d.ts +175 -0
  15. package/dist/compaction/archive.js +267 -0
  16. package/dist/compaction/child.d.ts +97 -0
  17. package/dist/compaction/child.js +118 -0
  18. package/dist/compaction/command.d.ts +41 -0
  19. package/dist/compaction/command.js +355 -0
  20. package/dist/compaction/compactor.d.ts +302 -0
  21. package/dist/compaction/compactor.js +810 -0
  22. package/dist/compaction/digest.d.ts +62 -0
  23. package/dist/compaction/digest.js +164 -0
  24. package/dist/compaction/limits.d.ts +165 -0
  25. package/dist/compaction/limits.js +172 -0
  26. package/dist/compaction/pressure.d.ts +135 -0
  27. package/dist/compaction/pressure.js +167 -0
  28. package/dist/compaction/summarize-call.d.ts +70 -0
  29. package/dist/compaction/summarize-call.js +86 -0
  30. package/dist/compaction/summary-prompt.d.ts +85 -0
  31. package/dist/compaction/summary-prompt.js +197 -0
  32. package/dist/compaction/tail-budget.d.ts +52 -0
  33. package/dist/compaction/tail-budget.js +67 -0
  34. package/dist/compaction/types.d.ts +215 -0
  35. package/dist/compaction/types.js +22 -0
  36. package/dist/compaction/wiring.d.ts +264 -0
  37. package/dist/compaction/wiring.js +577 -0
  38. package/dist/config/app-paths.d.ts +14 -0
  39. package/dist/config/app-paths.js +16 -0
  40. package/dist/config/cli-commands.d.ts +41 -0
  41. package/dist/config/cli-commands.js +77 -1
  42. package/dist/config/env.d.ts +5 -2
  43. package/dist/config/env.js +140 -3
  44. package/dist/config/load.d.ts +42 -0
  45. package/dist/config/load.js +101 -1
  46. package/dist/config/schema.d.ts +244 -0
  47. package/dist/config/schema.js +170 -0
  48. package/dist/config/store.js +37 -1
  49. package/dist/config/ui-state.d.ts +28 -1
  50. package/dist/config/ui-state.js +9 -0
  51. package/dist/exec/events.d.ts +50 -1
  52. package/dist/exec/runner.js +42 -0
  53. package/dist/input/limits.d.ts +72 -0
  54. package/dist/input/limits.js +63 -0
  55. package/dist/input/mouse-events.d.ts +31 -5
  56. package/dist/input/mouse-events.js +86 -27
  57. package/dist/input/paste-parse.d.ts +71 -0
  58. package/dist/input/paste-parse.js +95 -0
  59. package/dist/input/stdin-filter.d.ts +81 -0
  60. package/dist/input/stdin-filter.js +379 -0
  61. package/dist/logging/install.d.ts +53 -1
  62. package/dist/logging/install.js +81 -1
  63. package/dist/logging/logger.d.ts +1 -1
  64. package/dist/proc/classify.d.ts +52 -0
  65. package/dist/proc/classify.js +143 -0
  66. package/dist/proc/kill-tree.d.ts +42 -0
  67. package/dist/proc/kill-tree.js +119 -0
  68. package/dist/proc/limits.d.ts +84 -0
  69. package/dist/proc/limits.js +84 -0
  70. package/dist/proc/log-ring.d.ts +68 -0
  71. package/dist/proc/log-ring.js +102 -0
  72. package/dist/proc/prompt.d.ts +24 -0
  73. package/dist/proc/prompt.js +49 -0
  74. package/dist/proc/readiness.d.ts +102 -0
  75. package/dist/proc/readiness.js +219 -0
  76. package/dist/proc/supervisor.d.ts +103 -0
  77. package/dist/proc/supervisor.js +465 -0
  78. package/dist/proc/types.d.ts +175 -0
  79. package/dist/proc/types.js +15 -0
  80. package/dist/session/persist.js +55 -1
  81. package/dist/team/report.js +7 -1
  82. package/dist/team/runtime.d.ts +10 -0
  83. package/dist/team/runtime.js +7 -0
  84. package/dist/team/subagent.d.ts +31 -1
  85. package/dist/team/subagent.js +42 -15
  86. package/dist/team/types.d.ts +8 -0
  87. package/dist/tools/bash-tool.d.ts +51 -2
  88. package/dist/tools/bash-tool.js +321 -51
  89. package/dist/tools/fs-tools.d.ts +17 -0
  90. package/dist/tools/index.d.ts +55 -2
  91. package/dist/tools/index.js +39 -1
  92. package/dist/tools/proc-tools.d.ts +25 -0
  93. package/dist/tools/proc-tools.js +102 -0
  94. package/dist/ui/ActivityLine.d.ts +19 -1
  95. package/dist/ui/ActivityLine.js +10 -2
  96. package/dist/ui/App.d.ts +62 -2
  97. package/dist/ui/App.js +546 -16
  98. package/dist/ui/Composer.d.ts +66 -1
  99. package/dist/ui/Composer.js +50 -6
  100. package/dist/ui/PromptInput.d.ts +82 -3
  101. package/dist/ui/PromptInput.js +232 -49
  102. package/dist/ui/StatusBar.d.ts +59 -0
  103. package/dist/ui/StatusBar.js +16 -4
  104. package/dist/ui/TeamPanel.js +7 -1
  105. package/dist/ui/Transcript.d.ts +21 -1
  106. package/dist/ui/Transcript.js +61 -3
  107. package/dist/ui/clipboard.d.ts +52 -0
  108. package/dist/ui/clipboard.js +91 -0
  109. package/dist/ui/composer-limits.d.ts +31 -0
  110. package/dist/ui/composer-limits.js +50 -0
  111. package/dist/ui/composer-rows.d.ts +73 -0
  112. package/dist/ui/composer-rows.js +201 -0
  113. package/dist/ui/editor-reducer.d.ts +37 -0
  114. package/dist/ui/editor-reducer.js +88 -12
  115. package/dist/ui/entries/CompactionCard.d.ts +75 -0
  116. package/dist/ui/entries/CompactionCard.js +158 -0
  117. package/dist/ui/entries/ServiceCard.d.ts +66 -0
  118. package/dist/ui/entries/ServiceCard.js +116 -0
  119. package/dist/ui/entries/UserEntry.d.ts +17 -3
  120. package/dist/ui/entries/UserEntry.js +34 -5
  121. package/dist/ui/frame-differ.d.ts +46 -0
  122. package/dist/ui/frame-differ.js +94 -10
  123. package/dist/ui/gauge.d.ts +16 -1
  124. package/dist/ui/gauge.js +25 -6
  125. package/dist/ui/glyphs.d.ts +11 -0
  126. package/dist/ui/glyphs.js +2 -0
  127. package/dist/ui/layout/ScrollViewport.d.ts +43 -6
  128. package/dist/ui/layout/ScrollViewport.js +128 -21
  129. package/dist/ui/layout/budget.d.ts +44 -9
  130. package/dist/ui/layout/budget.js +42 -10
  131. package/dist/ui/layout/follow-state.d.ts +137 -0
  132. package/dist/ui/layout/follow-state.js +150 -0
  133. package/dist/ui/layout/virtual-window.d.ts +11 -0
  134. package/dist/ui/layout/virtual-window.js +85 -1
  135. package/dist/ui/overlays/HelpOverlay.js +9 -1
  136. package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
  137. package/dist/ui/overlays/QuestionOverlay.js +5 -1
  138. package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
  139. package/dist/ui/overlays/SettingsScreen.js +74 -2
  140. package/dist/ui/palettes.d.ts +16 -0
  141. package/dist/ui/palettes.js +6 -0
  142. package/dist/ui/paste-frames.d.ts +87 -0
  143. package/dist/ui/paste-frames.js +112 -0
  144. package/dist/ui/paste-tokens.d.ts +90 -0
  145. package/dist/ui/paste-tokens.js +147 -0
  146. package/dist/ui/screen.d.ts +38 -0
  147. package/dist/ui/screen.js +113 -8
  148. package/dist/ui/selection/highlight.d.ts +39 -0
  149. package/dist/ui/selection/highlight.js +64 -0
  150. package/dist/ui/selection/screen-mirror.d.ts +24 -0
  151. package/dist/ui/selection/screen-mirror.js +35 -0
  152. package/dist/ui/selection/selection-controller.d.ts +88 -0
  153. package/dist/ui/selection/selection-controller.js +282 -0
  154. package/dist/ui/selection/selection.d.ts +90 -0
  155. package/dist/ui/selection/selection.js +223 -0
  156. package/dist/ui/stdout-frame-writer.d.ts +33 -0
  157. package/dist/ui/stdout-frame-writer.js +15 -0
  158. package/dist/ui/theme.d.ts +11 -0
  159. package/dist/ui/theme.js +2 -0
  160. package/dist/ui/transcript-text.js +72 -0
  161. package/dist/ui/use-startup-notices.d.ts +27 -1
  162. package/dist/ui/use-startup-notices.js +43 -11
  163. package/dist/ui/use-wheel-routing.d.ts +1 -1
  164. package/dist/ui/use-wheel-routing.js +8 -0
  165. package/package.json +4 -2
  166. package/dist/input/stdin-mouse-filter.d.ts +0 -42
  167. 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>;