@oh-my-pi/pi-coding-agent 15.7.2 → 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 (160) hide show
  1. package/CHANGELOG.md +75 -6
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/extension-flags.d.ts +36 -0
  4. package/dist/types/config/config-file.d.ts +4 -0
  5. package/dist/types/config/file-lock.d.ts +23 -0
  6. package/dist/types/config/keybindings.d.ts +2 -1
  7. package/dist/types/config/model-registry.d.ts +6 -0
  8. package/dist/types/config/settings-schema.d.ts +88 -65
  9. package/dist/types/edit/hashline/diff.d.ts +3 -3
  10. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +1 -0
  11. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +1 -0
  12. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +1 -0
  13. package/dist/types/eval/agent-bridge.d.ts +25 -0
  14. package/dist/types/eval/backend.d.ts +17 -2
  15. package/dist/types/eval/budget-bridge.d.ts +29 -0
  16. package/dist/types/eval/idle-timeout.d.ts +28 -0
  17. package/dist/types/eval/js/executor.d.ts +8 -0
  18. package/dist/types/eval/js/tool-bridge.d.ts +2 -1
  19. package/dist/types/eval/py/executor.d.ts +13 -0
  20. package/dist/types/exec/bash-executor.d.ts +1 -0
  21. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  22. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  23. package/dist/types/extensibility/plugins/git-url.d.ts +11 -1
  24. package/dist/types/extensibility/plugins/manager.d.ts +12 -1
  25. package/dist/types/extensibility/shared-events.d.ts +2 -2
  26. package/dist/types/memory-backend/index.d.ts +1 -1
  27. package/dist/types/memory-backend/resolve.d.ts +1 -1
  28. package/dist/types/memory-backend/types.d.ts +3 -3
  29. package/dist/types/mnemopi/backend.d.ts +4 -0
  30. package/dist/types/mnemopi/config.d.ts +29 -0
  31. package/dist/types/mnemopi/state.d.ts +72 -0
  32. package/dist/types/modes/components/custom-editor.d.ts +2 -2
  33. package/dist/types/modes/components/omfg-panel.d.ts +19 -0
  34. package/dist/types/modes/controllers/command-controller.d.ts +7 -0
  35. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  36. package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
  37. package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
  38. package/dist/types/modes/gradient-highlight.d.ts +5 -1
  39. package/dist/types/modes/interactive-mode.d.ts +7 -3
  40. package/dist/types/modes/magic-keywords.d.ts +14 -0
  41. package/dist/types/modes/markdown-prose.d.ts +27 -0
  42. package/dist/types/modes/orchestrate.d.ts +7 -2
  43. package/dist/types/modes/shared.d.ts +1 -1
  44. package/dist/types/modes/turn-budget.d.ts +18 -0
  45. package/dist/types/modes/types.d.ts +7 -3
  46. package/dist/types/modes/ultrathink.d.ts +7 -2
  47. package/dist/types/modes/workflow.d.ts +15 -0
  48. package/dist/types/sdk.d.ts +13 -3
  49. package/dist/types/session/agent-session.d.ts +40 -17
  50. package/dist/types/session/session-manager.d.ts +18 -0
  51. package/dist/types/session/session-storage.d.ts +6 -0
  52. package/dist/types/session/shake-types.d.ts +24 -0
  53. package/dist/types/task/executor.d.ts +2 -2
  54. package/dist/types/tiny/models.d.ts +15 -1
  55. package/dist/types/tiny/title-protocol.d.ts +4 -0
  56. package/dist/types/tools/index.d.ts +19 -3
  57. package/dist/types/tools/memory-edit.d.ts +1 -1
  58. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  59. package/package.json +10 -10
  60. package/src/autoresearch/tools/run-experiment.ts +45 -113
  61. package/src/cli/args.ts +39 -16
  62. package/src/cli/extension-flags.ts +48 -0
  63. package/src/cli/plugin-cli.ts +11 -2
  64. package/src/config/config-file.ts +98 -13
  65. package/src/config/file-lock.ts +60 -17
  66. package/src/config/keybindings.ts +78 -27
  67. package/src/config/model-registry.ts +7 -1
  68. package/src/config/settings-schema.ts +94 -67
  69. package/src/config/settings.ts +12 -0
  70. package/src/edit/hashline/diff.ts +81 -24
  71. package/src/edit/renderer.ts +16 -12
  72. package/src/eval/__tests__/agent-bridge.test.ts +433 -0
  73. package/src/eval/__tests__/budget-bridge.test.ts +69 -0
  74. package/src/eval/__tests__/idle-timeout.test.ts +66 -0
  75. package/src/eval/__tests__/shared-executors.test.ts +21 -0
  76. package/src/eval/agent-bridge.ts +295 -0
  77. package/src/eval/backend.ts +17 -2
  78. package/src/eval/budget-bridge.ts +48 -0
  79. package/src/eval/idle-timeout.ts +80 -0
  80. package/src/eval/js/executor.ts +35 -7
  81. package/src/eval/js/index.ts +2 -1
  82. package/src/eval/js/shared/prelude.txt +85 -1
  83. package/src/eval/js/tool-bridge.ts +9 -0
  84. package/src/eval/py/executor.ts +41 -14
  85. package/src/eval/py/index.ts +2 -1
  86. package/src/eval/py/prelude.py +132 -1
  87. package/src/exec/bash-executor.ts +2 -3
  88. package/src/extensibility/custom-tools/types.ts +2 -2
  89. package/src/extensibility/extensions/runner.ts +12 -2
  90. package/src/extensibility/plugins/git-url.ts +90 -4
  91. package/src/extensibility/plugins/manager.ts +103 -7
  92. package/src/extensibility/shared-events.ts +2 -2
  93. package/src/internal-urls/docs-index.generated.ts +88 -88
  94. package/src/main.ts +44 -55
  95. package/src/memory-backend/index.ts +1 -1
  96. package/src/memory-backend/resolve.ts +3 -3
  97. package/src/memory-backend/types.ts +3 -3
  98. package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
  99. package/src/{mnemosyne → mnemopi}/config.ts +36 -36
  100. package/src/{mnemosyne → mnemopi}/state.ts +67 -67
  101. package/src/modes/components/agent-dashboard.ts +6 -6
  102. package/src/modes/components/custom-editor.ts +4 -11
  103. package/src/modes/components/extensions/state-manager.ts +3 -4
  104. package/src/modes/components/footer.ts +8 -9
  105. package/src/modes/components/hook-selector.ts +86 -20
  106. package/src/modes/components/oauth-selector.ts +93 -21
  107. package/src/modes/components/omfg-panel.ts +141 -0
  108. package/src/modes/components/settings-defs.ts +2 -2
  109. package/src/modes/components/tips.txt +2 -1
  110. package/src/modes/components/tool-execution.ts +38 -19
  111. package/src/modes/components/tree-selector.ts +4 -3
  112. package/src/modes/components/user-message-selector.ts +94 -19
  113. package/src/modes/components/user-message.ts +8 -1
  114. package/src/modes/controllers/command-controller.ts +57 -0
  115. package/src/modes/controllers/event-controller.ts +60 -2
  116. package/src/modes/controllers/input-controller.ts +14 -11
  117. package/src/modes/controllers/omfg-controller.ts +283 -0
  118. package/src/modes/controllers/omfg-rule.ts +647 -0
  119. package/src/modes/controllers/selector-controller.ts +1 -0
  120. package/src/modes/gradient-highlight.ts +23 -6
  121. package/src/modes/interactive-mode.ts +41 -7
  122. package/src/modes/magic-keywords.ts +20 -0
  123. package/src/modes/markdown-prose.ts +247 -0
  124. package/src/modes/orchestrate.ts +17 -11
  125. package/src/modes/shared.ts +3 -11
  126. package/src/modes/turn-budget.ts +31 -0
  127. package/src/modes/types.ts +7 -1
  128. package/src/modes/ultrathink.ts +16 -10
  129. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  130. package/src/modes/workflow.ts +42 -0
  131. package/src/prompts/system/omfg-user.md +51 -0
  132. package/src/prompts/system/system-prompt.md +1 -0
  133. package/src/prompts/system/workflow-notice.md +70 -0
  134. package/src/prompts/tools/eval.md +13 -1
  135. package/src/prompts/tools/memory-edit.md +1 -1
  136. package/src/sdk.ts +63 -33
  137. package/src/session/agent-session.ts +373 -56
  138. package/src/session/session-manager.ts +32 -0
  139. package/src/session/session-storage.ts +68 -8
  140. package/src/session/shake-types.ts +44 -0
  141. package/src/slash-commands/builtin-registry.ts +41 -16
  142. package/src/task/executor.ts +3 -3
  143. package/src/task/index.ts +6 -6
  144. package/src/tiny/models.ts +30 -2
  145. package/src/tiny/title-protocol.ts +11 -1
  146. package/src/tiny/worker.ts +19 -7
  147. package/src/tools/eval.ts +202 -26
  148. package/src/tools/grouped-file-output.ts +9 -2
  149. package/src/tools/index.ts +17 -5
  150. package/src/tools/memory-edit.ts +4 -4
  151. package/src/tools/memory-recall.ts +5 -5
  152. package/src/tools/memory-reflect.ts +5 -5
  153. package/src/tools/memory-retain.ts +4 -4
  154. package/src/tools/render-utils.ts +2 -1
  155. package/src/tools/search.ts +480 -76
  156. package/dist/types/mnemosyne/backend.d.ts +0 -4
  157. package/dist/types/mnemosyne/config.d.ts +0 -29
  158. package/dist/types/mnemosyne/state.d.ts +0 -72
  159. /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
  160. /package/src/{mnemosyne → mnemopi}/index.ts +0 -0
@@ -33,20 +33,29 @@ 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 {
@@ -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,
@@ -148,10 +157,12 @@ import type { Goal, GoalModeState } from "../goals/state";
148
157
  import type { HindsightSessionState } from "../hindsight/state";
149
158
  import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
150
159
  import { resolveMemoryBackend } from "../memory-backend";
151
- import { getMnemosyneSessionState, type MnemosyneSessionState, setMnemosyneSessionState } from "../mnemosyne/state";
160
+ import { getMnemopiSessionState, type MnemopiSessionState, setMnemopiSessionState } from "../mnemopi/state";
152
161
  import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
153
162
  import { getCurrentThemeName, theme } from "../modes/theme/theme";
163
+ import { parseTurnBudget } from "../modes/turn-budget";
154
164
  import { containsUltrathink, ULTRATHINK_NOTICE } from "../modes/ultrathink";
165
+ import { containsWorkflow, WORKFLOW_NOTICE } from "../modes/workflow";
155
166
  import type { PlanModeState } from "../plan-mode/state";
156
167
  import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
157
168
  import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
@@ -174,7 +185,8 @@ import {
174
185
  resolveThinkingLevelForModel,
175
186
  toReasoningEffort,
176
187
  } from "../thinking";
177
- import { shutdownTinyTitleClient } from "../tiny/title-client";
188
+ import { isTinyMemoryLocalModelKey } from "../tiny/models";
189
+ import { shutdownTinyTitleClient, tinyModelClient } from "../tiny/title-client";
178
190
  import {
179
191
  buildDiscoverableToolSearchIndex,
180
192
  collectDiscoverableTools,
@@ -219,6 +231,7 @@ import type {
219
231
  SessionManager,
220
232
  } from "./session-manager";
221
233
  import { getLatestCompactionEntry } from "./session-manager";
234
+ import type { ShakeMode, ShakeResult } from "./shake-types";
222
235
  import { ToolChoiceQueue } from "./tool-choice-queue";
223
236
  import { YieldQueue } from "./yield-queue";
224
237
 
@@ -228,11 +241,11 @@ export type AgentSessionEvent =
228
241
  | {
229
242
  type: "auto_compaction_start";
230
243
  reason: "threshold" | "overflow" | "idle" | "incomplete";
231
- action: "context-full" | "handoff";
244
+ action: "context-full" | "handoff" | "shake" | "shake-summary";
232
245
  }
233
246
  | {
234
247
  type: "auto_compaction_end";
235
- action: "context-full" | "handoff";
248
+ action: "context-full" | "handoff" | "shake" | "shake-summary";
236
249
  result: CompactionResult | undefined;
237
250
  aborted: boolean;
238
251
  willRetry: boolean;
@@ -269,6 +282,8 @@ export interface AsyncJobSnapshot {
269
282
  delivery: AsyncJobDeliveryState;
270
283
  }
271
284
 
285
+ export type { ShakeMode, ShakeResult };
286
+
272
287
  // ============================================================================
273
288
  // Types
274
289
  // ============================================================================
@@ -1287,8 +1302,8 @@ export class AgentSession {
1287
1302
  return previous;
1288
1303
  }
1289
1304
 
1290
- getMnemosyneSessionState(): MnemosyneSessionState | undefined {
1291
- return getMnemosyneSessionState(this);
1305
+ getMnemopiSessionState(): MnemopiSessionState | undefined {
1306
+ return getMnemopiSessionState(this);
1292
1307
  }
1293
1308
 
1294
1309
  /** TTSR manager for time-traveling stream rules */
@@ -2838,11 +2853,11 @@ export class AgentSession {
2838
2853
  this.getHindsightSessionState()?.setSessionId(sid);
2839
2854
  }
2840
2855
 
2841
- #rekeyMnemosyneMemoryForCurrentSessionId(): void {
2842
- if (resolveMemoryBackend(this.settings).id !== "mnemosyne") return;
2856
+ #rekeyMnemopiMemoryForCurrentSessionId(): void {
2857
+ if (resolveMemoryBackend(this.settings).id !== "mnemopi") return;
2843
2858
  const sid = this.agent.sessionId;
2844
2859
  if (!sid) return;
2845
- this.getMnemosyneSessionState()?.setSessionId(sid);
2860
+ this.getMnemopiSessionState()?.setSessionId(sid);
2846
2861
  }
2847
2862
 
2848
2863
  /** New session file: reset auto-recall / retain-threshold counters for the new transcript. */
@@ -2853,9 +2868,9 @@ export class AgentSession {
2853
2868
  state.resetConversationTracking();
2854
2869
  }
2855
2870
 
2856
- #resetMnemosyneConversationTrackingIfMnemosyne(): void {
2857
- if (resolveMemoryBackend(this.settings).id !== "mnemosyne") return;
2858
- const state = this.getMnemosyneSessionState();
2871
+ #resetMnemopiConversationTrackingIfMnemopi(): void {
2872
+ if (resolveMemoryBackend(this.settings).id !== "mnemopi") return;
2873
+ const state = this.getMnemopiSessionState();
2859
2874
  if (!state || state.aliasOf) return;
2860
2875
  state.resetConversationTracking();
2861
2876
  }
@@ -2923,8 +2938,8 @@ export class AgentSession {
2923
2938
  const hindsightState = this.setHindsightSessionState(undefined);
2924
2939
  await hindsightState?.flushRetainQueue();
2925
2940
  hindsightState?.dispose();
2926
- const mnemosyneState = setMnemosyneSessionState(this, undefined);
2927
- mnemosyneState?.dispose();
2941
+ const mnemopiState = setMnemopiSessionState(this, undefined);
2942
+ mnemopiState?.dispose();
2928
2943
  this.#disconnectFromAgent();
2929
2944
  if (this.#unsubscribeAppendOnly) {
2930
2945
  this.#unsubscribeAppendOnly();
@@ -4101,6 +4116,8 @@ export class AgentSession {
4101
4116
  const keywordNotices: CustomMessage[] = [];
4102
4117
  if (!options?.synthetic) {
4103
4118
  const timestamp = Date.now();
4119
+ const turnBudget = parseTurnBudget(expandedText);
4120
+ this.sessionManager.beginTurnBudget(turnBudget?.total ?? null, turnBudget?.hard ?? false);
4104
4121
  if (containsUltrathink(expandedText)) {
4105
4122
  keywordNotices.push({
4106
4123
  role: "custom",
@@ -4121,6 +4138,16 @@ export class AgentSession {
4121
4138
  timestamp,
4122
4139
  });
4123
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
+ }
4124
4151
  }
4125
4152
 
4126
4153
  // If streaming, queue via steer() or followUp() based on option
@@ -4973,9 +5000,9 @@ export class AgentSession {
4973
5000
  this.setTodoPhases([]);
4974
5001
  this.#syncAgentSessionId();
4975
5002
  this.#rekeyHindsightMemoryForCurrentSessionId();
4976
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
5003
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
4977
5004
  this.#resetHindsightConversationTrackingIfHindsight();
4978
- this.#resetMnemosyneConversationTrackingIfMnemosyne();
5005
+ this.#resetMnemopiConversationTrackingIfMnemopi();
4979
5006
  this.#steeringMessages = [];
4980
5007
  this.#followUpMessages = [];
4981
5008
  this.#pendingNextTurnMessages = [];
@@ -5070,8 +5097,8 @@ export class AgentSession {
5070
5097
  // Update agent session ID
5071
5098
  this.#syncAgentSessionId();
5072
5099
  this.#rekeyHindsightMemoryForCurrentSessionId();
5073
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
5074
- this.#resetMnemosyneConversationTrackingIfMnemosyne();
5100
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
5101
+ this.#resetMnemopiConversationTrackingIfMnemopi();
5075
5102
 
5076
5103
  // Emit session_switch event with reason "fork" to hooks
5077
5104
  if (this.#extensionRunner) {
@@ -5091,13 +5118,13 @@ export class AgentSession {
5091
5118
 
5092
5119
  /**
5093
5120
  * Set model directly.
5094
- * Validates API key, saves to session and settings.
5121
+ * Validates API key and saves to the active session. Persists settings only when requested.
5095
5122
  * @throws Error if no API key available for the model
5096
5123
  */
5097
5124
  async setModel(
5098
5125
  model: Model,
5099
5126
  role: string = "default",
5100
- options?: { selector?: string; thinkingLevel?: ThinkingLevel },
5127
+ options?: { selector?: string; thinkingLevel?: ThinkingLevel; persist?: boolean },
5101
5128
  ): Promise<void> {
5102
5129
  const previousEditMode = this.#resolveActiveEditMode();
5103
5130
  const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
@@ -5108,10 +5135,12 @@ export class AgentSession {
5108
5135
  this.#clearActiveRetryFallback();
5109
5136
  this.#setModelWithProviderSessionReset(model);
5110
5137
  this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, role);
5111
- this.settings.setModelRole(
5112
- role,
5113
- this.#formatRoleModelValue(role, model, options?.selector, options?.thinkingLevel),
5114
- );
5138
+ if (options?.persist) {
5139
+ this.settings.setModelRole(
5140
+ role,
5141
+ this.#formatRoleModelValue(role, model, options.selector, options.thinkingLevel),
5142
+ );
5143
+ }
5115
5144
  this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
5116
5145
 
5117
5146
  // Re-apply thinking for the newly selected model. Prefer the model's
@@ -5214,9 +5243,8 @@ export class AgentSession {
5214
5243
  }
5215
5244
 
5216
5245
  /**
5217
- * Apply a resolved role model as the active model, persisting the choice to
5218
- * settings under its role. Mirrors the non-temporary branch of
5219
- * {@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.
5220
5248
  */
5221
5249
  async applyRoleModel(entry: ResolvedRoleModel): Promise<void> {
5222
5250
  await this.setModel(entry.model, entry.role);
@@ -5227,24 +5255,21 @@ export class AgentSession {
5227
5255
 
5228
5256
  /**
5229
5257
  * Cycle through configured role models in a fixed order.
5230
- * Skips missing roles.
5258
+ * Skips missing roles and changes only the active session model.
5231
5259
  * @param roleOrder - Order of roles to cycle through (e.g., ["slow", "default", "smol"])
5232
- * @param options - Optional settings: `temporary` to not persist to settings
5260
+ * @param direction - "forward" (default) or "backward"
5233
5261
  */
5234
5262
  async cycleRoleModels(
5235
5263
  roleOrder: readonly string[],
5236
- options?: { temporary?: boolean },
5264
+ direction: "forward" | "backward" = "forward",
5237
5265
  ): Promise<RoleModelCycleResult | undefined> {
5238
5266
  const cycle = this.getRoleModelCycle(roleOrder);
5239
5267
  if (!cycle || cycle.models.length <= 1) return undefined;
5240
5268
 
5241
- 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];
5242
5271
 
5243
- if (options?.temporary) {
5244
- await this.setModelTemporary(next.model, next.explicitThinkingLevel ? next.thinkingLevel : undefined);
5245
- } else {
5246
- await this.applyRoleModel(next);
5247
- }
5272
+ await this.applyRoleModel(next);
5248
5273
 
5249
5274
  return { model: next.model, thinkingLevel: this.thinkingLevel, role: next.role };
5250
5275
  }
@@ -5288,7 +5313,6 @@ export class AgentSession {
5288
5313
  this.#clearActiveRetryFallback();
5289
5314
  this.#setModelWithProviderSessionReset(next.model);
5290
5315
  this.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
5291
- this.settings.setModelRole("default", this.#formatRoleModelValue("default", next.model));
5292
5316
  this.settings.getStorage()?.recordModelUsage(`${next.model.provider}/${next.model.id}`);
5293
5317
 
5294
5318
  // Apply the scoped model's configured thinking level, preserving auto.
@@ -5319,7 +5343,6 @@ export class AgentSession {
5319
5343
  this.#clearActiveRetryFallback();
5320
5344
  this.#setModelWithProviderSessionReset(nextModel);
5321
5345
  this.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
5322
- this.settings.setModelRole("default", this.#formatRoleModelValue("default", nextModel));
5323
5346
  this.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
5324
5347
  // Re-apply the current thinking level (or auto) for the newly selected model
5325
5348
  this.#reapplyThinkingLevel();
@@ -5340,9 +5363,10 @@ export class AgentSession {
5340
5363
  // =========================================================================
5341
5364
 
5342
5365
  /**
5343
- * Set the thinking level. `auto` enables per-turn classification (session-level,
5344
- * never written to the session log); a concrete level clears auto. The effective
5345
- * metadata-clamped level is saved to the session/settings only when 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.
5346
5370
  */
5347
5371
  setThinkingLevel(level: ConfiguredThinkingLevel | undefined, persist: boolean = false): void {
5348
5372
  if (level === AUTO_THINKING) {
@@ -5454,9 +5478,13 @@ export class AgentSession {
5454
5478
 
5455
5479
  const effort = resolved ?? resolveProvisionalAutoLevel(model);
5456
5480
  if (effort === undefined) return;
5481
+ const shouldPersistResolution = this.#autoResolvedLevel !== effort;
5457
5482
  this.#autoResolvedLevel = effort;
5458
5483
  this.#thinkingLevel = effort;
5459
5484
  this.agent.setThinkingLevel(toReasoningEffort(effort));
5485
+ if (shouldPersistResolution) {
5486
+ this.sessionManager.appendThinkingLevelChange(effort);
5487
+ }
5460
5488
  this.#emit({
5461
5489
  type: "thinking_level_changed",
5462
5490
  thinkingLevel: effort,
@@ -5625,6 +5653,165 @@ export class AgentSession {
5625
5653
  return { removed };
5626
5654
  }
5627
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
+
5628
5815
  /**
5629
5816
  * Manually compact the session context.
5630
5817
  * Aborts current agent operation first.
@@ -5935,9 +6122,9 @@ export class AgentSession {
5935
6122
  this.agent.reset();
5936
6123
  this.#syncAgentSessionId();
5937
6124
  this.#rekeyHindsightMemoryForCurrentSessionId();
5938
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
6125
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
5939
6126
  this.#resetHindsightConversationTrackingIfHindsight();
5940
- this.#resetMnemosyneConversationTrackingIfMnemosyne();
6127
+ this.#resetMnemopiConversationTrackingIfMnemopi();
5941
6128
  this.#steeringMessages = [];
5942
6129
  this.#followUpMessages = [];
5943
6130
  this.#pendingNextTurnMessages = [];
@@ -6837,6 +7024,14 @@ export class AgentSession {
6837
7024
  if (compactionSettings.strategy === "off") return false;
6838
7025
  if (reason !== "idle" && !compactionSettings.enabled) return false;
6839
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
+ }
6840
7035
  // "overflow" and "incomplete" force inline execution because they are recovery
6841
7036
  // paths the caller wants resolved before scheduling the next turn. "idle" is
6842
7037
  // triggered by the idle loop and does its own scheduling.
@@ -7219,6 +7414,119 @@ export class AgentSession {
7219
7414
  return false;
7220
7415
  }
7221
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
+
7222
7530
  /**
7223
7531
  * Toggle auto-compaction setting.
7224
7532
  */
@@ -8181,6 +8489,7 @@ export class AgentSession {
8181
8489
  promptText: string;
8182
8490
  onTextDelta?: (delta: string) => void;
8183
8491
  signal?: AbortSignal;
8492
+ dedupeReply?: boolean;
8184
8493
  }): Promise<{ replyText: string; assistantMessage: AssistantMessage }> {
8185
8494
  const model = this.model;
8186
8495
  if (!model) {
@@ -8243,7 +8552,10 @@ export class AgentSession {
8243
8552
  if (!assistantMessage) {
8244
8553
  throw new Error("Ephemeral turn ended without a final message");
8245
8554
  }
8246
- return { replyText: dedupeIrcReply(replyText.trim()), assistantMessage };
8555
+ return {
8556
+ replyText: args.dedupeReply === false ? replyText.trim() : dedupeIrcReply(replyText.trim()),
8557
+ assistantMessage,
8558
+ };
8247
8559
  }
8248
8560
 
8249
8561
  /**
@@ -8412,7 +8724,7 @@ export class AgentSession {
8412
8724
  await this.sessionManager.setSessionFile(sessionPath);
8413
8725
  this.#syncAgentSessionId();
8414
8726
  this.#rekeyHindsightMemoryForCurrentSessionId();
8415
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
8727
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
8416
8728
 
8417
8729
  const sessionContext = this.buildDisplaySessionContext();
8418
8730
  const didReloadConversationChange =
@@ -8471,11 +8783,16 @@ export class AgentSession {
8471
8783
  .some(entry => entry.type === "service_tier_change");
8472
8784
  const defaultThinkingLevel = this.settings.get("defaultThinkingLevel");
8473
8785
  const configuredServiceTier = this.settings.get("serviceTier");
8474
- // Session log entries only ever store concrete levels (auto is never
8475
- // written), so `auto` can only arrive via the settings default.
8476
- const restoredThinkingLevel: ConfiguredThinkingLevel | undefined = hasThinkingEntry
8477
- ? (sessionContext.thinkingLevel as ThinkingLevel | undefined)
8478
- : defaultThinkingLevel;
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;
8479
8796
  if (restoredThinkingLevel === AUTO_THINKING) {
8480
8797
  this.#autoThinking = true;
8481
8798
  this.#autoResolvedLevel = undefined;
@@ -8494,7 +8811,7 @@ export class AgentSession {
8494
8811
 
8495
8812
  if (switchingToDifferentSession) {
8496
8813
  this.#resetHindsightConversationTrackingIfHindsight();
8497
- this.#resetMnemosyneConversationTrackingIfMnemosyne();
8814
+ this.#resetMnemopiConversationTrackingIfMnemopi();
8498
8815
  }
8499
8816
  this.#reconnectToAgent();
8500
8817
  return true;
@@ -8502,7 +8819,7 @@ export class AgentSession {
8502
8819
  this.sessionManager.restoreState(previousSessionState);
8503
8820
  this.#syncAgentSessionId(previousSessionState.sessionId);
8504
8821
  this.#rekeyHindsightMemoryForCurrentSessionId();
8505
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
8822
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
8506
8823
  let restoreMcpError: unknown;
8507
8824
  try {
8508
8825
  await this.#restoreMCPSelectionsForSessionContext(previousSessionContext, {
@@ -8600,9 +8917,9 @@ export class AgentSession {
8600
8917
  this.#syncTodoPhasesFromBranch();
8601
8918
  this.#syncAgentSessionId();
8602
8919
  this.#rekeyHindsightMemoryForCurrentSessionId();
8603
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
8920
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
8604
8921
  this.#resetHindsightConversationTrackingIfHindsight();
8605
- this.#resetMnemosyneConversationTrackingIfMnemosyne();
8922
+ this.#resetMnemopiConversationTrackingIfMnemopi();
8606
8923
 
8607
8924
  // Reload messages from entries (works for both file and in-memory mode)
8608
8925
  const sessionContext = this.buildDisplaySessionContext();
@@ -8983,7 +9300,7 @@ export class AgentSession {
8983
9300
  const msg = messages[i];
8984
9301
  if (msg.role === "assistant") {
8985
9302
  const assistantMsg = msg as AssistantMessage;
8986
- if (assistantMsg.usage) {
9303
+ if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) {
8987
9304
  lastUsage = assistantMsg.usage;
8988
9305
  lastUsageIndex = i;
8989
9306
  break;