@oh-my-pi/pi-coding-agent 16.2.2 → 16.2.4
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 +60 -0
- package/dist/cli.js +3682 -3615
- package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
- package/dist/types/advisor/advise-tool.d.ts +8 -4
- package/dist/types/advisor/config.d.ts +88 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/transcript-recorder.d.ts +13 -2
- package/dist/types/advisor/watchdog.d.ts +20 -0
- package/dist/types/cli/update-cli.d.ts +15 -0
- package/dist/types/collab/guest.d.ts +29 -0
- package/dist/types/collab/replication-shrink.d.ts +39 -0
- package/dist/types/config/provider-globals.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +81 -0
- package/dist/types/debug/log-viewer.d.ts +1 -0
- package/dist/types/debug/raw-sse.d.ts +1 -0
- package/dist/types/edit/hashline/diff.d.ts +0 -11
- package/dist/types/extensibility/tool-event-input.d.ts +7 -0
- package/dist/types/extensibility/utils.d.ts +12 -0
- package/dist/types/mcp/transports/index.d.ts +1 -0
- package/dist/types/mcp/transports/sse.d.ts +20 -0
- package/dist/types/memories/index.d.ts +20 -1
- package/dist/types/modes/components/advisor-config.d.ts +59 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +35 -1
- package/dist/types/modes/components/status-line/types.d.ts +13 -1
- package/dist/types/modes/controllers/command-controller.d.ts +3 -3
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +11 -4
- package/dist/types/modes/skill-command.d.ts +32 -0
- package/dist/types/modes/types.d.ts +9 -3
- package/dist/types/session/agent-session.d.ts +58 -10
- package/dist/types/session/indexed-session-storage.d.ts +7 -1
- package/dist/types/session/messages.d.ts +26 -0
- package/dist/types/session/messages.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +31 -3
- package/dist/types/session/session-history-format.d.ts +6 -0
- package/dist/types/session/session-loader.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +8 -7
- package/dist/types/session/session-storage.d.ts +11 -0
- package/dist/types/session/session-title-slot.d.ts +19 -0
- package/dist/types/ssh/connection-manager.d.ts +47 -0
- package/dist/types/ssh/utils.d.ts +16 -0
- package/dist/types/task/executor.d.ts +3 -16
- package/dist/types/task/render.d.ts +0 -5
- package/dist/types/task/renderer.d.ts +13 -0
- package/dist/types/task/types.d.ts +16 -0
- package/dist/types/task/yield-assembly.d.ts +28 -0
- package/dist/types/tiny/text.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/review.d.ts +6 -4
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +6 -0
- package/dist/types/tools/yield.d.ts +8 -3
- package/dist/types/utils/thinking-display.d.ts +4 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +242 -10
- package/src/advisor/__tests__/config.test.ts +173 -0
- package/src/advisor/advise-tool.ts +11 -6
- package/src/advisor/config.ts +256 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +12 -2
- package/src/advisor/transcript-recorder.ts +25 -2
- package/src/advisor/watchdog.ts +57 -31
- package/src/autolearn/controller.ts +13 -22
- package/src/autoresearch/index.ts +7 -2
- package/src/cli/gc-cli.ts +17 -10
- package/src/cli/update-cli.ts +254 -0
- package/src/collab/guest.ts +43 -7
- package/src/collab/host.ts +13 -8
- package/src/collab/replication-shrink.ts +111 -0
- package/src/config/model-registry.ts +80 -18
- package/src/config/provider-globals.ts +25 -0
- package/src/config/settings-schema.ts +77 -0
- package/src/debug/index.ts +32 -7
- package/src/debug/log-viewer.ts +111 -53
- package/src/debug/raw-sse.ts +68 -48
- package/src/discovery/codex.ts +13 -5
- package/src/edit/hashline/diff.ts +57 -4
- package/src/eval/__tests__/julia-prelude.test.ts +2 -2
- package/src/eval/js/shared/local-module-loader.ts +23 -1
- package/src/export/html/template.js +13 -7
- package/src/extensibility/extensions/loader.ts +5 -3
- package/src/extensibility/extensions/wrapper.ts +9 -3
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/hooks/tool-wrapper.ts +13 -4
- package/src/extensibility/plugins/manager.ts +2 -1
- package/src/extensibility/tool-event-input.ts +23 -0
- package/src/extensibility/utils.ts +74 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/client.ts +3 -1
- package/src/mcp/manager.ts +12 -5
- package/src/mcp/transports/index.ts +1 -0
- package/src/mcp/transports/sse.ts +377 -0
- package/src/memories/index.ts +130 -15
- package/src/memory-backend/local-backend.ts +5 -3
- package/src/modes/components/advisor-config.ts +555 -0
- package/src/modes/components/advisor-message.ts +9 -2
- package/src/modes/components/agent-hub.ts +9 -4
- package/src/modes/components/index.ts +2 -0
- package/src/modes/components/model-selector.ts +79 -48
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/component.ts +150 -5
- package/src/modes/components/status-line/segments.ts +46 -21
- package/src/modes/components/status-line/types.ts +13 -1
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/controllers/command-controller.ts +27 -36
- package/src/modes/controllers/event-controller.ts +113 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +61 -61
- package/src/modes/controllers/selector-controller.ts +100 -9
- package/src/modes/interactive-mode.ts +74 -25
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +9 -3
- package/src/modes/utils/ui-helpers.ts +41 -23
- package/src/prompts/agents/reviewer.md +11 -10
- package/src/prompts/goals/goal-todo-context.md +12 -0
- package/src/prompts/review-custom-request.md +1 -2
- package/src/prompts/review-request.md +1 -2
- package/src/prompts/system/interrupted-thinking.md +7 -0
- package/src/prompts/system/recap-user.md +9 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -5
- package/src/prompts/system/subagent-yield-reminder.md +6 -5
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/irc.md +2 -2
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +28 -24
- package/src/session/agent-session.ts +899 -429
- package/src/session/indexed-session-storage.ts +86 -13
- package/src/session/messages.test.ts +125 -0
- package/src/session/messages.ts +172 -9
- package/src/session/redis-session-storage.ts +49 -2
- package/src/session/session-entries.ts +39 -2
- package/src/session/session-history-format.ts +29 -2
- package/src/session/session-listing.ts +54 -24
- package/src/session/session-loader.ts +66 -3
- package/src/session/session-manager.ts +115 -22
- package/src/session/session-persistence.ts +95 -1
- package/src/session/session-storage.ts +36 -0
- package/src/session/session-title-slot.ts +141 -0
- package/src/session/sql-session-storage.ts +71 -11
- package/src/slash-commands/builtin-registry.ts +23 -24
- package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
- package/src/ssh/connection-manager.ts +139 -12
- package/src/ssh/file-transfer.ts +23 -18
- package/src/ssh/ssh-executor.ts +2 -13
- package/src/ssh/utils.ts +19 -0
- package/src/task/executor.ts +21 -23
- package/src/task/render.ts +162 -20
- package/src/task/renderer.ts +14 -0
- package/src/task/types.ts +17 -0
- package/src/task/yield-assembly.ts +207 -0
- package/src/tiny/text.ts +23 -0
- package/src/tools/ask.ts +55 -4
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/renderers.ts +8 -2
- package/src/tools/review.ts +17 -7
- package/src/tools/ssh.ts +8 -4
- package/src/tools/todo.ts +17 -1
- package/src/tools/yield.ts +140 -31
- package/src/utils/thinking-display.ts +15 -0
- package/src/prompts/system/autolearn-nudge.md +0 -5
package/src/mcp/client.ts
CHANGED
|
@@ -8,6 +8,7 @@ import * as url from "node:url";
|
|
|
8
8
|
import { getProjectDir, logger, withTimeout } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import { describeMCPTimeout, isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "./timeout";
|
|
10
10
|
import { createHttpTransport } from "./transports/http";
|
|
11
|
+
import { createSseTransport } from "./transports/sse";
|
|
11
12
|
import { createStdioTransport } from "./transports/stdio";
|
|
12
13
|
import type {
|
|
13
14
|
MCPGetPromptParams,
|
|
@@ -77,8 +78,9 @@ async function createTransport(config: MCPServerConfig): Promise<MCPTransport> {
|
|
|
77
78
|
case "stdio":
|
|
78
79
|
return createStdioTransport(config as MCPStdioServerConfig);
|
|
79
80
|
case "http":
|
|
81
|
+
return createHttpTransport(config as MCPHttpServerConfig);
|
|
80
82
|
case "sse":
|
|
81
|
-
return
|
|
83
|
+
return createSseTransport(config as MCPSseServerConfig);
|
|
82
84
|
default:
|
|
83
85
|
throw new Error(`Unknown server type: ${serverType}`);
|
|
84
86
|
}
|
package/src/mcp/manager.ts
CHANGED
|
@@ -37,7 +37,6 @@ import type { McpConnectionStatusEvent } from "./startup-events";
|
|
|
37
37
|
import type { MCPToolDetails } from "./tool-bridge";
|
|
38
38
|
import { DeferredMCPTool, MCPTool } from "./tool-bridge";
|
|
39
39
|
import type { MCPToolCache } from "./tool-cache";
|
|
40
|
-
import { HttpTransport } from "./transports/http";
|
|
41
40
|
import type {
|
|
42
41
|
MCPGetPromptResult,
|
|
43
42
|
MCPPrompt,
|
|
@@ -48,6 +47,7 @@ import type {
|
|
|
48
47
|
MCPServerConfig,
|
|
49
48
|
MCPServerConnection,
|
|
50
49
|
MCPToolDefinition,
|
|
50
|
+
MCPTransport,
|
|
51
51
|
} from "./types";
|
|
52
52
|
import { MCPNotificationMethods } from "./types";
|
|
53
53
|
|
|
@@ -56,6 +56,13 @@ type ToolLoadResult = {
|
|
|
56
56
|
serverTools: MCPToolDefinition[];
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
interface AuthRefreshableMCPTransport extends MCPTransport {
|
|
60
|
+
onAuthError?: () => Promise<Record<string, string> | null>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isAuthRefreshableMCPTransport(transport: MCPTransport): transport is AuthRefreshableMCPTransport {
|
|
64
|
+
return "onAuthError" in transport;
|
|
65
|
+
}
|
|
59
66
|
type TrackedPromise<T> = {
|
|
60
67
|
promise: Promise<T>;
|
|
61
68
|
status: "pending" | "fulfilled" | "rejected";
|
|
@@ -419,12 +426,12 @@ export class MCPManager {
|
|
|
419
426
|
this.#connections.set(name, connection);
|
|
420
427
|
}
|
|
421
428
|
|
|
422
|
-
// Wire auth refresh for HTTP transports so 401s trigger token refresh.
|
|
429
|
+
// Wire auth refresh for HTTP-like transports so 401s trigger token refresh.
|
|
423
430
|
// Gate on a resolvable managed credential, not on the auth block:
|
|
424
431
|
// definition-only configs (url-keyed fallback) get Bearer injection
|
|
425
432
|
// too and need the same mid-session refresh hook.
|
|
426
433
|
if (
|
|
427
|
-
connection.transport
|
|
434
|
+
isAuthRefreshableMCPTransport(connection.transport) &&
|
|
428
435
|
lookupMcpOAuthCredential(this.#authStorage, config)
|
|
429
436
|
) {
|
|
430
437
|
connection.transport.onAuthError = async () => {
|
|
@@ -957,9 +964,9 @@ export class MCPManager {
|
|
|
957
964
|
|
|
958
965
|
this.#connections.set(name, connection);
|
|
959
966
|
|
|
960
|
-
// Wire auth refresh for HTTP transports, and reconnect for any transport.
|
|
967
|
+
// Wire auth refresh for HTTP-like transports, and reconnect for any transport.
|
|
961
968
|
// Same gate as connectServers: any resolvable managed credential.
|
|
962
|
-
if (connection.transport
|
|
969
|
+
if (isAuthRefreshableMCPTransport(connection.transport) && lookupMcpOAuthCredential(this.#authStorage, config)) {
|
|
963
970
|
connection.transport.onAuthError = async () => {
|
|
964
971
|
const refreshed = await this.#resolveAuthConfig(config, { forceRefresh: true });
|
|
965
972
|
if (refreshed.type === "http" || refreshed.type === "sse") {
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
2
|
+
import { logger, readSseEvents, Snowflake } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import type {
|
|
4
|
+
JsonRpcError,
|
|
5
|
+
JsonRpcMessage,
|
|
6
|
+
JsonRpcRequest,
|
|
7
|
+
JsonRpcResponse,
|
|
8
|
+
MCPRequestOptions,
|
|
9
|
+
MCPSseServerConfig,
|
|
10
|
+
MCPTransport,
|
|
11
|
+
} from "../../mcp/types";
|
|
12
|
+
import { toJsonRpcError } from "../../mcp/types";
|
|
13
|
+
import { createMCPTimeout, getNeverAbortSignal, resolveMCPTimeoutMs } from "../timeout";
|
|
14
|
+
|
|
15
|
+
interface MCPTimeoutOperation {
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
clear: () => void;
|
|
18
|
+
isTimeoutAbort: (error: unknown) => boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface PendingLegacySseRequest {
|
|
22
|
+
resolve: (value: unknown) => void;
|
|
23
|
+
reject: (reason?: unknown) => void;
|
|
24
|
+
operation: MCPTimeoutOperation;
|
|
25
|
+
abortHandler?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Legacy MCP HTTP+SSE transport from protocol revision 2024-11-05. */
|
|
29
|
+
export class LegacySseTransport implements MCPTransport {
|
|
30
|
+
#connected = false;
|
|
31
|
+
#endpointUrl: string | null = null;
|
|
32
|
+
#sseConnection: AbortController | null = null;
|
|
33
|
+
#pending = new Map<string | number, PendingLegacySseRequest>();
|
|
34
|
+
#config: MCPSseServerConfig;
|
|
35
|
+
|
|
36
|
+
onClose?: () => void;
|
|
37
|
+
onError?: (error: Error) => void;
|
|
38
|
+
onNotification?: (method: string, params: unknown) => void;
|
|
39
|
+
onRequest?: (method: string, params: unknown) => Promise<unknown>;
|
|
40
|
+
/** Called on 401/403 to attempt token refresh. Returns updated headers or null. */
|
|
41
|
+
onAuthError?: () => Promise<Record<string, string> | null>;
|
|
42
|
+
|
|
43
|
+
constructor(config: MCPSseServerConfig) {
|
|
44
|
+
this.#config = config;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get connected(): boolean {
|
|
48
|
+
return this.#connected;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get url(): string {
|
|
52
|
+
return this.#config.url;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async connect(): Promise<void> {
|
|
56
|
+
if (this.#connected) return;
|
|
57
|
+
if (this.#sseConnection) return;
|
|
58
|
+
|
|
59
|
+
const connection = new AbortController();
|
|
60
|
+
const timeout = resolveMCPTimeoutMs(this.#config.timeout);
|
|
61
|
+
const operation = createMCPTimeout(timeout, connection.signal);
|
|
62
|
+
const endpointReady = Promise.withResolvers<void>();
|
|
63
|
+
this.#sseConnection = connection;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch(this.#config.url, {
|
|
67
|
+
method: "GET",
|
|
68
|
+
headers: {
|
|
69
|
+
Accept: "text/event-stream",
|
|
70
|
+
...this.#config.headers,
|
|
71
|
+
},
|
|
72
|
+
signal: operation.signal,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const text = await response.text();
|
|
77
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
78
|
+
}
|
|
79
|
+
if (!response.body) {
|
|
80
|
+
throw new Error("Legacy SSE response did not include a body");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
void this.#readSSEStream(response.body, operation, endpointReady).finally(() => {
|
|
84
|
+
const wasConnected = this.#connected;
|
|
85
|
+
if (this.#sseConnection === connection) this.#sseConnection = null;
|
|
86
|
+
if (wasConnected) this.onClose?.();
|
|
87
|
+
});
|
|
88
|
+
await endpointReady.promise;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
operation.clear();
|
|
91
|
+
if (this.#sseConnection === connection) this.#sseConnection = null;
|
|
92
|
+
connection.abort();
|
|
93
|
+
if (operation.isTimeoutAbort(error)) {
|
|
94
|
+
throw new Error(`Legacy SSE endpoint timeout after ${timeout}ms`);
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async #readSSEStream(
|
|
101
|
+
body: ReadableStream<Uint8Array>,
|
|
102
|
+
operation: MCPTimeoutOperation,
|
|
103
|
+
endpointReady: PromiseWithResolvers<void>,
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
const signal = operation.signal ?? getNeverAbortSignal();
|
|
106
|
+
let endpointReceived = false;
|
|
107
|
+
try {
|
|
108
|
+
for await (const event of readSseEvents(body, signal)) {
|
|
109
|
+
if (event.event === "endpoint") {
|
|
110
|
+
if (!this.#endpointUrl) {
|
|
111
|
+
const endpointUrl = new URL(event.data, this.#config.url);
|
|
112
|
+
const configuredUrl = new URL(this.#config.url);
|
|
113
|
+
if (endpointUrl.origin !== configuredUrl.origin) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Legacy SSE endpoint origin mismatch: expected ${configuredUrl.origin}, received ${endpointUrl.origin}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
this.#endpointUrl = endpointUrl.href;
|
|
119
|
+
this.#connected = true;
|
|
120
|
+
endpointReceived = true;
|
|
121
|
+
operation.clear();
|
|
122
|
+
endpointReady.resolve();
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (event.data === "" || event.data === "[DONE]") continue;
|
|
127
|
+
|
|
128
|
+
let payload: unknown;
|
|
129
|
+
try {
|
|
130
|
+
payload = JSON.parse(event.data) as unknown;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error instanceof SyntaxError) {
|
|
133
|
+
throw new Error(`Legacy SSE message event contained non-JSON data: ${event.data}`);
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const messages = Array.isArray(payload) ? payload : [payload];
|
|
139
|
+
for (const message of messages) {
|
|
140
|
+
if (typeof message !== "object" || message === null) continue;
|
|
141
|
+
this.#dispatchMessage(message as JsonRpcMessage);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!endpointReceived) {
|
|
145
|
+
endpointReady.reject(new Error("Legacy SSE endpoint event not received"));
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (!endpointReceived) {
|
|
149
|
+
endpointReady.reject(error);
|
|
150
|
+
} else if (error instanceof Error && error.name !== "AbortError") {
|
|
151
|
+
logger.debug("Legacy SSE stream error", { url: this.#config.url, error: error.message });
|
|
152
|
+
this.onError?.(error);
|
|
153
|
+
this.#rejectPending(error);
|
|
154
|
+
}
|
|
155
|
+
} finally {
|
|
156
|
+
operation.clear();
|
|
157
|
+
if (endpointReceived) {
|
|
158
|
+
this.#rejectPending(new Error("Transport closed: legacy SSE stream closed"));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#dispatchMessage(message: JsonRpcMessage): void {
|
|
164
|
+
if ("id" in message && ("result" in message || "error" in message)) {
|
|
165
|
+
const pending = this.#pending.get(message.id);
|
|
166
|
+
if (pending) {
|
|
167
|
+
this.#pending.delete(message.id);
|
|
168
|
+
pending.operation.clear();
|
|
169
|
+
if (pending.abortHandler) pending.operation.signal?.removeEventListener("abort", pending.abortHandler);
|
|
170
|
+
const response = message as JsonRpcResponse;
|
|
171
|
+
if (response.error) {
|
|
172
|
+
pending.reject(new Error(`MCP error ${response.error.code}: ${response.error.message}`));
|
|
173
|
+
} else {
|
|
174
|
+
pending.resolve(response.result);
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if ("method" in message && "id" in message && message.id != null) {
|
|
180
|
+
void this.#handleServerRequest(message as JsonRpcRequest);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if ("method" in message && !("id" in message)) {
|
|
184
|
+
this.onNotification?.(message.method, message.params);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async request<T = unknown>(
|
|
189
|
+
method: string,
|
|
190
|
+
params?: Record<string, unknown>,
|
|
191
|
+
options?: MCPRequestOptions,
|
|
192
|
+
): Promise<T> {
|
|
193
|
+
if (!this.#connected || !this.#endpointUrl) {
|
|
194
|
+
throw new Error("Transport not connected");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const id = Snowflake.next();
|
|
198
|
+
const body = {
|
|
199
|
+
jsonrpc: "2.0" as const,
|
|
200
|
+
id,
|
|
201
|
+
method,
|
|
202
|
+
params: params ?? {},
|
|
203
|
+
};
|
|
204
|
+
const timeout = resolveMCPTimeoutMs(this.#config.timeout);
|
|
205
|
+
const operation = createMCPTimeout(timeout, options?.signal);
|
|
206
|
+
const deferred = Promise.withResolvers<unknown>();
|
|
207
|
+
// Observe the response promise synchronously so a stream-close rejection
|
|
208
|
+
// from `#rejectPending` that lands while `request()` is still awaiting the
|
|
209
|
+
// POST round-trip is never flagged as an unhandled rejection. The real
|
|
210
|
+
// `await deferred.promise` below still receives and propagates the error.
|
|
211
|
+
void deferred.promise.catch(() => undefined);
|
|
212
|
+
const pending: PendingLegacySseRequest = {
|
|
213
|
+
resolve: deferred.resolve,
|
|
214
|
+
reject: deferred.reject,
|
|
215
|
+
operation,
|
|
216
|
+
};
|
|
217
|
+
if (operation.signal) {
|
|
218
|
+
pending.abortHandler = () => {
|
|
219
|
+
this.#pending.delete(id);
|
|
220
|
+
operation.clear();
|
|
221
|
+
deferred.reject(
|
|
222
|
+
options?.signal?.aborted && options.signal.reason instanceof Error
|
|
223
|
+
? options.signal.reason
|
|
224
|
+
: new Error(`Legacy SSE response timeout after ${timeout}ms`),
|
|
225
|
+
);
|
|
226
|
+
};
|
|
227
|
+
operation.signal.addEventListener("abort", pending.abortHandler, { once: true });
|
|
228
|
+
}
|
|
229
|
+
this.#pending.set(id, pending);
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const response = await this.#postJson(body, operation.signal);
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
const text = await response.text();
|
|
235
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
236
|
+
}
|
|
237
|
+
await response.body?.cancel();
|
|
238
|
+
return (await deferred.promise) as T;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
this.#pending.delete(id);
|
|
241
|
+
operation.clear();
|
|
242
|
+
if (pending.abortHandler) operation.signal?.removeEventListener("abort", pending.abortHandler);
|
|
243
|
+
if (operation.isTimeoutAbort(error)) {
|
|
244
|
+
throw new Error(`Request timeout after ${timeout}ms`);
|
|
245
|
+
}
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async notify(method: string, params?: Record<string, unknown>): Promise<void> {
|
|
251
|
+
if (!this.#connected || !this.#endpointUrl) {
|
|
252
|
+
throw new Error("Transport not connected");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const timeout = resolveMCPTimeoutMs(this.#config.timeout);
|
|
256
|
+
const operation = createMCPTimeout(timeout);
|
|
257
|
+
try {
|
|
258
|
+
const response = await this.#postJson(
|
|
259
|
+
{
|
|
260
|
+
jsonrpc: "2.0" as const,
|
|
261
|
+
method,
|
|
262
|
+
params: params ?? {},
|
|
263
|
+
},
|
|
264
|
+
operation.signal,
|
|
265
|
+
);
|
|
266
|
+
operation.clear();
|
|
267
|
+
if (!response.ok) {
|
|
268
|
+
const text = await response.text();
|
|
269
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
270
|
+
}
|
|
271
|
+
await response.body?.cancel();
|
|
272
|
+
} catch (error) {
|
|
273
|
+
operation.clear();
|
|
274
|
+
if (operation.isTimeoutAbort(error)) {
|
|
275
|
+
throw new Error(`Notify timeout after ${timeout}ms`);
|
|
276
|
+
}
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async #postJson(
|
|
282
|
+
body: JsonRpcRequest | JsonRpcResponse | { jsonrpc: "2.0"; method: string; params: Record<string, unknown> },
|
|
283
|
+
signal?: AbortSignal,
|
|
284
|
+
): Promise<Response> {
|
|
285
|
+
const endpointUrl = this.#endpointUrl;
|
|
286
|
+
if (!endpointUrl) throw new Error("Transport not connected");
|
|
287
|
+
let headers: Record<string, string> = {
|
|
288
|
+
"Content-Type": "application/json",
|
|
289
|
+
Accept: "application/json, text/event-stream",
|
|
290
|
+
...this.#config.headers,
|
|
291
|
+
};
|
|
292
|
+
let response = await fetch(endpointUrl, {
|
|
293
|
+
method: "POST",
|
|
294
|
+
headers,
|
|
295
|
+
body: JSON.stringify(body),
|
|
296
|
+
signal,
|
|
297
|
+
});
|
|
298
|
+
const status = AIError.status(response);
|
|
299
|
+
if (!this.onAuthError || (status !== 401 && status !== 403)) return response;
|
|
300
|
+
|
|
301
|
+
const refreshedHeaders = await this.onAuthError();
|
|
302
|
+
if (!refreshedHeaders) return response;
|
|
303
|
+
await response.body?.cancel();
|
|
304
|
+
this.#config.headers = refreshedHeaders;
|
|
305
|
+
headers = {
|
|
306
|
+
"Content-Type": "application/json",
|
|
307
|
+
Accept: "application/json, text/event-stream",
|
|
308
|
+
...this.#config.headers,
|
|
309
|
+
};
|
|
310
|
+
response = await fetch(endpointUrl, {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers,
|
|
313
|
+
body: JSON.stringify(body),
|
|
314
|
+
signal,
|
|
315
|
+
});
|
|
316
|
+
return response;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async #handleServerRequest(request: JsonRpcRequest): Promise<void> {
|
|
320
|
+
if (!this.onRequest) {
|
|
321
|
+
await this.#sendServerResponse(request.id, undefined, { code: -32601, message: "Method not found" });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const result = await this.onRequest(request.method, request.params);
|
|
326
|
+
await this.#sendServerResponse(request.id, result);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
await this.#sendServerResponse(request.id, undefined, toJsonRpcError(error));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async #sendServerResponse(id: string | number, result?: unknown, error?: JsonRpcError): Promise<void> {
|
|
333
|
+
if (!this.#connected) return;
|
|
334
|
+
const timeout = resolveMCPTimeoutMs(this.#config.timeout);
|
|
335
|
+
const operation = createMCPTimeout(timeout);
|
|
336
|
+
try {
|
|
337
|
+
const response = await this.#postJson(
|
|
338
|
+
error ? { jsonrpc: "2.0" as const, id, error } : { jsonrpc: "2.0" as const, id, result: result ?? {} },
|
|
339
|
+
operation.signal,
|
|
340
|
+
);
|
|
341
|
+
operation.clear();
|
|
342
|
+
await response.body?.cancel();
|
|
343
|
+
} catch {
|
|
344
|
+
operation.clear();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
#rejectPending(error: Error): void {
|
|
349
|
+
for (const [id, pending] of this.#pending) {
|
|
350
|
+
this.#pending.delete(id);
|
|
351
|
+
pending.operation.clear();
|
|
352
|
+
if (pending.abortHandler) pending.operation.signal?.removeEventListener("abort", pending.abortHandler);
|
|
353
|
+
pending.reject(error);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async close(): Promise<void> {
|
|
358
|
+
if (!this.#connected && !this.#sseConnection) return;
|
|
359
|
+
const wasConnected = this.#connected;
|
|
360
|
+
this.#connected = false;
|
|
361
|
+
this.#endpointUrl = null;
|
|
362
|
+
if (this.#sseConnection) {
|
|
363
|
+
this.#sseConnection.abort();
|
|
364
|
+
this.#sseConnection = null;
|
|
365
|
+
}
|
|
366
|
+
this.#rejectPending(new Error("Transport closed"));
|
|
367
|
+
if (wasConnected) this.onClose?.();
|
|
368
|
+
this.onClose = undefined;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Create and connect a legacy HTTP+SSE transport. */
|
|
373
|
+
export async function createSseTransport(config: MCPSseServerConfig): Promise<LegacySseTransport> {
|
|
374
|
+
const transport = new LegacySseTransport(config);
|
|
375
|
+
await transport.connect();
|
|
376
|
+
return transport;
|
|
377
|
+
}
|
package/src/memories/index.ts
CHANGED
|
@@ -147,16 +147,42 @@ export function startMemoryStartupTask(options: {
|
|
|
147
147
|
});
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
interface MemoryInstructionSession {
|
|
151
|
+
sessionManager: Pick<AgentSession["sessionManager"], "getSessionFile">;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface MemoryToolDeveloperInstructionsSnapshot {
|
|
155
|
+
summary: string;
|
|
156
|
+
learned: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface CachedMemoryToolDeveloperInstructions {
|
|
160
|
+
sessionFile: string | undefined;
|
|
161
|
+
snapshot: MemoryToolDeveloperInstructionsSnapshot | undefined;
|
|
162
|
+
value: string | undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const memoryToolDeveloperInstructionsBySession = new WeakMap<
|
|
166
|
+
MemoryInstructionSession,
|
|
167
|
+
CachedMemoryToolDeveloperInstructions
|
|
168
|
+
>();
|
|
169
|
+
const memoryToolDeveloperInstructionsByRoot = new Map<string, MemoryToolDeveloperInstructionsSnapshot | undefined>();
|
|
170
|
+
|
|
171
|
+
function getMemoryInstructionRoot(agentDir: string, settings: Settings): string {
|
|
172
|
+
return getMemoryRoot(agentDir, settings.getCwd());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getMemoryInstructionSessionFile(session: MemoryInstructionSession): string | undefined {
|
|
176
|
+
return session.sessionManager.getSessionFile() ?? undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function readMemoryToolDeveloperInstructionsSnapshot(
|
|
154
180
|
agentDir: string,
|
|
155
181
|
settings: Settings,
|
|
156
|
-
): Promise<
|
|
182
|
+
): Promise<MemoryToolDeveloperInstructionsSnapshot | undefined> {
|
|
157
183
|
const cfg = loadMemoryConfig(settings);
|
|
158
184
|
if (!cfg.enabled) return undefined;
|
|
159
|
-
const memoryRoot =
|
|
185
|
+
const memoryRoot = getMemoryInstructionRoot(agentDir, settings);
|
|
160
186
|
|
|
161
187
|
let summary = "";
|
|
162
188
|
try {
|
|
@@ -166,9 +192,21 @@ export async function buildMemoryToolDeveloperInstructions(
|
|
|
166
192
|
// so any captured lessons still surface on their own.
|
|
167
193
|
}
|
|
168
194
|
const learned = await readLearnedLessons(memoryRoot);
|
|
169
|
-
|
|
195
|
+
return { summary, learned };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function renderMemoryToolDeveloperInstructionsSnapshot(
|
|
199
|
+
snapshot: MemoryToolDeveloperInstructionsSnapshot | undefined,
|
|
200
|
+
settings: Settings,
|
|
201
|
+
): string | undefined {
|
|
202
|
+
if (!snapshot) return undefined;
|
|
203
|
+
const cfg = loadMemoryConfig(settings);
|
|
204
|
+
if (!cfg.enabled) return undefined;
|
|
205
|
+
if (!snapshot.summary && !snapshot.learned) return undefined;
|
|
170
206
|
|
|
171
|
-
const summaryOut = summary
|
|
207
|
+
const summaryOut = snapshot.summary
|
|
208
|
+
? truncateByApproxTokens(snapshot.summary, cfg.summaryInjectionTokenLimit).trim()
|
|
209
|
+
: "";
|
|
172
210
|
// Lessons share ONE injection budget with the summary so the combined block
|
|
173
211
|
// stays within `summaryInjectionTokenLimit` (~4 chars/token, matching
|
|
174
212
|
// truncateByApproxTokens). With no summary, lessons get the whole budget.
|
|
@@ -176,7 +214,8 @@ export async function buildMemoryToolDeveloperInstructions(
|
|
|
176
214
|
// can exceed `limit * 4` chars and drive the remainder negative — when the
|
|
177
215
|
// summary already fills the budget, lessons are simply dropped.
|
|
178
216
|
const learnedBudget = Math.max(0, cfg.summaryInjectionTokenLimit - Math.ceil(summaryOut.length / 4));
|
|
179
|
-
const learnedOut =
|
|
217
|
+
const learnedOut =
|
|
218
|
+
snapshot.learned && learnedBudget > 0 ? truncateByApproxTokens(snapshot.learned, learnedBudget).trim() : "";
|
|
180
219
|
if (!summaryOut && !learnedOut) return undefined;
|
|
181
220
|
|
|
182
221
|
return prompt.render(readPathTemplate, {
|
|
@@ -185,6 +224,72 @@ export async function buildMemoryToolDeveloperInstructions(
|
|
|
185
224
|
});
|
|
186
225
|
}
|
|
187
226
|
|
|
227
|
+
function cacheMemoryToolDeveloperInstructions(
|
|
228
|
+
session: MemoryInstructionSession,
|
|
229
|
+
sessionFile: string | undefined,
|
|
230
|
+
snapshot: MemoryToolDeveloperInstructionsSnapshot | undefined,
|
|
231
|
+
settings: Settings,
|
|
232
|
+
): string | undefined {
|
|
233
|
+
const value = renderMemoryToolDeveloperInstructionsSnapshot(snapshot, settings);
|
|
234
|
+
memoryToolDeveloperInstructionsBySession.set(session, { sessionFile, snapshot, value });
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Drop the per-session memory instruction snapshot after explicit memory state
|
|
240
|
+
* changes that must affect the active conversation immediately, such as
|
|
241
|
+
* `/memory clear`.
|
|
242
|
+
*/
|
|
243
|
+
export function clearMemoryToolDeveloperInstructionsCache(session: MemoryInstructionSession | undefined): void {
|
|
244
|
+
if (session) memoryToolDeveloperInstructionsBySession.delete(session);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Refresh the active session's consolidated-memory snapshot after startup maintenance.
|
|
249
|
+
*
|
|
250
|
+
* Startup may finish after the first prompt build and write `memory_summary.md`;
|
|
251
|
+
* the active session should see that summary. It must not reread `learned.md`,
|
|
252
|
+
* because a `learn` call racing with startup belongs to the next session's
|
|
253
|
+
* memory prompt, not the active prompt-cache prefix.
|
|
254
|
+
*/
|
|
255
|
+
export async function refreshMemoryToolDeveloperInstructionsCacheAfterStartup(
|
|
256
|
+
session: MemoryInstructionSession,
|
|
257
|
+
agentDir: string,
|
|
258
|
+
settings: Settings,
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
const sessionFile = getMemoryInstructionSessionFile(session);
|
|
261
|
+
const cached = memoryToolDeveloperInstructionsBySession.get(session);
|
|
262
|
+
const current = await readMemoryToolDeveloperInstructionsSnapshot(agentDir, settings);
|
|
263
|
+
const root = getMemoryInstructionRoot(agentDir, settings);
|
|
264
|
+
const baseline = memoryToolDeveloperInstructionsByRoot.get(root);
|
|
265
|
+
const cachedLearned = cached && cached.sessionFile === sessionFile ? cached.snapshot?.learned : undefined;
|
|
266
|
+
const learned = cachedLearned ?? baseline?.learned ?? "";
|
|
267
|
+
const snapshot = current ? { summary: current.summary, learned } : undefined;
|
|
268
|
+
cacheMemoryToolDeveloperInstructions(session, sessionFile, snapshot, settings);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Build memory usage instructions for prompt injection.
|
|
273
|
+
*/
|
|
274
|
+
export async function buildMemoryToolDeveloperInstructions(
|
|
275
|
+
agentDir: string,
|
|
276
|
+
settings: Settings,
|
|
277
|
+
session?: MemoryInstructionSession,
|
|
278
|
+
): Promise<string | undefined> {
|
|
279
|
+
if (!session) {
|
|
280
|
+
const snapshot = await readMemoryToolDeveloperInstructionsSnapshot(agentDir, settings);
|
|
281
|
+
memoryToolDeveloperInstructionsByRoot.set(getMemoryInstructionRoot(agentDir, settings), snapshot);
|
|
282
|
+
return renderMemoryToolDeveloperInstructionsSnapshot(snapshot, settings);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const sessionFile = getMemoryInstructionSessionFile(session);
|
|
286
|
+
const cached = memoryToolDeveloperInstructionsBySession.get(session);
|
|
287
|
+
if (cached && cached.sessionFile === sessionFile) return cached.value;
|
|
288
|
+
|
|
289
|
+
const snapshot = await readMemoryToolDeveloperInstructionsSnapshot(agentDir, settings);
|
|
290
|
+
return cacheMemoryToolDeveloperInstructions(session, sessionFile, snapshot, settings);
|
|
291
|
+
}
|
|
292
|
+
|
|
188
293
|
/**
|
|
189
294
|
* Clear all persisted memory state and generated artifacts.
|
|
190
295
|
*/
|
|
@@ -219,6 +324,7 @@ async function runMemoryStartup(options: {
|
|
|
219
324
|
}): Promise<void> {
|
|
220
325
|
await runPhase1(options);
|
|
221
326
|
await runPhase2(options);
|
|
327
|
+
await refreshMemoryToolDeveloperInstructionsCacheAfterStartup(options.session, options.agentDir, options.settings);
|
|
222
328
|
await options.session.refreshBaseSystemPrompt?.();
|
|
223
329
|
}
|
|
224
330
|
|
|
@@ -529,12 +635,21 @@ async function collectThreads(session: AgentSession, currentThreadId?: string):
|
|
|
529
635
|
let id = name.slice(0, -6);
|
|
530
636
|
try {
|
|
531
637
|
const fileText = await Bun.file(fullPath).text();
|
|
532
|
-
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
638
|
+
let sawTitleSlot = false;
|
|
639
|
+
for (const rawLine of fileText.split(/\r?\n/)) {
|
|
640
|
+
const line = rawLine.trim();
|
|
641
|
+
if (!line) continue;
|
|
642
|
+
const parsed = parseJsonlLenient<Record<string, unknown>>(line);
|
|
643
|
+
const header = Array.isArray(parsed) && parsed.length > 0 ? parsed[0] : undefined;
|
|
644
|
+
if (!sawTitleSlot && header?.type === "title") {
|
|
645
|
+
sawTitleSlot = true;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (header?.type === "session") {
|
|
649
|
+
if (typeof header.cwd === "string") cwd = header.cwd;
|
|
650
|
+
if (typeof header.id === "string") id = header.id;
|
|
651
|
+
}
|
|
652
|
+
break;
|
|
538
653
|
}
|
|
539
654
|
} catch {
|
|
540
655
|
// ignore malformed session files
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildMemoryToolDeveloperInstructions,
|
|
3
3
|
clearMemoryData,
|
|
4
|
+
clearMemoryToolDeveloperInstructionsCache,
|
|
4
5
|
enqueueMemoryConsolidation,
|
|
5
6
|
saveLearnedLesson,
|
|
6
7
|
startMemoryStartupTask,
|
|
@@ -20,10 +21,11 @@ export const localBackend: MemoryBackend = {
|
|
|
20
21
|
start(options) {
|
|
21
22
|
startMemoryStartupTask(options);
|
|
22
23
|
},
|
|
23
|
-
async buildDeveloperInstructions(agentDir, settings) {
|
|
24
|
-
return buildMemoryToolDeveloperInstructions(agentDir, settings);
|
|
24
|
+
async buildDeveloperInstructions(agentDir, settings, session) {
|
|
25
|
+
return buildMemoryToolDeveloperInstructions(agentDir, settings, session);
|
|
25
26
|
},
|
|
26
|
-
async clear(agentDir, cwd) {
|
|
27
|
+
async clear(agentDir, cwd, session) {
|
|
28
|
+
clearMemoryToolDeveloperInstructionsCache(session);
|
|
27
29
|
await clearMemoryData(agentDir, cwd);
|
|
28
30
|
},
|
|
29
31
|
async enqueue(agentDir, cwd) {
|