@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
@@ -5,7 +5,7 @@ import { isEnoent } from "@oh-my-pi/pi-utils";
5
5
  import { AgentRegistry } from "../registry/agent-registry";
6
6
  import { parseInternalUrl } from "./parse";
7
7
  import { validateRelativePath } from "./skill-protocol";
8
- import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
8
+ import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";
9
9
 
10
10
  export interface LocalProtocolOptions {
11
11
  getArtifactsDir?: () => string | null;
@@ -164,13 +164,25 @@ export class LocalProtocolHandler implements ProtocolHandler {
164
164
  * Returns the active local-protocol options.
165
165
  *
166
166
  * Resolution order:
167
- * 1. Explicit override installed via {@link setOverride} (used by subagents
168
- * that share their parent's root and by SDK consumers with a custom
169
- * artifacts/session id mapping).
170
- * 2. The main session in `AgentRegistry.global()`. Its `SessionManager`
171
- * supplies both `getArtifactsDir` and `getSessionId`.
167
+ * 1. **Caller-supplied** `context.localProtocolOptions` (the actual session
168
+ * that initiated the `read`/`find`/`search`/`router.resolve` call). This
169
+ * is what keeps `local://` reads pinned to the calling session in
170
+ * multi-session hosts (cmux/ACP, embedded SDK consumers) where every
171
+ * session registers as `kind: "main"` and "first one wins" would route
172
+ * to the wrong artifacts directory.
173
+ * 2. Explicit process-global override installed via {@link setOverride}
174
+ * (used by SDK consumers with a custom artifacts/session-id mapping and
175
+ * by code paths that do not have a calling session, e.g. TUI hyperlink
176
+ * resolution).
177
+ * 3. The first `main`-kind session in `AgentRegistry.global()`. Its
178
+ * `SessionManager` supplies both `getArtifactsDir` and `getSessionId`.
179
+ * Last-resort fallback — every caller that has a session reference
180
+ * SHOULD thread it through `context` so this branch is never taken in
181
+ * multi-session setups.
172
182
  */
173
- static resolveOptions(): LocalProtocolOptions | undefined {
183
+ static resolveOptions(context?: ResolveContext): LocalProtocolOptions | undefined {
184
+ const fromContext = context?.localProtocolOptions;
185
+ if (fromContext) return fromContext;
174
186
  const override = LocalProtocolHandler.#override;
175
187
  if (override) return override;
176
188
  const main = AgentRegistry.global()
@@ -184,8 +196,8 @@ export class LocalProtocolHandler implements ProtocolHandler {
184
196
  };
185
197
  }
186
198
 
187
- async resolve(url: InternalUrl): Promise<InternalResource> {
188
- const opts = LocalProtocolHandler.resolveOptions();
199
+ async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
200
+ const opts = LocalProtocolHandler.resolveOptions(context);
189
201
  if (!opts) {
190
202
  throw new Error("No session - local:// unavailable");
191
203
  }
@@ -247,8 +259,8 @@ export class LocalProtocolHandler implements ProtocolHandler {
247
259
  };
248
260
  }
249
261
 
250
- async complete(): Promise<UrlCompletion[]> {
251
- const opts = LocalProtocolHandler.resolveOptions();
262
+ async complete(_query?: string, context?: ResolveContext): Promise<UrlCompletion[]> {
263
+ const opts = LocalProtocolHandler.resolveOptions(context);
252
264
  if (!opts) return [];
253
265
  const localRoot = path.resolve(resolveLocalRoot(opts));
254
266
  try {
@@ -5,6 +5,8 @@
5
5
  * providing access to agent outputs and server resources without exposing filesystem paths.
6
6
  */
7
7
 
8
+ import type { LocalProtocolOptions } from "./local-protocol";
9
+
8
10
  /**
9
11
  * Raw resource payload returned by protocol handlers. The `immutable` flag is
10
12
  * applied by the router from {@link ProtocolHandler.immutable}, so handlers do
@@ -77,6 +79,17 @@ export interface ResolveContext {
77
79
  settings?: unknown;
78
80
  /** Caller's abort signal. */
79
81
  signal?: AbortSignal;
82
+ /**
83
+ * Calling session's `local://` root mapping. When present, the local-protocol
84
+ * handler resolves the URL against THIS session's artifacts dir instead of
85
+ * picking the first `main`-kind session from the global `AgentRegistry`.
86
+ *
87
+ * Required for correctness in multi-session hosts (cmux/ACP, embedded SDK
88
+ * consumers) where multiple sessions are registered as `main` and the
89
+ * "first one wins" lookup picks the wrong artifacts directory — see
90
+ * [#1608](https://github.com/can1357/oh-my-pi/issues/1608).
91
+ */
92
+ localProtocolOptions?: LocalProtocolOptions;
80
93
  }
81
94
 
82
95
  /**
@@ -89,6 +102,8 @@ export interface WriteContext {
89
102
  cwd?: string;
90
103
  /** Caller's abort signal. */
91
104
  signal?: AbortSignal;
105
+ /** Calling session's `local://` root mapping — see {@link ResolveContext.localProtocolOptions}. */
106
+ localProtocolOptions?: LocalProtocolOptions;
92
107
  }
93
108
 
94
109
  /**
package/src/lsp/client.ts CHANGED
@@ -415,6 +415,10 @@ export const WARMUP_TIMEOUT_MS = 5000;
415
415
  /** Max time to wait for the server to report project loading completion via $/progress */
416
416
  const PROJECT_LOAD_TIMEOUT_MS = 15_000;
417
417
 
418
+ /** Max time to wait for graceful LSP shutdown and process exit. */
419
+ const SHUTDOWN_TIMEOUT_MS = 5_000;
420
+ const EXIT_TIMEOUT_MS = 1_000;
421
+
418
422
  /**
419
423
  * Get or create an LSP client for the given server configuration and working directory.
420
424
  * @param config - Server configuration
@@ -768,8 +772,18 @@ export async function refreshFile(client: LspClient, filePath: string, signal?:
768
772
  }
769
773
  }
770
774
 
775
+ async function waitForExit(client: LspClient, timeoutMs: number): Promise<boolean> {
776
+ return await Promise.race([
777
+ client.proc.exited.then(
778
+ () => true,
779
+ () => true,
780
+ ),
781
+ Bun.sleep(timeoutMs).then(() => false),
782
+ ]);
783
+ }
784
+
771
785
  /**
772
- * Shutdown a specific client by key.
786
+ * Shutdown a specific client instance using the LSP shutdown/exit handshake.
773
787
  */
774
788
  async function shutdownClientInstance(client: LspClient): Promise<void> {
775
789
  const err = new Error("LSP client shutdown");
@@ -778,13 +792,22 @@ async function shutdownClientInstance(client: LspClient): Promise<void> {
778
792
  }
779
793
  client.pendingRequests.clear();
780
794
 
781
- const timeout = Bun.sleep(5_000);
782
- const shutdown = sendRequest(client, "shutdown", null).catch(() => {});
783
- await Promise.race([shutdown, timeout]);
795
+ const shutdownCompleted = await sendRequest(client, "shutdown", null, undefined, SHUTDOWN_TIMEOUT_MS).then(
796
+ () => true,
797
+ () => false,
798
+ );
799
+ if (shutdownCompleted) {
800
+ await sendNotification(client, "exit", undefined).catch(() => {});
801
+ if (await waitForExit(client, EXIT_TIMEOUT_MS)) return;
802
+ }
803
+
784
804
  client.proc.kill();
785
- await Promise.race([client.proc.exited.catch(() => {}), Bun.sleep(1_000)]);
805
+ await waitForExit(client, EXIT_TIMEOUT_MS);
786
806
  }
787
807
 
808
+ /**
809
+ * Shutdown a specific client by key.
810
+ */
788
811
  export async function shutdownClient(key: string): Promise<void> {
789
812
  const client = clients.get(key);
790
813
  if (!client) return;
package/src/main.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  } from "@oh-my-pi/pi-utils";
23
23
  import chalk from "chalk";
24
24
  import type { Args } from "./cli/args";
25
+ import { applyExtensionFlags, type ExtensionFlagSink } from "./cli/extension-flags";
25
26
  import { processFileArguments } from "./cli/file-processor";
26
27
  import { buildInitialMessage } from "./cli/initial-message";
27
28
  import { runListModelsCommand } from "./cli/list-models";
@@ -39,6 +40,7 @@ import {
39
40
  } from "./discovery/helpers";
40
41
  import { injectOmpExtensionCliRoots } from "./discovery/omp-extension-roots";
41
42
  import { exportFromFile } from "./export/html";
43
+ import { ExtensionRunner } from "./extensibility/extensions/runner";
42
44
  import type { ExtensionUIContext } from "./extensibility/extensions/types";
43
45
  import {
44
46
  getInstalledPluginsRegistryPath,
@@ -57,6 +59,7 @@ import {
57
59
  type CreateAgentSessionResult,
58
60
  createAgentSession,
59
61
  discoverAuthStorage,
62
+ loadSessionExtensions,
60
63
  } from "./sdk";
61
64
  import type { AgentSession } from "./session/agent-session";
62
65
  import type { AuthStorage } from "./session/auth-storage";
@@ -65,7 +68,7 @@ import { resolvePromptInput } from "./system-prompt";
65
68
  import { AUTO_THINKING } from "./thinking";
66
69
  import type { LspStartupServerInfo } from "./tools";
67
70
  import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog";
68
- import type { EventBus } from "./utils/event-bus";
71
+ import { EventBus } from "./utils/event-bus";
69
72
 
70
73
  async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {
71
74
  if (!settings.get("startup.checkUpdate")) {
@@ -171,37 +174,6 @@ export async function submitInteractiveInput(
171
174
  }
172
175
  }
173
176
 
174
- function applyExtensionFlagValues(session: AgentSession, rawArgs: string[]): Map<string, boolean | string> {
175
- const extensionRunner = session.extensionRunner;
176
- if (!extensionRunner) {
177
- return new Map();
178
- }
179
-
180
- const extFlags = extensionRunner.getFlags();
181
- if (extFlags.size > 0) {
182
- for (let i = 0; i < rawArgs.length; i++) {
183
- const arg = rawArgs[i];
184
- if (!arg.startsWith("--")) {
185
- continue;
186
- }
187
- const flagName = arg.slice(2);
188
- const extFlag = extFlags.get(flagName);
189
- if (!extFlag) {
190
- continue;
191
- }
192
- if (extFlag.type === "boolean") {
193
- extensionRunner.setFlagValue(flagName, true);
194
- continue;
195
- }
196
- if (i + 1 < rawArgs.length) {
197
- extensionRunner.setFlagValue(flagName, rawArgs[++i]);
198
- }
199
- }
200
- }
201
-
202
- return extensionRunner.getFlagValues();
203
- }
204
-
205
177
  type AcpSessionFactory = (cwd: string) => Promise<AgentSession>;
206
178
 
207
179
  export interface AcpSessionFactoryOptions {
@@ -244,7 +216,7 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess
244
216
  if (args.parsedArgs.apiKey && !args.baseOptions.model && nextSession.model) {
245
217
  args.authStorage.setRuntimeApiKey(nextSession.model.provider, args.parsedArgs.apiKey);
246
218
  }
247
- applyExtensionFlagValues(nextSession, args.rawArgs);
219
+ applyExtensionFlags(nextSession.extensionRunner, args.rawArgs);
248
220
  return nextSession;
249
221
  };
250
222
  }
@@ -820,22 +792,7 @@ export async function runRootCommand(
820
792
  if (parsedArgs.noTitle || parsedArgs.mode === "rpc" || parsedArgs.mode === "rpc-ui" || parsedArgs.mode === "acp") {
821
793
  Bun.env.PI_NO_TITLE = "1";
822
794
  }
823
- const { pipedInput, fileText, fileImages } = await logger.time("prepareInitialMessage", async () => {
824
- const pipedInput = await readPipedInput();
825
- if (parsedArgs.fileArgs.length === 0) {
826
- return { pipedInput, fileText: undefined, fileImages: undefined };
827
- }
828
- const processed = await processFileArguments(parsedArgs.fileArgs, {
829
- autoResizeImages: settingsInstance.get("images.autoResize"),
830
- });
831
- return { pipedInput, fileText: processed.text, fileImages: processed.images };
832
- });
833
- const { initialMessage, initialImages } = buildInitialMessage({
834
- parsed: parsedArgs,
835
- fileText,
836
- fileImages,
837
- stdinContent: pipedInput,
838
- });
795
+ const pipedInput = await logger.time("readPipedInput", readPipedInput);
839
796
  const autoPrint = pipedInput !== undefined && !parsedArgs.print && parsedArgs.mode === undefined;
840
797
  const isInteractive = !parsedArgs.print && !autoPrint && parsedArgs.mode === undefined;
841
798
  const mode = parsedArgs.mode || "text";
@@ -984,8 +941,42 @@ export async function runRootCommand(
984
941
  });
985
942
  await (deps.runAcpMode ?? runAcpMode)(createAcpSession);
986
943
  } else {
987
- const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager, eventBus } =
988
- await createSession(sessionOptions);
944
+ // Resolve extension-registered CLI flags before creating the session so a
945
+ // bad `@file` fails fast WITHOUT leaving a junk session/breadcrumb
946
+ // (createAgentSession writes the terminal breadcrumb eagerly). Loading the
947
+ // extensions here also makes `@file` classification extension-aware — e.g. a
948
+ // string-flag value such as `--target @notes.md` is the flag's value, not a
949
+ // file — and the same result is handed to createAgentSession via
950
+ // `preloadedExtensions` so the discovery work is not repeated.
951
+ const eventBus = new EventBus();
952
+ const extensionsResult = await loadSessionExtensions(sessionOptions, cwd, settingsInstance, eventBus);
953
+ const extensionFlagSink: ExtensionFlagSink = {
954
+ getFlags: () => ExtensionRunner.aggregateFlags(extensionsResult.extensions),
955
+ setFlagValue: (name, value) => {
956
+ extensionsResult.runtime.flagValues.set(name, value);
957
+ },
958
+ };
959
+ const initialArgs = applyExtensionFlags(extensionFlagSink, rawArgs) ?? parsedArgs;
960
+ const processedFiles =
961
+ initialArgs.fileArgs.length > 0
962
+ ? await logger.time("processFileArguments", () =>
963
+ processFileArguments(initialArgs.fileArgs, {
964
+ autoResizeImages: settingsInstance.get("images.autoResize"),
965
+ }),
966
+ )
967
+ : undefined;
968
+ const { initialMessage, initialImages } = buildInitialMessage({
969
+ parsed: initialArgs,
970
+ fileText: processedFiles?.text,
971
+ fileImages: processedFiles?.images,
972
+ stdinContent: pipedInput,
973
+ });
974
+
975
+ const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager } = await createSession({
976
+ ...sessionOptions,
977
+ eventBus,
978
+ preloadedExtensions: extensionsResult,
979
+ });
989
980
  if (parsedArgs.apiKey && !sessionOptions.model && session.model) {
990
981
  authStorage.setRuntimeApiKey(session.model.provider, parsedArgs.apiKey);
991
982
  }
@@ -999,8 +990,6 @@ export async function runRootCommand(
999
990
  notifs.push({ kind: "error", message: modelRegistryError.message });
1000
991
  }
1001
992
 
1002
- applyExtensionFlagValues(session, rawArgs);
1003
-
1004
993
  if (!isInteractive && !session.model) {
1005
994
  if (modelFallbackMessage) {
1006
995
  process.stderr.write(`${chalk.red(modelFallbackMessage)}\n`);
@@ -1044,7 +1033,7 @@ export async function runRootCommand(
1044
1033
  changelogMarkdown,
1045
1034
  notifs,
1046
1035
  versionCheckPromise,
1047
- parsedArgs.messages,
1036
+ initialArgs.messages,
1048
1037
  setToolUIContext,
1049
1038
  lspServers,
1050
1039
  mcpManager,
@@ -1057,7 +1046,7 @@ export async function runRootCommand(
1057
1046
  } else {
1058
1047
  await runPrintMode(session, {
1059
1048
  mode,
1060
- messages: parsedArgs.messages,
1049
+ messages: initialArgs.messages,
1061
1050
  initialMessage,
1062
1051
  initialImages,
1063
1052
  });
@@ -59,6 +59,27 @@ type TrackedPromise<T> = {
59
59
 
60
60
  const STARTUP_TIMEOUT_MS = 250;
61
61
 
62
+ /**
63
+ * Per-server reconnect-storm circuit breaker.
64
+ *
65
+ * `transport.onClose` (wired in {@link MCPManager.connectServers} and
66
+ * {@link MCPManager.#connectAndWireServer}) fires `reconnectServer` on every
67
+ * clean process exit, so a stdio MCP server that completes the
68
+ * `initialize` + `tools/list` handshake and then exits will pull the agent
69
+ * into a fork loop with no rate limit. That pathology shipped in issue #1592
70
+ * (a `php`-shebang MCP fork-bombing macOS, parented directly to the agent's
71
+ * `bun` PID via shebang exec).
72
+ *
73
+ * We keep the sliding window short — older crashes age out so a single
74
+ * transient failure stays cheap — but cap the burst tightly enough that the
75
+ * agent never spawns more than `RECONNECT_BURST_LIMIT * #doReconnect retries`
76
+ * (≤ 25) processes per stuck server per window. Manual `/mcp reconnect`
77
+ * resets the window so users can recover after fixing the underlying
78
+ * misconfiguration.
79
+ */
80
+ const RECONNECT_BURST_WINDOW_MS = 30_000;
81
+ const RECONNECT_BURST_LIMIT = 5;
82
+
62
83
  function trackPromise<T>(promise: Promise<T>): TrackedPromise<T> {
63
84
  const tracked: TrackedPromise<T> = { promise, status: "pending" };
64
85
  promise.then(
@@ -166,6 +187,11 @@ export class MCPManager {
166
187
  #pendingReconnections = new Map<string, Promise<MCPServerConnection | null>>();
167
188
  /** Preserved configs for reconnection after connection loss. */
168
189
  #serverConfigs = new Map<string, MCPServerConfig>();
190
+ /**
191
+ * Timestamps of recent `reconnectServer` invocations per server, used by the
192
+ * crash-storm circuit breaker (see {@link RECONNECT_BURST_LIMIT}).
193
+ */
194
+ #reconnectHistory = new Map<string, number[]>();
169
195
  /** Monotonic epoch incremented on disconnectAll to invalidate stale reconnections. */
170
196
  #epoch = 0;
171
197
 
@@ -666,6 +692,7 @@ export class MCPManager {
666
692
  this.#sources.delete(name);
667
693
  this.#serverConfigs.delete(name);
668
694
  this.#pendingResourceRefresh.delete(name);
695
+ this.#reconnectHistory.delete(name);
669
696
 
670
697
  const connection = this.#connections.get(name);
671
698
 
@@ -714,24 +741,80 @@ export class MCPManager {
714
741
  this.#connections.clear();
715
742
  this.#tools = [];
716
743
  this.#subscribedResources.clear();
744
+ this.#reconnectHistory.clear();
717
745
  }
718
746
 
719
747
  /**
720
748
  * Reconnect to a server after a connection failure.
749
+ *
721
750
  * Tears down the stale connection, re-resolves auth, establishes a new
722
- * connection, reloads tools, and notifies consumers.
723
- * Concurrent calls for the same server share one reconnection attempt.
724
- * Returns the new connection, or null if reconnection failed.
751
+ * connection, reloads tools, and notifies consumers. Concurrent calls for
752
+ * the same server share one reconnection attempt. Returns the new
753
+ * connection, or `null` if reconnection failed or the per-server crash
754
+ * burst limit (see {@link RECONNECT_BURST_LIMIT}) is exceeded.
755
+ *
756
+ * @param options.manual - When `true`, resets the crash-burst window so a
757
+ * user-driven retry (e.g. `/mcp reconnect`) is never blocked by an
758
+ * earlier storm. Defaults to `false`; the transport `onClose` callback
759
+ * and the per-tool-call retry path in `tool-bridge` MUST NOT set it.
725
760
  */
726
- async reconnectServer(name: string): Promise<MCPServerConnection | null> {
761
+ async reconnectServer(name: string, options?: { manual?: boolean }): Promise<MCPServerConnection | null> {
762
+ if (options?.manual) {
763
+ this.#reconnectHistory.delete(name);
764
+ }
765
+
727
766
  const pending = this.#pendingReconnections.get(name);
728
767
  if (pending) return pending;
729
768
 
769
+ if (this.#tripReconnectBreaker(name)) {
770
+ return null;
771
+ }
772
+
730
773
  const attempt = this.#doReconnect(name);
731
774
  this.#pendingReconnections.set(name, attempt);
732
775
  return attempt.finally(() => this.#pendingReconnections.delete(name));
733
776
  }
734
777
 
778
+ /**
779
+ * Record a reconnect attempt against the per-server crash window and report
780
+ * whether the circuit breaker is now open. Sliding window: entries older
781
+ * than {@link RECONNECT_BURST_WINDOW_MS} are pruned before the new
782
+ * timestamp is appended, so a single transient failure ages out cheaply
783
+ * but repeated rapid crashes accumulate until the limit is hit.
784
+ */
785
+ #tripReconnectBreaker(name: string): boolean {
786
+ const now = Date.now();
787
+ const previous = this.#reconnectHistory.get(name) ?? [];
788
+ const recent = previous.filter(ts => now - ts < RECONNECT_BURST_WINDOW_MS);
789
+ recent.push(now);
790
+ this.#reconnectHistory.set(name, recent);
791
+
792
+ if (recent.length > RECONNECT_BURST_LIMIT) {
793
+ logger.error("MCP server crashed too many times; suspending automatic reconnects", {
794
+ path: `mcp:${name}`,
795
+ crashes: recent.length,
796
+ windowMs: RECONNECT_BURST_WINDOW_MS,
797
+ });
798
+ // Tear down the stale connection so `getConnectionStatus()` no
799
+ // longer reports it as "connected" and `waitForConnection()` does
800
+ // not hand a closed transport to callers. Tools stay registered
801
+ // in `#tools` — the user can recover with `/mcp reconnect <name>`
802
+ // once they've fixed the underlying misconfiguration. Mirrors the
803
+ // teardown in `#doReconnect`: detach `onClose` first so the
804
+ // transport's own `close()` cannot re-arm this path.
805
+ const stale = this.#connections.get(name);
806
+ if (stale) {
807
+ stale.transport.onClose = undefined;
808
+ void stale.transport.close().catch(() => {});
809
+ this.#connections.delete(name);
810
+ }
811
+ this.#pendingConnections.delete(name);
812
+ this.#pendingToolLoads.delete(name);
813
+ return true;
814
+ }
815
+ return false;
816
+ }
817
+
735
818
  async #doReconnect(name: string): Promise<MCPServerConnection | null> {
736
819
  const oldConnection = this.#connections.get(name);
737
820
  const config = oldConnection?.config ?? this.#serverConfigs.get(name);
@@ -1,4 +1,4 @@
1
- export * from "../mnemosyne";
1
+ export * from "../mnemopi";
2
2
  export * from "./local-backend";
3
3
  export * from "./off-backend";
4
4
  export * from "./resolve";
@@ -1,6 +1,6 @@
1
1
  import type { Settings } from "../config/settings";
2
2
  import { hindsightBackend } from "../hindsight";
3
- import { mnemosyneBackend } from "../mnemosyne";
3
+ import { mnemopiBackend } from "../mnemopi";
4
4
  import { localBackend } from "./local-backend";
5
5
  import { offBackend } from "./off-backend";
6
6
  import type { MemoryBackend } from "./types";
@@ -11,7 +11,7 @@ import type { MemoryBackend } from "./types";
11
11
  * Selection rules (single source of truth — every memory consumer routes
12
12
  * through this):
13
13
  * - `memory.backend === "hindsight"` → Hindsight remote memory
14
- * - `memory.backend === "mnemosyne"` → local Mnemosyne SQLite memory
14
+ * - `memory.backend === "mnemopi"` → local Mnemopi SQLite memory
15
15
  * - `memory.backend === "local"` → local rollout summary pipeline
16
16
  * - everything else → no-op
17
17
  *
@@ -21,7 +21,7 @@ import type { MemoryBackend } from "./types";
21
21
  export function resolveMemoryBackend(settings: Settings): MemoryBackend {
22
22
  const id = settings.get("memory.backend");
23
23
  if (id === "hindsight") return hindsightBackend;
24
- if (id === "mnemosyne") return mnemosyneBackend;
24
+ if (id === "mnemopi") return mnemopiBackend;
25
25
  if (id === "local") return localBackend;
26
26
  return offBackend;
27
27
  }
@@ -10,10 +10,10 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
10
10
  import type { ModelRegistry } from "../config/model-registry";
11
11
  import type { Settings } from "../config/settings";
12
12
  import type { HindsightSessionState } from "../hindsight/state";
13
- import type { MnemosyneSessionState } from "../mnemosyne/state";
13
+ import type { MnemopiSessionState } from "../mnemopi/state";
14
14
  import type { AgentSession } from "../session/agent-session";
15
15
 
16
- export type MemoryBackendId = "off" | "local" | "hindsight" | "mnemosyne";
16
+ export type MemoryBackendId = "off" | "local" | "hindsight" | "mnemopi";
17
17
 
18
18
  export interface MemoryBackendStartOptions {
19
19
  session: AgentSession;
@@ -22,7 +22,7 @@ export interface MemoryBackendStartOptions {
22
22
  agentDir: string;
23
23
  taskDepth: number;
24
24
  parentHindsightSessionState?: HindsightSessionState;
25
- parentMnemosyneSessionState?: MnemosyneSessionState;
25
+ parentMnemopiSessionState?: MnemopiSessionState;
26
26
  }
27
27
 
28
28
  export interface MemoryBackend {