@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
package/dist/agent/reducer.js
CHANGED
|
@@ -11,11 +11,25 @@
|
|
|
11
11
|
* synthesizes a generic error notice (a swallowed throw is never a no-op).
|
|
12
12
|
*/
|
|
13
13
|
import process from 'node:process';
|
|
14
|
-
import {
|
|
15
|
-
import { computeCost } from './usage.js';
|
|
14
|
+
import { emptyContextUsage } from '../compaction/meter.js';
|
|
15
|
+
import { addUsage, computeCost } from './usage.js';
|
|
16
16
|
import { ENTRY_LIMITS, appendBounded, entryRetain, trimEntries } from './entry-limits.js';
|
|
17
17
|
/** Default auto-dismiss window for a toast. */
|
|
18
18
|
export const DEFAULT_TOAST_TTL_MS = 2500;
|
|
19
|
+
/**
|
|
20
|
+
* The zero session total.
|
|
21
|
+
*
|
|
22
|
+
* ONE LITERAL FOR TWO SITES (`initialViewState` and `resetConversation`). They
|
|
23
|
+
* used to spell the object out twice, which is how `cacheReadTokens` would have
|
|
24
|
+
* been added to one of them and not the other.
|
|
25
|
+
*/
|
|
26
|
+
const EMPTY_USAGE_TOTAL = {
|
|
27
|
+
inputTokens: 0,
|
|
28
|
+
outputTokens: 0,
|
|
29
|
+
cacheReadTokens: 0,
|
|
30
|
+
cacheWriteTokens: 0,
|
|
31
|
+
costUsd: 0,
|
|
32
|
+
};
|
|
19
33
|
/**
|
|
20
34
|
* The initial view state, optionally seeded from config.
|
|
21
35
|
*
|
|
@@ -33,8 +47,8 @@ export function initialViewState(seed = {}) {
|
|
|
33
47
|
return {
|
|
34
48
|
entries: [],
|
|
35
49
|
status: 'idle',
|
|
36
|
-
usageTotal:
|
|
37
|
-
|
|
50
|
+
usageTotal: EMPTY_USAGE_TOTAL,
|
|
51
|
+
context: emptyContextUsage(),
|
|
38
52
|
overlay: null,
|
|
39
53
|
thinkingVisible: seed.thinkingVisible ?? false,
|
|
40
54
|
toasts: [],
|
|
@@ -48,7 +62,6 @@ export function initialViewState(seed = {}) {
|
|
|
48
62
|
retry: null,
|
|
49
63
|
fast: null,
|
|
50
64
|
compaction: null,
|
|
51
|
-
contextTokensEstimated: false,
|
|
52
65
|
seq: 0,
|
|
53
66
|
teamEntryId: undefined,
|
|
54
67
|
todoEntryId: undefined,
|
|
@@ -610,22 +623,12 @@ export function viewReducer(state, action) {
|
|
|
610
623
|
...state,
|
|
611
624
|
streamingId: undefined,
|
|
612
625
|
turnProduced: true,
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
|
|
619
|
-
// number from the one the user is watching.
|
|
620
|
-
contextTokens: occupiedTokens(action.usage),
|
|
621
|
-
// A MEASUREMENT SUPERSEDES A DERIVATION (§6.2). `compactionEnd` writes an
|
|
622
|
-
// estimate so the bar falls immediately; this is where it stops being one.
|
|
623
|
-
contextTokensEstimated: false,
|
|
624
|
-
usageTotal: {
|
|
625
|
-
inputTokens: state.usageTotal.inputTokens + action.usage.inputTokens,
|
|
626
|
-
outputTokens: state.usageTotal.outputTokens + action.usage.outputTokens,
|
|
627
|
-
costUsd: state.usageTotal.costUsd + action.costDelta,
|
|
628
|
-
},
|
|
626
|
+
// `contextTokens` IS NOT WRITTEN HERE ANY MORE (I-1). The occupancy this
|
|
627
|
+
// turn produced reaches the view through `ContextMeter`, which subscribes
|
|
628
|
+
// to the same `turn_end` and publishes the measured pressure - window,
|
|
629
|
+
// percentage and approximation markers included, none of which this
|
|
630
|
+
// branch ever had. Two branches writing one number is what P0-1 was.
|
|
631
|
+
usageTotal: addUsage(state.usageTotal, action.usage, action.costDelta),
|
|
629
632
|
// The turn produced an answer, so whatever it had to retry, it recovered.
|
|
630
633
|
...settleRetryCard(state, entries, 'recovered'),
|
|
631
634
|
};
|
|
@@ -763,8 +766,13 @@ export function viewReducer(state, action) {
|
|
|
763
766
|
droppedEntries: 0,
|
|
764
767
|
streamingId: undefined,
|
|
765
768
|
expandedToolIds: {},
|
|
766
|
-
|
|
767
|
-
|
|
769
|
+
// `context` IS DELIBERATELY UNTOUCHED (I-1). `builtins.ts` calls
|
|
770
|
+
// `controller.clearMessages()` BEFORE dispatching this, and that marks
|
|
771
|
+
// the meter dirty and schedules a publication; zeroing here would make
|
|
772
|
+
// the final state `0` rather than the meter's real answer - which for an
|
|
773
|
+
// emptied conversation is the system prompt's own couple of percent, not
|
|
774
|
+
// nothing. `usageTotal` DOES reset: that is session spend, not occupancy.
|
|
775
|
+
usageTotal: EMPTY_USAGE_TOTAL,
|
|
768
776
|
teamEntryId: undefined,
|
|
769
777
|
// `/reset` clears `messages`, so the belief is gone and the list goes
|
|
770
778
|
// with it — the other half of I-2. `controller.clearMessages()` clears
|
|
@@ -774,10 +782,6 @@ export function viewReducer(state, action) {
|
|
|
774
782
|
retryEntryId: undefined,
|
|
775
783
|
retry: null,
|
|
776
784
|
compactionEntryId: undefined,
|
|
777
|
-
// `contextTokens` is zeroed above, so the flag that says it is a
|
|
778
|
-
// DERIVATION has to go with it — otherwise a fresh conversation renders
|
|
779
|
-
// `~0%` and the tilde is a lie about a number that is simply zero.
|
|
780
|
-
contextTokensEstimated: false,
|
|
781
785
|
};
|
|
782
786
|
case 'setOverlay':
|
|
783
787
|
return { ...state, overlay: action.overlay };
|
|
@@ -860,18 +864,11 @@ export function viewReducer(state, action) {
|
|
|
860
864
|
}
|
|
861
865
|
case 'teamUsage':
|
|
862
866
|
// `usageTotal` AND NOTHING ELSE (§3.9 / AC-15). In particular NOT
|
|
863
|
-
// `
|
|
867
|
+
// `context`: that gauge shows the LEAD's context occupancy against
|
|
864
868
|
// the model's window, and folding five children into it would read 180%
|
|
865
869
|
// on a perfectly healthy session. Child spend is real money and has to
|
|
866
870
|
// appear in the cost readout; it is not the lead's context.
|
|
867
|
-
return {
|
|
868
|
-
...state,
|
|
869
|
-
usageTotal: {
|
|
870
|
-
inputTokens: state.usageTotal.inputTokens + action.usage.inputTokens,
|
|
871
|
-
outputTokens: state.usageTotal.outputTokens + action.usage.outputTokens,
|
|
872
|
-
costUsd: state.usageTotal.costUsd + action.costDelta,
|
|
873
|
-
},
|
|
874
|
-
};
|
|
871
|
+
return { ...state, usageTotal: addUsage(state.usageTotal, action.usage, action.costDelta) };
|
|
875
872
|
case 'teamEnd': {
|
|
876
873
|
const { outcome } = action;
|
|
877
874
|
const entries = state.teamEntryId
|
|
@@ -991,17 +988,10 @@ export function viewReducer(state, action) {
|
|
|
991
988
|
}
|
|
992
989
|
case 'fastUsage':
|
|
993
990
|
// `usageTotal` AND NOTHING ELSE, exactly as `teamUsage` (§3.6 / AC-18). In
|
|
994
|
-
// particular NOT `
|
|
991
|
+
// particular NOT `context`: that gauge shows the LEAD's context
|
|
995
992
|
// occupancy against the lead model's window, and a review is a separate
|
|
996
993
|
// conversation with a different model entirely.
|
|
997
|
-
return {
|
|
998
|
-
...state,
|
|
999
|
-
usageTotal: {
|
|
1000
|
-
inputTokens: state.usageTotal.inputTokens + action.usage.inputTokens,
|
|
1001
|
-
outputTokens: state.usageTotal.outputTokens + action.usage.outputTokens,
|
|
1002
|
-
costUsd: state.usageTotal.costUsd + action.costDelta,
|
|
1003
|
-
},
|
|
1004
|
-
};
|
|
994
|
+
return { ...state, usageTotal: addUsage(state.usageTotal, action.usage, action.costDelta) };
|
|
1005
995
|
case 'fastTier':
|
|
1006
996
|
return { ...state, fast: action.snapshot };
|
|
1007
997
|
// --- Context compaction (context-auto-compaction §5.5) ----------------
|
|
@@ -1128,40 +1118,41 @@ export function viewReducer(state, action) {
|
|
|
1128
1118
|
}
|
|
1129
1119
|
case 'compactionUsage':
|
|
1130
1120
|
// `usageTotal` AND NOTHING ELSE, exactly as `teamUsage` and `fastUsage`. In
|
|
1131
|
-
// particular NOT `
|
|
1121
|
+
// particular NOT `context`: that gauge shows the LEAD's context
|
|
1132
1122
|
// occupancy against the lead model's window, and a summarization is a
|
|
1133
1123
|
// separate conversation with a possibly different model entirely.
|
|
1134
|
-
return {
|
|
1135
|
-
...state,
|
|
1136
|
-
usageTotal: {
|
|
1137
|
-
inputTokens: state.usageTotal.inputTokens + action.usage.inputTokens,
|
|
1138
|
-
outputTokens: state.usageTotal.outputTokens + action.usage.outputTokens,
|
|
1139
|
-
costUsd: state.usageTotal.costUsd + action.costDelta,
|
|
1140
|
-
},
|
|
1141
|
-
};
|
|
1124
|
+
return { ...state, usageTotal: addUsage(state.usageTotal, action.usage, action.costDelta) };
|
|
1142
1125
|
case 'compactionSnapshot':
|
|
1143
1126
|
return { ...state, compaction: action.snapshot };
|
|
1144
1127
|
/**
|
|
1145
|
-
* THE
|
|
1128
|
+
* THE ONE WRITER OF `state.context` (I-1).
|
|
1146
1129
|
*
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1151
|
-
* the
|
|
1130
|
+
* THE IDENTITY SHORT-CIRCUIT BELOW IS LOAD-BEARING, NOT A MICRO-OPTIMISATION
|
|
1131
|
+
* (§5.2 / R-3). The meter publishes on a 400 ms tick throughout a turn and
|
|
1132
|
+
* most of those ticks measure the same number - the history did not change
|
|
1133
|
+
* between two tool calls of the same size, or the percentage rounded to the
|
|
1134
|
+
* same integer. Returning the SAME state object lets React skip the whole
|
|
1135
|
+
* subtree; returning a fresh one would make a permanently visible row into a
|
|
1136
|
+
* 2.5 Hz re-render source and walk the render governor up its ladder for no
|
|
1137
|
+
* visible change at all.
|
|
1152
1138
|
*
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1155
|
-
* never counts them, so writing the raw figure would make the bar fall too
|
|
1156
|
-
* far and then visibly climb again on the next `turn_end` for no reason the
|
|
1157
|
-
* user can see.
|
|
1139
|
+
* `ContextUsageSnapshot` carries no timestamp for exactly this reason: a
|
|
1140
|
+
* field that moves every tick would make this comparison never hold.
|
|
1158
1141
|
*/
|
|
1159
|
-
case '
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1142
|
+
case 'contextUsage': {
|
|
1143
|
+
const prev = state.context;
|
|
1144
|
+
const next = action.snapshot;
|
|
1145
|
+
if (prev.occupied === next.occupied &&
|
|
1146
|
+
prev.window === next.window &&
|
|
1147
|
+
prev.pct === next.pct &&
|
|
1148
|
+
prev.source === next.source &&
|
|
1149
|
+
prev.deltaTokens === next.deltaTokens &&
|
|
1150
|
+
prev.windowKnown === next.windowKnown &&
|
|
1151
|
+
prev.windowOverridden === next.windowOverridden) {
|
|
1152
|
+
return state;
|
|
1153
|
+
}
|
|
1154
|
+
return { ...state, context: next };
|
|
1155
|
+
}
|
|
1165
1156
|
// --- API retry (llm-api-retry-backoff §6.4) ---------------------------
|
|
1166
1157
|
case 'retryScheduled': {
|
|
1167
1158
|
const snapshot = {
|
package/dist/agent/usage.d.ts
CHANGED
|
@@ -3,8 +3,42 @@
|
|
|
3
3
|
* (which is expressed in USD per 1M tokens). See spec §6.2 status bar.
|
|
4
4
|
*/
|
|
5
5
|
import type { ModelCost, TokenUsage } from '@aragon-agent/core';
|
|
6
|
+
import type { UsageTotal } from './reducer.js';
|
|
6
7
|
/** Compute the USD cost of a single `TokenUsage` given a model's cost table. */
|
|
7
8
|
export declare function computeCost(usage: TokenUsage, cost?: ModelCost): number;
|
|
9
|
+
/**
|
|
10
|
+
* Fold one `TokenUsage` into a running session total
|
|
11
|
+
* (context-usage-gauge-accuracy §3.6 / W5).
|
|
12
|
+
*
|
|
13
|
+
* THE ONE ADDITION SITE, and collapsing four hand-written copies into it is the
|
|
14
|
+
* point rather than a tidy-up. The reducer had the same three lines in four
|
|
15
|
+
* branches - `turnEnd`, `teamUsage`, `fastUsage`, `compactionUsage` - and all
|
|
16
|
+
* four silently omitted the cache terms, so the status bar's `^` read LOWER than
|
|
17
|
+
* the `$` beside it on any session behind a caching gateway. A fifth source
|
|
18
|
+
* added later would have made the identical omission; now it cannot.
|
|
19
|
+
*
|
|
20
|
+
* `costDelta` IS PRECOMPUTED BY THE CALLER, exactly as the four branches already
|
|
21
|
+
* required: the summarizer and the fast tier may be different models with
|
|
22
|
+
* different price tables, and a reducer that priced its own would have to know
|
|
23
|
+
* about three of them.
|
|
24
|
+
*/
|
|
25
|
+
export declare function addUsage(total: UsageTotal, usage: TokenUsage, costDelta: number): UsageTotal;
|
|
26
|
+
/**
|
|
27
|
+
* The number the status bar's `^` shows: the PROMPT side of the session total.
|
|
28
|
+
*
|
|
29
|
+
* DELIBERATELY THE SAME TERMS AS `occupiedTokens` MINUS `outputTokens`
|
|
30
|
+
* (`compaction/pressure.ts`). Anthropic reports `input_tokens` EXCLUDING cached
|
|
31
|
+
* tokens and puts them in two separate fields, so `inputTokens` alone
|
|
32
|
+
* under-reports for anyone behind a caching gateway - while `computeCost` above
|
|
33
|
+
* has always priced all three. That is the "three units on one row" defect
|
|
34
|
+
* (P1-3): the percentage counted cache, the dollars counted cache, and the arrow
|
|
35
|
+
* did not.
|
|
36
|
+
*
|
|
37
|
+
* `$` IS STILL NOT THE SAME TOTAL, and the distinction is worth keeping straight
|
|
38
|
+
* (RV-14): `computeCost` also prices OUTPUT tokens. What this function asserts is
|
|
39
|
+
* narrower and exact - both cache terms are counted, on both sides.
|
|
40
|
+
*/
|
|
41
|
+
export declare function promptTokensOf(total: UsageTotal): number;
|
|
8
42
|
/** Format a USD amount for the status bar (`$0.0000`, or `$1.23` when large). */
|
|
9
43
|
export declare function formatCost(usd: number): string;
|
|
10
44
|
/** Format a token count compactly (`1.2k`, `3.4M`). */
|
package/dist/agent/usage.js
CHANGED
|
@@ -12,6 +12,49 @@ export function computeCost(usage, cost) {
|
|
|
12
12
|
perM(usage.cacheReadTokens ?? 0, cost.cacheRead) +
|
|
13
13
|
perM(usage.cacheWriteTokens ?? 0, cost.cacheWrite));
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Fold one `TokenUsage` into a running session total
|
|
17
|
+
* (context-usage-gauge-accuracy §3.6 / W5).
|
|
18
|
+
*
|
|
19
|
+
* THE ONE ADDITION SITE, and collapsing four hand-written copies into it is the
|
|
20
|
+
* point rather than a tidy-up. The reducer had the same three lines in four
|
|
21
|
+
* branches - `turnEnd`, `teamUsage`, `fastUsage`, `compactionUsage` - and all
|
|
22
|
+
* four silently omitted the cache terms, so the status bar's `^` read LOWER than
|
|
23
|
+
* the `$` beside it on any session behind a caching gateway. A fifth source
|
|
24
|
+
* added later would have made the identical omission; now it cannot.
|
|
25
|
+
*
|
|
26
|
+
* `costDelta` IS PRECOMPUTED BY THE CALLER, exactly as the four branches already
|
|
27
|
+
* required: the summarizer and the fast tier may be different models with
|
|
28
|
+
* different price tables, and a reducer that priced its own would have to know
|
|
29
|
+
* about three of them.
|
|
30
|
+
*/
|
|
31
|
+
export function addUsage(total, usage, costDelta) {
|
|
32
|
+
return {
|
|
33
|
+
inputTokens: total.inputTokens + usage.inputTokens,
|
|
34
|
+
outputTokens: total.outputTokens + usage.outputTokens,
|
|
35
|
+
cacheReadTokens: total.cacheReadTokens + (usage.cacheReadTokens ?? 0),
|
|
36
|
+
cacheWriteTokens: total.cacheWriteTokens + (usage.cacheWriteTokens ?? 0),
|
|
37
|
+
costUsd: total.costUsd + costDelta,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The number the status bar's `^` shows: the PROMPT side of the session total.
|
|
42
|
+
*
|
|
43
|
+
* DELIBERATELY THE SAME TERMS AS `occupiedTokens` MINUS `outputTokens`
|
|
44
|
+
* (`compaction/pressure.ts`). Anthropic reports `input_tokens` EXCLUDING cached
|
|
45
|
+
* tokens and puts them in two separate fields, so `inputTokens` alone
|
|
46
|
+
* under-reports for anyone behind a caching gateway - while `computeCost` above
|
|
47
|
+
* has always priced all three. That is the "three units on one row" defect
|
|
48
|
+
* (P1-3): the percentage counted cache, the dollars counted cache, and the arrow
|
|
49
|
+
* did not.
|
|
50
|
+
*
|
|
51
|
+
* `$` IS STILL NOT THE SAME TOTAL, and the distinction is worth keeping straight
|
|
52
|
+
* (RV-14): `computeCost` also prices OUTPUT tokens. What this function asserts is
|
|
53
|
+
* narrower and exact - both cache terms are counted, on both sides.
|
|
54
|
+
*/
|
|
55
|
+
export function promptTokensOf(total) {
|
|
56
|
+
return total.inputTokens + total.cacheReadTokens + total.cacheWriteTokens;
|
|
57
|
+
}
|
|
15
58
|
/** Format a USD amount for the status bar (`$0.0000`, or `$1.23` when large). */
|
|
16
59
|
export function formatCost(usd) {
|
|
17
60
|
if (usd <= 0)
|
package/dist/cli.js
CHANGED
|
@@ -27,7 +27,7 @@ import { COMPACTION_CONFIG_SET_KEYS, FAST_CONFIG_SET_KEYS, LOG_CONFIG_SET_KEYS,
|
|
|
27
27
|
import { attachAgentEvents, attachTeamEvents, installLogging, setScreenRestore, setSignalTerminator, } from './logging/install.js';
|
|
28
28
|
import { getLogger } from './logging/logger.js';
|
|
29
29
|
import { runLogsCommand } from './logging/cli-commands.js';
|
|
30
|
-
import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampMaxRenderInterval, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAdapterProvider, parseMaxTokensInput, DEFAULT_MAX_RENDER_INTERVAL_MS, DEFAULT_MAX_TOKENS, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SCROLL_RESUME_MS, DEFAULT_SKILLS_CONFIG, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from './config/schema.js';
|
|
30
|
+
import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampContextWindow, clampMaxRenderInterval, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAdapterProvider, isAutoToken, parseMaxTokensInput, DEFAULT_MAX_RENDER_INTERVAL_MS, DEFAULT_MAX_TOKENS, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SCROLL_RESUME_MS, DEFAULT_SKILLS_CONFIG, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from './config/schema.js';
|
|
31
31
|
import { makeGetApiKey } from './config/load.js';
|
|
32
32
|
import { AgentController, DENY_ALL_APPROVAL } from './agent/controller.js';
|
|
33
33
|
import { runSkillsCommand } from './skills/cli-commands.js';
|
|
@@ -45,6 +45,9 @@ import { createFrameDiffer } from './ui/frame-differ.js';
|
|
|
45
45
|
import { wrapStdoutForFrames } from './ui/stdout-frame-writer.js';
|
|
46
46
|
import { setFrameStatsProvider } from './commands/perf.js';
|
|
47
47
|
import { tryCreateStdinFilter } from './input/stdin-filter.js';
|
|
48
|
+
import { createScrollbarBridge } from './ui/scrollbar-controller.js';
|
|
49
|
+
import { createPointerRouter } from './input/pointer-router.js';
|
|
50
|
+
import { createFrameObserver } from './ui/frame-observer.js';
|
|
48
51
|
import { enterAltScreen, writeExitTranscript } from './ui/screen.js';
|
|
49
52
|
import { supportsWindowsVtInput } from './ui/win-vt-input.js';
|
|
50
53
|
import { forceWindowsVtInput } from './ui/win-vt-force.js';
|
|
@@ -153,6 +156,11 @@ function toFlags(opts) {
|
|
|
153
156
|
fastModel: opts.fastModel,
|
|
154
157
|
fastProvider: opts.fastProvider,
|
|
155
158
|
fastReview: opts.fastReview,
|
|
159
|
+
// …and again for `--fast-delegate` / `--no-fast-delegate`, one feature
|
|
160
|
+
// later — NINE features, one trap. It is `fast.delegate`'s only channel:
|
|
161
|
+
// there is no `ARAGON_FAST_DELEGATE`, so missing this line leaves a wrapper
|
|
162
|
+
// with no way at all to say "do not delegate on this run".
|
|
163
|
+
fastDelegate: opts.fastDelegate,
|
|
156
164
|
// `--verbose` is just a shorthand for `--log-level debug`; an explicit
|
|
157
165
|
// `--log-level` still wins, so the two can be combined without surprise.
|
|
158
166
|
logLevel: opts.logLevel ?? (opts.verbose ? 'debug' : undefined),
|
|
@@ -471,13 +479,14 @@ function runInteractive(flags, extras = {}) {
|
|
|
471
479
|
const disposeStdinFilter = () => filter?.dispose();
|
|
472
480
|
// --- Drag-select (tui-selection-and-scroll-follow §4.4). ------------------
|
|
473
481
|
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
// controller, the differ's `decorate` hook and the startup notice must never
|
|
477
|
-
// disagree about whether drag-select is running. With it false the session is
|
|
478
|
-
// byte-identical to a pre-feature build — no extra escape on the wire (AC-8),
|
|
479
|
-
// no controller, no `decorate`, no mirror.
|
|
482
|
+
// Selection owns its controller, decoration and notice. Button motion follows
|
|
483
|
+
// mouseOn independently so --no-mouse-select still permits scrollbar dragging.
|
|
480
484
|
const wantSelect = mouseOn && config.mouseSelect;
|
|
485
|
+
let mouseCaptured = mouseOn;
|
|
486
|
+
const scrollbar = createScrollbarBridge(() => mouseCaptured);
|
|
487
|
+
const pointerRouter = mouseOn && filter ? createPointerRouter({
|
|
488
|
+
source: filter.source, handle: (event) => scrollbar.controller?.handle(event) ?? false,
|
|
489
|
+
}) : null;
|
|
481
490
|
const selectionBridge = {
|
|
482
491
|
controller: null,
|
|
483
492
|
onCopied: null,
|
|
@@ -530,7 +539,13 @@ function runInteractive(flags, extras = {}) {
|
|
|
530
539
|
// than printing zeroes that read like a writer doing nothing (§5.4).
|
|
531
540
|
if (frameWriter)
|
|
532
541
|
setFrameStatsProvider(frameWriter.stats);
|
|
542
|
+
const frameObserver = mode === 'fullscreen' ? createFrameObserver({
|
|
543
|
+
stdout: frameWriter?.stdout ?? process.stdout, terminal: process.stdout, scrollbar,
|
|
544
|
+
}) : null;
|
|
533
545
|
const disposeFrameWriter = () => {
|
|
546
|
+
frameObserver?.dispose();
|
|
547
|
+
pointerRouter?.dispose();
|
|
548
|
+
scrollbar.controller?.dispose();
|
|
534
549
|
if (!frameWriter)
|
|
535
550
|
return;
|
|
536
551
|
setFrameStatsProvider(null);
|
|
@@ -569,7 +584,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
569
584
|
// keep their terminal's own selection.
|
|
570
585
|
screen = enterAltScreen(process.stdout, {
|
|
571
586
|
mouse: mouseOn,
|
|
572
|
-
motion:
|
|
587
|
+
motion: mouseOn,
|
|
573
588
|
bracketedPaste: pasteOn && mode === 'fullscreen',
|
|
574
589
|
});
|
|
575
590
|
const restore = () => screen?.restore();
|
|
@@ -632,7 +647,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
632
647
|
// attempted. Naming the mechanism honestly is the entire reason that return
|
|
633
648
|
// value exists (R-7), and a door that lies is worse than no door.
|
|
634
649
|
const writeForeign = frameWriter
|
|
635
|
-
? (text) => frameWriter.writeForeign(text)
|
|
650
|
+
? (text) => { frameObserver?.invalidate(); frameWriter.writeForeign(text); }
|
|
636
651
|
: process.stdout.isTTY
|
|
637
652
|
? (text) => {
|
|
638
653
|
try {
|
|
@@ -654,10 +669,9 @@ function runInteractive(flags, extras = {}) {
|
|
|
654
669
|
// than inside `screen.ts`'s closure because `App` needs to READ it and the
|
|
655
670
|
// handle's own flag is private (it has to be — `restore()` reads it, and
|
|
656
671
|
// exposing a setter would be a second way to get the two out of step).
|
|
657
|
-
let mouseCaptured = mouseOn;
|
|
658
672
|
if (wantSelect && filter) {
|
|
659
673
|
selectionBridge.controller = createSelectionController({
|
|
660
|
-
source: filter.source,
|
|
674
|
+
source: pointerRouter?.selectionSource ?? filter.source,
|
|
661
675
|
// The frame writer is absent under `--no-diff-render`, where there is no
|
|
662
676
|
// cache to repaint from at all; the controller then falls back to asking
|
|
663
677
|
// `App` for a React redraw on every drag flush, which is slower and
|
|
@@ -697,11 +711,14 @@ function runInteractive(flags, extras = {}) {
|
|
|
697
711
|
const terminalBridge = mode === 'fullscreen' || writeForeign
|
|
698
712
|
? {
|
|
699
713
|
mouseSelect: wantSelect,
|
|
714
|
+
...(mode === 'fullscreen' ? { scrollbar } : {}),
|
|
700
715
|
...(mouseOn
|
|
701
716
|
? {
|
|
702
717
|
setMouseCapture: (on) => {
|
|
703
718
|
screen?.setMouseCapture(on);
|
|
704
719
|
mouseCaptured = on;
|
|
720
|
+
if (!on)
|
|
721
|
+
scrollbar.controller?.cancel();
|
|
705
722
|
// A released mouse cannot be dragging, and a selection left
|
|
706
723
|
// painted over a screen the terminal is now selecting on its
|
|
707
724
|
// own would be two highlights claiming the same rows.
|
|
@@ -719,41 +736,67 @@ function runInteractive(flags, extras = {}) {
|
|
|
719
736
|
: undefined;
|
|
720
737
|
const updateBridge = { service: null, onAttach: null };
|
|
721
738
|
let updateService = null;
|
|
739
|
+
let interactiveClosed = false;
|
|
740
|
+
const failInteractive = (error) => {
|
|
741
|
+
// Ink consumes render exceptions; retain failure even if a disposer throws.
|
|
742
|
+
process.exitCode = 1;
|
|
743
|
+
interactiveClosed = true;
|
|
744
|
+
logger.error('cli', 'render_failed', { error: String(error) });
|
|
745
|
+
for (const cleanup of [
|
|
746
|
+
() => screen?.restore(), disposeSelection, disposeStdinFilter, disposeFrameWriter,
|
|
747
|
+
() => controller.abort(), () => controller.dispose(), () => updateService?.dispose(),
|
|
748
|
+
() => { process.stdin.pause(); process.stdin.unref?.(); },
|
|
749
|
+
]) {
|
|
750
|
+
try {
|
|
751
|
+
cleanup();
|
|
752
|
+
}
|
|
753
|
+
catch (cleanupError) {
|
|
754
|
+
logger.error('cli', 'render_cleanup_failed', { error: String(cleanupError) });
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
};
|
|
722
758
|
const updateEligible = !!process.stdout.isTTY &&
|
|
723
759
|
!!process.stdin.isTTY &&
|
|
724
760
|
!process.env.CI &&
|
|
725
761
|
config.update.mode !== 'off';
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
762
|
+
let instance;
|
|
763
|
+
try {
|
|
764
|
+
instance = render(_jsx(App, { controller: controller, version: VERSION, mode: mode, confirmBridge: confirmBridge, humanInputBridge: humanInputBridge, initialPrompt: extras.initialPrompt, initialOverlay: extras.initialOverlay,
|
|
765
|
+
// `mouseOn`, NOT the handle (I-11). `App.tsx:186-188` documents this
|
|
766
|
+
// prop's contract as "absent under `--no-mouse`, inline, or non-TTY", and
|
|
767
|
+
// it feeds `enabled: !!mouseSource` on a wheel subscription that would
|
|
768
|
+
// otherwise be a channel no event can ever arrive on.
|
|
769
|
+
mouseSource: mouseOn && filter ? filter.source : undefined,
|
|
770
|
+
// Present ONLY when this console swallows `CSI Z` and mouse reports.
|
|
771
|
+
// Passed in rather than probed inside `App` so that every test renders
|
|
772
|
+
// the same tree on every machine — reading `process.versions.node` from
|
|
773
|
+
// a component would make the notice appear or vanish according to which
|
|
774
|
+
// Node the developer happened to have installed.
|
|
775
|
+
vtInputWarning: vtInputSupported ? undefined : { nodeVersion: process.versions.node }, updateBridge: updateEligible ? updateBridge : undefined,
|
|
776
|
+
// Absent when nothing can refuse a paste, so a `--no-paste` tree is
|
|
777
|
+
// byte-identical to a build without this feature.
|
|
778
|
+
pasteBridge: pasteOn ? pasteBridge : undefined, terminal: terminalBridge }),
|
|
779
|
+
// Full-screen owns console.* itself (I-4): Ink's patchConsole writes
|
|
780
|
+
// straight to stdout and permanently shifts the fixed frame's accounting.
|
|
781
|
+
{
|
|
782
|
+
exitOnCtrlC: false,
|
|
783
|
+
patchConsole: mode === 'inline',
|
|
784
|
+
// I-5: with mouse support off the REAL stdin is handed over unwrapped, so
|
|
785
|
+
// every existing path is provably unchanged.
|
|
786
|
+
stdin: filter?.stdin ?? process.stdin,
|
|
787
|
+
// SPREAD, not `frameWriter?.stdout ?? process.stdout`. This call passed no
|
|
788
|
+
// `stdout` at all before, and Ink defaults it to `process.stdout`; omitting
|
|
789
|
+
// the key entirely when the writer is off keeps the options object the same
|
|
790
|
+
// SHAPE it has always had, which is what makes AC-5 / AC-6 ("byte-identical
|
|
791
|
+
// with --no-diff-render, inline mode and `aragon -p`") a statement about
|
|
792
|
+
// the code rather than about Ink's defaulting.
|
|
793
|
+
...(frameObserver ? { stdout: frameObserver.stdout } : {}),
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
failInteractive(error);
|
|
798
|
+
throw error;
|
|
799
|
+
}
|
|
757
800
|
// H1's primary disarm, and this is the earliest moment at which "this build
|
|
758
801
|
// STARTS" is proven: the module graph loaded, config resolved, Ink mounted
|
|
759
802
|
// (cli-auto-update-hardening section 5.1.4). It is what makes a long session
|
|
@@ -775,6 +818,8 @@ function runInteractive(flags, extras = {}) {
|
|
|
775
818
|
// must degrade to "no updater", never to a crashed TUI.
|
|
776
819
|
void import('./update/service.js')
|
|
777
820
|
.then(({ UpdateService }) => {
|
|
821
|
+
if (interactiveClosed)
|
|
822
|
+
return;
|
|
778
823
|
const service = new UpdateService({ config: config.update, currentVersion: VERSION });
|
|
779
824
|
updateService = service;
|
|
780
825
|
updateBridge.service = service;
|
|
@@ -805,6 +850,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
805
850
|
void instance
|
|
806
851
|
.waitUntilExit()
|
|
807
852
|
.then(() => {
|
|
853
|
+
interactiveClosed = true;
|
|
808
854
|
screen?.restore();
|
|
809
855
|
// BEFORE `disposeStdinFilter()`, which clears the listener set it subscribed to,
|
|
810
856
|
// and after `restore()` for the reason that call records: the terminal must
|
|
@@ -833,18 +879,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
833
879
|
updateService?.dispose();
|
|
834
880
|
replayTranscript();
|
|
835
881
|
})
|
|
836
|
-
.catch(
|
|
837
|
-
screen?.restore();
|
|
838
|
-
// BEFORE `disposeStdinFilter()`, which clears the listener set it subscribed to,
|
|
839
|
-
// and after `restore()` for the reason that call records: the terminal must
|
|
840
|
-
// stop reporting first, and anything already in flight is still consumed by
|
|
841
|
-
// a live filter.
|
|
842
|
-
disposeSelection();
|
|
843
|
-
disposeStdinFilter();
|
|
844
|
-
disposeFrameWriter();
|
|
845
|
-
controller.dispose();
|
|
846
|
-
updateService?.dispose();
|
|
847
|
-
});
|
|
882
|
+
.catch(failInteractive);
|
|
848
883
|
}
|
|
849
884
|
async function runOneShot(flags, prompt, quiet) {
|
|
850
885
|
// Headless: no App is ever rendered, so there is no human to approve a skill
|
|
@@ -924,6 +959,11 @@ const CONFIG_SET_KEYS = new Set([
|
|
|
924
959
|
'baseUrl',
|
|
925
960
|
'thinkingLevel',
|
|
926
961
|
'maxTokens',
|
|
962
|
+
// context-usage-gauge-accuracy §3.6. Listed here AND given its own branch in
|
|
963
|
+
// `runConfigSet` below - a key present here but absent there falls through
|
|
964
|
+
// every case, writes nothing, and still prints `Set contextWindow = 1000000`
|
|
965
|
+
// (P1-2).
|
|
966
|
+
'contextWindow',
|
|
927
967
|
'theme',
|
|
928
968
|
'confirmTools',
|
|
929
969
|
'toolTimeoutMs',
|
|
@@ -1020,6 +1060,30 @@ function runConfigSetMaxTokens(value) {
|
|
|
1020
1060
|
const merged = updatePersistedConfig({ maxTokens: stored });
|
|
1021
1061
|
process.stdout.write(`Set maxTokens = ${merged.maxTokens ?? 'auto'}\n`);
|
|
1022
1062
|
}
|
|
1063
|
+
/**
|
|
1064
|
+
* `aragon config set contextWindow <n|auto>`.
|
|
1065
|
+
*
|
|
1066
|
+
* ITS OWN BRANCH, AHEAD OF THE SWITCH, for the reason `maxTokens` has one: it
|
|
1067
|
+
* must echo the STORED value rather than the typed one, so
|
|
1068
|
+
* `config set contextWindow 99999999` reports the clamped 5000000 instead of a
|
|
1069
|
+
* number that was never written. It also REJECTS nonsense rather than clamping
|
|
1070
|
+
* it - a non-numeric argument here is almost always a typo for a number, and
|
|
1071
|
+
* storing AUTO for `20O000` would quietly discard the user's intent.
|
|
1072
|
+
*/
|
|
1073
|
+
function runConfigSetContextWindow(value) {
|
|
1074
|
+
const trimmed = value.trim();
|
|
1075
|
+
const auto = isAutoToken(trimmed);
|
|
1076
|
+
if (!auto && !/^\d+$/.test(trimmed)) {
|
|
1077
|
+
process.stderr.write(`Invalid contextWindow "${value}". Use a number of tokens or "auto".\n`);
|
|
1078
|
+
process.exitCode = 2;
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
// `null` GOES IN UNCLAMPED (RV-12). Running AUTO through the clamp yields
|
|
1082
|
+
// 8000 and removes "auto" from the config permanently.
|
|
1083
|
+
const stored = auto ? null : clampContextWindow(trimmed, null);
|
|
1084
|
+
const merged = updatePersistedConfig({ contextWindow: stored });
|
|
1085
|
+
process.stdout.write(`Set contextWindow = ${merged.contextWindow ?? 'auto'}\n`);
|
|
1086
|
+
}
|
|
1023
1087
|
function runConfigSet(key, value) {
|
|
1024
1088
|
if (!CONFIG_SET_KEYS.has(key)) {
|
|
1025
1089
|
process.stderr.write(`Unknown config key "${key}". Known keys: ${[...CONFIG_SET_KEYS].join(', ')}.\n`);
|
|
@@ -1040,6 +1104,11 @@ function runConfigSet(key, value) {
|
|
|
1040
1104
|
runConfigSetMaxTokens(value);
|
|
1041
1105
|
return;
|
|
1042
1106
|
}
|
|
1107
|
+
// The second tri-state key, and it needs its own branch for the same reason.
|
|
1108
|
+
if (key === 'contextWindow') {
|
|
1109
|
+
runConfigSetContextWindow(value);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1043
1112
|
const teamPatch = applyTeamConfigSet(key, value);
|
|
1044
1113
|
if (teamPatch) {
|
|
1045
1114
|
// The written value is echoed from the CLAMPED result rather than from the
|
|
@@ -1491,6 +1560,14 @@ function buildProgram() {
|
|
|
1491
1560
|
.option('--fast-model <id>', 'The fast model id (implies nothing about --fast)')
|
|
1492
1561
|
.option('--fast-provider <id>', 'Provider for the fast model (default: the main provider)')
|
|
1493
1562
|
.option('--fast-review <n|off>', 'Turns between automatic fast reviews, or "off"')
|
|
1563
|
+
// BOTH FORMS, POSITIVE FIRST, and here it matters more than anywhere above:
|
|
1564
|
+
// `fast.delegate` is PERSISTED and defaults to TRUE, so declaring only
|
|
1565
|
+
// `--no-fast-delegate` would make commander default `opts.fastDelegate` to
|
|
1566
|
+
// `true` and force `delegate: true` on EVERY run that passed no flag at all
|
|
1567
|
+
// — silently overwriting a user's stored `false`. Same polarity, same trap
|
|
1568
|
+
// and same fix as `--update` / `--compaction`.
|
|
1569
|
+
.option('--fast-delegate', 'Let the fast tier take mechanical sub-steps (the default)')
|
|
1570
|
+
.option('--no-fast-delegate', 'Run every sub-step on the main model')
|
|
1494
1571
|
// BOTH FORMS, positive first, for the reason recorded seven times above —
|
|
1495
1572
|
// and this is the pair whose omission would be worst. `update.mode` is
|
|
1496
1573
|
// PERSISTED and defaults to `'auto'`, so a lone `--no-update` would make
|