@aragon-agent/cli 0.6.13 → 0.6.15

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 (131) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +250 -222
  3. package/dist/agent/controller.d.ts +36 -2
  4. package/dist/agent/controller.js +244 -57
  5. package/dist/agent/model-profile-settings.js +11 -2
  6. package/dist/agent/plan-prompt.d.ts +1 -1
  7. package/dist/agent/plan-prompt.js +4 -3
  8. package/dist/agent/project-guidance-prompt.d.ts +4 -0
  9. package/dist/agent/project-guidance-prompt.js +104 -0
  10. package/dist/agent/reducer.d.ts +2 -0
  11. package/dist/agent/reducer.js +5 -0
  12. package/dist/agent/system-prompt.js +4 -0
  13. package/dist/boot/guard.d.ts +3 -3
  14. package/dist/boot/guard.js +3 -3
  15. package/dist/cli.js +33 -15
  16. package/dist/commands/builtins.js +2 -2
  17. package/dist/compaction/child.d.ts +9 -1
  18. package/dist/compaction/child.js +43 -13
  19. package/dist/compaction/command.js +20 -10
  20. package/dist/compaction/compactor.d.ts +33 -204
  21. package/dist/compaction/compactor.js +311 -723
  22. package/dist/compaction/context-command.js +5 -3
  23. package/dist/compaction/digest.d.ts +15 -51
  24. package/dist/compaction/digest.js +113 -148
  25. package/dist/compaction/limits.d.ts +19 -142
  26. package/dist/compaction/limits.js +20 -147
  27. package/dist/compaction/memory-identity.d.ts +16 -0
  28. package/dist/compaction/memory-identity.js +63 -0
  29. package/dist/compaction/memory-input.d.ts +25 -0
  30. package/dist/compaction/memory-input.js +106 -0
  31. package/dist/compaction/memory-render.d.ts +4 -0
  32. package/dist/compaction/memory-render.js +47 -0
  33. package/dist/compaction/memory-validation.d.ts +21 -0
  34. package/dist/compaction/memory-validation.js +268 -0
  35. package/dist/compaction/memory.d.ts +79 -0
  36. package/dist/compaction/memory.js +118 -0
  37. package/dist/compaction/meter.d.ts +14 -10
  38. package/dist/compaction/meter.js +55 -19
  39. package/dist/compaction/operation.d.ts +18 -0
  40. package/dist/compaction/operation.js +68 -0
  41. package/dist/compaction/pressure.d.ts +6 -20
  42. package/dist/compaction/pressure.js +14 -23
  43. package/dist/compaction/summarize-call.d.ts +3 -16
  44. package/dist/compaction/summarize-call.js +20 -20
  45. package/dist/compaction/summary-prompt.d.ts +29 -66
  46. package/dist/compaction/summary-prompt.js +132 -178
  47. package/dist/compaction/types.d.ts +23 -3
  48. package/dist/compaction/wiring.d.ts +10 -25
  49. package/dist/compaction/wiring.js +60 -67
  50. package/dist/config/load.d.ts +3 -1
  51. package/dist/config/load.js +8 -4
  52. package/dist/config/schema.d.ts +3 -3
  53. package/dist/config/schema.js +1 -1
  54. package/dist/exec/events.d.ts +8 -0
  55. package/dist/exec/index.js +13 -6
  56. package/dist/exec/runner.d.ts +8 -0
  57. package/dist/exec/runner.js +26 -2
  58. package/dist/session/persist.d.ts +3 -0
  59. package/dist/session/persist.js +9 -0
  60. package/dist/session/store.js +2 -0
  61. package/dist/session/validate-session.js +34 -0
  62. package/dist/team/subagent.js +8 -0
  63. package/dist/ui/App.js +221 -126
  64. package/dist/ui/BottomStatusRow.d.ts +5 -10
  65. package/dist/ui/BottomStatusRow.js +7 -11
  66. package/dist/ui/Composer.d.ts +4 -2
  67. package/dist/ui/Composer.js +2 -2
  68. package/dist/ui/Header.d.ts +6 -2
  69. package/dist/ui/Header.js +7 -6
  70. package/dist/ui/Logo.d.ts +3 -5
  71. package/dist/ui/Logo.js +4 -8
  72. package/dist/ui/PromptInput.d.ts +3 -1
  73. package/dist/ui/PromptInput.js +35 -42
  74. package/dist/ui/StatusBar.d.ts +6 -0
  75. package/dist/ui/StatusBar.js +8 -7
  76. package/dist/ui/Transcript.js +1 -1
  77. package/dist/ui/capabilities.d.ts +2 -0
  78. package/dist/ui/capabilities.js +6 -0
  79. package/dist/ui/color-runtime.d.ts +5 -0
  80. package/dist/ui/color-runtime.js +29 -0
  81. package/dist/ui/entries/CompactionCard.d.ts +3 -1
  82. package/dist/ui/entries/CompactionCard.js +7 -5
  83. package/dist/ui/frame-differ.js +27 -0
  84. package/dist/ui/frame-observer.js +18 -5
  85. package/dist/ui/glyphs.js +10 -8
  86. package/dist/ui/ink-runtime.d.ts +3 -0
  87. package/dist/ui/ink-runtime.js +30 -0
  88. package/dist/ui/interaction-copy.d.ts +73 -73
  89. package/dist/ui/interaction-copy.js +30 -38
  90. package/dist/ui/interaction-hints.d.ts +8 -1
  91. package/dist/ui/interaction-hints.js +56 -69
  92. package/dist/ui/layout/AppShell.d.ts +6 -2
  93. package/dist/ui/layout/AppShell.js +2 -2
  94. package/dist/ui/layout/OverlayFrame.d.ts +2 -0
  95. package/dist/ui/layout/OverlayFrame.js +23 -14
  96. package/dist/ui/layout/ScrollViewport.d.ts +3 -1
  97. package/dist/ui/layout/ScrollViewport.js +10 -2
  98. package/dist/ui/layout/budget.d.ts +25 -26
  99. package/dist/ui/layout/budget.js +29 -22
  100. package/dist/ui/layout/document-layout.d.ts +2 -2
  101. package/dist/ui/layout/document-layout.js +2 -3
  102. package/dist/ui/layout/queue-layout.js +4 -4
  103. package/dist/ui/layout/status-detail-layout.d.ts +9 -0
  104. package/dist/ui/layout/status-detail-layout.js +56 -0
  105. package/dist/ui/layout/status-layout.d.ts +19 -2
  106. package/dist/ui/layout/status-layout.js +95 -83
  107. package/dist/ui/layout/wrap-rows.d.ts +2 -0
  108. package/dist/ui/layout/wrap-rows.js +21 -7
  109. package/dist/ui/overlays/ConfirmDialog.d.ts +2 -1
  110. package/dist/ui/overlays/ConfirmDialog.js +22 -3
  111. package/dist/ui/overlays/HelpOverlay.js +9 -2
  112. package/dist/ui/overlays/ModelPicker.d.ts +5 -8
  113. package/dist/ui/overlays/ModelPicker.js +44 -10
  114. package/dist/ui/overlays/ModelProfileEditor.js +2 -0
  115. package/dist/ui/overlays/ModelProfilePicker.d.ts +1 -0
  116. package/dist/ui/overlays/ModelProfilePicker.js +4 -2
  117. package/dist/ui/overlays/PlanReviewOverlay.d.ts +2 -1
  118. package/dist/ui/overlays/PlanReviewOverlay.js +7 -4
  119. package/dist/ui/overlays/QuestionOverlay.d.ts +2 -1
  120. package/dist/ui/overlays/QuestionOverlay.js +45 -32
  121. package/dist/ui/overlays/QueueOverlay.js +2 -2
  122. package/dist/ui/overlays/SettingsScreen.d.ts +1 -0
  123. package/dist/ui/overlays/SettingsScreen.js +10 -3
  124. package/dist/ui/render-cache.d.ts +2 -2
  125. package/dist/ui/render-cache.js +4 -3
  126. package/dist/ui/run-metrics.d.ts +23 -0
  127. package/dist/ui/run-metrics.js +27 -0
  128. package/dist/ui/status-feedback.d.ts +33 -0
  129. package/dist/ui/status-feedback.js +36 -0
  130. package/dist/ui/stdout-frame-writer.js +28 -0
  131. package/package.json +2 -2
@@ -1,40 +1,4 @@
1
- /**
2
- * `CompactionWiring` — the controller-side glue for context compaction
3
- * (context-auto-compaction §3.2.1 / §5.2).
4
- *
5
- * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
6
- *
7
- * CONSTRUCT BEFORE THE `Agent`, ATTACH AFTER IT (C-12 / D-19), and the split is
8
- * the whole reason this class has an `attach` method at all:
9
- *
10
- * - The PORT has to be INSIDE the `Agent`'s constructor argument, because
11
- * `AgentConfig.contextManager` has no setter and the loop reads it from the
12
- * loop context.
13
- * - The WIRING has to SUBSCRIBE to the `Agent`, because `compaction_end` is
14
- * where the engine's `applied` verdict arrives and `agent_start` is what
15
- * resets the per-run cap.
16
- *
17
- * `AgentController` cannot do both in one step: `new Agent({...})` runs before
18
- * every optional subsystem, precisely because `FastWiring`'s constructor
19
- * subscribes. So THIS CONSTRUCTOR MUST NOT SUBSCRIBE. A field assigned after the
20
- * `new Agent` call and spread into it conditionally produces a build where the
21
- * feature is registered, enabled, and SILENTLY ABSENT - the exact shape of
22
- * failure `controller.ts`'s `toolOutputs` comment documents.
23
- *
24
- * `manager()` returns a STABLE object for the controller's lifetime, whose two
25
- * methods delegate to a compactor that reads config LIVE - which is what lets
26
- * `/compact off`, `/compact threshold 0.8` and a settings-screen save take effect
27
- * mid-session without touching the `Agent`.
28
- *
29
- * DECLINED COMPACTIONS PRODUCE NO CLI EVENTS (quiet-noop §3.6). A `pressure`
30
- * compaction that changed nothing, called nothing and spent nothing never emits
31
- * `compaction_start`, so `onCompactorEvent` never opens a card and `settlePending`
32
- * finds neither a held record nor a skeleton to synthesize. CORE still emits its
33
- * own pair for every attempt, which is why `settlePending` still runs - it just
34
- * refreshes the snapshot and returns. `onCompactorEvent`'s `if (!this.open)`
35
- * guard is now unreachable for declines and stays for the abort race it was
36
- * written for.
37
- */
1
+ import { shouldCompactAt } from './pressure.js';
38
2
  import { DEFAULT_RETRY_POLICY, estimatePromptTokens, initProviders, validateHistory, } from '@aragon-agent/core';
39
3
  import { getLogger } from '../logging/logger.js';
40
4
  import { mintRunId, writeArchive } from './archive.js';
@@ -126,6 +90,7 @@ export class CompactionWiring {
126
90
  * key, not an identity.
127
91
  */
128
92
  runId = mintRunId();
93
+ overflowNotified = false;
129
94
  constructor(deps) {
130
95
  this.deps = deps;
131
96
  this.enabled = deps.getConfig().compaction.enabled;
@@ -135,7 +100,10 @@ export class CompactionWiring {
135
100
  getMessages: deps.getMessages,
136
101
  getSystemPrompt: deps.getSystemPrompt,
137
102
  getModelInfo: () => deps.getModelInfoFor(this.mainRef()),
138
- isWindowKnown: () => deps.isPricedModel(this.mainRef()),
103
+ isWindowKnown: () => {
104
+ const source = deps.getModelInfoFor(this.mainRef()).contextWindowSource;
105
+ return source === undefined ? deps.isPricedModel(this.mainRef()) : source !== 'fallback';
106
+ },
139
107
  getWindowOverride: () => deps.getConfig().contextWindow,
140
108
  });
141
109
  this.compactor = new Compactor({
@@ -164,7 +132,14 @@ export class CompactionWiring {
164
132
  // setter and `contextManager` is never reassigned; the LIVE switch is inside
165
133
  // `shouldCompact`, not in the identity of this object.
166
134
  this.port = {
167
- shouldCompact: (probe) => this.enabled && this.compactor.shouldCompact(probe),
135
+ shouldCompact: (probe) => {
136
+ if (!this.enabled)
137
+ return false;
138
+ const allowed = this.compactor.shouldCompact(probe);
139
+ if (probe.trigger === 'overflow' && !allowed)
140
+ this.notifyOverflow();
141
+ return allowed;
142
+ },
168
143
  compact: (ctx) => this.compactor.compact(ctx),
169
144
  };
170
145
  }
@@ -190,6 +165,7 @@ export class CompactionWiring {
190
165
  }
191
166
  dispose() {
192
167
  this.compactor.abort();
168
+ this.compactor.clearPendingManual();
193
169
  this.unsubscribe?.();
194
170
  this.unsubscribe = null;
195
171
  this.listeners.clear();
@@ -202,6 +178,8 @@ export class CompactionWiring {
202
178
  }
203
179
  setEnabled(enabled) {
204
180
  this.enabled = enabled;
181
+ if (!enabled)
182
+ this.compactor.abort();
205
183
  // Turning it back on is the user overriding guard 4, which is the only way
206
184
  // out of a self-disable short of a restart.
207
185
  if (enabled)
@@ -212,6 +190,8 @@ export class CompactionWiring {
212
190
  onConfigChanged(enabled) {
213
191
  if (enabled !== undefined && enabled !== this.enabled) {
214
192
  this.enabled = enabled;
193
+ if (!enabled)
194
+ this.compactor.abort();
215
195
  if (enabled)
216
196
  this.compactor.clearSelfDisable();
217
197
  }
@@ -241,8 +221,19 @@ export class CompactionWiring {
241
221
  * for such a session. A deep reset is idempotent, so both firing costs
242
222
  * nothing; deleting the controller's copy "to de-duplicate" reintroduces P0-2.
243
223
  */
244
- onHistoryReplaced() {
245
- this.meter.onHistoryReplaced();
224
+ onHistoryReplaced(identity) {
225
+ this.compactor.onHistoryReplaced(identity);
226
+ }
227
+ getIdentity() { return this.compactor.getIdentity(); }
228
+ notifyOverflow() {
229
+ const p = this.meter.current();
230
+ if (this.overflowNotified || !Number.isFinite(p.occupied) || p.occupied < 0 ||
231
+ !Number.isFinite(p.contextWindow) || p.contextWindow <= 0 ||
232
+ shouldCompactAt(p, this.deps.getConfig().compaction.threshold))
233
+ return;
234
+ this.overflowNotified = true;
235
+ this.deps.notify('warn', 'Context limit reached before the configured compaction threshold. ' +
236
+ 'Use /compact or correct contextWindow.');
246
237
  }
247
238
  /**
248
239
  * The per-child manager factory, or `null` when children get none
@@ -266,7 +257,7 @@ export class CompactionWiring {
266
257
  // SINK (AC-H12). It is real money on the summarizer's price table, and a
267
258
  // background repair inside a background worker is exactly the spend a
268
259
  // user would otherwise never see.
269
- onUsage: (usage) => this.emit({ type: 'usage', usage }),
260
+ onUsage: (usage, billing) => this.emit({ type: 'usage', usage, ...billing }),
270
261
  });
271
262
  }
272
263
  // -----------------------------------------------------------------------
@@ -281,28 +272,13 @@ export class CompactionWiring {
281
272
  clearPendingManual() {
282
273
  this.compactor.clearPendingManual();
283
274
  }
284
- /**
285
- * The IDLE `/compact` path (§4.4 / D-25).
286
- *
287
- * IT VALIDATES BEFORE IT SPLICES, AND THAT IS NOT OPTIONAL (P1-7). When the
288
- * agent is idle there is no loop, so §3.3 step 5 does not run — and the only
289
- * way to replace the history is `Agent.replaceMessages`, which validates
290
- * nothing. D-4 declares that gate unbypassable and R-2 calls an invalid splice
291
- * the worst possible outcome, so this path mirrors the engine's steps 3-5
292
- * exactly, INCLUDING the failure vocabulary: the card, the log and
293
- * `/compact status` cannot tell the two paths apart.
294
- *
295
- * No watchdog handling is needed — `runLoopWithLifecycle`'s `finally` already
296
- * called `watchdog.stop()`, so nothing is armed while idle.
297
- *
298
- * ITS CALLER MUST ADOPT `messages` — THE ARCHIVE IS ALREADY WRITTEN BY THE
299
- * TIME THIS RETURNS (hardening RV-7). This path settles `applied: true` here,
300
- * one statement before the caller splices, so the "never archive a live
301
- * history" rule holds only because that caller applies unconditionally. That is
302
- * a fact about one call site rather than an invariant, which is why it is
303
- * written down where it can be checked.
304
- */
275
+ /** Prepare, validate and synchronously adopt before publishing a successful verdict. */
305
276
  async compactNow(args) {
277
+ if (!this.enabled)
278
+ return { ok: false, reason: 'disabled' };
279
+ if (this.compactor.sessionTotals().inFlight)
280
+ return { ok: false, reason: 'already_running' };
281
+ this.compactor.onRunStart();
306
282
  this.compactor.queueManual(args.instructions);
307
283
  // BOTH SIDES FROM CORE'S OWN ESTIMATOR, exactly as the in-loop path gets them
308
284
  // from `compaction_end` (§5.1). There is no engine event here to supply them,
@@ -339,6 +315,19 @@ export class CompactionWiring {
339
315
  this.settlePending({ applied: false, reason, tokensBefore, tokensAfter: tokensBefore });
340
316
  return { ok: false, reason };
341
317
  }
318
+ if (args.signal.aborted || args.isCurrent?.() === false || !args.adopt) {
319
+ const reason = args.signal.aborted ? 'aborted' : 'stale_history';
320
+ this.settlePending({ applied: false, reason, tokensBefore, tokensAfter: tokensBefore });
321
+ return { ok: false, reason };
322
+ }
323
+ try {
324
+ args.adopt(outcome.messages);
325
+ }
326
+ catch (error) {
327
+ const reason = `adoption_failed: ${error instanceof Error ? error.message : String(error)}`;
328
+ this.settlePending({ applied: false, reason, tokensBefore, tokensAfter: tokensBefore });
329
+ return { ok: false, reason };
330
+ }
342
331
  this.settlePending({
343
332
  applied: true,
344
333
  tokensBefore,
@@ -384,6 +373,7 @@ export class CompactionWiring {
384
373
  ...(totals.declined > 0 ? { declined: totals.declined } : {}),
385
374
  tokensReclaimed: totals.tokensReclaimed,
386
375
  usage: totals.usage,
376
+ costUsd: totals.costUsd,
387
377
  pricingUnknown: totals.pricingUnknown,
388
378
  inFlight: totals.inFlight,
389
379
  selfDisabled: totals.selfDisabled,
@@ -457,9 +447,13 @@ export class CompactionWiring {
457
447
  onAgentEvent(event) {
458
448
  if (event.type === 'agent_start') {
459
449
  this.compactor.onRunStart();
450
+ this.overflowNotified = false;
460
451
  return;
461
452
  }
453
+ if (event.type === 'agent_end')
454
+ this.compactor.onRunEnd();
462
455
  if (event.type === 'turn_end') {
456
+ this.meter.onTurnEnd(event.usage);
463
457
  // THE MEASUREMENT IS THE METER'S (context-usage-gauge-accuracy §3.3.4). It
464
458
  // subscribes to `turn_end` on the same stream and recomputes the offset,
465
459
  // the prefix length and the pressure there; the two calls that used to
@@ -498,6 +492,7 @@ export class CompactionWiring {
498
492
  // engine cannot report `true` without a `replace` outcome, which means the
499
493
  // compactor's own record already exists — so its zeroed counts are never
500
494
  // rendered.
495
+ this.compactor.settleOperation(verdict);
501
496
  const record = this.pending ?? this.skeletonRecord();
502
497
  this.pending = null;
503
498
  this.open = null;
@@ -530,7 +525,6 @@ export class CompactionWiring {
530
525
  mode: verdict.applied ? record.mode : 'none',
531
526
  };
532
527
  if (verdict.applied) {
533
- this.compactor.recordApplied(settled.tokensBefore, settled.tokensAfter);
534
528
  // SITE 1 OF THE FOUR INVALIDATION SITES (hardening §3.2.3), and THE LOAD-
535
529
  // BEARING LINE OF THE P0-1 FIX (context-usage-gauge-accuracy I-9).
536
530
  //
@@ -562,9 +556,8 @@ export class CompactionWiring {
562
556
  * archive a history that is still live" - and the two paths reach that verdict
563
557
  * at different moments relative to the splice. In loop, the ENGINE decides
564
558
  * after `validateHistory`, so this runs AFTER adoption. On the manual path the
565
- * WIRING decides and `compactNow`'s caller adopts on the next statement, so
566
- * this runs BEFORE adoption; `compactNow`'s doc comment carries that obligation
567
- * for its one caller (RV-7).
559
+ * WIRING invokes the host adoption callback before settling, so both paths
560
+ * archive only histories that have actually been replaced.
568
561
  *
569
562
  * BEST-EFFORT AND SYNCHRONOUS-BUT-GUARDED. `writeArchive` never throws and
570
563
  * logs its own failure; a failed archive never affects the compaction, and it
@@ -241,7 +241,9 @@ export interface CliFlags {
241
241
  * Resolve the effective config by merging all layers, then derive the runtime
242
242
  * (non-persisted) fields and enforce the timeout invariant.
243
243
  */
244
- export declare function loadConfig(flags?: CliFlags): CliConfig;
244
+ export declare function loadConfig(flags?: CliFlags, options?: {
245
+ interactive?: boolean;
246
+ }): CliConfig;
245
247
  /**
246
248
  * Build the `getApiKey(providerId)` resolver the core `Agent` consumes.
247
249
  * The one-shot `--api-key` override wins, but only for the active provider.
@@ -16,7 +16,7 @@ import { setEntryRetain } from '../agent/entry-limits.js';
16
16
  import { readSubmitCount } from './ui-state.js';
17
17
  import { getLogger } from '../logging/logger.js';
18
18
  import { registerSecret, registerSecretsFrom } from '../logging/secret-registry.js';
19
- import { detectCapabilities } from '../ui/capabilities.js';
19
+ import { detectCapabilities, resolveTuiCapabilities } from '../ui/capabilities.js';
20
20
  /** Interpret a boolean-ish env value (`1/true/on/yes`) as `true`. */
21
21
  function envFlagTrue(value) {
22
22
  if (!value)
@@ -436,7 +436,7 @@ function resolveContextWindow(env, file) {
436
436
  * Resolve the effective config by merging all layers, then derive the runtime
437
437
  * (non-persisted) fields and enforce the timeout invariant.
438
438
  */
439
- export function loadConfig(flags = {}) {
439
+ export function loadConfig(flags = {}, options = {}) {
440
440
  const cwd = flags.cwd ? flags.cwd : process.cwd();
441
441
  // `.env` must be loaded before reading env overrides.
442
442
  loadDotenv(cwd);
@@ -519,13 +519,17 @@ export function loadConfig(flags = {}) {
519
519
  // registering twice costs a `Set.add`, missing once costs a credential.
520
520
  registerSecretsFrom(apiKeys);
521
521
  registerSecret(flags.apiKey);
522
- const color = flags.color !== undefined ? flags.color : !process.env.NO_COLOR;
522
+ const caps = options.interactive
523
+ ? resolveTuiCapabilities(process.env, flags.color)
524
+ : detectCapabilities(process.env, process.stdout);
525
+ const color = options.interactive
526
+ ? caps.colorLevel !== 0
527
+ : flags.color !== undefined ? flags.color : !process.env.NO_COLOR;
523
528
  // `--no-color` implies calmer chrome; env / file may also opt in explicitly.
524
529
  const reducedMotion = envFlagTrue(process.env.ARAGON_REDUCED_MOTION) ||
525
530
  (file.reducedMotion ?? DEFAULT_CONFIG.reducedMotion) ||
526
531
  !color;
527
532
  // Detect terminal capabilities once; force monochrome when color is disabled.
528
- const caps = detectCapabilities(process.env, process.stdout);
529
533
  const colorLevel = color ? caps.colorLevel : 0;
530
534
  // No flag and no env var: a privacy switch you set once, not a per-run knob.
531
535
  const historyEnabled = file.historyEnabled ?? DEFAULT_CONFIG.historyEnabled;
@@ -577,7 +577,7 @@ export interface CompactionConfig {
577
577
  * SESSION'S OWN MODEL, and the first compaction is a ~30 k-token call on it.
578
578
  */
579
579
  useFastTier: boolean;
580
- /** What happens when summarization fails outright. See §3.7 rungs 3 / 4. */
580
+ /** Legacy truncate remains readable; all failures now preserve history. */
581
581
  onFailure: CompactionFailureMode;
582
582
  /**
583
583
  * Give `task` children their own compaction
@@ -1097,9 +1097,9 @@ export interface CliConfig {
1097
1097
  cwd: string;
1098
1098
  /** Whether ANSI color is enabled. */
1099
1099
  color: boolean;
1100
- /** Detected terminal color depth (0 none · 1 16 · 2 256 · 3 truecolor). */
1100
+ /** Resolved color depth: TUI policy or non-interactive terminal detection. */
1101
1101
  colorLevel?: 0 | 1 | 2 | 3;
1102
- /** Detected Unicode support (box/round glyphs & spinners). */
1102
+ /** TUI always enables Unicode; non-interactive output uses detection. */
1103
1103
  unicode?: boolean;
1104
1104
  /** One-shot `--api-key` override; applies to the active provider only. */
1105
1105
  apiKeyOverride?: string;
@@ -744,7 +744,7 @@ export const DEFAULT_COMPACTION_CONFIG = {
744
744
  warnThreshold: 0.75,
745
745
  keepRecentTurns: 4,
746
746
  useFastTier: true,
747
- onFailure: 'truncate',
747
+ onFailure: 'stop',
748
748
  subagents: true,
749
749
  archive: true,
750
750
  };
@@ -274,6 +274,14 @@ export interface ExecFastTierEvent {
274
274
  * it. What a wrapper does need is the SHAPE of what happened, which is here.
275
275
  */
276
276
  export interface ExecCompactionEvent {
277
+ decision?: {
278
+ occupied: number;
279
+ contextWindow: number;
280
+ threshold: number;
281
+ source: 'usage' | 'estimate';
282
+ deltaTokens: number;
283
+ };
284
+ memoryVersion?: 2;
277
285
  type: 'compaction';
278
286
  sessionId: string;
279
287
  turn: number;
@@ -111,7 +111,7 @@ async function execute(input) {
111
111
  now,
112
112
  });
113
113
  if (plan.saved) {
114
- controller.replaceMessages(plan.saved.messages);
114
+ controller.replaceMessages(plan.saved.messages, plan.saved.compactionIdentity);
115
115
  controller.restoreTodos(plan.saved.todos);
116
116
  }
117
117
  const config = controller.getConfig();
@@ -187,7 +187,7 @@ async function execute(input) {
187
187
  outputTokens: stats.usage.outputTokens,
188
188
  totalTokens: stats.usage.inputTokens + stats.usage.outputTokens,
189
189
  },
190
- cost: buildCost(controller, stats.usage),
190
+ cost: buildCost(controller, stats.usage, stats.compactionBilling),
191
191
  model: { provider: config.provider, id: config.model },
192
192
  todos: todo ? { total: todo.total, done: todo.doneCount } : null,
193
193
  error: stats.errorCode && stats.errorMessage
@@ -253,17 +253,24 @@ async function drive(input, controller, runner) {
253
253
  function withPermissionMode(flags, mode) {
254
254
  return mode === 'plan' ? { ...flags, plan: true } : flags;
255
255
  }
256
- function buildCost(controller, usage) {
256
+ function buildCost(controller, usage, compaction) {
257
257
  const config = controller.getConfig();
258
- const known = controller.isPricedModel({
258
+ const known = !compaction?.pricingUnknown && controller.isPricedModel({
259
259
  providerId: config.provider,
260
260
  modelId: config.model,
261
261
  });
262
+ const mainUsage = {
263
+ inputTokens: usage.inputTokens - (compaction?.usage.inputTokens ?? 0),
264
+ outputTokens: usage.outputTokens - (compaction?.usage.outputTokens ?? 0),
265
+ cacheReadTokens: (usage.cacheReadTokens ?? 0) - (compaction?.usage.cacheReadTokens ?? 0),
266
+ cacheWriteTokens: (usage.cacheWriteTokens ?? 0) - (compaction?.usage.cacheWriteTokens ?? 0),
267
+ };
262
268
  // `amount` is `0` when the price is unknown, mirroring the fast tier's rule
263
269
  // that a feature which looks free while it is spending money is worse than one
264
270
  // that admits it does not know.
265
271
  return {
266
- amount: known ? computeCost(usage, controller.getModelInfo().cost) : 0,
272
+ amount: known ? computeCost(mainUsage, controller.getModelInfo().cost) +
273
+ (compaction?.costUsd ?? 0) : 0,
267
274
  currency: 'USD',
268
275
  known,
269
276
  };
@@ -460,7 +467,7 @@ function persist(plan, controller, turns, usage, cli, at) {
460
467
  modelId: config.model,
461
468
  ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
462
469
  },
463
- messages: controller.getMessages(),
470
+ ...(controller.getSessionSnapshot?.() ?? { messages: controller.getMessages() }),
464
471
  entries: [],
465
472
  todos: controller.getTodoSnapshot?.()?.items ?? [],
466
473
  },
@@ -72,7 +72,13 @@ export interface ExecRunnerOptions {
72
72
  /** Turns already spent by earlier invocations of a resumed session. */
73
73
  priorTurns?: number;
74
74
  }
75
+ export interface ExecCompactionBilling {
76
+ usage: TokenUsage;
77
+ costUsd: number;
78
+ pricingUnknown: boolean;
79
+ }
75
80
  export interface ExecRunStats {
81
+ compactionBilling?: ExecCompactionBilling;
76
82
  turns: number;
77
83
  usage: TokenUsage;
78
84
  lastAssistantText: string;
@@ -93,6 +99,7 @@ export declare class ExecRunner {
93
99
  private unsubscribers;
94
100
  private turns;
95
101
  private readonly usage;
102
+ private compactionBilling;
96
103
  private lastAssistantText;
97
104
  private errored;
98
105
  private sawTurnEnd;
@@ -143,6 +150,7 @@ export declare class ExecRunner {
143
150
  * the two faces cannot drift about what "unfinished" means.
144
151
  */
145
152
  run(source: ExecPromptSource): Promise<void>;
153
+ private onCompactionUsage;
146
154
  private requestSeq;
147
155
  private requestHadError;
148
156
  private isRequestCancelled;
@@ -46,6 +46,7 @@ export class ExecRunner {
46
46
  unsubscribers = [];
47
47
  turns = 0;
48
48
  usage = { inputTokens: 0, outputTokens: 0 };
49
+ compactionBilling;
49
50
  lastAssistantText = '';
50
51
  errored = false;
51
52
  sawTurnEnd = false;
@@ -175,8 +176,7 @@ export class ExecRunner {
175
176
  // wrapper gets.
176
177
  const compaction = controller.subscribeCompaction?.((event) => {
177
178
  if (event.type === 'usage') {
178
- this.usage.inputTokens += event.usage.inputTokens;
179
- this.usage.outputTokens += event.usage.outputTokens;
179
+ this.onCompactionUsage(event);
180
180
  return;
181
181
  }
182
182
  if (event.type === 'compaction_start') {
@@ -208,6 +208,8 @@ export class ExecRunner {
208
208
  tokensBefore: r.tokensBefore,
209
209
  tokensAfter: r.tokensAfter,
210
210
  ...(r.tailRelief ? { tailRelief: r.tailRelief } : {}),
211
+ ...(r.decision ? { decision: r.decision } : {}),
212
+ ...(r.memoryVersion ? { memoryVersion: r.memoryVersion } : {}),
211
213
  durationMs: r.durationMs,
212
214
  });
213
215
  });
@@ -246,6 +248,9 @@ export class ExecRunner {
246
248
  return {
247
249
  turns: this.turns,
248
250
  usage: { ...this.usage },
251
+ ...(this.compactionBilling ? { compactionBilling: {
252
+ ...this.compactionBilling, usage: { ...this.compactionBilling.usage },
253
+ } } : {}),
249
254
  lastAssistantText: this.lastAssistantText,
250
255
  errored: this.errored,
251
256
  stopReason: this.stopReason,
@@ -322,6 +327,25 @@ export class ExecRunner {
322
327
  }
323
328
  }
324
329
  }
330
+ onCompactionUsage(event) {
331
+ const usage = event.usage;
332
+ this.usage.inputTokens += usage.inputTokens;
333
+ this.usage.outputTokens += usage.outputTokens;
334
+ for (const key of ['cacheReadTokens', 'cacheWriteTokens']) {
335
+ if (usage[key] !== undefined)
336
+ this.usage[key] = (this.usage[key] ?? 0) + usage[key];
337
+ }
338
+ if (event.costUsd === undefined && event.pricingUnknown === undefined)
339
+ return;
340
+ const billing = this.compactionBilling ??= { costUsd: 0, pricingUnknown: false,
341
+ usage: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 } };
342
+ billing.costUsd += event.costUsd ?? 0;
343
+ billing.pricingUnknown ||= event.pricingUnknown === true;
344
+ billing.usage.inputTokens += usage.inputTokens;
345
+ billing.usage.outputTokens += usage.outputTokens;
346
+ billing.usage.cacheReadTokens += usage.cacheReadTokens ?? 0;
347
+ billing.usage.cacheWriteTokens += usage.cacheWriteTokens ?? 0;
348
+ }
325
349
  requestSeq = 0;
326
350
  requestHadError = false;
327
351
  isRequestCancelled() {
@@ -8,6 +8,7 @@
8
8
  import type { Message, ModelRef } from '@aragon-agent/core';
9
9
  import type { Entry } from '../agent/reducer.js';
10
10
  import type { TodoItem } from '../todo/types.js';
11
+ import { type CompactionIdentity } from '../compaction/memory-identity.js';
11
12
  /**
12
13
  * Bookkeeping `aragon exec` attaches to the sessions it owns
13
14
  * (cli-integration-surface §5.2).
@@ -41,6 +42,7 @@ export interface SavedSession {
41
42
  savedAt: number;
42
43
  model?: ModelRef;
43
44
  messages: Message[];
45
+ compactionIdentity?: CompactionIdentity;
44
46
  entries: Entry[];
45
47
  /**
46
48
  * The live todo list at save time (todo-plan-execution §3.13).
@@ -79,6 +81,7 @@ export declare function resolveSessionPath(nameOrPath: string | undefined, cwd:
79
81
  export declare function saveSession(filePath: string, data: {
80
82
  model: ModelRef;
81
83
  messages: Message[];
84
+ compactionIdentity?: CompactionIdentity;
82
85
  entries: Entry[];
83
86
  todos: TodoItem[];
84
87
  meta?: SessionMeta;
@@ -9,6 +9,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
9
  import { dirname, isAbsolute, join, resolve } from 'node:path';
10
10
  import { getSessionsDir } from '../config/store.js';
11
11
  import { validateSession } from './validate-session.js';
12
+ import { validateCompactionIdentity, verifyCompactionIdentity, } from '../compaction/memory-identity.js';
13
+ import { getLogger } from '../logging/logger.js';
12
14
  const SESSION_VERSION = 1;
13
15
  /** Resolve a `/save` target: absolute/relative path, or a name in the data dir. */
14
16
  export function resolveSessionPath(nameOrPath, cwd) {
@@ -92,6 +94,12 @@ function stripServiceTail(entries) {
92
94
  * payload with no such key and a file byte-identical to today's.
93
95
  */
94
96
  export function saveSession(filePath, data) {
97
+ if (data.compactionIdentity) {
98
+ validateCompactionIdentity(data.compactionIdentity);
99
+ if (!verifyCompactionIdentity(data.messages, data.compactionIdentity)) {
100
+ getLogger().debug('agent', 'invalid_compaction_session_identity');
101
+ }
102
+ }
95
103
  const dir = dirname(filePath);
96
104
  if (!existsSync(dir))
97
105
  mkdirSync(dir, { recursive: true });
@@ -100,6 +108,7 @@ export function saveSession(filePath, data) {
100
108
  savedAt: Date.now(),
101
109
  model: data.model,
102
110
  messages: data.messages,
111
+ ...(data.compactionIdentity ? { compactionIdentity: data.compactionIdentity } : {}),
103
112
  entries: stripServiceTail(stripLiveToolOutput(data.entries)),
104
113
  todos: data.todos,
105
114
  ...(data.meta ? { meta: data.meta } : {}),
@@ -159,6 +159,8 @@ export function writeSession(input) {
159
159
  saveSession(path, {
160
160
  model: input.session.model,
161
161
  messages: input.session.messages,
162
+ ...(input.session.compactionIdentity
163
+ ? { compactionIdentity: input.session.compactionIdentity } : {}),
162
164
  entries: input.session.entries,
163
165
  todos: input.session.todos ?? [],
164
166
  meta: input.meta,
@@ -1,6 +1,8 @@
1
1
  import { isAdapterProvider } from '../config/schema.js';
2
2
  import { normalizeTodos } from '../todo/normalize.js';
3
3
  import { TODO_LIMITS } from '../todo/limits.js';
4
+ import { validateCompactionIdentity } from '../compaction/memory-identity.js';
5
+ import { MemoryError } from '../compaction/memory.js';
4
6
  function fail(path, expected) {
5
7
  throw new Error(`Invalid session file: ${path} must be ${expected}.`);
6
8
  }
@@ -26,6 +28,25 @@ const boolean = (value, path) => {
26
28
  if (typeof value !== 'boolean')
27
29
  fail(path, 'a boolean');
28
30
  };
31
+ const nonnegative = (value, path) => {
32
+ number(value, path);
33
+ if (value < 0)
34
+ fail(path, 'non-negative');
35
+ };
36
+ const positive = (value, path) => {
37
+ number(value, path);
38
+ if (value <= 0)
39
+ fail(path, 'positive');
40
+ };
41
+ const threshold = (value, path) => {
42
+ number(value, path);
43
+ if (value < 0.5 || value > 0.95)
44
+ fail(path, 'between 0.5 and 0.95');
45
+ };
46
+ const memoryVersion = (value, path) => {
47
+ if (value !== 2)
48
+ fail(path, '2');
49
+ };
29
50
  const object = (value, path) => { record(value, path); };
30
51
  function optional(check) {
31
52
  return (value, path) => { if (value !== undefined)
@@ -149,6 +170,9 @@ const ENTRY_FIELDS = {
149
170
  text: optional(string), detail: optional(string), turn: number,
150
171
  durationMs: optional(number), live: boolean },
151
172
  compaction: {
173
+ decision: optional(shape({ occupied: nonnegative, contextWindow: positive,
174
+ threshold, source: enumeration('usage', 'estimate'), deltaTokens: nonnegative })),
175
+ memoryVersion: optional(memoryVersion),
152
176
  index: number, trigger: enumeration('pressure', 'overflow', 'manual'),
153
177
  mode: enumeration('summarized', 'truncated', 'relieved', 'none'), applied: boolean,
154
178
  reason: optional(string), messagesBefore: number, messagesAfter: number,
@@ -198,6 +222,16 @@ export function validateSession(value) {
198
222
  validateEntries(source.entries);
199
223
  if (source.model !== undefined)
200
224
  validateModel(source.model);
225
+ if (source.compactionIdentity !== undefined) {
226
+ try {
227
+ validateCompactionIdentity(source.compactionIdentity);
228
+ }
229
+ catch (error) {
230
+ if (!(error instanceof MemoryError))
231
+ throw error;
232
+ fail('compactionIdentity', 'a valid adopted-memory identity');
233
+ }
234
+ }
201
235
  // Missing fields remain missing in old files, including exec metadata.
202
236
  const prepared = { ...source };
203
237
  if (source.todos !== undefined) {
@@ -331,6 +331,14 @@ export function createSubagent(spec, deps, hooks) {
331
331
  }
332
332
  break;
333
333
  }
334
+ case 'compaction_end':
335
+ contextManager?.onCompactionEnd({
336
+ applied: event.applied,
337
+ ...(event.reason !== undefined ? { reason: event.reason } : {}),
338
+ tokensBefore: event.estimatedTokensBefore,
339
+ tokensAfter: event.estimatedTokensAfter,
340
+ });
341
+ break;
334
342
  case 'tool_execution_start':
335
343
  run.phase = event.toolName === 'team_wait' ? 'waiting' : 'tool';
336
344
  run.lastTool = event.toolName;