@aragon-agent/cli 0.6.14 → 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 (113) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +250 -233
  3. package/dist/agent/controller.d.ts +27 -1
  4. package/dist/agent/controller.js +182 -45
  5. package/dist/agent/plan-prompt.d.ts +1 -1
  6. package/dist/agent/plan-prompt.js +4 -3
  7. package/dist/agent/project-guidance-prompt.d.ts +4 -0
  8. package/dist/agent/project-guidance-prompt.js +104 -0
  9. package/dist/agent/reducer.d.ts +2 -0
  10. package/dist/agent/reducer.js +4 -0
  11. package/dist/agent/system-prompt.js +4 -0
  12. package/dist/commands/builtins.js +2 -2
  13. package/dist/compaction/child.d.ts +9 -1
  14. package/dist/compaction/child.js +39 -13
  15. package/dist/compaction/command.js +20 -10
  16. package/dist/compaction/compactor.d.ts +33 -204
  17. package/dist/compaction/compactor.js +311 -726
  18. package/dist/compaction/digest.d.ts +15 -51
  19. package/dist/compaction/digest.js +113 -148
  20. package/dist/compaction/limits.d.ts +19 -142
  21. package/dist/compaction/limits.js +20 -147
  22. package/dist/compaction/memory-identity.d.ts +16 -0
  23. package/dist/compaction/memory-identity.js +63 -0
  24. package/dist/compaction/memory-input.d.ts +25 -0
  25. package/dist/compaction/memory-input.js +106 -0
  26. package/dist/compaction/memory-render.d.ts +4 -0
  27. package/dist/compaction/memory-render.js +47 -0
  28. package/dist/compaction/memory-validation.d.ts +21 -0
  29. package/dist/compaction/memory-validation.js +268 -0
  30. package/dist/compaction/memory.d.ts +79 -0
  31. package/dist/compaction/memory.js +118 -0
  32. package/dist/compaction/meter.d.ts +14 -10
  33. package/dist/compaction/meter.js +53 -19
  34. package/dist/compaction/operation.d.ts +18 -0
  35. package/dist/compaction/operation.js +68 -0
  36. package/dist/compaction/pressure.d.ts +5 -20
  37. package/dist/compaction/pressure.js +13 -23
  38. package/dist/compaction/summarize-call.d.ts +3 -16
  39. package/dist/compaction/summarize-call.js +20 -20
  40. package/dist/compaction/summary-prompt.d.ts +29 -66
  41. package/dist/compaction/summary-prompt.js +132 -178
  42. package/dist/compaction/types.d.ts +21 -3
  43. package/dist/compaction/wiring.d.ts +10 -25
  44. package/dist/compaction/wiring.js +56 -66
  45. package/dist/config/schema.d.ts +1 -1
  46. package/dist/config/schema.js +1 -1
  47. package/dist/exec/events.d.ts +8 -0
  48. package/dist/exec/index.js +13 -6
  49. package/dist/exec/runner.d.ts +8 -0
  50. package/dist/exec/runner.js +26 -2
  51. package/dist/session/persist.d.ts +3 -0
  52. package/dist/session/persist.js +9 -0
  53. package/dist/session/store.js +2 -0
  54. package/dist/session/validate-session.js +34 -0
  55. package/dist/team/subagent.js +8 -0
  56. package/dist/ui/App.js +218 -116
  57. package/dist/ui/BottomStatusRow.d.ts +5 -10
  58. package/dist/ui/BottomStatusRow.js +7 -11
  59. package/dist/ui/Composer.d.ts +4 -2
  60. package/dist/ui/Composer.js +2 -2
  61. package/dist/ui/Header.d.ts +6 -2
  62. package/dist/ui/Header.js +7 -6
  63. package/dist/ui/PromptInput.d.ts +3 -1
  64. package/dist/ui/PromptInput.js +32 -41
  65. package/dist/ui/StatusBar.d.ts +6 -0
  66. package/dist/ui/StatusBar.js +8 -7
  67. package/dist/ui/Transcript.js +1 -1
  68. package/dist/ui/entries/CompactionCard.d.ts +3 -1
  69. package/dist/ui/entries/CompactionCard.js +7 -5
  70. package/dist/ui/frame-differ.js +27 -0
  71. package/dist/ui/frame-observer.js +18 -5
  72. package/dist/ui/interaction-copy.d.ts +73 -73
  73. package/dist/ui/interaction-copy.js +30 -38
  74. package/dist/ui/interaction-hints.d.ts +8 -1
  75. package/dist/ui/interaction-hints.js +56 -69
  76. package/dist/ui/layout/AppShell.d.ts +6 -2
  77. package/dist/ui/layout/AppShell.js +2 -2
  78. package/dist/ui/layout/OverlayFrame.d.ts +2 -0
  79. package/dist/ui/layout/OverlayFrame.js +23 -14
  80. package/dist/ui/layout/ScrollViewport.d.ts +3 -1
  81. package/dist/ui/layout/ScrollViewport.js +10 -2
  82. package/dist/ui/layout/budget.d.ts +25 -26
  83. package/dist/ui/layout/budget.js +29 -22
  84. package/dist/ui/layout/document-layout.d.ts +2 -2
  85. package/dist/ui/layout/document-layout.js +2 -3
  86. package/dist/ui/layout/queue-layout.js +4 -4
  87. package/dist/ui/layout/status-detail-layout.d.ts +9 -0
  88. package/dist/ui/layout/status-detail-layout.js +56 -0
  89. package/dist/ui/layout/status-layout.d.ts +19 -2
  90. package/dist/ui/layout/status-layout.js +95 -84
  91. package/dist/ui/layout/wrap-rows.d.ts +2 -0
  92. package/dist/ui/layout/wrap-rows.js +21 -7
  93. package/dist/ui/overlays/ConfirmDialog.d.ts +2 -1
  94. package/dist/ui/overlays/ConfirmDialog.js +22 -3
  95. package/dist/ui/overlays/HelpOverlay.js +9 -2
  96. package/dist/ui/overlays/ModelPicker.d.ts +5 -8
  97. package/dist/ui/overlays/ModelPicker.js +44 -10
  98. package/dist/ui/overlays/ModelProfileEditor.js +2 -0
  99. package/dist/ui/overlays/ModelProfilePicker.d.ts +1 -0
  100. package/dist/ui/overlays/ModelProfilePicker.js +4 -2
  101. package/dist/ui/overlays/PlanReviewOverlay.d.ts +2 -1
  102. package/dist/ui/overlays/PlanReviewOverlay.js +7 -4
  103. package/dist/ui/overlays/QuestionOverlay.d.ts +2 -1
  104. package/dist/ui/overlays/QuestionOverlay.js +45 -32
  105. package/dist/ui/overlays/QueueOverlay.js +2 -2
  106. package/dist/ui/overlays/SettingsScreen.d.ts +1 -0
  107. package/dist/ui/overlays/SettingsScreen.js +10 -3
  108. package/dist/ui/run-metrics.d.ts +23 -0
  109. package/dist/ui/run-metrics.js +27 -0
  110. package/dist/ui/status-feedback.d.ts +33 -0
  111. package/dist/ui/status-feedback.js +36 -0
  112. package/dist/ui/stdout-frame-writer.js +28 -0
  113. 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;
@@ -167,7 +132,14 @@ export class CompactionWiring {
167
132
  // setter and `contextManager` is never reassigned; the LIVE switch is inside
168
133
  // `shouldCompact`, not in the identity of this object.
169
134
  this.port = {
170
- 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
+ },
171
143
  compact: (ctx) => this.compactor.compact(ctx),
172
144
  };
173
145
  }
@@ -193,6 +165,7 @@ export class CompactionWiring {
193
165
  }
194
166
  dispose() {
195
167
  this.compactor.abort();
168
+ this.compactor.clearPendingManual();
196
169
  this.unsubscribe?.();
197
170
  this.unsubscribe = null;
198
171
  this.listeners.clear();
@@ -205,6 +178,8 @@ export class CompactionWiring {
205
178
  }
206
179
  setEnabled(enabled) {
207
180
  this.enabled = enabled;
181
+ if (!enabled)
182
+ this.compactor.abort();
208
183
  // Turning it back on is the user overriding guard 4, which is the only way
209
184
  // out of a self-disable short of a restart.
210
185
  if (enabled)
@@ -215,6 +190,8 @@ export class CompactionWiring {
215
190
  onConfigChanged(enabled) {
216
191
  if (enabled !== undefined && enabled !== this.enabled) {
217
192
  this.enabled = enabled;
193
+ if (!enabled)
194
+ this.compactor.abort();
218
195
  if (enabled)
219
196
  this.compactor.clearSelfDisable();
220
197
  }
@@ -244,8 +221,19 @@ export class CompactionWiring {
244
221
  * for such a session. A deep reset is idempotent, so both firing costs
245
222
  * nothing; deleting the controller's copy "to de-duplicate" reintroduces P0-2.
246
223
  */
247
- onHistoryReplaced() {
248
- 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.');
249
237
  }
250
238
  /**
251
239
  * The per-child manager factory, or `null` when children get none
@@ -269,7 +257,7 @@ export class CompactionWiring {
269
257
  // SINK (AC-H12). It is real money on the summarizer's price table, and a
270
258
  // background repair inside a background worker is exactly the spend a
271
259
  // user would otherwise never see.
272
- onUsage: (usage) => this.emit({ type: 'usage', usage }),
260
+ onUsage: (usage, billing) => this.emit({ type: 'usage', usage, ...billing }),
273
261
  });
274
262
  }
275
263
  // -----------------------------------------------------------------------
@@ -284,28 +272,13 @@ export class CompactionWiring {
284
272
  clearPendingManual() {
285
273
  this.compactor.clearPendingManual();
286
274
  }
287
- /**
288
- * The IDLE `/compact` path (§4.4 / D-25).
289
- *
290
- * IT VALIDATES BEFORE IT SPLICES, AND THAT IS NOT OPTIONAL (P1-7). When the
291
- * agent is idle there is no loop, so §3.3 step 5 does not run — and the only
292
- * way to replace the history is `Agent.replaceMessages`, which validates
293
- * nothing. D-4 declares that gate unbypassable and R-2 calls an invalid splice
294
- * the worst possible outcome, so this path mirrors the engine's steps 3-5
295
- * exactly, INCLUDING the failure vocabulary: the card, the log and
296
- * `/compact status` cannot tell the two paths apart.
297
- *
298
- * No watchdog handling is needed — `runLoopWithLifecycle`'s `finally` already
299
- * called `watchdog.stop()`, so nothing is armed while idle.
300
- *
301
- * ITS CALLER MUST ADOPT `messages` — THE ARCHIVE IS ALREADY WRITTEN BY THE
302
- * TIME THIS RETURNS (hardening RV-7). This path settles `applied: true` here,
303
- * one statement before the caller splices, so the "never archive a live
304
- * history" rule holds only because that caller applies unconditionally. That is
305
- * a fact about one call site rather than an invariant, which is why it is
306
- * written down where it can be checked.
307
- */
275
+ /** Prepare, validate and synchronously adopt before publishing a successful verdict. */
308
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();
309
282
  this.compactor.queueManual(args.instructions);
310
283
  // BOTH SIDES FROM CORE'S OWN ESTIMATOR, exactly as the in-loop path gets them
311
284
  // from `compaction_end` (§5.1). There is no engine event here to supply them,
@@ -342,6 +315,19 @@ export class CompactionWiring {
342
315
  this.settlePending({ applied: false, reason, tokensBefore, tokensAfter: tokensBefore });
343
316
  return { ok: false, reason };
344
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
+ }
345
331
  this.settlePending({
346
332
  applied: true,
347
333
  tokensBefore,
@@ -387,6 +373,7 @@ export class CompactionWiring {
387
373
  ...(totals.declined > 0 ? { declined: totals.declined } : {}),
388
374
  tokensReclaimed: totals.tokensReclaimed,
389
375
  usage: totals.usage,
376
+ costUsd: totals.costUsd,
390
377
  pricingUnknown: totals.pricingUnknown,
391
378
  inFlight: totals.inFlight,
392
379
  selfDisabled: totals.selfDisabled,
@@ -460,9 +447,13 @@ export class CompactionWiring {
460
447
  onAgentEvent(event) {
461
448
  if (event.type === 'agent_start') {
462
449
  this.compactor.onRunStart();
450
+ this.overflowNotified = false;
463
451
  return;
464
452
  }
453
+ if (event.type === 'agent_end')
454
+ this.compactor.onRunEnd();
465
455
  if (event.type === 'turn_end') {
456
+ this.meter.onTurnEnd(event.usage);
466
457
  // THE MEASUREMENT IS THE METER'S (context-usage-gauge-accuracy §3.3.4). It
467
458
  // subscribes to `turn_end` on the same stream and recomputes the offset,
468
459
  // the prefix length and the pressure there; the two calls that used to
@@ -501,6 +492,7 @@ export class CompactionWiring {
501
492
  // engine cannot report `true` without a `replace` outcome, which means the
502
493
  // compactor's own record already exists — so its zeroed counts are never
503
494
  // rendered.
495
+ this.compactor.settleOperation(verdict);
504
496
  const record = this.pending ?? this.skeletonRecord();
505
497
  this.pending = null;
506
498
  this.open = null;
@@ -533,7 +525,6 @@ export class CompactionWiring {
533
525
  mode: verdict.applied ? record.mode : 'none',
534
526
  };
535
527
  if (verdict.applied) {
536
- this.compactor.recordApplied(settled.tokensBefore, settled.tokensAfter);
537
528
  // SITE 1 OF THE FOUR INVALIDATION SITES (hardening §3.2.3), and THE LOAD-
538
529
  // BEARING LINE OF THE P0-1 FIX (context-usage-gauge-accuracy I-9).
539
530
  //
@@ -565,9 +556,8 @@ export class CompactionWiring {
565
556
  * archive a history that is still live" - and the two paths reach that verdict
566
557
  * at different moments relative to the splice. In loop, the ENGINE decides
567
558
  * after `validateHistory`, so this runs AFTER adoption. On the manual path the
568
- * WIRING decides and `compactNow`'s caller adopts on the next statement, so
569
- * this runs BEFORE adoption; `compactNow`'s doc comment carries that obligation
570
- * 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.
571
561
  *
572
562
  * BEST-EFFORT AND SYNCHRONOUS-BUT-GUARDED. `writeArchive` never throws and
573
563
  * logs its own failure; a failed archive never affects the compaction, and it
@@ -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
@@ -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;