@aragon-agent/cli 0.6.3 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +203 -13
  3. package/dist/agent/controller.d.ts +158 -2
  4. package/dist/agent/controller.js +400 -1
  5. package/dist/agent/reducer.d.ts +102 -19
  6. package/dist/agent/reducer.js +162 -71
  7. package/dist/agent/system-prompt.d.ts +14 -0
  8. package/dist/agent/system-prompt.js +2 -0
  9. package/dist/agent/usage.d.ts +34 -0
  10. package/dist/agent/usage.js +43 -0
  11. package/dist/cli.js +132 -23
  12. package/dist/commands/builtins.js +101 -1
  13. package/dist/compaction/child.js +5 -0
  14. package/dist/compaction/command.js +14 -1
  15. package/dist/compaction/compactor.d.ts +94 -24
  16. package/dist/compaction/compactor.js +247 -98
  17. package/dist/compaction/context-command.d.ts +33 -0
  18. package/dist/compaction/context-command.js +166 -0
  19. package/dist/compaction/meter.d.ts +251 -0
  20. package/dist/compaction/meter.js +428 -0
  21. package/dist/compaction/pressure.d.ts +6 -0
  22. package/dist/compaction/pressure.js +4 -0
  23. package/dist/compaction/types.d.ts +77 -0
  24. package/dist/compaction/wiring.d.ts +56 -0
  25. package/dist/compaction/wiring.js +107 -25
  26. package/dist/config/cli-commands.js +5 -0
  27. package/dist/config/env.d.ts +1 -1
  28. package/dist/config/env.js +63 -2
  29. package/dist/config/load.d.ts +28 -0
  30. package/dist/config/load.js +83 -1
  31. package/dist/config/schema.d.ts +129 -0
  32. package/dist/config/schema.js +69 -0
  33. package/dist/config/store.js +47 -1
  34. package/dist/exec/events.d.ts +138 -1
  35. package/dist/exec/events.js +42 -0
  36. package/dist/exec/index.js +43 -6
  37. package/dist/exec/progress-reporter.d.ts +23 -0
  38. package/dist/exec/progress-reporter.js +88 -0
  39. package/dist/exec/runner.d.ts +34 -0
  40. package/dist/exec/runner.js +137 -25
  41. package/dist/fast/reviewer.d.ts +19 -1
  42. package/dist/fast/reviewer.js +22 -0
  43. package/dist/fast/types.d.ts +15 -0
  44. package/dist/fast/wiring.d.ts +10 -1
  45. package/dist/fast/wiring.js +18 -1
  46. package/dist/input/limits.d.ts +72 -0
  47. package/dist/input/limits.js +63 -0
  48. package/dist/input/paste-parse.d.ts +71 -0
  49. package/dist/input/paste-parse.js +95 -0
  50. package/dist/input/stdin-filter.d.ts +81 -0
  51. package/dist/input/stdin-filter.js +379 -0
  52. package/dist/logging/install.d.ts +2 -0
  53. package/dist/logging/install.js +53 -1
  54. package/dist/proc/classify.d.ts +52 -0
  55. package/dist/proc/classify.js +143 -0
  56. package/dist/proc/kill-tree.d.ts +42 -0
  57. package/dist/proc/kill-tree.js +119 -0
  58. package/dist/proc/limits.d.ts +84 -0
  59. package/dist/proc/limits.js +84 -0
  60. package/dist/proc/log-ring.d.ts +68 -0
  61. package/dist/proc/log-ring.js +102 -0
  62. package/dist/proc/prompt.d.ts +24 -0
  63. package/dist/proc/prompt.js +49 -0
  64. package/dist/proc/readiness.d.ts +102 -0
  65. package/dist/proc/readiness.js +219 -0
  66. package/dist/proc/supervisor.d.ts +103 -0
  67. package/dist/proc/supervisor.js +465 -0
  68. package/dist/proc/types.d.ts +175 -0
  69. package/dist/proc/types.js +15 -0
  70. package/dist/session/persist.js +36 -1
  71. package/dist/todo/limits.d.ts +4 -17
  72. package/dist/todo/limits.js +4 -17
  73. package/dist/todo/panel-rows.d.ts +8 -6
  74. package/dist/todo/panel-rows.js +19 -6
  75. package/dist/tools/bash-tool.d.ts +51 -2
  76. package/dist/tools/bash-tool.js +321 -51
  77. package/dist/tools/fs-tools.d.ts +17 -0
  78. package/dist/tools/index.d.ts +55 -2
  79. package/dist/tools/index.js +39 -1
  80. package/dist/tools/proc-tools.d.ts +25 -0
  81. package/dist/tools/proc-tools.js +102 -0
  82. package/dist/ui/App.d.ts +15 -2
  83. package/dist/ui/App.js +330 -69
  84. package/dist/ui/AutocompletePopup.d.ts +3 -1
  85. package/dist/ui/AutocompletePopup.js +9 -11
  86. package/dist/ui/BottomStatusRow.d.ts +1 -1
  87. package/dist/ui/Composer.d.ts +56 -1
  88. package/dist/ui/Composer.js +50 -6
  89. package/dist/ui/PromptInput.d.ts +52 -3
  90. package/dist/ui/PromptInput.js +201 -55
  91. package/dist/ui/StatusBar.d.ts +40 -15
  92. package/dist/ui/StatusBar.js +51 -9
  93. package/dist/ui/TeamPanel.d.ts +3 -1
  94. package/dist/ui/TeamPanel.js +13 -9
  95. package/dist/ui/TodoPanel.d.ts +5 -5
  96. package/dist/ui/TodoPanel.js +56 -24
  97. package/dist/ui/Transcript.js +29 -1
  98. package/dist/ui/UpdateLine.d.ts +1 -1
  99. package/dist/ui/capabilities.js +5 -5
  100. package/dist/ui/composer-limits.d.ts +31 -0
  101. package/dist/ui/composer-limits.js +50 -0
  102. package/dist/ui/composer-rows.d.ts +73 -0
  103. package/dist/ui/composer-rows.js +201 -0
  104. package/dist/ui/editor-reducer.d.ts +37 -0
  105. package/dist/ui/editor-reducer.js +88 -12
  106. package/dist/ui/entries/ServiceCard.d.ts +66 -0
  107. package/dist/ui/entries/ServiceCard.js +116 -0
  108. package/dist/ui/entries/UserEntry.d.ts +17 -3
  109. package/dist/ui/entries/UserEntry.js +34 -5
  110. package/dist/ui/exit-snapshot.d.ts +9 -0
  111. package/dist/ui/layout/ScrollViewport.d.ts +1 -1
  112. package/dist/ui/layout/autocomplete.d.ts +18 -0
  113. package/dist/ui/layout/autocomplete.js +26 -0
  114. package/dist/ui/layout/budget.d.ts +34 -6
  115. package/dist/ui/layout/budget.js +32 -7
  116. package/dist/ui/layout/rail.d.ts +4 -53
  117. package/dist/ui/layout/rail.js +10 -62
  118. package/dist/ui/layout/team-panel.d.ts +18 -0
  119. package/dist/ui/layout/team-panel.js +22 -0
  120. package/dist/ui/layout/todo-layout.d.ts +24 -0
  121. package/dist/ui/layout/todo-layout.js +27 -0
  122. package/dist/ui/layout/virtual-window.js +51 -1
  123. package/dist/ui/overlays/HelpOverlay.js +3 -0
  124. package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
  125. package/dist/ui/overlays/QuestionOverlay.js +5 -1
  126. package/dist/ui/overlays/SettingsScreen.js +14 -1
  127. package/dist/ui/paste-frames.d.ts +87 -0
  128. package/dist/ui/paste-frames.js +112 -0
  129. package/dist/ui/paste-tokens.d.ts +90 -0
  130. package/dist/ui/paste-tokens.js +147 -0
  131. package/dist/ui/screen.d.ts +16 -0
  132. package/dist/ui/screen.js +45 -2
  133. package/dist/ui/selection/selection-controller.d.ts +1 -1
  134. package/dist/ui/transcript-text.js +55 -2
  135. package/dist/ui/use-wheel-routing.d.ts +1 -1
  136. package/package.json +2 -2
  137. package/dist/input/stdin-mouse-filter.d.ts +0 -47
  138. package/dist/input/stdin-mouse-filter.js +0 -153
@@ -78,6 +78,27 @@ export declare const DEFAULT_TRANSCRIPT_WINDOW = 1000;
78
78
  export declare const MIN_TRANSCRIPT_WINDOW = 50;
79
79
  export declare const MAX_TRANSCRIPT_WINDOW = 20000;
80
80
  export declare function clampTranscriptWindow(v: unknown, fallback: number): number;
81
+ /**
82
+ * Bounds for a hand-supplied context window.
83
+ *
84
+ * The floor is roughly the smallest window any current model ships with; the
85
+ * ceiling is well above the largest, and both exist to keep a typo (a missing
86
+ * or an extra zero) from producing a gauge that is permanently red or
87
+ * permanently flat.
88
+ */
89
+ export declare const MIN_CONTEXT_WINDOW = 8000;
90
+ export declare const MAX_CONTEXT_WINDOW = 5000000;
91
+ /**
92
+ * THE single gate for every `contextWindow` read AND write.
93
+ *
94
+ * TRI-STATE, AND `null` MUST PASS STRAIGHT THROUGH (RV-12). `null` means AUTO -
95
+ * take the model table's number, or the 128k placeholder. Clamping it into
96
+ * `[8000, ...]` yields 8000, which removes "auto" from the config permanently
97
+ * and presents to the user as an 8k denominator with a permanently red bar and
98
+ * no visible source. `maxTokens` above is the existing tri-state precedent and
99
+ * this deliberately copies its shape rather than inventing a second convention.
100
+ */
101
+ export declare function clampContextWindow(v: unknown, fallback: number | null): number | null;
81
102
  /**
82
103
  * Entries kept in `ViewState` at all (tui-render-performance L1 / R5).
83
104
  *
@@ -599,6 +620,48 @@ export declare const DEFAULT_COMPACTION_CONFIG: CompactionConfig;
599
620
  * gives `warnThreshold` a range of `[0.4, 0.45]`.
600
621
  */
601
622
  export declare function clampCompactionConfig(raw: unknown): CompactionConfig;
623
+ export interface BashConfig {
624
+ /**
625
+ * Register `bash_output` / `bash_kill`, honour `background`, splice the prompt
626
+ * block, render the chip. Default TRUE.
627
+ *
628
+ * READ ONCE AT CONSTRUCTION, the `team.enabled` / `todo.enabled` /
629
+ * `fast.enabled` rule.
630
+ *
631
+ * IT GOVERNS *SERVICES*, NEVER *INTERRUPTIBILITY* (I-2 / D-9). `Esc`/`Esc`,
632
+ * the foreground child registry and `forceStop()` are unconditional, because
633
+ * "bash always settles" and "Esc twice always works" are promised
634
+ * unconditionally — a user who turned background services off has not asked
635
+ * for a less interruptible agent.
636
+ */
637
+ background: boolean;
638
+ /**
639
+ * Let the classifier decide when the model did not say. Default TRUE.
640
+ *
641
+ * RESOLVES TO `false` WITHOUT AN INTERACTIVE VIEW (D-15 / P1-8), and that is
642
+ * decided at the tool layer rather than here: the value on disk is the user's
643
+ * preference, and headless has no card, no `/bg` and no user to keep a service
644
+ * alive, so the rescue would be a surprise instead. An explicit
645
+ * `background: true` is still honoured there — that one the model asked for.
646
+ */
647
+ autoBackground: boolean;
648
+ /** How long a background launch may block the turn before returning. */
649
+ startupSettleMs: number;
650
+ /** How long a service may stay `starting` before it is called `running`. */
651
+ readyTimeoutMs: number;
652
+ }
653
+ export declare const DEFAULT_BASH_CONFIG: BashConfig;
654
+ /**
655
+ * THE single gate for every bash-config read AND write, mirroring
656
+ * `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` / `clampTodoConfig`
657
+ * for the reason those record: hardening only the read path leaves a bad value
658
+ * on disk that reverts to the default on every launch, which presents to the
659
+ * user as "my setting won't stick".
660
+ *
661
+ * CLAMPED, NEVER REJECTED — the discipline every other key in this file follows.
662
+ * A garbage `startupSettleMs` resolves to 4000 and throws nothing.
663
+ */
664
+ export declare function clampBashConfig(raw: unknown): BashConfig;
602
665
  /**
603
666
  * Parse `0.9`, `90%` or `90` into a ratio, or `null` when it is neither.
604
667
  *
@@ -650,6 +713,18 @@ export interface PersistedConfig {
650
713
  * `null`), which left the user no way to express "let the model decide".
651
714
  */
652
715
  maxTokens: number | null;
716
+ /**
717
+ * The context window to measure occupancy against, or `null` for AUTO
718
+ * (context-usage-gauge-accuracy §3.6).
719
+ *
720
+ * `null` MEANS AUTO - the model table's window, or `buildRuntimeModel`'s 128k
721
+ * placeholder for a model the table has never seen. It exists because that
722
+ * placeholder is a FABRICATED denominator: on a custom `baseUrl` or an
723
+ * unlisted model id the gauge is a percentage of a number nobody supplied, and
724
+ * before this key there was no way to correct it. Setting it also removes the
725
+ * `?` marker from the absolute pair (I-6).
726
+ */
727
+ contextWindow: number | null;
653
728
  theme: ThemeName;
654
729
  /** Replace spinners with a static glyph for calmer, low-motion output. */
655
730
  reducedMotion: boolean;
@@ -702,6 +777,16 @@ export interface PersistedConfig {
702
777
  * written at all, so `false` is byte-identical to a pre-feature session.
703
778
  */
704
779
  mouseSelect: boolean;
780
+ /**
781
+ * Recognise pasted text and collapse a large paste into a placeholder
782
+ * (tui-paste-handling D-10 / section 7.3).
783
+ *
784
+ * A PLAIN BOOLEAN and the ONLY user-facing key this feature adds: the collapse
785
+ * threshold and every size bound are structural and live in `input/limits.ts`
786
+ * / `ui/composer-limits.ts`. A tunable threshold is a preference nobody has
787
+ * asked for; a kill switch is what a bug report needs.
788
+ */
789
+ paste: boolean;
705
790
  /** Idle ms before a paused viewport returns to the newest line; `0` disables. */
706
791
  scrollResumeMs: number;
707
792
  /** Start every session in PLAN mode (plan-mode §4.4). */
@@ -726,6 +811,8 @@ export interface PersistedConfig {
726
811
  update: UpdateConfig;
727
812
  /** Context compaction. The NINTH nested object; scalars only (see CompactionConfig). */
728
813
  compaction: CompactionConfig;
814
+ /** Background services. The TENTH nested object; scalars only (see BashConfig). */
815
+ bash: BashConfig;
729
816
  }
730
817
  export declare const DEFAULT_CONFIG: PersistedConfig;
731
818
  /**
@@ -781,6 +868,17 @@ export interface CliConfig {
781
868
  liveToolOutput: boolean;
782
869
  /** The resolved cap, or `undefined` for AUTO (per-model ceiling, ≤ 64000). */
783
870
  maxTokens?: number;
871
+ /**
872
+ * The user's context-window override, or `null` for AUTO.
873
+ *
874
+ * IT MUST EXIST HERE AND NOT ONLY IN `PersistedConfig`, for the reason
875
+ * `showThinking` / `liveToolOutput` / `diffRender` each record above: the
876
+ * runtime reads `config.*`, where `config` is a `CliConfig`, so a key added to
877
+ * the persisted shape alone would round-trip through `config set`, appear in
878
+ * `config list` - and never reach `ContextMeter`. `ARAGON_CONTEXT_WINDOW`
879
+ * would parse and be discarded, with nothing failing.
880
+ */
881
+ contextWindow: number | null;
784
882
  theme: ThemeName;
785
883
  reducedMotion: boolean;
786
884
  /**
@@ -866,6 +964,17 @@ export interface CliConfig {
866
964
  * `config list`, and never reach a single one of them.
867
965
  */
868
966
  mouseSelect: boolean;
967
+ /**
968
+ * Whether this session recognises pastes at all.
969
+ *
970
+ * IT MUST EXIST HERE AND NOT ONLY IN `PersistedConfig`, for the reason
971
+ * `mouseSelect` above records: `runInteractive` reads `config.paste` -- where
972
+ * `config` is a `CliConfig` -- to decide the filter's `paste` feature, the
973
+ * `?2004h` write and the `PasteBridge`. A key added to the persisted shape
974
+ * alone would round-trip through `config set`, appear in `config list`, and
975
+ * reach none of them.
976
+ */
977
+ paste: boolean;
869
978
  /** Idle ms before a paused viewport returns to the newest line; `0` disables. */
870
979
  scrollResumeMs: number;
871
980
  /**
@@ -959,6 +1068,26 @@ export interface CliConfig {
959
1068
  * reach either of them.
960
1069
  */
961
1070
  compaction: CompactionConfig;
1071
+ /**
1072
+ * Resolved background-service settings (background-service-supervision §5.4).
1073
+ *
1074
+ * `background` here decides whether `bash`'s schema carries a `background`
1075
+ * property, whether `bash_output` / `bash_kill` are REGISTERED and whether
1076
+ * `<background_services>` is spliced — all three at construction, and none of
1077
+ * them undoable, the `team.enabled` / `todo.enabled` / `fast.enabled` rule.
1078
+ * The other three keys are read LIVE.
1079
+ *
1080
+ * IT DOES NOT GATE THE INTERRUPT LADDER (I-2 / D-9). `ProcSupervisor` is
1081
+ * constructed unconditionally, because `Esc`/`Esc` and the foreground registry
1082
+ * are what make G2 and G3 true and those are promised whatever this says.
1083
+ *
1084
+ * IT MUST EXIST HERE AND NOT ONLY IN `PersistedConfig`, for the reason every
1085
+ * section above records: the controller and `bash` read `config.bash`, where
1086
+ * `config` is a `CliConfig`, so a key added to the persisted shape alone would
1087
+ * round-trip through `config set`, appear in `config list` — and never reach
1088
+ * either of them.
1089
+ */
1090
+ bash: BashConfig;
962
1091
  /**
963
1092
  * Lifetime submit count driving the composer hint fade (`HINT_FADE_AFTER`).
964
1093
  *
@@ -115,6 +115,39 @@ export function clampTranscriptWindow(v, fallback) {
115
115
  const n = coercePositiveInt(v, fallback);
116
116
  return Math.min(MAX_TRANSCRIPT_WINDOW, Math.max(MIN_TRANSCRIPT_WINDOW, n));
117
117
  }
118
+ // ---------------------------------------------------------------------------
119
+ // Context window override (context-usage-gauge-accuracy §3.6 / W5)
120
+ // ---------------------------------------------------------------------------
121
+ /**
122
+ * Bounds for a hand-supplied context window.
123
+ *
124
+ * The floor is roughly the smallest window any current model ships with; the
125
+ * ceiling is well above the largest, and both exist to keep a typo (a missing
126
+ * or an extra zero) from producing a gauge that is permanently red or
127
+ * permanently flat.
128
+ */
129
+ export const MIN_CONTEXT_WINDOW = 8_000;
130
+ export const MAX_CONTEXT_WINDOW = 5_000_000;
131
+ /**
132
+ * THE single gate for every `contextWindow` read AND write.
133
+ *
134
+ * TRI-STATE, AND `null` MUST PASS STRAIGHT THROUGH (RV-12). `null` means AUTO -
135
+ * take the model table's number, or the 128k placeholder. Clamping it into
136
+ * `[8000, ...]` yields 8000, which removes "auto" from the config permanently
137
+ * and presents to the user as an 8k denominator with a permanently red bar and
138
+ * no visible source. `maxTokens` above is the existing tri-state precedent and
139
+ * this deliberately copies its shape rather than inventing a second convention.
140
+ */
141
+ export function clampContextWindow(v, fallback) {
142
+ if (v === null)
143
+ return null;
144
+ if (v === undefined || v === '')
145
+ return fallback;
146
+ const n = typeof v === 'number' ? v : Number.parseInt(String(v), 10);
147
+ if (!Number.isFinite(n) || n <= 0)
148
+ return fallback;
149
+ return Math.min(MAX_CONTEXT_WINDOW, Math.max(MIN_CONTEXT_WINDOW, Math.floor(n)));
150
+ }
118
151
  /**
119
152
  * Entries kept in `ViewState` at all (tui-render-performance L1 / R5).
120
153
  *
@@ -777,6 +810,33 @@ export function clampCompactionConfig(raw) {
777
810
  archive: bool(src.archive, DEFAULT_COMPACTION_CONFIG.archive),
778
811
  };
779
812
  }
813
+ export const DEFAULT_BASH_CONFIG = {
814
+ background: true,
815
+ autoBackground: true,
816
+ startupSettleMs: 4000,
817
+ readyTimeoutMs: 60_000,
818
+ };
819
+ const STARTUP_SETTLE_RANGE = { min: 500, max: 30_000 };
820
+ const READY_TIMEOUT_RANGE = { min: 5_000, max: 600_000 };
821
+ /**
822
+ * THE single gate for every bash-config read AND write, mirroring
823
+ * `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` / `clampTodoConfig`
824
+ * for the reason those record: hardening only the read path leaves a bad value
825
+ * on disk that reverts to the default on every launch, which presents to the
826
+ * user as "my setting won't stick".
827
+ *
828
+ * CLAMPED, NEVER REJECTED — the discipline every other key in this file follows.
829
+ * A garbage `startupSettleMs` resolves to 4000 and throws nothing.
830
+ */
831
+ export function clampBashConfig(raw) {
832
+ const src = (raw && typeof raw === 'object' ? raw : {});
833
+ return {
834
+ background: bool(src.background, DEFAULT_BASH_CONFIG.background),
835
+ autoBackground: bool(src.autoBackground, DEFAULT_BASH_CONFIG.autoBackground),
836
+ startupSettleMs: clampInt(src.startupSettleMs, DEFAULT_BASH_CONFIG.startupSettleMs, STARTUP_SETTLE_RANGE),
837
+ readyTimeoutMs: clampInt(src.readyTimeoutMs, DEFAULT_BASH_CONFIG.readyTimeoutMs, READY_TIMEOUT_RANGE),
838
+ };
839
+ }
780
840
  /**
781
841
  * Parse `0.9`, `90%` or `90` into a ratio, or `null` when it is neither.
782
842
  *
@@ -814,6 +874,9 @@ export const DEFAULT_CONFIG = {
814
874
  // The engine owns this number; spelling it here again is how the CLI and the
815
875
  // provider adapters end up disagreeing about what "the default" is.
816
876
  maxTokens: DEFAULT_MAX_OUTPUT_TOKENS,
877
+ // AUTO. The model table is right for every listed model, and a default number
878
+ // here would be the same fabricated denominator this key exists to correct.
879
+ contextWindow: null,
817
880
  theme: 'auto',
818
881
  reducedMotion: false,
819
882
  fullscreen: true,
@@ -833,6 +896,7 @@ export const DEFAULT_CONFIG = {
833
896
  hints: true,
834
897
  mouse: true,
835
898
  mouseSelect: DEFAULT_MOUSE_SELECT,
899
+ paste: true,
836
900
  scrollResumeMs: DEFAULT_SCROLL_RESUME_MS,
837
901
  planModeDefault: false,
838
902
  planModeMaxAskRounds: DEFAULT_PLAN_MAX_ASK_ROUNDS,
@@ -865,6 +929,11 @@ export const DEFAULT_CONFIG = {
865
929
  // arrive OFF for every existing user, which is the one direction this
866
930
  // particular default must never fail in.
867
931
  compaction: DEFAULT_COMPACTION_CONFIG,
932
+ // And one section later again, for the fifth time. `bash.background` is `true`
933
+ // by default, so a `config.json` predating this feature resolving it to
934
+ // `undefined` would arrive with background services OFF for every existing
935
+ // user — the same direction the compaction line above must never fail in.
936
+ bash: DEFAULT_BASH_CONFIG,
868
937
  };
869
938
  /**
870
939
  * Runtime state that used to be stored in `config.json` by mistake
@@ -14,7 +14,7 @@ import { dirname } from 'node:path';
14
14
  import process from 'node:process';
15
15
  import { getConfigPath, getSessionsDir } from './app-paths.js';
16
16
  import { registerSecretsFrom } from '../logging/secret-registry.js';
17
- import { CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
17
+ import { clampBashConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampContextWindow, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
18
18
  /**
19
19
  * Normalize `maxTokens` for both directions of the config file.
20
20
  *
@@ -31,6 +31,22 @@ function normalizeMaxTokens(v) {
31
31
  return null;
32
32
  return clampMaxTokens(v) ?? null;
33
33
  }
34
+ /**
35
+ * Normalize `contextWindow` for both directions of the config file.
36
+ *
37
+ * THE SAME TRI-STATE DISCIPLINE AS `maxTokens` ABOVE, and for the same reason:
38
+ * hardening only the read path leaves a bad value on disk that reverts on every
39
+ * launch and presents as "my setting won't stick". `null` is AUTO and survives
40
+ * untouched; `auto` / `0` / an empty string mean AUTO too; a number is clamped;
41
+ * anything else on a hand-edited file becomes AUTO rather than a silent default.
42
+ */
43
+ function normalizeContextWindow(v) {
44
+ if (v === undefined)
45
+ return DEFAULT_CONFIG.contextWindow;
46
+ if (v === null || isAutoToken(v))
47
+ return null;
48
+ return clampContextWindow(v, null);
49
+ }
34
50
  // Re-exported so the many existing importers of `store.js` keep working; the
35
51
  // definitions live in `app-paths.ts` because they are pure path arithmetic.
36
52
  export { getConfigPath, getSessionsDir };
@@ -114,6 +130,11 @@ export function loadPersistedConfig() {
114
130
  maxTokens: Object.prototype.hasOwnProperty.call(partial, 'maxTokens')
115
131
  ? normalizeMaxTokens(partial.maxTokens)
116
132
  : DEFAULT_CONFIG.maxTokens,
133
+ // Own-property aware for the reason its neighbour records: `null` is AUTO
134
+ // and a spread cannot tell it from an absent key.
135
+ contextWindow: Object.prototype.hasOwnProperty.call(partial, 'contextWindow')
136
+ ? normalizeContextWindow(partial.contextWindow)
137
+ : DEFAULT_CONFIG.contextWindow,
117
138
  apiKeys: { ...DEFAULT_CONFIG.apiKeys, ...(partial.apiKeys ?? {}) },
118
139
  // `skills` is the second nested object in this file and needs the same
119
140
  // treatment `apiKeys` gets — see the note on updatePersistedConfig below.
@@ -148,6 +169,20 @@ export function loadPersistedConfig() {
148
169
  ...DEFAULT_CONFIG.compaction,
149
170
  ...(partial.compaction ?? {}),
150
171
  }),
172
+ // `bash` is the tenth. Same one-level rule, same merge — and the READ half
173
+ // supplies a whole section to every user whose `config.json` predates
174
+ // background services, which is all of them. Without it
175
+ // `config.bash.background` reads `undefined`, which is FALSY, so a feature
176
+ // that is `true` by default would arrive off for every existing user.
177
+ //
178
+ // A SIBLING OF `compaction`, NOT A KEY INSIDE ITS ARGUMENT. Nesting it one
179
+ // level too deep type-checks — `...DEFAULT_CONFIG` above already supplies a
180
+ // `bash`, so nothing is missing from the return type — and hands
181
+ // `clampCompactionConfig` a key it drops on the floor. The section then
182
+ // reaches callers straight off the shallow `...partial` spread: unclamped,
183
+ // and NOT merged over the defaults, which is verbatim the failure the
184
+ // paragraph above says this line prevents.
185
+ bash: clampBashConfig({ ...DEFAULT_CONFIG.bash, ...(partial.bash ?? {}) }),
151
186
  };
152
187
  }
153
188
  /**
@@ -184,6 +219,11 @@ export function updatePersistedConfig(patch) {
184
219
  maxTokens: Object.prototype.hasOwnProperty.call(patch, 'maxTokens')
185
220
  ? normalizeMaxTokens(patch.maxTokens)
186
221
  : current.maxTokens,
222
+ // The write half of the same gate: without it `config set contextWindow
223
+ // 99999999` puts a number on disk that the read path silently rewrites.
224
+ contextWindow: Object.prototype.hasOwnProperty.call(patch, 'contextWindow')
225
+ ? normalizeContextWindow(patch.contextWindow)
226
+ : current.contextWindow,
187
227
  apiKeys: { ...current.apiKeys, ...(patch.apiKeys ?? {}) },
188
228
  skills: clampSkillsConfig({ ...current.skills, ...(patch.skills ?? {}) }),
189
229
  // Without this line, changing the log level from the settings screen would
@@ -224,6 +264,12 @@ export function updatePersistedConfig(patch) {
224
264
  // OFF finds it back on tomorrow — a feature that spends money on their behalf
225
265
  // re-enabling itself because they nudged a threshold.
226
266
  compaction: clampCompactionConfig({ ...current.compaction, ...(patch.compaction ?? {}) }),
267
+ // The tenth section, and the fifth time this file records the same trap.
268
+ // A patch carrying only `{ bash: { autoBackground: false } }` — turning the
269
+ // CLASSIFIER off — would, without this line, be spread shallowly over the
270
+ // top level and write `background` absent, silently unregistering
271
+ // `bash_output` / `bash_kill` from the next launch onward.
272
+ bash: clampBashConfig({ ...current.bash, ...(patch.bash ?? {}) }),
227
273
  };
228
274
  writeConfigFile(merged);
229
275
  return merged;
@@ -71,6 +71,43 @@ export interface ExecSystemInitEvent {
71
71
  permissionMode: ExecPermissionMode;
72
72
  tools: string[];
73
73
  resumed: boolean;
74
+ /**
75
+ * Capabilities of THIS build that a caller has to know about before using
76
+ * them. Optional so older consumers are unaffected, and additive so
77
+ * `EXEC_SCHEMA_VERSION` does not move (see the file header).
78
+ *
79
+ * `'interrupt'` means the stdin `{"type":"interrupt"}` frame really does run
80
+ * the full settle -> persist -> `result` sequence ON EVERY PLATFORM. Builds
81
+ * before that fix routed it through a self-directed SIGINT, which on Windows
82
+ * terminates the process outright with exit code 1 - no persistence, no
83
+ * `result`. A wrapper cannot tell the two apart by observation, and guessing
84
+ * wrong costs the user their conversation, so this build states it.
85
+ */
86
+ capabilities?: string[];
87
+ /**
88
+ * Absolute path of the session file THIS run reads and writes; `null` under
89
+ * `--no-save-session` (web-use-durable-control-and-tier-accountability W2).
90
+ *
91
+ * Optional and additive, so `EXEC_SCHEMA_VERSION` does not move (file header).
92
+ * Stamped by `buildInitEvent`, never set by a caller - the same discipline
93
+ * `capabilities` follows.
94
+ *
95
+ * ## Why a wrapper cannot work this out for itself
96
+ *
97
+ * Deriving it host-side means re-implementing `getSessionsDir()`
98
+ * (`envPaths('aragon-agent')` plus its environment override) in a second
99
+ * codebase that has no way to notice when this one changes. The failure mode
100
+ * of a drifted copy is a path that does not exist, which a wrapper reads as
101
+ * "that conversation is gone" - identical, on screen, to the session really
102
+ * being gone. The run that is writing the file is the only party that can say
103
+ * where it is without guessing.
104
+ *
105
+ * It is the id THIS RUN writes back under, not the string the caller typed:
106
+ * `--resume` accepts `<id|path>`, and `resumedSessionId()` decides what the
107
+ * file is actually called. A wrapper asking "can I reattach later" needs the
108
+ * latter.
109
+ */
110
+ sessionFile?: string | null;
74
111
  }
75
112
  export interface ExecUserEvent {
76
113
  type: 'user';
@@ -84,6 +121,22 @@ export interface ExecUserEvent {
84
121
  */
85
122
  source: 'caller' | 'todo_continue';
86
123
  }
124
+ /** One caller input and its complete automatic follow-through chain. */
125
+ export interface ExecTurnStateEvent {
126
+ type: 'turn_state';
127
+ sessionId: string;
128
+ requestSeq: number;
129
+ phase: 'started' | 'completed' | 'failed' | 'cancelled';
130
+ }
131
+ /** Semantic source progress; never includes model or tool content. */
132
+ export interface ExecProgressEvent {
133
+ type: 'execution_progress';
134
+ sessionId: string;
135
+ requestSeq: number;
136
+ progressSeq: number;
137
+ phase: 'model' | 'thinking' | 'tool_arguments' | 'tool' | 'compaction' | 'retry';
138
+ retryDelayMs?: number;
139
+ }
87
140
  export interface ExecTextDeltaEvent {
88
141
  type: 'text_delta';
89
142
  sessionId: string;
@@ -157,6 +210,57 @@ export interface ExecFastReviewEvent {
157
210
  model: string;
158
211
  text: string;
159
212
  }
213
+ /**
214
+ * What the fast tier is ACTUALLY doing, as opposed to what it was configured to
215
+ * do (web-use-tier-cooperation-and-control-closure W2).
216
+ *
217
+ * ONE ADDITIVE EVENT TYPE, AND `EXEC_SCHEMA_VERSION` DOES NOT MOVE — the same
218
+ * rule `ExecCompactionEvent` states. A consumer that ignores event types it does
219
+ * not know is unaffected.
220
+ *
221
+ * THE PROBLEM IT SOLVES. `fast_review` only fires when a review produced TEXT,
222
+ * so every other outcome — a review that failed, a tier that switched itself off
223
+ * after three failures, a budget that ran out, a delegation that did or did not
224
+ * happen — was invisible to a wrapper. The single most likely and least
225
+ * self-diagnosable failure ("it runs but not one review card ever arrives") had
226
+ * no signal at all on this stream.
227
+ *
228
+ * IT IS A STATE EVENT, NOT A LOG LINE. It fires on `tier_changed` and on every
229
+ * `review_end`, which in a session with reviews is a handful per hundred turns.
230
+ */
231
+ export interface ExecFastTierEvent {
232
+ type: 'fast_tier';
233
+ sessionId: string;
234
+ /** Taken from the runner's own turn counter — `tier_changed` carries none. */
235
+ turn: number;
236
+ /** The tier resolved AND the live switch is on. */
237
+ live: boolean;
238
+ /**
239
+ * The CLI switched the fast REVIEWS off by itself after three consecutive
240
+ * non-transient failures.
241
+ *
242
+ * ORTHOGONAL TO `live` AND NOT DERIVABLE FROM IT: the self-disable leaves
243
+ * `live` at `true`. A consumer reporting "the reviews stopped" MUST read this
244
+ * field; `live === false && reviews > 0` is a predicate that can never be true.
245
+ */
246
+ selfDisabled: boolean;
247
+ model: string;
248
+ sameAsMain: boolean;
249
+ reviews: number;
250
+ reviewBudget: number;
251
+ budgetReached: boolean;
252
+ /** Fast-tier children dispatched — the only evidence delegation is working. */
253
+ delegated: number;
254
+ inFlight: boolean;
255
+ /** Present only on the `review_end` branch. */
256
+ review?: {
257
+ index: number;
258
+ kind: 'ok' | 'advice' | 'empty' | 'failed' | 'dropped';
259
+ detail?: string;
260
+ durationMs: number;
261
+ injected: boolean;
262
+ };
263
+ }
160
264
  /**
161
265
  * Context compaction (context-auto-compaction §4.6).
162
266
  *
@@ -251,7 +355,7 @@ export interface ExecResultEvent {
251
355
  todos: ExecTodoCounts | null;
252
356
  error: ExecErrorInfo | null;
253
357
  }
254
- export type ExecEvent = ExecSystemInitEvent | ExecUserEvent | ExecTextDeltaEvent | ExecThinkingEvent | ExecAssistantEvent | ExecToolCallEvent | ExecToolResultEvent | ExecTodoEvent | ExecTeamEvent | ExecFastReviewEvent | ExecCompactionEvent | ExecRetryEvent | ExecErrorEvent | ExecResultEvent;
358
+ export type ExecEvent = ExecSystemInitEvent | ExecUserEvent | ExecTurnStateEvent | ExecProgressEvent | ExecTextDeltaEvent | ExecThinkingEvent | ExecAssistantEvent | ExecToolCallEvent | ExecToolResultEvent | ExecTodoEvent | ExecTeamEvent | ExecFastReviewEvent | ExecFastTierEvent | ExecCompactionEvent | ExecRetryEvent | ExecErrorEvent | ExecResultEvent;
255
359
  /** Everything `system/init` needs that the runner cannot derive for itself. */
256
360
  export interface ExecInitParams {
257
361
  sessionId: string;
@@ -266,7 +370,40 @@ export interface ExecInitParams {
266
370
  permissionMode: ExecPermissionMode;
267
371
  tools: string[];
268
372
  resumed: boolean;
373
+ /**
374
+ * Absolute path of the session file this run reads and writes; `null` under
375
+ * `--no-save-session`. See `ExecSystemInitEvent.sessionFile`.
376
+ */
377
+ sessionFile?: string | null;
269
378
  }
379
+ /**
380
+ * What this build announces in `system/init`.
381
+ *
382
+ * Stamped by `buildInitEvent`, never by a caller - the same discipline as
383
+ * `schemaVersion`. A caller that could set it could claim a capability the
384
+ * binary does not have, which is precisely the mistake the field exists to
385
+ * prevent.
386
+ */
387
+ /**
388
+ * APPEND ONLY, NEVER REORDER. Consumers use `includes()`, but the next one may
389
+ * not; inserting ahead of `'interrupt'` costs nothing and makes a diff look
390
+ * bigger than it is.
391
+ *
392
+ * · `'interrupt'` — `{"type":"interrupt"}` on stdin settles the turn
393
+ * without killing the process.
394
+ * · `'fast-policy'` — this build understands `--fast-delegate` and
395
+ * `--fast-review`. FOR AFTER-THE-FACT DISPLAY ONLY:
396
+ * argv is fixed before `system/init` arrives, so it
397
+ * cannot gate whether the flags are passed.
398
+ * · `'fast-tier-events'` — this build emits `fast_tier`. Its ABSENCE is what
399
+ * lets a wrapper say "this CLI does not report" and
400
+ * not "the fast tier did nothing" — two very
401
+ * different sentences to a user.
402
+ * · `'session-file'` — `system/init` carries `sessionFile`, so a wrapper
403
+ * can check on disk whether a stopped conversation
404
+ * is still resumable instead of guessing the path.
405
+ */
406
+ export declare const EXEC_CAPABILITIES: readonly string[];
270
407
  export declare function buildInitEvent(params: ExecInitParams): ExecSystemInitEvent;
271
408
  /** Everything `result` needs. `schemaVersion` is stamped here, never by a caller. */
272
409
  export type ExecResultParams = Omit<ExecResultEvent, 'type' | 'schemaVersion'>;
@@ -19,6 +19,41 @@
19
19
  * additive change and every wrapper would pin an exact number forever.
20
20
  */
21
21
  export const EXEC_SCHEMA_VERSION = 1;
22
+ /**
23
+ * What this build announces in `system/init`.
24
+ *
25
+ * Stamped by `buildInitEvent`, never by a caller - the same discipline as
26
+ * `schemaVersion`. A caller that could set it could claim a capability the
27
+ * binary does not have, which is precisely the mistake the field exists to
28
+ * prevent.
29
+ */
30
+ /**
31
+ * APPEND ONLY, NEVER REORDER. Consumers use `includes()`, but the next one may
32
+ * not; inserting ahead of `'interrupt'` costs nothing and makes a diff look
33
+ * bigger than it is.
34
+ *
35
+ * · `'interrupt'` — `{"type":"interrupt"}` on stdin settles the turn
36
+ * without killing the process.
37
+ * · `'fast-policy'` — this build understands `--fast-delegate` and
38
+ * `--fast-review`. FOR AFTER-THE-FACT DISPLAY ONLY:
39
+ * argv is fixed before `system/init` arrives, so it
40
+ * cannot gate whether the flags are passed.
41
+ * · `'fast-tier-events'` — this build emits `fast_tier`. Its ABSENCE is what
42
+ * lets a wrapper say "this CLI does not report" and
43
+ * not "the fast tier did nothing" — two very
44
+ * different sentences to a user.
45
+ * · `'session-file'` — `system/init` carries `sessionFile`, so a wrapper
46
+ * can check on disk whether a stopped conversation
47
+ * is still resumable instead of guessing the path.
48
+ */
49
+ export const EXEC_CAPABILITIES = [
50
+ 'interrupt',
51
+ 'fast-policy',
52
+ 'fast-tier-events',
53
+ 'session-file',
54
+ 'turn-lifecycle',
55
+ 'execution-progress',
56
+ ];
22
57
  export function buildInitEvent(params) {
23
58
  return {
24
59
  type: 'system',
@@ -32,6 +67,13 @@ export function buildInitEvent(params) {
32
67
  permissionMode: params.permissionMode,
33
68
  tools: params.tools,
34
69
  resumed: params.resumed,
70
+ capabilities: [...EXEC_CAPABILITIES],
71
+ // `?? null` rather than a conditional spread: the field's contract is
72
+ // three-valued for a CONSUMER (`undefined` = an old build that cannot say,
73
+ // `null` = this run saves nothing, a string = here it is), and omitting it
74
+ // when a caller passed nothing would make this build indistinguishable from
75
+ // one that predates the field.
76
+ sessionFile: params.sessionFile ?? null,
35
77
  };
36
78
  }
37
79
  export function buildResultEvent(params) {