@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.
- package/CHANGELOG.md +114 -6
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/extension-flags.d.ts +36 -0
- package/dist/types/config/config-file.d.ts +4 -0
- package/dist/types/config/file-lock.d.ts +23 -0
- package/dist/types/config/keybindings.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +69 -65
- package/dist/types/edit/hashline/diff.d.ts +3 -3
- package/dist/types/eval/__tests__/agent-bridge.test.d.ts +1 -0
- package/dist/types/eval/__tests__/budget-bridge.test.d.ts +1 -0
- package/dist/types/eval/__tests__/idle-timeout.test.d.ts +1 -0
- package/dist/types/eval/agent-bridge.d.ts +25 -0
- package/dist/types/eval/backend.d.ts +17 -2
- package/dist/types/eval/budget-bridge.d.ts +29 -0
- package/dist/types/eval/idle-timeout.d.ts +28 -0
- package/dist/types/eval/js/executor.d.ts +8 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- package/dist/types/eval/py/executor.d.ts +13 -0
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/plugins/git-url.d.ts +11 -1
- package/dist/types/extensibility/plugins/manager.d.ts +12 -1
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/local-protocol.d.ts +19 -9
- package/dist/types/internal-urls/types.d.ts +14 -0
- package/dist/types/lsp/client.d.ts +3 -0
- package/dist/types/mcp/manager.d.ts +14 -5
- package/dist/types/memory-backend/index.d.ts +1 -1
- package/dist/types/memory-backend/resolve.d.ts +1 -1
- package/dist/types/memory-backend/types.d.ts +3 -3
- package/dist/types/mnemopi/backend.d.ts +4 -0
- package/dist/types/mnemopi/config.d.ts +29 -0
- package/dist/types/mnemopi/state.d.ts +72 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -2
- package/dist/types/modes/components/omfg-panel.d.ts +19 -0
- package/dist/types/modes/controllers/command-controller.d.ts +6 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
- package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
- package/dist/types/modes/gradient-highlight.d.ts +5 -1
- package/dist/types/modes/interactive-mode.d.ts +7 -3
- package/dist/types/modes/magic-keywords.d.ts +14 -0
- package/dist/types/modes/markdown-prose.d.ts +27 -0
- package/dist/types/modes/orchestrate.d.ts +7 -2
- package/dist/types/modes/shared.d.ts +1 -1
- package/dist/types/modes/turn-budget.d.ts +18 -0
- package/dist/types/modes/types.d.ts +7 -3
- package/dist/types/modes/ultrathink.d.ts +7 -2
- package/dist/types/modes/workflow.d.ts +15 -0
- package/dist/types/sdk.d.ts +13 -3
- package/dist/types/session/agent-session.d.ts +36 -17
- package/dist/types/session/session-manager.d.ts +18 -0
- package/dist/types/session/session-storage.d.ts +6 -0
- package/dist/types/session/shake-types.d.ts +24 -0
- package/dist/types/task/executor.d.ts +2 -2
- package/dist/types/task/repair-args.d.ts +52 -0
- package/dist/types/tiny/models.d.ts +1 -1
- package/dist/types/tiny/title-client.d.ts +28 -2
- package/dist/types/tiny/title-protocol.d.ts +8 -5
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/index.d.ts +19 -3
- package/dist/types/tools/memory-edit.d.ts +1 -1
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tui/output-block.d.ts +7 -7
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/package.json +10 -10
- package/scripts/build-binary.ts +0 -1
- package/src/autoresearch/tools/run-experiment.ts +45 -113
- package/src/cli/args.ts +39 -16
- package/src/cli/extension-flags.ts +48 -0
- package/src/cli/plugin-cli.ts +11 -2
- package/src/cli.ts +59 -0
- package/src/config/config-file.ts +98 -13
- package/src/config/file-lock.ts +60 -17
- package/src/config/keybindings.ts +78 -27
- package/src/config/model-registry.ts +7 -1
- package/src/config/settings-schema.ts +73 -67
- package/src/config/settings.ts +22 -0
- package/src/edit/hashline/diff.ts +81 -24
- package/src/edit/renderer.ts +16 -12
- package/src/eval/__tests__/agent-bridge.test.ts +433 -0
- package/src/eval/__tests__/budget-bridge.test.ts +69 -0
- package/src/eval/__tests__/idle-timeout.test.ts +66 -0
- package/src/eval/__tests__/shared-executors.test.ts +21 -0
- package/src/eval/agent-bridge.ts +295 -0
- package/src/eval/backend.ts +17 -2
- package/src/eval/budget-bridge.ts +48 -0
- package/src/eval/idle-timeout.ts +80 -0
- package/src/eval/js/executor.ts +35 -7
- package/src/eval/js/index.ts +2 -1
- package/src/eval/js/shared/prelude.txt +85 -1
- package/src/eval/js/tool-bridge.ts +9 -0
- package/src/eval/py/executor.ts +41 -14
- package/src/eval/py/index.ts +2 -1
- package/src/eval/py/prelude.py +132 -1
- package/src/exec/bash-executor.ts +2 -3
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/runner.ts +12 -2
- package/src/extensibility/plugins/git-url.ts +90 -4
- package/src/extensibility/plugins/manager.ts +103 -7
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +88 -88
- package/src/internal-urls/local-protocol.ts +23 -11
- package/src/internal-urls/types.ts +15 -0
- package/src/lsp/client.ts +28 -5
- package/src/main.ts +44 -55
- package/src/mcp/manager.ts +87 -4
- package/src/memory-backend/index.ts +1 -1
- package/src/memory-backend/resolve.ts +3 -3
- package/src/memory-backend/types.ts +3 -3
- package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
- package/src/{mnemosyne → mnemopi}/config.ts +36 -36
- package/src/{mnemosyne → mnemopi}/state.ts +67 -67
- package/src/modes/components/agent-dashboard.ts +6 -6
- package/src/modes/components/custom-editor.ts +4 -11
- package/src/modes/components/extensions/state-manager.ts +3 -4
- package/src/modes/components/footer.ts +8 -9
- package/src/modes/components/hook-selector.ts +86 -20
- package/src/modes/components/oauth-selector.ts +93 -21
- package/src/modes/components/omfg-panel.ts +141 -0
- package/src/modes/components/settings-defs.ts +2 -2
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +38 -19
- package/src/modes/components/tree-selector.ts +4 -3
- package/src/modes/components/user-message-selector.ts +94 -19
- package/src/modes/components/user-message.ts +8 -1
- package/src/modes/controllers/command-controller.ts +25 -0
- package/src/modes/controllers/event-controller.ts +68 -3
- package/src/modes/controllers/input-controller.ts +14 -11
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/controllers/omfg-controller.ts +283 -0
- package/src/modes/controllers/omfg-rule.ts +647 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/gradient-highlight.ts +23 -6
- package/src/modes/interactive-mode.ts +41 -7
- package/src/modes/magic-keywords.ts +20 -0
- package/src/modes/markdown-prose.ts +247 -0
- package/src/modes/orchestrate.ts +17 -11
- package/src/modes/shared.ts +3 -11
- package/src/modes/turn-budget.ts +31 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/ultrathink.ts +16 -10
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/modes/workflow.ts +42 -0
- package/src/prompts/system/omfg-user.md +51 -0
- package/src/prompts/system/project-prompt.md +3 -2
- package/src/prompts/system/subagent-system-prompt.md +12 -8
- package/src/prompts/system/system-prompt.md +9 -6
- package/src/prompts/system/workflow-notice.md +70 -0
- package/src/prompts/tools/eval.md +13 -1
- package/src/prompts/tools/memory-edit.md +1 -1
- package/src/sdk.ts +63 -33
- package/src/session/agent-session.ts +290 -55
- package/src/session/session-manager.ts +32 -0
- package/src/session/session-storage.ts +68 -8
- package/src/session/shake-types.ts +43 -0
- package/src/slash-commands/builtin-registry.ts +39 -16
- package/src/task/executor.ts +17 -7
- package/src/task/index.ts +9 -8
- package/src/task/repair-args.ts +117 -0
- package/src/tiny/models.ts +2 -2
- package/src/tiny/title-client.ts +133 -43
- package/src/tiny/title-protocol.ts +10 -5
- package/src/tiny/worker.ts +3 -46
- package/src/tools/ast-edit.ts +3 -0
- package/src/tools/ast-grep.ts +3 -0
- package/src/tools/eval.ts +202 -26
- package/src/tools/find.ts +20 -6
- package/src/tools/gh.ts +1 -0
- package/src/tools/grouped-file-output.ts +9 -2
- package/src/tools/index.ts +17 -5
- package/src/tools/memory-edit.ts +4 -4
- package/src/tools/memory-recall.ts +5 -5
- package/src/tools/memory-reflect.ts +5 -5
- package/src/tools/memory-retain.ts +4 -4
- package/src/tools/path-utils.ts +13 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/render-utils.ts +2 -1
- package/src/tools/search.ts +491 -76
- package/src/tui/output-block.ts +37 -75
- package/src/utils/git.ts +9 -3
- package/dist/types/mnemosyne/backend.d.ts +0 -4
- package/dist/types/mnemosyne/config.d.ts +0 -29
- package/dist/types/mnemosyne/state.d.ts +0 -72
- /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
- /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.
|
|
168
|
-
* that
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
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
|
|
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
|
|
782
|
-
|
|
783
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
988
|
-
|
|
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
|
-
|
|
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:
|
|
1049
|
+
messages: initialArgs.messages,
|
|
1061
1050
|
initialMessage,
|
|
1062
1051
|
initialImages,
|
|
1063
1052
|
});
|
package/src/mcp/manager.ts
CHANGED
|
@@ -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
|
-
*
|
|
724
|
-
*
|
|
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,6 +1,6 @@
|
|
|
1
1
|
import type { Settings } from "../config/settings";
|
|
2
2
|
import { hindsightBackend } from "../hindsight";
|
|
3
|
-
import {
|
|
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 === "
|
|
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 === "
|
|
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 {
|
|
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" | "
|
|
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
|
-
|
|
25
|
+
parentMnemopiSessionState?: MnemopiSessionState;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface MemoryBackend {
|