@aragon-agent/cli 0.6.4 → 0.6.8
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 +37 -0
- package/README.md +163 -37
- package/dist/agent/controller.d.ts +49 -8
- package/dist/agent/controller.js +160 -48
- package/dist/agent/headless.d.ts +2 -1
- package/dist/agent/prompt-options.d.ts +15 -0
- package/dist/agent/prompt-options.js +1 -0
- package/dist/agent/reducer.d.ts +33 -19
- package/dist/agent/reducer.js +62 -71
- package/dist/agent/usage.d.ts +34 -0
- package/dist/agent/usage.js +43 -0
- package/dist/cli.js +131 -54
- package/dist/commands/builtins.js +12 -1
- package/dist/commands/registry.d.ts +2 -3
- package/dist/compaction/compactor.d.ts +60 -24
- package/dist/compaction/compactor.js +89 -61
- 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 +41 -0
- package/dist/compaction/wiring.d.ts +47 -0
- package/dist/compaction/wiring.js +88 -27
- package/dist/config/cli-commands.js +5 -0
- package/dist/config/env.js +15 -1
- package/dist/config/load.d.ts +17 -0
- package/dist/config/load.js +45 -1
- package/dist/config/schema.d.ts +44 -0
- package/dist/config/schema.js +36 -0
- package/dist/config/store.js +27 -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/interrupt-gesture.d.ts +18 -0
- package/dist/input/interrupt-gesture.js +19 -0
- package/dist/input/pointer-router.d.ts +11 -0
- package/dist/input/pointer-router.js +14 -0
- 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/todo/store.d.ts +8 -13
- package/dist/todo/store.js +13 -14
- package/dist/todo/types.d.ts +2 -1
- package/dist/ui/App.d.ts +2 -0
- package/dist/ui/App.js +233 -183
- 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 +13 -5
- package/dist/ui/Composer.js +7 -11
- package/dist/ui/PromptCaret.d.ts +11 -0
- package/dist/ui/PromptCaret.js +31 -0
- package/dist/ui/PromptInput.d.ts +12 -6
- package/dist/ui/PromptInput.js +50 -16
- package/dist/ui/StatusBar.d.ts +22 -15
- package/dist/ui/StatusBar.js +61 -16
- 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 +1 -0
- package/dist/ui/UpdateLine.d.ts +1 -1
- package/dist/ui/capabilities.js +5 -5
- package/dist/ui/composer-rows.js +50 -9
- package/dist/ui/exit-snapshot.d.ts +9 -0
- package/dist/ui/frame-differ.js +2 -19
- package/dist/ui/frame-observer.d.ts +17 -0
- package/dist/ui/frame-observer.js +81 -0
- package/dist/ui/frame-parser.d.ts +7 -0
- package/dist/ui/frame-parser.js +18 -0
- package/dist/ui/layout/AppShell.d.ts +7 -52
- package/dist/ui/layout/AppShell.js +2 -2
- package/dist/ui/layout/ScrollIndicator.d.ts +4 -3
- package/dist/ui/layout/ScrollIndicator.js +4 -4
- package/dist/ui/layout/ScrollViewport.d.ts +13 -66
- package/dist/ui/layout/ScrollViewport.js +183 -185
- package/dist/ui/layout/autocomplete.d.ts +18 -0
- package/dist/ui/layout/autocomplete.js +26 -0
- package/dist/ui/layout/budget.d.ts +3 -51
- package/dist/ui/layout/budget.js +4 -53
- package/dist/ui/layout/document-layout.d.ts +13 -0
- package/dist/ui/layout/document-layout.js +11 -0
- package/dist/ui/layout/follow-state.d.ts +1 -0
- package/dist/ui/layout/follow-state.js +1 -3
- package/dist/ui/layout/rail.d.ts +4 -53
- package/dist/ui/layout/rail.js +10 -62
- package/dist/ui/layout/scroll-indicator.d.ts +1 -1
- package/dist/ui/layout/scroll-indicator.js +3 -2
- package/dist/ui/layout/scrollbar-geometry.d.ts +17 -0
- package/dist/ui/layout/scrollbar-geometry.js +13 -0
- 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 +25 -0
- package/dist/ui/layout/todo-layout.js +28 -0
- package/dist/ui/layout/viewport-geometry.d.ts +1 -0
- package/dist/ui/layout/viewport-geometry.js +1 -0
- package/dist/ui/layout/virtual-window.d.ts +1 -0
- package/dist/ui/layout/virtual-window.js +1 -1
- package/dist/ui/overlays/HelpOverlay.js +5 -2
- package/dist/ui/screen.d.ts +1 -1
- package/dist/ui/screen.js +4 -6
- package/dist/ui/scrollbar-controller.d.ts +30 -0
- package/dist/ui/scrollbar-controller.js +124 -0
- package/dist/ui/transcript-text.js +6 -2
- package/dist/ui/use-wheel-routing.d.ts +2 -0
- package/dist/ui/use-wheel-routing.js +6 -0
- package/package.json +2 -2
|
@@ -17,6 +17,7 @@ import { getLogger } from '../logging/logger.js';
|
|
|
17
17
|
import { copyText } from '../ui/clipboard.js';
|
|
18
18
|
import { perfCommand } from './perf.js';
|
|
19
19
|
import { runCompactCommand } from '../compaction/command.js';
|
|
20
|
+
import { runContextCommand } from '../compaction/context-command.js';
|
|
20
21
|
/**
|
|
21
22
|
* `/fast status` — THE GUARANTEED REPORTING SURFACE for the tier (§4.4 / R-8).
|
|
22
23
|
*
|
|
@@ -723,7 +724,9 @@ const COMMANDS = [
|
|
|
723
724
|
// slash history already has. Putting this canned paragraph under the
|
|
724
725
|
// composer's up-arrow as well would be the same event recorded twice, in
|
|
725
726
|
// the more verbose of the two forms (AC-17).
|
|
726
|
-
ctx.submit(buildContinuationMessage(snapshot), {
|
|
727
|
+
ctx.submit(buildContinuationMessage(snapshot), {
|
|
728
|
+
userInitiated: false, todoPolicy: 'continue',
|
|
729
|
+
});
|
|
727
730
|
return;
|
|
728
731
|
}
|
|
729
732
|
ctx.notify('warn', `Unknown argument "${verb}" - use ` +
|
|
@@ -809,6 +812,14 @@ const COMMANDS = [
|
|
|
809
812
|
description: 'Context compaction: status | on | off | threshold <n> | keep <n> | <instructions>',
|
|
810
813
|
run: (ctx) => runCompactCommand(ctx),
|
|
811
814
|
},
|
|
815
|
+
{
|
|
816
|
+
// REGISTRATION ONLY, for the reason `/compact` records one entry up: this
|
|
817
|
+
// file is past the 1000-line guideline and the report has five sections.
|
|
818
|
+
// The body is in `compaction/context-command.ts`.
|
|
819
|
+
name: 'context',
|
|
820
|
+
description: 'Show context occupancy, the window it is measured against, and session spend',
|
|
821
|
+
run: (ctx) => runContextCommand(ctx),
|
|
822
|
+
},
|
|
812
823
|
{
|
|
813
824
|
name: 'expand',
|
|
814
825
|
// DELIBERATELY LEFT TOOL-ONLY, and the asymmetry with Ctrl+O is a decision
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Slash-command registry + dispatcher (spec §5.2). A leading `/` in the prompt
|
|
3
3
|
* input is routed here instead of to the agent; `//` is the literal-`/` escape.
|
|
4
4
|
*/
|
|
5
|
+
import type { SubmitMessageOptions } from '../agent/prompt-options.js';
|
|
5
6
|
import type { AgentController } from '../agent/controller.js';
|
|
6
7
|
import type { AgentMode } from '../agent/agent-mode.js';
|
|
7
8
|
import type { NoticeLevel, Overlay, ToastLevel, ViewAction, ViewState } from '../agent/reducer.js';
|
|
@@ -38,9 +39,7 @@ export interface CommandContext {
|
|
|
38
39
|
* (P1-5): `builtins.ts` reaches `submitMessage` only through here, so
|
|
39
40
|
* `/todo continue` cannot opt out of prompt history without it.
|
|
40
41
|
*/
|
|
41
|
-
submit: (text: string, opts?:
|
|
42
|
-
userInitiated?: boolean;
|
|
43
|
-
}) => void;
|
|
42
|
+
submit: (text: string, opts?: SubmitMessageOptions) => void;
|
|
44
43
|
/**
|
|
45
44
|
* The live auto-continue budget, for `/todo status`'s `(2/25 used)` clause
|
|
46
45
|
* (todo-plan-followthrough §4.3).
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { type Message, type ModelInfo, type ModelRef, type TokenUsage, type AssistantMessage, type CompactionContext, type CompactionOutcome, type CompactionProbe, type ContextManager, type LLMRequest } from '@aragon-agent/core';
|
|
13
13
|
import type { CliConfig } from '../config/schema.js';
|
|
14
14
|
import type { NoticeLevel } from '../agent/reducer.js';
|
|
15
|
+
import { ContextMeter } from './meter.js';
|
|
15
16
|
import type { CompactionEvent, Pressure } from './types.js';
|
|
16
17
|
export interface CompactorDeps {
|
|
17
18
|
/** Read LIVE on every call — the controller replaces the object on mutation. */
|
|
@@ -42,6 +43,24 @@ export interface CompactorDeps {
|
|
|
42
43
|
complete: (providerId: string, request: LLMRequest) => Promise<AssistantMessage>;
|
|
43
44
|
emit: (event: CompactionEvent) => void;
|
|
44
45
|
notify: (level: NoticeLevel, text: string) => void;
|
|
46
|
+
/**
|
|
47
|
+
* The ONE owner of occupancy measurement (context-usage-gauge-accuracy W1).
|
|
48
|
+
*
|
|
49
|
+
* THE COMPACTOR NO LONGER MEASURES. It used to hold `estimateOffset`,
|
|
50
|
+
* `measuredPrefixLength` and `lastPressure` itself, which locked the only
|
|
51
|
+
* measuring code in the process behind `compaction.enabled` - and a session
|
|
52
|
+
* that turned compaction OFF is exactly the one whose gauge then had a single
|
|
53
|
+
* sample per turn and nothing else. Reading a meter the controller owns makes
|
|
54
|
+
* "the trigger and the gauge read one number" a STRUCTURAL fact rather than a
|
|
55
|
+
* discipline (§3.3).
|
|
56
|
+
*
|
|
57
|
+
* OPTIONAL SO EVERY EXISTING CONSTRUCTION SITE COMPILES (R-1), and lazily
|
|
58
|
+
* replaced by a private one built from the deps already here. PRODUCTION MUST
|
|
59
|
+
* INJECT: `context-gauge-wiring.test.ts` asserts one instance per process
|
|
60
|
+
* (`controller.getContextMeter()` is the same object the wiring forwards), and
|
|
61
|
+
* two meters in one session means two answers to one question.
|
|
62
|
+
*/
|
|
63
|
+
meter?: ContextMeter;
|
|
45
64
|
/**
|
|
46
65
|
* Publish the occupancy this checkpoint measured (hardening §3.2.4 / DH-15).
|
|
47
66
|
*
|
|
@@ -96,18 +115,15 @@ export declare class Compactor implements ContextManager {
|
|
|
96
115
|
private selfDisabledReason;
|
|
97
116
|
private pendingManual;
|
|
98
117
|
private generation;
|
|
99
|
-
/** Calibration for every ESTIMATED figure (D-23). Recomputed on every turn_end. */
|
|
100
|
-
private estimateOffset;
|
|
101
|
-
/** The last pressure computed, so the status bar and `/compact status` agree. */
|
|
102
|
-
private lastPressure;
|
|
103
118
|
/**
|
|
104
|
-
* The
|
|
119
|
+
* The fallback meter, built on first use when no host injected one (R-1).
|
|
105
120
|
*
|
|
106
|
-
* `
|
|
107
|
-
*
|
|
108
|
-
*
|
|
121
|
+
* `estimateOffset`, `measuredPrefixLength` and `lastPressure` used to be
|
|
122
|
+
* FIELDS HERE. They moved to `ContextMeter` wholesale
|
|
123
|
+
* (context-usage-gauge-accuracy W1), because keeping them here is what put the
|
|
124
|
+
* only measurement in the process behind `compaction.enabled`.
|
|
109
125
|
*/
|
|
110
|
-
private
|
|
126
|
+
private ownMeter;
|
|
111
127
|
/**
|
|
112
128
|
* The messages the last splice dropped, held ONLY until the wiring collects
|
|
113
129
|
* them for the archive (W4 / RV-3).
|
|
@@ -129,21 +145,17 @@ export declare class Compactor implements ContextManager {
|
|
|
129
145
|
/** `agent_start` — the per-run cap resets, the session totals do not. */
|
|
130
146
|
onRunStart(): void;
|
|
131
147
|
/**
|
|
132
|
-
* `turn_end`
|
|
148
|
+
* `turn_end` - recompute the calibration offset (D-23).
|
|
133
149
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
|
|
137
|
-
onTurnEnd(usage: TokenUsage, messages: readonly Message[], systemPrompt: string): void;
|
|
138
|
-
/**
|
|
139
|
-
* Any host-side history mutation that is NOT an append (hardening §3.2.3).
|
|
150
|
+
* A THIN FORWARDER since context-usage-gauge-accuracy W1. The meter subscribes
|
|
151
|
+
* to `turn_end` itself, so BOTH PATHS MUST BE IDEMPOTENT: this one recomputes
|
|
152
|
+
* exactly what the other one did from the same inputs, which it is.
|
|
140
153
|
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* costs accuracy - the number degrades to round 1's - and never correctness.
|
|
154
|
+
* `messages` / `systemPrompt` stay in the signature and are ignored: the meter
|
|
155
|
+
* reads them from its own lazy deps, and every existing caller and test keeps
|
|
156
|
+
* compiling.
|
|
145
157
|
*/
|
|
146
|
-
|
|
158
|
+
onTurnEnd(usage: TokenUsage, _messages: readonly Message[], _systemPrompt: string): void;
|
|
147
159
|
/**
|
|
148
160
|
* The dropped messages, released as they are returned (W4 / RV-3).
|
|
149
161
|
*
|
|
@@ -255,15 +267,29 @@ export declare class Compactor implements ContextManager {
|
|
|
255
267
|
private noteNoProgress;
|
|
256
268
|
private finish;
|
|
257
269
|
/**
|
|
258
|
-
* The pressure the trigger and the gauge both read.
|
|
270
|
+
* The pressure the trigger and the gauge both read - MEASURED NOW.
|
|
259
271
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
272
|
+
* A FORWARDER since W1: the decision point supplies its own authoritative
|
|
273
|
+
* `lastUsage` (the probe's), and the meter owns everything else. `messages` /
|
|
274
|
+
* `systemPrompt` stay in the signature and are ignored, because the meter
|
|
275
|
+
* reads the LIVE history through its own deps - which is strictly better than
|
|
276
|
+
* a caller that could pass `[]` and get a 0 % reading (the old default here).
|
|
262
277
|
*/
|
|
263
278
|
measure(probe: Pick<CompactionProbe, 'lastUsage'> & {
|
|
264
279
|
messages?: readonly Message[];
|
|
265
280
|
systemPrompt?: string;
|
|
266
281
|
}): Pressure;
|
|
282
|
+
/**
|
|
283
|
+
* The last pressure PUBLISHED. NEVER re-measured - `lastPublished`, not
|
|
284
|
+
* `current` (I-10 / RV-3).
|
|
285
|
+
*
|
|
286
|
+
* `finish()` below reads this for `tokensBefore`, which is the sole upstream of
|
|
287
|
+
* `tokensReclaimed`, the card's "reclaimed N tokens" and `/compact status`'s
|
|
288
|
+
* session total. All three want the occupancy BEFORE the splice. Point this at
|
|
289
|
+
* `meter.current()` and the moment the meter is dirty - which after a splice it
|
|
290
|
+
* always is - `tokensBefore` becomes the POST-compaction figure, `reclaimed`
|
|
291
|
+
* collapses to 0, and the card renders that with nothing logging a fault.
|
|
292
|
+
*/
|
|
267
293
|
lastMeasured(): Pressure | null;
|
|
268
294
|
/** Written by the wiring once the engine has reported its verdict. */
|
|
269
295
|
recordApplied(tokensBefore: number, tokensAfter: number): void;
|
|
@@ -295,6 +321,16 @@ export declare class Compactor implements ContextManager {
|
|
|
295
321
|
* entire run, and the spend is on the card with the summarizer's own price.
|
|
296
322
|
*/
|
|
297
323
|
resolveSummarizer(): SummarizerChoice | null;
|
|
324
|
+
/**
|
|
325
|
+
* The meter this compactor reads, injected or private (R-1).
|
|
326
|
+
*
|
|
327
|
+
* MEMOIZED, because a fresh meter on every call would have no `estimateOffset`
|
|
328
|
+
* and no cached pressure - `lastMeasured()` would answer `null` forever and
|
|
329
|
+
* `tokensReclaimed` would sit at 0. The fallback exists so the ten existing
|
|
330
|
+
* `compaction-*.test.ts` construction sites compile unchanged; production
|
|
331
|
+
* injects, and `context-gauge-wiring.test.ts` asserts the single instance.
|
|
332
|
+
*/
|
|
333
|
+
private meter;
|
|
298
334
|
private mainRef;
|
|
299
335
|
private modelInfo;
|
|
300
336
|
private effectiveMaxOutputTokens;
|
|
@@ -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');
|
|
@@ -44,18 +45,15 @@ export class Compactor {
|
|
|
44
45
|
selfDisabledReason;
|
|
45
46
|
pendingManual = null;
|
|
46
47
|
generation = 0;
|
|
47
|
-
/** Calibration for every ESTIMATED figure (D-23). Recomputed on every turn_end. */
|
|
48
|
-
estimateOffset;
|
|
49
|
-
/** The last pressure computed, so the status bar and `/compact status` agree. */
|
|
50
|
-
lastPressure = null;
|
|
51
48
|
/**
|
|
52
|
-
* The
|
|
49
|
+
* The fallback meter, built on first use when no host injected one (R-1).
|
|
53
50
|
*
|
|
54
|
-
* `
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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`.
|
|
57
55
|
*/
|
|
58
|
-
|
|
56
|
+
ownMeter = null;
|
|
59
57
|
/**
|
|
60
58
|
* The messages the last splice dropped, held ONLY until the wiring collects
|
|
61
59
|
* them for the archive (W4 / RV-3).
|
|
@@ -87,28 +85,34 @@ export class Compactor {
|
|
|
87
85
|
this.lastCompactionTurn = -Infinity;
|
|
88
86
|
}
|
|
89
87
|
/**
|
|
90
|
-
* `turn_end`
|
|
88
|
+
* `turn_end` - recompute the calibration offset (D-23).
|
|
91
89
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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.
|
|
93
|
+
*
|
|
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.
|
|
94
97
|
*/
|
|
95
|
-
onTurnEnd(usage,
|
|
96
|
-
this.
|
|
97
|
-
// THE SAME ARRAY `computeEstimateOffset` JUST MEASURED, so the two numbers
|
|
98
|
-
// describe the same request by construction (hardening §3.2.3).
|
|
99
|
-
this.measuredPrefixLength = messages.length;
|
|
98
|
+
onTurnEnd(usage, _messages, _systemPrompt) {
|
|
99
|
+
this.meter().onTurnEnd(usage);
|
|
100
100
|
}
|
|
101
|
-
|
|
102
|
-
*
|
|
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.
|
|
103
112
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* array. `estimateAppendedTokens` bounds-checks as well, so a MISSED call here
|
|
107
|
-
* 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.
|
|
108
115
|
*/
|
|
109
|
-
invalidateMeasurement() {
|
|
110
|
-
this.measuredPrefixLength = undefined;
|
|
111
|
-
}
|
|
112
116
|
/**
|
|
113
117
|
* The dropped messages, released as they are returned (W4 / RV-3).
|
|
114
118
|
*
|
|
@@ -172,17 +176,13 @@ export class Compactor {
|
|
|
172
176
|
const config = this.deps.getConfig().compaction;
|
|
173
177
|
if (!config.enabled)
|
|
174
178
|
return false;
|
|
175
|
-
// THE
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
// The
|
|
180
|
-
//
|
|
181
|
-
const pressure = this.
|
|
182
|
-
...(probe.lastUsage ? { lastUsage: probe.lastUsage } : {}),
|
|
183
|
-
messages: this.deps.getMessages(),
|
|
184
|
-
systemPrompt: this.deps.getSystemPrompt(),
|
|
185
|
-
});
|
|
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);
|
|
186
186
|
this.deps.onPressure?.(pressure);
|
|
187
187
|
const manual = this.pendingManual !== null;
|
|
188
188
|
// Guard 2 — the per-run cap. BOTH TRIGGERS, HARD. It is the bound that makes
|
|
@@ -568,12 +568,17 @@ export class Compactor {
|
|
|
568
568
|
checkProgress(ctx, uiTrigger, after) {
|
|
569
569
|
const config = this.deps.getConfig().compaction;
|
|
570
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;
|
|
571
576
|
const projected = computePressure({
|
|
572
577
|
messages: after,
|
|
573
578
|
systemPrompt: ctx.systemPrompt,
|
|
574
579
|
contextWindow: info.contextWindow,
|
|
575
580
|
windowKnown: true,
|
|
576
|
-
...(
|
|
581
|
+
...(estimateOffset !== undefined ? { estimateOffset } : {}),
|
|
577
582
|
});
|
|
578
583
|
const target = config.threshold - COMPACTION_LIMITS.minReclaimRatio;
|
|
579
584
|
const madeProgress = projected.ratio < target;
|
|
@@ -619,7 +624,9 @@ export class Compactor {
|
|
|
619
624
|
finish(args, outcome, applied, keepRecentTurnsUsed, announced) {
|
|
620
625
|
const before = args.ctx.messages.length;
|
|
621
626
|
const after = outcome.action === 'replace' ? outcome.messages.length : before;
|
|
622
|
-
|
|
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;
|
|
623
630
|
// `'relieved'` IS A CLI-ONLY WORD (RV-5 / DH-20). The port reports
|
|
624
631
|
// `'truncated'` for a relief-only compaction, which is accurate for the
|
|
625
632
|
// engine's narrower question; the card's `summarized N messages with
|
|
@@ -690,32 +697,30 @@ export class Compactor {
|
|
|
690
697
|
// Reporting helpers
|
|
691
698
|
// =========================================================================
|
|
692
699
|
/**
|
|
693
|
-
* The pressure the trigger and the gauge both read.
|
|
700
|
+
* The pressure the trigger and the gauge both read - MEASURED NOW.
|
|
694
701
|
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
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).
|
|
697
707
|
*/
|
|
698
708
|
measure(probe) {
|
|
699
|
-
|
|
700
|
-
const pressure = computePressure({
|
|
701
|
-
...(probe.lastUsage ? { lastUsage: probe.lastUsage } : {}),
|
|
702
|
-
messages: probe.messages ?? [],
|
|
703
|
-
systemPrompt: probe.systemPrompt ?? '',
|
|
704
|
-
contextWindow: info.contextWindow,
|
|
705
|
-
windowKnown: this.deps.isPricedModel({
|
|
706
|
-
providerId: this.mainRef().providerId,
|
|
707
|
-
modelId: this.mainRef().modelId,
|
|
708
|
-
}),
|
|
709
|
-
...(this.estimateOffset !== undefined ? { estimateOffset: this.estimateOffset } : {}),
|
|
710
|
-
...(this.measuredPrefixLength !== undefined
|
|
711
|
-
? { measuredPrefixLength: this.measuredPrefixLength }
|
|
712
|
-
: {}),
|
|
713
|
-
});
|
|
714
|
-
this.lastPressure = pressure;
|
|
715
|
-
return pressure;
|
|
709
|
+
return this.meter().measureWith(probe.lastUsage);
|
|
716
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
|
+
*/
|
|
717
722
|
lastMeasured() {
|
|
718
|
-
return this.
|
|
723
|
+
return this.meter().lastPublished();
|
|
719
724
|
}
|
|
720
725
|
/** Written by the wiring once the engine has reported its verdict. */
|
|
721
726
|
recordApplied(tokensBefore, tokensAfter) {
|
|
@@ -768,6 +773,29 @@ export class Compactor {
|
|
|
768
773
|
return null;
|
|
769
774
|
return { ref, fromFastTier: false };
|
|
770
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
|
+
}
|
|
771
799
|
mainRef() {
|
|
772
800
|
const config = this.deps.getConfig();
|
|
773
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;
|
|
@@ -0,0 +1,166 @@
|
|
|
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 { formatCost, formatTokens, promptTokensOf } from '../agent/usage.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 function formatContextReport(ctx) {
|
|
32
|
+
const controller = ctx.controller;
|
|
33
|
+
const usage = controller.getContextUsage();
|
|
34
|
+
const lines = ['Context'];
|
|
35
|
+
lines.push(` Occupancy ${occupancyLine(usage)}`);
|
|
36
|
+
lines.push(` Window ${windowLine(usage)}`);
|
|
37
|
+
lines.push(` Since measured ${freshnessLine(usage)}`);
|
|
38
|
+
lines.push(` Compaction ${compactionLine(ctx)}`);
|
|
39
|
+
lines.push(` Session spend ${spendLine(ctx)}`);
|
|
40
|
+
// THE SENTENCE THAT MAKES THE LINE ABOVE HONEST (D-7 / N-3). The session
|
|
41
|
+
// total folds in subagents, the fast tier and compaction's own summarization
|
|
42
|
+
// calls - three other models' tokens in a figure sitting next to the lead's
|
|
43
|
+
// occupancy. Splitting it into four counters would need four units of
|
|
44
|
+
// provenance argument for one line of output; SAYING what it contains costs
|
|
45
|
+
// one line and answers the same question.
|
|
46
|
+
lines.push(' includes subagent, fast-tier and compaction spend,');
|
|
47
|
+
lines.push(' not just this conversation');
|
|
48
|
+
return lines.join('\n');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `43% 86.2k of 200k [measured 85.1k + 1.1k estimated]`
|
|
52
|
+
*
|
|
53
|
+
* THE BREAKDOWN IS THE POINT. "43%" alone cannot be checked by a user who thinks
|
|
54
|
+
* it is wrong; naming how much of it is a provider measurement and how much is
|
|
55
|
+
* our own guess about messages appended since tells them whether to distrust the
|
|
56
|
+
* number or the model.
|
|
57
|
+
*/
|
|
58
|
+
function occupancyLine(usage) {
|
|
59
|
+
const approximate = usage.source === 'estimate' || usage.deltaTokens > 0;
|
|
60
|
+
const pct = `${approximate || !usage.windowKnown ? '~' : ''}${usage.pct}%`;
|
|
61
|
+
const measured = Math.max(0, usage.occupied - usage.deltaTokens);
|
|
62
|
+
const breakdown = usage.source === 'estimate'
|
|
63
|
+
? `[estimated ${formatTokens(usage.occupied)}]`
|
|
64
|
+
: usage.deltaTokens > 0
|
|
65
|
+
? `[measured ${formatTokens(measured)} + ${formatTokens(usage.deltaTokens)} estimated]`
|
|
66
|
+
: `[measured ${formatTokens(measured)}]`;
|
|
67
|
+
return `${pct} ${formatTokens(usage.occupied)} of ${formatTokens(usage.window)} ${breakdown}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Where the denominator came from. THREE SOURCES, ALL NAMED.
|
|
71
|
+
*
|
|
72
|
+
* THE PLACEHOLDER BRANCH IS THE ONE THAT MATTERS. `buildRuntimeModel` returns a
|
|
73
|
+
* flat 128000 for any model the static table has never seen - a custom
|
|
74
|
+
* `baseUrl`, a self-hosted id, a model released last week - and before this
|
|
75
|
+
* command there was nothing that would admit the number was invented. A user
|
|
76
|
+
* reading `128000` next to their 1M-token model concludes the gauge is broken;
|
|
77
|
+
* they are right, and this line tells them which key fixes it.
|
|
78
|
+
*/
|
|
79
|
+
function windowLine(usage) {
|
|
80
|
+
if (usage.windowOverridden) {
|
|
81
|
+
return `${usage.window} from contextWindow in config.json (overrides the model table)`;
|
|
82
|
+
}
|
|
83
|
+
if (!usage.windowKnown) {
|
|
84
|
+
return (`${usage.window} PLACEHOLDER - this model is not in the table; ` +
|
|
85
|
+
'set contextWindow to correct it');
|
|
86
|
+
}
|
|
87
|
+
return `${usage.window} from the model table`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* How stale the measured half is.
|
|
91
|
+
*
|
|
92
|
+
* `deltaTokens` IS A TOKEN COUNT, NOT A MESSAGE COUNT, and this line says so in
|
|
93
|
+
* tokens for that reason: the meter estimates the appended SLICE, and turning
|
|
94
|
+
* that back into "4 messages" would be a second derivation of something nobody
|
|
95
|
+
* recorded.
|
|
96
|
+
*/
|
|
97
|
+
function freshnessLine(usage) {
|
|
98
|
+
if (usage.source === 'estimate') {
|
|
99
|
+
return 'no provider-reported usage yet - the whole figure is an estimate';
|
|
100
|
+
}
|
|
101
|
+
if (usage.deltaTokens <= 0) {
|
|
102
|
+
return 'nothing appended since the last provider-reported usage';
|
|
103
|
+
}
|
|
104
|
+
return `${formatTokens(usage.deltaTokens)} estimated tokens appended since the last provider-reported usage`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The compaction state, IN FIVE FORMS (RV-8).
|
|
108
|
+
*
|
|
109
|
+
* A SINGLE FORM WOULD BE A LIE FOUR TIMES OUT OF FIVE. The obvious version of
|
|
110
|
+
* this line - "on - triggers at 90% (amber at 75%)" - is printed unconditionally
|
|
111
|
+
* only if you forget that compaction has four other observable states, and the
|
|
112
|
+
* first of them is `aragon --no-compaction`, where that sentence promises a
|
|
113
|
+
* rescue that cannot come. A report that exists to be trusted does not get to
|
|
114
|
+
* have one branch.
|
|
115
|
+
*
|
|
116
|
+
* THE PREDICATES ARE THE ONES `formatCompactionStatus` ALREADY USES
|
|
117
|
+
* (`compaction/command.ts`), not a second set: two ladders over the same three
|
|
118
|
+
* booleans will eventually disagree, and the disagreement would be between two
|
|
119
|
+
* commands the user runs precisely because they already distrust something.
|
|
120
|
+
*/
|
|
121
|
+
function compactionLine(ctx) {
|
|
122
|
+
const controller = ctx.controller;
|
|
123
|
+
if (!controller.isCompactionRegistered()) {
|
|
124
|
+
return 'off - not registered for this session (started with --no-compaction)';
|
|
125
|
+
}
|
|
126
|
+
const snapshot = controller.getCompactionSnapshot();
|
|
127
|
+
if (snapshot.selfDisabled) {
|
|
128
|
+
const reason = snapshot.selfDisabledReason ?? 'no progress';
|
|
129
|
+
return `self-disabled (${reason}) - no compaction will run until /compact on`;
|
|
130
|
+
}
|
|
131
|
+
if (!controller.isCompactionEnabled()) {
|
|
132
|
+
return 'off for this session - /compact on re-enables it';
|
|
133
|
+
}
|
|
134
|
+
const config = controller.getCompactionConfig();
|
|
135
|
+
const thresholds = `triggers at ${Math.round(config.threshold * 100)}% ` +
|
|
136
|
+
`(amber at ${Math.round(config.warnThreshold * 100)}%)`;
|
|
137
|
+
if (!snapshot.live) {
|
|
138
|
+
return `on, but no summarizer model resolves - ${thresholds}`;
|
|
139
|
+
}
|
|
140
|
+
const count = snapshot.compactions;
|
|
141
|
+
return (`on - ${thresholds}, ${count} this session, ` +
|
|
142
|
+
`${formatTokens(snapshot.tokensReclaimed)} reclaimed`);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* `1.24M in (incl. 940k cache read, 12k cache write) / 48.2k out, $3.21`
|
|
146
|
+
*
|
|
147
|
+
* THE CACHE PARENTHETICAL IS OMITTED WHEN BOTH TERMS ARE ZERO, which is every
|
|
148
|
+
* direct connection - this CLI never sends `cache_control`, so the terms are
|
|
149
|
+
* non-zero only behind a gateway that caches on the user's behalf. Those are
|
|
150
|
+
* exactly the users for whom the pre-feature `^` under-reported (P1-3), and the
|
|
151
|
+
* clause is how they can see that it no longer does.
|
|
152
|
+
*/
|
|
153
|
+
function spendLine(ctx) {
|
|
154
|
+
const total = ctx.state.usageTotal;
|
|
155
|
+
const cached = total.cacheReadTokens + total.cacheWriteTokens;
|
|
156
|
+
const parenthetical = cached > 0
|
|
157
|
+
? ` (incl. ${formatTokens(total.cacheReadTokens)} cache read, ` +
|
|
158
|
+
`${formatTokens(total.cacheWriteTokens)} cache write)`
|
|
159
|
+
: '';
|
|
160
|
+
return (`${formatTokens(promptTokensOf(total))} in${parenthetical} / ` +
|
|
161
|
+
`${formatTokens(total.outputTokens)} out, ${formatCost(total.costUsd)}`);
|
|
162
|
+
}
|
|
163
|
+
/** `/context` takes no arguments; anything after it is ignored. */
|
|
164
|
+
export function runContextCommand(ctx) {
|
|
165
|
+
ctx.notify('info', formatContextReport(ctx));
|
|
166
|
+
}
|