@oh-my-pi/pi-coding-agent 15.5.13 → 15.6.0

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 (192) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/types/cli/classify-install-target.d.ts +0 -10
  3. package/dist/types/cli/initial-message.d.ts +1 -1
  4. package/dist/types/cli/tiny-models-cli.d.ts +9 -0
  5. package/dist/types/commands/tiny-models.d.ts +22 -0
  6. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  7. package/dist/types/commit/analysis/summary.d.ts +1 -1
  8. package/dist/types/commit/changelog/generate.d.ts +1 -1
  9. package/dist/types/commit/changelog/index.d.ts +2 -2
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  12. package/dist/types/config/model-id-affixes.d.ts +10 -0
  13. package/dist/types/config/model-registry.d.ts +1 -1
  14. package/dist/types/config/models-config-schema.d.ts +2 -0
  15. package/dist/types/config/settings-schema.d.ts +233 -17
  16. package/dist/types/discovery/helpers.d.ts +1 -1
  17. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  18. package/dist/types/eval/__tests__/llm-bridge.test.d.ts +1 -0
  19. package/dist/types/eval/js/shared/rewrite-imports.d.ts +16 -1
  20. package/dist/types/eval/llm-bridge.d.ts +25 -0
  21. package/dist/types/export/html/template.generated.d.ts +1 -1
  22. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +15 -0
  23. package/dist/types/internal-urls/agent-protocol.d.ts +2 -1
  24. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -1
  25. package/dist/types/internal-urls/local-protocol.d.ts +2 -1
  26. package/dist/types/internal-urls/memory-protocol.d.ts +2 -1
  27. package/dist/types/internal-urls/omp-protocol.d.ts +2 -1
  28. package/dist/types/internal-urls/router.d.ts +8 -1
  29. package/dist/types/internal-urls/rule-protocol.d.ts +2 -1
  30. package/dist/types/internal-urls/skill-protocol.d.ts +2 -1
  31. package/dist/types/internal-urls/types.d.ts +26 -0
  32. package/dist/types/memory-backend/index.d.ts +1 -0
  33. package/dist/types/memory-backend/resolve.d.ts +2 -1
  34. package/dist/types/memory-backend/types.d.ts +7 -1
  35. package/dist/types/mnemosyne/backend.d.ts +4 -0
  36. package/dist/types/mnemosyne/config.d.ts +29 -0
  37. package/dist/types/mnemosyne/index.d.ts +3 -0
  38. package/dist/types/mnemosyne/state.d.ts +72 -0
  39. package/dist/types/modes/components/custom-editor.d.ts +2 -3
  40. package/dist/types/modes/components/hook-selector.d.ts +27 -0
  41. package/dist/types/modes/components/index.d.ts +1 -0
  42. package/dist/types/modes/components/status-line/context-thresholds.d.ts +6 -0
  43. package/dist/types/modes/components/tiny-title-download-progress.d.ts +11 -0
  44. package/dist/types/modes/components/welcome.d.ts +1 -0
  45. package/dist/types/modes/controllers/extension-ui-controller.d.ts +4 -1
  46. package/dist/types/modes/gradient-highlight.d.ts +23 -0
  47. package/dist/types/modes/interactive-mode.d.ts +4 -2
  48. package/dist/types/modes/internal-url-autocomplete.d.ts +43 -0
  49. package/dist/types/modes/orchestrate.d.ts +10 -0
  50. package/dist/types/modes/theme/defaults/index.d.ts +8406 -8406
  51. package/dist/types/modes/theme/theme.d.ts +2 -1
  52. package/dist/types/modes/ultrathink.d.ts +3 -3
  53. package/dist/types/modes/utils/keybinding-matchers.d.ts +5 -0
  54. package/dist/types/sdk.d.ts +3 -0
  55. package/dist/types/session/agent-session.d.ts +35 -0
  56. package/dist/types/system-prompt.d.ts +2 -0
  57. package/dist/types/task/executor.d.ts +2 -0
  58. package/dist/types/task/render.d.ts +5 -1
  59. package/dist/types/tiny/models.d.ts +185 -0
  60. package/dist/types/tiny/text.d.ts +4 -0
  61. package/dist/types/tiny/title-client.d.ts +24 -0
  62. package/dist/types/tiny/title-protocol.d.ts +74 -0
  63. package/dist/types/tiny/worker.d.ts +2 -0
  64. package/dist/types/tools/bash.d.ts +3 -1
  65. package/dist/types/tools/index.d.ts +7 -4
  66. package/dist/types/tools/memory-edit.d.ts +40 -0
  67. package/dist/types/tools/{hindsight-recall.d.ts → memory-recall.d.ts} +6 -6
  68. package/dist/types/tools/{hindsight-reflect.d.ts → memory-reflect.d.ts} +6 -6
  69. package/dist/types/tools/memory-render.d.ts +60 -0
  70. package/dist/types/tools/{hindsight-retain.d.ts → memory-retain.d.ts} +6 -6
  71. package/dist/types/tools/todo-write.d.ts +8 -0
  72. package/dist/types/tools/tool-result.d.ts +2 -0
  73. package/dist/types/utils/title-generator.d.ts +3 -0
  74. package/package.json +18 -14
  75. package/scripts/build-binary.ts +1 -0
  76. package/src/cli/tiny-models-cli.ts +127 -0
  77. package/src/cli-commands.ts +1 -0
  78. package/src/cli.ts +8 -8
  79. package/src/commands/tiny-models.ts +36 -0
  80. package/src/config/model-equivalence.ts +43 -2
  81. package/src/config/model-id-affixes.ts +64 -0
  82. package/src/config/model-registry.ts +166 -8
  83. package/src/config/models-config-schema.ts +1 -1
  84. package/src/config/settings-schema.ts +206 -14
  85. package/src/edit/hashline/diff.ts +5 -7
  86. package/src/eval/__tests__/llm-bridge.test.ts +297 -0
  87. package/src/eval/__tests__/shared-executors.test.ts +36 -0
  88. package/src/eval/js/shared/local-module-loader.ts +13 -1
  89. package/src/eval/js/shared/prelude.txt +8 -0
  90. package/src/eval/js/shared/rewrite-imports.ts +31 -26
  91. package/src/eval/js/tool-bridge.ts +4 -0
  92. package/src/eval/llm-bridge.ts +181 -0
  93. package/src/eval/py/prelude.py +52 -31
  94. package/src/export/html/template.generated.ts +1 -1
  95. package/src/export/html/template.js +0 -13
  96. package/src/extensibility/plugins/legacy-pi-compat.ts +60 -23
  97. package/src/internal-urls/agent-protocol.ts +18 -1
  98. package/src/internal-urls/artifact-protocol.ts +19 -1
  99. package/src/internal-urls/docs-index.generated.ts +5 -4
  100. package/src/internal-urls/local-protocol.ts +14 -1
  101. package/src/internal-urls/memory-protocol.ts +6 -1
  102. package/src/internal-urls/omp-protocol.ts +5 -1
  103. package/src/internal-urls/router.ts +20 -1
  104. package/src/internal-urls/rule-protocol.ts +8 -1
  105. package/src/internal-urls/skill-protocol.ts +8 -1
  106. package/src/internal-urls/types.ts +27 -0
  107. package/src/lsp/render.ts +1 -1
  108. package/src/main.ts +4 -0
  109. package/src/mcp/oauth-flow.ts +2 -2
  110. package/src/memory-backend/index.ts +1 -0
  111. package/src/memory-backend/resolve.ts +4 -1
  112. package/src/memory-backend/types.ts +8 -1
  113. package/src/mnemosyne/backend.ts +374 -0
  114. package/src/mnemosyne/config.ts +160 -0
  115. package/src/mnemosyne/index.ts +3 -0
  116. package/src/mnemosyne/state.ts +548 -0
  117. package/src/modes/acp/acp-agent.ts +11 -6
  118. package/src/modes/components/agent-dashboard.ts +4 -4
  119. package/src/modes/components/custom-editor.ts +3 -2
  120. package/src/modes/components/diff.ts +2 -2
  121. package/src/modes/components/extensions/extension-list.ts +3 -2
  122. package/src/modes/components/footer.ts +5 -6
  123. package/src/modes/components/history-search.ts +3 -3
  124. package/src/modes/components/hook-selector.ts +94 -8
  125. package/src/modes/components/index.ts +1 -0
  126. package/src/modes/components/mcp-add-wizard.ts +3 -3
  127. package/src/modes/components/model-selector.ts +124 -26
  128. package/src/modes/components/oauth-selector.ts +3 -3
  129. package/src/modes/components/session-observer-overlay.ts +19 -13
  130. package/src/modes/components/session-selector.ts +3 -3
  131. package/src/modes/components/settings-defs.ts +7 -0
  132. package/src/modes/components/status-line/context-thresholds.ts +11 -0
  133. package/src/modes/components/status-line/presets.ts +1 -0
  134. package/src/modes/components/status-line/segments.ts +25 -2
  135. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  136. package/src/modes/components/tips.txt +12 -0
  137. package/src/modes/components/tool-execution.ts +67 -3
  138. package/src/modes/components/tree-selector.ts +3 -3
  139. package/src/modes/components/user-message-selector.ts +3 -3
  140. package/src/modes/components/welcome.ts +55 -1
  141. package/src/modes/controllers/command-controller.ts +16 -1
  142. package/src/modes/controllers/extension-ui-controller.ts +3 -1
  143. package/src/modes/controllers/input-controller.ts +57 -0
  144. package/src/modes/gradient-highlight.ts +70 -0
  145. package/src/modes/interactive-mode.ts +80 -196
  146. package/src/modes/internal-url-autocomplete.ts +143 -0
  147. package/src/modes/orchestrate.ts +36 -0
  148. package/src/modes/prompt-action-autocomplete.ts +12 -0
  149. package/src/modes/theme/theme.ts +7 -0
  150. package/src/modes/ultrathink.ts +9 -53
  151. package/src/modes/utils/keybinding-matchers.ts +11 -0
  152. package/src/prompts/system/memory-consolidation-system.md +8 -0
  153. package/src/prompts/system/memory-extraction-system.md +26 -0
  154. package/src/prompts/{commands/orchestrate.md → system/orchestrate-notice.md} +5 -16
  155. package/src/prompts/system/system-prompt.md +2 -0
  156. package/src/prompts/system/tiny-title-system.md +8 -0
  157. package/src/prompts/tools/eval.md +2 -0
  158. package/src/prompts/tools/memory-edit.md +8 -0
  159. package/src/prompts/tools/task.md +4 -7
  160. package/src/sdk.ts +8 -6
  161. package/src/session/agent-session.ts +147 -44
  162. package/src/session/session-manager.ts +47 -0
  163. package/src/slash-commands/builtin-registry.ts +10 -1
  164. package/src/system-prompt.ts +4 -0
  165. package/src/task/commands.ts +1 -5
  166. package/src/task/executor.ts +8 -0
  167. package/src/task/index.ts +2 -0
  168. package/src/task/render.ts +69 -26
  169. package/src/tiny/models.ts +217 -0
  170. package/src/tiny/text.ts +19 -0
  171. package/src/tiny/title-client.ts +340 -0
  172. package/src/tiny/title-protocol.ts +51 -0
  173. package/src/tiny/worker.ts +523 -0
  174. package/src/tools/bash.ts +58 -16
  175. package/src/tools/browser/tab-worker.ts +1 -1
  176. package/src/tools/eval.ts +24 -48
  177. package/src/tools/index.ts +17 -15
  178. package/src/tools/memory-edit.ts +59 -0
  179. package/src/tools/memory-recall.ts +100 -0
  180. package/src/tools/memory-reflect.ts +88 -0
  181. package/src/tools/memory-render.ts +185 -0
  182. package/src/tools/memory-retain.ts +91 -0
  183. package/src/tools/renderers.ts +4 -2
  184. package/src/tools/todo-write.ts +128 -29
  185. package/src/tools/tool-result.ts +8 -0
  186. package/src/utils/title-generator.ts +115 -13
  187. package/dist/types/tools/calculator.d.ts +0 -77
  188. package/src/prompts/tools/calculator.md +0 -10
  189. package/src/tools/calculator.ts +0 -541
  190. package/src/tools/hindsight-recall.ts +0 -69
  191. package/src/tools/hindsight-reflect.ts +0 -58
  192. package/src/tools/hindsight-retain.ts +0 -57
@@ -147,6 +147,8 @@ import type { Goal, GoalModeState } from "../goals/state";
147
147
  import type { HindsightSessionState } from "../hindsight/state";
148
148
  import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
149
149
  import { resolveMemoryBackend } from "../memory-backend";
150
+ import { getMnemosyneSessionState, type MnemosyneSessionState, setMnemosyneSessionState } from "../mnemosyne/state";
151
+ import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
150
152
  import { getCurrentThemeName, theme } from "../modes/theme/theme";
151
153
  import { containsUltrathink, ULTRATHINK_NOTICE } from "../modes/ultrathink";
152
154
  import type { PlanModeState } from "../plan-mode/state";
@@ -164,6 +166,7 @@ import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
164
166
  import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
165
167
  import { invalidateHostMetadata } from "../ssh/connection-manager";
166
168
  import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
169
+ import { shutdownTinyTitleClient } from "../tiny/title-client";
167
170
  import {
168
171
  buildDiscoverableToolSearchIndex,
169
172
  collectDiscoverableTools,
@@ -385,6 +388,22 @@ export interface RoleModelCycleResult {
385
388
  role: string;
386
389
  }
387
390
 
391
+ /** A configured role resolved to a concrete model, used by role cycling and
392
+ * the plan-approval model slider. */
393
+ export interface ResolvedRoleModel {
394
+ role: string;
395
+ model: Model;
396
+ thinkingLevel?: ThinkingLevel;
397
+ explicitThinkingLevel: boolean;
398
+ }
399
+
400
+ /** The set of resolvable role models plus the index of the currently active
401
+ * one within {@link ResolvedRoleModel.role} order. */
402
+ export interface RoleModelCycle {
403
+ models: ResolvedRoleModel[];
404
+ currentIndex: number;
405
+ }
406
+
388
407
  /** Session statistics for /session command */
389
408
  export interface SessionStats {
390
409
  sessionFile: string | undefined;
@@ -453,6 +472,15 @@ function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): strin
453
472
  }
454
473
 
455
474
  const IRC_REPLY_MAX_BYTES = 4096;
475
+ export const ANTHROPIC_TOOL_CALL_BATCH_CAP = 4;
476
+ const CLAUDE_OPUS_4_8_MODEL_ID = /(?:^|[./_-])claude-opus-4[.-]8\b/i;
477
+
478
+ export function resolveToolCallBatchCapForModel(model: Model | undefined): number | undefined {
479
+ if (!model) return undefined;
480
+ return model.provider === "anthropic" && CLAUDE_OPUS_4_8_MODEL_ID.test(model.id)
481
+ ? ANTHROPIC_TOOL_CALL_BATCH_CAP
482
+ : undefined;
483
+ }
456
484
 
457
485
  /**
458
486
  * Collapse degenerate IRC ephemeral replies before they hit the relay.
@@ -993,6 +1021,10 @@ export class AgentSession {
993
1021
  this.#flushPendingAgentEnd();
994
1022
  }
995
1023
 
1024
+ #syncToolCallBatchCap(model: Model | undefined = this.model): void {
1025
+ this.agent.maxToolCallsPerTurn = resolveToolCallBatchCapForModel(model);
1026
+ }
1027
+
996
1028
  #flushPendingAgentEnd(): void {
997
1029
  const pending = this.#pendingAgentEndEmit;
998
1030
  if (!pending) return;
@@ -1097,6 +1129,7 @@ export class AgentSession {
1097
1129
  this.#agentId = config.agentId;
1098
1130
  this.#agentRegistry = config.agentRegistry;
1099
1131
  this.#providerSessionId = config.providerSessionId;
1132
+ this.#syncToolCallBatchCap();
1100
1133
  this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
1101
1134
  const event: AgentEvent = {
1102
1135
  type: "message_update",
@@ -1226,6 +1259,10 @@ export class AgentSession {
1226
1259
  return previous;
1227
1260
  }
1228
1261
 
1262
+ getMnemosyneSessionState(): MnemosyneSessionState | undefined {
1263
+ return getMnemosyneSessionState(this);
1264
+ }
1265
+
1229
1266
  /** TTSR manager for time-traveling stream rules */
1230
1267
  get ttsrManager(): TtsrManager | undefined {
1231
1268
  return this.#ttsrManager;
@@ -2773,6 +2810,13 @@ export class AgentSession {
2773
2810
  this.getHindsightSessionState()?.setSessionId(sid);
2774
2811
  }
2775
2812
 
2813
+ #rekeyMnemosyneMemoryForCurrentSessionId(): void {
2814
+ if (resolveMemoryBackend(this.settings).id !== "mnemosyne") return;
2815
+ const sid = this.agent.sessionId;
2816
+ if (!sid) return;
2817
+ this.getMnemosyneSessionState()?.setSessionId(sid);
2818
+ }
2819
+
2776
2820
  /** New session file: reset auto-recall / retain-threshold counters for the new transcript. */
2777
2821
  #resetHindsightConversationTrackingIfHindsight(): void {
2778
2822
  if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
@@ -2781,6 +2825,13 @@ export class AgentSession {
2781
2825
  state.resetConversationTracking();
2782
2826
  }
2783
2827
 
2828
+ #resetMnemosyneConversationTrackingIfMnemosyne(): void {
2829
+ if (resolveMemoryBackend(this.settings).id !== "mnemosyne") return;
2830
+ const state = this.getMnemosyneSessionState();
2831
+ if (!state || state.aliasOf) return;
2832
+ state.resetConversationTracking();
2833
+ }
2834
+
2784
2835
  /**
2785
2836
  * Remove all listeners, flush pending writes, and disconnect from agent.
2786
2837
  * Call this when completely done with the session.
@@ -2837,12 +2888,15 @@ export class AgentSession {
2837
2888
  );
2838
2889
  }
2839
2890
  await disposeKernelSessionsByOwner(this.#evalKernelOwnerId);
2891
+ await shutdownTinyTitleClient();
2840
2892
  this.#releasePowerAssertion();
2841
2893
  await this.sessionManager.close();
2842
2894
  this.#closeAllProviderSessions("dispose");
2843
2895
  const hindsightState = this.setHindsightSessionState(undefined);
2844
2896
  await hindsightState?.flushRetainQueue();
2845
2897
  hindsightState?.dispose();
2898
+ const mnemosyneState = setMnemosyneSessionState(this, undefined);
2899
+ mnemosyneState?.dispose();
2846
2900
  this.#disconnectFromAgent();
2847
2901
  if (this.#unsubscribeAppendOnly) {
2848
2902
  this.#unsubscribeAppendOnly();
@@ -3998,20 +4052,33 @@ export class AgentSession {
3998
4052
  // Expand file-based prompt templates if requested
3999
4053
  const expandedText = expandPromptTemplates ? expandPromptTemplate(text, [...this.#promptTemplates]) : text;
4000
4054
 
4001
- // "ultrathink" keyword: nudge the model toward careful multi-step reasoning by
4002
- // appending a hidden notice after the user's message. User-authored prompts only —
4003
- // synthetic/agent-initiated turns never trigger it.
4004
- const ultrathinkNotice: CustomMessage | undefined =
4005
- !options?.synthetic && containsUltrathink(expandedText)
4006
- ? {
4007
- role: "custom",
4008
- customType: "ultrathink-notice",
4009
- content: ULTRATHINK_NOTICE,
4010
- display: false,
4011
- attribution: "user",
4012
- timestamp: Date.now(),
4013
- }
4014
- : undefined;
4055
+ // Magic keywords ("ultrathink", "orchestrate"): append hidden system notices after the
4056
+ // user's message that steer this turn. User-authored prompts only — synthetic /
4057
+ // agent-initiated turns never trigger them.
4058
+ const keywordNotices: CustomMessage[] = [];
4059
+ if (!options?.synthetic) {
4060
+ const timestamp = Date.now();
4061
+ if (containsUltrathink(expandedText)) {
4062
+ keywordNotices.push({
4063
+ role: "custom",
4064
+ customType: "ultrathink-notice",
4065
+ content: ULTRATHINK_NOTICE,
4066
+ display: false,
4067
+ attribution: "user",
4068
+ timestamp,
4069
+ });
4070
+ }
4071
+ if (containsOrchestrate(expandedText)) {
4072
+ keywordNotices.push({
4073
+ role: "custom",
4074
+ customType: "orchestrate-notice",
4075
+ content: ORCHESTRATE_NOTICE,
4076
+ display: false,
4077
+ attribution: "user",
4078
+ timestamp,
4079
+ });
4080
+ }
4081
+ }
4015
4082
 
4016
4083
  // If streaming, queue via steer() or followUp() based on option
4017
4084
  if (this.isStreaming) {
@@ -4023,9 +4090,9 @@ export class AgentSession {
4023
4090
  } else {
4024
4091
  await this.#queueSteer(expandedText, options?.images);
4025
4092
  }
4026
- // Steer/follow-up the ultrathink notice alongside the queued user message.
4027
- if (ultrathinkNotice) {
4028
- await this.sendCustomMessage(ultrathinkNotice, { deliverAs: options.streamingBehavior });
4093
+ // Steer/follow-up the keyword notices alongside the queued user message.
4094
+ for (const notice of keywordNotices) {
4095
+ await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
4029
4096
  }
4030
4097
  return;
4031
4098
  }
@@ -4055,7 +4122,7 @@ export class AgentSession {
4055
4122
  await this.#promptWithMessage(message, expandedText, {
4056
4123
  ...options,
4057
4124
  prependMessages: eagerTodoPrelude ? [eagerTodoPrelude.message] : undefined,
4058
- appendMessages: ultrathinkNotice ? [ultrathinkNotice] : undefined,
4125
+ appendMessages: keywordNotices.length > 0 ? keywordNotices : undefined,
4059
4126
  });
4060
4127
  } finally {
4061
4128
  // Clean up residual eager-todo directive if the prompt never consumed it
@@ -4852,7 +4919,9 @@ export class AgentSession {
4852
4919
  this.setTodoPhases([]);
4853
4920
  this.#syncAgentSessionId();
4854
4921
  this.#rekeyHindsightMemoryForCurrentSessionId();
4922
+ this.#rekeyMnemosyneMemoryForCurrentSessionId();
4855
4923
  this.#resetHindsightConversationTrackingIfHindsight();
4924
+ this.#resetMnemosyneConversationTrackingIfMnemosyne();
4856
4925
  this.#steeringMessages = [];
4857
4926
  this.#followUpMessages = [];
4858
4927
  this.#pendingNextTurnMessages = [];
@@ -4947,6 +5016,8 @@ export class AgentSession {
4947
5016
  // Update agent session ID
4948
5017
  this.#syncAgentSessionId();
4949
5018
  this.#rekeyHindsightMemoryForCurrentSessionId();
5019
+ this.#rekeyMnemosyneMemoryForCurrentSessionId();
5020
+ this.#resetMnemosyneConversationTrackingIfMnemosyne();
4950
5021
 
4951
5022
  // Emit session_switch event with reason "fork" to hooks
4952
5023
  if (this.#extensionRunner) {
@@ -5032,27 +5103,23 @@ export class AgentSession {
5032
5103
  }
5033
5104
 
5034
5105
  /**
5035
- * Cycle through configured role models in a fixed order.
5036
- * Skips missing roles.
5037
- * @param roleOrder - Order of roles to cycle through (e.g., ["slow", "default", "smol"])
5038
- * @param options - Optional settings: `temporary` to not persist to settings
5106
+ * Resolve the configured role models in the given order plus the index of
5107
+ * the currently active one. Roles that have no configured model, or whose
5108
+ * configured model is not currently available, are skipped. The `default`
5109
+ * role falls back to the active model when no explicit assignment exists.
5110
+ *
5111
+ * Returns `undefined` only when there is no current model or no available
5112
+ * models at all; an empty `models` array is never returned (callers should
5113
+ * still guard on `models.length`).
5039
5114
  */
5040
- async cycleRoleModels(
5041
- roleOrder: readonly string[],
5042
- options?: { temporary?: boolean },
5043
- ): Promise<RoleModelCycleResult | undefined> {
5115
+ getRoleModelCycle(roleOrder: readonly string[]): RoleModelCycle | undefined {
5044
5116
  const availableModels = this.#modelRegistry.getAvailable();
5045
5117
  if (availableModels.length === 0) return undefined;
5046
5118
 
5047
5119
  const currentModel = this.model;
5048
5120
  if (!currentModel) return undefined;
5049
5121
  const matchPreferences = { usageOrder: this.settings.getStorage()?.getModelUsageOrder() };
5050
- const roleModels: Array<{
5051
- role: string;
5052
- model: Model;
5053
- thinkingLevel?: ThinkingLevel;
5054
- explicitThinkingLevel: boolean;
5055
- }> = [];
5122
+ const models: ResolvedRoleModel[] = [];
5056
5123
 
5057
5124
  for (const role of roleOrder) {
5058
5125
  const roleModelStr =
@@ -5068,7 +5135,7 @@ export class AgentSession {
5068
5135
  });
5069
5136
  if (!resolved.model) continue;
5070
5137
 
5071
- roleModels.push({
5138
+ models.push({
5072
5139
  role,
5073
5140
  model: resolved.model,
5074
5141
  thinkingLevel: resolved.thinkingLevel,
@@ -5076,25 +5143,49 @@ export class AgentSession {
5076
5143
  });
5077
5144
  }
5078
5145
 
5079
- if (roleModels.length <= 1) return undefined;
5146
+ if (models.length === 0) return undefined;
5080
5147
 
5081
5148
  const lastRole = this.sessionManager.getLastModelChangeRole();
5082
- let currentIndex = lastRole ? roleModels.findIndex(entry => entry.role === lastRole) : -1;
5149
+ let currentIndex = lastRole ? models.findIndex(entry => entry.role === lastRole) : -1;
5083
5150
  if (currentIndex === -1) {
5084
- currentIndex = roleModels.findIndex(entry => modelsAreEqual(entry.model, currentModel));
5151
+ currentIndex = models.findIndex(entry => modelsAreEqual(entry.model, currentModel));
5085
5152
  }
5086
5153
  if (currentIndex === -1) currentIndex = 0;
5087
5154
 
5088
- const nextIndex = (currentIndex + 1) % roleModels.length;
5089
- const next = roleModels[nextIndex];
5155
+ return { models, currentIndex };
5156
+ }
5157
+
5158
+ /**
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.
5162
+ */
5163
+ async applyRoleModel(entry: ResolvedRoleModel): Promise<void> {
5164
+ await this.setModel(entry.model, entry.role);
5165
+ if (entry.explicitThinkingLevel && entry.thinkingLevel !== undefined) {
5166
+ this.setThinkingLevel(entry.thinkingLevel);
5167
+ }
5168
+ }
5169
+
5170
+ /**
5171
+ * Cycle through configured role models in a fixed order.
5172
+ * Skips missing roles.
5173
+ * @param roleOrder - Order of roles to cycle through (e.g., ["slow", "default", "smol"])
5174
+ * @param options - Optional settings: `temporary` to not persist to settings
5175
+ */
5176
+ async cycleRoleModels(
5177
+ roleOrder: readonly string[],
5178
+ options?: { temporary?: boolean },
5179
+ ): Promise<RoleModelCycleResult | undefined> {
5180
+ const cycle = this.getRoleModelCycle(roleOrder);
5181
+ if (!cycle || cycle.models.length <= 1) return undefined;
5182
+
5183
+ const next = cycle.models[(cycle.currentIndex + 1) % cycle.models.length];
5090
5184
 
5091
5185
  if (options?.temporary) {
5092
5186
  await this.setModelTemporary(next.model, next.explicitThinkingLevel ? next.thinkingLevel : undefined);
5093
5187
  } else {
5094
- await this.setModel(next.model, next.role);
5095
- if (next.explicitThinkingLevel && next.thinkingLevel !== undefined) {
5096
- this.setThinkingLevel(next.thinkingLevel);
5097
- }
5188
+ await this.applyRoleModel(next);
5098
5189
  }
5099
5190
 
5100
5191
  return { model: next.model, thinkingLevel: this.thinkingLevel, role: next.role };
@@ -5698,7 +5789,9 @@ export class AgentSession {
5698
5789
  this.agent.reset();
5699
5790
  this.#syncAgentSessionId();
5700
5791
  this.#rekeyHindsightMemoryForCurrentSessionId();
5792
+ this.#rekeyMnemosyneMemoryForCurrentSessionId();
5701
5793
  this.#resetHindsightConversationTrackingIfHindsight();
5794
+ this.#resetMnemosyneConversationTrackingIfMnemosyne();
5702
5795
  this.#steeringMessages = [];
5703
5796
  this.#followUpMessages = [];
5704
5797
  this.#pendingNextTurnMessages = [];
@@ -6162,6 +6255,7 @@ export class AgentSession {
6162
6255
  this.#closeProviderSessionsForModelSwitch(currentModel, model);
6163
6256
  }
6164
6257
  this.agent.setModel(model);
6258
+ this.#syncToolCallBatchCap(model);
6165
6259
 
6166
6260
  // Re-evaluate append-only context mode — provider or setting may have changed
6167
6261
  this.#syncAppendOnlyContext(model);
@@ -6169,7 +6263,7 @@ export class AgentSession {
6169
6263
 
6170
6264
  #closeCodexProviderSessionsForHistoryRewrite(): void {
6171
6265
  const currentModel = this.model;
6172
- if (!currentModel || currentModel.api !== "openai-codex-responses") return;
6266
+ if (currentModel?.api !== "openai-codex-responses") return;
6173
6267
  this.#closeProviderSessionsForModelSwitch(currentModel, currentModel);
6174
6268
  }
6175
6269
 
@@ -8168,6 +8262,7 @@ export class AgentSession {
8168
8262
  await this.sessionManager.setSessionFile(sessionPath);
8169
8263
  this.#syncAgentSessionId();
8170
8264
  this.#rekeyHindsightMemoryForCurrentSessionId();
8265
+ this.#rekeyMnemosyneMemoryForCurrentSessionId();
8171
8266
 
8172
8267
  const sessionContext = this.buildDisplaySessionContext();
8173
8268
  const didReloadConversationChange =
@@ -8214,6 +8309,7 @@ export class AgentSession {
8214
8309
  this.#setModelWithProviderSessionReset(match);
8215
8310
  } else {
8216
8311
  this.agent.setModel(match);
8312
+ this.#syncToolCallBatchCap(match);
8217
8313
  }
8218
8314
  }
8219
8315
  }
@@ -8239,6 +8335,7 @@ export class AgentSession {
8239
8335
 
8240
8336
  if (switchingToDifferentSession) {
8241
8337
  this.#resetHindsightConversationTrackingIfHindsight();
8338
+ this.#resetMnemosyneConversationTrackingIfMnemosyne();
8242
8339
  }
8243
8340
  this.#reconnectToAgent();
8244
8341
  return true;
@@ -8246,6 +8343,7 @@ export class AgentSession {
8246
8343
  this.sessionManager.restoreState(previousSessionState);
8247
8344
  this.#syncAgentSessionId(previousSessionState.sessionId);
8248
8345
  this.#rekeyHindsightMemoryForCurrentSessionId();
8346
+ this.#rekeyMnemosyneMemoryForCurrentSessionId();
8249
8347
  let restoreMcpError: unknown;
8250
8348
  try {
8251
8349
  await this.#restoreMCPSelectionsForSessionContext(previousSessionContext, {
@@ -8272,6 +8370,9 @@ export class AgentSession {
8272
8370
  this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
8273
8371
  if (previousModel) {
8274
8372
  this.agent.setModel(previousModel);
8373
+ this.#syncToolCallBatchCap(previousModel);
8374
+ } else {
8375
+ this.#syncToolCallBatchCap(undefined);
8275
8376
  }
8276
8377
  this.#thinkingLevel = previousThinkingLevel;
8277
8378
  this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
@@ -8301,7 +8402,7 @@ export class AgentSession {
8301
8402
  const previousSessionFile = this.sessionFile;
8302
8403
  const selectedEntry = this.sessionManager.getEntry(entryId);
8303
8404
 
8304
- if (!selectedEntry || selectedEntry.type !== "message" || selectedEntry.message.role !== "user") {
8405
+ if (selectedEntry?.type !== "message" || selectedEntry.message.role !== "user") {
8305
8406
  throw new Error("Invalid entry ID for branching");
8306
8407
  }
8307
8408
 
@@ -8338,7 +8439,9 @@ export class AgentSession {
8338
8439
  this.#syncTodoPhasesFromBranch();
8339
8440
  this.#syncAgentSessionId();
8340
8441
  this.#rekeyHindsightMemoryForCurrentSessionId();
8442
+ this.#rekeyMnemosyneMemoryForCurrentSessionId();
8341
8443
  this.#resetHindsightConversationTrackingIfHindsight();
8444
+ this.#resetMnemosyneConversationTrackingIfMnemosyne();
8342
8445
 
8343
8446
  // Reload messages from entries (works for both file and in-memory mode)
8344
8447
  const sessionContext = this.buildDisplaySessionContext();
@@ -702,6 +702,53 @@ export function buildSessionContext(
702
702
  }
703
703
  }
704
704
 
705
+ // Strip dangling tool_use blocks — a tool_use with no matching tool_result on the
706
+ // resolved leaf→root path — from ANY assistant turn, not just the trailing one.
707
+ // This happens whenever the leaf (or a branch point) lands such that an assistant
708
+ // turn's tool results are off the selected path: its result children live on a
709
+ // sibling branch, or it is the leaf itself (results are children below it). Left
710
+ // in place, `transformMessages` fabricates one synthetic "aborted"/"No result
711
+ // provided" result per dangling call plus a `<turn-aborted>` developer note, which
712
+ // render as phantom failed calls and re-inject the failed batch into the model's
713
+ // context — the rewind/restore loop.
714
+ //
715
+ // Stripping is necessary but not sufficient: a *modified* assistant turn that still
716
+ // carries signed `thinking`/`redacted_thinking` is rejected by Anthropic — "thinking
717
+ // blocks in the latest assistant message cannot be modified", and signed thinking
718
+ // replayed out of its original turn shape can also fail signature validation (this
719
+ // bites the handoff/branch-summary request). So when we rewrite a turn we also
720
+ // neutralize its protected reasoning: drop `redactedThinking` (encrypted, no
721
+ // plaintext to keep) and clear `thinking` signatures so the provider encoder
722
+ // downgrades them to plain text (verified accepted by the live API), preserving the
723
+ // visible reasoning while removing the immutability/invalid-signature hazard. Drop a
724
+ // turn left with no content. (Live turns never qualify: their results are persisted
725
+ // on the same path before any context rebuild.)
726
+ const pairedToolResultIds = new Set<string>();
727
+ for (const message of messages) {
728
+ if (message.role === "toolResult") pairedToolResultIds.add(message.toolCallId);
729
+ }
730
+ for (let i = messages.length - 1; i >= 0; i--) {
731
+ const message = messages[i];
732
+ if (message.role !== "assistant") continue;
733
+ const hasDangling = message.content.some(
734
+ block => block.type === "toolCall" && !pairedToolResultIds.has(block.id),
735
+ );
736
+ if (!hasDangling) continue;
737
+ const normalized = message.content
738
+ .filter(
739
+ block =>
740
+ !(block.type === "toolCall" && !pairedToolResultIds.has(block.id)) && block.type !== "redactedThinking",
741
+ )
742
+ .map(block =>
743
+ block.type === "thinking" && block.thinkingSignature ? { ...block, thinkingSignature: undefined } : block,
744
+ );
745
+ if (normalized.length === 0) {
746
+ messages.splice(i, 1);
747
+ } else {
748
+ messages[i] = { ...message, content: normalized };
749
+ }
750
+ }
751
+
705
752
  return {
706
753
  messages,
707
754
  thinkingLevel,
@@ -891,6 +891,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
891
891
  acpInputHint: "<subcommand>",
892
892
  subcommands: [
893
893
  { name: "view", description: "Show current memory injection payload" },
894
+ { name: "stats", description: "Show memory backend statistics" },
895
+ { name: "diagnose", description: "Run memory backend diagnostics" },
894
896
  { name: "clear", description: "Clear persisted memory data and artifacts" },
895
897
  { name: "reset", description: "Alias for clear" },
896
898
  { name: "enqueue", description: "Enqueue memory consolidation maintenance" },
@@ -933,13 +935,20 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
933
935
  await runtime.output("Memory consolidation enqueued.");
934
936
  return commandConsumed();
935
937
  }
938
+ case "stats":
939
+ case "diagnose": {
940
+ const hook = verb === "stats" ? backend.stats : backend.diagnose;
941
+ const payload = await hook?.(runtime.settings.getAgentDir(), runtime.cwd, runtime.session);
942
+ await runtime.output(payload ?? `Memory ${verb} is not available for the ${backend.id} backend.`);
943
+ return commandConsumed();
944
+ }
936
945
  case "mm":
937
946
  return usage(
938
947
  "Mental-model maintenance via /memory mm is unsupported in ACP mode; use the hindsight HTTP API directly.",
939
948
  runtime,
940
949
  );
941
950
  default:
942
- return usage("Usage: /memory <view|clear|reset|enqueue|rebuild>", runtime);
951
+ return usage("Usage: /memory <view|stats|diagnose|clear|reset|enqueue|rebuild>", runtime);
943
952
  }
944
953
  },
945
954
  handleTui: async (command, runtime) => {
@@ -361,6 +361,8 @@ export interface BuildSystemPromptOptions {
361
361
  secretsEnabled?: boolean;
362
362
  /** Pre-loaded workspace tree (skips discovery if provided). May be a Promise to allow early kick-off. */
363
363
  workspaceTree?: WorkspaceTree | Promise<WorkspaceTree>;
364
+ /** Whether the local memory://root summary is active. */
365
+ memoryRootEnabled?: boolean;
364
366
  }
365
367
 
366
368
  /** Result of building provider-facing system prompt messages. */
@@ -393,6 +395,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
393
395
  eagerTasks = false,
394
396
  secretsEnabled = false,
395
397
  workspaceTree: providedWorkspaceTree,
398
+ memoryRootEnabled = false,
396
399
  } = options;
397
400
  const resolvedCwd = cwd ?? getProjectDir();
398
401
 
@@ -570,6 +573,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
570
573
  mcpDiscoveryServerSummaries,
571
574
  eagerTasks,
572
575
  secretsEnabled,
576
+ hasMemoryRoot: memoryRootEnabled,
573
577
  hasObsidian: hasObsidian(),
574
578
  };
575
579
  const rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
@@ -9,12 +9,8 @@ import { type SlashCommand, slashCommandCapability } from "../capability/slash-c
9
9
  import { loadCapability } from "../discovery";
10
10
  // Embed command markdown files at build time
11
11
  import initMd from "../prompts/agents/init.md" with { type: "text" };
12
- import orchestrateMd from "../prompts/commands/orchestrate.md" with { type: "text" };
13
12
 
14
- const EMBEDDED_COMMANDS: { name: string; content: string }[] = [
15
- { name: "init.md", content: prompt.render(initMd) },
16
- { name: "orchestrate.md", content: prompt.render(orchestrateMd) },
17
- ];
13
+ const EMBEDDED_COMMANDS: { name: string; content: string }[] = [{ name: "init.md", content: prompt.render(initMd) }];
18
14
 
19
15
  export const EMBEDDED_COMMAND_TEMPLATES: ReadonlyArray<{ name: string; content: string }> = EMBEDDED_COMMANDS;
20
16
 
@@ -21,6 +21,7 @@ import type { HindsightSessionState } from "../hindsight/state";
21
21
  import type { LocalProtocolOptions } from "../internal-urls";
22
22
  import { callTool } from "../mcp/client";
23
23
  import type { MCPManager } from "../mcp/manager";
24
+ import type { MnemosyneSessionState } from "../mnemosyne/state";
24
25
  import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
25
26
  import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md" with { type: "text" };
26
27
  import { AgentRegistry } from "../registry/agent-registry";
@@ -185,6 +186,7 @@ export interface ExecutorOptions {
185
186
  */
186
187
  parentArtifactManager?: ArtifactManager;
187
188
  parentHindsightSessionState?: HindsightSessionState;
189
+ parentMnemosyneSessionState?: MnemosyneSessionState;
188
190
  /** Parent agent's eval executor session id. Subagents reuse it so eval state is shared. */
189
191
  parentEvalSessionId?: string;
190
192
  /**
@@ -631,6 +633,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
631
633
  if (atMaxDepth && toolNames?.includes("task")) {
632
634
  toolNames = toolNames.filter(name => name !== "task");
633
635
  }
636
+ // IRC is always available; the [COOP] prompt advertises it, so a restricted
637
+ // whitelist must still carry `irc` for the subagent to actually use it.
638
+ if (toolNames && !toolNames.includes("irc")) {
639
+ toolNames = [...toolNames, "irc"];
640
+ }
634
641
  if (toolNames?.includes("exec")) {
635
642
  const allowEvalPy = settings.get("eval.py") ?? true;
636
643
  const allowEvalJs = settings.get("eval.js") ?? true;
@@ -1238,6 +1245,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1238
1245
  spawns: spawnsEnv,
1239
1246
  taskDepth: childDepth,
1240
1247
  parentHindsightSessionState: options.parentHindsightSessionState,
1248
+ parentMnemosyneSessionState: options.parentMnemosyneSessionState,
1241
1249
  parentTaskPrefix: id,
1242
1250
  agentId: id,
1243
1251
  agentDisplayName: agent.name,
package/src/task/index.ts CHANGED
@@ -930,6 +930,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
930
930
  localProtocolOptions,
931
931
  parentArtifactManager,
932
932
  parentHindsightSessionState: this.session.getHindsightSessionState?.(),
933
+ parentMnemosyneSessionState: this.session.getMnemosyneSessionState?.(),
933
934
  parentTelemetry: this.session.getTelemetry?.(),
934
935
  parentEvalSessionId,
935
936
  });
@@ -986,6 +987,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
986
987
  localProtocolOptions,
987
988
  parentArtifactManager,
988
989
  parentHindsightSessionState: this.session.getHindsightSessionState?.(),
990
+ parentMnemosyneSessionState: this.session.getMnemosyneSessionState?.(),
989
991
  parentTelemetry: this.session.getTelemetry?.(),
990
992
  parentEvalSessionId,
991
993
  });