@aragon-agent/cli 0.6.12 → 0.6.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +108 -31
  3. package/dist/agent/controller.d.ts +38 -2
  4. package/dist/agent/controller.js +185 -34
  5. package/dist/agent/model-profile-settings.d.ts +31 -0
  6. package/dist/agent/model-profile-settings.js +208 -0
  7. package/dist/agent/queued-messages.d.ts +12 -0
  8. package/dist/agent/queued-messages.js +18 -0
  9. package/dist/agent/reducer.d.ts +13 -1
  10. package/dist/agent/reducer.js +52 -12
  11. package/dist/boot/guard.d.ts +3 -3
  12. package/dist/boot/guard.js +3 -3
  13. package/dist/cli.js +48 -16
  14. package/dist/commands/builtins.js +66 -39
  15. package/dist/commands/queue-command.d.ts +3 -0
  16. package/dist/commands/queue-command.js +5 -0
  17. package/dist/commands/registry.d.ts +3 -0
  18. package/dist/compaction/child.d.ts +4 -2
  19. package/dist/compaction/child.js +8 -1
  20. package/dist/compaction/compactor.d.ts +7 -2
  21. package/dist/compaction/compactor.js +21 -16
  22. package/dist/compaction/context-command.js +10 -3
  23. package/dist/compaction/meter.js +2 -0
  24. package/dist/compaction/pressure.d.ts +1 -0
  25. package/dist/compaction/pressure.js +1 -0
  26. package/dist/compaction/types.d.ts +2 -0
  27. package/dist/compaction/wiring.d.ts +4 -3
  28. package/dist/compaction/wiring.js +10 -2
  29. package/dist/config/cli-commands.d.ts +2 -0
  30. package/dist/config/cli-commands.js +16 -4
  31. package/dist/config/load.d.ts +5 -2
  32. package/dist/config/load.js +23 -12
  33. package/dist/config/model-profile-resolution.d.ts +20 -0
  34. package/dist/config/model-profile-resolution.js +81 -0
  35. package/dist/config/model-profile-store.d.ts +67 -0
  36. package/dist/config/model-profile-store.js +153 -0
  37. package/dist/config/model-profiles.d.ts +44 -0
  38. package/dist/config/model-profiles.js +121 -0
  39. package/dist/config/schema.d.ts +11 -2
  40. package/dist/config/store.d.ts +11 -1
  41. package/dist/config/store.js +43 -27
  42. package/dist/fast/resolve.d.ts +2 -1
  43. package/dist/fast/resolve.js +6 -4
  44. package/dist/fast/wiring.d.ts +5 -3
  45. package/dist/fast/wiring.js +8 -4
  46. package/dist/input/stdin-filter.js +3 -2
  47. package/dist/logging/install.js +4 -2
  48. package/dist/logging/secret-registry.d.ts +2 -0
  49. package/dist/logging/secret-registry.js +18 -0
  50. package/dist/team/runtime.d.ts +2 -1
  51. package/dist/team/subagent.d.ts +3 -1
  52. package/dist/team/subagent.js +3 -1
  53. package/dist/ui/ActivityLine.d.ts +6 -70
  54. package/dist/ui/ActivityLine.js +12 -34
  55. package/dist/ui/App.d.ts +1 -0
  56. package/dist/ui/App.js +116 -215
  57. package/dist/ui/BottomStatusRow.d.ts +6 -86
  58. package/dist/ui/BottomStatusRow.js +11 -26
  59. package/dist/ui/Composer.d.ts +16 -83
  60. package/dist/ui/Composer.js +9 -100
  61. package/dist/ui/Logo.d.ts +3 -5
  62. package/dist/ui/Logo.js +4 -8
  63. package/dist/ui/PromptInput.d.ts +4 -1
  64. package/dist/ui/PromptInput.js +44 -35
  65. package/dist/ui/QueuePanel.d.ts +8 -0
  66. package/dist/ui/QueuePanel.js +9 -0
  67. package/dist/ui/QueueStatusRow.d.ts +2 -0
  68. package/dist/ui/QueueStatusRow.js +1 -1
  69. package/dist/ui/StatusBar.d.ts +13 -149
  70. package/dist/ui/StatusBar.js +27 -142
  71. package/dist/ui/capabilities.d.ts +2 -0
  72. package/dist/ui/capabilities.js +6 -0
  73. package/dist/ui/clipboard-task.d.ts +29 -0
  74. package/dist/ui/clipboard-task.js +66 -0
  75. package/dist/ui/clipboard.d.ts +29 -50
  76. package/dist/ui/clipboard.js +141 -75
  77. package/dist/ui/color-runtime.d.ts +5 -0
  78. package/dist/ui/color-runtime.js +29 -0
  79. package/dist/ui/composer-limits.d.ts +3 -4
  80. package/dist/ui/composer-limits.js +4 -9
  81. package/dist/ui/composer-rows.d.ts +1 -1
  82. package/dist/ui/composer-rows.js +24 -137
  83. package/dist/ui/editor-navigation.d.ts +45 -0
  84. package/dist/ui/editor-navigation.js +85 -0
  85. package/dist/ui/editor-reducer.d.ts +4 -0
  86. package/dist/ui/editor-reducer.js +29 -15
  87. package/dist/ui/glyphs.js +10 -8
  88. package/dist/ui/ink-runtime.d.ts +3 -0
  89. package/dist/ui/ink-runtime.js +30 -0
  90. package/dist/ui/interaction-copy.d.ts +77 -0
  91. package/dist/ui/interaction-copy.js +40 -0
  92. package/dist/ui/interaction-hints.d.ts +18 -0
  93. package/dist/ui/interaction-hints.js +92 -0
  94. package/dist/ui/layout/budget.js +1 -1
  95. package/dist/ui/layout/queue-layout.d.ts +19 -0
  96. package/dist/ui/layout/queue-layout.js +26 -0
  97. package/dist/ui/layout/status-layout.d.ts +55 -0
  98. package/dist/ui/layout/status-layout.js +110 -0
  99. package/dist/ui/layout/todo-layout.d.ts +1 -0
  100. package/dist/ui/layout/todo-layout.js +1 -1
  101. package/dist/ui/overlays/HelpOverlay.js +8 -8
  102. package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
  103. package/dist/ui/overlays/ModelProfileEditor.js +103 -0
  104. package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
  105. package/dist/ui/overlays/ModelProfilePicker.js +167 -0
  106. package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
  107. package/dist/ui/overlays/QueueOverlay.js +79 -0
  108. package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
  109. package/dist/ui/overlays/SettingsScreen.js +347 -3
  110. package/dist/ui/overlays/model-profile-state.d.ts +74 -0
  111. package/dist/ui/overlays/model-profile-state.js +119 -0
  112. package/dist/ui/render-cache.d.ts +2 -2
  113. package/dist/ui/render-cache.js +4 -3
  114. package/dist/ui/run-status-row.d.ts +7 -19
  115. package/dist/ui/run-status-row.js +2 -18
  116. package/dist/ui/selection/selection-controller.d.ts +2 -2
  117. package/dist/ui/selection/selection-controller.js +1 -0
  118. package/dist/ui/use-wheel-routing.js +2 -1
  119. package/package.json +2 -2
@@ -4,6 +4,7 @@
4
4
  * /resume /copy /terminal-setup /exit (/quit), plus the /team, /fast and
5
5
  * /todo subsystems.
6
6
  */
7
+ import { modelSettingsResultMessage } from '../agent/model-profile-settings.js';
7
8
  import { existsSync, statSync } from 'node:fs';
8
9
  import { isAbsolute, resolve } from 'node:path';
9
10
  import { resolveOutputTokens } from '@aragon-agent/core';
@@ -13,10 +14,9 @@ import { mergePendingEntries } from '../agent/queued-messages.js';
13
14
  import { TODO_FOLLOW_LIMITS } from '../todo/limits.js';
14
15
  import { buildContinuationMessage } from '../todo/follow-through.js';
15
16
  import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
16
- import { loadConfig } from '../config/load.js';
17
17
  import { loadSession, resolveSessionPath, saveSession } from '../session/persist.js';
18
18
  import { getLogger } from '../logging/logger.js';
19
- import { copyText } from '../ui/clipboard.js';
19
+ import { registerQueueCommand } from './queue-command.js';
20
20
  import { perfCommand } from './perf.js';
21
21
  import { runCompactCommand } from '../compaction/command.js';
22
22
  import { runContextCommand } from '../compaction/context-command.js';
@@ -371,8 +371,16 @@ const COMMANDS = [
371
371
  return;
372
372
  }
373
373
  const persist = (patch) => {
374
- controller.setFastConfig(patch);
375
- ctx.persistConfig({ fast: patch });
374
+ const connection = ['provider', 'model', 'baseUrl'].some((key) => key in patch);
375
+ if (!connection) {
376
+ controller.setFastConfig(patch);
377
+ ctx.persistConfig({ fast: patch });
378
+ return true;
379
+ }
380
+ const result = controller.saveModelSettingsPatch({ fast: patch });
381
+ if (!result.ok)
382
+ ctx.notify('warn', modelSettingsResultMessage(result));
383
+ return result.ok;
376
384
  };
377
385
  if (!verb || verb === 'status') {
378
386
  ctx.notify('info', formatFastStatus(controller));
@@ -380,7 +388,8 @@ const COMMANDS = [
380
388
  }
381
389
  if (verb === 'on' || verb === 'off') {
382
390
  const enabled = verb === 'on';
383
- persist({ enabled });
391
+ if (!persist({ enabled }))
392
+ return;
384
393
  if (!controller.isFastRegistered()) {
385
394
  // The honest branch, and the whole point of splitting by what was
386
395
  // decided at construction (§3.3). It is one sentence, and it keeps
@@ -414,11 +423,13 @@ const COMMANDS = [
414
423
  ctx.notify('warn', `Unknown provider "${provider}". Choose: ${ADAPTER_PROVIDERS.join(', ')}.`);
415
424
  return;
416
425
  }
417
- persist({ provider, model });
426
+ if (!persist({ provider, model }))
427
+ return;
418
428
  ctx.toast('success', `Fast model set to ${provider}:${model}.`);
419
429
  return;
420
430
  }
421
- persist({ model: raw });
431
+ if (!persist({ model: raw }))
432
+ return;
422
433
  ctx.toast('success', `Fast model set to ${raw}.`);
423
434
  return;
424
435
  }
@@ -432,7 +443,8 @@ const COMMANDS = [
432
443
  ctx.notify('warn', `Unknown provider "${raw}". Choose: ${ADAPTER_PROVIDERS.join(', ')}.`);
433
444
  return;
434
445
  }
435
- persist({ provider: raw });
446
+ if (!persist({ provider: raw }))
447
+ return;
436
448
  ctx.toast('success', `Fast provider set to ${raw}.`);
437
449
  return;
438
450
  }
@@ -441,18 +453,20 @@ const COMMANDS = [
441
453
  // (R-13): the user is allowed to run both tiers on one model, and the
442
454
  // review still costs what it costs.
443
455
  const cfg = controller.getConfig();
444
- persist({
456
+ if (!persist({
445
457
  provider: cfg.provider,
446
458
  model: cfg.model,
447
459
  baseUrl: cfg.baseUrl ?? '',
448
- });
460
+ }))
461
+ return;
449
462
  ctx.toast('success', `Fast tier set to ${cfg.provider}:${cfg.model} (same as main).`);
450
463
  return;
451
464
  }
452
465
  if (verb === 'review') {
453
466
  const raw = (value ?? '').trim().toLowerCase();
454
467
  if (raw === 'off') {
455
- persist({ review: false });
468
+ if (!persist({ review: false }))
469
+ return;
456
470
  ctx.toast('success', 'Fast review off.');
457
471
  return;
458
472
  }
@@ -508,7 +522,8 @@ const COMMANDS = [
508
522
  ctx.notify('warn', 'Usage: /fast delegate <on|off>');
509
523
  return;
510
524
  }
511
- persist({ delegate: raw === 'on' });
525
+ if (!persist({ delegate: raw === 'on' }))
526
+ return;
512
527
  ctx.toast('success', `Fast delegation ${raw}.`);
513
528
  return;
514
529
  }
@@ -891,6 +906,8 @@ const COMMANDS = [
891
906
  ctx.controller.clearAllQueues();
892
907
  ctx.dispatch({ type: 'resetConversation' });
893
908
  ctx.toast('info', 'Started a new conversation.');
909
+ if (ctx.state.pendingSteering.length)
910
+ ctx.notify('warn', `\u5df2\u53d6\u6d88 ${ctx.state.pendingSteering.length} \u6761\u672a\u63a5\u6536\u6d88\u606f`);
894
911
  },
895
912
  },
896
913
  {
@@ -941,15 +958,15 @@ const COMMANDS = [
941
958
  name: 'resume',
942
959
  description: 'Load a saved session',
943
960
  run: (ctx) => {
944
- if (ctx.controller.isRunning()) {
945
- ctx.notify('warn', 'Cannot resume while the agent is running. Wait for it to stop.');
961
+ if (ctx.controller.isRunning() || ctx.controller.isModelSettingsBusy?.()) {
962
+ ctx.notify('warn', 'Cannot resume while running work, team, review, or compaction is busy.');
946
963
  return;
947
964
  }
948
965
  try {
949
966
  const path = resolveSessionPath(ctx.args, ctx.controller.getCwd());
950
967
  const session = loadSession(path);
951
- if (ctx.controller.isRunning()) {
952
- ctx.notify('warn', 'Cannot resume while the agent is running. Wait for it to stop.');
968
+ if (ctx.controller.isRunning() || ctx.controller.isModelSettingsBusy?.()) {
969
+ ctx.notify('warn', 'Cannot resume while running work, team, review, or compaction is busy.');
953
970
  return;
954
971
  }
955
972
  // Loading has validated and normalized every external value. Commit
@@ -963,6 +980,8 @@ const COMMANDS = [
963
980
  ctx.controller.setModel(session.model.providerId, session.model.modelId, session.model.baseUrl);
964
981
  }
965
982
  ctx.notify('info', `Resumed session from ${path}`);
983
+ if (ctx.state.pendingSteering.length)
984
+ ctx.notify('warn', `\u5df2\u53d6\u6d88 ${ctx.state.pendingSteering.length} \u6761\u672a\u63a5\u6536\u6d88\u606f`);
966
985
  }
967
986
  catch (err) {
968
987
  ctx.notify('error', `Resume failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -972,7 +991,7 @@ const COMMANDS = [
972
991
  {
973
992
  name: 'copy',
974
993
  description: 'Copy the last answer to the clipboard',
975
- run: (ctx) => {
994
+ run: async (ctx) => {
976
995
  const last = [...ctx.state.entries].reverse().find((e) => e.kind === 'assistant');
977
996
  if (!last || last.kind !== 'assistant' || last.text.trim().length === 0) {
978
997
  ctx.notify('warn', 'No assistant message to copy.');
@@ -984,15 +1003,11 @@ const COMMANDS = [
984
1003
  // which the local `pbcopy` never could.
985
1004
  //
986
1005
  // `ctx.writeForeign` and not `stdout.write` — see the field's comment.
987
- const via = copyText(last.text, ctx.writeForeign);
988
- if (via === 'none') {
989
- ctx.toast('warn', 'Clipboard not available.');
1006
+ if (!ctx.requestCopy) {
1007
+ ctx.notify('warn', 'Clipboard coordinator unavailable.');
990
1008
  return;
991
1009
  }
992
- // NAMES THE MECHANISM RATHER THAN CLAIMING SUCCESS: neither path is
993
- // detectable, and tmux without `set -g set-clipboard on` swallows OSC 52
994
- // silently (R-7).
995
- ctx.toast('success', via === 'osc52' ? 'Sent to the terminal clipboard.' : 'Sent to the system clipboard.');
1010
+ await ctx.requestCopy({ text: last.text, lines: last.text.split('\n').length });
996
1011
  },
997
1012
  },
998
1013
  {
@@ -1053,20 +1068,24 @@ const COMMANDS = [
1053
1068
  const lines = [
1054
1069
  'Shift+Enter newline - terminal setup',
1055
1070
  '',
1056
- 'Ctrl+J and Alt+Enter insert a newline in every terminal, with no',
1057
- 'setup. To get a dedicated Shift+Enter key, make the terminal send',
1071
+ 'Ctrl+J inserts a newline when delivered; Alt+Enter may be intercepted.',
1072
+ 'To get a dedicated Shift+Enter key, make the terminal send',
1058
1073
  'the CSI-u sequence \\u001b[13;2u for it:',
1059
1074
  '',
1060
- ' Windows Terminal settings.json, actions array:',
1061
- ' { "keys": "shift+enter", "command": { "action": "sendInput",',
1062
- ' "input": "\\u001b[13;2u" } }',
1075
+ ' Windows Terminal settings.json, merge root-level arrays:',
1076
+ ' "copyOnSelect": false,',
1077
+ ' "actions": [{ "id": "User.AragonNewline", "command":',
1078
+ ' { "action": "sendInput", "input": "\\u001b[13;2u" } }],',
1079
+ ' "keybindings": [{ "keys": "shift+enter", "id": "User.AragonNewline" }]',
1063
1080
  ' PowerShell runs inside the terminal; configure this binding in',
1064
1081
  ' Windows Terminal, not in the PowerShell profile. If both keys',
1065
1082
  ' send CR, the application cannot distinguish Shift+Enter from Enter.',
1066
1083
  ' VS Code terminal keybindings.json:',
1067
1084
  ' { "key": "shift+enter",',
1068
1085
  ' "command": "workbench.action.terminal.sendSequence",',
1086
+ ' "when": "terminalFocus",',
1069
1087
  ' "args": { "text": "\\u001b[13;2u" } }',
1088
+ ' User settings: "terminal.integrated.copyOnSelection": false',
1070
1089
  ' iTerm2 Settings > Profiles > Keys > Key Mappings:',
1071
1090
  ' Shift+Enter -> Send Escape Sequence: [13;2u',
1072
1091
  ' kitty/WezTerm/foot native once keyboard enhancement is on',
@@ -1077,6 +1096,13 @@ const COMMANDS = [
1077
1096
  ' or Alt+Enter',
1078
1097
  '',
1079
1098
  'This command only prints advice; it changes no terminal modes.',
1099
+ 'Host selections (Shift+drag or /mouse off) belong to the terminal.',
1100
+ 'App selections wait for Ctrl+C; only a native tool exit confirms copying.',
1101
+ 'SSH uses OSC 52: the terminal may deny it; sent does not mean confirmed.',
1102
+ 'Without the stdin filter, Delete falls back to Backspace.',
1103
+ 'Without paste support, multiline paste cannot be guaranteed safe.',
1104
+ 'IME confirmation has no composition signal in PTY bytes. If it emits CR,',
1105
+ 'edit externally and use bracketed paste; this host combination needs validation.',
1080
1106
  ];
1081
1107
  ctx.notify('info', lines.join('\n'));
1082
1108
  },
@@ -1104,12 +1130,12 @@ const COMMANDS = [
1104
1130
  ctx.notify('warn', 'Cannot reload the config while a run is in progress.');
1105
1131
  return;
1106
1132
  }
1107
- const config = loadConfig({ cwd: ctx.controller.getCwd() });
1108
- ctx.controller.setModel(config.provider, config.model, config.baseUrl);
1109
- ctx.controller.setThinkingLevel(config.thinkingLevel);
1110
- ctx.controller.setMaxTokens(config.maxTokens);
1111
- ctx.controller.setTheme(config.theme);
1112
- getLogger().reconfigure(config.log);
1133
+ const result = ctx.controller.reloadModelSettings();
1134
+ if (!result.ok) {
1135
+ ctx.notify('warn', modelSettingsResultMessage(result));
1136
+ return;
1137
+ }
1138
+ const config = ctx.controller.getConfig();
1113
1139
  getLogger().info('config', 'reload', { provider: config.provider, model: config.model });
1114
1140
  ctx.toast('success', `Reloaded config: ${config.provider}:${config.model}.`);
1115
1141
  },
@@ -1219,6 +1245,7 @@ const DEFAULT_BG_LOG_ROWS = 40;
1219
1245
  export function registerBuiltinCommands(registry) {
1220
1246
  for (const command of COMMANDS)
1221
1247
  registry.register(command);
1248
+ registerQueueCommand(registry);
1222
1249
  }
1223
1250
  /**
1224
1251
  * Every built-in command name INCLUDING aliases (`quit`), for documentation and
@@ -1228,10 +1255,10 @@ export function registerBuiltinCommands(registry) {
1228
1255
  * `registry.get(name)` instead, because that is the only check that stays
1229
1256
  * correct when commands are registered dynamically (§7.1 / P1-2).
1230
1257
  */
1231
- export const BUILTIN_COMMAND_NAMES = COMMANDS.flatMap((c) => [
1232
- c.name,
1233
- ...(c.aliases ?? []),
1234
- ]).sort();
1258
+ export const BUILTIN_COMMAND_NAMES = ['queue', ...COMMANDS.flatMap((c) => [
1259
+ c.name,
1260
+ ...(c.aliases ?? []),
1261
+ ])].sort();
1235
1262
  // The `copyToClipboard` helper that used to live here moved into
1236
1263
  // `ui/clipboard.ts` (tui-selection-and-scroll-follow §4.4.5), unchanged, so that
1237
1264
  // `/copy` and the drag-select release share ONE clipboard path instead of two
@@ -0,0 +1,3 @@
1
+ import type { CommandRegistry } from './registry.js';
2
+ /** Read-only access to the live receipt-backed queue through the standard overlay port. */
3
+ export declare function registerQueueCommand(registry: CommandRegistry): void;
@@ -0,0 +1,5 @@
1
+ /** Read-only access to the live receipt-backed queue through the standard overlay port. */
2
+ export function registerQueueCommand(registry) {
3
+ registry.register({ name: 'queue', description: '\u67e5\u770b\u5f85\u5904\u7406\u6d88\u606f\u5168\u6587',
4
+ run: (ctx) => { ctx.setOverlay('queue'); } });
5
+ }
@@ -3,6 +3,7 @@
3
3
  * input is routed here instead of to the agent; `//` is the literal-`/` escape.
4
4
  */
5
5
  import type { SubmitMessageOptions } from '../agent/prompt-options.js';
6
+ import type { CopyRequest, CopyRequestResult } from '../ui/clipboard-task.js';
6
7
  import type { AgentController } from '../agent/controller.js';
7
8
  import type { AgentMode } from '../agent/agent-mode.js';
8
9
  import type { NoticeLevel, Overlay, ToastLevel, ViewAction, ViewState } from '../agent/reducer.js';
@@ -10,6 +11,8 @@ import type { PersistedConfig } from '../config/schema.js';
10
11
  import type { FollowThroughBudget } from '../todo/follow-through.js';
11
12
  import type { UpdateCommandPort } from '../update/types.js';
12
13
  export interface CommandContext {
14
+ /** App-owned shared lock and single feedback path for command and selection copies. */
15
+ requestCopy?: (request: CopyRequest) => Promise<CopyRequestResult>;
13
16
  /** Raw argument string after the command word (may be empty). */
14
17
  args: string;
15
18
  controller: AgentController;
@@ -28,13 +28,14 @@
28
28
  * lead's own history had been compacted. The team panel row and the dispatch
29
29
  * report are where a child's internals belong.
30
30
  */
31
+ import type { ModelRole } from '../config/model-profiles.js';
31
32
  import type { AssistantMessage, ContextManager, LLMRequest, Message, ModelInfo, ModelRef, TokenUsage } from '@aragon-agent/core';
32
33
  import type { CliConfig } from '../config/schema.js';
33
34
  export interface ChildCompactionDeps {
34
35
  /** Read LIVE on every call - see `childConfig` below. */
35
36
  getConfig: () => CliConfig;
36
- hasKey: (providerId: string) => boolean;
37
- getApiKey: (providerId: string) => string | undefined;
37
+ hasKey: (providerId: string, role?: ModelRole) => boolean;
38
+ getApiKey: (providerId: string, role?: ModelRole) => string | undefined;
38
39
  getModelInfoFor: (ref: Pick<ModelRef, 'providerId' | 'modelId'>) => ModelInfo;
39
40
  isPricedModel: (ref: Pick<ModelRef, 'providerId' | 'modelId'>) => boolean;
40
41
  /** The lead wiring's own transport, reused (D-10 stays true). */
@@ -50,6 +51,7 @@ export interface ChildCompactionDeps {
50
51
  * exactly on the cap.
51
52
  */
52
53
  export interface ChildContextManagerRequest {
54
+ role?: ModelRole;
53
55
  label: string;
54
56
  /**
55
57
  * The model the child actually runs on.
@@ -28,6 +28,7 @@
28
28
  * lead's own history had been compacted. The team panel row and the dispatch
29
29
  * report are where a child's internals belong.
30
30
  */
31
+ import { resolveFastTier } from '../fast/resolve.js';
31
32
  import { getLogger } from '../logging/logger.js';
32
33
  import { Compactor } from './compactor.js';
33
34
  import { COMPACTION_LIMITS } from './limits.js';
@@ -52,11 +53,15 @@ export function createChildContextManager(req, deps) {
52
53
  */
53
54
  const childConfig = () => {
54
55
  const base = deps.getConfig();
56
+ const sameConnection = base.provider === req.model.providerId &&
57
+ base.model === req.model.modelId && base.baseUrl === req.model.baseUrl;
55
58
  return {
56
59
  ...base,
57
60
  provider: req.model.providerId,
58
61
  model: req.model.modelId,
59
- ...(req.model.baseUrl !== undefined ? { baseUrl: req.model.baseUrl } : {}),
62
+ baseUrl: req.model.baseUrl,
63
+ // The lead's manual limit describes its connection, not every worker.
64
+ contextWindow: sameConnection ? base.contextWindow : null,
60
65
  compaction: {
61
66
  ...base.compaction,
62
67
  keepRecentTurns: COMPACTION_LIMITS.childKeepRecentTurns,
@@ -71,6 +76,8 @@ export function createChildContextManager(req, deps) {
71
76
  };
72
77
  const compactor = new Compactor({
73
78
  getConfig: childConfig,
79
+ mainRole: req.role ?? 'main',
80
+ resolveFastCandidate: () => resolveFastTier(deps.getConfig(), deps.hasKey),
74
81
  hasKey: deps.hasKey,
75
82
  getApiKey: deps.getApiKey,
76
83
  getModelInfoFor: deps.getModelInfoFor,
@@ -9,16 +9,20 @@
9
9
  * keep, which model summarizes, what the prompt asks for, what happens when the
10
10
  * call fails, and when to stop spending.
11
11
  */
12
+ import type { ModelRole } from '../config/model-profiles.js';
12
13
  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
14
  import type { CliConfig } from '../config/schema.js';
14
15
  import type { NoticeLevel } from '../agent/reducer.js';
16
+ import { resolveFastTier } from '../fast/resolve.js';
15
17
  import { ContextMeter } from './meter.js';
16
18
  import type { CompactionEvent, Pressure } from './types.js';
17
19
  export interface CompactorDeps {
20
+ mainRole?: ModelRole;
21
+ resolveFastCandidate?: () => ReturnType<typeof resolveFastTier>;
18
22
  /** Read LIVE on every call — the controller replaces the object on mutation. */
19
23
  getConfig: () => CliConfig;
20
- hasKey: (providerId: string) => boolean;
21
- getApiKey: (providerId: string) => string | undefined;
24
+ hasKey: (providerId: string, role?: ModelRole) => boolean;
25
+ getApiKey: (providerId: string, role?: ModelRole) => string | undefined;
22
26
  /**
23
27
  * The engine's live history and system prompt.
24
28
  *
@@ -87,6 +91,7 @@ export interface CompactorOptions {
87
91
  }
88
92
  /** The summarizer the next compaction would use. */
89
93
  export interface SummarizerChoice {
94
+ role: ModelRole;
90
95
  ref: ModelRef;
91
96
  /** Whether the fast tier supplied it, for `/compact status` and the card. */
92
97
  fromFastTier: boolean;
@@ -468,7 +468,7 @@ export class Compactor {
468
468
  from,
469
469
  systemPrompt,
470
470
  budget: tailBudgetTokens({
471
- contextWindow: this.modelInfo().contextWindow,
471
+ contextWindow: this.meter().current().contextWindow,
472
472
  maxOutputTokens: this.effectiveMaxOutputTokens(),
473
473
  }),
474
474
  log: (event, fields) => this.log.info(event, fields),
@@ -486,20 +486,22 @@ export class Compactor {
486
486
  const { summarizer } = args;
487
487
  if (!summarizer)
488
488
  return { kind: 'failed', reason: 'no_summarizer_model' };
489
- const first = await this.callOnce(summarizer.ref, args);
489
+ const first = await this.callOnce(summarizer, args);
490
490
  if (first.kind === 'ok' || first.kind === 'aborted')
491
491
  return first;
492
492
  // Rung 2 — retry, on the MAIN model when rung 1 was the fast tier.
493
- const mainRef = this.mainRef();
494
- const retryRef = summarizer.fromFastTier && mainRef.modelId !== summarizer.ref.modelId ? mainRef : summarizer.ref;
495
- this.log.warn('summarize_retry', { reason: first.reason, model: retryRef.modelId });
496
- const second = await this.callOnce(retryRef, args);
493
+ const retryChoice = summarizer.fromFastTier
494
+ ? { ref: this.mainRef(), role: this.deps.mainRole ?? 'main', fromFastTier: false }
495
+ : summarizer;
496
+ this.log.warn('summarize_retry', { reason: first.reason, model: retryChoice.ref.modelId });
497
+ const second = await this.callOnce(retryChoice, args);
497
498
  if (second.kind === 'ok' || second.kind === 'aborted')
498
499
  return second;
499
500
  return { kind: 'failed', reason: second.reason };
500
501
  }
501
- async callOnce(ref, args) {
502
- const apiKey = this.deps.getApiKey(ref.providerId);
502
+ async callOnce(choice, args) {
503
+ const { ref, role } = choice;
504
+ const apiKey = this.deps.getApiKey(ref.providerId, role);
503
505
  if (!apiKey)
504
506
  return { kind: 'failed', reason: 'no_api_key' };
505
507
  if (args.ctx.signal.aborted)
@@ -567,7 +569,7 @@ export class Compactor {
567
569
  */
568
570
  checkProgress(ctx, uiTrigger, after) {
569
571
  const config = this.deps.getConfig().compaction;
570
- const info = this.modelInfo();
572
+ const pressure = this.meter().current();
571
573
  // THE OFFSET NOW LIVES ON THE METER (W1), and it is read from the last
572
574
  // PUBLISHED pressure rather than re-derived: `Pressure.estimateOffset` is
573
575
  // carried on every measurement, so this is the same number the field used to
@@ -576,8 +578,8 @@ export class Compactor {
576
578
  const projected = computePressure({
577
579
  messages: after,
578
580
  systemPrompt: ctx.systemPrompt,
579
- contextWindow: info.contextWindow,
580
- windowKnown: true,
581
+ contextWindow: pressure.contextWindow,
582
+ windowKnown: pressure.windowKnown,
581
583
  ...(estimateOffset !== undefined ? { estimateOffset } : {}),
582
584
  });
583
585
  const target = config.threshold - COMPACTION_LIMITS.minReclaimRatio;
@@ -764,14 +766,14 @@ export class Compactor {
764
766
  resolveSummarizer() {
765
767
  const config = this.deps.getConfig();
766
768
  if (config.compaction.useFastTier) {
767
- const tier = resolveFastTier(config, this.deps.hasKey);
769
+ const tier = this.deps.resolveFastCandidate?.() ?? resolveFastTier(config, this.deps.hasKey);
768
770
  if (tier.ok)
769
- return { ref: tier.ref, fromFastTier: true };
771
+ return { ref: tier.ref, role: 'fast', fromFastTier: true };
770
772
  }
771
773
  const ref = this.mainRef();
772
- if (!this.deps.hasKey(ref.providerId))
774
+ if (!this.deps.hasKey(ref.providerId, this.deps.mainRole ?? 'main'))
773
775
  return null;
774
- return { ref, fromFastTier: false };
776
+ return { ref, role: this.deps.mainRole ?? 'main', fromFastTier: false };
775
777
  }
776
778
  /**
777
779
  * The meter this compactor reads, injected or private (R-1).
@@ -790,7 +792,10 @@ export class Compactor {
790
792
  getMessages: () => this.deps.getMessages(),
791
793
  getSystemPrompt: () => this.deps.getSystemPrompt(),
792
794
  getModelInfo: () => this.modelInfo(),
793
- isWindowKnown: () => this.deps.isPricedModel(this.mainRef()),
795
+ isWindowKnown: () => {
796
+ const source = this.modelInfo().contextWindowSource;
797
+ return source === undefined ? this.deps.isPricedModel(this.mainRef()) : source !== 'fallback';
798
+ },
794
799
  getWindowOverride: () => this.deps.getConfig().contextWindow,
795
800
  });
796
801
  }
@@ -45,6 +45,11 @@ export function formatContextReport(ctx) {
45
45
  // one line and answers the same question.
46
46
  lines.push(' includes subagent, fast-tier and compaction spend,');
47
47
  lines.push(' not just this conversation');
48
+ lines.push(' Scope lead occupancy is used / limit in tokens; session input includes caches.');
49
+ lines.push(' Estimate USD session cost, including restored usage; missing prices count as 0.');
50
+ lines.push(' Zero estimated cost does not guarantee free usage or match the bill.');
51
+ lines.push(' Run average booked output-token delta / elapsed time, not instantaneous generation.');
52
+ lines.push(' May include team, fast-tier and compaction output booked during this run.');
48
53
  return lines.join('\n');
49
54
  }
50
55
  /**
@@ -64,6 +69,8 @@ function occupancyLine(usage) {
64
69
  : usage.deltaTokens > 0
65
70
  ? `[measured ${formatTokens(measured)} + ${formatTokens(usage.deltaTokens)} estimated]`
66
71
  : `[measured ${formatTokens(measured)}]`;
72
+ if (!usage.windowKnown)
73
+ return `${formatTokens(usage.occupied)} of ? ${breakdown}`;
67
74
  return `${pct} ${formatTokens(usage.occupied)} of ${formatTokens(usage.window)} ${breakdown}`;
68
75
  }
69
76
  /**
@@ -78,13 +85,13 @@ function occupancyLine(usage) {
78
85
  */
79
86
  function windowLine(usage) {
80
87
  if (usage.windowOverridden) {
81
- return `${usage.window} from contextWindow in config.json (overrides the model table)`;
88
+ return `${usage.window} from contextWindow configuration (overrides API and model table)`;
82
89
  }
83
90
  if (!usage.windowKnown) {
84
- return (`${usage.window} PLACEHOLDER - this model is not in the table; ` +
91
+ return (`UNKNOWN - no verified model limit; internal fallback budget ${usage.window}; ` +
85
92
  'set contextWindow to correct it');
86
93
  }
87
- return `${usage.window} from the model table`;
94
+ return `${usage.window} from ${usage.windowSource === 'api' ? 'the model API' : 'the model table'}`;
88
95
  }
89
96
  /**
90
97
  * How stale the measured half is.
@@ -53,6 +53,7 @@ export const CONTEXT_METER_TICK_MS = 400;
53
53
  export function toContextUsage(p) {
54
54
  return {
55
55
  occupied: p.occupied,
56
+ ...(p.windowSource ? { windowSource: p.windowSource } : {}),
56
57
  window: p.contextWindow,
57
58
  // FROM `ratio`, NOT FROM A SECOND DIVISION. `computePressure` already
58
59
  // clamped it to [0, 1] and already decided what a zero window means.
@@ -380,6 +381,7 @@ export class ContextMeter {
380
381
  messages: this.deps.getMessages(),
381
382
  systemPrompt: this.readSystemPrompt(),
382
383
  contextWindow: window.contextWindow,
384
+ windowSource: this.deps.getModelInfo().contextWindowSource,
383
385
  windowKnown: window.windowKnown,
384
386
  windowOverridden: window.windowOverridden,
385
387
  ...(this.estimateOffset !== undefined ? { estimateOffset: this.estimateOffset } : {}),
@@ -50,6 +50,7 @@ export declare function occupiedTokens(usage: TokenUsage): number;
50
50
  */
51
51
  export declare function computeEstimateOffset(usage: TokenUsage, messages: readonly Message[], systemPrompt: string): number;
52
52
  export interface PressureInput {
53
+ windowSource?: Pressure['windowSource'];
53
54
  /** Authoritative usage of the last completed turn, or `undefined` (§3.4.2). */
54
55
  lastUsage?: TokenUsage;
55
56
  messages: readonly Message[];
@@ -133,6 +133,7 @@ export function computePressure(input) {
133
133
  const ratio = window > 0 ? Math.max(0, Math.min(1, occupied / window)) : 0;
134
134
  return {
135
135
  occupied,
136
+ ...(input.windowSource ? { windowSource: input.windowSource } : {}),
136
137
  contextWindow: window,
137
138
  ratio,
138
139
  headroom: Math.max(0, window - occupied),
@@ -51,6 +51,7 @@ export type CompactionUiTrigger = 'pressure' | 'overflow' | 'manual';
51
51
  /** Whether a figure was measured by the provider or estimated by us. */
52
52
  export type PressureSource = 'usage' | 'estimate';
53
53
  export interface Pressure {
54
+ windowSource?: 'api' | 'catalog' | 'fallback';
54
55
  occupied: number;
55
56
  contextWindow: number;
56
57
  /** `occupied / contextWindow`, clamped to [0, 1]. */
@@ -115,6 +116,7 @@ export interface Pressure {
115
116
  * view state, and `toContextUsage` in `meter.ts` is the ONLY converter.
116
117
  */
117
118
  export interface ContextUsageSnapshot {
119
+ windowSource?: Pressure['windowSource'];
118
120
  /** Occupied tokens (measured, plus an estimate of anything appended since). */
119
121
  occupied: number;
120
122
  /** The denominator actually used. */
@@ -35,6 +35,7 @@
35
35
  * guard is now unreachable for declines and stays for the abort race it was
36
36
  * written for.
37
37
  */
38
+ import type { ModelRole } from '../config/model-profiles.js';
38
39
  import { type AgentEvent, type ContextManager, type Message, type ModelInfo, type ModelRef, type ProviderRegistry, type ProviderRegistryOptions, type TokenUsage } from '@aragon-agent/core';
39
40
  import type { CliConfig } from '../config/schema.js';
40
41
  import type { NoticeLevel } from '../agent/reducer.js';
@@ -44,8 +45,8 @@ import type { CompactionEventListener, CompactionSnapshot } from './types.js';
44
45
  export interface CompactionWiringDeps {
45
46
  /** Read LIVE on every call - the controller replaces the object on mutation. */
46
47
  getConfig: () => CliConfig;
47
- hasKey: (providerId: string) => boolean;
48
- getApiKey: (providerId: string) => string | undefined;
48
+ hasKey: (providerId: string, role?: ModelRole) => boolean;
49
+ getApiKey: (providerId: string, role?: ModelRole) => string | undefined;
49
50
  getModelInfoFor: (ref: Pick<ModelRef, 'providerId' | 'modelId'>) => ModelInfo;
50
51
  isPricedModel: (ref: Pick<ModelRef, 'providerId' | 'modelId'>) => boolean;
51
52
  /**
@@ -158,7 +159,7 @@ export declare class CompactionWiring {
158
159
  isEnabled(): boolean;
159
160
  setEnabled(enabled: boolean): void;
160
161
  /** Re-emit the snapshot after any config mutation, so the chip and the gauge move. */
161
- onConfigChanged(): void;
162
+ onConfigChanged(enabled?: boolean): void;
162
163
  abort(): void;
163
164
  /**
164
165
  * The host replaced or cleared the history (hardening §3.2.3, sites 3 and 4).
@@ -135,7 +135,10 @@ export class CompactionWiring {
135
135
  getMessages: deps.getMessages,
136
136
  getSystemPrompt: deps.getSystemPrompt,
137
137
  getModelInfo: () => deps.getModelInfoFor(this.mainRef()),
138
- isWindowKnown: () => deps.isPricedModel(this.mainRef()),
138
+ isWindowKnown: () => {
139
+ const source = deps.getModelInfoFor(this.mainRef()).contextWindowSource;
140
+ return source === undefined ? deps.isPricedModel(this.mainRef()) : source !== 'fallback';
141
+ },
139
142
  getWindowOverride: () => deps.getConfig().contextWindow,
140
143
  });
141
144
  this.compactor = new Compactor({
@@ -209,7 +212,12 @@ export class CompactionWiring {
209
212
  this.emit({ type: 'snapshot', snapshot: this.snapshot() });
210
213
  }
211
214
  /** Re-emit the snapshot after any config mutation, so the chip and the gauge move. */
212
- onConfigChanged() {
215
+ onConfigChanged(enabled) {
216
+ if (enabled !== undefined && enabled !== this.enabled) {
217
+ this.enabled = enabled;
218
+ if (enabled)
219
+ this.compactor.clearSelfDisable();
220
+ }
213
221
  this.emit({ type: 'snapshot', snapshot: this.snapshot() });
214
222
  }
215
223
  abort() {
@@ -168,6 +168,8 @@ export declare function applyCompactionConfigSet(key: string, value: string): Pa
168
168
  * later pastes a log into an issue.
169
169
  */
170
170
  export declare function warnIfRedactionDisabled(key: string, value: string): void;
171
+ /** Every secret is masked wherever it is printed, `--json` included (R-12). */
172
+ export declare function maskConfig(config: PersistedConfig): Record<string, unknown>;
171
173
  export declare function runConfigGet(key: string): void;
172
174
  export declare function runConfigList(opts?: {
173
175
  json?: boolean;