@oh-my-pi/pi-coding-agent 15.7.1 → 15.7.3

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 (177) hide show
  1. package/CHANGELOG.md +95 -6
  2. package/dist/types/auto-thinking/classifier.d.ts +35 -0
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/cli/extension-flags.d.ts +36 -0
  5. package/dist/types/config/config-file.d.ts +4 -0
  6. package/dist/types/config/file-lock.d.ts +23 -0
  7. package/dist/types/config/keybindings.d.ts +2 -1
  8. package/dist/types/config/model-registry.d.ts +6 -0
  9. package/dist/types/config/settings-schema.d.ts +112 -69
  10. package/dist/types/edit/hashline/diff.d.ts +9 -3
  11. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +1 -0
  12. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +1 -0
  13. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +1 -0
  14. package/dist/types/eval/agent-bridge.d.ts +25 -0
  15. package/dist/types/eval/backend.d.ts +17 -2
  16. package/dist/types/eval/budget-bridge.d.ts +29 -0
  17. package/dist/types/eval/idle-timeout.d.ts +28 -0
  18. package/dist/types/eval/js/executor.d.ts +8 -0
  19. package/dist/types/eval/js/tool-bridge.d.ts +2 -1
  20. package/dist/types/eval/py/executor.d.ts +13 -0
  21. package/dist/types/exec/bash-executor.d.ts +1 -0
  22. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  23. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  24. package/dist/types/extensibility/plugins/git-url.d.ts +11 -1
  25. package/dist/types/extensibility/plugins/manager.d.ts +12 -1
  26. package/dist/types/extensibility/shared-events.d.ts +2 -2
  27. package/dist/types/memory-backend/index.d.ts +1 -1
  28. package/dist/types/memory-backend/resolve.d.ts +1 -1
  29. package/dist/types/memory-backend/types.d.ts +3 -3
  30. package/dist/types/mnemopi/backend.d.ts +4 -0
  31. package/dist/types/mnemopi/config.d.ts +29 -0
  32. package/dist/types/mnemopi/state.d.ts +72 -0
  33. package/dist/types/modes/components/custom-editor.d.ts +2 -2
  34. package/dist/types/modes/components/model-selector.d.ts +3 -2
  35. package/dist/types/modes/components/omfg-panel.d.ts +19 -0
  36. package/dist/types/modes/controllers/command-controller.d.ts +7 -0
  37. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  38. package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
  39. package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
  40. package/dist/types/modes/gradient-highlight.d.ts +5 -1
  41. package/dist/types/modes/interactive-mode.d.ts +7 -3
  42. package/dist/types/modes/magic-keywords.d.ts +14 -0
  43. package/dist/types/modes/markdown-prose.d.ts +27 -0
  44. package/dist/types/modes/orchestrate.d.ts +7 -2
  45. package/dist/types/modes/shared.d.ts +1 -1
  46. package/dist/types/modes/theme/theme.d.ts +2 -1
  47. package/dist/types/modes/turn-budget.d.ts +18 -0
  48. package/dist/types/modes/types.d.ts +7 -3
  49. package/dist/types/modes/ultrathink.d.ts +7 -2
  50. package/dist/types/modes/workflow.d.ts +15 -0
  51. package/dist/types/sdk.d.ts +15 -4
  52. package/dist/types/session/agent-session.d.ts +59 -23
  53. package/dist/types/session/session-manager.d.ts +18 -0
  54. package/dist/types/session/session-storage.d.ts +6 -0
  55. package/dist/types/session/shake-types.d.ts +24 -0
  56. package/dist/types/task/executor.d.ts +2 -2
  57. package/dist/types/thinking.d.ts +39 -1
  58. package/dist/types/tiny/device.d.ts +3 -3
  59. package/dist/types/tiny/models.d.ts +34 -1
  60. package/dist/types/tiny/title-protocol.d.ts +4 -0
  61. package/dist/types/tools/index.d.ts +19 -3
  62. package/dist/types/tools/memory-edit.d.ts +1 -1
  63. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  64. package/package.json +10 -10
  65. package/src/auto-thinking/classifier.ts +180 -0
  66. package/src/autoresearch/tools/run-experiment.ts +45 -113
  67. package/src/cli/args.ts +39 -16
  68. package/src/cli/extension-flags.ts +48 -0
  69. package/src/cli/plugin-cli.ts +11 -2
  70. package/src/config/config-file.ts +98 -13
  71. package/src/config/file-lock.ts +60 -17
  72. package/src/config/keybindings.ts +78 -27
  73. package/src/config/model-registry.ts +7 -1
  74. package/src/config/settings-schema.ts +118 -71
  75. package/src/config/settings.ts +12 -0
  76. package/src/edit/hashline/diff.ts +87 -22
  77. package/src/edit/renderer.ts +16 -12
  78. package/src/edit/streaming.ts +17 -6
  79. package/src/eval/__tests__/agent-bridge.test.ts +433 -0
  80. package/src/eval/__tests__/budget-bridge.test.ts +69 -0
  81. package/src/eval/__tests__/idle-timeout.test.ts +66 -0
  82. package/src/eval/__tests__/shared-executors.test.ts +53 -0
  83. package/src/eval/agent-bridge.ts +295 -0
  84. package/src/eval/backend.ts +17 -2
  85. package/src/eval/budget-bridge.ts +48 -0
  86. package/src/eval/idle-timeout.ts +80 -0
  87. package/src/eval/js/executor.ts +35 -7
  88. package/src/eval/js/index.ts +2 -1
  89. package/src/eval/js/shared/local-module-loader.ts +75 -10
  90. package/src/eval/js/shared/prelude.txt +85 -1
  91. package/src/eval/js/tool-bridge.ts +9 -0
  92. package/src/eval/py/executor.ts +41 -14
  93. package/src/eval/py/index.ts +2 -1
  94. package/src/eval/py/prelude.py +132 -1
  95. package/src/exec/bash-executor.ts +2 -3
  96. package/src/extensibility/custom-tools/types.ts +2 -2
  97. package/src/extensibility/extensions/runner.ts +12 -2
  98. package/src/extensibility/plugins/git-url.ts +90 -4
  99. package/src/extensibility/plugins/manager.ts +103 -7
  100. package/src/extensibility/shared-events.ts +2 -2
  101. package/src/internal-urls/docs-index.generated.ts +88 -88
  102. package/src/main.ts +50 -56
  103. package/src/memory-backend/index.ts +1 -1
  104. package/src/memory-backend/resolve.ts +3 -3
  105. package/src/memory-backend/types.ts +3 -3
  106. package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
  107. package/src/{mnemosyne → mnemopi}/config.ts +36 -36
  108. package/src/{mnemosyne → mnemopi}/state.ts +67 -67
  109. package/src/modes/acp/acp-agent.ts +13 -3
  110. package/src/modes/components/agent-dashboard.ts +6 -6
  111. package/src/modes/components/custom-editor.ts +4 -11
  112. package/src/modes/components/extensions/state-manager.ts +3 -4
  113. package/src/modes/components/footer.ts +18 -12
  114. package/src/modes/components/hook-selector.ts +86 -20
  115. package/src/modes/components/model-selector.ts +20 -11
  116. package/src/modes/components/oauth-selector.ts +93 -21
  117. package/src/modes/components/omfg-panel.ts +141 -0
  118. package/src/modes/components/settings-defs.ts +9 -2
  119. package/src/modes/components/settings-selector.ts +4 -1
  120. package/src/modes/components/status-line/segments.ts +13 -5
  121. package/src/modes/components/tips.txt +2 -1
  122. package/src/modes/components/tool-execution.ts +38 -19
  123. package/src/modes/components/tree-selector.ts +4 -3
  124. package/src/modes/components/user-message-selector.ts +94 -19
  125. package/src/modes/components/user-message.ts +8 -1
  126. package/src/modes/controllers/command-controller.ts +57 -0
  127. package/src/modes/controllers/event-controller.ts +65 -3
  128. package/src/modes/controllers/input-controller.ts +14 -11
  129. package/src/modes/controllers/omfg-controller.ts +283 -0
  130. package/src/modes/controllers/omfg-rule.ts +647 -0
  131. package/src/modes/controllers/selector-controller.ts +21 -6
  132. package/src/modes/gradient-highlight.ts +23 -6
  133. package/src/modes/interactive-mode.ts +41 -7
  134. package/src/modes/magic-keywords.ts +20 -0
  135. package/src/modes/markdown-prose.ts +247 -0
  136. package/src/modes/orchestrate.ts +17 -11
  137. package/src/modes/shared.ts +3 -11
  138. package/src/modes/theme/theme.ts +6 -0
  139. package/src/modes/turn-budget.ts +31 -0
  140. package/src/modes/types.ts +7 -1
  141. package/src/modes/ultrathink.ts +16 -10
  142. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  143. package/src/modes/workflow.ts +42 -0
  144. package/src/prompts/system/auto-thinking-difficulty-local.md +14 -0
  145. package/src/prompts/system/auto-thinking-difficulty.md +12 -0
  146. package/src/prompts/system/omfg-user.md +51 -0
  147. package/src/prompts/system/system-prompt.md +1 -0
  148. package/src/prompts/system/workflow-notice.md +70 -0
  149. package/src/prompts/tools/eval.md +13 -1
  150. package/src/prompts/tools/memory-edit.md +1 -1
  151. package/src/sdk.ts +86 -38
  152. package/src/session/agent-session.ts +558 -80
  153. package/src/session/session-manager.ts +32 -0
  154. package/src/session/session-storage.ts +68 -8
  155. package/src/session/shake-types.ts +44 -0
  156. package/src/slash-commands/builtin-registry.ts +41 -16
  157. package/src/task/executor.ts +3 -3
  158. package/src/task/index.ts +6 -6
  159. package/src/thinking.ts +73 -1
  160. package/src/tiny/device.ts +4 -10
  161. package/src/tiny/models.ts +54 -2
  162. package/src/tiny/title-protocol.ts +11 -1
  163. package/src/tiny/worker.ts +19 -7
  164. package/src/tools/eval.ts +202 -26
  165. package/src/tools/grouped-file-output.ts +9 -2
  166. package/src/tools/index.ts +17 -5
  167. package/src/tools/memory-edit.ts +4 -4
  168. package/src/tools/memory-recall.ts +5 -5
  169. package/src/tools/memory-reflect.ts +5 -5
  170. package/src/tools/memory-retain.ts +4 -4
  171. package/src/tools/render-utils.ts +2 -1
  172. package/src/tools/search.ts +480 -76
  173. package/dist/types/mnemosyne/backend.d.ts +0 -4
  174. package/dist/types/mnemosyne/config.d.ts +0 -29
  175. package/dist/types/mnemosyne/state.d.ts +0 -72
  176. /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
  177. /package/src/{mnemosyne → mnemopi}/index.ts +0 -0
@@ -33,26 +33,34 @@ import {
33
33
  ThinkingLevel,
34
34
  } from "@oh-my-pi/pi-agent-core";
35
35
  import {
36
+ AGGRESSIVE_SHAKE_CONFIG,
36
37
  AUTO_HANDOFF_THRESHOLD_FOCUS,
38
+ applyShakeRegions,
37
39
  CompactionCancelledError,
38
40
  type CompactionPreparation,
39
41
  type CompactionResult,
40
42
  calculateContextTokens,
41
43
  calculatePromptTokens,
42
44
  collectEntriesForBranchSummary,
45
+ collectShakeRegions,
43
46
  compact,
47
+ DEFAULT_SHAKE_CONFIG,
44
48
  estimateTokens,
45
49
  generateBranchSummary,
46
50
  generateHandoff,
47
51
  prepareCompaction,
52
+ type ShakeConfig,
53
+ type ShakeRegion,
54
+ type ShakeSummaryComplete,
55
+ type ShakeSummaryItem,
48
56
  type SummaryOptions,
49
57
  shouldCompact,
58
+ summarizeShakeRegions,
50
59
  } from "@oh-my-pi/pi-agent-core/compaction";
51
60
  import { DEFAULT_PRUNE_CONFIG, pruneToolOutputs } from "@oh-my-pi/pi-agent-core/compaction/pruning";
52
61
  import type {
53
62
  AssistantMessage,
54
63
  Context,
55
- Effort,
56
64
  ImageContent,
57
65
  Message,
58
66
  MessageAttribution,
@@ -69,6 +77,7 @@ import type {
69
77
  import {
70
78
  calculateRateLimitBackoffMs,
71
79
  clearAnthropicFastModeFallback,
80
+ Effort,
72
81
  getSupportedEfforts,
73
82
  isContextOverflow,
74
83
  isUsageLimitError,
@@ -77,7 +86,7 @@ import {
77
86
  resolveServiceTier,
78
87
  streamSimple,
79
88
  } from "@oh-my-pi/pi-ai";
80
- import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
89
+ import { countTokens, MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
81
90
  import {
82
91
  extractRetryHint,
83
92
  getAgentDbPath,
@@ -88,6 +97,7 @@ import {
88
97
  Snowflake,
89
98
  } from "@oh-my-pi/pi-utils";
90
99
  import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
100
+ import { classifyDifficulty } from "../auto-thinking/classifier";
91
101
  import { reset as resetCapabilities } from "../capability";
92
102
  import type { Rule } from "../capability/rule";
93
103
  import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
@@ -147,10 +157,12 @@ import type { Goal, GoalModeState } from "../goals/state";
147
157
  import type { HindsightSessionState } from "../hindsight/state";
148
158
  import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
149
159
  import { resolveMemoryBackend } from "../memory-backend";
150
- import { getMnemosyneSessionState, type MnemosyneSessionState, setMnemosyneSessionState } from "../mnemosyne/state";
160
+ import { getMnemopiSessionState, type MnemopiSessionState, setMnemopiSessionState } from "../mnemopi/state";
151
161
  import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
152
162
  import { getCurrentThemeName, theme } from "../modes/theme/theme";
163
+ import { parseTurnBudget } from "../modes/turn-budget";
153
164
  import { containsUltrathink, ULTRATHINK_NOTICE } from "../modes/ultrathink";
165
+ import { containsWorkflow, WORKFLOW_NOTICE } from "../modes/workflow";
154
166
  import type { PlanModeState } from "../plan-mode/state";
155
167
  import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
156
168
  import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
@@ -165,8 +177,16 @@ import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" w
165
177
  import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
166
178
  import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
167
179
  import { invalidateHostMetadata } from "../ssh/connection-manager";
168
- import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
169
- import { shutdownTinyTitleClient } from "../tiny/title-client";
180
+ import {
181
+ AUTO_THINKING,
182
+ type ConfiguredThinkingLevel,
183
+ clampAutoThinkingEffort,
184
+ resolveProvisionalAutoLevel,
185
+ resolveThinkingLevelForModel,
186
+ toReasoningEffort,
187
+ } from "../thinking";
188
+ import { isTinyMemoryLocalModelKey } from "../tiny/models";
189
+ import { shutdownTinyTitleClient, tinyModelClient } from "../tiny/title-client";
170
190
  import {
171
191
  buildDiscoverableToolSearchIndex,
172
192
  collectDiscoverableTools,
@@ -211,6 +231,7 @@ import type {
211
231
  SessionManager,
212
232
  } from "./session-manager";
213
233
  import { getLatestCompactionEntry } from "./session-manager";
234
+ import type { ShakeMode, ShakeResult } from "./shake-types";
214
235
  import { ToolChoiceQueue } from "./tool-choice-queue";
215
236
  import { YieldQueue } from "./yield-queue";
216
237
 
@@ -220,11 +241,11 @@ export type AgentSessionEvent =
220
241
  | {
221
242
  type: "auto_compaction_start";
222
243
  reason: "threshold" | "overflow" | "idle" | "incomplete";
223
- action: "context-full" | "handoff";
244
+ action: "context-full" | "handoff" | "shake" | "shake-summary";
224
245
  }
225
246
  | {
226
247
  type: "auto_compaction_end";
227
- action: "context-full" | "handoff";
248
+ action: "context-full" | "handoff" | "shake" | "shake-summary";
228
249
  result: CompactionResult | undefined;
229
250
  aborted: boolean;
230
251
  willRetry: boolean;
@@ -241,7 +262,14 @@ export type AgentSessionEvent =
241
262
  | { type: "todo_auto_clear" }
242
263
  | { type: "irc_message"; message: CustomMessage }
243
264
  | { type: "notice"; level: "info" | "warning" | "error"; message: string; source?: string }
244
- | { type: "thinking_level_changed"; thinkingLevel: ThinkingLevel | undefined }
265
+ | {
266
+ type: "thinking_level_changed";
267
+ thinkingLevel: ThinkingLevel | undefined;
268
+ /** The user-configured selector when it differs from the effective level (e.g. `auto`). */
269
+ configured?: ConfiguredThinkingLevel;
270
+ /** The level `auto` resolved to this turn, once classified. */
271
+ resolved?: Effort;
272
+ }
245
273
  | { type: "goal_updated"; goal: Goal | null; state?: GoalModeState };
246
274
 
247
275
  /** Listener function for agent session events */
@@ -254,6 +282,8 @@ export interface AsyncJobSnapshot {
254
282
  delivery: AsyncJobDeliveryState;
255
283
  }
256
284
 
285
+ export type { ShakeMode, ShakeResult };
286
+
257
287
  // ============================================================================
258
288
  // Types
259
289
  // ============================================================================
@@ -265,7 +295,7 @@ export interface AgentSessionConfig {
265
295
  /** Models to cycle through with Ctrl+P (from --models flag) */
266
296
  scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
267
297
  /** Initial session thinking selector. */
268
- thinkingLevel?: ThinkingLevel;
298
+ thinkingLevel?: ConfiguredThinkingLevel;
269
299
  /** Prompt templates for expansion */
270
300
  promptTemplates?: PromptTemplate[];
271
301
  /** File-based slash commands for expansion */
@@ -445,8 +475,8 @@ interface RetryFallbackSelector {
445
475
  interface ActiveRetryFallbackState {
446
476
  role: string;
447
477
  originalSelector: string;
448
- originalThinkingLevel: ThinkingLevel | undefined;
449
- lastAppliedFallbackThinkingLevel: ThinkingLevel | undefined;
478
+ originalThinkingLevel: ConfiguredThinkingLevel | undefined;
479
+ lastAppliedFallbackThinkingLevel: ConfiguredThinkingLevel | undefined;
450
480
  }
451
481
 
452
482
  function parseRetryFallbackSelector(selector: string): RetryFallbackSelector | undefined {
@@ -782,7 +812,12 @@ export class AgentSession {
782
812
  readonly configWarnings: string[] = [];
783
813
 
784
814
  #scopedModels: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
815
+ /** Effective, metadata-clamped thinking level applied to the agent (never `auto`). */
785
816
  #thinkingLevel: ThinkingLevel | undefined;
817
+ /** True when the user configured `auto`; the effective level is resolved per turn. */
818
+ #autoThinking: boolean = false;
819
+ /** The level `auto` last resolved to (for UI); undefined until a turn is classified. */
820
+ #autoResolvedLevel: Effort | undefined;
786
821
  #promptTemplates: PromptTemplate[];
787
822
  #slashCommands: FileSlashCommand[];
788
823
 
@@ -1041,7 +1076,15 @@ export class AgentSession {
1041
1076
  this.#parentEvalSessionId = config.parentEvalSessionId;
1042
1077
  this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
1043
1078
  this.#scopedModels = config.scopedModels ?? [];
1044
- this.#thinkingLevel = config.thinkingLevel;
1079
+ if (config.thinkingLevel === AUTO_THINKING) {
1080
+ // `auto` is session-level: keep the flag and show a provisional concrete
1081
+ // level (the agent's initial effort was already set by the caller) until
1082
+ // the first user turn is classified.
1083
+ this.#autoThinking = true;
1084
+ this.#thinkingLevel = resolveProvisionalAutoLevel(this.model);
1085
+ } else {
1086
+ this.#thinkingLevel = config.thinkingLevel;
1087
+ }
1045
1088
  this.#promptTemplates = config.promptTemplates ?? [];
1046
1089
  this.#slashCommands = config.slashCommands ?? [];
1047
1090
  this.#extensionRunner = config.extensionRunner;
@@ -1259,8 +1302,8 @@ export class AgentSession {
1259
1302
  return previous;
1260
1303
  }
1261
1304
 
1262
- getMnemosyneSessionState(): MnemosyneSessionState | undefined {
1263
- return getMnemosyneSessionState(this);
1305
+ getMnemopiSessionState(): MnemopiSessionState | undefined {
1306
+ return getMnemopiSessionState(this);
1264
1307
  }
1265
1308
 
1266
1309
  /** TTSR manager for time-traveling stream rules */
@@ -2810,11 +2853,11 @@ export class AgentSession {
2810
2853
  this.getHindsightSessionState()?.setSessionId(sid);
2811
2854
  }
2812
2855
 
2813
- #rekeyMnemosyneMemoryForCurrentSessionId(): void {
2814
- if (resolveMemoryBackend(this.settings).id !== "mnemosyne") return;
2856
+ #rekeyMnemopiMemoryForCurrentSessionId(): void {
2857
+ if (resolveMemoryBackend(this.settings).id !== "mnemopi") return;
2815
2858
  const sid = this.agent.sessionId;
2816
2859
  if (!sid) return;
2817
- this.getMnemosyneSessionState()?.setSessionId(sid);
2860
+ this.getMnemopiSessionState()?.setSessionId(sid);
2818
2861
  }
2819
2862
 
2820
2863
  /** New session file: reset auto-recall / retain-threshold counters for the new transcript. */
@@ -2825,9 +2868,9 @@ export class AgentSession {
2825
2868
  state.resetConversationTracking();
2826
2869
  }
2827
2870
 
2828
- #resetMnemosyneConversationTrackingIfMnemosyne(): void {
2829
- if (resolveMemoryBackend(this.settings).id !== "mnemosyne") return;
2830
- const state = this.getMnemosyneSessionState();
2871
+ #resetMnemopiConversationTrackingIfMnemopi(): void {
2872
+ if (resolveMemoryBackend(this.settings).id !== "mnemopi") return;
2873
+ const state = this.getMnemopiSessionState();
2831
2874
  if (!state || state.aliasOf) return;
2832
2875
  state.resetConversationTracking();
2833
2876
  }
@@ -2895,8 +2938,8 @@ export class AgentSession {
2895
2938
  const hindsightState = this.setHindsightSessionState(undefined);
2896
2939
  await hindsightState?.flushRetainQueue();
2897
2940
  hindsightState?.dispose();
2898
- const mnemosyneState = setMnemosyneSessionState(this, undefined);
2899
- mnemosyneState?.dispose();
2941
+ const mnemopiState = setMnemopiSessionState(this, undefined);
2942
+ mnemopiState?.dispose();
2900
2943
  this.#disconnectFromAgent();
2901
2944
  if (this.#unsubscribeAppendOnly) {
2902
2945
  this.#unsubscribeAppendOnly();
@@ -2935,11 +2978,26 @@ export class AgentSession {
2935
2978
  return this.agent.state.model;
2936
2979
  }
2937
2980
 
2938
- /** Current thinking level */
2981
+ /** Effective thinking level applied to the agent (the resolved level when `auto`). */
2939
2982
  get thinkingLevel(): ThinkingLevel | undefined {
2940
2983
  return this.#thinkingLevel;
2941
2984
  }
2942
2985
 
2986
+ /** The selector the user configured: `auto` when auto mode is active, else the effective level. */
2987
+ configuredThinkingLevel(): ConfiguredThinkingLevel | undefined {
2988
+ return this.#autoThinking ? AUTO_THINKING : this.#thinkingLevel;
2989
+ }
2990
+
2991
+ /** True when `auto` thinking mode is active. */
2992
+ get isAutoThinking(): boolean {
2993
+ return this.#autoThinking;
2994
+ }
2995
+
2996
+ /** The level `auto` resolved to for the current turn (undefined until classified). */
2997
+ autoResolvedThinkingLevel(): Effort | undefined {
2998
+ return this.#autoResolvedLevel;
2999
+ }
3000
+
2943
3001
  get serviceTier(): ServiceTier | undefined {
2944
3002
  return this.agent.serviceTier;
2945
3003
  }
@@ -4058,6 +4116,8 @@ export class AgentSession {
4058
4116
  const keywordNotices: CustomMessage[] = [];
4059
4117
  if (!options?.synthetic) {
4060
4118
  const timestamp = Date.now();
4119
+ const turnBudget = parseTurnBudget(expandedText);
4120
+ this.sessionManager.beginTurnBudget(turnBudget?.total ?? null, turnBudget?.hard ?? false);
4061
4121
  if (containsUltrathink(expandedText)) {
4062
4122
  keywordNotices.push({
4063
4123
  role: "custom",
@@ -4078,6 +4138,16 @@ export class AgentSession {
4078
4138
  timestamp,
4079
4139
  });
4080
4140
  }
4141
+ if (containsWorkflow(expandedText)) {
4142
+ keywordNotices.push({
4143
+ role: "custom",
4144
+ customType: "workflow-notice",
4145
+ content: WORKFLOW_NOTICE,
4146
+ display: false,
4147
+ attribution: "user",
4148
+ timestamp,
4149
+ });
4150
+ }
4081
4151
  }
4082
4152
 
4083
4153
  // If streaming, queue via steer() or followUp() based on option
@@ -4304,6 +4374,17 @@ export class AgentSession {
4304
4374
  return;
4305
4375
  }
4306
4376
 
4377
+ // Auto thinking: classify this real user turn and set the effective level
4378
+ // before the model request. Synthetic/tool-continuation turns (developer/
4379
+ // custom roles) and non-auto sessions are skipped. Never blocks the turn —
4380
+ // failures fall back to a concrete level inside the helper.
4381
+ if (this.#autoThinking && message.role === "user") {
4382
+ await this.#applyAutoThinkingLevel(expandedText, generation);
4383
+ if (this.#promptGeneration !== generation) {
4384
+ return;
4385
+ }
4386
+ }
4387
+
4307
4388
  const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined;
4308
4389
  await this.#promptAgentWithIdleRetry(messages, agentPromptOptions);
4309
4390
  if (!options?.skipPostPromptRecoveryWait) {
@@ -4919,9 +5000,9 @@ export class AgentSession {
4919
5000
  this.setTodoPhases([]);
4920
5001
  this.#syncAgentSessionId();
4921
5002
  this.#rekeyHindsightMemoryForCurrentSessionId();
4922
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
5003
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
4923
5004
  this.#resetHindsightConversationTrackingIfHindsight();
4924
- this.#resetMnemosyneConversationTrackingIfMnemosyne();
5005
+ this.#resetMnemopiConversationTrackingIfMnemopi();
4925
5006
  this.#steeringMessages = [];
4926
5007
  this.#followUpMessages = [];
4927
5008
  this.#pendingNextTurnMessages = [];
@@ -5016,8 +5097,8 @@ export class AgentSession {
5016
5097
  // Update agent session ID
5017
5098
  this.#syncAgentSessionId();
5018
5099
  this.#rekeyHindsightMemoryForCurrentSessionId();
5019
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
5020
- this.#resetMnemosyneConversationTrackingIfMnemosyne();
5100
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
5101
+ this.#resetMnemopiConversationTrackingIfMnemopi();
5021
5102
 
5022
5103
  // Emit session_switch event with reason "fork" to hooks
5023
5104
  if (this.#extensionRunner) {
@@ -5037,13 +5118,13 @@ export class AgentSession {
5037
5118
 
5038
5119
  /**
5039
5120
  * Set model directly.
5040
- * Validates API key, saves to session and settings.
5121
+ * Validates API key and saves to the active session. Persists settings only when requested.
5041
5122
  * @throws Error if no API key available for the model
5042
5123
  */
5043
5124
  async setModel(
5044
5125
  model: Model,
5045
5126
  role: string = "default",
5046
- options?: { selector?: string; thinkingLevel?: ThinkingLevel },
5127
+ options?: { selector?: string; thinkingLevel?: ThinkingLevel; persist?: boolean },
5047
5128
  ): Promise<void> {
5048
5129
  const previousEditMode = this.#resolveActiveEditMode();
5049
5130
  const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
@@ -5054,15 +5135,17 @@ export class AgentSession {
5054
5135
  this.#clearActiveRetryFallback();
5055
5136
  this.#setModelWithProviderSessionReset(model);
5056
5137
  this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, role);
5057
- this.settings.setModelRole(
5058
- role,
5059
- this.#formatRoleModelValue(role, model, options?.selector, options?.thinkingLevel),
5060
- );
5138
+ if (options?.persist) {
5139
+ this.settings.setModelRole(
5140
+ role,
5141
+ this.#formatRoleModelValue(role, model, options.selector, options.thinkingLevel),
5142
+ );
5143
+ }
5061
5144
  this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
5062
5145
 
5063
5146
  // Re-apply thinking for the newly selected model. Prefer the model's
5064
- // configured defaultLevel; otherwise preserve the current level.
5065
- this.setThinkingLevel(model.thinking?.defaultLevel ?? this.thinkingLevel);
5147
+ // configured defaultLevel; otherwise preserve the current level (or auto).
5148
+ this.#reapplyThinkingLevel(model.thinking?.defaultLevel);
5066
5149
  await this.#syncEditToolModeAfterModelChange(previousEditMode);
5067
5150
  }
5068
5151
 
@@ -5084,8 +5167,12 @@ export class AgentSession {
5084
5167
  this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
5085
5168
 
5086
5169
  // Apply explicit thinking level if given; otherwise prefer the model's
5087
- // configured defaultLevel; otherwise re-clamp the current level.
5088
- this.setThinkingLevel(thinkingLevel ?? model.thinking?.defaultLevel ?? this.thinkingLevel);
5170
+ // configured defaultLevel; otherwise re-clamp the current level (or auto).
5171
+ if (thinkingLevel !== undefined) {
5172
+ this.setThinkingLevel(thinkingLevel);
5173
+ } else {
5174
+ this.#reapplyThinkingLevel(model.thinking?.defaultLevel);
5175
+ }
5089
5176
  await this.#syncEditToolModeAfterModelChange(previousEditMode);
5090
5177
  }
5091
5178
 
@@ -5156,9 +5243,8 @@ export class AgentSession {
5156
5243
  }
5157
5244
 
5158
5245
  /**
5159
- * Apply a resolved role model as the active model, persisting the choice to
5160
- * settings under its role. Mirrors the non-temporary branch of
5161
- * {@link cycleRoleModels} and is shared with the plan-approval model slider.
5246
+ * Apply a resolved role model as the active model without changing global
5247
+ * settings. Shared with role cycling and the plan-approval model slider.
5162
5248
  */
5163
5249
  async applyRoleModel(entry: ResolvedRoleModel): Promise<void> {
5164
5250
  await this.setModel(entry.model, entry.role);
@@ -5169,24 +5255,21 @@ export class AgentSession {
5169
5255
 
5170
5256
  /**
5171
5257
  * Cycle through configured role models in a fixed order.
5172
- * Skips missing roles.
5258
+ * Skips missing roles and changes only the active session model.
5173
5259
  * @param roleOrder - Order of roles to cycle through (e.g., ["slow", "default", "smol"])
5174
- * @param options - Optional settings: `temporary` to not persist to settings
5260
+ * @param direction - "forward" (default) or "backward"
5175
5261
  */
5176
5262
  async cycleRoleModels(
5177
5263
  roleOrder: readonly string[],
5178
- options?: { temporary?: boolean },
5264
+ direction: "forward" | "backward" = "forward",
5179
5265
  ): Promise<RoleModelCycleResult | undefined> {
5180
5266
  const cycle = this.getRoleModelCycle(roleOrder);
5181
5267
  if (!cycle || cycle.models.length <= 1) return undefined;
5182
5268
 
5183
- const next = cycle.models[(cycle.currentIndex + 1) % cycle.models.length];
5269
+ const step = direction === "backward" ? -1 : 1;
5270
+ const next = cycle.models[(cycle.currentIndex + step + cycle.models.length) % cycle.models.length];
5184
5271
 
5185
- if (options?.temporary) {
5186
- await this.setModelTemporary(next.model, next.explicitThinkingLevel ? next.thinkingLevel : undefined);
5187
- } else {
5188
- await this.applyRoleModel(next);
5189
- }
5272
+ await this.applyRoleModel(next);
5190
5273
 
5191
5274
  return { model: next.model, thinkingLevel: this.thinkingLevel, role: next.role };
5192
5275
  }
@@ -5230,11 +5313,10 @@ export class AgentSession {
5230
5313
  this.#clearActiveRetryFallback();
5231
5314
  this.#setModelWithProviderSessionReset(next.model);
5232
5315
  this.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
5233
- this.settings.setModelRole("default", this.#formatRoleModelValue("default", next.model));
5234
5316
  this.settings.getStorage()?.recordModelUsage(`${next.model.provider}/${next.model.id}`);
5235
5317
 
5236
- // Apply the scoped model's configured thinking level
5237
- this.setThinkingLevel(next.thinkingLevel);
5318
+ // Apply the scoped model's configured thinking level, preserving auto.
5319
+ this.setThinkingLevel(this.#autoThinking ? AUTO_THINKING : next.thinkingLevel);
5238
5320
  await this.#syncEditToolModeAfterModelChange(previousEditMode);
5239
5321
 
5240
5322
  return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
@@ -5261,10 +5343,9 @@ export class AgentSession {
5261
5343
  this.#clearActiveRetryFallback();
5262
5344
  this.#setModelWithProviderSessionReset(nextModel);
5263
5345
  this.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
5264
- this.settings.setModelRole("default", this.#formatRoleModelValue("default", nextModel));
5265
5346
  this.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
5266
- // Re-apply the current thinking level for the newly selected model
5267
- this.setThinkingLevel(this.thinkingLevel);
5347
+ // Re-apply the current thinking level (or auto) for the newly selected model
5348
+ this.#reapplyThinkingLevel();
5268
5349
  await this.#syncEditToolModeAfterModelChange(previousEditMode);
5269
5350
 
5270
5351
  return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
@@ -5282,10 +5363,30 @@ export class AgentSession {
5282
5363
  // =========================================================================
5283
5364
 
5284
5365
  /**
5285
- * Set thinking level.
5286
- * Saves the effective metadata-clamped level to session and settings only if it changes.
5366
+ * Set the thinking level. `auto` enables per-turn classification; the selector
5367
+ * itself is never written to the session log, but resolved concrete levels are
5368
+ * persisted when real user turns are classified so resumed sessions keep the
5369
+ * last resolved effort instead of reverting to pending auto.
5287
5370
  */
5288
- setThinkingLevel(level: ThinkingLevel | undefined, persist: boolean = false): void {
5371
+ setThinkingLevel(level: ConfiguredThinkingLevel | undefined, persist: boolean = false): void {
5372
+ if (level === AUTO_THINKING) {
5373
+ const provisional = resolveProvisionalAutoLevel(this.model);
5374
+ const wasAuto = this.#autoThinking;
5375
+ this.#autoThinking = true;
5376
+ this.#autoResolvedLevel = undefined;
5377
+ this.#thinkingLevel = provisional;
5378
+ this.agent.setThinkingLevel(toReasoningEffort(provisional));
5379
+ if (persist) {
5380
+ this.settings.set("defaultThinkingLevel", AUTO_THINKING);
5381
+ }
5382
+ if (!wasAuto || this.#thinkingLevel !== provisional) {
5383
+ this.#emit({ type: "thinking_level_changed", thinkingLevel: provisional, configured: AUTO_THINKING });
5384
+ }
5385
+ return;
5386
+ }
5387
+
5388
+ this.#autoThinking = false;
5389
+ this.#autoResolvedLevel = undefined;
5289
5390
  const effectiveLevel = resolveThinkingLevelForModel(this.model, level);
5290
5391
  const isChanging = effectiveLevel !== this.#thinkingLevel;
5291
5392
 
@@ -5302,14 +5403,28 @@ export class AgentSession {
5302
5403
  }
5303
5404
 
5304
5405
  /**
5305
- * Cycle to next thinking level.
5306
- * @returns New level, or undefined if model doesn't support thinking
5406
+ * Re-apply the active thinking selection after a model change. Preserves `auto`
5407
+ * (re-clamping the provisional level to the new model); otherwise re-applies the
5408
+ * preferred default or the current effective level.
5409
+ */
5410
+ #reapplyThinkingLevel(preferredDefault?: ThinkingLevel): void {
5411
+ this.setThinkingLevel(this.#autoThinking ? AUTO_THINKING : (preferredDefault ?? this.#thinkingLevel));
5412
+ }
5413
+
5414
+ /**
5415
+ * Cycle to next thinking level: off → auto → minimal..xhigh → off.
5416
+ * @returns New selector, or undefined if model doesn't support thinking
5307
5417
  */
5308
- cycleThinkingLevel(): ThinkingLevel | undefined {
5418
+ cycleThinkingLevel(): ConfiguredThinkingLevel | undefined {
5309
5419
  if (!this.model?.reasoning) return undefined;
5310
5420
 
5311
- const levels = [ThinkingLevel.Off, ...this.getAvailableThinkingLevels()];
5312
- const currentLevel = this.thinkingLevel === ThinkingLevel.Inherit ? ThinkingLevel.Off : this.thinkingLevel;
5421
+ const levels: ConfiguredThinkingLevel[] = [
5422
+ ThinkingLevel.Off,
5423
+ AUTO_THINKING,
5424
+ ...this.getAvailableThinkingLevels(),
5425
+ ];
5426
+ const configured = this.configuredThinkingLevel();
5427
+ const currentLevel = configured === ThinkingLevel.Inherit ? ThinkingLevel.Off : configured;
5313
5428
  const currentIndex = currentLevel ? levels.indexOf(currentLevel) : -1;
5314
5429
  const nextIndex = (currentIndex + 1) % levels.length;
5315
5430
  const nextLevel = levels[nextIndex];
@@ -5319,6 +5434,65 @@ export class AgentSession {
5319
5434
  return nextLevel;
5320
5435
  }
5321
5436
 
5437
+ /** Timeout (ms) for per-turn auto-thinking classification before falling back. */
5438
+ static readonly #AUTO_THINKING_TIMEOUT_MS = 4000;
5439
+
5440
+ /**
5441
+ * Classify the current user turn and set the effective thinking level for it.
5442
+ * Bounded by a timeout + abort; on any failure (no smol model, timeout, parse
5443
+ * error) it falls back to the provisional concrete level and continues. Never
5444
+ * throws into the turn, and never clears `#autoThinking` (auto stays active).
5445
+ */
5446
+ async #applyAutoThinkingLevel(promptText: string, generation: number): Promise<void> {
5447
+ const model = this.model;
5448
+ if (!model?.reasoning) return;
5449
+
5450
+ let resolved: Effort | undefined;
5451
+ if (containsUltrathink(promptText)) {
5452
+ // The user explicitly asked for maximum thinking; bypass the classifier
5453
+ // and jump straight to the highest auto-supported level for this model.
5454
+ resolved = clampAutoThinkingEffort(model, Effort.XHigh);
5455
+ } else {
5456
+ const controller = new AbortController();
5457
+ const timer = setTimeout(() => controller.abort(), AgentSession.#AUTO_THINKING_TIMEOUT_MS);
5458
+ try {
5459
+ resolved = await classifyDifficulty(promptText, {
5460
+ settings: this.settings,
5461
+ registry: this.#modelRegistry,
5462
+ model,
5463
+ sessionId: this.sessionId,
5464
+ signal: controller.signal,
5465
+ metadataResolver: provider => this.agent.metadataForProvider(provider),
5466
+ });
5467
+ } catch (error) {
5468
+ logger.debug("auto-thinking: classification failed; using fallback level", {
5469
+ error: error instanceof Error ? error.message : String(error),
5470
+ });
5471
+ } finally {
5472
+ clearTimeout(timer);
5473
+ }
5474
+ }
5475
+
5476
+ // Drop the result if the turn was aborted/superseded while classifying.
5477
+ if (this.#promptGeneration !== generation || !this.#autoThinking) return;
5478
+
5479
+ const effort = resolved ?? resolveProvisionalAutoLevel(model);
5480
+ if (effort === undefined) return;
5481
+ const shouldPersistResolution = this.#autoResolvedLevel !== effort;
5482
+ this.#autoResolvedLevel = effort;
5483
+ this.#thinkingLevel = effort;
5484
+ this.agent.setThinkingLevel(toReasoningEffort(effort));
5485
+ if (shouldPersistResolution) {
5486
+ this.sessionManager.appendThinkingLevelChange(effort);
5487
+ }
5488
+ this.#emit({
5489
+ type: "thinking_level_changed",
5490
+ thinkingLevel: effort,
5491
+ configured: AUTO_THINKING,
5492
+ resolved: effort,
5493
+ });
5494
+ }
5495
+
5322
5496
  /**
5323
5497
  * True when *any* fast-mode-granting service tier is configured, regardless
5324
5498
  * of whether the active model's provider actually realizes it. Used by the
@@ -5479,6 +5653,165 @@ export class AgentSession {
5479
5653
  return { removed };
5480
5654
  }
5481
5655
 
5656
+ /**
5657
+ * Surgically reduce context by dropping heavy content ("shake").
5658
+ *
5659
+ * - `images` delegates to {@link dropImages}.
5660
+ * - `elide` replaces whole tool-call results and large fenced/XML blocks
5661
+ * with short placeholders that embed an `artifact://` recovery link.
5662
+ * - `summary` extractively compresses the same regions with the configured
5663
+ * local on-device model (`providers.shakeSummaryModel`), falling back to
5664
+ * the elide placeholder per region (or wholesale when the local model is
5665
+ * unavailable). Never calls a remote/cloud LLM.
5666
+ *
5667
+ * Mutates the branch in place, persists via `rewriteEntries`, replays the
5668
+ * rebuilt context through the agent, and tears down provider sessions that
5669
+ * cache message identity — same rewrite contract as {@link dropImages}.
5670
+ *
5671
+ * No-op (zero counts) when nothing is eligible.
5672
+ */
5673
+ async shake(mode: ShakeMode, opts: { config?: ShakeConfig; signal?: AbortSignal } = {}): Promise<ShakeResult> {
5674
+ if (mode === "images") {
5675
+ const { removed } = await this.dropImages();
5676
+ return { mode, toolResultsDropped: 0, blocksDropped: 0, imagesDropped: removed, tokensFreed: 0 };
5677
+ }
5678
+
5679
+ const config = opts.config ?? AGGRESSIVE_SHAKE_CONFIG;
5680
+ const regions = collectShakeRegions(this.sessionManager.getBranch(), config);
5681
+ if (regions.length === 0) {
5682
+ return { mode, toolResultsDropped: 0, blocksDropped: 0, tokensFreed: 0 };
5683
+ }
5684
+
5685
+ const artifactId = await this.#saveShakeArtifact(regions);
5686
+ let replacements: string[];
5687
+ if (mode === "summary") {
5688
+ // Manual `/shake summary` installs the compaction controller so Esc /
5689
+ // `abortCompaction()` can cancel the local-model pass; the auto-shake path
5690
+ // passes its own signal and manages `#autoCompactionAbortController`.
5691
+ let controller: AbortController | undefined;
5692
+ let signal = opts.signal;
5693
+ if (!signal) {
5694
+ if (this.#compactionAbortController) throw new Error("Compaction already in progress");
5695
+ controller = new AbortController();
5696
+ this.#compactionAbortController = controller;
5697
+ signal = controller.signal;
5698
+ }
5699
+ try {
5700
+ replacements = await this.#buildShakeSummaryReplacements(regions, artifactId, signal);
5701
+ } finally {
5702
+ if (controller && this.#compactionAbortController === controller) {
5703
+ this.#compactionAbortController = undefined;
5704
+ }
5705
+ }
5706
+ } else {
5707
+ replacements = regions.map((region, index) => this.#shakeElidePlaceholder(region, index, artifactId));
5708
+ }
5709
+
5710
+ let toolResultsDropped = 0;
5711
+ let blocksDropped = 0;
5712
+ let originalTokens = 0;
5713
+ let replacementTokens = 0;
5714
+ const items = regions.map((region, index) => {
5715
+ if (region.kind === "toolResult") toolResultsDropped++;
5716
+ else blocksDropped++;
5717
+ originalTokens += region.tokens;
5718
+ const replacement = replacements[index];
5719
+ if (replacement.length > 0) replacementTokens += countTokens(replacement);
5720
+ return { region, replacement };
5721
+ });
5722
+
5723
+ applyShakeRegions(items);
5724
+
5725
+ await this.sessionManager.rewriteEntries();
5726
+ const sessionContext = this.buildDisplaySessionContext();
5727
+ this.agent.replaceMessages(sessionContext.messages);
5728
+ this.#closeCodexProviderSessionsForHistoryRewrite();
5729
+
5730
+ return {
5731
+ mode,
5732
+ toolResultsDropped,
5733
+ blocksDropped,
5734
+ tokensFreed: Math.max(0, originalTokens - replacementTokens),
5735
+ artifactId,
5736
+ };
5737
+ }
5738
+
5739
+ #shakeElidePlaceholder(region: ShakeRegion, index: number, artifactId: string | undefined): string {
5740
+ if (artifactId) {
5741
+ return `[shaken ~${region.tokens} tokens — recover: artifact://${artifactId} (region ${index + 1})]`;
5742
+ }
5743
+ return `[shaken ~${region.tokens} tokens]`;
5744
+ }
5745
+
5746
+ /**
5747
+ * Concatenate the original region contents into one session artifact so the
5748
+ * agent can read them back via `artifact://<id>`. Returns `undefined` when
5749
+ * the session is not persisted or the write fails — callers degrade to a
5750
+ * bare placeholder.
5751
+ */
5752
+ async #saveShakeArtifact(regions: ShakeRegion[]): Promise<string | undefined> {
5753
+ const parts: string[] = [];
5754
+ for (let i = 0; i < regions.length; i++) {
5755
+ const region = regions[i];
5756
+ parts.push(`### region ${i + 1} (${region.label}, ~${region.tokens} tok)`, "", region.originalText, "");
5757
+ }
5758
+ try {
5759
+ return await this.sessionManager.saveArtifact(parts.join("\n"), "shake");
5760
+ } catch {
5761
+ return undefined;
5762
+ }
5763
+ }
5764
+
5765
+ /**
5766
+ * Build per-region replacements for summary mode using the configured local
5767
+ * on-device model (`providers.shakeSummaryModel`) via {@link tinyModelClient}.
5768
+ * Shake summary never calls a remote/cloud LLM. When the configured model is
5769
+ * not a known local key, every region falls back to the elide placeholder.
5770
+ * Otherwise compresses via {@link summarizeShakeRegions}; per region, uses
5771
+ * the parsed summary (with a recovery footer) or the elide placeholder when
5772
+ * the local model omitted it / was unavailable. Any thrown failure degrades
5773
+ * the whole batch to elide so the reduction still happens.
5774
+ */
5775
+ async #buildShakeSummaryReplacements(
5776
+ regions: ShakeRegion[],
5777
+ artifactId: string | undefined,
5778
+ signal: AbortSignal | undefined,
5779
+ ): Promise<string[]> {
5780
+ const elide = (): string[] =>
5781
+ regions.map((region, index) => this.#shakeElidePlaceholder(region, index, artifactId));
5782
+
5783
+ const modelKey = this.settings.get("providers.shakeSummaryModel");
5784
+ if (!isTinyMemoryLocalModelKey(modelKey)) return elide();
5785
+
5786
+ const items: ShakeSummaryItem[] = regions.map((region, index) => ({
5787
+ index,
5788
+ label: region.label,
5789
+ text: region.originalText,
5790
+ }));
5791
+
5792
+ const complete: ShakeSummaryComplete = (promptText, opts) =>
5793
+ tinyModelClient.complete(modelKey, promptText, { maxTokens: opts.maxTokens, signal: opts.signal });
5794
+
5795
+ let summaries: Map<number, string>;
5796
+ try {
5797
+ summaries = await summarizeShakeRegions(items, complete, { signal });
5798
+ } catch (error) {
5799
+ logger.warn("Shake summary compression failed; falling back to elide", {
5800
+ error: error instanceof Error ? error.message : String(error),
5801
+ });
5802
+ return elide();
5803
+ }
5804
+
5805
+ return regions.map((region, index) => {
5806
+ const summary = summaries.get(index);
5807
+ if (!summary) return this.#shakeElidePlaceholder(region, index, artifactId);
5808
+ if (artifactId) {
5809
+ return `${summary}\n\n[recover full: artifact://${artifactId} (region ${index + 1})]`;
5810
+ }
5811
+ return summary;
5812
+ });
5813
+ }
5814
+
5482
5815
  /**
5483
5816
  * Manually compact the session context.
5484
5817
  * Aborts current agent operation first.
@@ -5789,9 +6122,9 @@ export class AgentSession {
5789
6122
  this.agent.reset();
5790
6123
  this.#syncAgentSessionId();
5791
6124
  this.#rekeyHindsightMemoryForCurrentSessionId();
5792
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
6125
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
5793
6126
  this.#resetHindsightConversationTrackingIfHindsight();
5794
- this.#resetMnemosyneConversationTrackingIfMnemosyne();
6127
+ this.#resetMnemopiConversationTrackingIfMnemopi();
5795
6128
  this.#steeringMessages = [];
5796
6129
  this.#followUpMessages = [];
5797
6130
  this.#pendingNextTurnMessages = [];
@@ -6691,6 +7024,14 @@ export class AgentSession {
6691
7024
  if (compactionSettings.strategy === "off") return false;
6692
7025
  if (reason !== "idle" && !compactionSettings.enabled) return false;
6693
7026
  const generation = this.#promptGeneration;
7027
+
7028
+ // Shake strategies run inline (cheap, no remote LLM). On overflow recovery,
7029
+ // if shake reclaims nothing we fall through to the summary-compaction body
7030
+ // below so the oversized input still gets resolved.
7031
+ if (compactionSettings.strategy === "shake" || compactionSettings.strategy === "shake-summary") {
7032
+ const outcome = await this.#runAutoShake(reason, compactionSettings.strategy, willRetry, generation);
7033
+ if (outcome !== "fallback") return false;
7034
+ }
6694
7035
  // "overflow" and "incomplete" force inline execution because they are recovery
6695
7036
  // paths the caller wants resolved before scheduling the next turn. "idle" is
6696
7037
  // triggered by the idle loop and does its own scheduling.
@@ -7073,6 +7414,119 @@ export class AgentSession {
7073
7414
  return false;
7074
7415
  }
7075
7416
 
7417
+ /**
7418
+ * Run a shake-strategy auto-maintenance pass. Emits the
7419
+ * `auto_compaction_start`/`auto_compaction_end` pair with a shake `action`,
7420
+ * runs {@link shake} inline against the protect-window config, and schedules
7421
+ * continuation exactly like the context-full tail.
7422
+ *
7423
+ * Returns `"fallback"` only for an overflow recovery where shake reclaimed
7424
+ * nothing (or threw) — the caller then runs the summary-compaction body so
7425
+ * the oversized input still gets resolved. Returns `"handled"` otherwise.
7426
+ */
7427
+ async #runAutoShake(
7428
+ reason: "overflow" | "threshold" | "idle" | "incomplete",
7429
+ strategy: "shake" | "shake-summary",
7430
+ willRetry: boolean,
7431
+ generation: number,
7432
+ ): Promise<"handled" | "fallback"> {
7433
+ const action = strategy === "shake-summary" ? "shake-summary" : "shake";
7434
+ const mode = strategy === "shake-summary" ? "summary" : "elide";
7435
+ await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
7436
+ this.#autoCompactionAbortController?.abort();
7437
+ const controller = new AbortController();
7438
+ this.#autoCompactionAbortController = controller;
7439
+ const signal = controller.signal;
7440
+ const compactionSettings = this.settings.getGroup("compaction");
7441
+ try {
7442
+ const result = await this.shake(mode, { config: DEFAULT_SHAKE_CONFIG, signal });
7443
+ if (signal.aborted) {
7444
+ await this.#emitSessionEvent({
7445
+ type: "auto_compaction_end",
7446
+ action,
7447
+ result: undefined,
7448
+ aborted: true,
7449
+ willRetry: false,
7450
+ });
7451
+ return "handled";
7452
+ }
7453
+ const reclaimed = result.toolResultsDropped + result.blocksDropped > 0;
7454
+ // Overflow needs the input to actually shrink before the retry; if shake
7455
+ // reclaimed nothing, summarization is the only remaining recovery.
7456
+ if (reason === "overflow" && !reclaimed) {
7457
+ await this.#emitSessionEvent({
7458
+ type: "auto_compaction_end",
7459
+ action,
7460
+ result: undefined,
7461
+ aborted: false,
7462
+ willRetry: false,
7463
+ skipped: true,
7464
+ });
7465
+ return "fallback";
7466
+ }
7467
+ await this.#emitSessionEvent({
7468
+ type: "auto_compaction_end",
7469
+ action,
7470
+ result: undefined,
7471
+ aborted: false,
7472
+ willRetry,
7473
+ skipped: !reclaimed,
7474
+ });
7475
+
7476
+ if (!willRetry && reason !== "idle" && compactionSettings.autoContinue !== false) {
7477
+ this.#scheduleAutoContinuePrompt(generation);
7478
+ }
7479
+ if (willRetry) {
7480
+ // The shake rebuild replays every entry, so a trailing error/length
7481
+ // assistant from the failed turn re-enters agent state — drop it before
7482
+ // retrying, same as the context-full tail.
7483
+ const messages = this.agent.state.messages;
7484
+ const lastMsg = messages[messages.length - 1];
7485
+ if (lastMsg?.role === "assistant") {
7486
+ const lastAssistant = lastMsg as AssistantMessage;
7487
+ const shouldDrop =
7488
+ lastAssistant.stopReason === "error" ||
7489
+ (reason === "incomplete" && lastAssistant.stopReason === "length");
7490
+ if (shouldDrop) this.agent.replaceMessages(messages.slice(0, -1));
7491
+ }
7492
+ this.#scheduleAgentContinue({ delayMs: 100, generation });
7493
+ } else if (this.agent.hasQueuedMessages()) {
7494
+ this.#scheduleAgentContinue({
7495
+ delayMs: 100,
7496
+ generation,
7497
+ shouldContinue: () => this.agent.hasQueuedMessages(),
7498
+ });
7499
+ }
7500
+ return "handled";
7501
+ } catch (error) {
7502
+ if (signal.aborted) {
7503
+ await this.#emitSessionEvent({
7504
+ type: "auto_compaction_end",
7505
+ action,
7506
+ result: undefined,
7507
+ aborted: true,
7508
+ willRetry: false,
7509
+ });
7510
+ return "handled";
7511
+ }
7512
+ const message = error instanceof Error ? error.message : "shake failed";
7513
+ await this.#emitSessionEvent({
7514
+ type: "auto_compaction_end",
7515
+ action,
7516
+ result: undefined,
7517
+ aborted: false,
7518
+ willRetry: false,
7519
+ errorMessage: `Auto-shake failed: ${message}`,
7520
+ });
7521
+ // Overflow still needs recovery even if shake threw.
7522
+ return reason === "overflow" ? "fallback" : "handled";
7523
+ } finally {
7524
+ if (this.#autoCompactionAbortController === controller) {
7525
+ this.#autoCompactionAbortController = undefined;
7526
+ }
7527
+ }
7528
+ }
7529
+
7076
7530
  /**
7077
7531
  * Toggle auto-compaction setting.
7078
7532
  */
@@ -7260,7 +7714,9 @@ export class AgentSession {
7260
7714
  throw new Error(`No API key for retry fallback ${selector.raw}`);
7261
7715
  }
7262
7716
 
7263
- const currentThinkingLevel = this.thinkingLevel;
7717
+ // Capture the configured selector (auto-aware) so a fallback chain preserves
7718
+ // `auto` instead of collapsing it to the level it resolved to this turn.
7719
+ const currentThinkingLevel = this.configuredThinkingLevel();
7264
7720
  const nextThinkingLevel = selector.thinkingLevel ?? currentThinkingLevel;
7265
7721
 
7266
7722
  this.#setModelWithProviderSessionReset(candidate);
@@ -7333,7 +7789,7 @@ export class AgentSession {
7333
7789
  const apiKey = await this.#modelRegistry.getApiKey(primaryModel, this.sessionId);
7334
7790
  if (!apiKey) return;
7335
7791
 
7336
- const currentThinkingLevel = this.thinkingLevel;
7792
+ const currentThinkingLevel = this.configuredThinkingLevel();
7337
7793
  const thinkingToApply =
7338
7794
  currentThinkingLevel === lastAppliedFallbackThinkingLevel ? originalThinkingLevel : currentThinkingLevel;
7339
7795
  this.#setModelWithProviderSessionReset(primaryModel);
@@ -8033,6 +8489,7 @@ export class AgentSession {
8033
8489
  promptText: string;
8034
8490
  onTextDelta?: (delta: string) => void;
8035
8491
  signal?: AbortSignal;
8492
+ dedupeReply?: boolean;
8036
8493
  }): Promise<{ replyText: string; assistantMessage: AssistantMessage }> {
8037
8494
  const model = this.model;
8038
8495
  if (!model) {
@@ -8095,7 +8552,10 @@ export class AgentSession {
8095
8552
  if (!assistantMessage) {
8096
8553
  throw new Error("Ephemeral turn ended without a final message");
8097
8554
  }
8098
- return { replyText: dedupeIrcReply(replyText.trim()), assistantMessage };
8555
+ return {
8556
+ replyText: args.dedupeReply === false ? replyText.trim() : dedupeIrcReply(replyText.trim()),
8557
+ assistantMessage,
8558
+ };
8099
8559
  }
8100
8560
 
8101
8561
  /**
@@ -8244,6 +8704,8 @@ export class AgentSession {
8244
8704
  const previousScheduledHiddenNextTurnGeneration = this.#scheduledHiddenNextTurnGeneration;
8245
8705
  const previousModel = this.model;
8246
8706
  const previousThinkingLevel = this.#thinkingLevel;
8707
+ const previousAutoThinking = this.#autoThinking;
8708
+ const previousAutoResolvedLevel = this.#autoResolvedLevel;
8247
8709
  const previousServiceTier = this.agent.serviceTier;
8248
8710
  const previousSelectedMCPToolNames = new Set(this.#selectedMCPToolNames);
8249
8711
  const previousTools = [...this.agent.state.tools];
@@ -8262,7 +8724,7 @@ export class AgentSession {
8262
8724
  await this.sessionManager.setSessionFile(sessionPath);
8263
8725
  this.#syncAgentSessionId();
8264
8726
  this.#rekeyHindsightMemoryForCurrentSessionId();
8265
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
8727
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
8266
8728
 
8267
8729
  const sessionContext = this.buildDisplaySessionContext();
8268
8730
  const didReloadConversationChange =
@@ -8321,12 +8783,26 @@ export class AgentSession {
8321
8783
  .some(entry => entry.type === "service_tier_change");
8322
8784
  const defaultThinkingLevel = this.settings.get("defaultThinkingLevel");
8323
8785
  const configuredServiceTier = this.settings.get("serviceTier");
8324
- const nextThinkingLevel = resolveThinkingLevelForModel(
8325
- this.model,
8326
- hasThinkingEntry ? (sessionContext.thinkingLevel as ThinkingLevel | undefined) : defaultThinkingLevel,
8327
- );
8328
- this.#thinkingLevel = nextThinkingLevel;
8329
- this.agent.setThinkingLevel(toReasoningEffort(nextThinkingLevel));
8786
+ // Session log entries store only concrete levels. When `auto` has resolved
8787
+ // for a turn, the persisted context may already carry that concrete level
8788
+ // even if the branch scan races a just-flushed thinking entry under isolated
8789
+ // parallel test workers. Prefer the concrete context value in that case;
8790
+ // otherwise keep the configured `auto` selector so fresh sessions still
8791
+ // classify their first turn.
8792
+ const restoredThinkingLevel: ConfiguredThinkingLevel | undefined =
8793
+ hasThinkingEntry || (defaultThinkingLevel === AUTO_THINKING && sessionContext.thinkingLevel !== "off")
8794
+ ? (sessionContext.thinkingLevel as ThinkingLevel | undefined)
8795
+ : defaultThinkingLevel;
8796
+ if (restoredThinkingLevel === AUTO_THINKING) {
8797
+ this.#autoThinking = true;
8798
+ this.#autoResolvedLevel = undefined;
8799
+ this.#thinkingLevel = resolveProvisionalAutoLevel(this.model);
8800
+ } else {
8801
+ this.#autoThinking = false;
8802
+ this.#autoResolvedLevel = undefined;
8803
+ this.#thinkingLevel = resolveThinkingLevelForModel(this.model, restoredThinkingLevel);
8804
+ }
8805
+ this.agent.setThinkingLevel(toReasoningEffort(this.#thinkingLevel));
8330
8806
  this.agent.serviceTier = hasServiceTierEntry
8331
8807
  ? sessionContext.serviceTier
8332
8808
  : configuredServiceTier === "none"
@@ -8335,7 +8811,7 @@ export class AgentSession {
8335
8811
 
8336
8812
  if (switchingToDifferentSession) {
8337
8813
  this.#resetHindsightConversationTrackingIfHindsight();
8338
- this.#resetMnemosyneConversationTrackingIfMnemosyne();
8814
+ this.#resetMnemopiConversationTrackingIfMnemopi();
8339
8815
  }
8340
8816
  this.#reconnectToAgent();
8341
8817
  return true;
@@ -8343,7 +8819,7 @@ export class AgentSession {
8343
8819
  this.sessionManager.restoreState(previousSessionState);
8344
8820
  this.#syncAgentSessionId(previousSessionState.sessionId);
8345
8821
  this.#rekeyHindsightMemoryForCurrentSessionId();
8346
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
8822
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
8347
8823
  let restoreMcpError: unknown;
8348
8824
  try {
8349
8825
  await this.#restoreMCPSelectionsForSessionContext(previousSessionContext, {
@@ -8375,6 +8851,8 @@ export class AgentSession {
8375
8851
  this.#syncToolCallBatchCap(undefined);
8376
8852
  }
8377
8853
  this.#thinkingLevel = previousThinkingLevel;
8854
+ this.#autoThinking = previousAutoThinking;
8855
+ this.#autoResolvedLevel = previousAutoResolvedLevel;
8378
8856
  this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
8379
8857
  this.agent.serviceTier = previousServiceTier;
8380
8858
  this.#syncTodoPhasesFromBranch();
@@ -8439,9 +8917,9 @@ export class AgentSession {
8439
8917
  this.#syncTodoPhasesFromBranch();
8440
8918
  this.#syncAgentSessionId();
8441
8919
  this.#rekeyHindsightMemoryForCurrentSessionId();
8442
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
8920
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
8443
8921
  this.#resetHindsightConversationTrackingIfHindsight();
8444
- this.#resetMnemosyneConversationTrackingIfMnemosyne();
8922
+ this.#resetMnemopiConversationTrackingIfMnemopi();
8445
8923
 
8446
8924
  // Reload messages from entries (works for both file and in-memory mode)
8447
8925
  const sessionContext = this.buildDisplaySessionContext();
@@ -8822,7 +9300,7 @@ export class AgentSession {
8822
9300
  const msg = messages[i];
8823
9301
  if (msg.role === "assistant") {
8824
9302
  const assistantMsg = msg as AssistantMessage;
8825
- if (assistantMsg.usage) {
9303
+ if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) {
8826
9304
  lastUsage = assistantMsg.usage;
8827
9305
  lastUsageIndex = i;
8828
9306
  break;