@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,197 @@
1
+ /**
2
+ * The summarizer's system prompt and the `<compacted_context>` splice
3
+ * (context-auto-compaction §3.6.4 / §3.6.5).
4
+ *
5
+ * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
6
+ *
7
+ * PURE. Every input arrives as an argument, so the block a session produced can
8
+ * be reconstructed from a bug report with one call.
9
+ */
10
+ import { ANCHOR_TAG_CLOSE, ANCHOR_TAG_OPEN, ANCHOR_TRUNCATION_MARK, BLOCK_TAG_CLOSE, BLOCK_TAG_OPEN, COMPACTION_BLOCK_VERSION, COMPACTION_LIMITS, } from './limits.js';
11
+ import { isPriorSummaryBlock } from './digest.js';
12
+ /**
13
+ * The section skeleton the summary must fill.
14
+ *
15
+ * THE HEADINGS ARE PART OF THE CONTRACT, because the next model to read them is
16
+ * an LLM: a summary with predictable sections can be scanned for "what is still
17
+ * open" without re-reading the whole thing.
18
+ */
19
+ export function buildSummarySystemPrompt(opts) {
20
+ const lines = [
21
+ 'You are compacting a software-engineering conversation so the agent can keep working',
22
+ 'after older messages are dropped from its context window.',
23
+ '',
24
+ 'Write a record of the conversation below using EXACTLY these sections, in this order:',
25
+ '',
26
+ '## Task',
27
+ "The user's original goal, and any revision to it.",
28
+ '## Decisions',
29
+ 'What was decided and why. One line each.',
30
+ '## Files',
31
+ 'path -> what changed in it, or what is in it that matters.',
32
+ '## Facts',
33
+ 'Commands, paths, APIs, identifiers and values discovered. Verbatim where exactness matters.',
34
+ '## Open',
35
+ 'What remains to be done, in the order it should be done.',
36
+ '## Pitfalls',
37
+ 'What was tried and did not work, so it is not tried again.',
38
+ '',
39
+ 'Rules:',
40
+ `- Stay under ${opts.maxChars} characters in total.`,
41
+ '- Prefer verbatim identifiers, paths and commands over prose describing them.',
42
+ '- NEVER invent a fact the transcript does not contain. An empty section is correct',
43
+ ' when the transcript says nothing about it; write "(none)" under it.',
44
+ '- Write in the language of the conversation.',
45
+ '- Output the sections and nothing else. No preamble, no sign-off.',
46
+ ];
47
+ if (opts.hasPriorSummary) {
48
+ // WITHOUT THIS CLAUSE the second compaction is a model deciding, with no
49
+ // instruction either way, how much of a summary a summary needs - which is
50
+ // the mechanism by which flattened summaries decay into nothing over a long
51
+ // run (D-22 / R-14).
52
+ lines.push('', 'The transcript below OPENS WITH A `PRIOR SUMMARY` SECTION. That section is an', 'authoritative record of everything that came before it, produced by an earlier', 'compaction of this same conversation. MERGE IT AND CARRY IT FORWARD - do not', 're-condense it. Facts, file paths and pitfalls already recorded there are to be', 'reproduced, not paraphrased away, and anything it marks as incomplete stays marked.');
53
+ }
54
+ if (opts.instructions && opts.instructions.trim().length > 0) {
55
+ lines.push('', '## Focus', 'The user asked you to pay particular attention to the following. Keep everything', 'the sections above require, and be more detailed where it is relevant:', opts.instructions.trim());
56
+ }
57
+ return lines.join('\n');
58
+ }
59
+ /** The user message the summarization call sends: the digest, and nothing else. */
60
+ export function buildSummaryUserMessage(digest, now) {
61
+ return { role: 'user', content: digest, timestamp: now };
62
+ }
63
+ function contentIsParts(content) {
64
+ return Array.isArray(content);
65
+ }
66
+ function firstText(content) {
67
+ if (typeof content === 'string')
68
+ return content;
69
+ return content.map((p) => (p.type === 'text' ? p.text : '[image]')).join('\n');
70
+ }
71
+ /**
72
+ * Recognize an anchor this feature produced on a previous pass.
73
+ *
74
+ * STRUCTURAL, NOT LENGTH-DEPENDENT (D-21 / P1-9). v1 of the design identified
75
+ * the anchor as "the first `user` message of the conversation" and chose between
76
+ * two shapes by length. Neither rule composes across repeated compactions,
77
+ * because after the first pass the first user message IS the compacted block:
78
+ * the merged shape promotes a SUMMARY to "the user's original task", and the
79
+ * standalone shape keeps the block as an anchor AND re-digests it. Neither
80
+ * failure raises anything, and both get worse every pass.
81
+ */
82
+ export function isTaggedAnchor(message) {
83
+ if (message.role !== 'user')
84
+ return false;
85
+ return firstText(message.content).trimStart().startsWith(ANCHOR_TAG_OPEN);
86
+ }
87
+ /**
88
+ * Build (or carry through) the leading anchor.
89
+ *
90
+ * ALWAYS ITS OWN LEADING MESSAGE, AND ALWAYS STRUCTURALLY IDENTIFIABLE. A
91
+ * summarizer that is 90 % faithful is fine for the middle of a conversation and
92
+ * catastrophic for the sentence that says what the user wanted (D-6).
93
+ */
94
+ export function buildAnchor(messages) {
95
+ const first = messages.find((m) => m.role === 'user');
96
+ if (!first)
97
+ return null;
98
+ // Already ours: carry it through byte-for-byte. This is what makes the rule
99
+ // idempotent across an unbounded number of compactions.
100
+ if (isTaggedAnchor(first))
101
+ return { message: first, shape: 'tagged' };
102
+ // A message carrying `ContentPart[]` may depend on an IMAGE, which no tag can
103
+ // wrap and no clip can preserve. It is kept byte-for-byte verbatim, and the
104
+ // block records `anchor="verbatim"` so the next pass recognizes it by position
105
+ // rather than by a tag it cannot carry.
106
+ if (contentIsParts(first.content)) {
107
+ return { message: first, shape: 'verbatim' };
108
+ }
109
+ const raw = first.content;
110
+ const clipped = raw.length > COMPACTION_LIMITS.anchorChars
111
+ ? `${raw.slice(0, COMPACTION_LIMITS.anchorChars)}${ANCHOR_TRUNCATION_MARK}`
112
+ : raw;
113
+ const message = {
114
+ role: 'user',
115
+ content: `${ANCHOR_TAG_OPEN}\n${clipped}\n${ANCHOR_TAG_CLOSE}`,
116
+ ...(first.timestamp !== undefined ? { timestamp: first.timestamp } : {}),
117
+ };
118
+ return { message, shape: 'tagged' };
119
+ }
120
+ /**
121
+ * How many leading messages are ALREADY structurally preserved, and must be
122
+ * carried through untouched rather than digested.
123
+ *
124
+ * This is `planCompaction`'s `protectedPrefix` (§3.6.2). The HOST computes it
125
+ * because it is the host that knows what an `<original_task>` message is; core
126
+ * stays free of any knowledge of the block format.
127
+ */
128
+ export function countProtectedPrefix(messages) {
129
+ const first = messages[0];
130
+ if (!first)
131
+ return 0;
132
+ // Case 1 — a TAGGED anchor identifies itself. The block, if any, is next.
133
+ if (isTaggedAnchor(first)) {
134
+ return messages[1] && isPriorSummaryBlock(messages[1]) ? 2 : 1;
135
+ }
136
+ // Case 2 — a VERBATIM anchor is not self-identifying (no tag can wrap the image
137
+ // it exists to preserve), so it is recognized BY POSITION: a `user` message at
138
+ // index 0 followed IMMEDIATELY by our block. `messages[1]`, never `messages[n]`
139
+ // — reading the slot the anchor itself occupies is an off-by-one whose only
140
+ // symptom is that a verbatim-anchor session re-digests its own anchor and block
141
+ // on every subsequent compaction, which is the exact P1-9 decay this function
142
+ // exists to prevent, arriving through the back door.
143
+ if (first.role === 'user' && messages[1] && isPriorSummaryBlock(messages[1]))
144
+ return 2;
145
+ // Case 3 — no anchor of ours, but a block at the head anyway (a history hand-
146
+ // edited, or restored from a file this feature did not write).
147
+ if (isPriorSummaryBlock(first))
148
+ return 1;
149
+ return 0;
150
+ }
151
+ /** Read `generation="N"` off a prior block, so the next one can say `N + 1`. */
152
+ export function readGeneration(messages) {
153
+ for (const message of messages) {
154
+ if (!isPriorSummaryBlock(message))
155
+ continue;
156
+ const text = firstText(message.content);
157
+ const match = /generation="(\d+)"/.exec(text);
158
+ if (match)
159
+ return Number.parseInt(match[1], 10);
160
+ return 1;
161
+ }
162
+ return 0;
163
+ }
164
+ /** The body used when the ladder ran out and there is no summary (§3.7 rung 3). */
165
+ export function truncationBody(replaced) {
166
+ return (`${replaced} earlier ${replaced === 1 ? 'message was' : 'messages were'} dropped to free ` +
167
+ 'context. Their content is not available. Do not assume anything about what they ' +
168
+ 'contained; ask the user if you need it.');
169
+ }
170
+ /**
171
+ * The spliced block.
172
+ *
173
+ * A `user` MESSAGE, NEVER A FABRICATED ASSISTANT ONE (D-5). An assistant message
174
+ * claiming the model said something it did not is a lie the model will then act
175
+ * on, and it corrupts every subsequent turn. The XML tag is the same disclosure
176
+ * device `<fast_review>` and `<skill>` already use in this codebase, and the
177
+ * instruction line is what stops the model reading the block as the user ASKING
178
+ * for a summary.
179
+ */
180
+ export function buildCompactedBlock(input) {
181
+ const body = input.summary.slice(0, COMPACTION_LIMITS.summaryMaxChars);
182
+ const header = `${BLOCK_TAG_OPEN} version="${COMPACTION_BLOCK_VERSION}" replaced="${input.replaced}" ` +
183
+ `turns="${input.turns}" anchor="${input.anchor}" generation="${input.generation}">`;
184
+ const intro = [
185
+ 'The conversation above this point was compacted to free context. The following',
186
+ 'is an accurate record of it. Continue the work from here; do not re-derive this',
187
+ 'history, do not ask the user to repeat it, and do not apologise for the gap.',
188
+ ];
189
+ if (input.truncated) {
190
+ intro.push('', 'THE RECORD IS INCOMPLETE: summarization failed, so the dropped messages were not', 'summarized at all. Treat anything not stated below as unknown rather than absent.');
191
+ }
192
+ return {
193
+ role: 'user',
194
+ content: `${header}\n${intro.join('\n')}\n\n${body}\n${BLOCK_TAG_CLOSE}`,
195
+ timestamp: Date.now(),
196
+ };
197
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The tail-relief projection - what a retained tail may occupy, and whether it
3
+ * has to be clipped (context-auto-compaction-hardening §3.3.3 / W2).
4
+ *
5
+ * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
6
+ *
7
+ * ITS OWN MODULE BY A PRE-MADE DECISION, not an improvised split. The design's
8
+ * §7 records it in advance: "If `compactor.ts` approaches the guideline during
9
+ * implementation, the tail-relief projection block moves to
10
+ * `compaction/tail-budget.ts` - noted here so the decision is not improvised."
11
+ * It did (`CLAUDE.md` caps a source file at 1 000 lines), so it did.
12
+ *
13
+ * PURE APART FROM THE LOG SINK. Everything it needs arrives as an argument,
14
+ * which is what lets the projection be tested without standing up a `Compactor`.
15
+ */
16
+ import { type Message, type TailReliefResult } from '@aragon-agent/core';
17
+ /**
18
+ * What the whole history may occupy and still leave room for one reply.
19
+ *
20
+ * THE BLOCK ALLOWANCE IS NOT PADDING. On the `plan === null` path the
21
+ * `<compacted_context>` block has not been written when this is measured, so a
22
+ * budget with no room reserved for it would let the block push the relieved
23
+ * history straight back over.
24
+ */
25
+ export declare function tailBudgetTokens(input: {
26
+ contextWindow: number;
27
+ maxOutputTokens: number;
28
+ }): number;
29
+ export interface TailReliefAttempt {
30
+ messages: readonly Message[];
31
+ /** Index the tail starts at; nothing before it is eligible. */
32
+ from: number;
33
+ systemPrompt: string;
34
+ budget: number;
35
+ /** W2 fires rarely; when it does it is the most interesting event in the session. */
36
+ log: (event: string, fields: Record<string, unknown>) => void;
37
+ }
38
+ /**
39
+ * Relieve the tail when - and ONLY when - the history is over budget.
40
+ * `null` means "it fits, or nothing was eligible".
41
+ *
42
+ * THE PROJECTION IS THE GATE. Relief is a real, bounded, announced data loss
43
+ * inside the turns the run is about to continue from, so it must never become a
44
+ * routine tidy-up: it runs only when the alternative is a request the provider
45
+ * cannot accept.
46
+ */
47
+ export declare function attemptTailRelief(attempt: TailReliefAttempt): TailReliefResult | null;
48
+ /** The shape `CompactionRecord.tailRelief` and the archive both carry. */
49
+ export declare function reliefSummary(relief: TailReliefResult): {
50
+ messages: number;
51
+ charsRemoved: number;
52
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The tail-relief projection - what a retained tail may occupy, and whether it
3
+ * has to be clipped (context-auto-compaction-hardening §3.3.3 / W2).
4
+ *
5
+ * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
6
+ *
7
+ * ITS OWN MODULE BY A PRE-MADE DECISION, not an improvised split. The design's
8
+ * §7 records it in advance: "If `compactor.ts` approaches the guideline during
9
+ * implementation, the tail-relief projection block moves to
10
+ * `compaction/tail-budget.ts` - noted here so the decision is not improvised."
11
+ * It did (`CLAUDE.md` caps a source file at 1 000 lines), so it did.
12
+ *
13
+ * PURE APART FROM THE LOG SINK. Everything it needs arrives as an argument,
14
+ * which is what lets the projection be tested without standing up a `Compactor`.
15
+ */
16
+ import { estimatePromptTokens, relieveTail, } from '@aragon-agent/core';
17
+ import { COMPACTION_LIMITS, tailClipMarker } from './limits.js';
18
+ import { requiredHeadroom } from './pressure.js';
19
+ /**
20
+ * What the whole history may occupy and still leave room for one reply.
21
+ *
22
+ * THE BLOCK ALLOWANCE IS NOT PADDING. On the `plan === null` path the
23
+ * `<compacted_context>` block has not been written when this is measured, so a
24
+ * budget with no room reserved for it would let the block push the relieved
25
+ * history straight back over.
26
+ */
27
+ export function tailBudgetTokens(input) {
28
+ return Math.max(0, input.contextWindow -
29
+ requiredHeadroom({ maxOutputTokens: input.maxOutputTokens }) -
30
+ COMPACTION_LIMITS.blockAllowanceTokens);
31
+ }
32
+ /**
33
+ * Relieve the tail when - and ONLY when - the history is over budget.
34
+ * `null` means "it fits, or nothing was eligible".
35
+ *
36
+ * THE PROJECTION IS THE GATE. Relief is a real, bounded, announced data loss
37
+ * inside the turns the run is about to continue from, so it must never become a
38
+ * routine tidy-up: it runs only when the alternative is a request the provider
39
+ * cannot accept.
40
+ */
41
+ export function attemptTailRelief(attempt) {
42
+ if (attempt.budget <= 0)
43
+ return null;
44
+ const projected = estimatePromptTokens(attempt.messages, attempt.systemPrompt);
45
+ if (projected <= attempt.budget)
46
+ return null;
47
+ const relief = relieveTail(attempt.messages, {
48
+ from: attempt.from,
49
+ clipChars: COMPACTION_LIMITS.tailToolResultChars,
50
+ targetTokens: attempt.budget,
51
+ systemPrompt: attempt.systemPrompt,
52
+ marker: tailClipMarker,
53
+ });
54
+ if (relief.clippedMessages === 0)
55
+ return null;
56
+ attempt.log('compaction_tail_relief', {
57
+ clippedMessages: relief.clippedMessages,
58
+ charsRemoved: relief.charsRemoved,
59
+ projectedBefore: projected,
60
+ budget: attempt.budget,
61
+ });
62
+ return relief;
63
+ }
64
+ /** The shape `CompactionRecord.tailRelief` and the archive both carry. */
65
+ export function reliefSummary(relief) {
66
+ return { messages: relief.clippedMessages, charsRemoved: relief.charsRemoved };
67
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * CLI-local compaction shapes (context-auto-compaction §5.2 / §3.4.2).
3
+ *
4
+ * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
5
+ *
6
+ * A CLI-LOCAL EVENT STREAM, like `FastEvent` / `TeamEvent` / `TodoEvent`, for
7
+ * the reason each of those records: core's `AgentEvent` union is a public
8
+ * contract and must not learn host shapes.
9
+ *
10
+ * NOTE THE ASYMMETRY WITH CORE. Core emits the two ENGINE events because the
11
+ * engine is what performs the splice and decides `applied`; the CLI emits the
12
+ * richer ones because only the CLI knows the model name, the price and the
13
+ * session totals.
14
+ *
15
+ * THIS STREAM IS THE SOLE SOURCE OF REDUCER ACTIONS (D-20 / P1-6), and
16
+ * `reduceEvent` gains NO cases for `compaction_start` / `compaction_end`. One
17
+ * compaction arriving through two channels is two transcript cards and a doubled
18
+ * `tokensReclaimed`; `reduceEvent`'s `default: return []` is already exactly
19
+ * right for the two core events, which makes "one authority" also the smallest
20
+ * possible change.
21
+ */
22
+ import type { TokenUsage } from '@aragon-agent/core';
23
+ /**
24
+ * `relieved` MEANS THE TAIL WAS CLIPPED AND NOTHING WAS DROPPED (hardening
25
+ * RV-5) — the `plan === null` recovery of §3.3.3.
26
+ *
27
+ * IT EXISTS ONLY HERE. The port still reports `'truncated'` for it (DH-5),
28
+ * because the engine's question is narrower: "was this history reduced without a
29
+ * summary", and the answer is yes.
30
+ *
31
+ * WITHOUT IT THE CARD LIES. `detailLine` renders
32
+ * `summarized ${before - after} messages with ${model}` for `'summarized'`, and
33
+ * relief drops NO messages — so a compaction in which no model was called would
34
+ * render "summarized 0 messages with claude-haiku-4-5", in the rare and alarming
35
+ * case the user most needs to read correctly.
36
+ *
37
+ * ANY NEW `switch` ON THIS UNION MUST PAIR ITS BRANCHES. TypeScript finds the
38
+ * exhaustive switches; it does not find an `else` that meant `'summarized'`.
39
+ */
40
+ export type CompactionMode = 'summarized' | 'truncated' | 'relieved' | 'none';
41
+ /**
42
+ * `manual` exists ONLY here, never in core.
43
+ *
44
+ * The engine's `CompactionTrigger` is `pressure | overflow`, because those are
45
+ * the two the LOOP can originate. A `/compact` while running is queued and then
46
+ * arrives at the loop as an ordinary `pressure` checkpoint, so the distinction
47
+ * is a host-side fact about WHY the flag was set - which is exactly the kind of
48
+ * thing a CLI-local type is for.
49
+ */
50
+ export type CompactionUiTrigger = 'pressure' | 'overflow' | 'manual';
51
+ /** Whether a figure was measured by the provider or estimated by us. */
52
+ export type PressureSource = 'usage' | 'estimate';
53
+ export interface Pressure {
54
+ occupied: number;
55
+ contextWindow: number;
56
+ /** `occupied / contextWindow`, clamped to [0, 1]. */
57
+ ratio: number;
58
+ /** `contextWindow - occupied`, floored at 0. */
59
+ headroom: number;
60
+ source: PressureSource;
61
+ /** False when the window came from `buildRuntimeModel`'s 128k placeholder. */
62
+ windowKnown: boolean;
63
+ /**
64
+ * `occupiedTokens(lastUsage) - estimateOf(the history that produced it)`,
65
+ * clamped to `>= 0`, or `undefined` before the first `turn_end`.
66
+ *
67
+ * THE SYSTEMATIC PART OF THE ESTIMATOR'S ERROR (D-23 / P1-11), chiefly the
68
+ * TOOL SCHEMAS. `estimatePromptTokens` counts the system prompt and the message
69
+ * bodies and NOTHING ELSE, while the loop also sends `tools: toolDefs` and the
70
+ * provider's `input_tokens` includes them - several thousand tokens for this
71
+ * CLI's toolset. So the estimate is biased LOW, always, in the one direction
72
+ * that hurts: the resumed 180k-token session is exactly where the estimate is
73
+ * used, and it reads lower than the truth.
74
+ *
75
+ * One shared FUNCTION is not the same as one shared UNIT. Everywhere an
76
+ * estimated figure is compared against a threshold or shown to a user it is
77
+ * `estimate + (estimateOffset ?? 0)`.
78
+ */
79
+ estimateOffset?: number;
80
+ /**
81
+ * Tokens estimated for messages APPENDED AFTER the measurement in `lastUsage`
82
+ * (hardening §3.2 / W1). `0` on the estimate branch, and `0` whenever the
83
+ * recorded prefix is unusable.
84
+ *
85
+ * ALWAYS PRESENT, never optional, so `isApproximate` is a total function and a
86
+ * consumer cannot forget the `?? 0`. `> 0` is what makes the measured branch
87
+ * approximate: the base is the provider's own number for a request that really
88
+ * happened, and this is a guess about messages no request has yet carried.
89
+ */
90
+ deltaTokens: number;
91
+ }
92
+ /** One compaction, in whatever state it reached. Rendered by `CompactionCard`. */
93
+ export interface CompactionRecord {
94
+ /** 1-based within the session, so the card can name itself. */
95
+ index: number;
96
+ trigger: CompactionUiTrigger;
97
+ mode: CompactionMode;
98
+ applied: boolean;
99
+ reason?: string;
100
+ messagesBefore: number;
101
+ messagesAfter: number;
102
+ tokensBefore: number;
103
+ tokensAfter: number;
104
+ summary?: string;
105
+ model: string;
106
+ durationMs: number;
107
+ /** What the summarization call itself spent. */
108
+ usage?: TokenUsage;
109
+ /** Set when an overflow attempt halved `keepRecentTurns` for itself (D-24). */
110
+ keepRecentTurnsUsed?: number;
111
+ /**
112
+ * Present ONLY when tail relief fired (hardening §3.3.4 / W2).
113
+ *
114
+ * A bounded, ANNOUNCED data loss inside the turns the run is about to continue
115
+ * from, so it is disclosed on the card, in `/compact status`, on the JSON
116
+ * stream and in the archive — the honesty rule `truncated` already follows.
117
+ */
118
+ tailRelief?: {
119
+ messages: number;
120
+ charsRemoved: number;
121
+ };
122
+ /**
123
+ * The archive file this compaction was written to, for `/compact show`
124
+ * (hardening §3.5 / W4). Absent when `compaction.archive` is off, and absent
125
+ * when the write failed — a best-effort artefact never claims to exist.
126
+ */
127
+ archivePath?: string;
128
+ }
129
+ /**
130
+ * WHAT DELIBERATELY DOES NOT LIVE ON `CompactionRecord` (hardening RV-3): the
131
+ * dropped `Message[]`.
132
+ *
133
+ * This record is dispatched into the reducer (`App.tsx`'s `compaction_end` case)
134
+ * and retained in view state for the transcript card's lifetime. Hanging a
135
+ * megabyte-scale array off it would park the compacted history in React state
136
+ * and drag it through every subsequent render — an archive feature that creates
137
+ * the leak it audits. The messages travel on `Compactor.takeLastDropped()`
138
+ * instead, which never enters the event stream.
139
+ */
140
+ /** Everything `/compact status`, the chip and the settings row read. */
141
+ export interface CompactionSnapshot {
142
+ /** Registered AND enabled AND a summarizer model resolves. */
143
+ live: boolean;
144
+ /** The summarizer's model id, or `''`. */
145
+ model: string;
146
+ /**
147
+ * How many compactions ANNOUNCED themselves (quiet-noop §6.2 / D-5).
148
+ *
149
+ * COMPACTIONS THAT RAN, NOT CHECKPOINTS THAT FIRED. A `pressure` compaction
150
+ * that declines before calling a model or changing the history never advances
151
+ * this, which is what keeps the card's `#N` contiguous. `declined` below is
152
+ * the other half of the same total.
153
+ */
154
+ compactions: number;
155
+ /**
156
+ * How many `pressure` checkpoints declined BEFORE committing to any work
157
+ * (quiet-noop D-9 / P1-2).
158
+ *
159
+ * OPTIONAL SO EVERY EXISTING CONSTRUCTION SITE STAYS UNTOUCHED, and rendered
160
+ * by `/compact status` only when non-zero — a healthy session must not read
161
+ * `(0 checkpoints declined)`. This is the one human surface that still speaks
162
+ * about a decline after the transcript went quiet, which is what keeps
163
+ * `context-auto-compaction` §6.5's "guaranteed reporting surface" promise true.
164
+ */
165
+ declined?: number;
166
+ /** Sum of `(tokensBefore - tokensAfter)` over applied compactions. */
167
+ tokensReclaimed: number;
168
+ /** What compaction itself has spent this session. */
169
+ usage: TokenUsage;
170
+ /**
171
+ * CARRIED, NEVER RE-DERIVED (the C-11 / RV-4 lesson from `fast-model-tier`).
172
+ * An unknown model priced at `$0.00` would make the feature look free while it
173
+ * spends money - and here the DEFAULT summarizer is the session's own model.
174
+ */
175
+ pricingUnknown: boolean;
176
+ inFlight: boolean;
177
+ selfDisabled: boolean;
178
+ selfDisabledReason?: string;
179
+ /** How many times this history has been compacted, from the block's `generation`. */
180
+ generation: number;
181
+ pressure: Pressure;
182
+ }
183
+ /**
184
+ * THE COMPACTION EVENT STREAM.
185
+ *
186
+ * `compaction_start` and `compaction_end` are emitted ONLY for compactions that
187
+ * COMMITTED TO WORK (quiet-noop §3.1): a `pressure` compaction that declines
188
+ * before calling a model or changing the history emits NEITHER, and is
189
+ * deliberately indistinguishable from a turn at which `shouldCompact` returned
190
+ * false. `manual` and `overflow` always emit both.
191
+ *
192
+ * The pair is still BALANCED: every `compaction_start` is followed by exactly
193
+ * one `compaction_end` (a `finally` in the engine guarantees the verdict, and
194
+ * `settlePending` synthesizes a skeleton record when the compactor's own has not
195
+ * arrived). Consumers that count PAIRS are unaffected; consumers that counted
196
+ * ATTEMPTS must read `/compact status` or the JSONL diagnostic log instead -
197
+ * CORE's own `compaction_start` / `compaction_end` pair still fires for every
198
+ * attempt, declined or not, and that is the stream the log subscribes to.
199
+ */
200
+ export type CompactionEvent = {
201
+ type: 'compaction_start';
202
+ index: number;
203
+ trigger: CompactionUiTrigger;
204
+ model: string;
205
+ } | {
206
+ type: 'compaction_end';
207
+ record: CompactionRecord;
208
+ } | {
209
+ type: 'usage';
210
+ usage: TokenUsage;
211
+ } | {
212
+ type: 'snapshot';
213
+ snapshot: CompactionSnapshot;
214
+ };
215
+ export type CompactionEventListener = (event: CompactionEvent) => void;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * CLI-local compaction shapes (context-auto-compaction §5.2 / §3.4.2).
3
+ *
4
+ * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
5
+ *
6
+ * A CLI-LOCAL EVENT STREAM, like `FastEvent` / `TeamEvent` / `TodoEvent`, for
7
+ * the reason each of those records: core's `AgentEvent` union is a public
8
+ * contract and must not learn host shapes.
9
+ *
10
+ * NOTE THE ASYMMETRY WITH CORE. Core emits the two ENGINE events because the
11
+ * engine is what performs the splice and decides `applied`; the CLI emits the
12
+ * richer ones because only the CLI knows the model name, the price and the
13
+ * session totals.
14
+ *
15
+ * THIS STREAM IS THE SOLE SOURCE OF REDUCER ACTIONS (D-20 / P1-6), and
16
+ * `reduceEvent` gains NO cases for `compaction_start` / `compaction_end`. One
17
+ * compaction arriving through two channels is two transcript cards and a doubled
18
+ * `tokensReclaimed`; `reduceEvent`'s `default: return []` is already exactly
19
+ * right for the two core events, which makes "one authority" also the smallest
20
+ * possible change.
21
+ */
22
+ export {};