@aragon-agent/cli 0.6.4 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +163 -37
  3. package/dist/agent/controller.d.ts +49 -8
  4. package/dist/agent/controller.js +160 -48
  5. package/dist/agent/headless.d.ts +2 -1
  6. package/dist/agent/prompt-options.d.ts +15 -0
  7. package/dist/agent/prompt-options.js +1 -0
  8. package/dist/agent/reducer.d.ts +33 -19
  9. package/dist/agent/reducer.js +62 -71
  10. package/dist/agent/usage.d.ts +34 -0
  11. package/dist/agent/usage.js +43 -0
  12. package/dist/cli.js +131 -54
  13. package/dist/commands/builtins.js +12 -1
  14. package/dist/commands/registry.d.ts +2 -3
  15. package/dist/compaction/compactor.d.ts +60 -24
  16. package/dist/compaction/compactor.js +89 -61
  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 +41 -0
  24. package/dist/compaction/wiring.d.ts +47 -0
  25. package/dist/compaction/wiring.js +88 -27
  26. package/dist/config/cli-commands.js +5 -0
  27. package/dist/config/env.js +15 -1
  28. package/dist/config/load.d.ts +17 -0
  29. package/dist/config/load.js +45 -1
  30. package/dist/config/schema.d.ts +44 -0
  31. package/dist/config/schema.js +36 -0
  32. package/dist/config/store.js +27 -1
  33. package/dist/exec/events.d.ts +138 -1
  34. package/dist/exec/events.js +42 -0
  35. package/dist/exec/index.js +43 -6
  36. package/dist/exec/progress-reporter.d.ts +23 -0
  37. package/dist/exec/progress-reporter.js +88 -0
  38. package/dist/exec/runner.d.ts +34 -0
  39. package/dist/exec/runner.js +137 -25
  40. package/dist/fast/reviewer.d.ts +19 -1
  41. package/dist/fast/reviewer.js +22 -0
  42. package/dist/fast/types.d.ts +15 -0
  43. package/dist/fast/wiring.d.ts +10 -1
  44. package/dist/fast/wiring.js +18 -1
  45. package/dist/input/interrupt-gesture.d.ts +18 -0
  46. package/dist/input/interrupt-gesture.js +19 -0
  47. package/dist/input/pointer-router.d.ts +11 -0
  48. package/dist/input/pointer-router.js +14 -0
  49. package/dist/todo/limits.d.ts +4 -17
  50. package/dist/todo/limits.js +4 -17
  51. package/dist/todo/panel-rows.d.ts +8 -6
  52. package/dist/todo/panel-rows.js +19 -6
  53. package/dist/todo/store.d.ts +8 -13
  54. package/dist/todo/store.js +13 -14
  55. package/dist/todo/types.d.ts +2 -1
  56. package/dist/ui/App.d.ts +2 -0
  57. package/dist/ui/App.js +233 -183
  58. package/dist/ui/AutocompletePopup.d.ts +3 -1
  59. package/dist/ui/AutocompletePopup.js +9 -11
  60. package/dist/ui/BottomStatusRow.d.ts +1 -1
  61. package/dist/ui/Composer.d.ts +13 -5
  62. package/dist/ui/Composer.js +7 -11
  63. package/dist/ui/PromptCaret.d.ts +11 -0
  64. package/dist/ui/PromptCaret.js +31 -0
  65. package/dist/ui/PromptInput.d.ts +12 -6
  66. package/dist/ui/PromptInput.js +50 -16
  67. package/dist/ui/StatusBar.d.ts +22 -15
  68. package/dist/ui/StatusBar.js +61 -16
  69. package/dist/ui/TeamPanel.d.ts +3 -1
  70. package/dist/ui/TeamPanel.js +13 -9
  71. package/dist/ui/TodoPanel.d.ts +5 -5
  72. package/dist/ui/TodoPanel.js +56 -24
  73. package/dist/ui/Transcript.js +1 -0
  74. package/dist/ui/UpdateLine.d.ts +1 -1
  75. package/dist/ui/capabilities.js +5 -5
  76. package/dist/ui/composer-rows.js +50 -9
  77. package/dist/ui/exit-snapshot.d.ts +9 -0
  78. package/dist/ui/frame-differ.js +2 -19
  79. package/dist/ui/frame-observer.d.ts +17 -0
  80. package/dist/ui/frame-observer.js +81 -0
  81. package/dist/ui/frame-parser.d.ts +7 -0
  82. package/dist/ui/frame-parser.js +18 -0
  83. package/dist/ui/layout/AppShell.d.ts +7 -52
  84. package/dist/ui/layout/AppShell.js +2 -2
  85. package/dist/ui/layout/ScrollIndicator.d.ts +4 -3
  86. package/dist/ui/layout/ScrollIndicator.js +4 -4
  87. package/dist/ui/layout/ScrollViewport.d.ts +13 -66
  88. package/dist/ui/layout/ScrollViewport.js +183 -185
  89. package/dist/ui/layout/autocomplete.d.ts +18 -0
  90. package/dist/ui/layout/autocomplete.js +26 -0
  91. package/dist/ui/layout/budget.d.ts +3 -51
  92. package/dist/ui/layout/budget.js +4 -53
  93. package/dist/ui/layout/document-layout.d.ts +13 -0
  94. package/dist/ui/layout/document-layout.js +11 -0
  95. package/dist/ui/layout/follow-state.d.ts +1 -0
  96. package/dist/ui/layout/follow-state.js +1 -3
  97. package/dist/ui/layout/rail.d.ts +4 -53
  98. package/dist/ui/layout/rail.js +10 -62
  99. package/dist/ui/layout/scroll-indicator.d.ts +1 -1
  100. package/dist/ui/layout/scroll-indicator.js +3 -2
  101. package/dist/ui/layout/scrollbar-geometry.d.ts +17 -0
  102. package/dist/ui/layout/scrollbar-geometry.js +13 -0
  103. package/dist/ui/layout/team-panel.d.ts +18 -0
  104. package/dist/ui/layout/team-panel.js +22 -0
  105. package/dist/ui/layout/todo-layout.d.ts +25 -0
  106. package/dist/ui/layout/todo-layout.js +28 -0
  107. package/dist/ui/layout/viewport-geometry.d.ts +1 -0
  108. package/dist/ui/layout/viewport-geometry.js +1 -0
  109. package/dist/ui/layout/virtual-window.d.ts +1 -0
  110. package/dist/ui/layout/virtual-window.js +1 -1
  111. package/dist/ui/overlays/HelpOverlay.js +5 -2
  112. package/dist/ui/screen.d.ts +1 -1
  113. package/dist/ui/screen.js +4 -6
  114. package/dist/ui/scrollbar-controller.d.ts +30 -0
  115. package/dist/ui/scrollbar-controller.js +124 -0
  116. package/dist/ui/transcript-text.js +6 -2
  117. package/dist/ui/use-wheel-routing.d.ts +2 -0
  118. package/dist/ui/use-wheel-routing.js +6 -0
  119. package/package.json +2 -2
@@ -28,6 +28,7 @@ import { addSignalHook } from '../logging/install.js';
28
28
  import { describeFastTierProblem, fastProviderOf, resolveFastTier } from '../fast/resolve.js';
29
29
  import { FastWiring, offFastStatus } from '../fast/wiring.js';
30
30
  import { CompactionWiring, offCompactionSnapshot } from '../compaction/wiring.js';
31
+ import { ContextMeter } from '../compaction/meter.js';
31
32
  import { TodoStore } from '../todo/store.js';
32
33
  import { createTodoTool } from '../todo/todo-tool.js';
33
34
  import { buildTodoBlock } from '../todo/prompt.js';
@@ -44,7 +45,7 @@ import { buildSystemPrompt } from './system-prompt.js';
44
45
  *
45
46
  * `bash`'s own kill grace is what a wedged foreground command costs; this covers
46
47
  * 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
+ * tool that never settles is exactly why the force-stop rung exists.
48
49
  */
49
50
  const ENGINE_UNWIND_GRACE_MS = 3000;
50
51
  /** Approval gate used whenever there is provably no human to ask (headless). */
@@ -187,6 +188,23 @@ export class AgentController {
187
188
  // -----------------------------------------------------------------------
188
189
  compaction;
189
190
  // -----------------------------------------------------------------------
191
+ // Context occupancy (context-usage-gauge-accuracy §3.2 / W1)
192
+ //
193
+ // CONSTRUCTED UNCONDITIONALLY, unlike its neighbour, and that asymmetry IS the
194
+ // feature. `compaction` above is `null` for a session started with
195
+ // `--no-compaction` - and before this field existed, that took the only
196
+ // measuring code in the process with it: such a session's gauge had exactly
197
+ // one sample per turn (the provider's `turn_end` usage) and read `0 %` after a
198
+ // `/resume`. Occupancy is not a compaction concern; it is what compaction
199
+ // READS.
200
+ //
201
+ // IT COSTS A HEADLESS HOST NOTHING (I-11). `aragon exec` builds an
202
+ // `AgentController` too, and nothing there reads this number; the meter arms
203
+ // no timer while it has no subscribers, so the extra object is a few scalars
204
+ // and a `Set`.
205
+ // -----------------------------------------------------------------------
206
+ contextMeter;
207
+ // -----------------------------------------------------------------------
190
208
  // Background services (background-service-supervision §3.3 / §3.6)
191
209
  //
192
210
  // CONSTRUCTED UNCONDITIONALLY, and that is the one thing about this field that
@@ -228,6 +246,7 @@ export class AgentController {
228
246
  * abort lands.
229
247
  */
230
248
  runGen = 0;
249
+ startupSequence = 0;
231
250
  /**
232
251
  * Set SYNCHRONOUSLY by `abort()` before `agent.abort()` (§3.5.4a guard 1).
233
252
  * Cleared at the next `agent_start`.
@@ -500,6 +519,28 @@ export class AgentController {
500
519
  modelId: config.model,
501
520
  ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
502
521
  };
522
+ // --- Context occupancy, part 1: BEFORE the Agent, UNCONDITIONALLY ------
523
+ //
524
+ // Built here for the same C-12 reason the wiring is - lazy `getMessages` /
525
+ // `getSystemPrompt` closures, `attach` after `new Agent` - and built for
526
+ // EVERY session, including one with compaction off, because that is the one
527
+ // whose gauge was broken (P1-4 / P0-2).
528
+ this.contextMeter = new ContextMeter({
529
+ getMessages: () => this.agent.state.messages,
530
+ getSystemPrompt: () => this.agent.state.systemPrompt,
531
+ getModelInfo: () => this.getModelInfo(),
532
+ // ONE PREDICATE FOR "IS THE DENOMINATOR REAL". `isPricedModel` is
533
+ // `modelRegistry.getModel(...) !== undefined`, and `getModelInfoFor` falls
534
+ // back to `buildRuntimeModel` - a 128k placeholder AND a zero cost table -
535
+ // for exactly the models it answers `false` for. The two are the same
536
+ // question about the same static table, not a coincidence.
537
+ isWindowKnown: () => this.isPricedModel({
538
+ providerId: this.config.provider,
539
+ modelId: this.config.model,
540
+ }),
541
+ // LIVE, so a settings-screen edit moves the denominator without a relaunch.
542
+ getWindowOverride: () => this.config.contextWindow,
543
+ });
503
544
  // --- Context compaction, part 1: BEFORE the Agent ----------------------
504
545
  //
505
546
  // It needs no agent reference to EXIST — only live config, the key
@@ -517,6 +558,9 @@ export class AgentController {
517
558
  getMessages: () => this.agent.state.messages,
518
559
  getSystemPrompt: () => this.agent.state.systemPrompt,
519
560
  notify: (level, text) => deps.notify?.(level, text),
561
+ // ONE METER PER PROCESS (R-1). The wiring forwards it to the compactor,
562
+ // so the trigger and the gauge cannot read two different numbers.
563
+ meter: this.contextMeter,
520
564
  })
521
565
  : null;
522
566
  this.agent = new Agent({
@@ -567,6 +611,15 @@ export class AgentController {
567
611
  // subscribed could not run before `new Agent(...)`, and the port has to be
568
612
  // inside its constructor argument (C-12 / P0-2).
569
613
  this.compaction?.attach((listener) => this.agent.subscribe(listener));
614
+ // --- Context occupancy, part 2: after the Agent exists -----------------
615
+ //
616
+ // ORDER RELATIVE TO THE LINE ABOVE IS DELIBERATELY IRRELEVANT (I-9 / T7).
617
+ // The append direction is order-free because the meter marks itself dirty
618
+ // and re-measures on read; the replace direction is order-free because the
619
+ // splice is announced from `settlePending`'s own synchronous code rather
620
+ // than from a listener. If a future change makes this ordering matter, the
621
+ // fix is at those two sites, not here.
622
+ this.contextMeter.attach((listener) => this.agent.subscribe(listener));
570
623
  // --- Fast tier, part 2: after the Agent exists --------------------------
571
624
  //
572
625
  // `FastWiring` subscribes to the lead's event stream, so it cannot be built
@@ -803,43 +856,33 @@ export class AgentController {
803
856
  * underlying promise so headless mode can await completion. `agent.prompt()`
804
857
  * never rejects, so callers must not treat resolution as success.
805
858
  */
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
- }
859
+ async prompt(text, options = {}) {
860
+ // Capture BEFORE stopping: synchronous abort listeners may cancel this request.
861
+ const request = ++this.startupSequence;
829
862
  try {
830
- await this.startPrompt(text);
863
+ if (this.agent.state.isRunning) {
864
+ this.stopEngine();
865
+ await this.waitForEngineIdle();
866
+ }
867
+ if (request !== this.startupSequence) {
868
+ return { status: 'not-started', reason: 'cancelled' };
869
+ }
870
+ if (this.agent.state.isRunning)
871
+ throw new Error('The previous run is still stopping.');
872
+ return await this.startPrompt(text, options, request);
831
873
  }
832
874
  catch (err) {
833
875
  const reason = err instanceof Error ? err.message : String(err);
834
876
  getLogger().warn('agent', 'prompt_failed', { reason });
835
877
  this.notifyHostFn?.('error', `Could not start the run: ${reason}`);
878
+ return { status: 'not-started', reason: 'failed' };
836
879
  }
837
880
  }
838
881
  /**
839
882
  * Wait for the engine to unwind, bounded.
840
883
  *
841
884
  * 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()`
885
+ * that never settles is exactly why the force-stop rung exists, and `Agent.waitForIdle()`
843
886
  * alone would hang here for as long as it hangs there. The budget is the two
844
887
  * graces a wedged `bash` can legitimately spend, plus the executor's own.
845
888
  */
@@ -859,31 +902,25 @@ export class AgentController {
859
902
  void this.agent.waitForIdle().then(finish, finish);
860
903
  });
861
904
  }
862
- /** The pre-round body of `prompt`, unchanged apart from being private. */
863
- startPrompt(text) {
864
- // A new user message is a new intent, so the previous turn's tool ceiling
865
- // expires here and anything a slash command queued is promoted (D-G1 / D-G2).
866
- // This and `steer()` below are the ONLY two places a user message enters the
867
- // engine, which is what makes a per-turn scope implementable at all (FG9).
905
+ /** Prepare before clearing the plan, then enter the engine without yielding. */
906
+ async startPrompt(text, options, request) {
868
907
  if (this.skillsEnabled)
869
908
  this.skills.beginUserTurn();
870
- // The ask-round budget is per USER TURN and resets here for the same reason
871
- // the skill frame does. `steer()` deliberately does NOT reset it: a user
872
- // steering "actually, use Postgres" into a running plan is answering the
873
- // questions, not buying four more rounds of them.
874
909
  this.askRounds = 0;
875
- // Same asymmetry, same reason (§3.5): a completed plan is dropped HERE
876
- // rather than at `agent_end`, so the user gets their `7/7 done` moment, and
877
- // an unfinished one survives "continue" / "now do the rest" but not four
878
- // unrelated turns.
879
- this.todos?.beginUserTurn();
880
- // The fast reviewer is asked whether the run is on track FOR THIS (§3.5.2).
881
- // Without it the reviewer judges progress without being told the goal, which
882
- // is the one input it cannot reconstruct from the event stream.
883
910
  this.fast?.setGoal(text);
884
- return this.agent.prompt(text);
911
+ if (request !== this.startupSequence) {
912
+ return { status: 'not-started', reason: 'cancelled' };
913
+ }
914
+ this.todos?.beginUserTurn(options.todoPolicy);
915
+ await this.agent.prompt(text);
916
+ return { status: 'finished' };
885
917
  }
918
+ /** Cancel pending startup as well as any engine run already in progress. */
886
919
  abort() {
920
+ this.startupSequence += 1;
921
+ this.stopEngine();
922
+ }
923
+ stopEngine() {
887
924
  // SET BEFORE `agent.abort()`, SYNCHRONOUSLY — guard 1 of §3.5.4a. The loop
888
925
  // still emits the batch's final `tool_execution_end` (`agent-loop.ts:249`)
889
926
  // after an Esc lands inside the last tool's `await`, and the reviewer's
@@ -907,7 +944,7 @@ export class AgentController {
907
944
  this.teamRuntime?.abortAll();
908
945
  }
909
946
  /**
910
- * Rung two of the Esc ladder: stop the AGENT, whatever it is blocked on
947
+ * Final rung of the Esc ladder: stop the AGENT, whatever it is blocked on
911
948
  * (§3.6 / G3).
912
949
  *
913
950
  * FOUR STEPS, AND THE FOURTH IS AN ABSENCE.
@@ -915,7 +952,7 @@ export class AgentController {
915
952
  * 1. `abort()` - everything it does today, unchanged.
916
953
  * 2. `killForeground('force')` - hard-kill every tracked foreground `bash`
917
954
  * 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
955
+ * an abort signal a tool ignores is advisory, and the force-stop rung exists for
919
956
  * precisely the tools that ignore it.
920
957
  * 3. `runGen += 1` - so `App` can drop the events the engine will keep
921
958
  * emitting while it unwinds (I-7). Without it the view bounces straight
@@ -924,7 +961,7 @@ export class AgentController {
924
961
  * `Ctrl+C` stops the SERVICES. That asymmetry is the user's own two
925
962
  * sentences and it is exactly the kind of thing a later reader will
926
963
  * "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
964
+ * confirmed a force-stop of a runaway turn also lost the dev server they
928
965
  * had been working against for an hour. It is invariant I-4.
929
966
  *
930
967
  * UNCONDITIONAL ON `bash.background` (P1-6 / D-9). The supervisor is always
@@ -1207,6 +1244,7 @@ export class AgentController {
1207
1244
  this.teamRuntime?.dispose();
1208
1245
  this.fast?.dispose();
1209
1246
  this.compaction?.dispose();
1247
+ this.contextMeter.dispose();
1210
1248
  // REAP BEFORE RELEASING THE HOOK, in that order: disposal is a teardown, and
1211
1249
  // a service that outlives it has nothing left that could ever stop it.
1212
1250
  this.procs.reapSync();
@@ -1337,6 +1375,13 @@ export class AgentController {
1337
1375
  // so the snapshot the chip and `/compact status` read has to be refreshed
1338
1376
  // for the same reason and at the same moments (the RV-3 lesson).
1339
1377
  this.compaction?.onConfigChanged();
1378
+ // THE DENOMINATOR MOVED, NOT THE HISTORY (context-usage-gauge-accuracy §3.4).
1379
+ // A re-measure, deliberately NOT a reset: the measured base is still a true
1380
+ // statement about the messages, and throwing it away would swap a correct
1381
+ // numerator for a whole-history estimate on a common operation. The
1382
+ // numerator's calibration is one turn stale until the next `turn_end`
1383
+ // (RV-13), which is the trade this call names.
1384
+ this.contextMeter.onWindowChanged();
1340
1385
  }
1341
1386
  setThinkingLevel(level) {
1342
1387
  this.config = { ...this.config, thinkingLevel: level };
@@ -1432,10 +1477,18 @@ export class AgentController {
1432
1477
  }
1433
1478
  clearMessages() {
1434
1479
  this.agent.clearMessages();
1480
+ this.startupSequence += 1;
1435
1481
  // INVALIDATION SITE 3 (context-auto-compaction-hardening §3.2.3). The history
1436
1482
  // was CLEARED, not appended to, so the prefix length the last measurement
1437
1483
  // covered indexes an array that no longer exists.
1484
+ //
1485
+ // BOTH LINES, AND THE SECOND IS NOT REDUNDANT (context-usage-gauge-accuracy
1486
+ // I-9). The wiring is `null` for a `--no-compaction` session, so it is the
1487
+ // meter call that guarantees a re-measure - deleting it "to de-duplicate"
1488
+ // reintroduces P0-2 for exactly the sessions this feature was written for. A
1489
+ // deep reset is idempotent, so both firing costs nothing.
1438
1490
  this.compaction?.onHistoryReplaced();
1491
+ this.contextMeter.onHistoryReplaced();
1439
1492
  // A new conversation has no loaded skills, so the "already loaded earlier
1440
1493
  // in this conversation" hint would otherwise start lying (P2-9).
1441
1494
  this.skills.getRegistry().clearActive();
@@ -1452,10 +1505,19 @@ export class AgentController {
1452
1505
  }
1453
1506
  replaceMessages(messages) {
1454
1507
  this.agent.replaceMessages(messages);
1508
+ this.startupSequence += 1;
1455
1509
  // INVALIDATION SITE 4 (§3.2.3) - `/resume`, and any future host-side rewrite.
1456
1510
  // `estimateAppendedTokens` bounds-checks as well, so a MISSED site costs
1457
1511
  // accuracy and never correctness; that belt does not make this call optional.
1512
+ //
1513
+ // THE SECOND LINE IS THE P0-2 FIX. Before it, `/resume` of a 180k-token
1514
+ // session left the gauge and `/compact status` reading 0 %: nothing
1515
+ // re-measured and nothing published, so the mount-time "empty history"
1516
+ // reading stood until the next completed turn. The DEEP reset also drops
1517
+ // `estimateOffset`, because `/resume` can change the model in the same
1518
+ // breath (I-8).
1458
1519
  this.compaction?.onHistoryReplaced();
1520
+ this.contextMeter.onHistoryReplaced();
1459
1521
  }
1460
1522
  getMessages() {
1461
1523
  return this.agent.state.messages;
@@ -1571,6 +1633,56 @@ export class AgentController {
1571
1633
  return () => { };
1572
1634
  return this.fast.subscribe(listener);
1573
1635
  }
1636
+ /**
1637
+ * The FIFTH thin forwarder: the current fast-tier snapshot, or `null` when
1638
+ * this session never registered a tier at all
1639
+ * (web-use-tier-cooperation-and-control-closure §4.2.1).
1640
+ *
1641
+ * `null` and "a snapshot with `live: false`" are different answers and callers
1642
+ * depend on the difference: the first means "there is no tier here", the
1643
+ * second means "there is one and it is not usable right now".
1644
+ *
1645
+ * Deliberately NOT folded into the existing `review_end` event: that shape is
1646
+ * consumed by `App` too, and this round promised to be purely additive.
1647
+ */
1648
+ fastSnapshot() {
1649
+ return this.fast?.snapshot() ?? null;
1650
+ }
1651
+ // -----------------------------------------------------------------------
1652
+ // Context occupancy (context-usage-gauge-accuracy §4.2) — THREE FORWARDERS
1653
+ //
1654
+ // ALL THREE ARE AVAILABLE UNCONDITIONALLY, which is the whole difference
1655
+ // between this block and the compaction one below it: there is no
1656
+ // `?? offSomething()` fallback here, because the meter always exists.
1657
+ // -----------------------------------------------------------------------
1658
+ /**
1659
+ * Subscribe to occupancy publications. Returns an unsubscribe function.
1660
+ *
1661
+ * THE SOLE UPSTREAM OF `ViewState.context` (I-1). The gauge used to have two
1662
+ * writers - `turnEnd` and a `contextTokensEstimated` dispatch fired from two
1663
+ * places in `App` - which is the entire cause of P0-1: both wrote in the same
1664
+ * synchronous fan-out and the second one won, restoring the pre-compaction
1665
+ * figure one statement after the bar had correctly fallen. One writer makes
1666
+ * that class of bug unrepresentable rather than merely fixed.
1667
+ */
1668
+ subscribeContextUsage(listener) {
1669
+ return this.contextMeter.subscribe(listener);
1670
+ }
1671
+ /**
1672
+ * Occupancy right now, re-measuring if the history has moved.
1673
+ *
1674
+ * `/context` READS THIS AND NOT `getCompactionSnapshot().pressure` (RV-8). The
1675
+ * latter returns `offCompactionSnapshot()`'s hardcoded zero pressure when
1676
+ * compaction was never registered, so a report built on it would read 0 % for
1677
+ * precisely the sessions this feature exists to fix.
1678
+ */
1679
+ getContextUsage() {
1680
+ return this.contextMeter.currentUsage();
1681
+ }
1682
+ /** The meter itself, for the wiring's single-instance assertion and tests. */
1683
+ getContextMeter() {
1684
+ return this.contextMeter;
1685
+ }
1574
1686
  // -----------------------------------------------------------------------
1575
1687
  // Context compaction (context-auto-compaction §3.2.1) — THIN FORWARDERS
1576
1688
  // -----------------------------------------------------------------------
@@ -4,6 +4,7 @@
4
4
  * derived from the event stream (R2), not from the resolved promise:
5
5
  * 0 = success, 1 = agent error (incl. a silently-swallowed throw), 2 = config.
6
6
  */
7
+ import type { PromptOutcome } from './prompt-options.js';
7
8
  import type { AgentEvent, ModelInfo } from '@aragon-agent/core';
8
9
  import type { PreflightResult } from './controller.js';
9
10
  import type { TeamEvent } from '../team/types.js';
@@ -15,7 +16,7 @@ import { type FollowThroughMode } from '../todo/follow-through.js';
15
16
  export interface HeadlessController {
16
17
  preflight(): PreflightResult;
17
18
  subscribe(listener: (event: AgentEvent) => void): () => void;
18
- prompt(text: string): Promise<void>;
19
+ prompt(text: string): Promise<void | PromptOutcome>;
19
20
  getModelInfo(): ModelInfo;
20
21
  /**
21
22
  * The CLI-local team stream — OPTIONAL, deliberately (team-subagents §3.12 /
@@ -0,0 +1,15 @@
1
+ /** Explicit TUI task boundary; omission retains legacy programmatic behavior. */
2
+ export interface PromptOptions {
3
+ todoPolicy?: 'new-task' | 'continue';
4
+ }
5
+ /** History recording is independent of whether this message continues a plan. */
6
+ export interface SubmitMessageOptions extends PromptOptions {
7
+ userInitiated?: boolean;
8
+ }
9
+ /** Startup outcome only; engine events still describe the result of the run. */
10
+ export type PromptOutcome = {
11
+ status: 'finished';
12
+ } | {
13
+ status: 'not-started';
14
+ reason: 'cancelled' | 'failed';
15
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -14,7 +14,7 @@ import type { AgentEvent, ModelCost, ToolResult, TokenUsage } from '@aragon-agen
14
14
  import type { AgentMode } from './agent-mode.js';
15
15
  import type { DispatchOutcome, SubagentRun, SubagentSpec, TeamSnapshot } from '../team/types.js';
16
16
  import type { FastReview, FastSnapshot } from '../fast/types.js';
17
- import type { CompactionMode, CompactionRecord, CompactionSnapshot, CompactionUiTrigger } from '../compaction/types.js';
17
+ import type { CompactionMode, CompactionRecord, CompactionSnapshot, CompactionUiTrigger, ContextUsageSnapshot } from '../compaction/types.js';
18
18
  import type { TodoItem, TodoSnapshot } from '../todo/types.js';
19
19
  import type { ServiceSnapshot, ServiceStatus } from '../proc/types.js';
20
20
  import type { RetryPhase } from './retry-view.js';
@@ -329,6 +329,16 @@ export type Entry = {
329
329
  export interface UsageTotal {
330
330
  inputTokens: number;
331
331
  outputTokens: number;
332
+ /**
333
+ * Cache read hits (context-usage-gauge-accuracy §3.6 / P1-3).
334
+ *
335
+ * `occupiedTokens` counts them and `computeCost` PRICES them, so a session
336
+ * total that omits them makes the status bar's `^` read lower than the `$`
337
+ * next to it - on the same row, from the same turns. That was three units on
338
+ * one line, and this field plus its sibling are what collapse them to one.
339
+ */
340
+ cacheReadTokens: number;
341
+ cacheWriteTokens: number;
332
342
  costUsd: number;
333
343
  }
334
344
  /**
@@ -349,8 +359,20 @@ export interface ViewState {
349
359
  entries: Entry[];
350
360
  status: 'idle' | 'running';
351
361
  usageTotal: UsageTotal;
352
- /** Running input size of the most recent turn vs. the model context window. */
353
- contextTokens: number;
362
+ /**
363
+ * Context occupancy, as published by `AgentController`'s `ContextMeter`.
364
+ *
365
+ * ITS ONLY WRITER IS `case 'contextUsage'` (I-1), and that is the structural
366
+ * half of the P0-1 fix. It replaced `contextTokens` + `contextTokensEstimated`,
367
+ * which between them had THREE writing branches (`turnEnd`,
368
+ * `contextTokensEstimated`, `resetConversation`) fed by two competing
369
+ * upstreams. Two writers in one synchronous fan-out means the later one wins,
370
+ * and the later one was the stale `snapshot` carrying the PRE-compaction
371
+ * figure - so the bar fell and bounced straight back, every time, with nothing
372
+ * logging a fault. `context-one-writer.test.ts` scans this file to keep it
373
+ * that way.
374
+ */
375
+ context: ContextUsageSnapshot;
354
376
  overlay: Overlay;
355
377
  thinkingVisible: boolean;
356
378
  /** Ephemeral acks; auto-dismissed by App (spec §3.9). */
@@ -408,16 +430,6 @@ export interface ViewState {
408
430
  * totals. `null` is what leaves an ordinary session's status bar unchanged.
409
431
  */
410
432
  compaction: CompactionSnapshot | null;
411
- /**
412
- * Whether `contextTokens` is currently a DERIVED figure rather than a measured
413
- * one (§6.2 / P1-11).
414
- *
415
- * Set by `compactionEnd`, cleared by the next `turnEnd`. The status bar widens
416
- * its leading `~` to cover it: today `~` means "the window is a guess", and
417
- * this makes it also mean "the occupancy is a guess". Honesty about a guess is
418
- * cheaper than a support thread about a wrong percentage.
419
- */
420
- contextTokensEstimated: boolean;
421
433
  seq: number;
422
434
  /** The `kind: 'team'` entry the live dispatch is writing into. */
423
435
  teamEntryId?: string;
@@ -648,15 +660,17 @@ export type ViewAction = {
648
660
  snapshot: CompactionSnapshot;
649
661
  }
650
662
  /**
651
- * Overwrite the gauge with a DERIVED occupancy (§6.2).
663
+ * The occupancy reading, from `AgentController.subscribeContextUsage`.
652
664
  *
653
- * Separate from `compactionEnd` because the value is not always available at
654
- * the same instant, and because the flag it sets — `contextTokensEstimated` —
655
- * has to be clearable by the next `turnEnd` independently.
665
+ * THE ONLY ACTION THAT WRITES `state.context`, AND IT IS DISPATCHED FROM
666
+ * EXACTLY ONE PLACE (I-1 / T11). It replaced `contextTokensEstimated`, which
667
+ * `App` fired from two branches of the compaction stream while `turnEnd` wrote
668
+ * the same field from the agent stream - three writers, two upstreams, one
669
+ * number, and the P0-1 bounce as the result.
656
670
  */
657
671
  | {
658
- type: 'contextTokensEstimated';
659
- tokens: number;
672
+ type: 'contextUsage';
673
+ snapshot: ContextUsageSnapshot;
660
674
  } | {
661
675
  type: 'serviceStart';
662
676
  service: ServiceSnapshot;