@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,5 +1,6 @@
1
1
  import { type ModelSettingsSnapshot } from './model-profile-settings.js';
2
2
  import type { ModelSettingsDraft, ModelSettingsPatch, ModelSettingsSaveResult } from '../config/model-profile-store.js';
3
+ import { type CompactionIdentity } from '../compaction/memory-identity.js';
3
4
  /**
4
5
  * AgentController — builds the core `Agent`, wires the built-in toolset, and
5
6
  * exposes a small run/abort/steer surface plus the pre-flight validation that
@@ -188,6 +189,10 @@ export declare class AgentController {
188
189
  private readonly fast;
189
190
  private readonly compaction;
190
191
  private settingsRevision;
192
+ private requestVersion;
193
+ private idleCompaction;
194
+ private restoredCompactionIdentity;
195
+ private readonly compactionBusyListeners;
191
196
  private modelSettingsBlocked;
192
197
  private preparedSystemPrompt;
193
198
  private readonly contextMeter;
@@ -291,6 +296,7 @@ export declare class AgentController {
291
296
  private waitForEngineIdle;
292
297
  /** Prepare before clearing the plan, then enter the engine without yielding. */
293
298
  private startPrompt;
299
+ private prepareUserTurn;
294
300
  /** Cancel pending startup as well as any engine run already in progress. */
295
301
  abort(): void;
296
302
  private stopEngine;
@@ -379,6 +385,11 @@ export declare class AgentController {
379
385
  queueUserMessage(text: string): string;
380
386
  private enqueueSteering;
381
387
  followUp(text: string): void;
388
+ /** Resume queued input without appending a duplicate user message. */
389
+ continue(): Promise<PromptOutcome>;
390
+ /** Pending user receipts survive until Core accepts or the host clears them. */
391
+ hasPendingUserMessages(): boolean;
392
+ private compactionBusyOutcome;
382
393
  isRunning(): boolean;
383
394
  getAgentMode(): AgentMode;
384
395
  /**
@@ -538,6 +549,7 @@ export declare class AgentController {
538
549
  isModelSettingsBusy(): boolean;
539
550
  /** Monotonic revision protects an editor from all intervening settings changes. */
540
551
  getSettingsRevision(): number;
552
+ private advanceSettingsRevision;
541
553
  /** Read-only draft creation; no startup migration or credential copying. */
542
554
  createModelSettingsDraft(): ModelSettingsDraft;
543
555
  /** Save an explicit draft as a single disk/live transaction. */
@@ -596,8 +608,15 @@ export declare class AgentController {
596
608
  clearMessages(): void;
597
609
  /** Read-only view of the live system prompt (regression tests, /debug). */
598
610
  getSystemPrompt(): string;
599
- replaceMessages(messages: Message[]): void;
611
+ replaceMessages(messages: Message[], identity?: CompactionIdentity): void;
600
612
  getMessages(): Message[];
613
+ /** Atomically capture adopted history and its host credential for serialization. */
614
+ getSessionSnapshot(): {
615
+ messages: Message[];
616
+ compactionIdentity?: CompactionIdentity;
617
+ };
618
+ /** Compatibility name for the documented compaction session boundary. */
619
+ getCompactionSessionState(): ReturnType<AgentController['getSessionSnapshot']>;
601
620
  clearAllQueues(): void;
602
621
  getConfig(): CliConfig;
603
622
  getProviderRegistry(): ProviderRegistry;
@@ -759,6 +778,13 @@ export declare class AgentController {
759
778
  ok: boolean;
760
779
  reason?: string;
761
780
  }>;
781
+ /** Local prepare/adopt ownership, available before any network-start event. */
782
+ isCompactionBusy(): boolean;
783
+ /** Cancel local compaction and await ownership release before a conversation switch. */
784
+ cancelCompaction(): Promise<void>;
785
+ /** Subscribe to synchronous ownership transitions; observers cannot break adoption. */
786
+ subscribeCompactionBusy(listener: (busy: boolean) => void): () => void;
787
+ private publishCompactionBusy;
762
788
  /**
763
789
  * Subscribe to the CLI-local compaction event stream.
764
790
  *
@@ -1,5 +1,7 @@
1
1
  import { createModelSettingsDraft, saveModelSettings, reloadModelSettings, } from './model-profile-settings.js';
2
2
  import { randomUUID } from 'node:crypto';
3
+ import { verifyCompactionIdentity } from '../compaction/memory-identity.js';
4
+ import { COMPACTION_LIMITS } from '../compaction/limits.js';
3
5
  import { Agent, initProviders, ModelRegistry, } from '@aragon-agent/core';
4
6
  import { clampCompactionConfig, clampFastConfig, clampRetryConfig, clampTeamConfig, clampTodoConfig, isAdapterProvider, toRetryPolicy, } from '../config/schema.js';
5
7
  import { resolveModelProfileState } from '../config/model-profile-resolution.js';
@@ -201,6 +203,10 @@ export class AgentController {
201
203
  // and a `Set`.
202
204
  // -----------------------------------------------------------------------
203
205
  settingsRevision = 0;
206
+ requestVersion = 0;
207
+ idleCompaction;
208
+ restoredCompactionIdentity;
209
+ compactionBusyListeners = new Set();
204
210
  modelSettingsBlocked = false;
205
211
  preparedSystemPrompt;
206
212
  contextMeter;
@@ -253,7 +259,7 @@ export class AgentController {
253
259
  */
254
260
  abortRequested = false;
255
261
  /** Only exact acceptance receipts release user messages from reviewer protection. */
256
- pendingUserSteering = new Set();
262
+ pendingUserSteering = new Map();
257
263
  /** Session changes must not make a delayed receipt match a new message. */
258
264
  steeringPrefix = randomUUID();
259
265
  steeringSequence = 0;
@@ -530,6 +536,7 @@ export class AgentController {
530
536
  // EVERY session, including one with compaction off, because that is the one
531
537
  // whose gauge was broken (P1-4 / P0-2).
532
538
  this.contextMeter = new ContextMeter({
539
+ getRequestVersion: () => this.requestVersion,
533
540
  getMessages: () => this.agent.state.messages,
534
541
  getSystemPrompt: () => this.agent.state.systemPrompt,
535
542
  getModelInfo: () => this.getModelInfo(),
@@ -768,7 +775,10 @@ export class AgentController {
768
775
  });
769
776
  }
770
777
  rebuildSystemPrompt() {
771
- this.agent.setSystemPrompt(this.preparedSystemPrompt ?? this.composeSystemPrompt());
778
+ const prompt = this.preparedSystemPrompt ?? this.composeSystemPrompt();
779
+ if (prompt !== this.agent.state.systemPrompt)
780
+ this.idleCompaction?.abort();
781
+ this.agent.setSystemPrompt(prompt);
772
782
  }
773
783
  /** Re-render the prompt from the current skill set and tell the UI. */
774
784
  refreshSkills() {
@@ -799,7 +809,7 @@ export class AgentController {
799
809
  * re-memoizes the palette (P1-1). Does not touch `@aragon-agent/core`.
800
810
  */
801
811
  setTheme(name) {
802
- this.settingsRevision += 1;
812
+ this.advanceSettingsRevision();
803
813
  this.config = { ...this.config, theme: name };
804
814
  }
805
815
  /**
@@ -856,6 +866,8 @@ export class AgentController {
856
866
  * never rejects, so callers must not treat resolution as success.
857
867
  */
858
868
  async prompt(text, options = {}) {
869
+ if (this.isCompactionBusy())
870
+ return this.compactionBusyOutcome();
859
871
  // Capture BEFORE stopping: synchronous abort listeners may cancel this request.
860
872
  if (this.modelSettingsBlocked) {
861
873
  return { status: 'not-started', reason: 'failed' };
@@ -873,6 +885,8 @@ export class AgentController {
873
885
  }
874
886
  if (this.agent.state.isRunning)
875
887
  throw new Error('The previous run is still stopping.');
888
+ if (this.isCompactionBusy())
889
+ return this.compactionBusyOutcome();
876
890
  return await this.startPrompt(text, options, request);
877
891
  }
878
892
  catch (err) {
@@ -908,10 +922,7 @@ export class AgentController {
908
922
  }
909
923
  /** Prepare before clearing the plan, then enter the engine without yielding. */
910
924
  async startPrompt(text, options, request) {
911
- if (this.skillsEnabled)
912
- this.skills.beginUserTurn();
913
- this.askRounds = 0;
914
- this.fast?.setGoal(text);
925
+ this.prepareUserTurn(text);
915
926
  if (request !== this.startupSequence) {
916
927
  return { status: 'not-started', reason: 'cancelled' };
917
928
  }
@@ -919,9 +930,16 @@ export class AgentController {
919
930
  await this.agent.prompt(text);
920
931
  return { status: 'finished' };
921
932
  }
933
+ prepareUserTurn(text) {
934
+ if (this.skillsEnabled)
935
+ this.skills.beginUserTurn();
936
+ this.askRounds = 0;
937
+ this.fast?.setGoal(text);
938
+ }
922
939
  /** Cancel pending startup as well as any engine run already in progress. */
923
940
  abort() {
924
941
  this.startupSequence += 1;
942
+ this.idleCompaction?.abort();
925
943
  this.stopEngine();
926
944
  }
927
945
  stopEngine() {
@@ -1066,11 +1084,11 @@ export class AgentController {
1066
1084
  }
1067
1085
  enqueueSteering(text, user) {
1068
1086
  // Both reviewer and user guidance can activate queued skill frames.
1069
- if (this.skillsEnabled)
1087
+ if (this.skillsEnabled && this.isRunning())
1070
1088
  this.skills.absorbPendingFrames();
1071
1089
  const id = user ? `${this.steeringPrefix}:${++this.steeringSequence}` : undefined;
1072
1090
  if (id !== undefined)
1073
- this.pendingUserSteering.add(id);
1091
+ this.pendingUserSteering.set(id, text);
1074
1092
  try {
1075
1093
  this.agent.steer(text, id);
1076
1094
  }
@@ -1084,6 +1102,30 @@ export class AgentController {
1084
1102
  followUp(text) {
1085
1103
  this.agent.followUp(text);
1086
1104
  }
1105
+ /** Resume queued input without appending a duplicate user message. */
1106
+ async continue() {
1107
+ if (this.isCompactionBusy())
1108
+ return this.compactionBusyOutcome();
1109
+ if (this.modelSettingsBlocked || this.isRunning()) {
1110
+ return { status: 'not-started', reason: 'failed' };
1111
+ }
1112
+ if (this.pendingUserSteering.size > 0) {
1113
+ const request = ++this.startupSequence;
1114
+ this.prepareUserTurn([...this.pendingUserSteering.values()].join('\n\n'));
1115
+ if (request !== this.startupSequence) {
1116
+ return { status: 'not-started', reason: 'cancelled' };
1117
+ }
1118
+ this.todos?.beginUserTurn('new-task');
1119
+ }
1120
+ await this.agent.continue();
1121
+ return { status: 'finished' };
1122
+ }
1123
+ /** Pending user receipts survive until Core accepts or the host clears them. */
1124
+ hasPendingUserMessages() { return this.pendingUserSteering.size > 0; }
1125
+ compactionBusyOutcome() {
1126
+ this.notifyHostFn?.('warn', 'Context compaction is busy; your input was not submitted.');
1127
+ return { status: 'not-started', reason: 'failed' };
1128
+ }
1087
1129
  isRunning() {
1088
1130
  return this.agent.state.isRunning;
1089
1131
  }
@@ -1216,7 +1258,7 @@ export class AgentController {
1216
1258
  * honestly by checking `isTeamRegistered()` first.
1217
1259
  */
1218
1260
  setTeamEnabled(enabled) {
1219
- this.settingsRevision += 1;
1261
+ this.advanceSettingsRevision();
1220
1262
  this.teamEnabled = enabled;
1221
1263
  this.rebuildSystemPrompt();
1222
1264
  }
@@ -1230,7 +1272,7 @@ export class AgentController {
1230
1272
  */
1231
1273
  setTeamConfig(patch) {
1232
1274
  const team = clampTeamConfig({ ...this.config.team, ...patch });
1233
- this.settingsRevision += 1;
1275
+ this.advanceSettingsRevision();
1234
1276
  this.config = { ...this.config, team };
1235
1277
  this.rebuildSystemPrompt();
1236
1278
  return team;
@@ -1256,6 +1298,7 @@ export class AgentController {
1256
1298
  }
1257
1299
  /** Abort every child and clear timers. Idempotent; safe to call twice. */
1258
1300
  dispose() {
1301
+ this.idleCompaction?.abort();
1259
1302
  this.disposed = true;
1260
1303
  this.modelMetadataGeneration += 1;
1261
1304
  this.modelMetadataAbort.abort();
@@ -1295,7 +1338,7 @@ export class AgentController {
1295
1338
  * reports that honestly by checking `isTodoRegistered()` first.
1296
1339
  */
1297
1340
  setTodoEnabled(enabled) {
1298
- this.settingsRevision += 1;
1341
+ this.advanceSettingsRevision();
1299
1342
  this.todoEnabled = enabled;
1300
1343
  this.rebuildSystemPrompt();
1301
1344
  }
@@ -1318,7 +1361,7 @@ export class AgentController {
1318
1361
  */
1319
1362
  setTodoConfig(patch) {
1320
1363
  const todo = clampTodoConfig({ ...this.config.todo, ...patch });
1321
- this.settingsRevision += 1;
1364
+ this.advanceSettingsRevision();
1322
1365
  this.config = { ...this.config, todo };
1323
1366
  this.rebuildSystemPrompt();
1324
1367
  return todo;
@@ -1374,7 +1417,7 @@ export class AgentController {
1374
1417
  */
1375
1418
  setRetryConfig(patch) {
1376
1419
  const retry = clampRetryConfig({ ...this.config.retry, ...patch });
1377
- this.settingsRevision += 1;
1420
+ this.advanceSettingsRevision();
1378
1421
  this.config = { ...this.config, retry };
1379
1422
  this.providerRegistry.setRetryPolicy(toRetryPolicy(retry));
1380
1423
  return retry;
@@ -1384,11 +1427,16 @@ export class AgentController {
1384
1427
  // -----------------------------------------------------------------------
1385
1428
  /** True while any request can still use a model or credential snapshot. */
1386
1429
  isModelSettingsBusy() {
1387
- return this.isRunning() || this.isTeamBusy() || this.getCompactionSnapshot().inFlight
1430
+ return this.isRunning() || this.isTeamBusy() || this.isCompactionBusy()
1431
+ || this.getCompactionSnapshot().inFlight
1388
1432
  || this.getFastStatus().snapshot.inFlight;
1389
1433
  }
1390
1434
  /** Monotonic revision protects an editor from all intervening settings changes. */
1391
1435
  getSettingsRevision() { return this.settingsRevision; }
1436
+ advanceSettingsRevision() {
1437
+ this.settingsRevision += 1;
1438
+ this.idleCompaction?.abort();
1439
+ }
1392
1440
  /** Read-only draft creation; no startup migration or credential copying. */
1393
1441
  createModelSettingsDraft() {
1394
1442
  return createModelSettingsDraft(this.config, this.settingsRevision);
@@ -1414,10 +1462,13 @@ export class AgentController {
1414
1462
  }
1415
1463
  /** Apply prepared state synchronously, without rebuilding the controller or its history. */
1416
1464
  applyModelSettingsSnapshot(snapshot) {
1465
+ const connectionChanged = this.config.provider !== snapshot.config.provider
1466
+ || this.config.model !== snapshot.config.model || this.config.baseUrl !== snapshot.config.baseUrl;
1467
+ this.idleCompaction?.abort();
1417
1468
  this.preparedSystemPrompt = snapshot.systemPrompt;
1418
1469
  try {
1419
1470
  this.config = snapshot.config;
1420
- this.settingsRevision += 1;
1471
+ this.advanceSettingsRevision();
1421
1472
  this.agent.setModel(snapshot.model);
1422
1473
  this.agent.setThinkingLevel(snapshot.config.thinkingLevel);
1423
1474
  this.agent.setMaxTokens(snapshot.config.maxTokens);
@@ -1429,7 +1480,12 @@ export class AgentController {
1429
1480
  }
1430
1481
  this.fast?.onConfigChanged(false, snapshot.config.fast.enabled);
1431
1482
  this.compaction?.onConfigChanged(snapshot.config.compaction.enabled);
1432
- this.contextMeter.onWindowChanged();
1483
+ if (connectionChanged) {
1484
+ this.requestVersion += 1;
1485
+ this.contextMeter.onHistoryReplaced();
1486
+ }
1487
+ else
1488
+ this.contextMeter.onWindowChanged();
1433
1489
  this.rebuildSystemPrompt();
1434
1490
  }
1435
1491
  finally {
@@ -1451,9 +1507,11 @@ export class AgentController {
1451
1507
  setModel(provider, model, baseUrl) {
1452
1508
  if (this.isModelSettingsBusy())
1453
1509
  throw new Error('Model settings are busy.');
1510
+ const connectionChanged = this.config.provider !== provider || this.config.model !== model
1511
+ || this.config.baseUrl !== baseUrl;
1454
1512
  if (this.modelMetadataEnabled)
1455
1513
  this.resetModelMetadataRequest();
1456
- this.settingsRevision += 1;
1514
+ this.advanceSettingsRevision();
1457
1515
  this.config = { ...this.config, provider, model, baseUrl };
1458
1516
  this.config.modelProfileState = resolveModelProfileState(this.config);
1459
1517
  this.agent.setModel({ providerId: provider, modelId: model, ...(baseUrl ? { baseUrl } : {}) });
@@ -1468,30 +1526,29 @@ export class AgentController {
1468
1526
  // so the snapshot the chip and `/compact status` read has to be refreshed
1469
1527
  // for the same reason and at the same moments (the RV-3 lesson).
1470
1528
  this.compaction?.onConfigChanged();
1471
- // THE DENOMINATOR MOVED, NOT THE HISTORY (context-usage-gauge-accuracy §3.4).
1472
- // A re-measure, deliberately NOT a reset: the measured base is still a true
1473
- // statement about the messages, and throwing it away would swap a correct
1474
- // numerator for a whole-history estimate on a common operation. The
1475
- // numerator's calibration is one turn stale until the next `turn_end`
1476
- // (RV-13), which is the trade this call names.
1477
- this.contextMeter.onWindowChanged();
1529
+ if (connectionChanged) {
1530
+ this.requestVersion += 1;
1531
+ this.contextMeter.onHistoryReplaced();
1532
+ }
1533
+ else
1534
+ this.contextMeter.onWindowChanged();
1478
1535
  if (this.modelMetadataEnabled)
1479
1536
  void this.refreshModelMetadata();
1480
1537
  }
1481
1538
  setThinkingLevel(level) {
1482
- this.settingsRevision += 1;
1539
+ this.advanceSettingsRevision();
1483
1540
  this.config = { ...this.config, thinkingLevel: level };
1484
1541
  this.agent.setThinkingLevel(level);
1485
1542
  }
1486
1543
  setMaxTokens(value) {
1487
- this.settingsRevision += 1;
1544
+ this.advanceSettingsRevision();
1488
1545
  this.config = { ...this.config, maxTokens: value };
1489
1546
  this.agent.setMaxTokens(value);
1490
1547
  }
1491
1548
  setApiKey(provider, key) {
1492
1549
  // The Agent resolves keys via `resolveKey(this.config)` at call time, so
1493
1550
  // updating the config here is sufficient — no Agent rebuild needed.
1494
- this.settingsRevision += 1;
1551
+ this.advanceSettingsRevision();
1495
1552
  this.config = {
1496
1553
  ...this.config,
1497
1554
  apiKeys: { ...this.config.apiKeys, [provider]: key },
@@ -1512,7 +1569,7 @@ export class AgentController {
1512
1569
  }
1513
1570
  setCwd(cwd) {
1514
1571
  this.cwd = cwd;
1515
- this.settingsRevision += 1;
1572
+ this.advanceSettingsRevision();
1516
1573
  this.config = { ...this.config, cwd };
1517
1574
  // The project scope moved with the cwd, so rescan BEFORE rebuilding — then
1518
1575
  // let the single prompt entry point emit a prompt that reflects both the new
@@ -1581,6 +1638,8 @@ export class AgentController {
1581
1638
  };
1582
1639
  }
1583
1640
  clearMessages() {
1641
+ this.idleCompaction?.abort();
1642
+ this.restoredCompactionIdentity = undefined;
1584
1643
  this.agent.clearMessages();
1585
1644
  this.startupSequence += 1;
1586
1645
  // INVALIDATION SITE 3 (context-auto-compaction-hardening §3.2.3). The history
@@ -1608,7 +1667,9 @@ export class AgentController {
1608
1667
  getSystemPrompt() {
1609
1668
  return this.agent.state.systemPrompt;
1610
1669
  }
1611
- replaceMessages(messages) {
1670
+ replaceMessages(messages, identity) {
1671
+ this.idleCompaction?.abort();
1672
+ this.restoredCompactionIdentity = identity;
1612
1673
  this.agent.replaceMessages(messages);
1613
1674
  this.startupSequence += 1;
1614
1675
  // INVALIDATION SITE 4 (§3.2.3) - `/resume`, and any future host-side rewrite.
@@ -1621,12 +1682,26 @@ export class AgentController {
1621
1682
  // reading stood until the next completed turn. The DEEP reset also drops
1622
1683
  // `estimateOffset`, because `/resume` can change the model in the same
1623
1684
  // breath (I-8).
1624
- this.compaction?.onHistoryReplaced();
1685
+ this.compaction?.onHistoryReplaced(identity);
1625
1686
  this.contextMeter.onHistoryReplaced();
1626
1687
  }
1627
1688
  getMessages() {
1628
1689
  return this.agent.state.messages;
1629
1690
  }
1691
+ /** Atomically capture adopted history and its host credential for serialization. */
1692
+ getSessionSnapshot() {
1693
+ const messages = this.getMessages();
1694
+ const identity = this.compaction ? this.compaction.getIdentity() : this.restoredCompactionIdentity;
1695
+ if (identity && !verifyCompactionIdentity(messages, identity)) {
1696
+ getLogger().debug('agent', 'invalid_compaction_session_identity');
1697
+ }
1698
+ // Retain invalid evidence so damaged host memory cannot become ordinary text.
1699
+ return { messages, ...(identity ? { compactionIdentity: { ...identity } } : {}) };
1700
+ }
1701
+ /** Compatibility name for the documented compaction session boundary. */
1702
+ getCompactionSessionState() {
1703
+ return this.getSessionSnapshot();
1704
+ }
1630
1705
  clearAllQueues() {
1631
1706
  this.agent.clearAllQueues();
1632
1707
  this.pendingUserSteering.clear();
@@ -1726,7 +1801,7 @@ export class AgentController {
1726
1801
  * `/fast` reports that honestly by checking `isFastRegistered()` first.
1727
1802
  */
1728
1803
  setFastEnabled(enabled) {
1729
- this.settingsRevision += 1;
1804
+ this.advanceSettingsRevision();
1730
1805
  this.fast?.setEnabled(enabled);
1731
1806
  }
1732
1807
  getFastConfig() {
@@ -1742,7 +1817,7 @@ export class AgentController {
1742
1817
  */
1743
1818
  setFastConfig(patch) {
1744
1819
  const fast = clampFastConfig({ ...this.config.fast, ...patch });
1745
- this.settingsRevision += 1;
1820
+ this.advanceSettingsRevision();
1746
1821
  this.config = { ...this.config, fast };
1747
1822
  // `onConfigChanged` rebuilds the prompt; when there is no wiring (the tier
1748
1823
  // was never registered) nothing has to be rebuilt, because no block is
@@ -1842,7 +1917,7 @@ export class AgentController {
1842
1917
  * on purpose (§3.2).
1843
1918
  */
1844
1919
  setCompactionEnabled(enabled) {
1845
- this.settingsRevision += 1;
1920
+ this.advanceSettingsRevision();
1846
1921
  this.compaction?.setEnabled(enabled);
1847
1922
  }
1848
1923
  getCompactionConfig() {
@@ -1857,7 +1932,7 @@ export class AgentController {
1857
1932
  */
1858
1933
  setCompactionConfig(patch) {
1859
1934
  const compaction = clampCompactionConfig({ ...this.config.compaction, ...patch });
1860
- this.settingsRevision += 1;
1935
+ this.advanceSettingsRevision();
1861
1936
  this.config = { ...this.config, compaction };
1862
1937
  this.compaction?.onConfigChanged();
1863
1938
  return compaction;
@@ -1910,18 +1985,80 @@ export class AgentController {
1910
1985
  return { ok: false, reason: 'settings_restart_required' };
1911
1986
  if (!this.compaction)
1912
1987
  return { ok: false, reason: 'not_registered' };
1988
+ if (this.isCompactionBusy() || this.isRunning())
1989
+ return { ok: false, reason: 'busy' };
1913
1990
  const controller = new AbortController();
1914
- const outcome = await this.compaction.compactNow({
1915
- messages: this.agent.state.messages,
1916
- systemPrompt: this.agent.state.systemPrompt,
1917
- model: this.agent.state.model,
1918
- signal: controller.signal,
1919
- ...(instructions ? { instructions } : {}),
1991
+ const epoch = this.startupSequence;
1992
+ const settings = this.settingsRevision;
1993
+ let adopted = false;
1994
+ let timedOut = false;
1995
+ const isCurrent = () => this.idleCompaction === controller && !controller.signal.aborted
1996
+ && epoch === this.startupSequence && settings === this.settingsRevision
1997
+ && !this.isRunning() && !this.disposed;
1998
+ this.idleCompaction = controller;
1999
+ this.publishCompactionBusy(true);
2000
+ const deadline = setTimeout(() => {
2001
+ timedOut = true;
2002
+ controller.abort();
2003
+ }, COMPACTION_LIMITS.operationTimeoutMs);
2004
+ deadline.unref?.();
2005
+ try {
2006
+ // The wiring races cancellation locally and settles before releasing ownership.
2007
+ const outcome = await this.compaction.compactNow({
2008
+ messages: this.agent.state.messages,
2009
+ systemPrompt: this.agent.state.systemPrompt,
2010
+ model: this.agent.state.model,
2011
+ signal: controller.signal,
2012
+ ...(instructions ? { instructions } : {}),
2013
+ isCurrent,
2014
+ adopt: (messages) => {
2015
+ if (!isCurrent())
2016
+ throw new Error('stale_history');
2017
+ this.agent.replaceMessages(messages);
2018
+ adopted = true;
2019
+ },
2020
+ });
2021
+ return outcome.ok || adopted ? { ok: true }
2022
+ : { ok: false, reason: timedOut ? 'timeout' : outcome.reason };
2023
+ }
2024
+ finally {
2025
+ clearTimeout(deadline);
2026
+ if (this.idleCompaction === controller) {
2027
+ this.idleCompaction = undefined;
2028
+ this.publishCompactionBusy(false);
2029
+ }
2030
+ }
2031
+ }
2032
+ /** Local prepare/adopt ownership, available before any network-start event. */
2033
+ isCompactionBusy() { return this.idleCompaction !== undefined; }
2034
+ /** Cancel local compaction and await ownership release before a conversation switch. */
2035
+ cancelCompaction() {
2036
+ if (!this.idleCompaction)
2037
+ return Promise.resolve();
2038
+ return new Promise((resolve) => {
2039
+ const unsubscribe = this.subscribeCompactionBusy((busy) => {
2040
+ if (busy)
2041
+ return;
2042
+ unsubscribe();
2043
+ resolve();
2044
+ });
2045
+ this.abort();
1920
2046
  });
1921
- if (!outcome.ok)
1922
- return { ok: false, reason: outcome.reason };
1923
- this.agent.replaceMessages(outcome.messages);
1924
- return { ok: true };
2047
+ }
2048
+ /** Subscribe to synchronous ownership transitions; observers cannot break adoption. */
2049
+ subscribeCompactionBusy(listener) {
2050
+ this.compactionBusyListeners.add(listener);
2051
+ return () => { this.compactionBusyListeners.delete(listener); };
2052
+ }
2053
+ publishCompactionBusy(busy) {
2054
+ for (const listener of this.compactionBusyListeners) {
2055
+ try {
2056
+ listener(busy);
2057
+ }
2058
+ catch {
2059
+ getLogger().debug('agent', 'compaction_busy_listener_threw');
2060
+ }
2061
+ }
1925
2062
  }
1926
2063
  /**
1927
2064
  * Subscribe to the CLI-local compaction event stream.
@@ -8,7 +8,7 @@
8
8
  * ASCII ONLY — `agent/` is inside the glyph scanner's scope.
9
9
  */
10
10
  /** Bump when the wording changes, so a transcript can be dated. */
11
- export declare const PLAN_MODE_BLOCK_VERSION = "v1-2026-07";
11
+ export declare const PLAN_MODE_BLOCK_VERSION = "v2-2026-10-08";
12
12
  export interface PlanModeBlockOptions {
13
13
  /** False under `-p` / piped stdin: no overlay can be rendered, so no tools. */
14
14
  interactive: boolean;
@@ -8,7 +8,7 @@
8
8
  * ASCII ONLY — `agent/` is inside the glyph scanner's scope.
9
9
  */
10
10
  /** Bump when the wording changes, so a transcript can be dated. */
11
- export const PLAN_MODE_BLOCK_VERSION = 'v1-2026-07';
11
+ export const PLAN_MODE_BLOCK_VERSION = 'v2-2026-10-08';
12
12
  /**
13
13
  * The closing paragraph of the interactive block.
14
14
  *
@@ -75,8 +75,9 @@ export function buildPlanModeBlock(options) {
75
75
  'design mode: you must not change anything on disk and must not run commands.',
76
76
  '',
77
77
  'Work in this order:',
78
- '1. Ground yourself in the real code. Use read_file, list_dir, glob, grep and any',
79
- ' relevant skill. Never plan against a directory structure you have not read.',
78
+ '1. Follow the project-guidance protocol: read applicable guidance and index navigation',
79
+ ' before any source search or read. Then inspect the real code with read_file,',
80
+ ' list_dir, glob, grep and relevant skills. Never plan against unread structure.',
80
81
  ...askStep,
81
82
  ...submitStep,
82
83
  '',
@@ -0,0 +1,4 @@
1
+ /** Shared workspace-orientation protocol; no local content is injected. */
2
+ export declare const PROJECT_GUIDANCE_BLOCK_VERSION = "v5-2026-10-08";
3
+ /** Return the versioned English protocol without performing discovery or IO. */
4
+ export declare function buildProjectGuidanceBlock(): string;