@aragon-agent/cli 0.6.14 → 0.6.16

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 +14 -0
  2. package/README.md +295 -238
  3. package/dist/agent/controller.d.ts +37 -1
  4. package/dist/agent/controller.js +195 -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/cli.js +49 -2
  13. package/dist/commands/builtins.js +26 -28
  14. package/dist/compaction/child.d.ts +9 -1
  15. package/dist/compaction/child.js +39 -13
  16. package/dist/compaction/command.js +20 -10
  17. package/dist/compaction/compactor.d.ts +33 -204
  18. package/dist/compaction/compactor.js +311 -726
  19. package/dist/compaction/context-command.js +14 -3
  20. package/dist/compaction/digest.d.ts +15 -51
  21. package/dist/compaction/digest.js +113 -148
  22. package/dist/compaction/limits.d.ts +19 -142
  23. package/dist/compaction/limits.js +20 -147
  24. package/dist/compaction/memory-identity.d.ts +16 -0
  25. package/dist/compaction/memory-identity.js +63 -0
  26. package/dist/compaction/memory-input.d.ts +25 -0
  27. package/dist/compaction/memory-input.js +106 -0
  28. package/dist/compaction/memory-render.d.ts +4 -0
  29. package/dist/compaction/memory-render.js +47 -0
  30. package/dist/compaction/memory-validation.d.ts +21 -0
  31. package/dist/compaction/memory-validation.js +268 -0
  32. package/dist/compaction/memory.d.ts +79 -0
  33. package/dist/compaction/memory.js +118 -0
  34. package/dist/compaction/meter.d.ts +14 -10
  35. package/dist/compaction/meter.js +53 -19
  36. package/dist/compaction/operation.d.ts +18 -0
  37. package/dist/compaction/operation.js +68 -0
  38. package/dist/compaction/pressure.d.ts +5 -20
  39. package/dist/compaction/pressure.js +13 -23
  40. package/dist/compaction/summarize-call.d.ts +3 -16
  41. package/dist/compaction/summarize-call.js +20 -20
  42. package/dist/compaction/summary-prompt.d.ts +29 -66
  43. package/dist/compaction/summary-prompt.js +132 -178
  44. package/dist/compaction/types.d.ts +27 -4
  45. package/dist/compaction/wiring.d.ts +10 -25
  46. package/dist/compaction/wiring.js +56 -66
  47. package/dist/config/app-paths.d.ts +10 -0
  48. package/dist/config/app-paths.js +12 -0
  49. package/dist/config/env.d.ts +2 -1
  50. package/dist/config/env.js +13 -1
  51. package/dist/config/load.d.ts +9 -0
  52. package/dist/config/load.js +14 -0
  53. package/dist/config/model-windows.d.ts +91 -0
  54. package/dist/config/model-windows.js +181 -0
  55. package/dist/config/schema.d.ts +18 -1
  56. package/dist/config/schema.js +2 -1
  57. package/dist/exec/events.d.ts +8 -0
  58. package/dist/exec/index.js +13 -6
  59. package/dist/exec/runner.d.ts +8 -0
  60. package/dist/exec/runner.js +26 -2
  61. package/dist/input/csiu-keys.d.ts +66 -0
  62. package/dist/input/csiu-keys.js +170 -0
  63. package/dist/input/keyboard-enhancement.d.ts +42 -0
  64. package/dist/input/keyboard-enhancement.js +65 -0
  65. package/dist/input/stdin-filter.d.ts +9 -0
  66. package/dist/input/stdin-filter.js +45 -9
  67. package/dist/input/win32-input-mode.d.ts +61 -0
  68. package/dist/input/win32-input-mode.js +251 -0
  69. package/dist/session/persist.d.ts +3 -0
  70. package/dist/session/persist.js +9 -0
  71. package/dist/session/store.js +2 -0
  72. package/dist/session/validate-session.js +34 -0
  73. package/dist/team/subagent.js +8 -0
  74. package/dist/ui/App.js +218 -116
  75. package/dist/ui/BottomStatusRow.d.ts +5 -10
  76. package/dist/ui/BottomStatusRow.js +7 -11
  77. package/dist/ui/Composer.d.ts +4 -2
  78. package/dist/ui/Composer.js +2 -2
  79. package/dist/ui/Header.d.ts +6 -2
  80. package/dist/ui/Header.js +7 -6
  81. package/dist/ui/PromptInput.d.ts +3 -1
  82. package/dist/ui/PromptInput.js +32 -41
  83. package/dist/ui/StatusBar.d.ts +6 -0
  84. package/dist/ui/StatusBar.js +8 -7
  85. package/dist/ui/Transcript.js +1 -1
  86. package/dist/ui/entries/CompactionCard.d.ts +3 -1
  87. package/dist/ui/entries/CompactionCard.js +7 -5
  88. package/dist/ui/frame-differ.js +27 -0
  89. package/dist/ui/frame-observer.js +18 -5
  90. package/dist/ui/interaction-copy.d.ts +73 -73
  91. package/dist/ui/interaction-copy.js +30 -38
  92. package/dist/ui/interaction-hints.d.ts +8 -1
  93. package/dist/ui/interaction-hints.js +56 -69
  94. package/dist/ui/layout/AppShell.d.ts +6 -2
  95. package/dist/ui/layout/AppShell.js +2 -2
  96. package/dist/ui/layout/OverlayFrame.d.ts +2 -0
  97. package/dist/ui/layout/OverlayFrame.js +23 -14
  98. package/dist/ui/layout/ScrollViewport.d.ts +3 -1
  99. package/dist/ui/layout/ScrollViewport.js +10 -2
  100. package/dist/ui/layout/budget.d.ts +25 -26
  101. package/dist/ui/layout/budget.js +29 -22
  102. package/dist/ui/layout/document-layout.d.ts +2 -2
  103. package/dist/ui/layout/document-layout.js +2 -3
  104. package/dist/ui/layout/queue-layout.js +4 -4
  105. package/dist/ui/layout/status-detail-layout.d.ts +9 -0
  106. package/dist/ui/layout/status-detail-layout.js +56 -0
  107. package/dist/ui/layout/status-layout.d.ts +19 -2
  108. package/dist/ui/layout/status-layout.js +95 -84
  109. package/dist/ui/layout/wrap-rows.d.ts +2 -0
  110. package/dist/ui/layout/wrap-rows.js +21 -7
  111. package/dist/ui/overlays/ConfirmDialog.d.ts +2 -1
  112. package/dist/ui/overlays/ConfirmDialog.js +22 -3
  113. package/dist/ui/overlays/HelpOverlay.js +9 -2
  114. package/dist/ui/overlays/ModelPicker.d.ts +5 -8
  115. package/dist/ui/overlays/ModelPicker.js +44 -10
  116. package/dist/ui/overlays/ModelProfileEditor.js +2 -0
  117. package/dist/ui/overlays/ModelProfilePicker.d.ts +1 -0
  118. package/dist/ui/overlays/ModelProfilePicker.js +4 -2
  119. package/dist/ui/overlays/PlanReviewOverlay.d.ts +2 -1
  120. package/dist/ui/overlays/PlanReviewOverlay.js +7 -4
  121. package/dist/ui/overlays/QuestionOverlay.d.ts +2 -1
  122. package/dist/ui/overlays/QuestionOverlay.js +45 -32
  123. package/dist/ui/overlays/QueueOverlay.js +2 -2
  124. package/dist/ui/overlays/SettingsScreen.d.ts +1 -0
  125. package/dist/ui/overlays/SettingsScreen.js +10 -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,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
@@ -14,6 +15,7 @@ import type { PromptOptions, PromptOutcome } from './prompt-options.js';
14
15
  import { ModelRegistry, type AgentEvent, type AgentTool, type Message, type ModelInfo, type ModelRef, type ProviderRegistry, type ThinkingLevel } from '@aragon-agent/core';
15
16
  import type { CliConfig, CompactionConfig, FastConfig, RetryConfig, TeamConfig, ThemeName, TodoConfig } from '../config/schema.js';
16
17
  import type { ModelRole } from '../config/model-profiles.js';
18
+ import { ModelWindows } from '../config/model-windows.js';
17
19
  import { type ConfirmRequest } from '../tools/index.js';
18
20
  import type { ToolPermission } from '../exec/permission.js';
19
21
  import { type ToolOutputListener } from '../tools/tool-output-store.js';
@@ -101,6 +103,13 @@ export interface ControllerDeps {
101
103
  * once at construction and lost at the first `rebuildSystemPrompt()`.
102
104
  */
103
105
  appendSystemPrompt?: string;
106
+ /**
107
+ * The per-model window table from ~/.aragon-agent/model-windows.json.
108
+ * Injectable so tests can hand the controller a table without writing into
109
+ * the shared vitest home root (app-paths.ts TEST-ISOLATION CONTRACT);
110
+ * production leaves it absent and the constructor builds the real one.
111
+ */
112
+ modelWindows?: ModelWindows;
104
113
  }
105
114
  /** What `setAgentMode` actually adopted — see `AgentController.setAgentMode`. */
106
115
  export interface AgentModeState {
@@ -123,6 +132,8 @@ export declare class AgentController {
123
132
  private readonly agent;
124
133
  private readonly providerRegistry;
125
134
  private readonly modelRegistry;
135
+ /** Per-model user-declared context windows; see `config/model-windows.ts`. */
136
+ private readonly modelWindows;
126
137
  private modelMetadataEnabled;
127
138
  private modelMetadataGeneration;
128
139
  private modelMetadataAbort;
@@ -188,6 +199,10 @@ export declare class AgentController {
188
199
  private readonly fast;
189
200
  private readonly compaction;
190
201
  private settingsRevision;
202
+ private requestVersion;
203
+ private idleCompaction;
204
+ private restoredCompactionIdentity;
205
+ private readonly compactionBusyListeners;
191
206
  private modelSettingsBlocked;
192
207
  private preparedSystemPrompt;
193
208
  private readonly contextMeter;
@@ -291,6 +306,7 @@ export declare class AgentController {
291
306
  private waitForEngineIdle;
292
307
  /** Prepare before clearing the plan, then enter the engine without yielding. */
293
308
  private startPrompt;
309
+ private prepareUserTurn;
294
310
  /** Cancel pending startup as well as any engine run already in progress. */
295
311
  abort(): void;
296
312
  private stopEngine;
@@ -379,6 +395,11 @@ export declare class AgentController {
379
395
  queueUserMessage(text: string): string;
380
396
  private enqueueSteering;
381
397
  followUp(text: string): void;
398
+ /** Resume queued input without appending a duplicate user message. */
399
+ continue(): Promise<PromptOutcome>;
400
+ /** Pending user receipts survive until Core accepts or the host clears them. */
401
+ hasPendingUserMessages(): boolean;
402
+ private compactionBusyOutcome;
382
403
  isRunning(): boolean;
383
404
  getAgentMode(): AgentMode;
384
405
  /**
@@ -538,6 +559,7 @@ export declare class AgentController {
538
559
  isModelSettingsBusy(): boolean;
539
560
  /** Monotonic revision protects an editor from all intervening settings changes. */
540
561
  getSettingsRevision(): number;
562
+ private advanceSettingsRevision;
541
563
  /** Read-only draft creation; no startup migration or credential copying. */
542
564
  createModelSettingsDraft(): ModelSettingsDraft;
543
565
  /** Save an explicit draft as a single disk/live transaction. */
@@ -596,8 +618,15 @@ export declare class AgentController {
596
618
  clearMessages(): void;
597
619
  /** Read-only view of the live system prompt (regression tests, /debug). */
598
620
  getSystemPrompt(): string;
599
- replaceMessages(messages: Message[]): void;
621
+ replaceMessages(messages: Message[], identity?: CompactionIdentity): void;
600
622
  getMessages(): Message[];
623
+ /** Atomically capture adopted history and its host credential for serialization. */
624
+ getSessionSnapshot(): {
625
+ messages: Message[];
626
+ compactionIdentity?: CompactionIdentity;
627
+ };
628
+ /** Compatibility name for the documented compaction session boundary. */
629
+ getCompactionSessionState(): ReturnType<AgentController['getSessionSnapshot']>;
601
630
  clearAllQueues(): void;
602
631
  getConfig(): CliConfig;
603
632
  getProviderRegistry(): ProviderRegistry;
@@ -759,6 +788,13 @@ export declare class AgentController {
759
788
  ok: boolean;
760
789
  reason?: string;
761
790
  }>;
791
+ /** Local prepare/adopt ownership, available before any network-start event. */
792
+ isCompactionBusy(): boolean;
793
+ /** Cancel local compaction and await ownership release before a conversation switch. */
794
+ cancelCompaction(): Promise<void>;
795
+ /** Subscribe to synchronous ownership transitions; observers cannot break adoption. */
796
+ subscribeCompactionBusy(listener: (busy: boolean) => void): () => void;
797
+ private publishCompactionBusy;
762
798
  /**
763
799
  * Subscribe to the CLI-local compaction event stream.
764
800
  *
@@ -1,10 +1,13 @@
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';
6
8
  import { makeGetApiKey } from '../config/load.js';
7
9
  import { updatePersistedConfig } from '../config/store.js';
10
+ import { ModelWindows } from '../config/model-windows.js';
8
11
  import { createBuiltinTools } from '../tools/index.js';
9
12
  import { createFileChangeStore } from '../tools/file-change-store.js';
10
13
  import { createToolOutputStore, } from '../tools/tool-output-store.js';
@@ -51,6 +54,8 @@ export class AgentController {
51
54
  agent;
52
55
  providerRegistry;
53
56
  modelRegistry;
57
+ /** Per-model user-declared context windows; see `config/model-windows.ts`. */
58
+ modelWindows;
54
59
  modelMetadataEnabled = false;
55
60
  modelMetadataGeneration = 0;
56
61
  modelMetadataAbort = new AbortController();
@@ -201,6 +206,10 @@ export class AgentController {
201
206
  // and a `Set`.
202
207
  // -----------------------------------------------------------------------
203
208
  settingsRevision = 0;
209
+ requestVersion = 0;
210
+ idleCompaction;
211
+ restoredCompactionIdentity;
212
+ compactionBusyListeners = new Set();
204
213
  modelSettingsBlocked = false;
205
214
  preparedSystemPrompt;
206
215
  contextMeter;
@@ -253,7 +262,7 @@ export class AgentController {
253
262
  */
254
263
  abortRequested = false;
255
264
  /** Only exact acceptance receipts release user messages from reviewer protection. */
256
- pendingUserSteering = new Set();
265
+ pendingUserSteering = new Map();
257
266
  /** Session changes must not make a delayed receipt match a new message. */
258
267
  steeringPrefix = randomUUID();
259
268
  steeringSequence = 0;
@@ -269,6 +278,7 @@ export class AgentController {
269
278
  // reuses this very registry instance.
270
279
  this.providerRegistry = initProviders({ retryPolicy: toRetryPolicy(config.retry) });
271
280
  this.modelRegistry = new ModelRegistry(this.providerRegistry);
281
+ this.modelWindows = deps.modelWindows ?? new ModelWindows();
272
282
  this.skillsEnabled = config.skills.enabled;
273
283
  this.onSkillsChanged = deps.onSkillsChanged;
274
284
  this.skills = new SkillService({
@@ -530,6 +540,7 @@ export class AgentController {
530
540
  // EVERY session, including one with compaction off, because that is the one
531
541
  // whose gauge was broken (P1-4 / P0-2).
532
542
  this.contextMeter = new ContextMeter({
543
+ getRequestVersion: () => this.requestVersion,
533
544
  getMessages: () => this.agent.state.messages,
534
545
  getSystemPrompt: () => this.agent.state.systemPrompt,
535
546
  getModelInfo: () => this.getModelInfo(),
@@ -768,7 +779,10 @@ export class AgentController {
768
779
  });
769
780
  }
770
781
  rebuildSystemPrompt() {
771
- this.agent.setSystemPrompt(this.preparedSystemPrompt ?? this.composeSystemPrompt());
782
+ const prompt = this.preparedSystemPrompt ?? this.composeSystemPrompt();
783
+ if (prompt !== this.agent.state.systemPrompt)
784
+ this.idleCompaction?.abort();
785
+ this.agent.setSystemPrompt(prompt);
772
786
  }
773
787
  /** Re-render the prompt from the current skill set and tell the UI. */
774
788
  refreshSkills() {
@@ -799,7 +813,7 @@ export class AgentController {
799
813
  * re-memoizes the palette (P1-1). Does not touch `@aragon-agent/core`.
800
814
  */
801
815
  setTheme(name) {
802
- this.settingsRevision += 1;
816
+ this.advanceSettingsRevision();
803
817
  this.config = { ...this.config, theme: name };
804
818
  }
805
819
  /**
@@ -856,6 +870,8 @@ export class AgentController {
856
870
  * never rejects, so callers must not treat resolution as success.
857
871
  */
858
872
  async prompt(text, options = {}) {
873
+ if (this.isCompactionBusy())
874
+ return this.compactionBusyOutcome();
859
875
  // Capture BEFORE stopping: synchronous abort listeners may cancel this request.
860
876
  if (this.modelSettingsBlocked) {
861
877
  return { status: 'not-started', reason: 'failed' };
@@ -873,6 +889,8 @@ export class AgentController {
873
889
  }
874
890
  if (this.agent.state.isRunning)
875
891
  throw new Error('The previous run is still stopping.');
892
+ if (this.isCompactionBusy())
893
+ return this.compactionBusyOutcome();
876
894
  return await this.startPrompt(text, options, request);
877
895
  }
878
896
  catch (err) {
@@ -908,10 +926,7 @@ export class AgentController {
908
926
  }
909
927
  /** Prepare before clearing the plan, then enter the engine without yielding. */
910
928
  async startPrompt(text, options, request) {
911
- if (this.skillsEnabled)
912
- this.skills.beginUserTurn();
913
- this.askRounds = 0;
914
- this.fast?.setGoal(text);
929
+ this.prepareUserTurn(text);
915
930
  if (request !== this.startupSequence) {
916
931
  return { status: 'not-started', reason: 'cancelled' };
917
932
  }
@@ -919,9 +934,16 @@ export class AgentController {
919
934
  await this.agent.prompt(text);
920
935
  return { status: 'finished' };
921
936
  }
937
+ prepareUserTurn(text) {
938
+ if (this.skillsEnabled)
939
+ this.skills.beginUserTurn();
940
+ this.askRounds = 0;
941
+ this.fast?.setGoal(text);
942
+ }
922
943
  /** Cancel pending startup as well as any engine run already in progress. */
923
944
  abort() {
924
945
  this.startupSequence += 1;
946
+ this.idleCompaction?.abort();
925
947
  this.stopEngine();
926
948
  }
927
949
  stopEngine() {
@@ -1066,11 +1088,11 @@ export class AgentController {
1066
1088
  }
1067
1089
  enqueueSteering(text, user) {
1068
1090
  // Both reviewer and user guidance can activate queued skill frames.
1069
- if (this.skillsEnabled)
1091
+ if (this.skillsEnabled && this.isRunning())
1070
1092
  this.skills.absorbPendingFrames();
1071
1093
  const id = user ? `${this.steeringPrefix}:${++this.steeringSequence}` : undefined;
1072
1094
  if (id !== undefined)
1073
- this.pendingUserSteering.add(id);
1095
+ this.pendingUserSteering.set(id, text);
1074
1096
  try {
1075
1097
  this.agent.steer(text, id);
1076
1098
  }
@@ -1084,6 +1106,30 @@ export class AgentController {
1084
1106
  followUp(text) {
1085
1107
  this.agent.followUp(text);
1086
1108
  }
1109
+ /** Resume queued input without appending a duplicate user message. */
1110
+ async continue() {
1111
+ if (this.isCompactionBusy())
1112
+ return this.compactionBusyOutcome();
1113
+ if (this.modelSettingsBlocked || this.isRunning()) {
1114
+ return { status: 'not-started', reason: 'failed' };
1115
+ }
1116
+ if (this.pendingUserSteering.size > 0) {
1117
+ const request = ++this.startupSequence;
1118
+ this.prepareUserTurn([...this.pendingUserSteering.values()].join('\n\n'));
1119
+ if (request !== this.startupSequence) {
1120
+ return { status: 'not-started', reason: 'cancelled' };
1121
+ }
1122
+ this.todos?.beginUserTurn('new-task');
1123
+ }
1124
+ await this.agent.continue();
1125
+ return { status: 'finished' };
1126
+ }
1127
+ /** Pending user receipts survive until Core accepts or the host clears them. */
1128
+ hasPendingUserMessages() { return this.pendingUserSteering.size > 0; }
1129
+ compactionBusyOutcome() {
1130
+ this.notifyHostFn?.('warn', 'Context compaction is busy; your input was not submitted.');
1131
+ return { status: 'not-started', reason: 'failed' };
1132
+ }
1087
1133
  isRunning() {
1088
1134
  return this.agent.state.isRunning;
1089
1135
  }
@@ -1216,7 +1262,7 @@ export class AgentController {
1216
1262
  * honestly by checking `isTeamRegistered()` first.
1217
1263
  */
1218
1264
  setTeamEnabled(enabled) {
1219
- this.settingsRevision += 1;
1265
+ this.advanceSettingsRevision();
1220
1266
  this.teamEnabled = enabled;
1221
1267
  this.rebuildSystemPrompt();
1222
1268
  }
@@ -1230,7 +1276,7 @@ export class AgentController {
1230
1276
  */
1231
1277
  setTeamConfig(patch) {
1232
1278
  const team = clampTeamConfig({ ...this.config.team, ...patch });
1233
- this.settingsRevision += 1;
1279
+ this.advanceSettingsRevision();
1234
1280
  this.config = { ...this.config, team };
1235
1281
  this.rebuildSystemPrompt();
1236
1282
  return team;
@@ -1256,6 +1302,7 @@ export class AgentController {
1256
1302
  }
1257
1303
  /** Abort every child and clear timers. Idempotent; safe to call twice. */
1258
1304
  dispose() {
1305
+ this.idleCompaction?.abort();
1259
1306
  this.disposed = true;
1260
1307
  this.modelMetadataGeneration += 1;
1261
1308
  this.modelMetadataAbort.abort();
@@ -1295,7 +1342,7 @@ export class AgentController {
1295
1342
  * reports that honestly by checking `isTodoRegistered()` first.
1296
1343
  */
1297
1344
  setTodoEnabled(enabled) {
1298
- this.settingsRevision += 1;
1345
+ this.advanceSettingsRevision();
1299
1346
  this.todoEnabled = enabled;
1300
1347
  this.rebuildSystemPrompt();
1301
1348
  }
@@ -1318,7 +1365,7 @@ export class AgentController {
1318
1365
  */
1319
1366
  setTodoConfig(patch) {
1320
1367
  const todo = clampTodoConfig({ ...this.config.todo, ...patch });
1321
- this.settingsRevision += 1;
1368
+ this.advanceSettingsRevision();
1322
1369
  this.config = { ...this.config, todo };
1323
1370
  this.rebuildSystemPrompt();
1324
1371
  return todo;
@@ -1374,7 +1421,7 @@ export class AgentController {
1374
1421
  */
1375
1422
  setRetryConfig(patch) {
1376
1423
  const retry = clampRetryConfig({ ...this.config.retry, ...patch });
1377
- this.settingsRevision += 1;
1424
+ this.advanceSettingsRevision();
1378
1425
  this.config = { ...this.config, retry };
1379
1426
  this.providerRegistry.setRetryPolicy(toRetryPolicy(retry));
1380
1427
  return retry;
@@ -1384,11 +1431,16 @@ export class AgentController {
1384
1431
  // -----------------------------------------------------------------------
1385
1432
  /** True while any request can still use a model or credential snapshot. */
1386
1433
  isModelSettingsBusy() {
1387
- return this.isRunning() || this.isTeamBusy() || this.getCompactionSnapshot().inFlight
1434
+ return this.isRunning() || this.isTeamBusy() || this.isCompactionBusy()
1435
+ || this.getCompactionSnapshot().inFlight
1388
1436
  || this.getFastStatus().snapshot.inFlight;
1389
1437
  }
1390
1438
  /** Monotonic revision protects an editor from all intervening settings changes. */
1391
1439
  getSettingsRevision() { return this.settingsRevision; }
1440
+ advanceSettingsRevision() {
1441
+ this.settingsRevision += 1;
1442
+ this.idleCompaction?.abort();
1443
+ }
1392
1444
  /** Read-only draft creation; no startup migration or credential copying. */
1393
1445
  createModelSettingsDraft() {
1394
1446
  return createModelSettingsDraft(this.config, this.settingsRevision);
@@ -1414,10 +1466,13 @@ export class AgentController {
1414
1466
  }
1415
1467
  /** Apply prepared state synchronously, without rebuilding the controller or its history. */
1416
1468
  applyModelSettingsSnapshot(snapshot) {
1469
+ const connectionChanged = this.config.provider !== snapshot.config.provider
1470
+ || this.config.model !== snapshot.config.model || this.config.baseUrl !== snapshot.config.baseUrl;
1471
+ this.idleCompaction?.abort();
1417
1472
  this.preparedSystemPrompt = snapshot.systemPrompt;
1418
1473
  try {
1419
1474
  this.config = snapshot.config;
1420
- this.settingsRevision += 1;
1475
+ this.advanceSettingsRevision();
1421
1476
  this.agent.setModel(snapshot.model);
1422
1477
  this.agent.setThinkingLevel(snapshot.config.thinkingLevel);
1423
1478
  this.agent.setMaxTokens(snapshot.config.maxTokens);
@@ -1429,7 +1484,12 @@ export class AgentController {
1429
1484
  }
1430
1485
  this.fast?.onConfigChanged(false, snapshot.config.fast.enabled);
1431
1486
  this.compaction?.onConfigChanged(snapshot.config.compaction.enabled);
1432
- this.contextMeter.onWindowChanged();
1487
+ if (connectionChanged) {
1488
+ this.requestVersion += 1;
1489
+ this.contextMeter.onHistoryReplaced();
1490
+ }
1491
+ else
1492
+ this.contextMeter.onWindowChanged();
1433
1493
  this.rebuildSystemPrompt();
1434
1494
  }
1435
1495
  finally {
@@ -1451,9 +1511,11 @@ export class AgentController {
1451
1511
  setModel(provider, model, baseUrl) {
1452
1512
  if (this.isModelSettingsBusy())
1453
1513
  throw new Error('Model settings are busy.');
1514
+ const connectionChanged = this.config.provider !== provider || this.config.model !== model
1515
+ || this.config.baseUrl !== baseUrl;
1454
1516
  if (this.modelMetadataEnabled)
1455
1517
  this.resetModelMetadataRequest();
1456
- this.settingsRevision += 1;
1518
+ this.advanceSettingsRevision();
1457
1519
  this.config = { ...this.config, provider, model, baseUrl };
1458
1520
  this.config.modelProfileState = resolveModelProfileState(this.config);
1459
1521
  this.agent.setModel({ providerId: provider, modelId: model, ...(baseUrl ? { baseUrl } : {}) });
@@ -1468,30 +1530,29 @@ export class AgentController {
1468
1530
  // so the snapshot the chip and `/compact status` read has to be refreshed
1469
1531
  // for the same reason and at the same moments (the RV-3 lesson).
1470
1532
  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();
1533
+ if (connectionChanged) {
1534
+ this.requestVersion += 1;
1535
+ this.contextMeter.onHistoryReplaced();
1536
+ }
1537
+ else
1538
+ this.contextMeter.onWindowChanged();
1478
1539
  if (this.modelMetadataEnabled)
1479
1540
  void this.refreshModelMetadata();
1480
1541
  }
1481
1542
  setThinkingLevel(level) {
1482
- this.settingsRevision += 1;
1543
+ this.advanceSettingsRevision();
1483
1544
  this.config = { ...this.config, thinkingLevel: level };
1484
1545
  this.agent.setThinkingLevel(level);
1485
1546
  }
1486
1547
  setMaxTokens(value) {
1487
- this.settingsRevision += 1;
1548
+ this.advanceSettingsRevision();
1488
1549
  this.config = { ...this.config, maxTokens: value };
1489
1550
  this.agent.setMaxTokens(value);
1490
1551
  }
1491
1552
  setApiKey(provider, key) {
1492
1553
  // The Agent resolves keys via `resolveKey(this.config)` at call time, so
1493
1554
  // updating the config here is sufficient — no Agent rebuild needed.
1494
- this.settingsRevision += 1;
1555
+ this.advanceSettingsRevision();
1495
1556
  this.config = {
1496
1557
  ...this.config,
1497
1558
  apiKeys: { ...this.config.apiKeys, [provider]: key },
@@ -1512,7 +1573,7 @@ export class AgentController {
1512
1573
  }
1513
1574
  setCwd(cwd) {
1514
1575
  this.cwd = cwd;
1515
- this.settingsRevision += 1;
1576
+ this.advanceSettingsRevision();
1516
1577
  this.config = { ...this.config, cwd };
1517
1578
  // The project scope moved with the cwd, so rescan BEFORE rebuilding — then
1518
1579
  // let the single prompt entry point emit a prompt that reflects both the new
@@ -1581,6 +1642,8 @@ export class AgentController {
1581
1642
  };
1582
1643
  }
1583
1644
  clearMessages() {
1645
+ this.idleCompaction?.abort();
1646
+ this.restoredCompactionIdentity = undefined;
1584
1647
  this.agent.clearMessages();
1585
1648
  this.startupSequence += 1;
1586
1649
  // INVALIDATION SITE 3 (context-auto-compaction-hardening §3.2.3). The history
@@ -1608,7 +1671,9 @@ export class AgentController {
1608
1671
  getSystemPrompt() {
1609
1672
  return this.agent.state.systemPrompt;
1610
1673
  }
1611
- replaceMessages(messages) {
1674
+ replaceMessages(messages, identity) {
1675
+ this.idleCompaction?.abort();
1676
+ this.restoredCompactionIdentity = identity;
1612
1677
  this.agent.replaceMessages(messages);
1613
1678
  this.startupSequence += 1;
1614
1679
  // INVALIDATION SITE 4 (§3.2.3) - `/resume`, and any future host-side rewrite.
@@ -1621,12 +1686,26 @@ export class AgentController {
1621
1686
  // reading stood until the next completed turn. The DEEP reset also drops
1622
1687
  // `estimateOffset`, because `/resume` can change the model in the same
1623
1688
  // breath (I-8).
1624
- this.compaction?.onHistoryReplaced();
1689
+ this.compaction?.onHistoryReplaced(identity);
1625
1690
  this.contextMeter.onHistoryReplaced();
1626
1691
  }
1627
1692
  getMessages() {
1628
1693
  return this.agent.state.messages;
1629
1694
  }
1695
+ /** Atomically capture adopted history and its host credential for serialization. */
1696
+ getSessionSnapshot() {
1697
+ const messages = this.getMessages();
1698
+ const identity = this.compaction ? this.compaction.getIdentity() : this.restoredCompactionIdentity;
1699
+ if (identity && !verifyCompactionIdentity(messages, identity)) {
1700
+ getLogger().debug('agent', 'invalid_compaction_session_identity');
1701
+ }
1702
+ // Retain invalid evidence so damaged host memory cannot become ordinary text.
1703
+ return { messages, ...(identity ? { compactionIdentity: { ...identity } } : {}) };
1704
+ }
1705
+ /** Compatibility name for the documented compaction session boundary. */
1706
+ getCompactionSessionState() {
1707
+ return this.getSessionSnapshot();
1708
+ }
1630
1709
  clearAllQueues() {
1631
1710
  this.agent.clearAllQueues();
1632
1711
  this.pendingUserSteering.clear();
@@ -1662,9 +1741,18 @@ export class AgentController {
1662
1741
  const found = this.modelRegistry.getModel(ref.providerId, ref.modelId);
1663
1742
  const baseUrl = ref.baseUrl ?? (ref.providerId === this.config.provider && ref.modelId === this.config.model
1664
1743
  ? this.config.baseUrl : undefined);
1744
+ const userWindow = this.modelWindows.lookup(ref.modelId);
1665
1745
  return {
1666
1746
  ...(found ?? this.modelRegistry.buildRuntimeModel(ref.providerId, ref.modelId)),
1667
1747
  ...this.modelRegistry.getContextWindow(ref.providerId, ref.modelId, baseUrl),
1748
+ // A USER-DECLARED WINDOW OUTRANKS EVERY TABLE (model-windows.json): the
1749
+ // user asserted the number for exactly this id, while api/catalog are
1750
+ // what this process managed to look up about it. It still LOSES to
1751
+ // config.json's `contextWindow`, which `ContextMeter.resolveWindow`
1752
+ // consults before it ever reaches here.
1753
+ ...(userWindow !== undefined
1754
+ ? { contextWindow: userWindow, contextWindowSource: 'user' }
1755
+ : {}),
1668
1756
  };
1669
1757
  }
1670
1758
  /** Best-effort metadata lookup; never block typing or a model request. */
@@ -1726,7 +1814,7 @@ export class AgentController {
1726
1814
  * `/fast` reports that honestly by checking `isFastRegistered()` first.
1727
1815
  */
1728
1816
  setFastEnabled(enabled) {
1729
- this.settingsRevision += 1;
1817
+ this.advanceSettingsRevision();
1730
1818
  this.fast?.setEnabled(enabled);
1731
1819
  }
1732
1820
  getFastConfig() {
@@ -1742,7 +1830,7 @@ export class AgentController {
1742
1830
  */
1743
1831
  setFastConfig(patch) {
1744
1832
  const fast = clampFastConfig({ ...this.config.fast, ...patch });
1745
- this.settingsRevision += 1;
1833
+ this.advanceSettingsRevision();
1746
1834
  this.config = { ...this.config, fast };
1747
1835
  // `onConfigChanged` rebuilds the prompt; when there is no wiring (the tier
1748
1836
  // was never registered) nothing has to be rebuilt, because no block is
@@ -1842,7 +1930,7 @@ export class AgentController {
1842
1930
  * on purpose (§3.2).
1843
1931
  */
1844
1932
  setCompactionEnabled(enabled) {
1845
- this.settingsRevision += 1;
1933
+ this.advanceSettingsRevision();
1846
1934
  this.compaction?.setEnabled(enabled);
1847
1935
  }
1848
1936
  getCompactionConfig() {
@@ -1857,7 +1945,7 @@ export class AgentController {
1857
1945
  */
1858
1946
  setCompactionConfig(patch) {
1859
1947
  const compaction = clampCompactionConfig({ ...this.config.compaction, ...patch });
1860
- this.settingsRevision += 1;
1948
+ this.advanceSettingsRevision();
1861
1949
  this.config = { ...this.config, compaction };
1862
1950
  this.compaction?.onConfigChanged();
1863
1951
  return compaction;
@@ -1910,18 +1998,80 @@ export class AgentController {
1910
1998
  return { ok: false, reason: 'settings_restart_required' };
1911
1999
  if (!this.compaction)
1912
2000
  return { ok: false, reason: 'not_registered' };
2001
+ if (this.isCompactionBusy() || this.isRunning())
2002
+ return { ok: false, reason: 'busy' };
1913
2003
  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 } : {}),
2004
+ const epoch = this.startupSequence;
2005
+ const settings = this.settingsRevision;
2006
+ let adopted = false;
2007
+ let timedOut = false;
2008
+ const isCurrent = () => this.idleCompaction === controller && !controller.signal.aborted
2009
+ && epoch === this.startupSequence && settings === this.settingsRevision
2010
+ && !this.isRunning() && !this.disposed;
2011
+ this.idleCompaction = controller;
2012
+ this.publishCompactionBusy(true);
2013
+ const deadline = setTimeout(() => {
2014
+ timedOut = true;
2015
+ controller.abort();
2016
+ }, COMPACTION_LIMITS.operationTimeoutMs);
2017
+ deadline.unref?.();
2018
+ try {
2019
+ // The wiring races cancellation locally and settles before releasing ownership.
2020
+ const outcome = await this.compaction.compactNow({
2021
+ messages: this.agent.state.messages,
2022
+ systemPrompt: this.agent.state.systemPrompt,
2023
+ model: this.agent.state.model,
2024
+ signal: controller.signal,
2025
+ ...(instructions ? { instructions } : {}),
2026
+ isCurrent,
2027
+ adopt: (messages) => {
2028
+ if (!isCurrent())
2029
+ throw new Error('stale_history');
2030
+ this.agent.replaceMessages(messages);
2031
+ adopted = true;
2032
+ },
2033
+ });
2034
+ return outcome.ok || adopted ? { ok: true }
2035
+ : { ok: false, reason: timedOut ? 'timeout' : outcome.reason };
2036
+ }
2037
+ finally {
2038
+ clearTimeout(deadline);
2039
+ if (this.idleCompaction === controller) {
2040
+ this.idleCompaction = undefined;
2041
+ this.publishCompactionBusy(false);
2042
+ }
2043
+ }
2044
+ }
2045
+ /** Local prepare/adopt ownership, available before any network-start event. */
2046
+ isCompactionBusy() { return this.idleCompaction !== undefined; }
2047
+ /** Cancel local compaction and await ownership release before a conversation switch. */
2048
+ cancelCompaction() {
2049
+ if (!this.idleCompaction)
2050
+ return Promise.resolve();
2051
+ return new Promise((resolve) => {
2052
+ const unsubscribe = this.subscribeCompactionBusy((busy) => {
2053
+ if (busy)
2054
+ return;
2055
+ unsubscribe();
2056
+ resolve();
2057
+ });
2058
+ this.abort();
1920
2059
  });
1921
- if (!outcome.ok)
1922
- return { ok: false, reason: outcome.reason };
1923
- this.agent.replaceMessages(outcome.messages);
1924
- return { ok: true };
2060
+ }
2061
+ /** Subscribe to synchronous ownership transitions; observers cannot break adoption. */
2062
+ subscribeCompactionBusy(listener) {
2063
+ this.compactionBusyListeners.add(listener);
2064
+ return () => { this.compactionBusyListeners.delete(listener); };
2065
+ }
2066
+ publishCompactionBusy(busy) {
2067
+ for (const listener of this.compactionBusyListeners) {
2068
+ try {
2069
+ listener(busy);
2070
+ }
2071
+ catch {
2072
+ getLogger().debug('agent', 'compaction_busy_listener_threw');
2073
+ }
2074
+ }
1925
2075
  }
1926
2076
  /**
1927
2077
  * 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;