@oh-my-pi/pi-coding-agent 15.7.2 → 15.7.5

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 (188) hide show
  1. package/CHANGELOG.md +114 -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 +69 -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/internal-urls/local-protocol.d.ts +19 -9
  27. package/dist/types/internal-urls/types.d.ts +14 -0
  28. package/dist/types/lsp/client.d.ts +3 -0
  29. package/dist/types/mcp/manager.d.ts +14 -5
  30. package/dist/types/memory-backend/index.d.ts +1 -1
  31. package/dist/types/memory-backend/resolve.d.ts +1 -1
  32. package/dist/types/memory-backend/types.d.ts +3 -3
  33. package/dist/types/mnemopi/backend.d.ts +4 -0
  34. package/dist/types/mnemopi/config.d.ts +29 -0
  35. package/dist/types/mnemopi/state.d.ts +72 -0
  36. package/dist/types/modes/components/custom-editor.d.ts +2 -2
  37. package/dist/types/modes/components/omfg-panel.d.ts +19 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +6 -0
  39. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  40. package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
  41. package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
  42. package/dist/types/modes/gradient-highlight.d.ts +5 -1
  43. package/dist/types/modes/interactive-mode.d.ts +7 -3
  44. package/dist/types/modes/magic-keywords.d.ts +14 -0
  45. package/dist/types/modes/markdown-prose.d.ts +27 -0
  46. package/dist/types/modes/orchestrate.d.ts +7 -2
  47. package/dist/types/modes/shared.d.ts +1 -1
  48. package/dist/types/modes/turn-budget.d.ts +18 -0
  49. package/dist/types/modes/types.d.ts +7 -3
  50. package/dist/types/modes/ultrathink.d.ts +7 -2
  51. package/dist/types/modes/workflow.d.ts +15 -0
  52. package/dist/types/sdk.d.ts +13 -3
  53. package/dist/types/session/agent-session.d.ts +36 -17
  54. package/dist/types/session/session-manager.d.ts +18 -0
  55. package/dist/types/session/session-storage.d.ts +6 -0
  56. package/dist/types/session/shake-types.d.ts +24 -0
  57. package/dist/types/task/executor.d.ts +2 -2
  58. package/dist/types/task/repair-args.d.ts +52 -0
  59. package/dist/types/tiny/models.d.ts +1 -1
  60. package/dist/types/tiny/title-client.d.ts +28 -2
  61. package/dist/types/tiny/title-protocol.d.ts +8 -5
  62. package/dist/types/tools/find.d.ts +1 -1
  63. package/dist/types/tools/index.d.ts +19 -3
  64. package/dist/types/tools/memory-edit.d.ts +1 -1
  65. package/dist/types/tools/path-utils.d.ts +7 -0
  66. package/dist/types/tui/output-block.d.ts +7 -7
  67. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  68. package/package.json +10 -10
  69. package/scripts/build-binary.ts +0 -1
  70. package/src/autoresearch/tools/run-experiment.ts +45 -113
  71. package/src/cli/args.ts +39 -16
  72. package/src/cli/extension-flags.ts +48 -0
  73. package/src/cli/plugin-cli.ts +11 -2
  74. package/src/cli.ts +59 -0
  75. package/src/config/config-file.ts +98 -13
  76. package/src/config/file-lock.ts +60 -17
  77. package/src/config/keybindings.ts +78 -27
  78. package/src/config/model-registry.ts +7 -1
  79. package/src/config/settings-schema.ts +73 -67
  80. package/src/config/settings.ts +22 -0
  81. package/src/edit/hashline/diff.ts +81 -24
  82. package/src/edit/renderer.ts +16 -12
  83. package/src/eval/__tests__/agent-bridge.test.ts +433 -0
  84. package/src/eval/__tests__/budget-bridge.test.ts +69 -0
  85. package/src/eval/__tests__/idle-timeout.test.ts +66 -0
  86. package/src/eval/__tests__/shared-executors.test.ts +21 -0
  87. package/src/eval/agent-bridge.ts +295 -0
  88. package/src/eval/backend.ts +17 -2
  89. package/src/eval/budget-bridge.ts +48 -0
  90. package/src/eval/idle-timeout.ts +80 -0
  91. package/src/eval/js/executor.ts +35 -7
  92. package/src/eval/js/index.ts +2 -1
  93. package/src/eval/js/shared/prelude.txt +85 -1
  94. package/src/eval/js/tool-bridge.ts +9 -0
  95. package/src/eval/py/executor.ts +41 -14
  96. package/src/eval/py/index.ts +2 -1
  97. package/src/eval/py/prelude.py +132 -1
  98. package/src/exec/bash-executor.ts +2 -3
  99. package/src/extensibility/custom-tools/types.ts +2 -2
  100. package/src/extensibility/extensions/runner.ts +12 -2
  101. package/src/extensibility/plugins/git-url.ts +90 -4
  102. package/src/extensibility/plugins/manager.ts +103 -7
  103. package/src/extensibility/shared-events.ts +2 -2
  104. package/src/internal-urls/docs-index.generated.ts +88 -88
  105. package/src/internal-urls/local-protocol.ts +23 -11
  106. package/src/internal-urls/types.ts +15 -0
  107. package/src/lsp/client.ts +28 -5
  108. package/src/main.ts +44 -55
  109. package/src/mcp/manager.ts +87 -4
  110. package/src/memory-backend/index.ts +1 -1
  111. package/src/memory-backend/resolve.ts +3 -3
  112. package/src/memory-backend/types.ts +3 -3
  113. package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
  114. package/src/{mnemosyne → mnemopi}/config.ts +36 -36
  115. package/src/{mnemosyne → mnemopi}/state.ts +67 -67
  116. package/src/modes/components/agent-dashboard.ts +6 -6
  117. package/src/modes/components/custom-editor.ts +4 -11
  118. package/src/modes/components/extensions/state-manager.ts +3 -4
  119. package/src/modes/components/footer.ts +8 -9
  120. package/src/modes/components/hook-selector.ts +86 -20
  121. package/src/modes/components/oauth-selector.ts +93 -21
  122. package/src/modes/components/omfg-panel.ts +141 -0
  123. package/src/modes/components/settings-defs.ts +2 -2
  124. package/src/modes/components/tips.txt +2 -1
  125. package/src/modes/components/tool-execution.ts +38 -19
  126. package/src/modes/components/tree-selector.ts +4 -3
  127. package/src/modes/components/user-message-selector.ts +94 -19
  128. package/src/modes/components/user-message.ts +8 -1
  129. package/src/modes/controllers/command-controller.ts +25 -0
  130. package/src/modes/controllers/event-controller.ts +68 -3
  131. package/src/modes/controllers/input-controller.ts +14 -11
  132. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  133. package/src/modes/controllers/omfg-controller.ts +283 -0
  134. package/src/modes/controllers/omfg-rule.ts +647 -0
  135. package/src/modes/controllers/selector-controller.ts +1 -0
  136. package/src/modes/gradient-highlight.ts +23 -6
  137. package/src/modes/interactive-mode.ts +41 -7
  138. package/src/modes/magic-keywords.ts +20 -0
  139. package/src/modes/markdown-prose.ts +247 -0
  140. package/src/modes/orchestrate.ts +17 -11
  141. package/src/modes/shared.ts +3 -11
  142. package/src/modes/turn-budget.ts +31 -0
  143. package/src/modes/types.ts +7 -1
  144. package/src/modes/ultrathink.ts +16 -10
  145. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  146. package/src/modes/workflow.ts +42 -0
  147. package/src/prompts/system/omfg-user.md +51 -0
  148. package/src/prompts/system/project-prompt.md +3 -2
  149. package/src/prompts/system/subagent-system-prompt.md +12 -8
  150. package/src/prompts/system/system-prompt.md +9 -6
  151. package/src/prompts/system/workflow-notice.md +70 -0
  152. package/src/prompts/tools/eval.md +13 -1
  153. package/src/prompts/tools/memory-edit.md +1 -1
  154. package/src/sdk.ts +63 -33
  155. package/src/session/agent-session.ts +290 -55
  156. package/src/session/session-manager.ts +32 -0
  157. package/src/session/session-storage.ts +68 -8
  158. package/src/session/shake-types.ts +43 -0
  159. package/src/slash-commands/builtin-registry.ts +39 -16
  160. package/src/task/executor.ts +17 -7
  161. package/src/task/index.ts +9 -8
  162. package/src/task/repair-args.ts +117 -0
  163. package/src/tiny/models.ts +2 -2
  164. package/src/tiny/title-client.ts +133 -43
  165. package/src/tiny/title-protocol.ts +10 -5
  166. package/src/tiny/worker.ts +3 -46
  167. package/src/tools/ast-edit.ts +3 -0
  168. package/src/tools/ast-grep.ts +3 -0
  169. package/src/tools/eval.ts +202 -26
  170. package/src/tools/find.ts +20 -6
  171. package/src/tools/gh.ts +1 -0
  172. package/src/tools/grouped-file-output.ts +9 -2
  173. package/src/tools/index.ts +17 -5
  174. package/src/tools/memory-edit.ts +4 -4
  175. package/src/tools/memory-recall.ts +5 -5
  176. package/src/tools/memory-reflect.ts +5 -5
  177. package/src/tools/memory-retain.ts +4 -4
  178. package/src/tools/path-utils.ts +13 -2
  179. package/src/tools/read.ts +1 -0
  180. package/src/tools/render-utils.ts +2 -1
  181. package/src/tools/search.ts +491 -76
  182. package/src/tui/output-block.ts +37 -75
  183. package/src/utils/git.ts +9 -3
  184. package/dist/types/mnemosyne/backend.d.ts +0 -4
  185. package/dist/types/mnemosyne/config.d.ts +0 -29
  186. package/dist/types/mnemosyne/state.d.ts +0 -72
  187. /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
  188. /package/src/{mnemosyne → mnemopi}/index.ts +0 -0
@@ -33,18 +33,24 @@ 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,
48
54
  type SummaryOptions,
49
55
  shouldCompact,
50
56
  } from "@oh-my-pi/pi-agent-core/compaction";
@@ -77,7 +83,7 @@ import {
77
83
  resolveServiceTier,
78
84
  streamSimple,
79
85
  } from "@oh-my-pi/pi-ai";
80
- import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
86
+ import { countTokens, MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
81
87
  import {
82
88
  extractRetryHint,
83
89
  getAgentDbPath,
@@ -148,10 +154,12 @@ import type { Goal, GoalModeState } from "../goals/state";
148
154
  import type { HindsightSessionState } from "../hindsight/state";
149
155
  import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
150
156
  import { resolveMemoryBackend } from "../memory-backend";
151
- import { getMnemosyneSessionState, type MnemosyneSessionState, setMnemosyneSessionState } from "../mnemosyne/state";
157
+ import { getMnemopiSessionState, type MnemopiSessionState, setMnemopiSessionState } from "../mnemopi/state";
152
158
  import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
153
159
  import { getCurrentThemeName, theme } from "../modes/theme/theme";
160
+ import { parseTurnBudget } from "../modes/turn-budget";
154
161
  import { containsUltrathink, ULTRATHINK_NOTICE } from "../modes/ultrathink";
162
+ import { containsWorkflow, WORKFLOW_NOTICE } from "../modes/workflow";
155
163
  import type { PlanModeState } from "../plan-mode/state";
156
164
  import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
157
165
  import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
@@ -219,6 +227,7 @@ import type {
219
227
  SessionManager,
220
228
  } from "./session-manager";
221
229
  import { getLatestCompactionEntry } from "./session-manager";
230
+ import type { ShakeMode, ShakeResult } from "./shake-types";
222
231
  import { ToolChoiceQueue } from "./tool-choice-queue";
223
232
  import { YieldQueue } from "./yield-queue";
224
233
 
@@ -228,11 +237,11 @@ export type AgentSessionEvent =
228
237
  | {
229
238
  type: "auto_compaction_start";
230
239
  reason: "threshold" | "overflow" | "idle" | "incomplete";
231
- action: "context-full" | "handoff";
240
+ action: "context-full" | "handoff" | "shake";
232
241
  }
233
242
  | {
234
243
  type: "auto_compaction_end";
235
- action: "context-full" | "handoff";
244
+ action: "context-full" | "handoff" | "shake";
236
245
  result: CompactionResult | undefined;
237
246
  aborted: boolean;
238
247
  willRetry: boolean;
@@ -269,6 +278,8 @@ export interface AsyncJobSnapshot {
269
278
  delivery: AsyncJobDeliveryState;
270
279
  }
271
280
 
281
+ export type { ShakeMode, ShakeResult };
282
+
272
283
  // ============================================================================
273
284
  // Types
274
285
  // ============================================================================
@@ -1287,8 +1298,8 @@ export class AgentSession {
1287
1298
  return previous;
1288
1299
  }
1289
1300
 
1290
- getMnemosyneSessionState(): MnemosyneSessionState | undefined {
1291
- return getMnemosyneSessionState(this);
1301
+ getMnemopiSessionState(): MnemopiSessionState | undefined {
1302
+ return getMnemopiSessionState(this);
1292
1303
  }
1293
1304
 
1294
1305
  /** TTSR manager for time-traveling stream rules */
@@ -2838,11 +2849,11 @@ export class AgentSession {
2838
2849
  this.getHindsightSessionState()?.setSessionId(sid);
2839
2850
  }
2840
2851
 
2841
- #rekeyMnemosyneMemoryForCurrentSessionId(): void {
2842
- if (resolveMemoryBackend(this.settings).id !== "mnemosyne") return;
2852
+ #rekeyMnemopiMemoryForCurrentSessionId(): void {
2853
+ if (resolveMemoryBackend(this.settings).id !== "mnemopi") return;
2843
2854
  const sid = this.agent.sessionId;
2844
2855
  if (!sid) return;
2845
- this.getMnemosyneSessionState()?.setSessionId(sid);
2856
+ this.getMnemopiSessionState()?.setSessionId(sid);
2846
2857
  }
2847
2858
 
2848
2859
  /** New session file: reset auto-recall / retain-threshold counters for the new transcript. */
@@ -2853,9 +2864,9 @@ export class AgentSession {
2853
2864
  state.resetConversationTracking();
2854
2865
  }
2855
2866
 
2856
- #resetMnemosyneConversationTrackingIfMnemosyne(): void {
2857
- if (resolveMemoryBackend(this.settings).id !== "mnemosyne") return;
2858
- const state = this.getMnemosyneSessionState();
2867
+ #resetMnemopiConversationTrackingIfMnemopi(): void {
2868
+ if (resolveMemoryBackend(this.settings).id !== "mnemopi") return;
2869
+ const state = this.getMnemopiSessionState();
2859
2870
  if (!state || state.aliasOf) return;
2860
2871
  state.resetConversationTracking();
2861
2872
  }
@@ -2923,8 +2934,8 @@ export class AgentSession {
2923
2934
  const hindsightState = this.setHindsightSessionState(undefined);
2924
2935
  await hindsightState?.flushRetainQueue();
2925
2936
  hindsightState?.dispose();
2926
- const mnemosyneState = setMnemosyneSessionState(this, undefined);
2927
- mnemosyneState?.dispose();
2937
+ const mnemopiState = setMnemopiSessionState(this, undefined);
2938
+ mnemopiState?.dispose();
2928
2939
  this.#disconnectFromAgent();
2929
2940
  if (this.#unsubscribeAppendOnly) {
2930
2941
  this.#unsubscribeAppendOnly();
@@ -4101,6 +4112,8 @@ export class AgentSession {
4101
4112
  const keywordNotices: CustomMessage[] = [];
4102
4113
  if (!options?.synthetic) {
4103
4114
  const timestamp = Date.now();
4115
+ const turnBudget = parseTurnBudget(expandedText);
4116
+ this.sessionManager.beginTurnBudget(turnBudget?.total ?? null, turnBudget?.hard ?? false);
4104
4117
  if (containsUltrathink(expandedText)) {
4105
4118
  keywordNotices.push({
4106
4119
  role: "custom",
@@ -4121,6 +4134,16 @@ export class AgentSession {
4121
4134
  timestamp,
4122
4135
  });
4123
4136
  }
4137
+ if (containsWorkflow(expandedText)) {
4138
+ keywordNotices.push({
4139
+ role: "custom",
4140
+ customType: "workflow-notice",
4141
+ content: WORKFLOW_NOTICE,
4142
+ display: false,
4143
+ attribution: "user",
4144
+ timestamp,
4145
+ });
4146
+ }
4124
4147
  }
4125
4148
 
4126
4149
  // If streaming, queue via steer() or followUp() based on option
@@ -4973,9 +4996,9 @@ export class AgentSession {
4973
4996
  this.setTodoPhases([]);
4974
4997
  this.#syncAgentSessionId();
4975
4998
  this.#rekeyHindsightMemoryForCurrentSessionId();
4976
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
4999
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
4977
5000
  this.#resetHindsightConversationTrackingIfHindsight();
4978
- this.#resetMnemosyneConversationTrackingIfMnemosyne();
5001
+ this.#resetMnemopiConversationTrackingIfMnemopi();
4979
5002
  this.#steeringMessages = [];
4980
5003
  this.#followUpMessages = [];
4981
5004
  this.#pendingNextTurnMessages = [];
@@ -5070,8 +5093,8 @@ export class AgentSession {
5070
5093
  // Update agent session ID
5071
5094
  this.#syncAgentSessionId();
5072
5095
  this.#rekeyHindsightMemoryForCurrentSessionId();
5073
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
5074
- this.#resetMnemosyneConversationTrackingIfMnemosyne();
5096
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
5097
+ this.#resetMnemopiConversationTrackingIfMnemopi();
5075
5098
 
5076
5099
  // Emit session_switch event with reason "fork" to hooks
5077
5100
  if (this.#extensionRunner) {
@@ -5091,13 +5114,13 @@ export class AgentSession {
5091
5114
 
5092
5115
  /**
5093
5116
  * Set model directly.
5094
- * Validates API key, saves to session and settings.
5117
+ * Validates API key and saves to the active session. Persists settings only when requested.
5095
5118
  * @throws Error if no API key available for the model
5096
5119
  */
5097
5120
  async setModel(
5098
5121
  model: Model,
5099
5122
  role: string = "default",
5100
- options?: { selector?: string; thinkingLevel?: ThinkingLevel },
5123
+ options?: { selector?: string; thinkingLevel?: ThinkingLevel; persist?: boolean },
5101
5124
  ): Promise<void> {
5102
5125
  const previousEditMode = this.#resolveActiveEditMode();
5103
5126
  const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
@@ -5108,10 +5131,12 @@ export class AgentSession {
5108
5131
  this.#clearActiveRetryFallback();
5109
5132
  this.#setModelWithProviderSessionReset(model);
5110
5133
  this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, role);
5111
- this.settings.setModelRole(
5112
- role,
5113
- this.#formatRoleModelValue(role, model, options?.selector, options?.thinkingLevel),
5114
- );
5134
+ if (options?.persist) {
5135
+ this.settings.setModelRole(
5136
+ role,
5137
+ this.#formatRoleModelValue(role, model, options.selector, options.thinkingLevel),
5138
+ );
5139
+ }
5115
5140
  this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
5116
5141
 
5117
5142
  // Re-apply thinking for the newly selected model. Prefer the model's
@@ -5214,9 +5239,8 @@ export class AgentSession {
5214
5239
  }
5215
5240
 
5216
5241
  /**
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.
5242
+ * Apply a resolved role model as the active model without changing global
5243
+ * settings. Shared with role cycling and the plan-approval model slider.
5220
5244
  */
5221
5245
  async applyRoleModel(entry: ResolvedRoleModel): Promise<void> {
5222
5246
  await this.setModel(entry.model, entry.role);
@@ -5227,24 +5251,21 @@ export class AgentSession {
5227
5251
 
5228
5252
  /**
5229
5253
  * Cycle through configured role models in a fixed order.
5230
- * Skips missing roles.
5254
+ * Skips missing roles and changes only the active session model.
5231
5255
  * @param roleOrder - Order of roles to cycle through (e.g., ["slow", "default", "smol"])
5232
- * @param options - Optional settings: `temporary` to not persist to settings
5256
+ * @param direction - "forward" (default) or "backward"
5233
5257
  */
5234
5258
  async cycleRoleModels(
5235
5259
  roleOrder: readonly string[],
5236
- options?: { temporary?: boolean },
5260
+ direction: "forward" | "backward" = "forward",
5237
5261
  ): Promise<RoleModelCycleResult | undefined> {
5238
5262
  const cycle = this.getRoleModelCycle(roleOrder);
5239
5263
  if (!cycle || cycle.models.length <= 1) return undefined;
5240
5264
 
5241
- const next = cycle.models[(cycle.currentIndex + 1) % cycle.models.length];
5265
+ const step = direction === "backward" ? -1 : 1;
5266
+ const next = cycle.models[(cycle.currentIndex + step + cycle.models.length) % cycle.models.length];
5242
5267
 
5243
- if (options?.temporary) {
5244
- await this.setModelTemporary(next.model, next.explicitThinkingLevel ? next.thinkingLevel : undefined);
5245
- } else {
5246
- await this.applyRoleModel(next);
5247
- }
5268
+ await this.applyRoleModel(next);
5248
5269
 
5249
5270
  return { model: next.model, thinkingLevel: this.thinkingLevel, role: next.role };
5250
5271
  }
@@ -5288,7 +5309,6 @@ export class AgentSession {
5288
5309
  this.#clearActiveRetryFallback();
5289
5310
  this.#setModelWithProviderSessionReset(next.model);
5290
5311
  this.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
5291
- this.settings.setModelRole("default", this.#formatRoleModelValue("default", next.model));
5292
5312
  this.settings.getStorage()?.recordModelUsage(`${next.model.provider}/${next.model.id}`);
5293
5313
 
5294
5314
  // Apply the scoped model's configured thinking level, preserving auto.
@@ -5319,7 +5339,6 @@ export class AgentSession {
5319
5339
  this.#clearActiveRetryFallback();
5320
5340
  this.#setModelWithProviderSessionReset(nextModel);
5321
5341
  this.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
5322
- this.settings.setModelRole("default", this.#formatRoleModelValue("default", nextModel));
5323
5342
  this.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
5324
5343
  // Re-apply the current thinking level (or auto) for the newly selected model
5325
5344
  this.#reapplyThinkingLevel();
@@ -5340,9 +5359,10 @@ export class AgentSession {
5340
5359
  // =========================================================================
5341
5360
 
5342
5361
  /**
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.
5362
+ * Set the thinking level. `auto` enables per-turn classification; the selector
5363
+ * itself is never written to the session log, but resolved concrete levels are
5364
+ * persisted when real user turns are classified so resumed sessions keep the
5365
+ * last resolved effort instead of reverting to pending auto.
5346
5366
  */
5347
5367
  setThinkingLevel(level: ConfiguredThinkingLevel | undefined, persist: boolean = false): void {
5348
5368
  if (level === AUTO_THINKING) {
@@ -5454,9 +5474,13 @@ export class AgentSession {
5454
5474
 
5455
5475
  const effort = resolved ?? resolveProvisionalAutoLevel(model);
5456
5476
  if (effort === undefined) return;
5477
+ const shouldPersistResolution = this.#autoResolvedLevel !== effort;
5457
5478
  this.#autoResolvedLevel = effort;
5458
5479
  this.#thinkingLevel = effort;
5459
5480
  this.agent.setThinkingLevel(toReasoningEffort(effort));
5481
+ if (shouldPersistResolution) {
5482
+ this.sessionManager.appendThinkingLevelChange(effort);
5483
+ }
5460
5484
  this.#emit({
5461
5485
  type: "thinking_level_changed",
5462
5486
  thinkingLevel: effort,
@@ -5625,6 +5649,89 @@ export class AgentSession {
5625
5649
  return { removed };
5626
5650
  }
5627
5651
 
5652
+ /**
5653
+ * Surgically reduce context by dropping heavy content ("shake").
5654
+ *
5655
+ * - `images` delegates to {@link dropImages}.
5656
+ * - `elide` replaces whole tool-call results and large fenced/XML blocks
5657
+ * with short placeholders that embed an `artifact://` recovery link.
5658
+ *
5659
+ * Mutates the branch in place, persists via `rewriteEntries`, replays the
5660
+ * rebuilt context through the agent, and tears down provider sessions that
5661
+ * cache message identity — same rewrite contract as {@link dropImages}.
5662
+ *
5663
+ * No-op (zero counts) when nothing is eligible.
5664
+ */
5665
+ async shake(mode: ShakeMode, opts: { config?: ShakeConfig; signal?: AbortSignal } = {}): Promise<ShakeResult> {
5666
+ if (mode === "images") {
5667
+ const { removed } = await this.dropImages();
5668
+ return { mode, toolResultsDropped: 0, blocksDropped: 0, imagesDropped: removed, tokensFreed: 0 };
5669
+ }
5670
+
5671
+ const config = opts.config ?? AGGRESSIVE_SHAKE_CONFIG;
5672
+ const regions = collectShakeRegions(this.sessionManager.getBranch(), config);
5673
+ if (regions.length === 0) {
5674
+ return { mode, toolResultsDropped: 0, blocksDropped: 0, tokensFreed: 0 };
5675
+ }
5676
+
5677
+ const artifactId = await this.#saveShakeArtifact(regions);
5678
+ const replacements = regions.map((region, index) => this.#shakeElidePlaceholder(region, index, artifactId));
5679
+
5680
+ let toolResultsDropped = 0;
5681
+ let blocksDropped = 0;
5682
+ let originalTokens = 0;
5683
+ let replacementTokens = 0;
5684
+ const items = regions.map((region, index) => {
5685
+ if (region.kind === "toolResult") toolResultsDropped++;
5686
+ else blocksDropped++;
5687
+ originalTokens += region.tokens;
5688
+ const replacement = replacements[index];
5689
+ if (replacement.length > 0) replacementTokens += countTokens(replacement);
5690
+ return { region, replacement };
5691
+ });
5692
+
5693
+ applyShakeRegions(items);
5694
+
5695
+ await this.sessionManager.rewriteEntries();
5696
+ const sessionContext = this.buildDisplaySessionContext();
5697
+ this.agent.replaceMessages(sessionContext.messages);
5698
+ this.#closeCodexProviderSessionsForHistoryRewrite();
5699
+
5700
+ return {
5701
+ mode,
5702
+ toolResultsDropped,
5703
+ blocksDropped,
5704
+ tokensFreed: Math.max(0, originalTokens - replacementTokens),
5705
+ artifactId,
5706
+ };
5707
+ }
5708
+
5709
+ #shakeElidePlaceholder(region: ShakeRegion, index: number, artifactId: string | undefined): string {
5710
+ if (artifactId) {
5711
+ return `[shaken ~${region.tokens} tokens — recover: artifact://${artifactId} (region ${index + 1})]`;
5712
+ }
5713
+ return `[shaken ~${region.tokens} tokens]`;
5714
+ }
5715
+
5716
+ /**
5717
+ * Concatenate the original region contents into one session artifact so the
5718
+ * agent can read them back via `artifact://<id>`. Returns `undefined` when
5719
+ * the session is not persisted or the write fails — callers degrade to a
5720
+ * bare placeholder.
5721
+ */
5722
+ async #saveShakeArtifact(regions: ShakeRegion[]): Promise<string | undefined> {
5723
+ const parts: string[] = [];
5724
+ for (let i = 0; i < regions.length; i++) {
5725
+ const region = regions[i];
5726
+ parts.push(`### region ${i + 1} (${region.label}, ~${region.tokens} tok)`, "", region.originalText, "");
5727
+ }
5728
+ try {
5729
+ return await this.sessionManager.saveArtifact(parts.join("\n"), "shake");
5730
+ } catch {
5731
+ return undefined;
5732
+ }
5733
+ }
5734
+
5628
5735
  /**
5629
5736
  * Manually compact the session context.
5630
5737
  * Aborts current agent operation first.
@@ -5935,9 +6042,9 @@ export class AgentSession {
5935
6042
  this.agent.reset();
5936
6043
  this.#syncAgentSessionId();
5937
6044
  this.#rekeyHindsightMemoryForCurrentSessionId();
5938
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
6045
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
5939
6046
  this.#resetHindsightConversationTrackingIfHindsight();
5940
- this.#resetMnemosyneConversationTrackingIfMnemosyne();
6047
+ this.#resetMnemopiConversationTrackingIfMnemopi();
5941
6048
  this.#steeringMessages = [];
5942
6049
  this.#followUpMessages = [];
5943
6050
  this.#pendingNextTurnMessages = [];
@@ -6837,6 +6944,14 @@ export class AgentSession {
6837
6944
  if (compactionSettings.strategy === "off") return false;
6838
6945
  if (reason !== "idle" && !compactionSettings.enabled) return false;
6839
6946
  const generation = this.#promptGeneration;
6947
+
6948
+ // Shake runs inline (cheap, no remote LLM). On overflow recovery, if shake
6949
+ // reclaims nothing we fall through to the summary-compaction body below so
6950
+ // the oversized input still gets resolved.
6951
+ if (compactionSettings.strategy === "shake") {
6952
+ const outcome = await this.#runAutoShake(reason, willRetry, generation);
6953
+ if (outcome !== "fallback") return false;
6954
+ }
6840
6955
  // "overflow" and "incomplete" force inline execution because they are recovery
6841
6956
  // paths the caller wants resolved before scheduling the next turn. "idle" is
6842
6957
  // triggered by the idle loop and does its own scheduling.
@@ -7219,6 +7334,117 @@ export class AgentSession {
7219
7334
  return false;
7220
7335
  }
7221
7336
 
7337
+ /**
7338
+ * Run a shake-strategy auto-maintenance pass. Emits the
7339
+ * `auto_compaction_start`/`auto_compaction_end` pair with a shake `action`,
7340
+ * runs {@link shake} inline against the protect-window config, and schedules
7341
+ * continuation exactly like the context-full tail.
7342
+ *
7343
+ * Returns `"fallback"` only for an overflow recovery where shake reclaimed
7344
+ * nothing (or threw) — the caller then runs the summary-compaction body so
7345
+ * the oversized input still gets resolved. Returns `"handled"` otherwise.
7346
+ */
7347
+ async #runAutoShake(
7348
+ reason: "overflow" | "threshold" | "idle" | "incomplete",
7349
+ willRetry: boolean,
7350
+ generation: number,
7351
+ ): Promise<"handled" | "fallback"> {
7352
+ const action = "shake";
7353
+ await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
7354
+ this.#autoCompactionAbortController?.abort();
7355
+ const controller = new AbortController();
7356
+ this.#autoCompactionAbortController = controller;
7357
+ const signal = controller.signal;
7358
+ const compactionSettings = this.settings.getGroup("compaction");
7359
+ try {
7360
+ const result = await this.shake("elide", { config: DEFAULT_SHAKE_CONFIG, signal });
7361
+ if (signal.aborted) {
7362
+ await this.#emitSessionEvent({
7363
+ type: "auto_compaction_end",
7364
+ action,
7365
+ result: undefined,
7366
+ aborted: true,
7367
+ willRetry: false,
7368
+ });
7369
+ return "handled";
7370
+ }
7371
+ const reclaimed = result.toolResultsDropped + result.blocksDropped > 0;
7372
+ // Overflow needs the input to actually shrink before the retry; if shake
7373
+ // reclaimed nothing, summarization is the only remaining recovery.
7374
+ if (reason === "overflow" && !reclaimed) {
7375
+ await this.#emitSessionEvent({
7376
+ type: "auto_compaction_end",
7377
+ action,
7378
+ result: undefined,
7379
+ aborted: false,
7380
+ willRetry: false,
7381
+ skipped: true,
7382
+ });
7383
+ return "fallback";
7384
+ }
7385
+ await this.#emitSessionEvent({
7386
+ type: "auto_compaction_end",
7387
+ action,
7388
+ result: undefined,
7389
+ aborted: false,
7390
+ willRetry,
7391
+ skipped: !reclaimed,
7392
+ });
7393
+
7394
+ if (!willRetry && reason !== "idle" && compactionSettings.autoContinue !== false) {
7395
+ this.#scheduleAutoContinuePrompt(generation);
7396
+ }
7397
+ if (willRetry) {
7398
+ // The shake rebuild replays every entry, so a trailing error/length
7399
+ // assistant from the failed turn re-enters agent state — drop it before
7400
+ // retrying, same as the context-full tail.
7401
+ const messages = this.agent.state.messages;
7402
+ const lastMsg = messages[messages.length - 1];
7403
+ if (lastMsg?.role === "assistant") {
7404
+ const lastAssistant = lastMsg as AssistantMessage;
7405
+ const shouldDrop =
7406
+ lastAssistant.stopReason === "error" ||
7407
+ (reason === "incomplete" && lastAssistant.stopReason === "length");
7408
+ if (shouldDrop) this.agent.replaceMessages(messages.slice(0, -1));
7409
+ }
7410
+ this.#scheduleAgentContinue({ delayMs: 100, generation });
7411
+ } else if (this.agent.hasQueuedMessages()) {
7412
+ this.#scheduleAgentContinue({
7413
+ delayMs: 100,
7414
+ generation,
7415
+ shouldContinue: () => this.agent.hasQueuedMessages(),
7416
+ });
7417
+ }
7418
+ return "handled";
7419
+ } catch (error) {
7420
+ if (signal.aborted) {
7421
+ await this.#emitSessionEvent({
7422
+ type: "auto_compaction_end",
7423
+ action,
7424
+ result: undefined,
7425
+ aborted: true,
7426
+ willRetry: false,
7427
+ });
7428
+ return "handled";
7429
+ }
7430
+ const message = error instanceof Error ? error.message : "shake failed";
7431
+ await this.#emitSessionEvent({
7432
+ type: "auto_compaction_end",
7433
+ action,
7434
+ result: undefined,
7435
+ aborted: false,
7436
+ willRetry: false,
7437
+ errorMessage: `Auto-shake failed: ${message}`,
7438
+ });
7439
+ // Overflow still needs recovery even if shake threw.
7440
+ return reason === "overflow" ? "fallback" : "handled";
7441
+ } finally {
7442
+ if (this.#autoCompactionAbortController === controller) {
7443
+ this.#autoCompactionAbortController = undefined;
7444
+ }
7445
+ }
7446
+ }
7447
+
7222
7448
  /**
7223
7449
  * Toggle auto-compaction setting.
7224
7450
  */
@@ -8181,6 +8407,7 @@ export class AgentSession {
8181
8407
  promptText: string;
8182
8408
  onTextDelta?: (delta: string) => void;
8183
8409
  signal?: AbortSignal;
8410
+ dedupeReply?: boolean;
8184
8411
  }): Promise<{ replyText: string; assistantMessage: AssistantMessage }> {
8185
8412
  const model = this.model;
8186
8413
  if (!model) {
@@ -8243,7 +8470,10 @@ export class AgentSession {
8243
8470
  if (!assistantMessage) {
8244
8471
  throw new Error("Ephemeral turn ended without a final message");
8245
8472
  }
8246
- return { replyText: dedupeIrcReply(replyText.trim()), assistantMessage };
8473
+ return {
8474
+ replyText: args.dedupeReply === false ? replyText.trim() : dedupeIrcReply(replyText.trim()),
8475
+ assistantMessage,
8476
+ };
8247
8477
  }
8248
8478
 
8249
8479
  /**
@@ -8412,7 +8642,7 @@ export class AgentSession {
8412
8642
  await this.sessionManager.setSessionFile(sessionPath);
8413
8643
  this.#syncAgentSessionId();
8414
8644
  this.#rekeyHindsightMemoryForCurrentSessionId();
8415
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
8645
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
8416
8646
 
8417
8647
  const sessionContext = this.buildDisplaySessionContext();
8418
8648
  const didReloadConversationChange =
@@ -8471,11 +8701,16 @@ export class AgentSession {
8471
8701
  .some(entry => entry.type === "service_tier_change");
8472
8702
  const defaultThinkingLevel = this.settings.get("defaultThinkingLevel");
8473
8703
  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;
8704
+ // Session log entries store only concrete levels. When `auto` has resolved
8705
+ // for a turn, the persisted context may already carry that concrete level
8706
+ // even if the branch scan races a just-flushed thinking entry under isolated
8707
+ // parallel test workers. Prefer the concrete context value in that case;
8708
+ // otherwise keep the configured `auto` selector so fresh sessions still
8709
+ // classify their first turn.
8710
+ const restoredThinkingLevel: ConfiguredThinkingLevel | undefined =
8711
+ hasThinkingEntry || (defaultThinkingLevel === AUTO_THINKING && sessionContext.thinkingLevel !== "off")
8712
+ ? (sessionContext.thinkingLevel as ThinkingLevel | undefined)
8713
+ : defaultThinkingLevel;
8479
8714
  if (restoredThinkingLevel === AUTO_THINKING) {
8480
8715
  this.#autoThinking = true;
8481
8716
  this.#autoResolvedLevel = undefined;
@@ -8494,7 +8729,7 @@ export class AgentSession {
8494
8729
 
8495
8730
  if (switchingToDifferentSession) {
8496
8731
  this.#resetHindsightConversationTrackingIfHindsight();
8497
- this.#resetMnemosyneConversationTrackingIfMnemosyne();
8732
+ this.#resetMnemopiConversationTrackingIfMnemopi();
8498
8733
  }
8499
8734
  this.#reconnectToAgent();
8500
8735
  return true;
@@ -8502,7 +8737,7 @@ export class AgentSession {
8502
8737
  this.sessionManager.restoreState(previousSessionState);
8503
8738
  this.#syncAgentSessionId(previousSessionState.sessionId);
8504
8739
  this.#rekeyHindsightMemoryForCurrentSessionId();
8505
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
8740
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
8506
8741
  let restoreMcpError: unknown;
8507
8742
  try {
8508
8743
  await this.#restoreMCPSelectionsForSessionContext(previousSessionContext, {
@@ -8600,9 +8835,9 @@ export class AgentSession {
8600
8835
  this.#syncTodoPhasesFromBranch();
8601
8836
  this.#syncAgentSessionId();
8602
8837
  this.#rekeyHindsightMemoryForCurrentSessionId();
8603
- this.#rekeyMnemosyneMemoryForCurrentSessionId();
8838
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
8604
8839
  this.#resetHindsightConversationTrackingIfHindsight();
8605
- this.#resetMnemosyneConversationTrackingIfMnemosyne();
8840
+ this.#resetMnemopiConversationTrackingIfMnemopi();
8606
8841
 
8607
8842
  // Reload messages from entries (works for both file and in-memory mode)
8608
8843
  const sessionContext = this.buildDisplaySessionContext();
@@ -8983,7 +9218,7 @@ export class AgentSession {
8983
9218
  const msg = messages[i];
8984
9219
  if (msg.role === "assistant") {
8985
9220
  const assistantMsg = msg as AssistantMessage;
8986
- if (assistantMsg.usage) {
9221
+ if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) {
8987
9222
  lastUsage = assistantMsg.usage;
8988
9223
  lastUsageIndex = i;
8989
9224
  break;
@@ -1837,6 +1837,12 @@ export class SessionManager {
1837
1837
  premiumRequests: 0,
1838
1838
  cost: 0,
1839
1839
  } satisfies UsageStatistics;
1840
+ /** Per-turn output-token budget set by a `+Nk` directive (total null when none this turn). */
1841
+ #turnBudget: { total: number | null; hard: boolean } = { total: null, hard: false };
1842
+ /** Cumulative `output` snapshot captured when the current turn budget window opened. */
1843
+ #turnBaselineOutput = 0;
1844
+ /** Output tokens consumed by eval-spawned subagents in the current turn window. */
1845
+ #turnEvalOutput = 0;
1840
1846
  #persistWriter: NdjsonFileWriter | undefined;
1841
1847
  #persistWriterPath: string | undefined;
1842
1848
  #persistChain: Promise<void> = Promise.resolve();
@@ -2397,6 +2403,32 @@ export class SessionManager {
2397
2403
  return this.#usageStatistics;
2398
2404
  }
2399
2405
 
2406
+ /**
2407
+ * Open a new per-turn budget window: snapshot the cumulative output baseline,
2408
+ * reset the eval-subagent counter, and set the (optional) ceiling. Called once
2409
+ * per real user message; `total` is null when no `+Nk` directive was present.
2410
+ */
2411
+ beginTurnBudget(total: number | null, hard: boolean): void {
2412
+ this.#turnBudget = { total, hard };
2413
+ this.#turnBaselineOutput = this.#usageStatistics.output;
2414
+ this.#turnEvalOutput = 0;
2415
+ }
2416
+
2417
+ /** Record output tokens consumed by an eval-spawned subagent in the current turn. */
2418
+ recordEvalSubagentOutput(output: number): void {
2419
+ if (Number.isFinite(output) && output > 0) this.#turnEvalOutput += output;
2420
+ }
2421
+
2422
+ /**
2423
+ * Current turn budget for the eval `budget` helper: the ceiling (null = none),
2424
+ * output tokens spent this turn (main loop + eval-spawned subagents, no
2425
+ * double-count), and whether the ceiling is hard.
2426
+ */
2427
+ getTurnBudget(): { total: number | null; spent: number; hard: boolean } {
2428
+ const mainDelta = Math.max(0, this.#usageStatistics.output - this.#turnBaselineOutput);
2429
+ return { total: this.#turnBudget.total, spent: mainDelta + this.#turnEvalOutput, hard: this.#turnBudget.hard };
2430
+ }
2431
+
2400
2432
  getSessionDir(): string {
2401
2433
  return this.sessionDir;
2402
2434
  }