@aragon-agent/cli 0.6.2 → 0.6.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.
Files changed (118) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/README.md +202 -0
  3. package/dist/agent/controller.d.ts +74 -1
  4. package/dist/agent/controller.js +200 -1
  5. package/dist/agent/headless.d.ts +8 -0
  6. package/dist/agent/headless.js +47 -0
  7. package/dist/agent/reducer.d.ts +120 -0
  8. package/dist/agent/reducer.js +201 -2
  9. package/dist/cli.js +224 -5
  10. package/dist/commands/builtins.js +73 -23
  11. package/dist/commands/registry.d.ts +27 -0
  12. package/dist/compaction/archive.d.ts +175 -0
  13. package/dist/compaction/archive.js +267 -0
  14. package/dist/compaction/child.d.ts +97 -0
  15. package/dist/compaction/child.js +113 -0
  16. package/dist/compaction/command.d.ts +41 -0
  17. package/dist/compaction/command.js +342 -0
  18. package/dist/compaction/compactor.d.ts +268 -0
  19. package/dist/compaction/compactor.js +689 -0
  20. package/dist/compaction/digest.d.ts +62 -0
  21. package/dist/compaction/digest.js +164 -0
  22. package/dist/compaction/limits.d.ts +165 -0
  23. package/dist/compaction/limits.js +172 -0
  24. package/dist/compaction/pressure.d.ts +135 -0
  25. package/dist/compaction/pressure.js +167 -0
  26. package/dist/compaction/summarize-call.d.ts +70 -0
  27. package/dist/compaction/summarize-call.js +86 -0
  28. package/dist/compaction/summary-prompt.d.ts +85 -0
  29. package/dist/compaction/summary-prompt.js +197 -0
  30. package/dist/compaction/tail-budget.d.ts +52 -0
  31. package/dist/compaction/tail-budget.js +67 -0
  32. package/dist/compaction/types.d.ts +179 -0
  33. package/dist/compaction/types.js +22 -0
  34. package/dist/compaction/wiring.d.ts +255 -0
  35. package/dist/compaction/wiring.js +556 -0
  36. package/dist/config/app-paths.d.ts +14 -0
  37. package/dist/config/app-paths.js +16 -0
  38. package/dist/config/cli-commands.d.ts +41 -0
  39. package/dist/config/cli-commands.js +77 -1
  40. package/dist/config/env.d.ts +4 -1
  41. package/dist/config/env.js +92 -2
  42. package/dist/config/load.d.ts +31 -0
  43. package/dist/config/load.js +63 -1
  44. package/dist/config/schema.d.ts +159 -0
  45. package/dist/config/schema.js +137 -0
  46. package/dist/config/store.js +17 -1
  47. package/dist/config/ui-state.d.ts +28 -1
  48. package/dist/config/ui-state.js +9 -0
  49. package/dist/exec/events.d.ts +50 -1
  50. package/dist/exec/runner.js +42 -0
  51. package/dist/input/mouse-events.d.ts +31 -5
  52. package/dist/input/mouse-events.js +86 -27
  53. package/dist/input/stdin-mouse-filter.d.ts +8 -3
  54. package/dist/logging/install.d.ts +51 -1
  55. package/dist/logging/install.js +28 -0
  56. package/dist/logging/logger.d.ts +1 -1
  57. package/dist/session/persist.js +19 -0
  58. package/dist/team/report.js +7 -1
  59. package/dist/team/runtime.d.ts +10 -0
  60. package/dist/team/runtime.js +7 -0
  61. package/dist/team/subagent.d.ts +31 -1
  62. package/dist/team/subagent.js +42 -15
  63. package/dist/team/types.d.ts +8 -0
  64. package/dist/ui/ActivityLine.d.ts +19 -1
  65. package/dist/ui/ActivityLine.js +10 -2
  66. package/dist/ui/App.d.ts +48 -1
  67. package/dist/ui/App.js +294 -13
  68. package/dist/ui/Composer.d.ts +13 -1
  69. package/dist/ui/Composer.js +2 -2
  70. package/dist/ui/PromptInput.d.ts +34 -1
  71. package/dist/ui/PromptInput.js +54 -3
  72. package/dist/ui/StatusBar.d.ts +40 -0
  73. package/dist/ui/StatusBar.js +13 -4
  74. package/dist/ui/TeamPanel.js +7 -1
  75. package/dist/ui/Transcript.d.ts +21 -1
  76. package/dist/ui/Transcript.js +32 -2
  77. package/dist/ui/clipboard.d.ts +52 -0
  78. package/dist/ui/clipboard.js +91 -0
  79. package/dist/ui/entries/CompactionCard.d.ts +75 -0
  80. package/dist/ui/entries/CompactionCard.js +158 -0
  81. package/dist/ui/frame-differ.d.ts +46 -0
  82. package/dist/ui/frame-differ.js +94 -10
  83. package/dist/ui/gauge.d.ts +16 -1
  84. package/dist/ui/gauge.js +25 -6
  85. package/dist/ui/glyphs.d.ts +11 -0
  86. package/dist/ui/glyphs.js +2 -0
  87. package/dist/ui/layout/ScrollViewport.d.ts +43 -6
  88. package/dist/ui/layout/ScrollViewport.js +128 -21
  89. package/dist/ui/layout/budget.d.ts +10 -3
  90. package/dist/ui/layout/budget.js +10 -3
  91. package/dist/ui/layout/follow-state.d.ts +137 -0
  92. package/dist/ui/layout/follow-state.js +150 -0
  93. package/dist/ui/layout/virtual-window.d.ts +11 -0
  94. package/dist/ui/layout/virtual-window.js +34 -0
  95. package/dist/ui/overlays/HelpOverlay.js +6 -1
  96. package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
  97. package/dist/ui/overlays/SettingsScreen.js +60 -1
  98. package/dist/ui/palettes.d.ts +16 -0
  99. package/dist/ui/palettes.js +6 -0
  100. package/dist/ui/screen.d.ts +22 -0
  101. package/dist/ui/screen.js +70 -8
  102. package/dist/ui/selection/highlight.d.ts +39 -0
  103. package/dist/ui/selection/highlight.js +64 -0
  104. package/dist/ui/selection/screen-mirror.d.ts +24 -0
  105. package/dist/ui/selection/screen-mirror.js +35 -0
  106. package/dist/ui/selection/selection-controller.d.ts +88 -0
  107. package/dist/ui/selection/selection-controller.js +282 -0
  108. package/dist/ui/selection/selection.d.ts +90 -0
  109. package/dist/ui/selection/selection.js +223 -0
  110. package/dist/ui/stdout-frame-writer.d.ts +33 -0
  111. package/dist/ui/stdout-frame-writer.js +15 -0
  112. package/dist/ui/theme.d.ts +11 -0
  113. package/dist/ui/theme.js +2 -0
  114. package/dist/ui/transcript-text.js +23 -0
  115. package/dist/ui/use-startup-notices.d.ts +27 -1
  116. package/dist/ui/use-startup-notices.js +43 -11
  117. package/dist/ui/use-wheel-routing.js +8 -0
  118. package/package.json +4 -2
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The head of the history, rendered as a bounded plain-text transcript
3
+ * (context-auto-compaction §3.6.3).
4
+ *
5
+ * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
6
+ *
7
+ * PURE. Two reasons this is a digest rather than raw messages, and the second
8
+ * one is the one that bites:
9
+ *
10
+ * - Tool results are the bulk. Four 60 KB file reads are 240 KB of body that
11
+ * says almost nothing about what was decided.
12
+ * - THE SUMMARIZER MAY HAVE A SMALLER CONTEXT WINDOW THAN THE MODEL BEING
13
+ * COMPACTED. The whole point of using a cheap model is that it is cheap, and
14
+ * cheap models are exactly the ones with 32 k windows. Sending 180 k of
15
+ * history to summarize would fail with the same error we are recovering from
16
+ * (R-4).
17
+ */
18
+ import { type Message } from '@aragon-agent/core';
19
+ /**
20
+ * Is this `user` message a previous `<compacted_context>` block?
21
+ *
22
+ * Recognized on the OPENING TAG rather than a full parse: the block carries
23
+ * attributes (`version`, `replaced`, `turns`, `anchor`, `generation`) that this
24
+ * module has no business knowing about, and a stricter match would fail on the
25
+ * first attribute anybody adds.
26
+ */
27
+ export declare function isPriorSummaryBlock(message: Message): boolean;
28
+ export interface DigestInput {
29
+ /** The head — `messages[protectedPrefix..cutIndex)`. */
30
+ head: readonly Message[];
31
+ /**
32
+ * The summarizer's own context window, or `undefined` when it is unknown.
33
+ *
34
+ * NOT the lead model's. This is the whole reason a digest exists rather than
35
+ * raw messages (R-4).
36
+ */
37
+ summarizerWindow?: number;
38
+ }
39
+ export interface DigestResult {
40
+ text: string;
41
+ /** Entries the budget forced out. `> 0` means the marker is present. */
42
+ omitted: number;
43
+ /** The budget actually used, after the window derivation. */
44
+ budgetChars: number;
45
+ }
46
+ /**
47
+ * The character budget for one digest.
48
+ *
49
+ * `min(digestMaxChars, (summarizerWindow - summarizerReserveTokens) * CHARS_PER_TOKEN)`.
50
+ * `CHARS_PER_TOKEN` comes from core (P2-3) rather than being re-spelled here: two
51
+ * chars-per-token constants that must agree and nothing checking that they do is
52
+ * how a digest budget and an occupancy estimate drift apart.
53
+ */
54
+ export declare function digestBudgetChars(summarizerWindow?: number): number;
55
+ /**
56
+ * Render the head into a bounded transcript.
57
+ *
58
+ * OLDEST ENTRIES ARE DROPPED FIRST, except the pinned `PRIOR SUMMARY`, which is
59
+ * evicted last of all. A single omission marker is prepended so the summary can
60
+ * say that it is incomplete instead of pretending.
61
+ */
62
+ export declare function buildDigest(input: DigestInput): DigestResult;
@@ -0,0 +1,164 @@
1
+ /**
2
+ * The head of the history, rendered as a bounded plain-text transcript
3
+ * (context-auto-compaction §3.6.3).
4
+ *
5
+ * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
6
+ *
7
+ * PURE. Two reasons this is a digest rather than raw messages, and the second
8
+ * one is the one that bites:
9
+ *
10
+ * - Tool results are the bulk. Four 60 KB file reads are 240 KB of body that
11
+ * says almost nothing about what was decided.
12
+ * - THE SUMMARIZER MAY HAVE A SMALLER CONTEXT WINDOW THAN THE MODEL BEING
13
+ * COMPACTED. The whole point of using a cheap model is that it is cheap, and
14
+ * cheap models are exactly the ones with 32 k windows. Sending 180 k of
15
+ * history to summarize would fail with the same error we are recovering from
16
+ * (R-4).
17
+ */
18
+ import { CHARS_PER_TOKEN } from '@aragon-agent/core';
19
+ import { BLOCK_TAG_CLOSE, BLOCK_TAG_OPEN, COMPACTION_LIMITS } from './limits.js';
20
+ /** Prepended when the budget forced entries out, so the summary can say so. */
21
+ const OMISSION_MARKER = (n) => `[... ${n} earlier ${n === 1 ? 'message' : 'messages'} omitted from this digest ...]`;
22
+ function clip(text, max) {
23
+ if (text.length <= max)
24
+ return text;
25
+ return `${text.slice(0, max)}...`;
26
+ }
27
+ /** Flatten `string | ContentPart[]` into text, naming images rather than dropping them. */
28
+ function partsToText(content) {
29
+ if (typeof content === 'string')
30
+ return content;
31
+ if (!Array.isArray(content))
32
+ return '';
33
+ return content
34
+ .map((part) => (typeof part?.text === 'string' ? part.text : '[image]'))
35
+ .join('\n');
36
+ }
37
+ /**
38
+ * Is this `user` message a previous `<compacted_context>` block?
39
+ *
40
+ * Recognized on the OPENING TAG rather than a full parse: the block carries
41
+ * attributes (`version`, `replaced`, `turns`, `anchor`, `generation`) that this
42
+ * module has no business knowing about, and a stricter match would fail on the
43
+ * first attribute anybody adds.
44
+ */
45
+ export function isPriorSummaryBlock(message) {
46
+ if (message.role !== 'user')
47
+ return false;
48
+ const text = partsToText(message.content).trimStart();
49
+ return text.startsWith(BLOCK_TAG_OPEN);
50
+ }
51
+ /** The block's body with its framing removed, for re-rendering as PRIOR SUMMARY. */
52
+ function stripBlockTags(text) {
53
+ const trimmed = text.trim();
54
+ const open = trimmed.indexOf('>');
55
+ const close = trimmed.lastIndexOf(BLOCK_TAG_CLOSE);
56
+ if (open < 0)
57
+ return trimmed;
58
+ const body = close > open ? trimmed.slice(open + 1, close) : trimmed.slice(open + 1);
59
+ return body.trim();
60
+ }
61
+ function renderMessage(message) {
62
+ if (message.role === 'user') {
63
+ if (isPriorSummaryBlock(message)) {
64
+ // CARRIED WHOLE, up to `summaryMaxChars` (D-22 / P1-9).
65
+ //
66
+ // A `<compacted_context>` block IS a `user` message (D-5, deliberately),
67
+ // so a per-kind table that clipped it to `userChars` (2 000) would discard
68
+ // up to two thirds of the previous summary - which §5.4 permits to be
69
+ // 6 000 long - BEFORE summarizing it, and then report `summarized`. Every
70
+ // compaction after the first would decay the record silently.
71
+ const body = stripBlockTags(partsToText(message.content));
72
+ return [{ text: `PRIOR SUMMARY: ${clip(body, COMPACTION_LIMITS.summaryMaxChars)}`, pinned: true }];
73
+ }
74
+ return [
75
+ { text: `USER: ${clip(partsToText(message.content), COMPACTION_LIMITS.userChars)}`, pinned: false },
76
+ ];
77
+ }
78
+ if (message.role === 'tool_result') {
79
+ const status = message.isError ? 'error' : 'ok';
80
+ const body = clip(partsToText(message.content), COMPACTION_LIMITS.toolResultChars);
81
+ return [{ text: `RESULT[${status}] ${body}`, pinned: false }];
82
+ }
83
+ // Assistant: one entry per block, so a turn's text and its calls can be
84
+ // evicted independently.
85
+ const out = [];
86
+ for (const block of message.content) {
87
+ if (block.type === 'text') {
88
+ if (block.text.trim().length === 0)
89
+ continue;
90
+ out.push({ text: `ASSISTANT: ${clip(block.text, COMPACTION_LIMITS.assistantChars)}`, pinned: false });
91
+ }
92
+ else if (block.type === 'tool_call') {
93
+ const args = clip(JSON.stringify(block.args ?? {}), COMPACTION_LIMITS.toolArgChars);
94
+ out.push({ text: `CALL ${block.toolName}(${args})`, pinned: false });
95
+ }
96
+ // `thinking` blocks are DROPPED ENTIRELY (D-8): they are the model's private
97
+ // scratch, they are the single largest contributor at `thinkingLevel: high`,
98
+ // and nothing after the turn depends on them.
99
+ }
100
+ return out;
101
+ }
102
+ /**
103
+ * The character budget for one digest.
104
+ *
105
+ * `min(digestMaxChars, (summarizerWindow - summarizerReserveTokens) * CHARS_PER_TOKEN)`.
106
+ * `CHARS_PER_TOKEN` comes from core (P2-3) rather than being re-spelled here: two
107
+ * chars-per-token constants that must agree and nothing checking that they do is
108
+ * how a digest budget and an occupancy estimate drift apart.
109
+ */
110
+ export function digestBudgetChars(summarizerWindow) {
111
+ const ceiling = COMPACTION_LIMITS.digestMaxChars;
112
+ if (!summarizerWindow || summarizerWindow <= 0)
113
+ return ceiling;
114
+ const usableTokens = summarizerWindow - COMPACTION_LIMITS.summarizerReserveTokens;
115
+ if (usableTokens <= 0)
116
+ return ceiling;
117
+ return Math.min(ceiling, usableTokens * CHARS_PER_TOKEN);
118
+ }
119
+ /**
120
+ * Render the head into a bounded transcript.
121
+ *
122
+ * OLDEST ENTRIES ARE DROPPED FIRST, except the pinned `PRIOR SUMMARY`, which is
123
+ * evicted last of all. A single omission marker is prepended so the summary can
124
+ * say that it is incomplete instead of pretending.
125
+ */
126
+ export function buildDigest(input) {
127
+ const budgetChars = digestBudgetChars(input.summarizerWindow);
128
+ const entries = input.head.flatMap(renderMessage);
129
+ const sep = '\n';
130
+ let total = entries.reduce((acc, e) => acc + e.text.length + sep.length, 0);
131
+ if (total <= budgetChars) {
132
+ return { text: entries.map((e) => e.text).join(sep), omitted: 0, budgetChars };
133
+ }
134
+ // Evict oldest-first among the unpinned. The marker itself costs characters,
135
+ // so it is charged up front against a worst-case width.
136
+ const kept = [...entries];
137
+ let omitted = 0;
138
+ let reserve = OMISSION_MARKER(entries.length).length + sep.length;
139
+ for (let i = 0; i < kept.length && total + reserve > budgetChars; i += 1) {
140
+ const entry = kept[i];
141
+ if (entry.pinned)
142
+ continue;
143
+ total -= entry.text.length + sep.length;
144
+ kept[i] = { text: '', pinned: false };
145
+ omitted += 1;
146
+ }
147
+ // Still over budget with only pinned entries left: clip the pinned text rather
148
+ // than dropping it, so the densest record survives in some form.
149
+ if (total + reserve > budgetChars) {
150
+ const room = Math.max(0, budgetChars - reserve);
151
+ const joined = kept.filter((e) => e.text.length > 0).map((e) => e.text).join(sep);
152
+ return {
153
+ text: `${OMISSION_MARKER(omitted)}${sep}${joined.slice(0, room)}`,
154
+ omitted,
155
+ budgetChars,
156
+ };
157
+ }
158
+ const body = kept.filter((e) => e.text.length > 0).map((e) => e.text).join(sep);
159
+ return {
160
+ text: omitted > 0 ? `${OMISSION_MARKER(omitted)}${sep}${body}` : body,
161
+ omitted,
162
+ budgetChars,
163
+ };
164
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * COMPACTION_LIMITS — the single authority on every bound context compaction
3
+ * enforces (context-auto-compaction §5.4).
4
+ *
5
+ * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope
6
+ * (`glyphs.test.ts::inScope`), so no literal in this tree may hold a non-ASCII
7
+ * byte. That regex is a HARDCODED DIRECTORY LIST, which is why adding this tree
8
+ * and adding `compaction` to that list are the SAME change (C-7) - a scanner
9
+ * that silently stops scanning is worse than no scanner. `fast/limits.ts`
10
+ * records the identical trap one feature earlier.
11
+ *
12
+ * TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE - the distinction
13
+ * `TEAM_LIMITS`, `TODO_LIMITS` and `FAST_LIMITS` all state:
14
+ *
15
+ * - The entries below are STRUCTURAL. They describe what a digest, a
16
+ * summarization call and a card can physically carry. A user has no business
17
+ * tuning them.
18
+ * - The six `compaction.*` keys in `config/schema.ts` are POLICY: when to fire,
19
+ * how much to keep, which model, what to do on failure. Those are clamped,
20
+ * persisted and user-facing.
21
+ */
22
+ export declare const COMPACTION_LIMITS: {
23
+ /** Ceiling on the rendered digest, before the summarizer-window check. ~30k tokens. */
24
+ readonly digestMaxChars: 120000;
25
+ /** Output + system prompt + framing, kept out of the digest's share of the window. */
26
+ readonly summarizerReserveTokens: 8000;
27
+ readonly userChars: 2000;
28
+ readonly assistantChars: 2000;
29
+ readonly toolArgChars: 200;
30
+ readonly toolResultChars: 600;
31
+ /**
32
+ * Clip bound on the TAGGED text anchor.
33
+ *
34
+ * A CLIP, NOT A SHAPE SWITCH (D-21 / P1-9). v1 of the design kept the anchor
35
+ * verbatim-standalone past this length and merged it into the block below it,
36
+ * which made the anchor rule LENGTH-DEPENDENT and therefore non-idempotent: on
37
+ * the second compaction the "first user message" is a summary. A 4 000-character
38
+ * first message clipped with a visible marker keeps the rule "one leading
39
+ * anchor, always recognizable"; a 40 000-character first message is not a goal
40
+ * statement, and truncating it with a marker is the honest outcome.
41
+ */
42
+ readonly anchorChars: 4000;
43
+ /** What the summarizer prompt asks for, and what the splice clips to. */
44
+ readonly summaryMaxChars: 6000;
45
+ readonly summaryOutputTokens: 2048;
46
+ /**
47
+ * Wall-clock budget for ONE summarization call.
48
+ *
49
+ * Longer than the fast reviewer's 20 s because the input is ~30x larger and the
50
+ * output ~4x larger, and because the cost of a timeout here is a DEGRADED RUN
51
+ * rather than a missing critique.
52
+ *
53
+ * COUPLED TO THE ENGINE'S `COMPACTION_HARD_TIMEOUT_MS` (120 s). The worst legal
54
+ * host budget is two calls at this value back to back plus the digest render,
55
+ * so `2 * callTimeoutMs` must stay strictly under the engine's ceiling. Raising
56
+ * this without raising that makes the §3.7 ladder unreachable and turns every
57
+ * slow-but-correct summarization into `manager_timeout`.
58
+ */
59
+ readonly callTimeoutMs: 45000;
60
+ /** Turns between compactions. Never two in a row: a compaction that just ran
61
+ * needs a real turn before its effect is measurable, because `lastUsage` is
62
+ * still the pre-compaction number until the next `turn_end`. */
63
+ readonly minTurnsBetween: 2;
64
+ /** Compactions STARTED per run, both triggers, hard. */
65
+ readonly maxPerRun: 5;
66
+ /** A splice must project occupancy below `threshold - this`, or it bought two turns. */
67
+ readonly minReclaimRatio: 0.15;
68
+ /** Consecutive no-progress PRESSURE compactions before the proactive trigger self-disables. */
69
+ readonly stuckLimit: 2;
70
+ /**
71
+ * Clip bound on an oversized `tool_result` body inside the RETAINED tail.
72
+ *
73
+ * 2 000 rather than `toolResultChars`' 600, and the two are not the same
74
+ * question. That one bounds a message in the DIGEST, which the summarizer
75
+ * reads once and which exists to be lossy. This one bounds a message the model
76
+ * keeps WORKING FROM: it is the last two-to-four turns, and the run is about to
77
+ * continue on them. Relief only ever runs when the alternative is a request the
78
+ * provider cannot accept.
79
+ */
80
+ readonly tailToolResultChars: 2000;
81
+ /**
82
+ * Room reserved for the `<compacted_context>` block in the tail-budget
83
+ * projection.
84
+ *
85
+ * THE BLOCK IS WRITTEN AFTER THE PROJECTION, which is why this exists at all:
86
+ * relief measures a spliced history whose summary body is already in it, but
87
+ * the `plan === null` path measures one where nothing has been written yet, and
88
+ * a budget with no allowance would let the block push the result back over.
89
+ */
90
+ readonly blockAllowanceTokens: 2000;
91
+ /**
92
+ * Files kept for ONE `runId`, newest first.
93
+ *
94
+ * PER RUN, NOT PER DIRECTORY (RV-6). `<home>/compaction/` is shared by every
95
+ * `aragon` on the machine — `file-sink.ts:94-95` already treats concurrent
96
+ * processes as normal — so a global count-based prune would delete a live
97
+ * session's archives out from under it.
98
+ */
99
+ readonly archiveMaxFiles: 20;
100
+ /**
101
+ * The cross-run sweep: 7 days.
102
+ *
103
+ * AGE RATHER THAN COUNT, for the reason above. Age can only ever delete files
104
+ * whose run finished a week ago; a count can delete the newest file another
105
+ * process wrote a second ago.
106
+ */
107
+ readonly archiveMaxAgeMs: 604800000;
108
+ /**
109
+ * Per-file ceiling before dropped message bodies are clipped from the OLDEST
110
+ * end. The metadata and the summary are never clipped: a truncated archive
111
+ * that still says what happened beats none.
112
+ */
113
+ readonly archiveMaxBytes: 8000000;
114
+ /** A child's `keepRecentTurns`, delivered through its config view. */
115
+ readonly childKeepRecentTurns: 2;
116
+ /**
117
+ * A child's guard-2 ceiling, delivered through `Compactor`'s instance bound.
118
+ *
119
+ * HERE RATHER THAN AS A `compaction.*` KEY, and the placement is the whole
120
+ * argument: `maxPerRun` is STRUCTURAL (see this file's header), there is no
121
+ * config path to it by design, and a child needs a tighter one. Both numbers
122
+ * therefore stay in the one file that owns structural bounds.
123
+ */
124
+ readonly childMaxPerRun: 2;
125
+ /** Rows the collapsed transcript card renders; Ctrl+O expands. */
126
+ readonly cardTextRows: 6;
127
+ /**
128
+ * Below this many columns the status chip drops.
129
+ *
130
+ * 100, matching all four existing peers - `retry-view.ts`, `fast/limits.ts`,
131
+ * `team/limits.ts`, `todo/limits.ts` (P2-2). A status-bar degradation ladder
132
+ * that is nearly-but-not-quite uniform is a reading hazard bought for nothing.
133
+ * The guaranteed reporting surface is `/compact status`, which is what makes
134
+ * dropping the chip acceptable at all.
135
+ */
136
+ readonly statusCompactCols: 100;
137
+ };
138
+ /**
139
+ * Bumped whenever the wording of the `<compacted_context>` block or the
140
+ * summarizer system prompt changes, so a behaviour report can be tied to a block
141
+ * revision with one grep - exactly what `FAST_BLOCK_VERSION` and
142
+ * `TEAM_BLOCK_VERSION` are for.
143
+ */
144
+ export declare const COMPACTION_BLOCK_VERSION = "v1-2026-08";
145
+ /** Opening tag of the verbatim goal anchor (§3.6.5). */
146
+ export declare const ANCHOR_TAG_OPEN = "<original_task>";
147
+ export declare const ANCHOR_TAG_CLOSE = "</original_task>";
148
+ /** Opening tag of the spliced summary block. Recognized on the NEXT compaction. */
149
+ export declare const BLOCK_TAG_OPEN = "<compacted_context";
150
+ export declare const BLOCK_TAG_CLOSE = "</compacted_context>";
151
+ /** Marker appended to an anchor clipped at `anchorChars`. ASCII (see the header). */
152
+ export declare const ANCHOR_TRUNCATION_MARK = " [truncated]";
153
+ /**
154
+ * What tail relief writes where it removed text (hardening §3.3.2 rule 4).
155
+ *
156
+ * IN THE HISTORY THE MODEL READS, at the exact place the data went missing. A
157
+ * model that sees this knows the output is partial and can re-run the tool; a
158
+ * model handed a silently truncated file cannot tell the difference between "the
159
+ * file ends here" and "the file was cut here".
160
+ *
161
+ * THE HOST OWNS IT, not core: `engine/compaction.ts` has no glyph vocabulary and
162
+ * must not learn one, so `relieveTail` takes this as a callback. ASCII, per this
163
+ * file's header.
164
+ */
165
+ export declare function tailClipMarker(removed: number): string;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * COMPACTION_LIMITS — the single authority on every bound context compaction
3
+ * enforces (context-auto-compaction §5.4).
4
+ *
5
+ * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope
6
+ * (`glyphs.test.ts::inScope`), so no literal in this tree may hold a non-ASCII
7
+ * byte. That regex is a HARDCODED DIRECTORY LIST, which is why adding this tree
8
+ * and adding `compaction` to that list are the SAME change (C-7) - a scanner
9
+ * that silently stops scanning is worse than no scanner. `fast/limits.ts`
10
+ * records the identical trap one feature earlier.
11
+ *
12
+ * TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE - the distinction
13
+ * `TEAM_LIMITS`, `TODO_LIMITS` and `FAST_LIMITS` all state:
14
+ *
15
+ * - The entries below are STRUCTURAL. They describe what a digest, a
16
+ * summarization call and a card can physically carry. A user has no business
17
+ * tuning them.
18
+ * - The six `compaction.*` keys in `config/schema.ts` are POLICY: when to fire,
19
+ * how much to keep, which model, what to do on failure. Those are clamped,
20
+ * persisted and user-facing.
21
+ */
22
+ export const COMPACTION_LIMITS = {
23
+ /** Ceiling on the rendered digest, before the summarizer-window check. ~30k tokens. */
24
+ digestMaxChars: 120_000,
25
+ /** Output + system prompt + framing, kept out of the digest's share of the window. */
26
+ summarizerReserveTokens: 8_000,
27
+ // --- Per-message clips in the digest (§3.6.3). ---
28
+ userChars: 2_000,
29
+ assistantChars: 2_000,
30
+ toolArgChars: 200,
31
+ toolResultChars: 600,
32
+ /**
33
+ * Clip bound on the TAGGED text anchor.
34
+ *
35
+ * A CLIP, NOT A SHAPE SWITCH (D-21 / P1-9). v1 of the design kept the anchor
36
+ * verbatim-standalone past this length and merged it into the block below it,
37
+ * which made the anchor rule LENGTH-DEPENDENT and therefore non-idempotent: on
38
+ * the second compaction the "first user message" is a summary. A 4 000-character
39
+ * first message clipped with a visible marker keeps the rule "one leading
40
+ * anchor, always recognizable"; a 40 000-character first message is not a goal
41
+ * statement, and truncating it with a marker is the honest outcome.
42
+ */
43
+ anchorChars: 4_000,
44
+ /** What the summarizer prompt asks for, and what the splice clips to. */
45
+ summaryMaxChars: 6_000,
46
+ summaryOutputTokens: 2_048,
47
+ /**
48
+ * Wall-clock budget for ONE summarization call.
49
+ *
50
+ * Longer than the fast reviewer's 20 s because the input is ~30x larger and the
51
+ * output ~4x larger, and because the cost of a timeout here is a DEGRADED RUN
52
+ * rather than a missing critique.
53
+ *
54
+ * COUPLED TO THE ENGINE'S `COMPACTION_HARD_TIMEOUT_MS` (120 s). The worst legal
55
+ * host budget is two calls at this value back to back plus the digest render,
56
+ * so `2 * callTimeoutMs` must stay strictly under the engine's ceiling. Raising
57
+ * this without raising that makes the §3.7 ladder unreachable and turns every
58
+ * slow-but-correct summarization into `manager_timeout`.
59
+ */
60
+ callTimeoutMs: 45_000,
61
+ // --- Anti-loop guards (§3.8). ---
62
+ /** Turns between compactions. Never two in a row: a compaction that just ran
63
+ * needs a real turn before its effect is measurable, because `lastUsage` is
64
+ * still the pre-compaction number until the next `turn_end`. */
65
+ minTurnsBetween: 2,
66
+ /** Compactions STARTED per run, both triggers, hard. */
67
+ maxPerRun: 5,
68
+ /** A splice must project occupancy below `threshold - this`, or it bought two turns. */
69
+ minReclaimRatio: 0.15,
70
+ /** Consecutive no-progress PRESSURE compactions before the proactive trigger self-disables. */
71
+ stuckLimit: 2,
72
+ // --- Tail relief (hardening §3.3 / W2). ---
73
+ /**
74
+ * Clip bound on an oversized `tool_result` body inside the RETAINED tail.
75
+ *
76
+ * 2 000 rather than `toolResultChars`' 600, and the two are not the same
77
+ * question. That one bounds a message in the DIGEST, which the summarizer
78
+ * reads once and which exists to be lossy. This one bounds a message the model
79
+ * keeps WORKING FROM: it is the last two-to-four turns, and the run is about to
80
+ * continue on them. Relief only ever runs when the alternative is a request the
81
+ * provider cannot accept.
82
+ */
83
+ tailToolResultChars: 2_000,
84
+ /**
85
+ * Room reserved for the `<compacted_context>` block in the tail-budget
86
+ * projection.
87
+ *
88
+ * THE BLOCK IS WRITTEN AFTER THE PROJECTION, which is why this exists at all:
89
+ * relief measures a spliced history whose summary body is already in it, but
90
+ * the `plan === null` path measures one where nothing has been written yet, and
91
+ * a budget with no allowance would let the block push the result back over.
92
+ */
93
+ blockAllowanceTokens: 2_000,
94
+ // --- The archive (hardening §3.5 / W4). ---
95
+ /**
96
+ * Files kept for ONE `runId`, newest first.
97
+ *
98
+ * PER RUN, NOT PER DIRECTORY (RV-6). `<home>/compaction/` is shared by every
99
+ * `aragon` on the machine — `file-sink.ts:94-95` already treats concurrent
100
+ * processes as normal — so a global count-based prune would delete a live
101
+ * session's archives out from under it.
102
+ */
103
+ archiveMaxFiles: 20,
104
+ /**
105
+ * The cross-run sweep: 7 days.
106
+ *
107
+ * AGE RATHER THAN COUNT, for the reason above. Age can only ever delete files
108
+ * whose run finished a week ago; a count can delete the newest file another
109
+ * process wrote a second ago.
110
+ */
111
+ archiveMaxAgeMs: 604_800_000,
112
+ /**
113
+ * Per-file ceiling before dropped message bodies are clipped from the OLDEST
114
+ * end. The metadata and the summary are never clipped: a truncated archive
115
+ * that still says what happened beats none.
116
+ */
117
+ archiveMaxBytes: 8_000_000,
118
+ // --- Sub-agent compaction (hardening §3.4 / W3). ---
119
+ /** A child's `keepRecentTurns`, delivered through its config view. */
120
+ childKeepRecentTurns: 2,
121
+ /**
122
+ * A child's guard-2 ceiling, delivered through `Compactor`'s instance bound.
123
+ *
124
+ * HERE RATHER THAN AS A `compaction.*` KEY, and the placement is the whole
125
+ * argument: `maxPerRun` is STRUCTURAL (see this file's header), there is no
126
+ * config path to it by design, and a child needs a tighter one. Both numbers
127
+ * therefore stay in the one file that owns structural bounds.
128
+ */
129
+ childMaxPerRun: 2,
130
+ /** Rows the collapsed transcript card renders; Ctrl+O expands. */
131
+ cardTextRows: 6,
132
+ /**
133
+ * Below this many columns the status chip drops.
134
+ *
135
+ * 100, matching all four existing peers - `retry-view.ts`, `fast/limits.ts`,
136
+ * `team/limits.ts`, `todo/limits.ts` (P2-2). A status-bar degradation ladder
137
+ * that is nearly-but-not-quite uniform is a reading hazard bought for nothing.
138
+ * The guaranteed reporting surface is `/compact status`, which is what makes
139
+ * dropping the chip acceptable at all.
140
+ */
141
+ statusCompactCols: 100,
142
+ };
143
+ /**
144
+ * Bumped whenever the wording of the `<compacted_context>` block or the
145
+ * summarizer system prompt changes, so a behaviour report can be tied to a block
146
+ * revision with one grep - exactly what `FAST_BLOCK_VERSION` and
147
+ * `TEAM_BLOCK_VERSION` are for.
148
+ */
149
+ export const COMPACTION_BLOCK_VERSION = 'v1-2026-08';
150
+ /** Opening tag of the verbatim goal anchor (§3.6.5). */
151
+ export const ANCHOR_TAG_OPEN = '<original_task>';
152
+ export const ANCHOR_TAG_CLOSE = '</original_task>';
153
+ /** Opening tag of the spliced summary block. Recognized on the NEXT compaction. */
154
+ export const BLOCK_TAG_OPEN = '<compacted_context';
155
+ export const BLOCK_TAG_CLOSE = '</compacted_context>';
156
+ /** Marker appended to an anchor clipped at `anchorChars`. ASCII (see the header). */
157
+ export const ANCHOR_TRUNCATION_MARK = ' [truncated]';
158
+ /**
159
+ * What tail relief writes where it removed text (hardening §3.3.2 rule 4).
160
+ *
161
+ * IN THE HISTORY THE MODEL READS, at the exact place the data went missing. A
162
+ * model that sees this knows the output is partial and can re-run the tool; a
163
+ * model handed a silently truncated file cannot tell the difference between "the
164
+ * file ends here" and "the file was cut here".
165
+ *
166
+ * THE HOST OWNS IT, not core: `engine/compaction.ts` has no glyph vocabulary and
167
+ * must not learn one, so `relieveTail` takes this as a callback. ASCII, per this
168
+ * file's header.
169
+ */
170
+ export function tailClipMarker(removed) {
171
+ return `\n[... ${removed} characters removed by context compaction ...]`;
172
+ }