@aragon-agent/cli 0.6.3 → 0.6.7

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 (138) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +203 -13
  3. package/dist/agent/controller.d.ts +158 -2
  4. package/dist/agent/controller.js +400 -1
  5. package/dist/agent/reducer.d.ts +102 -19
  6. package/dist/agent/reducer.js +162 -71
  7. package/dist/agent/system-prompt.d.ts +14 -0
  8. package/dist/agent/system-prompt.js +2 -0
  9. package/dist/agent/usage.d.ts +34 -0
  10. package/dist/agent/usage.js +43 -0
  11. package/dist/cli.js +132 -23
  12. package/dist/commands/builtins.js +101 -1
  13. package/dist/compaction/child.js +5 -0
  14. package/dist/compaction/command.js +14 -1
  15. package/dist/compaction/compactor.d.ts +94 -24
  16. package/dist/compaction/compactor.js +247 -98
  17. package/dist/compaction/context-command.d.ts +33 -0
  18. package/dist/compaction/context-command.js +166 -0
  19. package/dist/compaction/meter.d.ts +251 -0
  20. package/dist/compaction/meter.js +428 -0
  21. package/dist/compaction/pressure.d.ts +6 -0
  22. package/dist/compaction/pressure.js +4 -0
  23. package/dist/compaction/types.d.ts +77 -0
  24. package/dist/compaction/wiring.d.ts +56 -0
  25. package/dist/compaction/wiring.js +107 -25
  26. package/dist/config/cli-commands.js +5 -0
  27. package/dist/config/env.d.ts +1 -1
  28. package/dist/config/env.js +63 -2
  29. package/dist/config/load.d.ts +28 -0
  30. package/dist/config/load.js +83 -1
  31. package/dist/config/schema.d.ts +129 -0
  32. package/dist/config/schema.js +69 -0
  33. package/dist/config/store.js +47 -1
  34. package/dist/exec/events.d.ts +138 -1
  35. package/dist/exec/events.js +42 -0
  36. package/dist/exec/index.js +43 -6
  37. package/dist/exec/progress-reporter.d.ts +23 -0
  38. package/dist/exec/progress-reporter.js +88 -0
  39. package/dist/exec/runner.d.ts +34 -0
  40. package/dist/exec/runner.js +137 -25
  41. package/dist/fast/reviewer.d.ts +19 -1
  42. package/dist/fast/reviewer.js +22 -0
  43. package/dist/fast/types.d.ts +15 -0
  44. package/dist/fast/wiring.d.ts +10 -1
  45. package/dist/fast/wiring.js +18 -1
  46. package/dist/input/limits.d.ts +72 -0
  47. package/dist/input/limits.js +63 -0
  48. package/dist/input/paste-parse.d.ts +71 -0
  49. package/dist/input/paste-parse.js +95 -0
  50. package/dist/input/stdin-filter.d.ts +81 -0
  51. package/dist/input/stdin-filter.js +379 -0
  52. package/dist/logging/install.d.ts +2 -0
  53. package/dist/logging/install.js +53 -1
  54. package/dist/proc/classify.d.ts +52 -0
  55. package/dist/proc/classify.js +143 -0
  56. package/dist/proc/kill-tree.d.ts +42 -0
  57. package/dist/proc/kill-tree.js +119 -0
  58. package/dist/proc/limits.d.ts +84 -0
  59. package/dist/proc/limits.js +84 -0
  60. package/dist/proc/log-ring.d.ts +68 -0
  61. package/dist/proc/log-ring.js +102 -0
  62. package/dist/proc/prompt.d.ts +24 -0
  63. package/dist/proc/prompt.js +49 -0
  64. package/dist/proc/readiness.d.ts +102 -0
  65. package/dist/proc/readiness.js +219 -0
  66. package/dist/proc/supervisor.d.ts +103 -0
  67. package/dist/proc/supervisor.js +465 -0
  68. package/dist/proc/types.d.ts +175 -0
  69. package/dist/proc/types.js +15 -0
  70. package/dist/session/persist.js +36 -1
  71. package/dist/todo/limits.d.ts +4 -17
  72. package/dist/todo/limits.js +4 -17
  73. package/dist/todo/panel-rows.d.ts +8 -6
  74. package/dist/todo/panel-rows.js +19 -6
  75. package/dist/tools/bash-tool.d.ts +51 -2
  76. package/dist/tools/bash-tool.js +321 -51
  77. package/dist/tools/fs-tools.d.ts +17 -0
  78. package/dist/tools/index.d.ts +55 -2
  79. package/dist/tools/index.js +39 -1
  80. package/dist/tools/proc-tools.d.ts +25 -0
  81. package/dist/tools/proc-tools.js +102 -0
  82. package/dist/ui/App.d.ts +15 -2
  83. package/dist/ui/App.js +330 -69
  84. package/dist/ui/AutocompletePopup.d.ts +3 -1
  85. package/dist/ui/AutocompletePopup.js +9 -11
  86. package/dist/ui/BottomStatusRow.d.ts +1 -1
  87. package/dist/ui/Composer.d.ts +56 -1
  88. package/dist/ui/Composer.js +50 -6
  89. package/dist/ui/PromptInput.d.ts +52 -3
  90. package/dist/ui/PromptInput.js +201 -55
  91. package/dist/ui/StatusBar.d.ts +40 -15
  92. package/dist/ui/StatusBar.js +51 -9
  93. package/dist/ui/TeamPanel.d.ts +3 -1
  94. package/dist/ui/TeamPanel.js +13 -9
  95. package/dist/ui/TodoPanel.d.ts +5 -5
  96. package/dist/ui/TodoPanel.js +56 -24
  97. package/dist/ui/Transcript.js +29 -1
  98. package/dist/ui/UpdateLine.d.ts +1 -1
  99. package/dist/ui/capabilities.js +5 -5
  100. package/dist/ui/composer-limits.d.ts +31 -0
  101. package/dist/ui/composer-limits.js +50 -0
  102. package/dist/ui/composer-rows.d.ts +73 -0
  103. package/dist/ui/composer-rows.js +201 -0
  104. package/dist/ui/editor-reducer.d.ts +37 -0
  105. package/dist/ui/editor-reducer.js +88 -12
  106. package/dist/ui/entries/ServiceCard.d.ts +66 -0
  107. package/dist/ui/entries/ServiceCard.js +116 -0
  108. package/dist/ui/entries/UserEntry.d.ts +17 -3
  109. package/dist/ui/entries/UserEntry.js +34 -5
  110. package/dist/ui/exit-snapshot.d.ts +9 -0
  111. package/dist/ui/layout/ScrollViewport.d.ts +1 -1
  112. package/dist/ui/layout/autocomplete.d.ts +18 -0
  113. package/dist/ui/layout/autocomplete.js +26 -0
  114. package/dist/ui/layout/budget.d.ts +34 -6
  115. package/dist/ui/layout/budget.js +32 -7
  116. package/dist/ui/layout/rail.d.ts +4 -53
  117. package/dist/ui/layout/rail.js +10 -62
  118. package/dist/ui/layout/team-panel.d.ts +18 -0
  119. package/dist/ui/layout/team-panel.js +22 -0
  120. package/dist/ui/layout/todo-layout.d.ts +24 -0
  121. package/dist/ui/layout/todo-layout.js +27 -0
  122. package/dist/ui/layout/virtual-window.js +51 -1
  123. package/dist/ui/overlays/HelpOverlay.js +3 -0
  124. package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
  125. package/dist/ui/overlays/QuestionOverlay.js +5 -1
  126. package/dist/ui/overlays/SettingsScreen.js +14 -1
  127. package/dist/ui/paste-frames.d.ts +87 -0
  128. package/dist/ui/paste-frames.js +112 -0
  129. package/dist/ui/paste-tokens.d.ts +90 -0
  130. package/dist/ui/paste-tokens.js +147 -0
  131. package/dist/ui/screen.d.ts +16 -0
  132. package/dist/ui/screen.js +45 -2
  133. package/dist/ui/selection/selection-controller.d.ts +1 -1
  134. package/dist/ui/transcript-text.js +55 -2
  135. package/dist/ui/use-wheel-routing.d.ts +1 -1
  136. package/package.json +2 -2
  137. package/dist/input/stdin-mouse-filter.d.ts +0 -47
  138. package/dist/input/stdin-mouse-filter.js +0 -153
@@ -0,0 +1,428 @@
1
+ /**
2
+ * `ContextMeter` - the ONE owner of "how full is the context right now"
3
+ * (context-usage-gauge-accuracy §3.2 / W1).
4
+ *
5
+ * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope
6
+ * (`glyphs.test.ts::inScope`). If this module is ever moved to a new tree, the
7
+ * scanner's hardcoded directory list must be widened IN THE SAME COMMIT - a
8
+ * scanner that silently stops scanning is worse than no scanner, and this
9
+ * package has paid for that edit eleven times (I-7).
10
+ *
11
+ * IT LIVES HERE BUT IT IS NOT PART OF COMPACTION, and that is deliberate. The
12
+ * whole point of this class is that occupancy is measurable when compaction is
13
+ * OFF: `AgentController` builds a `CompactionWiring` only when
14
+ * `compaction.enabled` is true, so a measurement owned by the compactor is
15
+ * unreachable for exactly the sessions that most need a working gauge. It sits
16
+ * in this directory because `pressure.ts` - the arithmetic it drives - already
17
+ * does, and because moving it would cost a scanner edit for no gain (R-10).
18
+ *
19
+ * THE STATE MACHINE IN ONE PARAGRAPH. Every mutation of the history marks the
20
+ * meter DIRTY; every read that finds it dirty re-measures SYNCHRONOUSLY. That
21
+ * ordering is what removes the subscription-order dependency in the APPEND
22
+ * direction: it does not matter whether this class or the compaction wiring
23
+ * hears `tool_execution_end` first, because whoever reads gets a number that
24
+ * includes the appended message either way (I-2). It does NOT remove the
25
+ * dependency in the REPLACE direction - a splice does not announce itself to a
26
+ * listener that has not run yet - which is why the splice marks this dirty from
27
+ * `CompactionWiring.settlePending`'s own synchronous code (I-9), and why
28
+ * `current()` carries a structural belt for a missed invalidation site.
29
+ */
30
+ import { computeEstimateOffset, computePressure } from './pressure.js';
31
+ /**
32
+ * How long a scheduled publish waits.
33
+ *
34
+ * IT MUST BE A DELAY, NOT ZERO, AND NOT A SYNCHRONOUS MEASUREMENT (I-3). The
35
+ * loop emits `tool_execution_end` BEFORE it pushes the `tool_result` message, so
36
+ * anything that measures inside that event measures a history missing the single
37
+ * largest message of the turn - silently, and every turn. A `setTimeout` is
38
+ * necessarily later than that synchronous push.
39
+ *
40
+ * 400 ms also caps the publish rate at 2.5 Hz, which together with the reducer's
41
+ * identity short-circuit keeps a permanently visible row from becoming a
42
+ * re-render source (R-3).
43
+ */
44
+ export const CONTEXT_METER_TICK_MS = 400;
45
+ /**
46
+ * The projection `ViewState` and `/context` read (§4.1).
47
+ *
48
+ * THE ONLY CONVERTER between `Pressure` and `ContextUsageSnapshot`. Two of them
49
+ * would eventually round the percentage differently, and "the status bar says 43
50
+ * and `/context` says 44" is the exact class of disagreement this whole feature
51
+ * exists to remove.
52
+ */
53
+ export function toContextUsage(p) {
54
+ return {
55
+ occupied: p.occupied,
56
+ window: p.contextWindow,
57
+ // FROM `ratio`, NOT FROM A SECOND DIVISION. `computePressure` already
58
+ // clamped it to [0, 1] and already decided what a zero window means.
59
+ pct: Math.max(0, Math.min(100, Math.round(p.ratio * 100))),
60
+ source: p.source,
61
+ deltaTokens: p.deltaTokens,
62
+ windowKnown: p.windowKnown,
63
+ windowOverridden: p.windowOverridden,
64
+ };
65
+ }
66
+ /** The reading a meter with nothing measured yet reports. */
67
+ export function emptyContextUsage() {
68
+ return {
69
+ occupied: 0,
70
+ window: 0,
71
+ pct: 0,
72
+ source: 'estimate',
73
+ deltaTokens: 0,
74
+ windowKnown: false,
75
+ windowOverridden: false,
76
+ };
77
+ }
78
+ export class ContextMeter {
79
+ deps;
80
+ listeners = new Set();
81
+ unsubscribe = null;
82
+ timer = null;
83
+ disposed = false;
84
+ /**
85
+ * The authoritative usage of the last completed turn.
86
+ *
87
+ * Its presence is what selects `computePressure`'s MEASURED branch, so
88
+ * dropping it is how every reset degrades to an estimate rather than to a
89
+ * number describing a history that no longer exists.
90
+ */
91
+ lastUsage;
92
+ /**
93
+ * The systematic difference between what `estimatePromptTokens` counts and
94
+ * what the provider bills (D-23) - chiefly the tool schemas.
95
+ *
96
+ * SURVIVES A SPLICE AND NOT A SESSION CHANGE (I-8). See `onHistorySpliced`.
97
+ */
98
+ estimateOffset;
99
+ /** `messages.length` at the `turn_end` that produced `lastUsage`. */
100
+ measuredPrefixLength;
101
+ /** The last pressure PUBLISHED. Never re-derived on read - see `lastPublished`. */
102
+ last = null;
103
+ /** Whether the history has changed since the last measurement. */
104
+ dirty = true;
105
+ constructor(deps) {
106
+ this.deps = deps;
107
+ }
108
+ // =========================================================================
109
+ // Reading
110
+ // =========================================================================
111
+ /**
112
+ * Occupancy right now. FOR THE SCREEN.
113
+ *
114
+ * Re-measures synchronously when dirty (I-2), and publishes when it does - the
115
+ * gauge has to fall in the SAME FRAME as the compaction that caused it (AC-1),
116
+ * and the caller that triggers this re-measurement is `CompactionWiring`'s own
117
+ * synchronous `snapshot()` inside `settlePending`. Leaving the publish to the
118
+ * scheduled tick instead would be worse than slow: the tick fires only when
119
+ * `dirty`, and this call is what clears it.
120
+ *
121
+ * NEVER THROWS. A meter that can throw is a status bar that can take the
122
+ * process down.
123
+ */
124
+ current() {
125
+ // THE BELT (I-9), AND IT RUNS EVEN WHEN NOT DIRTY. The case it exists for is
126
+ // a splice that NOBODY announced, and in that case a missing `dirty` flag is
127
+ // precisely the symptom. `estimateAppendedTokens` bounds-checks too, but it
128
+ // only zeroes the DELTA and keeps the wrong measured base, so that belt does
129
+ // not cover this.
130
+ if (this.discardStalePrefix())
131
+ this.dirty = true;
132
+ if (!this.dirty && this.last)
133
+ return this.last;
134
+ return this.measureAndPublish(this.lastUsage);
135
+ }
136
+ /**
137
+ * The last pressure this meter PUBLISHED. FOR THE BOOKS. Never re-measures.
138
+ *
139
+ * `Compactor.lastMeasured()` forwards here and not to `current()`, and the
140
+ * distinction is load-bearing (I-10): `compactor.ts`'s `tokensBefore` is the
141
+ * sole upstream of `tokensReclaimed`, the card's "reclaimed N tokens" line and
142
+ * `/compact status`'s session total. It wants the occupancy BEFORE the splice.
143
+ * Wired to `current()` it would re-measure the already-spliced history, report
144
+ * `reclaimed 0`, and render that number without a single log line about it.
145
+ */
146
+ lastPublished() {
147
+ return this.last;
148
+ }
149
+ /** The published projection, or a zeroed one before the first measurement. */
150
+ currentUsage() {
151
+ return toContextUsage(this.current());
152
+ }
153
+ // =========================================================================
154
+ // Writing
155
+ // =========================================================================
156
+ /**
157
+ * Measure with the caller's own authoritative usage. FOR A DECISION POINT.
158
+ *
159
+ * `Compactor.shouldCompact` has a `CompactionProbe.lastUsage` the meter may
160
+ * not have seen yet, so it supplies it rather than trusting the field here.
161
+ */
162
+ measureWith(lastUsage) {
163
+ if (this.discardStalePrefix())
164
+ this.dirty = true;
165
+ return this.measureAndPublish(lastUsage);
166
+ }
167
+ /**
168
+ * `turn_end` - the one moment in the whole flow that is FULLY measured.
169
+ *
170
+ * The loop emits it BEFORE pushing the assistant message, so the history right
171
+ * now is exactly the history the reported `usage` was billed for: the offset
172
+ * and the prefix length describe the same array by construction, and
173
+ * `deltaTokens` is 0. Published immediately for that reason - there is no
174
+ * better number to wait for.
175
+ */
176
+ onTurnEnd(usage) {
177
+ const messages = this.deps.getMessages();
178
+ const systemPrompt = this.readSystemPrompt();
179
+ this.lastUsage = usage;
180
+ this.estimateOffset = computeEstimateOffset(usage, messages, systemPrompt);
181
+ this.measuredPrefixLength = messages.length;
182
+ this.measureAndPublish(usage);
183
+ }
184
+ /**
185
+ * A compaction spliced the history (I-8, the SHALLOW reset).
186
+ *
187
+ * `estimateOffset` IS KEPT, and that is the whole reason this method is not
188
+ * `onHistoryReplaced`. The offset describes "what this toolset costs on this
189
+ * model above what the estimator counts"; a splice changes neither the toolset
190
+ * nor the model, so it is still valid - and it is the only calibration the
191
+ * estimate branch has until the next `turn_end`. Dropping it makes the gauge
192
+ * fall several thousand tokens too far and then visibly climb back on the next
193
+ * turn, for no reason the user can see (I-5).
194
+ *
195
+ * IDEMPOTENT. Both `settlePending` (the authority, I-9) and this meter's own
196
+ * `compaction_end` handler reach it, and the second one must cost nothing.
197
+ */
198
+ onHistorySpliced() {
199
+ this.lastUsage = undefined;
200
+ this.measuredPrefixLength = undefined;
201
+ if (this.dirty)
202
+ return;
203
+ this.dirty = true;
204
+ this.scheduleTick();
205
+ }
206
+ /**
207
+ * The history came from somewhere else entirely (I-8, the DEEP reset):
208
+ * `/clear`, `/reset`, `/resume`.
209
+ *
210
+ * `estimateOffset` GOES TOO, unlike the shallow reset above. `/resume` can
211
+ * change the model in the same breath (`builtins.ts`'s restore path does), and
212
+ * an offset carried across that is a calibration constant applied to something
213
+ * it never calibrated. The first reading afterwards is therefore a bare
214
+ * estimate - biased LOW, and corrected by the very next `turn_end`. Biased low
215
+ * and self-correcting beats biased high and permanent.
216
+ */
217
+ onHistoryReplaced() {
218
+ this.lastUsage = undefined;
219
+ this.measuredPrefixLength = undefined;
220
+ this.estimateOffset = undefined;
221
+ this.dirty = true;
222
+ this.scheduleTick();
223
+ }
224
+ /**
225
+ * Only the DENOMINATOR moved (`/model`, a `contextWindow` edit).
226
+ *
227
+ * DELIBERATELY NOT A RESET. The measured base is still a true statement about
228
+ * the history, so throwing it away would trade a correct numerator for a whole
229
+ * history estimate and produce a visible jump on a common operation. The
230
+ * numerator's calibration is one turn stale until the next `turn_end`
231
+ * refreshes it, which is the trade this method names (RV-13).
232
+ */
233
+ onWindowChanged() {
234
+ this.dirty = true;
235
+ this.measureAndPublish(this.lastUsage);
236
+ }
237
+ /**
238
+ * Something was appended. Mark dirty, arrange a throttled publish, MEASURE
239
+ * NOTHING (I-3).
240
+ */
241
+ scheduleTick() {
242
+ this.dirty = true;
243
+ // NO SUBSCRIBERS, NO TIMER (I-11). `AgentController` is shared with
244
+ // `aragon exec`, where nothing reads this number; a headless run must not
245
+ // pay a timer per tool call, and a pending tick must never be able to hold
246
+ // the process open for an extra 400 ms. `current()` is unaffected - it
247
+ // measures on demand, so `wiring.snapshot()` and `/context` still work in a
248
+ // host with no UI.
249
+ if (this.listeners.size === 0)
250
+ return;
251
+ if (this.timer !== null || this.disposed)
252
+ return;
253
+ const handle = setTimeout(() => {
254
+ this.timer = null;
255
+ if (!this.dirty || this.disposed)
256
+ return;
257
+ this.measureAndPublish(this.lastUsage);
258
+ }, CONTEXT_METER_TICK_MS);
259
+ // The other half of I-11: an armed tick must not keep the event loop alive.
260
+ const unrefable = handle;
261
+ if (typeof unrefable.unref === 'function')
262
+ unrefable.unref();
263
+ this.timer = handle;
264
+ }
265
+ // =========================================================================
266
+ // Plumbing
267
+ // =========================================================================
268
+ /** Publish to this meter's own subscribers. Returns an unsubscribe function. */
269
+ subscribe(listener) {
270
+ this.listeners.add(listener);
271
+ return () => {
272
+ this.listeners.delete(listener);
273
+ };
274
+ }
275
+ /**
276
+ * Subscribe to the lead agent's stream. IDEMPOTENT.
277
+ *
278
+ * THIS TABLE IS NOT THE SOURCE OF COMPACTION CORRECTNESS (I-9). The splice is
279
+ * marked dirty by `CompactionWiring.settlePending` inside its own synchronous
280
+ * code, BEFORE it emits either event, so a reader is correct regardless of
281
+ * which listener the emitter reaches first. The `compaction_end` row below is
282
+ * a second, idempotent path that serves the manual `/compact` route and any
283
+ * future splicer.
284
+ */
285
+ attach(subscribe) {
286
+ if (this.unsubscribe)
287
+ return;
288
+ this.unsubscribe = subscribe((event) => this.onAgentEvent(event));
289
+ }
290
+ dispose() {
291
+ this.disposed = true;
292
+ if (this.timer !== null) {
293
+ clearTimeout(this.timer);
294
+ this.timer = null;
295
+ }
296
+ this.unsubscribe?.();
297
+ this.unsubscribe = null;
298
+ this.listeners.clear();
299
+ }
300
+ // =========================================================================
301
+ // Internals
302
+ // =========================================================================
303
+ onAgentEvent(event) {
304
+ switch (event.type) {
305
+ case 'turn_end':
306
+ this.onTurnEnd(event.usage);
307
+ return;
308
+ case 'tool_execution_end':
309
+ // DELAYED, NEVER SYNCHRONOUS. See `CONTEXT_METER_TICK_MS` (I-3).
310
+ this.scheduleTick();
311
+ return;
312
+ case 'compaction_end':
313
+ // The engine's verdict. `applied` means the history really was replaced.
314
+ if (event.applied)
315
+ this.onHistorySpliced();
316
+ else
317
+ this.scheduleTick();
318
+ return;
319
+ case 'agent_end':
320
+ this.scheduleTick();
321
+ return;
322
+ default:
323
+ // `message_update` is a STREAM event and is deliberately ignored (N-2):
324
+ // the output tokens of an in-flight reply are not knowable until `done`,
325
+ // and guessing them would add a third approximation to a number that
326
+ // already carries two.
327
+ return;
328
+ }
329
+ }
330
+ /**
331
+ * The structural belt for a missed invalidation site (I-9).
332
+ *
333
+ * A recorded prefix that is LONGER than the history it indexes describes an
334
+ * array that no longer exists. Rather than merely zeroing the delta - which is
335
+ * what `estimateAppendedTokens` does, keeping the wrong measured base - this
336
+ * drops the base too and forces the estimate branch.
337
+ *
338
+ * THE COMPARISON IS STRICT, AND THAT BOUNDARY IS LOAD-BEARING (IF-1). The
339
+ * design document wrote `length <= measuredPrefixLength`, which is off by one:
340
+ * `turn_end` is emitted BEFORE the assistant message is pushed, so the instant
341
+ * `onTurnEnd` records the prefix the history is EXACTLY that long. An
342
+ * inclusive test therefore discards a measurement one statement after taking
343
+ * it - but only when this meter's listener runs before the compaction wiring's,
344
+ * because the wiring's `snapshot()` is what reads in that window. That is a
345
+ * silent, subscription-order-dependent fallback to the estimate branch on
346
+ * every single turn, which is the exact class of bug this feature exists to
347
+ * remove. A measurement taken at length N describes every history of length
348
+ * >= N; only a SHORTER one is impossible.
349
+ *
350
+ * Returns whether anything was discarded.
351
+ */
352
+ discardStalePrefix() {
353
+ if (this.measuredPrefixLength === undefined)
354
+ return false;
355
+ const length = this.deps.getMessages().length;
356
+ if (length >= this.measuredPrefixLength)
357
+ return false;
358
+ this.lastUsage = undefined;
359
+ this.measuredPrefixLength = undefined;
360
+ return true;
361
+ }
362
+ /**
363
+ * Measure, cache, clear the dirty flag, publish. The ONE write path.
364
+ *
365
+ * Every public mutator funnels through here so "what was published" and "what
366
+ * is cached" cannot drift, which is what makes `lastPublished()` a meaningful
367
+ * answer rather than a stale one.
368
+ */
369
+ measureAndPublish(lastUsage) {
370
+ const pressure = this.compute(lastUsage);
371
+ this.last = pressure;
372
+ this.dirty = false;
373
+ this.publish(pressure);
374
+ return pressure;
375
+ }
376
+ compute(lastUsage) {
377
+ const window = this.resolveWindow();
378
+ return computePressure({
379
+ ...(lastUsage ? { lastUsage } : {}),
380
+ messages: this.deps.getMessages(),
381
+ systemPrompt: this.readSystemPrompt(),
382
+ contextWindow: window.contextWindow,
383
+ windowKnown: window.windowKnown,
384
+ windowOverridden: window.windowOverridden,
385
+ ...(this.estimateOffset !== undefined ? { estimateOffset: this.estimateOffset } : {}),
386
+ ...(this.measuredPrefixLength !== undefined
387
+ ? { measuredPrefixLength: this.measuredPrefixLength }
388
+ : {}),
389
+ });
390
+ }
391
+ /**
392
+ * Where the denominator comes from, and whether it can be trusted.
393
+ *
394
+ * AN OVERRIDE MAKES THE WINDOW KNOWN (I-6): the user asserted it, so the `~`
395
+ * that means "this denominator is a guess" has no business staying on the
396
+ * percentage. `windowOverridden` travels separately so `/context` can still
397
+ * name the source - a hand-typed window that is wrong is exactly the case that
398
+ * most needs to be visible.
399
+ */
400
+ resolveWindow() {
401
+ const override = this.deps.getWindowOverride();
402
+ if (override !== null && Number.isFinite(override) && override > 0) {
403
+ return { contextWindow: override, windowKnown: true, windowOverridden: true };
404
+ }
405
+ return {
406
+ contextWindow: this.deps.getModelInfo().contextWindow,
407
+ windowKnown: this.deps.isWindowKnown(),
408
+ windowOverridden: false,
409
+ };
410
+ }
411
+ readSystemPrompt() {
412
+ return this.deps.getSystemPrompt();
413
+ }
414
+ publish(pressure) {
415
+ if (this.listeners.size === 0)
416
+ return;
417
+ const usage = toContextUsage(pressure);
418
+ for (const listener of [...this.listeners]) {
419
+ try {
420
+ listener(usage);
421
+ }
422
+ catch {
423
+ // One bad subscriber must not take the run down with it - the rule
424
+ // `CompactionWiring.emit` and `TeamRuntime.emit` both already state.
425
+ }
426
+ }
427
+ }
428
+ }
@@ -57,6 +57,12 @@ export interface PressureInput {
57
57
  contextWindow: number;
58
58
  /** False when the window is `buildRuntimeModel`'s 128k placeholder. */
59
59
  windowKnown: boolean;
60
+ /**
61
+ * Whether `contextWindow` came from the user's `contextWindow` override
62
+ * (context-usage-gauge-accuracy I-6). Defaults to `false`, so every existing
63
+ * construction site is unchanged and the arithmetic below is untouched.
64
+ */
65
+ windowOverridden?: boolean;
60
66
  /** Carried from the last `turn_end`; applied to every ESTIMATED figure. */
61
67
  estimateOffset?: number;
62
68
  /**
@@ -138,6 +138,10 @@ export function computePressure(input) {
138
138
  headroom: Math.max(0, window - occupied),
139
139
  source,
140
140
  windowKnown: input.windowKnown,
141
+ // PURE PASS-THROUGH. This function's arithmetic is unchanged by
142
+ // context-usage-gauge-accuracy; the flag only travels with the number so
143
+ // `/context` can name the denominator's source (I-6).
144
+ windowOverridden: input.windowOverridden ?? false,
141
145
  ...(input.estimateOffset !== undefined ? { estimateOffset: input.estimateOffset } : {}),
142
146
  deltaTokens,
143
147
  };
@@ -60,6 +60,18 @@ export interface Pressure {
60
60
  source: PressureSource;
61
61
  /** False when the window came from `buildRuntimeModel`'s 128k placeholder. */
62
62
  windowKnown: boolean;
63
+ /**
64
+ * Whether the denominator came from the user's `contextWindow` override
65
+ * (context-usage-gauge-accuracy I-6).
66
+ *
67
+ * SEPARATE FROM `windowKnown` RATHER THAN FOLDED INTO IT. An override makes
68
+ * the window KNOWN - the user asserted it, so the `~` has no business staying
69
+ * on the percentage - but `/context` still has to be able to say WHO supplied
70
+ * the number. Collapsing the two into one boolean makes "found in the model
71
+ * table" and "typed by hand" indistinguishable in the report, and the second
72
+ * one being wrong is exactly the case that most needs to be visible.
73
+ */
74
+ windowOverridden: boolean;
63
75
  /**
64
76
  * `occupiedTokens(lastUsage) - estimateOf(the history that produced it)`,
65
77
  * clamped to `>= 0`, or `undefined` before the first `turn_end`.
@@ -89,6 +101,35 @@ export interface Pressure {
89
101
  */
90
102
  deltaTokens: number;
91
103
  }
104
+ /**
105
+ * The ONE occupancy projection the status bar and `/context` read
106
+ * (context-usage-gauge-accuracy §4.1).
107
+ *
108
+ * DELIBERATELY CARRIES NO TIMESTAMP. Its only consumer is the reducer's identity
109
+ * short-circuit (§5.2), and a field that changes on every tick would make that
110
+ * short-circuit never hold - turning a permanently visible row into a 2.5 Hz
111
+ * re-render source and pushing the render governor up its ladder for nothing.
112
+ *
113
+ * `Pressure` is the compaction subsystem's shape and carries a headroom and a
114
+ * ratio the UI does not want; this is the projection of it that crosses into
115
+ * view state, and `toContextUsage` in `meter.ts` is the ONLY converter.
116
+ */
117
+ export interface ContextUsageSnapshot {
118
+ /** Occupied tokens (measured, plus an estimate of anything appended since). */
119
+ occupied: number;
120
+ /** The denominator actually used. */
121
+ window: number;
122
+ /** `occupied / window` as a percentage, clamped to [0, 100] and rounded. */
123
+ pct: number;
124
+ /** Where the BASE came from. */
125
+ source: PressureSource;
126
+ /** How much of `occupied` is an estimate of appended messages; `> 0` means approximate. */
127
+ deltaTokens: number;
128
+ /** Whether the denominator is trustworthy (in the model table, or overridden). */
129
+ windowKnown: boolean;
130
+ /** Whether the denominator came from the user's `contextWindow` (I-6). */
131
+ windowOverridden: boolean;
132
+ }
92
133
  /** One compaction, in whatever state it reached. Rendered by `CompactionCard`. */
93
134
  export interface CompactionRecord {
94
135
  /** 1-based within the session, so the card can name itself. */
@@ -143,7 +184,26 @@ export interface CompactionSnapshot {
143
184
  live: boolean;
144
185
  /** The summarizer's model id, or `''`. */
145
186
  model: string;
187
+ /**
188
+ * How many compactions ANNOUNCED themselves (quiet-noop §6.2 / D-5).
189
+ *
190
+ * COMPACTIONS THAT RAN, NOT CHECKPOINTS THAT FIRED. A `pressure` compaction
191
+ * that declines before calling a model or changing the history never advances
192
+ * this, which is what keeps the card's `#N` contiguous. `declined` below is
193
+ * the other half of the same total.
194
+ */
146
195
  compactions: number;
196
+ /**
197
+ * How many `pressure` checkpoints declined BEFORE committing to any work
198
+ * (quiet-noop D-9 / P1-2).
199
+ *
200
+ * OPTIONAL SO EVERY EXISTING CONSTRUCTION SITE STAYS UNTOUCHED, and rendered
201
+ * by `/compact status` only when non-zero — a healthy session must not read
202
+ * `(0 checkpoints declined)`. This is the one human surface that still speaks
203
+ * about a decline after the transcript went quiet, which is what keeps
204
+ * `context-auto-compaction` §6.5's "guaranteed reporting surface" promise true.
205
+ */
206
+ declined?: number;
147
207
  /** Sum of `(tokensBefore - tokensAfter)` over applied compactions. */
148
208
  tokensReclaimed: number;
149
209
  /** What compaction itself has spent this session. */
@@ -161,6 +221,23 @@ export interface CompactionSnapshot {
161
221
  generation: number;
162
222
  pressure: Pressure;
163
223
  }
224
+ /**
225
+ * THE COMPACTION EVENT STREAM.
226
+ *
227
+ * `compaction_start` and `compaction_end` are emitted ONLY for compactions that
228
+ * COMMITTED TO WORK (quiet-noop §3.1): a `pressure` compaction that declines
229
+ * before calling a model or changing the history emits NEITHER, and is
230
+ * deliberately indistinguishable from a turn at which `shouldCompact` returned
231
+ * false. `manual` and `overflow` always emit both.
232
+ *
233
+ * The pair is still BALANCED: every `compaction_start` is followed by exactly
234
+ * one `compaction_end` (a `finally` in the engine guarantees the verdict, and
235
+ * `settlePending` synthesizes a skeleton record when the compactor's own has not
236
+ * arrived). Consumers that count PAIRS are unaffected; consumers that counted
237
+ * ATTEMPTS must read `/compact status` or the JSONL diagnostic log instead -
238
+ * CORE's own `compaction_start` / `compaction_end` pair still fires for every
239
+ * attempt, declined or not, and that is the stream the log subscribes to.
240
+ */
164
241
  export type CompactionEvent = {
165
242
  type: 'compaction_start';
166
243
  index: number;
@@ -25,11 +25,21 @@
25
25
  * methods delegate to a compactor that reads config LIVE - which is what lets
26
26
  * `/compact off`, `/compact threshold 0.8` and a settings-screen save take effect
27
27
  * mid-session without touching the `Agent`.
28
+ *
29
+ * DECLINED COMPACTIONS PRODUCE NO CLI EVENTS (quiet-noop §3.6). A `pressure`
30
+ * compaction that changed nothing, called nothing and spent nothing never emits
31
+ * `compaction_start`, so `onCompactorEvent` never opens a card and `settlePending`
32
+ * finds neither a held record nor a skeleton to synthesize. CORE still emits its
33
+ * own pair for every attempt, which is why `settlePending` still runs - it just
34
+ * refreshes the snapshot and returns. `onCompactorEvent`'s `if (!this.open)`
35
+ * guard is now unreachable for declines and stays for the abort race it was
36
+ * written for.
28
37
  */
29
38
  import { type AgentEvent, type ContextManager, type Message, type ModelInfo, type ModelRef, type ProviderRegistry, type ProviderRegistryOptions, type TokenUsage } from '@aragon-agent/core';
30
39
  import type { CliConfig } from '../config/schema.js';
31
40
  import type { NoticeLevel } from '../agent/reducer.js';
32
41
  import { type ChildContextManagerFactory } from './child.js';
42
+ import { ContextMeter } from './meter.js';
33
43
  import type { CompactionEventListener, CompactionSnapshot } from './types.js';
34
44
  export interface CompactionWiringDeps {
35
45
  /** Read LIVE on every call - the controller replaces the object on mutation. */
@@ -45,6 +55,17 @@ export interface CompactionWiringDeps {
45
55
  getMessages: () => readonly Message[];
46
56
  getSystemPrompt: () => string;
47
57
  notify: (level: NoticeLevel, text: string) => void;
58
+ /**
59
+ * The controller's `ContextMeter`, forwarded to the compactor
60
+ * (context-usage-gauge-accuracy W1).
61
+ *
62
+ * OPTIONAL SO EVERY EXISTING CONSTRUCTION SITE COMPILES (R-1); production
63
+ * always injects, and `AgentController` owns the instance because it builds
64
+ * one UNCONDITIONALLY while this class only exists when `compaction.enabled`
65
+ * is true. That asymmetry is the whole point: occupancy has to be measurable
66
+ * in a session that turned compaction off.
67
+ */
68
+ meter?: ContextMeter;
48
69
  /**
49
70
  * How the summarizer's own registry is built. Defaults to `initProviders`.
50
71
  *
@@ -71,6 +92,8 @@ export declare class CompactionWiring {
71
92
  private readonly log;
72
93
  private readonly listeners;
73
94
  private readonly compactor;
95
+ /** The occupancy meter, injected or private (R-1). Forwarded to the compactor. */
96
+ private readonly meter;
74
97
  /** The compactor's OWN transport (D-10). Lazy - see `getRegistry()`. */
75
98
  private registry;
76
99
  private unsubscribe;
@@ -143,6 +166,20 @@ export declare class CompactionWiring {
143
166
  * `/clear`, `/resume`, and any future host-side rewrite. A recorded prefix
144
167
  * length indexes an array that no longer exists, so the delta must be
145
168
  * abandoned rather than applied to a different history.
169
+ *
170
+ * THE **DEEP** RESET (I-8). `/resume` can change the model in the same breath,
171
+ * so `estimateOffset` - a calibration for one toolset on one model - goes too.
172
+ * The SHALLOW reset is `settlePending`'s splice branch, and the two are not
173
+ * interchangeable: swapping them produces silent errors in OPPOSITE
174
+ * directions (a splice that drops the offset reads too low and climbs back; a
175
+ * resume that keeps it reads too high and never self-corrects).
176
+ *
177
+ * DUPLICATED BY `AgentController` ON PURPOSE. The controller calls
178
+ * `contextMeter.onHistoryReplaced()` directly at the same two sites, because
179
+ * this class does not exist at all when `compaction.enabled` is false - and
180
+ * that unconditional call is the only thing that keeps `/resume` re-measuring
181
+ * for such a session. A deep reset is idempotent, so both firing costs
182
+ * nothing; deleting the controller's copy "to de-duplicate" reintroduces P0-2.
146
183
  */
147
184
  onHistoryReplaced(): void;
148
185
  /**
@@ -192,6 +229,17 @@ export declare class CompactionWiring {
192
229
  ok: false;
193
230
  reason: string;
194
231
  }>;
232
+ /**
233
+ * The status a reader gets right now.
234
+ *
235
+ * IT READS `meter.current()`, WHICH RE-MEASURES WHEN DIRTY - and that is the
236
+ * fix for P0-1 (§3.3). This method is called one statement after
237
+ * `settlePending` marks the meter spliced, so the `snapshot` event emitted
238
+ * immediately after `compaction_end` carries the POST-compaction occupancy.
239
+ * The old body preferred `compactor.lastMeasured()`, i.e. the measurement that
240
+ * TRIGGERED this compaction, so the gauge fell and was bounced straight back
241
+ * to the pre-compaction figure inside the same synchronous emit loop.
242
+ */
195
243
  snapshot(): CompactionSnapshot;
196
244
  /**
197
245
  * `snapshot()` with the pressure argument substituted for the re-measurement.
@@ -205,6 +253,14 @@ export declare class CompactionWiring {
205
253
  /** The summarizer that would be used right now, for `/compact status`. */
206
254
  summarizerRef(): ModelRef | null;
207
255
  subscribe(listener: CompactionEventListener): () => void;
256
+ /**
257
+ * The main model, for the FALLBACK meter's deps only (R-1).
258
+ *
259
+ * Production injects a meter built by `AgentController`, which resolves the
260
+ * same ref from the same live config; this exists so a test that constructs a
261
+ * bare wiring still gets a working denominator.
262
+ */
263
+ private mainRef;
208
264
  /**
209
265
  * The transport one summarization call goes through - separate from the lead's,
210
266
  * and fail-fast (D-10).