@aragon-agent/cli 0.6.2 → 0.6.4

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 (167) hide show
  1. package/CHANGELOG.md +321 -0
  2. package/README.md +297 -2
  3. package/dist/agent/controller.d.ts +193 -1
  4. package/dist/agent/controller.js +475 -2
  5. package/dist/agent/headless.d.ts +8 -0
  6. package/dist/agent/headless.js +47 -0
  7. package/dist/agent/reducer.d.ts +189 -0
  8. package/dist/agent/reducer.js +301 -2
  9. package/dist/agent/system-prompt.d.ts +14 -0
  10. package/dist/agent/system-prompt.js +2 -0
  11. package/dist/cli.js +297 -16
  12. package/dist/commands/builtins.js +165 -24
  13. package/dist/commands/registry.d.ts +27 -0
  14. package/dist/compaction/archive.d.ts +175 -0
  15. package/dist/compaction/archive.js +267 -0
  16. package/dist/compaction/child.d.ts +97 -0
  17. package/dist/compaction/child.js +118 -0
  18. package/dist/compaction/command.d.ts +41 -0
  19. package/dist/compaction/command.js +355 -0
  20. package/dist/compaction/compactor.d.ts +302 -0
  21. package/dist/compaction/compactor.js +810 -0
  22. package/dist/compaction/digest.d.ts +62 -0
  23. package/dist/compaction/digest.js +164 -0
  24. package/dist/compaction/limits.d.ts +165 -0
  25. package/dist/compaction/limits.js +172 -0
  26. package/dist/compaction/pressure.d.ts +135 -0
  27. package/dist/compaction/pressure.js +167 -0
  28. package/dist/compaction/summarize-call.d.ts +70 -0
  29. package/dist/compaction/summarize-call.js +86 -0
  30. package/dist/compaction/summary-prompt.d.ts +85 -0
  31. package/dist/compaction/summary-prompt.js +197 -0
  32. package/dist/compaction/tail-budget.d.ts +52 -0
  33. package/dist/compaction/tail-budget.js +67 -0
  34. package/dist/compaction/types.d.ts +215 -0
  35. package/dist/compaction/types.js +22 -0
  36. package/dist/compaction/wiring.d.ts +264 -0
  37. package/dist/compaction/wiring.js +577 -0
  38. package/dist/config/app-paths.d.ts +14 -0
  39. package/dist/config/app-paths.js +16 -0
  40. package/dist/config/cli-commands.d.ts +41 -0
  41. package/dist/config/cli-commands.js +77 -1
  42. package/dist/config/env.d.ts +5 -2
  43. package/dist/config/env.js +140 -3
  44. package/dist/config/load.d.ts +42 -0
  45. package/dist/config/load.js +101 -1
  46. package/dist/config/schema.d.ts +244 -0
  47. package/dist/config/schema.js +170 -0
  48. package/dist/config/store.js +37 -1
  49. package/dist/config/ui-state.d.ts +28 -1
  50. package/dist/config/ui-state.js +9 -0
  51. package/dist/exec/events.d.ts +50 -1
  52. package/dist/exec/runner.js +42 -0
  53. package/dist/input/limits.d.ts +72 -0
  54. package/dist/input/limits.js +63 -0
  55. package/dist/input/mouse-events.d.ts +31 -5
  56. package/dist/input/mouse-events.js +86 -27
  57. package/dist/input/paste-parse.d.ts +71 -0
  58. package/dist/input/paste-parse.js +95 -0
  59. package/dist/input/stdin-filter.d.ts +81 -0
  60. package/dist/input/stdin-filter.js +379 -0
  61. package/dist/logging/install.d.ts +53 -1
  62. package/dist/logging/install.js +81 -1
  63. package/dist/logging/logger.d.ts +1 -1
  64. package/dist/proc/classify.d.ts +52 -0
  65. package/dist/proc/classify.js +143 -0
  66. package/dist/proc/kill-tree.d.ts +42 -0
  67. package/dist/proc/kill-tree.js +119 -0
  68. package/dist/proc/limits.d.ts +84 -0
  69. package/dist/proc/limits.js +84 -0
  70. package/dist/proc/log-ring.d.ts +68 -0
  71. package/dist/proc/log-ring.js +102 -0
  72. package/dist/proc/prompt.d.ts +24 -0
  73. package/dist/proc/prompt.js +49 -0
  74. package/dist/proc/readiness.d.ts +102 -0
  75. package/dist/proc/readiness.js +219 -0
  76. package/dist/proc/supervisor.d.ts +103 -0
  77. package/dist/proc/supervisor.js +465 -0
  78. package/dist/proc/types.d.ts +175 -0
  79. package/dist/proc/types.js +15 -0
  80. package/dist/session/persist.js +55 -1
  81. package/dist/team/report.js +7 -1
  82. package/dist/team/runtime.d.ts +10 -0
  83. package/dist/team/runtime.js +7 -0
  84. package/dist/team/subagent.d.ts +31 -1
  85. package/dist/team/subagent.js +42 -15
  86. package/dist/team/types.d.ts +8 -0
  87. package/dist/tools/bash-tool.d.ts +51 -2
  88. package/dist/tools/bash-tool.js +321 -51
  89. package/dist/tools/fs-tools.d.ts +17 -0
  90. package/dist/tools/index.d.ts +55 -2
  91. package/dist/tools/index.js +39 -1
  92. package/dist/tools/proc-tools.d.ts +25 -0
  93. package/dist/tools/proc-tools.js +102 -0
  94. package/dist/ui/ActivityLine.d.ts +19 -1
  95. package/dist/ui/ActivityLine.js +10 -2
  96. package/dist/ui/App.d.ts +62 -2
  97. package/dist/ui/App.js +546 -16
  98. package/dist/ui/Composer.d.ts +66 -1
  99. package/dist/ui/Composer.js +50 -6
  100. package/dist/ui/PromptInput.d.ts +82 -3
  101. package/dist/ui/PromptInput.js +232 -49
  102. package/dist/ui/StatusBar.d.ts +59 -0
  103. package/dist/ui/StatusBar.js +16 -4
  104. package/dist/ui/TeamPanel.js +7 -1
  105. package/dist/ui/Transcript.d.ts +21 -1
  106. package/dist/ui/Transcript.js +61 -3
  107. package/dist/ui/clipboard.d.ts +52 -0
  108. package/dist/ui/clipboard.js +91 -0
  109. package/dist/ui/composer-limits.d.ts +31 -0
  110. package/dist/ui/composer-limits.js +50 -0
  111. package/dist/ui/composer-rows.d.ts +73 -0
  112. package/dist/ui/composer-rows.js +201 -0
  113. package/dist/ui/editor-reducer.d.ts +37 -0
  114. package/dist/ui/editor-reducer.js +88 -12
  115. package/dist/ui/entries/CompactionCard.d.ts +75 -0
  116. package/dist/ui/entries/CompactionCard.js +158 -0
  117. package/dist/ui/entries/ServiceCard.d.ts +66 -0
  118. package/dist/ui/entries/ServiceCard.js +116 -0
  119. package/dist/ui/entries/UserEntry.d.ts +17 -3
  120. package/dist/ui/entries/UserEntry.js +34 -5
  121. package/dist/ui/frame-differ.d.ts +46 -0
  122. package/dist/ui/frame-differ.js +94 -10
  123. package/dist/ui/gauge.d.ts +16 -1
  124. package/dist/ui/gauge.js +25 -6
  125. package/dist/ui/glyphs.d.ts +11 -0
  126. package/dist/ui/glyphs.js +2 -0
  127. package/dist/ui/layout/ScrollViewport.d.ts +43 -6
  128. package/dist/ui/layout/ScrollViewport.js +128 -21
  129. package/dist/ui/layout/budget.d.ts +44 -9
  130. package/dist/ui/layout/budget.js +42 -10
  131. package/dist/ui/layout/follow-state.d.ts +137 -0
  132. package/dist/ui/layout/follow-state.js +150 -0
  133. package/dist/ui/layout/virtual-window.d.ts +11 -0
  134. package/dist/ui/layout/virtual-window.js +85 -1
  135. package/dist/ui/overlays/HelpOverlay.js +9 -1
  136. package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
  137. package/dist/ui/overlays/QuestionOverlay.js +5 -1
  138. package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
  139. package/dist/ui/overlays/SettingsScreen.js +74 -2
  140. package/dist/ui/palettes.d.ts +16 -0
  141. package/dist/ui/palettes.js +6 -0
  142. package/dist/ui/paste-frames.d.ts +87 -0
  143. package/dist/ui/paste-frames.js +112 -0
  144. package/dist/ui/paste-tokens.d.ts +90 -0
  145. package/dist/ui/paste-tokens.js +147 -0
  146. package/dist/ui/screen.d.ts +38 -0
  147. package/dist/ui/screen.js +113 -8
  148. package/dist/ui/selection/highlight.d.ts +39 -0
  149. package/dist/ui/selection/highlight.js +64 -0
  150. package/dist/ui/selection/screen-mirror.d.ts +24 -0
  151. package/dist/ui/selection/screen-mirror.js +35 -0
  152. package/dist/ui/selection/selection-controller.d.ts +88 -0
  153. package/dist/ui/selection/selection-controller.js +282 -0
  154. package/dist/ui/selection/selection.d.ts +90 -0
  155. package/dist/ui/selection/selection.js +223 -0
  156. package/dist/ui/stdout-frame-writer.d.ts +33 -0
  157. package/dist/ui/stdout-frame-writer.js +15 -0
  158. package/dist/ui/theme.d.ts +11 -0
  159. package/dist/ui/theme.js +2 -0
  160. package/dist/ui/transcript-text.js +72 -0
  161. package/dist/ui/use-startup-notices.d.ts +27 -1
  162. package/dist/ui/use-startup-notices.js +43 -11
  163. package/dist/ui/use-wheel-routing.d.ts +1 -1
  164. package/dist/ui/use-wheel-routing.js +8 -0
  165. package/package.json +4 -2
  166. package/dist/input/stdin-mouse-filter.d.ts +0 -42
  167. package/dist/input/stdin-mouse-filter.js +0 -153
@@ -9,7 +9,7 @@
9
9
  * from the event stream by the reducer (TUI) and the headless writer.
10
10
  */
11
11
  import { Agent, initProviders, ModelRegistry, } from '@aragon-agent/core';
12
- import { clampFastConfig, clampRetryConfig, clampTeamConfig, clampTodoConfig, isAdapterProvider, toRetryPolicy, } from '../config/schema.js';
12
+ import { clampCompactionConfig, clampFastConfig, clampRetryConfig, clampTeamConfig, clampTodoConfig, isAdapterProvider, toRetryPolicy, } from '../config/schema.js';
13
13
  import { makeGetApiKey } from '../config/load.js';
14
14
  import { updatePersistedConfig } from '../config/store.js';
15
15
  import { createBuiltinTools } from '../tools/index.js';
@@ -20,8 +20,14 @@ import { TeamHumanQueue } from '../team/human-queue.js';
20
20
  import { createTaskTool } from '../team/task-tool.js';
21
21
  import { buildTeamBlock } from '../team/prompt.js';
22
22
  import { buildFastBlock } from '../fast/prompt.js';
23
+ import { ProcSupervisor } from '../proc/supervisor.js';
24
+ import { buildBackgroundServicesBlock } from '../proc/prompt.js';
25
+ import { PROC_LIMITS } from '../proc/limits.js';
26
+ import { makeBashKill, makeBashOutput } from '../tools/proc-tools.js';
27
+ import { addSignalHook } from '../logging/install.js';
23
28
  import { describeFastTierProblem, fastProviderOf, resolveFastTier } from '../fast/resolve.js';
24
29
  import { FastWiring, offFastStatus } from '../fast/wiring.js';
30
+ import { CompactionWiring, offCompactionSnapshot } from '../compaction/wiring.js';
25
31
  import { TodoStore } from '../todo/store.js';
26
32
  import { createTodoTool } from '../todo/todo-tool.js';
27
33
  import { buildTodoBlock } from '../todo/prompt.js';
@@ -32,6 +38,15 @@ import { createNodeSkillHost } from '../skills/node-host.js';
32
38
  import { SkillService } from '../skills/service.js';
33
39
  import { createSkillTools } from '../skills/tools.js';
34
40
  import { buildSystemPrompt } from './system-prompt.js';
41
+ /**
42
+ * Extra budget, on top of `PROC_LIMITS.killGraceMs`, that `prompt()` allows a
43
+ * force-stopped engine to unwind in before it starts a new run anyway (I-8).
44
+ *
45
+ * `bash`'s own kill grace is what a wedged foreground command costs; this covers
46
+ * the loop's own unwinding on top of it. Waiting FOREVER is not an option - a
47
+ * tool that never settles is exactly why rung two exists.
48
+ */
49
+ const ENGINE_UNWIND_GRACE_MS = 3000;
35
50
  /** Approval gate used whenever there is provably no human to ask (headless). */
36
51
  export const DENY_ALL_APPROVAL = {
37
52
  canPrompt: () => false,
@@ -153,6 +168,66 @@ export class AgentController {
153
168
  // property, and `<fast_tier>` is never spliced (I-2).
154
169
  // -----------------------------------------------------------------------
155
170
  fast;
171
+ // -----------------------------------------------------------------------
172
+ // Context compaction (context-auto-compaction §3.2.1)
173
+ //
174
+ // ONE FIELD AND THIN FORWARDERS, the `fast` shape one feature later, and for
175
+ // the same budget reason (C-16): every piece of state — the guards, the
176
+ // ladder, the manual queue, the session totals — lives in
177
+ // `compaction/wiring.ts` and `compaction/compactor.ts`.
178
+ //
179
+ // `null` when `compaction.enabled` was false at construction, which is the
180
+ // byte-identity branch: no manager is passed to `new Agent({...})`, nothing is
181
+ // subscribed, no registry is allocated, and the engine loop's checkpoint is one
182
+ // `if` against a field that is genuinely absent (AC-1).
183
+ //
184
+ // CONSTRUCTED ABOVE `new Agent({...})` AND ATTACHED BELOW IT (C-12 / P0-2).
185
+ // The port has to be inside the Agent's constructor argument and the wiring has
186
+ // to subscribe to the Agent, and this class cannot do both in one step.
187
+ // -----------------------------------------------------------------------
188
+ compaction;
189
+ // -----------------------------------------------------------------------
190
+ // Background services (background-service-supervision §3.3 / §3.6)
191
+ //
192
+ // CONSTRUCTED UNCONDITIONALLY, and that is the one thing about this field that
193
+ // is easy to get wrong (P1-6 / D-9). `cfg.bash.background` gates the SERVICE
194
+ // half - the `background` tool param, `bash_output`/`bash_kill`, the prompt
195
+ // block, the status chip - and NEVER the foreground half, because G2 ("bash
196
+ // always settles") and G3 ("Esc, Esc always works, and the session stays
197
+ // usable") are promised unconditionally. Gating the whole supervisor on the
198
+ // flag would make rung two of the Esc ladder a silent no-op for every user who
199
+ // turned background services off.
200
+ //
201
+ // With the flag off it holds foreground pids, nothing else, and allocates no
202
+ // timers.
203
+ // -----------------------------------------------------------------------
204
+ procs;
205
+ /**
206
+ * The host's notice channel, kept so `prompt()`'s totality has somewhere to
207
+ * report to (I-8). Every other consumer reads `deps.notify` through a closure
208
+ * built in the constructor; this one is called from a method, which is why it
209
+ * needs a field.
210
+ */
211
+ notifyHostFn;
212
+ /** Whether the SERVICE half is on: tools, prompt block, `background` param. */
213
+ bashBackgroundRegistered;
214
+ /** Release the exit reaper. Called by `dispose()`. */
215
+ releaseSignalHook;
216
+ /**
217
+ * Bumped by `forceStop()`; captured by `App` when it subscribes to a run.
218
+ *
219
+ * THE VIEW AND THE ENGINE DELIBERATELY DISAGREE AFTER A FORCE-STOP (D-10 /
220
+ * I-7). Rung two dispatches `runEnd` locally so the composer is usable
221
+ * immediately, but the engine keeps unwinding and keeps EMITTING -
222
+ * `turn_start`, `tool_call_start`, `turn_end`, `agent_end` - and
223
+ * `runStart`/`turnStart` would set `status: 'running'` again, bouncing the
224
+ * view straight back out of the `idle` the user was just given and appending
225
+ * entries to a transcript they believe is finished. `App` drops every event
226
+ * whose generation is stale, which is the same shape as `abortRequested`
227
+ * above and exists for the same reason: the loop legitimately emits after an
228
+ * abort lands.
229
+ */
230
+ runGen = 0;
156
231
  /**
157
232
  * Set SYNCHRONOUSLY by `abort()` before `agent.abort()` (§3.5.4a guard 1).
158
233
  * Cleared at the next `agent_start`.
@@ -217,6 +292,11 @@ export class AgentController {
217
292
  this.teamRuntime = this.teamRegistered
218
293
  ? new TeamRuntime({
219
294
  getConfig: () => this.config,
295
+ // Read LIVE through the wiring, exactly as `resolveTier` below is: the
296
+ // closure is only ever called from inside a dispatch, and
297
+ // `childFactory()` re-reads `compaction.subagents` on every call so a
298
+ // settings-screen edit reaches the next dispatch.
299
+ contextManagerFor: (req) => this.compaction?.childFactory()?.(req),
220
300
  // Read LIVE through the wiring, which does not exist yet — the closure
221
301
  // is only ever called from inside a dispatch, long after construction.
222
302
  resolveTier: (tier) => this.fast
@@ -266,6 +346,19 @@ export class AgentController {
266
346
  // reason the field's own comment gives: the binding below is spread
267
347
  // conditionally on it (§3.1.3 / P1-6).
268
348
  this.toolOutputs = config.liveToolOutput ? createToolOutputStore() : null;
349
+ // ALSO ABOVE `createBuiltinTools`, and UNCONDITIONAL - see the field's own
350
+ // comment. `readyTimeoutMs` is read through a closure rather than captured,
351
+ // so a live config change reaches the next service rather than the next
352
+ // launch of the CLI.
353
+ this.procs = new ProcSupervisor({ readyTimeoutMs: () => this.config.bash.readyTimeoutMs });
354
+ this.notifyHostFn = deps.notify;
355
+ this.bashBackgroundRegistered = config.bash.background;
356
+ // THE EXIT REAPER (G5 / P1-2 / R-10). An APPEND-ONLY hook rather than a
357
+ // second `setSignalTerminator`: that is a single slot `cli.tsx` already owns
358
+ // for the alternate-screen restore, and replacing it is the documented bug
359
+ // that leaves the user staring at a blank alternate screen. `reapSync` is
360
+ // the only entry point it calls, and it is synchronous end to end (I-9).
361
+ this.releaseSignalHook = addSignalHook(() => this.procs.reapSync());
269
362
  this.tools = createBuiltinTools({
270
363
  getCwd: () => this.cwd,
271
364
  // OWNED HERE, NOT IN `cli.tsx` (D-18): `createBuiltinTools` is called from
@@ -282,6 +375,28 @@ export class AgentController {
282
375
  ...(this.toolOutputs
283
376
  ? { recordOutput: (id, chunk) => this.emitToolOutput('lead', id, chunk) }
284
377
  : {}),
378
+ // THE SERVICE HALF, AND ONLY THE SERVICE HALF (P1-6 / I-2). With
379
+ // `bash.background: false` the options bag has no `procs` key at all, so
380
+ // `createBuiltinTools` adds no property to `deps`, `bash` gets its default
381
+ // `{}` second argument, and the tool is the pre-feature one: no
382
+ // `background` in its schema, the pre-feature description string, no
383
+ // supervisor call. The FOREGROUND half of `this.procs` is still live, and
384
+ // `forceStop()` still reaches it.
385
+ ...(this.bashBackgroundRegistered
386
+ ? {
387
+ procs: this.procs,
388
+ // Read LIVE for the reason every other closure in this bag is:
389
+ // `aragon config set bash.autoBackground false` must take effect in
390
+ // the running session.
391
+ autoBackground: () => this.config.bash.autoBackground,
392
+ startupSettleMs: () => this.config.bash.startupSettleMs,
393
+ // D-15 / P1-8. `planInteractive` is the SAME signal `planTools`
394
+ // already uses to decide that no overlay can be rendered, which is
395
+ // exactly the question being asked here: can a card be drawn, and is
396
+ // there a user who could keep a service alive?
397
+ interactive: this.planInteractive,
398
+ }
399
+ : {}),
285
400
  confirmTools: config.confirmTools,
286
401
  // Routed through the same watchdog pause as the plan tools. This is a
287
402
  // pre-existing bug being fixed in passing: a `--confirm` user who took
@@ -321,6 +436,12 @@ export class AgentController {
321
436
  }),
322
437
  ]
323
438
  : [],
439
+ // THROUGH THE FACTORY for the reason `teamTools` records below (P0-2), and
440
+ // empty when background services were off at construction - which leaves
441
+ // the array byte-identical to the pre-feature build (I-2).
442
+ procTools: this.bashBackgroundRegistered
443
+ ? [makeBashOutput({ supervisor: this.procs }), makeBashKill({ supervisor: this.procs })]
444
+ : [],
324
445
  // THROUGH THE FACTORY, NEVER APPENDED AFTERWARDS (D-15 / P0-1).
325
446
  // `tools.test.ts::C7` asserts `HOST_TOOL_NAMES` equals what
326
447
  // `createBuiltinTools` produces, so appending `task` here would turn that
@@ -379,6 +500,25 @@ export class AgentController {
379
500
  modelId: config.model,
380
501
  ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
381
502
  };
503
+ // --- Context compaction, part 1: BEFORE the Agent ----------------------
504
+ //
505
+ // It needs no agent reference to EXIST — only live config, the key
506
+ // resolvers, the model-info lookup and a notifier — which is exactly what
507
+ // makes the C-12 split work. `getMessages` / `getSystemPrompt` are LAZY for
508
+ // the same reason: `this.agent` is assigned on the very next statement, and
509
+ // they are only ever called from inside an event handler.
510
+ this.compaction = config.compaction.enabled
511
+ ? new CompactionWiring({
512
+ getConfig: () => this.config,
513
+ hasKey: (id) => this.hasApiKey(id),
514
+ getApiKey: (id) => this.resolveKey(id),
515
+ getModelInfoFor: (ref) => this.getModelInfoFor(ref),
516
+ isPricedModel: (ref) => this.isPricedModel(ref),
517
+ getMessages: () => this.agent.state.messages,
518
+ getSystemPrompt: () => this.agent.state.systemPrompt,
519
+ notify: (level, text) => deps.notify?.(level, text),
520
+ })
521
+ : null;
382
522
  this.agent = new Agent({
383
523
  systemPrompt: this.composeSystemPrompt(),
384
524
  model,
@@ -389,6 +529,13 @@ export class AgentController {
389
529
  // without rebuilding the Agent — the closure reads the current config.
390
530
  getApiKey: (id) => this.resolveKey(id),
391
531
  maxTokens: config.maxTokens,
532
+ // SPREAD CONDITIONALLY, never `contextManager: this.compaction?.manager()`.
533
+ // With the feature off the options bag has no such key, so the loop's gate
534
+ // tests a field that is genuinely `undefined` rather than a property
535
+ // holding `undefined` — which is what makes AC-1's byte-identity claim
536
+ // provable by construction rather than by inspection. `createBuiltinTools`
537
+ // above spreads `permission` for the identical reason (P1-2 / AC-28).
538
+ ...(this.compaction ? { contextManager: this.compaction.manager() } : {}),
392
539
  timeouts: {
393
540
  // Timeout invariant (R1): idle ≥ tool so long tool runs are not killed.
394
541
  toolTimeout: config.toolTimeoutMs,
@@ -408,6 +555,18 @@ export class AgentController {
408
555
  },
409
556
  },
410
557
  });
558
+ // --- Context compaction, part 2: AFTER the Agent exists -----------------
559
+ //
560
+ // `attach` is what lets the wiring see `compaction_end` — the ENGINE's
561
+ // `applied` verdict, which the compactor cannot know because validation
562
+ // happens after `compact()` returns — and `agent_start`, which resets the
563
+ // per-run cap. It is idempotent, and `dispose()` unsubscribes.
564
+ //
565
+ // A SEPARATE CALL RATHER THAN A `subscribe` DEP LIKE `FastWiring`'s, and that
566
+ // is the constraint that makes the whole split work: a constructor that
567
+ // subscribed could not run before `new Agent(...)`, and the port has to be
568
+ // inside its constructor argument (C-12 / P0-2).
569
+ this.compaction?.attach((listener) => this.agent.subscribe(listener));
411
570
  // --- Fast tier, part 2: after the Agent exists --------------------------
412
571
  //
413
572
  // `FastWiring` subscribes to the lead's event stream, so it cannot be built
@@ -550,6 +709,12 @@ export class AgentController {
550
709
  review: this.config.fast.review,
551
710
  })
552
711
  : '',
712
+ // ONE FLAG, unlike team/todo's two: there is no live on/off switch for
713
+ // background services - `bash.background` is read once at construction,
714
+ // and the tools it registers cannot be unregistered (the `team.enabled` /
715
+ // `todo.enabled` rule). So the block is present exactly when the tools are,
716
+ // and can never advertise a capability the model does not have.
717
+ backgroundBlock: this.bashBackgroundRegistered ? buildBackgroundServicesBlock() : '',
553
718
  // Read from the FIELD on every rebuild, for the reason the field's own
554
719
  // comment gives. Empty everywhere but `aragon exec`, and `buildSystemPrompt`
555
720
  // splices it conditionally, so every existing prompt is byte-identical.
@@ -638,7 +803,64 @@ export class AgentController {
638
803
  * underlying promise so headless mode can await completion. `agent.prompt()`
639
804
  * never rejects, so callers must not treat resolution as success.
640
805
  */
641
- prompt(text) {
806
+ async prompt(text) {
807
+ // I-8 - THIS METHOD NEVER REJECTS, AND THAT HAS TO BE A PROPERTY OF THE
808
+ // METHOD RATHER THAN A COMMENT ABOVE IT (P0-1).
809
+ //
810
+ // `App` calls this as `void controller.prompt(...)`, and
811
+ // `logging/install.ts` routes an `unhandledRejection` into `handleFatal`,
812
+ // which EXITS THE PROCESS. `Agent.prompt()` throws SYNCHRONOUSLY while
813
+ // `Agent.running` is true - so before the force-stop ladder existed this was
814
+ // merely nearly-true (the view was only ever idle when the engine was), and
815
+ // rung two makes the view idle while the engine is still unwinding. Without
816
+ // the wait and the guard below, "Esc, Esc, then type" would kill the CLI on
817
+ // the very screen this feature exists to rescue.
818
+ //
819
+ // TWO PARTS, BOTH REQUIRED. First WAIT the engine out, bounded, so the
820
+ // common case (the engine unwinds in a few hundred ms) starts a real run
821
+ // rather than reporting a failure. Then swallow anything that still escapes
822
+ // and surface it through the existing notify channel, because a message that
823
+ // cannot be started must produce a visible sentence - never a crash, and
824
+ // never silence.
825
+ if (this.agent.state.isRunning) {
826
+ this.abort();
827
+ await this.waitForEngineIdle();
828
+ }
829
+ try {
830
+ await this.startPrompt(text);
831
+ }
832
+ catch (err) {
833
+ const reason = err instanceof Error ? err.message : String(err);
834
+ getLogger().warn('agent', 'prompt_failed', { reason });
835
+ this.notifyHostFn?.('error', `Could not start the run: ${reason}`);
836
+ }
837
+ }
838
+ /**
839
+ * Wait for the engine to unwind, bounded.
840
+ *
841
+ * BOUNDED BECAUSE THE UNBOUNDED CASE IS THE ONE THIS FEATURE IS ABOUT: a tool
842
+ * that never settles is exactly why rung two exists, and `Agent.waitForIdle()`
843
+ * alone would hang here for as long as it hangs there. The budget is the two
844
+ * graces a wedged `bash` can legitimately spend, plus the executor's own.
845
+ */
846
+ waitForEngineIdle() {
847
+ const budget = PROC_LIMITS.killGraceMs + ENGINE_UNWIND_GRACE_MS;
848
+ return new Promise((resolve) => {
849
+ let done = false;
850
+ const finish = () => {
851
+ if (done)
852
+ return;
853
+ done = true;
854
+ clearTimeout(timer);
855
+ resolve();
856
+ };
857
+ const timer = setTimeout(finish, budget);
858
+ timer.unref?.();
859
+ void this.agent.waitForIdle().then(finish, finish);
860
+ });
861
+ }
862
+ /** The pre-round body of `prompt`, unchanged apart from being private. */
863
+ startPrompt(text) {
642
864
  // A new user message is a new intent, so the previous turn's tool ceiling
643
865
  // expires here and anything a slash command queued is promoted (D-G1 / D-G2).
644
866
  // This and `steer()` below are the ONLY two places a user message enters the
@@ -670,6 +892,11 @@ export class AgentController {
670
892
  // nothing ever clears (RV-1).
671
893
  this.abortRequested = true;
672
894
  this.fast?.abort();
895
+ // The engine also races `ctx.signal` inside `runCompaction`, so this is the
896
+ // same DELIBERATE REDUNDANCY the `teamRuntime` line below records: it makes
897
+ // "Esc cancels an in-flight summarization" a property of this class rather
898
+ // than a consequence of the compactor honouring a signal three modules away.
899
+ this.compaction?.abort();
673
900
  this.agent.abort();
674
901
  // DELIBERATE REDUNDANCY (§3.8). Aborting the lead already aborts the
675
902
  // in-flight `task` call's `ctx.signal`, which the runtime listens to — that
@@ -679,6 +906,115 @@ export class AgentController {
679
906
  // `App.tsx` records for `cancelPendingHuman()` on `agent_end`.
680
907
  this.teamRuntime?.abortAll();
681
908
  }
909
+ /**
910
+ * Rung two of the Esc ladder: stop the AGENT, whatever it is blocked on
911
+ * (§3.6 / G3).
912
+ *
913
+ * FOUR STEPS, AND THE FOURTH IS AN ABSENCE.
914
+ *
915
+ * 1. `abort()` - everything it does today, unchanged.
916
+ * 2. `killForeground('force')` - hard-kill every tracked foreground `bash`
917
+ * child. THIS is what actually unblocks a wedged `await tool.execute`;
918
+ * an abort signal a tool ignores is advisory, and rung two exists for
919
+ * precisely the tools that ignore it.
920
+ * 3. `runGen += 1` - so `App` can drop the events the engine will keep
921
+ * emitting while it unwinds (I-7). Without it the view bounces straight
922
+ * back out of the `idle` the user was just given.
923
+ * 4. IT DOES NOT TOUCH BACKGROUND SERVICES. `Esc` interrupts the AGENT;
924
+ * `Ctrl+C` stops the SERVICES. That asymmetry is the user's own two
925
+ * sentences and it is exactly the kind of thing a later reader will
926
+ * "fix" by making this kill everything - which would mean a user who
927
+ * pressed Esc twice to stop a runaway turn also lost the dev server they
928
+ * had been working against for an hour. It is invariant I-4.
929
+ *
930
+ * UNCONDITIONAL ON `bash.background` (P1-6 / D-9). The supervisor is always
931
+ * constructed; with services off its foreground registry is all there is, and
932
+ * that is the half this method uses.
933
+ */
934
+ forceStop() {
935
+ this.abort();
936
+ const killed = this.procs.killForeground('force');
937
+ this.runGen += 1;
938
+ getLogger().info('agent', 'force_stop', { generation: this.runGen, killed });
939
+ }
940
+ /**
941
+ * The current run generation (I-7).
942
+ *
943
+ * `App` captures it when it subscribes to a run and drops every engine event
944
+ * whose generation is stale. Same shape as the `abortRequested` guard the fast
945
+ * reviewer already uses, and for the same reason: the loop legitimately emits
946
+ * after an abort lands.
947
+ */
948
+ get runGeneration() {
949
+ return this.runGen;
950
+ }
951
+ // -----------------------------------------------------------------------
952
+ // Background services (background-service-supervision §3.5 / §3.6)
953
+ // -----------------------------------------------------------------------
954
+ /** Whether `bash_output` / `bash_kill` are in `this.tools`. Fixed at construction. */
955
+ isBackgroundRegistered() {
956
+ return this.bashBackgroundRegistered;
957
+ }
958
+ /**
959
+ * Subscribe to the CLI-local `ProcEvent` stream.
960
+ *
961
+ * Returns a live unsubscribe even when services are off, because the
962
+ * supervisor exists either way - the contract `subscribeTeam` /
963
+ * `subscribeTodos` / `subscribeFast` / `subscribeToolOutput` already set. With
964
+ * services off nothing is ever emitted, so an unconditional subscription in
965
+ * `App` costs one `Set` entry.
966
+ */
967
+ subscribeProc(listener) {
968
+ return this.procs.subscribe(listener);
969
+ }
970
+ getServiceSnapshot(id) {
971
+ return this.procs.get(id);
972
+ }
973
+ listServices() {
974
+ return this.procs.list();
975
+ }
976
+ /** `starting | ready | running`. Drives the status chip and the Ctrl+C rung. */
977
+ liveServiceCount() {
978
+ return this.procs.liveCount();
979
+ }
980
+ readServiceLog(id, since) {
981
+ return this.procs.read(id, since);
982
+ }
983
+ /**
984
+ * Stop one service, or every service when `id` is `'all'`.
985
+ *
986
+ * The GRACEFUL ladder (`SIGTERM` -> `SIGKILL` after `stopGraceMs`), because
987
+ * this path has a live event loop to run it on. `reapServicesSync` is the
988
+ * other one, and it is the only one an exit path may use.
989
+ */
990
+ stopService(id) {
991
+ return this.procs.stop(id);
992
+ }
993
+ /**
994
+ * Rung one of `Ctrl+C`: stop every live service (§3.6 / G4).
995
+ *
996
+ * `force: true` means `reapSync()` SEMANTICS - immediate `SIGKILL`, no
997
+ * `stopGraceMs` - and is what `doExit` uses, because `doExit` awaits nothing
998
+ * and must not start (§3.6 step 3). The graceful ladder belongs to `Ctrl+C`,
999
+ * `bash_kill` and `/bg stop`.
1000
+ */
1001
+ stopAllServices(opts = {}) {
1002
+ if (opts.force) {
1003
+ this.procs.reapSync();
1004
+ return Promise.resolve(this.procs.list());
1005
+ }
1006
+ return this.procs.stop('all');
1007
+ }
1008
+ /**
1009
+ * The synchronous reaper (I-9).
1010
+ *
1011
+ * Exposed so `doExit` can call it without awaiting anything. It is also what
1012
+ * the signal hook registered in the constructor calls, and it is idempotent -
1013
+ * which it has to be, since a normal quit reaches it twice.
1014
+ */
1015
+ reapServicesSync() {
1016
+ this.procs.reapSync();
1017
+ }
682
1018
  steer(text) {
683
1019
  // Mid-run interjection: promote the queue, keep what is already in force
684
1020
  // (D-G3). Clearing here would make "ask a follow-up" the way around the
@@ -870,6 +1206,12 @@ export class AgentController {
870
1206
  dispose() {
871
1207
  this.teamRuntime?.dispose();
872
1208
  this.fast?.dispose();
1209
+ this.compaction?.dispose();
1210
+ // REAP BEFORE RELEASING THE HOOK, in that order: disposal is a teardown, and
1211
+ // a service that outlives it has nothing left that could ever stop it.
1212
+ this.procs.reapSync();
1213
+ this.releaseSignalHook();
1214
+ this.procs.dispose();
873
1215
  }
874
1216
  // -----------------------------------------------------------------------
875
1217
  // Todo planning (todo-plan-execution §3.6a)
@@ -991,6 +1333,10 @@ export class AgentController {
991
1333
  // another vendor's gateway — and `<fast_tier>` would go on naming the model
992
1334
  // it used to resolve to.
993
1335
  this.fast?.onConfigChanged();
1336
+ // The summarizer resolves from LIVE config too — it may BE the fast tier —
1337
+ // so the snapshot the chip and `/compact status` read has to be refreshed
1338
+ // for the same reason and at the same moments (the RV-3 lesson).
1339
+ this.compaction?.onConfigChanged();
994
1340
  }
995
1341
  setThinkingLevel(level) {
996
1342
  this.config = { ...this.config, thinkingLevel: level };
@@ -1010,6 +1356,10 @@ export class AgentController {
1010
1356
  // Rule 5 of §3.2 reads `apiKeys`, so a key edit is a tier event: this is the
1011
1357
  // path that turns `no_key` back into a working tier without a relaunch.
1012
1358
  this.fast?.onConfigChanged();
1359
+ // The summarizer resolves from LIVE config too — it may BE the fast tier —
1360
+ // so the snapshot the chip and `/compact status` read has to be refreshed
1361
+ // for the same reason and at the same moments (the RV-3 lesson).
1362
+ this.compaction?.onConfigChanged();
1013
1363
  }
1014
1364
  setCwd(cwd) {
1015
1365
  this.cwd = cwd;
@@ -1082,6 +1432,10 @@ export class AgentController {
1082
1432
  }
1083
1433
  clearMessages() {
1084
1434
  this.agent.clearMessages();
1435
+ // INVALIDATION SITE 3 (context-auto-compaction-hardening §3.2.3). The history
1436
+ // was CLEARED, not appended to, so the prefix length the last measurement
1437
+ // covered indexes an array that no longer exists.
1438
+ this.compaction?.onHistoryReplaced();
1085
1439
  // A new conversation has no loaded skills, so the "already loaded earlier
1086
1440
  // in this conversation" hint would otherwise start lying (P2-9).
1087
1441
  this.skills.getRegistry().clearActive();
@@ -1098,6 +1452,10 @@ export class AgentController {
1098
1452
  }
1099
1453
  replaceMessages(messages) {
1100
1454
  this.agent.replaceMessages(messages);
1455
+ // INVALIDATION SITE 4 (§3.2.3) - `/resume`, and any future host-side rewrite.
1456
+ // `estimateAppendedTokens` bounds-checks as well, so a MISSED site costs
1457
+ // accuracy and never correctness; that belt does not make this call optional.
1458
+ this.compaction?.onHistoryReplaced();
1101
1459
  }
1102
1460
  getMessages() {
1103
1461
  return this.agent.state.messages;
@@ -1191,6 +1549,10 @@ export class AgentController {
1191
1549
  // was never registered) nothing has to be rebuilt, because no block is
1192
1550
  // spliced either way.
1193
1551
  this.fast?.onConfigChanged();
1552
+ // The summarizer resolves from LIVE config too — it may BE the fast tier —
1553
+ // so the snapshot the chip and `/compact status` read has to be refreshed
1554
+ // for the same reason and at the same moments (the RV-3 lesson).
1555
+ this.compaction?.onConfigChanged();
1194
1556
  return fast;
1195
1557
  }
1196
1558
  /** Everything `/fast status`, the settings line and the chip read. */
@@ -1209,6 +1571,117 @@ export class AgentController {
1209
1571
  return () => { };
1210
1572
  return this.fast.subscribe(listener);
1211
1573
  }
1574
+ // -----------------------------------------------------------------------
1575
+ // Context compaction (context-auto-compaction §3.2.1) — THIN FORWARDERS
1576
+ // -----------------------------------------------------------------------
1577
+ /** Whether a `ContextManager` exists at all. Fixed at construction. */
1578
+ isCompactionRegistered() {
1579
+ return this.compaction !== null;
1580
+ }
1581
+ /** Whether it will actually fire. Flipped live by `/compact on|off`. */
1582
+ isCompactionEnabled() {
1583
+ return this.compaction?.isEnabled() === true;
1584
+ }
1585
+ /**
1586
+ * Flip the live switch.
1587
+ *
1588
+ * A NO-OP WHEN NOTHING IS REGISTERED, and `/compact` reports that honestly by
1589
+ * checking `isCompactionRegistered()` first — the `/fast on` and `/team on`
1590
+ * shape. `enabled` decides whether the wiring EXISTS, which happens once at
1591
+ * construction and cannot be undone: a session that started with no manager
1592
+ * would have to be given one mid-flight, and the `Agent` has no setter for it
1593
+ * on purpose (§3.2).
1594
+ */
1595
+ setCompactionEnabled(enabled) {
1596
+ this.compaction?.setEnabled(enabled);
1597
+ }
1598
+ getCompactionConfig() {
1599
+ return this.config.compaction;
1600
+ }
1601
+ /**
1602
+ * Adopt a new compaction config for this session.
1603
+ *
1604
+ * THE `onConfigChanged` CALL IS NOT OPTIONAL, for the reason `setFastConfig`
1605
+ * records: a persist without it reports success and changes nothing visible
1606
+ * until relaunch, because the chip and `/compact status` read a snapshot.
1607
+ */
1608
+ setCompactionConfig(patch) {
1609
+ const compaction = clampCompactionConfig({ ...this.config.compaction, ...patch });
1610
+ this.config = { ...this.config, compaction };
1611
+ this.compaction?.onConfigChanged();
1612
+ return compaction;
1613
+ }
1614
+ /** Everything `/compact status`, the settings row and the chip read. */
1615
+ getCompactionSnapshot() {
1616
+ return this.compaction ? this.compaction.snapshot() : offCompactionSnapshot();
1617
+ }
1618
+ /**
1619
+ * The `ModelRef` the next compaction would summarize with, or `null`.
1620
+ *
1621
+ * IT IS CARRIED, NEVER RE-DERIVED, and that is the whole reason it exists. The
1622
+ * summarizer is the fast tier when `useFastTier` is on AND the tier resolves,
1623
+ * and the session's own model otherwise — a combination no caller can
1624
+ * reconstruct from `CliConfig` without re-implementing `resolveFastTier`, and
1625
+ * one that a plausible guess (`fast.enabled ? fast.provider : provider`) gets
1626
+ * WRONG whenever `fast.enabled` is true and `compaction.useFastTier` is false.
1627
+ * Pricing the spend against the wrong provider looks up a model the static
1628
+ * table has never seen, which prices it at `$0.00` — the C-11 / RV-4 lie, on
1629
+ * the one feature that spends money without being asked (AC-15).
1630
+ */
1631
+ getCompactionSummarizerRef() {
1632
+ return this.compaction?.summarizerRef() ?? null;
1633
+ }
1634
+ /**
1635
+ * This run's archive key, or `null` when compaction is unregistered
1636
+ * (context-auto-compaction-hardening §3.5 / W4).
1637
+ *
1638
+ * `/compact history` and `/compact show` scope themselves to it, so a second
1639
+ * `aragon` in another terminal is invisible here rather than interleaved with
1640
+ * this one's compactions.
1641
+ */
1642
+ getCompactionRunId() {
1643
+ return this.compaction?.archiveRunId() ?? null;
1644
+ }
1645
+ /** Ask for a compaction at the next turn boundary (§4.4 / D-17). */
1646
+ queueCompaction(instructions) {
1647
+ this.compaction?.queueManual(instructions);
1648
+ }
1649
+ /**
1650
+ * The IDLE `/compact` path (§4.4 / D-25).
1651
+ *
1652
+ * IT VALIDATES BEFORE IT SPLICES. `CompactionWiring.compactNow` runs
1653
+ * `validateHistory` itself and refuses on failure, because with no loop running
1654
+ * the engine's own gate is unreachable and D-4 declares that gate
1655
+ * unbypassable. `replaceMessages` validates NOTHING.
1656
+ */
1657
+ async compactNow(instructions) {
1658
+ if (!this.compaction)
1659
+ return { ok: false, reason: 'not_registered' };
1660
+ const controller = new AbortController();
1661
+ const outcome = await this.compaction.compactNow({
1662
+ messages: this.agent.state.messages,
1663
+ systemPrompt: this.agent.state.systemPrompt,
1664
+ model: this.agent.state.model,
1665
+ signal: controller.signal,
1666
+ ...(instructions ? { instructions } : {}),
1667
+ });
1668
+ if (!outcome.ok)
1669
+ return { ok: false, reason: outcome.reason };
1670
+ this.agent.replaceMessages(outcome.messages);
1671
+ return { ok: true };
1672
+ }
1673
+ /**
1674
+ * Subscribe to the CLI-local compaction event stream.
1675
+ *
1676
+ * Returns a no-op unsubscribe when compaction was off at construction, so
1677
+ * every caller can subscribe unconditionally — the contract `subscribeTeam`,
1678
+ * `subscribeTodos` and `subscribeFast` already set.
1679
+ */
1680
+ subscribeCompaction(listener) {
1681
+ if (!this.compaction)
1682
+ return () => { };
1683
+ return this.compaction.subscribe(listener);
1684
+ }
1212
1685
  /** List usable tools (name + description) for the /tools command. */
1213
1686
  listTools() {
1214
1687
  return this.tools;
@@ -9,6 +9,7 @@ import type { PreflightResult } from './controller.js';
9
9
  import type { TeamEvent } from '../team/types.js';
10
10
  import type { TodoEvent, TodoSnapshot } from '../todo/types.js';
11
11
  import type { FastEvent } from '../fast/types.js';
12
+ import type { CompactionEvent } from '../compaction/types.js';
12
13
  import { type FollowThroughMode } from '../todo/follow-through.js';
13
14
  /** The minimal controller surface headless mode needs (real or stubbed). */
14
15
  export interface HeadlessController {
@@ -43,6 +44,13 @@ export interface HeadlessController {
43
44
  * byte-identical to today's.
44
45
  */
45
46
  subscribeFast?(listener: (event: FastEvent) => void): () => void;
47
+ /**
48
+ * The CLI-local compaction stream — OPTIONAL, for the identical reason the
49
+ * three above are (context-auto-compaction §4.7). A controller with no
50
+ * `ContextManager` has no compaction events, and `runHeadless` calls it with
51
+ * `?.`, which keeps a `--no-compaction` headless run byte-identical to today's.
52
+ */
53
+ subscribeCompaction?(listener: (event: CompactionEvent) => void): () => void;
46
54
  /**
47
55
  * The current plan — OPTIONAL, for the identical reason the two subscriptions
48
56
  * above are (todo-plan-followthrough §3.6 rule 2). ABSENT MEANS THE