@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.
- package/CHANGELOG.md +142 -0
- package/README.md +203 -13
- package/dist/agent/controller.d.ts +158 -2
- package/dist/agent/controller.js +400 -1
- package/dist/agent/reducer.d.ts +102 -19
- package/dist/agent/reducer.js +162 -71
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/agent/usage.d.ts +34 -0
- package/dist/agent/usage.js +43 -0
- package/dist/cli.js +132 -23
- package/dist/commands/builtins.js +101 -1
- package/dist/compaction/child.js +5 -0
- package/dist/compaction/command.js +14 -1
- package/dist/compaction/compactor.d.ts +94 -24
- package/dist/compaction/compactor.js +247 -98
- package/dist/compaction/context-command.d.ts +33 -0
- package/dist/compaction/context-command.js +166 -0
- package/dist/compaction/meter.d.ts +251 -0
- package/dist/compaction/meter.js +428 -0
- package/dist/compaction/pressure.d.ts +6 -0
- package/dist/compaction/pressure.js +4 -0
- package/dist/compaction/types.d.ts +77 -0
- package/dist/compaction/wiring.d.ts +56 -0
- package/dist/compaction/wiring.js +107 -25
- package/dist/config/cli-commands.js +5 -0
- package/dist/config/env.d.ts +1 -1
- package/dist/config/env.js +63 -2
- package/dist/config/load.d.ts +28 -0
- package/dist/config/load.js +83 -1
- package/dist/config/schema.d.ts +129 -0
- package/dist/config/schema.js +69 -0
- package/dist/config/store.js +47 -1
- package/dist/exec/events.d.ts +138 -1
- package/dist/exec/events.js +42 -0
- package/dist/exec/index.js +43 -6
- package/dist/exec/progress-reporter.d.ts +23 -0
- package/dist/exec/progress-reporter.js +88 -0
- package/dist/exec/runner.d.ts +34 -0
- package/dist/exec/runner.js +137 -25
- package/dist/fast/reviewer.d.ts +19 -1
- package/dist/fast/reviewer.js +22 -0
- package/dist/fast/types.d.ts +15 -0
- package/dist/fast/wiring.d.ts +10 -1
- package/dist/fast/wiring.js +18 -1
- package/dist/input/limits.d.ts +72 -0
- package/dist/input/limits.js +63 -0
- package/dist/input/paste-parse.d.ts +71 -0
- package/dist/input/paste-parse.js +95 -0
- package/dist/input/stdin-filter.d.ts +81 -0
- package/dist/input/stdin-filter.js +379 -0
- package/dist/logging/install.d.ts +2 -0
- package/dist/logging/install.js +53 -1
- package/dist/proc/classify.d.ts +52 -0
- package/dist/proc/classify.js +143 -0
- package/dist/proc/kill-tree.d.ts +42 -0
- package/dist/proc/kill-tree.js +119 -0
- package/dist/proc/limits.d.ts +84 -0
- package/dist/proc/limits.js +84 -0
- package/dist/proc/log-ring.d.ts +68 -0
- package/dist/proc/log-ring.js +102 -0
- package/dist/proc/prompt.d.ts +24 -0
- package/dist/proc/prompt.js +49 -0
- package/dist/proc/readiness.d.ts +102 -0
- package/dist/proc/readiness.js +219 -0
- package/dist/proc/supervisor.d.ts +103 -0
- package/dist/proc/supervisor.js +465 -0
- package/dist/proc/types.d.ts +175 -0
- package/dist/proc/types.js +15 -0
- package/dist/session/persist.js +36 -1
- package/dist/todo/limits.d.ts +4 -17
- package/dist/todo/limits.js +4 -17
- package/dist/todo/panel-rows.d.ts +8 -6
- package/dist/todo/panel-rows.js +19 -6
- package/dist/tools/bash-tool.d.ts +51 -2
- package/dist/tools/bash-tool.js +321 -51
- package/dist/tools/fs-tools.d.ts +17 -0
- package/dist/tools/index.d.ts +55 -2
- package/dist/tools/index.js +39 -1
- package/dist/tools/proc-tools.d.ts +25 -0
- package/dist/tools/proc-tools.js +102 -0
- package/dist/ui/App.d.ts +15 -2
- package/dist/ui/App.js +330 -69
- package/dist/ui/AutocompletePopup.d.ts +3 -1
- package/dist/ui/AutocompletePopup.js +9 -11
- package/dist/ui/BottomStatusRow.d.ts +1 -1
- package/dist/ui/Composer.d.ts +56 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +52 -3
- package/dist/ui/PromptInput.js +201 -55
- package/dist/ui/StatusBar.d.ts +40 -15
- package/dist/ui/StatusBar.js +51 -9
- package/dist/ui/TeamPanel.d.ts +3 -1
- package/dist/ui/TeamPanel.js +13 -9
- package/dist/ui/TodoPanel.d.ts +5 -5
- package/dist/ui/TodoPanel.js +56 -24
- package/dist/ui/Transcript.js +29 -1
- package/dist/ui/UpdateLine.d.ts +1 -1
- package/dist/ui/capabilities.js +5 -5
- package/dist/ui/composer-limits.d.ts +31 -0
- package/dist/ui/composer-limits.js +50 -0
- package/dist/ui/composer-rows.d.ts +73 -0
- package/dist/ui/composer-rows.js +201 -0
- package/dist/ui/editor-reducer.d.ts +37 -0
- package/dist/ui/editor-reducer.js +88 -12
- package/dist/ui/entries/ServiceCard.d.ts +66 -0
- package/dist/ui/entries/ServiceCard.js +116 -0
- package/dist/ui/entries/UserEntry.d.ts +17 -3
- package/dist/ui/entries/UserEntry.js +34 -5
- package/dist/ui/exit-snapshot.d.ts +9 -0
- package/dist/ui/layout/ScrollViewport.d.ts +1 -1
- package/dist/ui/layout/autocomplete.d.ts +18 -0
- package/dist/ui/layout/autocomplete.js +26 -0
- package/dist/ui/layout/budget.d.ts +34 -6
- package/dist/ui/layout/budget.js +32 -7
- package/dist/ui/layout/rail.d.ts +4 -53
- package/dist/ui/layout/rail.js +10 -62
- package/dist/ui/layout/team-panel.d.ts +18 -0
- package/dist/ui/layout/team-panel.js +22 -0
- package/dist/ui/layout/todo-layout.d.ts +24 -0
- package/dist/ui/layout/todo-layout.js +27 -0
- package/dist/ui/layout/virtual-window.js +51 -1
- package/dist/ui/overlays/HelpOverlay.js +3 -0
- package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
- package/dist/ui/overlays/QuestionOverlay.js +5 -1
- package/dist/ui/overlays/SettingsScreen.js +14 -1
- package/dist/ui/paste-frames.d.ts +87 -0
- package/dist/ui/paste-frames.js +112 -0
- package/dist/ui/paste-tokens.d.ts +90 -0
- package/dist/ui/paste-tokens.js +147 -0
- package/dist/ui/screen.d.ts +16 -0
- package/dist/ui/screen.js +45 -2
- package/dist/ui/selection/selection-controller.d.ts +1 -1
- package/dist/ui/transcript-text.js +55 -2
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/package.json +2 -2
- package/dist/input/stdin-mouse-filter.d.ts +0 -47
- package/dist/input/stdin-mouse-filter.js +0 -153
|
@@ -17,7 +17,8 @@ import { attemptTailRelief, reliefSummary, tailBudgetTokens } from './tail-budge
|
|
|
17
17
|
import { buildDigest, isPriorSummaryBlock } from './digest.js';
|
|
18
18
|
import { buildAnchor, buildCompactedBlock, countProtectedPrefix, readGeneration, truncationBody, } from './summary-prompt.js';
|
|
19
19
|
import { accumulateUsage, errorText, runSummarizeCall } from './summarize-call.js';
|
|
20
|
-
import {
|
|
20
|
+
import { computePressure, shouldCompactAt } from './pressure.js';
|
|
21
|
+
import { ContextMeter } from './meter.js';
|
|
21
22
|
export class Compactor {
|
|
22
23
|
deps;
|
|
23
24
|
log = getLogger().child('compaction');
|
|
@@ -26,6 +27,15 @@ export class Compactor {
|
|
|
26
27
|
lastCompactionTurn = -Infinity;
|
|
27
28
|
// --- Per-session state ---
|
|
28
29
|
compactions = 0;
|
|
30
|
+
/**
|
|
31
|
+
* How many `compact()` calls returned WITHOUT announcing (quiet-noop D-9).
|
|
32
|
+
*
|
|
33
|
+
* A SESSION TOTAL, LIKE `compactions`, and reset by neither `onRunStart()` nor
|
|
34
|
+
* `clearSelfDisable()`: `/compact on` overrides a guard, it does not rewrite
|
|
35
|
+
* history. `/compact status` is its only reader, and it is the whole of what
|
|
36
|
+
* keeps a decline reportable once the transcript stops carrying one.
|
|
37
|
+
*/
|
|
38
|
+
declined = 0;
|
|
29
39
|
tokensReclaimed = 0;
|
|
30
40
|
sessionUsage = { inputTokens: 0, outputTokens: 0 };
|
|
31
41
|
pricingUnknown = false;
|
|
@@ -35,18 +45,15 @@ export class Compactor {
|
|
|
35
45
|
selfDisabledReason;
|
|
36
46
|
pendingManual = null;
|
|
37
47
|
generation = 0;
|
|
38
|
-
/** Calibration for every ESTIMATED figure (D-23). Recomputed on every turn_end. */
|
|
39
|
-
estimateOffset;
|
|
40
|
-
/** The last pressure computed, so the status bar and `/compact status` agree. */
|
|
41
|
-
lastPressure = null;
|
|
42
48
|
/**
|
|
43
|
-
* The
|
|
49
|
+
* The fallback meter, built on first use when no host injected one (R-1).
|
|
44
50
|
*
|
|
45
|
-
* `
|
|
46
|
-
*
|
|
47
|
-
*
|
|
51
|
+
* `estimateOffset`, `measuredPrefixLength` and `lastPressure` used to be
|
|
52
|
+
* FIELDS HERE. They moved to `ContextMeter` wholesale
|
|
53
|
+
* (context-usage-gauge-accuracy W1), because keeping them here is what put the
|
|
54
|
+
* only measurement in the process behind `compaction.enabled`.
|
|
48
55
|
*/
|
|
49
|
-
|
|
56
|
+
ownMeter = null;
|
|
50
57
|
/**
|
|
51
58
|
* The messages the last splice dropped, held ONLY until the wiring collects
|
|
52
59
|
* them for the archive (W4 / RV-3).
|
|
@@ -78,28 +85,34 @@ export class Compactor {
|
|
|
78
85
|
this.lastCompactionTurn = -Infinity;
|
|
79
86
|
}
|
|
80
87
|
/**
|
|
81
|
-
* `turn_end`
|
|
88
|
+
* `turn_end` - recompute the calibration offset (D-23).
|
|
89
|
+
*
|
|
90
|
+
* A THIN FORWARDER since context-usage-gauge-accuracy W1. The meter subscribes
|
|
91
|
+
* to `turn_end` itself, so BOTH PATHS MUST BE IDEMPOTENT: this one recomputes
|
|
92
|
+
* exactly what the other one did from the same inputs, which it is.
|
|
82
93
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
94
|
+
* `messages` / `systemPrompt` stay in the signature and are ignored: the meter
|
|
95
|
+
* reads them from its own lazy deps, and every existing caller and test keeps
|
|
96
|
+
* compiling.
|
|
85
97
|
*/
|
|
86
|
-
onTurnEnd(usage,
|
|
87
|
-
this.
|
|
88
|
-
// THE SAME ARRAY `computeEstimateOffset` JUST MEASURED, so the two numbers
|
|
89
|
-
// describe the same request by construction (hardening §3.2.3).
|
|
90
|
-
this.measuredPrefixLength = messages.length;
|
|
98
|
+
onTurnEnd(usage, _messages, _systemPrompt) {
|
|
99
|
+
this.meter().onTurnEnd(usage);
|
|
91
100
|
}
|
|
92
|
-
|
|
93
|
-
*
|
|
101
|
+
/*
|
|
102
|
+
* `invalidateMeasurement()` IS GONE, DELIBERATELY (I-9 / RV-2 round two).
|
|
103
|
+
*
|
|
104
|
+
* It had exactly two production callers and their reset depths are now
|
|
105
|
+
* OPPOSITE: `CompactionWiring.onHistoryReplaced` (a `/clear`, `/reset` or
|
|
106
|
+
* `/resume`) must drop `estimateOffset`, while `settlePending`'s applied
|
|
107
|
+
* branch (a splice) must KEEP it - the toolset and the model are unchanged, so
|
|
108
|
+
* the calibration still holds and dropping it makes the gauge fall too far and
|
|
109
|
+
* climb back (I-5 / I-8). The two used to share a method only because the one
|
|
110
|
+
* field it cleared happened to be their intersection; that intersection no
|
|
111
|
+
* longer exists. Both call sites now name the depth they mean, on the meter.
|
|
94
112
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* array. `estimateAppendedTokens` bounds-checks as well, so a MISSED call here
|
|
98
|
-
* costs accuracy - the number degrades to round 1's - and never correctness.
|
|
113
|
+
* Reinstating a forwarder here would hide "which caller gets which depth"
|
|
114
|
+
* behind a name that mentions neither, and BOTH wrong choices are silent.
|
|
99
115
|
*/
|
|
100
|
-
invalidateMeasurement() {
|
|
101
|
-
this.measuredPrefixLength = undefined;
|
|
102
|
-
}
|
|
103
116
|
/**
|
|
104
117
|
* The dropped messages, released as they are returned (W4 / RV-3).
|
|
105
118
|
*
|
|
@@ -163,17 +176,13 @@ export class Compactor {
|
|
|
163
176
|
const config = this.deps.getConfig().compaction;
|
|
164
177
|
if (!config.enabled)
|
|
165
178
|
return false;
|
|
166
|
-
// THE
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
// The
|
|
171
|
-
//
|
|
172
|
-
const pressure = this.
|
|
173
|
-
...(probe.lastUsage ? { lastUsage: probe.lastUsage } : {}),
|
|
174
|
-
messages: this.deps.getMessages(),
|
|
175
|
-
systemPrompt: this.deps.getSystemPrompt(),
|
|
176
|
-
});
|
|
179
|
+
// THE METER READS THE LIVE HISTORY ITSELF (W1), so the estimate branch can no
|
|
180
|
+
// longer be handed an empty array by a caller that forgot - which used to be
|
|
181
|
+
// a 0 % reading on precisely the session that most needs a trigger, the
|
|
182
|
+
// resumed 180 k-token one where `lastUsage` is `undefined` by definition.
|
|
183
|
+
// The probe's own usage is still passed, because it is the authority for
|
|
184
|
+
// THIS checkpoint.
|
|
185
|
+
const pressure = this.meter().measureWith(probe.lastUsage);
|
|
177
186
|
this.deps.onPressure?.(pressure);
|
|
178
187
|
const manual = this.pendingManual !== null;
|
|
179
188
|
// Guard 2 — the per-run cap. BOTH TRIGGERS, HARD. It is the bound that makes
|
|
@@ -218,22 +227,19 @@ export class Compactor {
|
|
|
218
227
|
const uiTrigger = manual ? 'manual' : ctx.trigger;
|
|
219
228
|
const config = this.deps.getConfig().compaction;
|
|
220
229
|
const summarizer = this.resolveSummarizer();
|
|
230
|
+
// GUARDS 1 AND 2 COUNT *ATTEMPTS*, NOT ANNOUNCEMENTS, and they stay here
|
|
231
|
+
// (quiet-noop §3.3). A decline still consumed a checkpoint: it still has to
|
|
232
|
+
// serve the cooldown and it still has to be charged to the per-run cap, or a
|
|
233
|
+
// history that can never be compacted would re-attempt on every turn for the
|
|
234
|
+
// whole run. `inFlight` stays for the same reason - the chip reports what the
|
|
235
|
+
// compactor is DOING, not what it has decided to say.
|
|
221
236
|
this.compactionsThisRun += 1;
|
|
222
237
|
this.lastCompactionTurn = ctx.turnIndex;
|
|
223
|
-
this.compactions += 1;
|
|
224
238
|
this.inFlight = true;
|
|
225
|
-
const index = this.compactions;
|
|
226
|
-
this.deps.emit({
|
|
227
|
-
type: 'compaction_start',
|
|
228
|
-
index,
|
|
229
|
-
trigger: uiTrigger,
|
|
230
|
-
model: summarizer?.ref.modelId ?? '',
|
|
231
|
-
});
|
|
232
239
|
const started = Date.now();
|
|
233
240
|
try {
|
|
234
241
|
return await this.runCompaction({
|
|
235
242
|
ctx,
|
|
236
|
-
index,
|
|
237
243
|
uiTrigger,
|
|
238
244
|
config,
|
|
239
245
|
summarizer,
|
|
@@ -250,8 +256,56 @@ export class Compactor {
|
|
|
250
256
|
// =========================================================================
|
|
251
257
|
// Internals
|
|
252
258
|
// =========================================================================
|
|
259
|
+
/**
|
|
260
|
+
* Open the card (quiet-noop §3.2).
|
|
261
|
+
*
|
|
262
|
+
* THE ONLY EMITTER OF `compaction_start`, AND THE ONLY PLACE `this.compactions`
|
|
263
|
+
* ADVANCES. A compaction that never reaches an announce point is a DECLINE: it
|
|
264
|
+
* changed nothing, called nothing and spent nothing, and it is deliberately
|
|
265
|
+
* indistinguishable - on every human surface except `/compact status`'s
|
|
266
|
+
* `declined` count (D-9) - from a turn at which `shouldCompact` returned false.
|
|
267
|
+
* `finish` is the other half of that rule.
|
|
268
|
+
*
|
|
269
|
+
* IT RETURNS THE ANNOUNCEMENT RATHER THAN STORING IT (P1-1), and the caller
|
|
270
|
+
* keeps it in a LOCAL for the lifetime of one `runCompaction` call - the same
|
|
271
|
+
* slot `index` occupied before this change. An instance field would have needed
|
|
272
|
+
* a no-re-entrancy argument, and the obvious one is WRONG: guard 1 lives in
|
|
273
|
+
* `shouldCompact`, while the idle `/compact` path calls `compact()` straight
|
|
274
|
+
* through `wiring.compactNow()` without consulting it. What actually keeps two
|
|
275
|
+
* compactions apart is that `command.ts` QUEUES instead of calling `compactNow`
|
|
276
|
+
* while the loop runs - a property of a different file, which is exactly why
|
|
277
|
+
* this one must not depend on it. A clobbered field would leave `finish` unable
|
|
278
|
+
* to settle the card it opened, and a card that never settles pins
|
|
279
|
+
* `Transcript`'s monotonic boundary for the rest of the session.
|
|
280
|
+
*/
|
|
281
|
+
announce(trigger, summarizer) {
|
|
282
|
+
this.compactions += 1;
|
|
283
|
+
this.deps.emit({
|
|
284
|
+
type: 'compaction_start',
|
|
285
|
+
index: this.compactions,
|
|
286
|
+
trigger,
|
|
287
|
+
model: summarizer?.ref.modelId ?? '',
|
|
288
|
+
});
|
|
289
|
+
return { index: this.compactions };
|
|
290
|
+
}
|
|
253
291
|
async runCompaction(args) {
|
|
254
292
|
const { ctx, config, summarizer } = args;
|
|
293
|
+
// THE ANNOUNCEMENT IS A LOCAL, NEVER A FIELD (quiet-noop §3.2 / P1-1), and
|
|
294
|
+
// `??=` is what makes the eager path and the two commit points idempotent
|
|
295
|
+
// with respect to each other.
|
|
296
|
+
let announced = null;
|
|
297
|
+
const announce = (model) => {
|
|
298
|
+
announced ??= this.announce(args.uiTrigger, model);
|
|
299
|
+
};
|
|
300
|
+
// A REQUESTED compaction and a REACTIVE one announce BEFORE they know the
|
|
301
|
+
// answer, for two different reasons. `manual` is a user instruction, and its
|
|
302
|
+
// QUEUED form (`/compact` typed mid-run) has NO other reporting surface - a
|
|
303
|
+
// silent decline there would be a command that did nothing and said nothing.
|
|
304
|
+
// `overflow` runs after the provider has ALREADY refused the request; if it
|
|
305
|
+
// cannot recover, the card is the only place that says why the run is about
|
|
306
|
+
// to die. Both paths stay byte-identical to the round before this one.
|
|
307
|
+
if (args.uiTrigger !== 'pressure')
|
|
308
|
+
announce(summarizer);
|
|
255
309
|
const before = ctx.messages;
|
|
256
310
|
const protectedPrefix = countProtectedPrefix(before);
|
|
257
311
|
// THE ONE LEVER THE GUARDS DO NOT HAVE (D-24). A tail that alone exceeds the
|
|
@@ -271,6 +325,12 @@ export class Compactor {
|
|
|
271
325
|
// `plan === null` branch tries it before conceding.
|
|
272
326
|
const reliefOnly = this.relieveInPlace(before, protectedPrefix, ctx.systemPrompt);
|
|
273
327
|
if (reliefOnly) {
|
|
328
|
+
// COMMIT POINT A (quiet-noop §3.4). Relief changes the history the run
|
|
329
|
+
// continues from - a bounded, announced data loss - so it is announced
|
|
330
|
+
// even though no model was called. `null` summarizer for the same reason
|
|
331
|
+
// `finish` writes `model: ''` on a relief-only record: naming a model here
|
|
332
|
+
// would claim a call that never ran.
|
|
333
|
+
announce(null);
|
|
274
334
|
this.checkProgress(ctx, args.uiTrigger, reliefOnly.messages);
|
|
275
335
|
return this.finish({ ...args, tailRelief: reliefSummary(reliefOnly) }, {
|
|
276
336
|
action: 'replace',
|
|
@@ -282,17 +342,51 @@ export class Compactor {
|
|
|
282
342
|
mode: 'truncated',
|
|
283
343
|
messages: reliefOnly.messages,
|
|
284
344
|
reason: 'tail_relief_only',
|
|
285
|
-
}, { droppedMessages: 0 }, keepRecentTurns);
|
|
345
|
+
}, { droppedMessages: 0 }, keepRecentTurns, announced);
|
|
286
346
|
}
|
|
287
347
|
this.noteNoProgress(args.uiTrigger, 'nothing_to_drop');
|
|
288
|
-
return this.finish(args, { action: 'keep', reason: 'nothing_to_drop' }, null, keepRecentTurns);
|
|
348
|
+
return this.finish(args, { action: 'keep', reason: 'nothing_to_drop' }, null, keepRecentTurns, announced);
|
|
289
349
|
}
|
|
290
350
|
const anchor = buildAnchor(before);
|
|
291
351
|
if (!anchor) {
|
|
292
352
|
// A history with no `user` message at all cannot have a goal anchor, and
|
|
293
353
|
// D-6 makes the anchor non-negotiable. Refusing is the honest outcome.
|
|
294
|
-
|
|
354
|
+
//
|
|
355
|
+
// STRUCTURALLY UNREACHABLE, AND KEPT ANYWAY (quiet-noop E-23 / §3.4). This
|
|
356
|
+
// is reached only after `plan !== null`, and `planCompaction` returns a plan
|
|
357
|
+
// only when some cut leaves `countTurns >= keep` behind it - `countTurns`
|
|
358
|
+
// counts `user`-role messages and `keep` is at least 1, so a plan PROVES the
|
|
359
|
+
// one thing `buildAnchor` looks for. A provably dead guard is worth strictly
|
|
360
|
+
// more than the non-null assertion that would replace it. It stays ABOVE the
|
|
361
|
+
// commit point on purpose: if the plan/anchor relationship ever changes, the
|
|
362
|
+
// reason that appears is one decided before any work began, and silence is
|
|
363
|
+
// what it should get - but any new PRE-COMMIT decline must then either
|
|
364
|
+
// charge guard 4 (as `nothing_to_drop` does) or be proven unreachable in
|
|
365
|
+
// turn, because guard 4 is what bounds how long a silent decline can repeat.
|
|
366
|
+
return this.finish(args, { action: 'keep', reason: 'no_anchor' }, null, keepRecentTurns, announced);
|
|
295
367
|
}
|
|
368
|
+
// COMMIT POINT B (quiet-noop §3.4). Everything below this line either spends
|
|
369
|
+
// money (`runLadder`), rewrites the history, or faults - and all three are the
|
|
370
|
+
// user's business.
|
|
371
|
+
//
|
|
372
|
+
// BEFORE `buildDigest`, NOT AFTER. A throw inside the digest renderer reaches
|
|
373
|
+
// the engine as `manager_threw`, and the engine settles the card from the
|
|
374
|
+
// verdict alone; with no card open, that fault would be invisible on every
|
|
375
|
+
// human surface. Announcing first costs one event on a path that is about to
|
|
376
|
+
// spend seconds and dollars.
|
|
377
|
+
//
|
|
378
|
+
// WHAT IT STILL DOES NOT COVER (recorded, not closed - quiet-noop §12 / IF-2).
|
|
379
|
+
// A throw in the SYNCHRONOUS region ABOVE this line - `countProtectedPrefix`,
|
|
380
|
+
// `planCompaction`, `relieveInPlace`, `buildAnchor` - also reaches the engine
|
|
381
|
+
// as `manager_threw`, and on `pressure` that fault now has no card, charges no
|
|
382
|
+
// guard (`noteNoProgress` is never reached) and does not advance `declined`
|
|
383
|
+
// (`finish` is never called) - so a host bug there would repeat at every
|
|
384
|
+
// checkpoint for a whole run in silence. `manager_timeout` cannot land there:
|
|
385
|
+
// that region holds no `await`, which is the property AC-Q12 pins. Closing it
|
|
386
|
+
// means hoisting the announcement out of this function, which is precisely
|
|
387
|
+
// what P1-1 decided against - so it is written down rather than fixed under
|
|
388
|
+
// another change's name.
|
|
389
|
+
announce(summarizer);
|
|
296
390
|
const head = before.slice(protectedPrefix, plan.cutIndex);
|
|
297
391
|
const priorBlock = before.slice(0, protectedPrefix).find((m) => isPriorSummaryBlock(m));
|
|
298
392
|
// The prior block is carried into the DIGEST rather than the tail, so the
|
|
@@ -312,14 +406,14 @@ export class Compactor {
|
|
|
312
406
|
...(args.instructions ? { instructions: args.instructions } : {}),
|
|
313
407
|
});
|
|
314
408
|
if (ladder.kind === 'aborted') {
|
|
315
|
-
return this.finish(args, { action: 'keep', reason: 'aborted' }, null, keepRecentTurns);
|
|
409
|
+
return this.finish(args, { action: 'keep', reason: 'aborted' }, null, keepRecentTurns, announced);
|
|
316
410
|
}
|
|
317
411
|
if (ladder.kind === 'failed' && config.onFailure === 'stop') {
|
|
318
412
|
// Rung 4. The run proceeds and will probably hit `context_overflow`, which
|
|
319
413
|
// is what the user asked for by setting this.
|
|
320
414
|
this.deps.notify('error', `Context compaction failed (${ladder.reason}) and compaction.onFailure is "stop". ` +
|
|
321
415
|
'The next request may exceed the context window. Try /compact again, or /clear, or /reset.');
|
|
322
|
-
return this.finish(args, { action: 'keep', reason: `summarize_failed: ${ladder.reason}` }, null, keepRecentTurns);
|
|
416
|
+
return this.finish(args, { action: 'keep', reason: `summarize_failed: ${ladder.reason}` }, null, keepRecentTurns, announced);
|
|
323
417
|
}
|
|
324
418
|
const truncated = ladder.kind === 'failed';
|
|
325
419
|
const mode = truncated ? 'truncated' : 'summarized';
|
|
@@ -365,7 +459,7 @@ export class Compactor {
|
|
|
365
459
|
messages,
|
|
366
460
|
...(truncated ? {} : { summary: ladder.text }),
|
|
367
461
|
...(truncated ? { reason: `truncated: ${ladder.reason}` } : {}),
|
|
368
|
-
}, { droppedMessages: plan.droppedMessages, summary: truncated ? undefined : ladder.text }, keepRecentTurns);
|
|
462
|
+
}, { droppedMessages: plan.droppedMessages, summary: truncated ? undefined : ladder.text }, keepRecentTurns, announced);
|
|
369
463
|
}
|
|
370
464
|
/** Tail relief against this session's window (hardening §3.3.3 / W2). */
|
|
371
465
|
relieveInPlace(messages, from, systemPrompt) {
|
|
@@ -474,12 +568,17 @@ export class Compactor {
|
|
|
474
568
|
checkProgress(ctx, uiTrigger, after) {
|
|
475
569
|
const config = this.deps.getConfig().compaction;
|
|
476
570
|
const info = this.modelInfo();
|
|
571
|
+
// THE OFFSET NOW LIVES ON THE METER (W1), and it is read from the last
|
|
572
|
+
// PUBLISHED pressure rather than re-derived: `Pressure.estimateOffset` is
|
|
573
|
+
// carried on every measurement, so this is the same number the field used to
|
|
574
|
+
// hold, without the compactor owning a second copy of it.
|
|
575
|
+
const estimateOffset = this.meter().lastPublished()?.estimateOffset;
|
|
477
576
|
const projected = computePressure({
|
|
478
577
|
messages: after,
|
|
479
578
|
systemPrompt: ctx.systemPrompt,
|
|
480
579
|
contextWindow: info.contextWindow,
|
|
481
580
|
windowKnown: true,
|
|
482
|
-
...(
|
|
581
|
+
...(estimateOffset !== undefined ? { estimateOffset } : {}),
|
|
483
582
|
});
|
|
484
583
|
const target = config.threshold - COMPACTION_LIMITS.minReclaimRatio;
|
|
485
584
|
const madeProgress = projected.ratio < target;
|
|
@@ -522,19 +621,59 @@ export class Compactor {
|
|
|
522
621
|
`${this.deps.getConfig().compaction.keepRecentTurns} turns alone exceed the threshold. ` +
|
|
523
622
|
'Lower compaction.keepRecentTurns, or use /clear or /reset.');
|
|
524
623
|
}
|
|
525
|
-
finish(args, outcome, applied, keepRecentTurnsUsed) {
|
|
624
|
+
finish(args, outcome, applied, keepRecentTurnsUsed, announced) {
|
|
526
625
|
const before = args.ctx.messages.length;
|
|
527
626
|
const after = outcome.action === 'replace' ? outcome.messages.length : before;
|
|
528
|
-
|
|
627
|
+
// THE OCCUPANCY BEFORE THE SPLICE (I-10). `lastMeasured()` is the pure cache
|
|
628
|
+
// read on purpose; anything that re-measures here reports 0 reclaimed.
|
|
629
|
+
const tokensBefore = this.lastMeasured()?.occupied ?? 0;
|
|
630
|
+
// `'relieved'` IS A CLI-ONLY WORD (RV-5 / DH-20). The port reports
|
|
631
|
+
// `'truncated'` for a relief-only compaction, which is accurate for the
|
|
632
|
+
// engine's narrower question; the card's `summarized N messages with
|
|
633
|
+
// <model>` line would otherwise render "summarized 0 messages with
|
|
634
|
+
// claude-haiku-4-5" for a compaction in which no model was called.
|
|
635
|
+
const mode = reliefOnly(outcome)
|
|
636
|
+
? 'relieved'
|
|
637
|
+
: outcome.action === 'replace'
|
|
638
|
+
? outcome.mode
|
|
639
|
+
: 'none';
|
|
640
|
+
// NO MODEL ON A RELIEF-ONLY RECORD, because no model was called. The card
|
|
641
|
+
// names the summarizer from this field, and naming one here would be the
|
|
642
|
+
// same claim `mode: 'relieved'` exists to stop making.
|
|
643
|
+
const model = reliefOnly(outcome) ? '' : args.summarizer?.ref.modelId ?? '';
|
|
644
|
+
const durationMs = Date.now() - args.started;
|
|
645
|
+
// THE ON-DISK RECORD OF EVERY ATTEMPT IS UNCHANGED (quiet-noop §9). This
|
|
646
|
+
// feature makes the TRANSCRIPT quiet, not the logs: a declined compaction is
|
|
647
|
+
// still one `compaction` line here, still one `compaction_start` /
|
|
648
|
+
// `compaction_end` pair from CORE in the JSONL sink, and still reflected in
|
|
649
|
+
// `/compact status`.
|
|
650
|
+
this.log.info('compaction', {
|
|
651
|
+
trigger: args.uiTrigger,
|
|
652
|
+
announced: announced !== null,
|
|
653
|
+
before,
|
|
654
|
+
after,
|
|
655
|
+
droppedMessages: applied?.droppedMessages ?? 0,
|
|
656
|
+
mode,
|
|
657
|
+
model,
|
|
658
|
+
durationMs,
|
|
659
|
+
});
|
|
660
|
+
// A SILENT DECLINE (quiet-noop §3.1). No card was opened, so there is nothing
|
|
661
|
+
// to settle and nothing to say IN THE TRANSCRIPT. Returning here is what makes
|
|
662
|
+
// a declined checkpoint indistinguishable from one at which `shouldCompact`
|
|
663
|
+
// said no.
|
|
664
|
+
//
|
|
665
|
+
// `declined` IS THE ONE THING THAT STILL SPEAKS (D-9). Quiet is the goal;
|
|
666
|
+
// invisible is not, and `/compact status` is a published guaranteed surface
|
|
667
|
+
// (`context-auto-compaction` §6.5). One integer here is what keeps that
|
|
668
|
+
// promise true while the card goes away.
|
|
669
|
+
if (!announced) {
|
|
670
|
+
this.declined += 1;
|
|
671
|
+
return outcome;
|
|
672
|
+
}
|
|
529
673
|
const record = {
|
|
530
|
-
index:
|
|
674
|
+
index: announced.index,
|
|
531
675
|
trigger: args.uiTrigger,
|
|
532
|
-
|
|
533
|
-
// `'truncated'` for a relief-only compaction, which is accurate for the
|
|
534
|
-
// engine's narrower question; the card's `summarized N messages with
|
|
535
|
-
// <model>` line would otherwise render "summarized 0 messages with
|
|
536
|
-
// claude-haiku-4-5" for a compaction in which no model was called.
|
|
537
|
-
mode: reliefOnly(outcome) ? 'relieved' : outcome.action === 'replace' ? outcome.mode : 'none',
|
|
676
|
+
mode,
|
|
538
677
|
// OPTIMISTIC AND CORRECTED BY THE WIRING (§3.2.1). Only the ENGINE knows
|
|
539
678
|
// whether the splice passed the structural gate, and it says so on
|
|
540
679
|
// `compaction_end`; the wiring rewrites this before the card settles.
|
|
@@ -546,23 +685,11 @@ export class Compactor {
|
|
|
546
685
|
tokensBefore,
|
|
547
686
|
tokensAfter: 0, // filled by the wiring from the engine's own estimator
|
|
548
687
|
...(applied?.summary ? { summary: applied.summary } : {}),
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
// same claim `mode: 'relieved'` exists to stop making.
|
|
552
|
-
model: reliefOnly(outcome) ? '' : args.summarizer?.ref.modelId ?? '',
|
|
553
|
-
durationMs: Date.now() - args.started,
|
|
688
|
+
model,
|
|
689
|
+
durationMs,
|
|
554
690
|
...(args.ctx.trigger === 'overflow' ? { keepRecentTurnsUsed } : {}),
|
|
555
691
|
...(args.tailRelief ? { tailRelief: args.tailRelief } : {}),
|
|
556
692
|
};
|
|
557
|
-
this.log.info('compaction', {
|
|
558
|
-
trigger: args.uiTrigger,
|
|
559
|
-
before,
|
|
560
|
-
after,
|
|
561
|
-
droppedMessages: applied?.droppedMessages ?? 0,
|
|
562
|
-
mode: record.mode,
|
|
563
|
-
model: record.model,
|
|
564
|
-
durationMs: record.durationMs,
|
|
565
|
-
});
|
|
566
693
|
this.deps.emit({ type: 'compaction_end', record });
|
|
567
694
|
return outcome;
|
|
568
695
|
}
|
|
@@ -570,32 +697,30 @@ export class Compactor {
|
|
|
570
697
|
// Reporting helpers
|
|
571
698
|
// =========================================================================
|
|
572
699
|
/**
|
|
573
|
-
* The pressure the trigger and the gauge both read.
|
|
700
|
+
* The pressure the trigger and the gauge both read - MEASURED NOW.
|
|
574
701
|
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
702
|
+
* A FORWARDER since W1: the decision point supplies its own authoritative
|
|
703
|
+
* `lastUsage` (the probe's), and the meter owns everything else. `messages` /
|
|
704
|
+
* `systemPrompt` stay in the signature and are ignored, because the meter
|
|
705
|
+
* reads the LIVE history through its own deps - which is strictly better than
|
|
706
|
+
* a caller that could pass `[]` and get a 0 % reading (the old default here).
|
|
577
707
|
*/
|
|
578
708
|
measure(probe) {
|
|
579
|
-
|
|
580
|
-
const pressure = computePressure({
|
|
581
|
-
...(probe.lastUsage ? { lastUsage: probe.lastUsage } : {}),
|
|
582
|
-
messages: probe.messages ?? [],
|
|
583
|
-
systemPrompt: probe.systemPrompt ?? '',
|
|
584
|
-
contextWindow: info.contextWindow,
|
|
585
|
-
windowKnown: this.deps.isPricedModel({
|
|
586
|
-
providerId: this.mainRef().providerId,
|
|
587
|
-
modelId: this.mainRef().modelId,
|
|
588
|
-
}),
|
|
589
|
-
...(this.estimateOffset !== undefined ? { estimateOffset: this.estimateOffset } : {}),
|
|
590
|
-
...(this.measuredPrefixLength !== undefined
|
|
591
|
-
? { measuredPrefixLength: this.measuredPrefixLength }
|
|
592
|
-
: {}),
|
|
593
|
-
});
|
|
594
|
-
this.lastPressure = pressure;
|
|
595
|
-
return pressure;
|
|
709
|
+
return this.meter().measureWith(probe.lastUsage);
|
|
596
710
|
}
|
|
711
|
+
/**
|
|
712
|
+
* The last pressure PUBLISHED. NEVER re-measured - `lastPublished`, not
|
|
713
|
+
* `current` (I-10 / RV-3).
|
|
714
|
+
*
|
|
715
|
+
* `finish()` below reads this for `tokensBefore`, which is the sole upstream of
|
|
716
|
+
* `tokensReclaimed`, the card's "reclaimed N tokens" and `/compact status`'s
|
|
717
|
+
* session total. All three want the occupancy BEFORE the splice. Point this at
|
|
718
|
+
* `meter.current()` and the moment the meter is dirty - which after a splice it
|
|
719
|
+
* always is - `tokensBefore` becomes the POST-compaction figure, `reclaimed`
|
|
720
|
+
* collapses to 0, and the card renders that with nothing logging a fault.
|
|
721
|
+
*/
|
|
597
722
|
lastMeasured() {
|
|
598
|
-
return this.
|
|
723
|
+
return this.meter().lastPublished();
|
|
599
724
|
}
|
|
600
725
|
/** Written by the wiring once the engine has reported its verdict. */
|
|
601
726
|
recordApplied(tokensBefore, tokensAfter) {
|
|
@@ -605,6 +730,7 @@ export class Compactor {
|
|
|
605
730
|
sessionTotals() {
|
|
606
731
|
return {
|
|
607
732
|
compactions: this.compactions,
|
|
733
|
+
declined: this.declined,
|
|
608
734
|
tokensReclaimed: this.tokensReclaimed,
|
|
609
735
|
usage: this.sessionUsage,
|
|
610
736
|
pricingUnknown: this.pricingUnknown,
|
|
@@ -647,6 +773,29 @@ export class Compactor {
|
|
|
647
773
|
return null;
|
|
648
774
|
return { ref, fromFastTier: false };
|
|
649
775
|
}
|
|
776
|
+
/**
|
|
777
|
+
* The meter this compactor reads, injected or private (R-1).
|
|
778
|
+
*
|
|
779
|
+
* MEMOIZED, because a fresh meter on every call would have no `estimateOffset`
|
|
780
|
+
* and no cached pressure - `lastMeasured()` would answer `null` forever and
|
|
781
|
+
* `tokensReclaimed` would sit at 0. The fallback exists so the ten existing
|
|
782
|
+
* `compaction-*.test.ts` construction sites compile unchanged; production
|
|
783
|
+
* injects, and `context-gauge-wiring.test.ts` asserts the single instance.
|
|
784
|
+
*/
|
|
785
|
+
meter() {
|
|
786
|
+
if (this.deps.meter)
|
|
787
|
+
return this.deps.meter;
|
|
788
|
+
if (!this.ownMeter) {
|
|
789
|
+
this.ownMeter = new ContextMeter({
|
|
790
|
+
getMessages: () => this.deps.getMessages(),
|
|
791
|
+
getSystemPrompt: () => this.deps.getSystemPrompt(),
|
|
792
|
+
getModelInfo: () => this.modelInfo(),
|
|
793
|
+
isWindowKnown: () => this.deps.isPricedModel(this.mainRef()),
|
|
794
|
+
getWindowOverride: () => this.deps.getConfig().contextWindow,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
return this.ownMeter;
|
|
798
|
+
}
|
|
650
799
|
mainRef() {
|
|
651
800
|
const config = this.deps.getConfig();
|
|
652
801
|
return {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/context` slash command (context-usage-gauge-accuracy §3.7 / W6).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope, and this
|
|
5
|
+
* text lands in a transcript `notice` - produced outside `src/ui/**`, where no
|
|
6
|
+
* terminal capabilities are available, so it must be safe on a legacy `cmd.exe`.
|
|
7
|
+
*
|
|
8
|
+
* IT LIVES HERE RATHER THAN IN `builtins.ts` FOR THE REASON `/compact` DOES
|
|
9
|
+
* (C-16 / P1-12): `CLAUDE.md` caps a source file at 1000 lines and `builtins.ts`
|
|
10
|
+
* is already past it. `builtins.ts` gets the registration and nothing else.
|
|
11
|
+
*
|
|
12
|
+
* THIS IS THE GUARANTEED REPORTING SURFACE FOR OCCUPANCY. The status bar drops
|
|
13
|
+
* readouts on a narrow terminal, the gauge is a bar and a rounded percentage,
|
|
14
|
+
* and neither can say WHERE the denominator came from or WHAT the session total
|
|
15
|
+
* includes. Every question a user asks after distrusting the bar is answered
|
|
16
|
+
* here, at any width, in any mode - which is also why the honesty rules below
|
|
17
|
+
* are not decoration.
|
|
18
|
+
*/
|
|
19
|
+
import type { CommandContext } from '../commands/registry.js';
|
|
20
|
+
/**
|
|
21
|
+
* `/context` - occupancy, denominator, freshness, compaction, session spend.
|
|
22
|
+
*
|
|
23
|
+
* THE OCCUPANCY COMES FROM `controller.getContextUsage()` AND NOWHERE ELSE
|
|
24
|
+
* (RV-8). The obvious alternative, `getCompactionSnapshot().pressure`, returns
|
|
25
|
+
* `offCompactionSnapshot()`'s HARDCODED ZERO pressure whenever compaction was
|
|
26
|
+
* never registered - so a report built on it would read `0%` for exactly the
|
|
27
|
+
* sessions this whole feature exists to fix. `/compact status` gets away with
|
|
28
|
+
* reading it only because it returns early on that branch and never prints an
|
|
29
|
+
* occupancy line at all.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatContextReport(ctx: CommandContext): string;
|
|
32
|
+
/** `/context` takes no arguments; anything after it is ignored. */
|
|
33
|
+
export declare function runContextCommand(ctx: CommandContext): void;
|