@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.
Files changed (164) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/cli.js +3682 -3615
  3. package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
  4. package/dist/types/advisor/advise-tool.d.ts +8 -4
  5. package/dist/types/advisor/config.d.ts +88 -0
  6. package/dist/types/advisor/index.d.ts +1 -0
  7. package/dist/types/advisor/transcript-recorder.d.ts +13 -2
  8. package/dist/types/advisor/watchdog.d.ts +20 -0
  9. package/dist/types/cli/update-cli.d.ts +15 -0
  10. package/dist/types/collab/guest.d.ts +29 -0
  11. package/dist/types/collab/replication-shrink.d.ts +39 -0
  12. package/dist/types/config/provider-globals.d.ts +7 -0
  13. package/dist/types/config/settings-schema.d.ts +81 -0
  14. package/dist/types/debug/log-viewer.d.ts +1 -0
  15. package/dist/types/debug/raw-sse.d.ts +1 -0
  16. package/dist/types/edit/hashline/diff.d.ts +0 -11
  17. package/dist/types/extensibility/tool-event-input.d.ts +7 -0
  18. package/dist/types/extensibility/utils.d.ts +12 -0
  19. package/dist/types/mcp/transports/index.d.ts +1 -0
  20. package/dist/types/mcp/transports/sse.d.ts +20 -0
  21. package/dist/types/memories/index.d.ts +20 -1
  22. package/dist/types/modes/components/advisor-config.d.ts +59 -0
  23. package/dist/types/modes/components/index.d.ts +1 -0
  24. package/dist/types/modes/components/model-selector.d.ts +9 -1
  25. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  26. package/dist/types/modes/components/status-line/component.d.ts +35 -1
  27. package/dist/types/modes/components/status-line/types.d.ts +13 -1
  28. package/dist/types/modes/controllers/command-controller.d.ts +3 -3
  29. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  30. package/dist/types/modes/interactive-mode.d.ts +11 -4
  31. package/dist/types/modes/skill-command.d.ts +32 -0
  32. package/dist/types/modes/types.d.ts +9 -3
  33. package/dist/types/session/agent-session.d.ts +58 -10
  34. package/dist/types/session/indexed-session-storage.d.ts +7 -1
  35. package/dist/types/session/messages.d.ts +26 -0
  36. package/dist/types/session/messages.test.d.ts +1 -0
  37. package/dist/types/session/session-entries.d.ts +31 -3
  38. package/dist/types/session/session-history-format.d.ts +6 -0
  39. package/dist/types/session/session-loader.d.ts +9 -1
  40. package/dist/types/session/session-manager.d.ts +8 -7
  41. package/dist/types/session/session-storage.d.ts +11 -0
  42. package/dist/types/session/session-title-slot.d.ts +19 -0
  43. package/dist/types/ssh/connection-manager.d.ts +47 -0
  44. package/dist/types/ssh/utils.d.ts +16 -0
  45. package/dist/types/task/executor.d.ts +3 -16
  46. package/dist/types/task/render.d.ts +0 -5
  47. package/dist/types/task/renderer.d.ts +13 -0
  48. package/dist/types/task/types.d.ts +16 -0
  49. package/dist/types/task/yield-assembly.d.ts +28 -0
  50. package/dist/types/tiny/text.d.ts +8 -0
  51. package/dist/types/tools/render-utils.d.ts +2 -0
  52. package/dist/types/tools/review.d.ts +6 -4
  53. package/dist/types/tools/ssh.d.ts +1 -1
  54. package/dist/types/tools/todo.d.ts +6 -0
  55. package/dist/types/tools/yield.d.ts +8 -3
  56. package/dist/types/utils/thinking-display.d.ts +4 -0
  57. package/package.json +12 -12
  58. package/src/advisor/__tests__/advisor.test.ts +242 -10
  59. package/src/advisor/__tests__/config.test.ts +173 -0
  60. package/src/advisor/advise-tool.ts +11 -6
  61. package/src/advisor/config.ts +256 -0
  62. package/src/advisor/index.ts +1 -0
  63. package/src/advisor/runtime.ts +12 -2
  64. package/src/advisor/transcript-recorder.ts +25 -2
  65. package/src/advisor/watchdog.ts +57 -31
  66. package/src/autolearn/controller.ts +13 -22
  67. package/src/autoresearch/index.ts +7 -2
  68. package/src/cli/gc-cli.ts +17 -10
  69. package/src/cli/update-cli.ts +254 -0
  70. package/src/collab/guest.ts +43 -7
  71. package/src/collab/host.ts +13 -8
  72. package/src/collab/replication-shrink.ts +111 -0
  73. package/src/config/model-registry.ts +80 -18
  74. package/src/config/provider-globals.ts +25 -0
  75. package/src/config/settings-schema.ts +77 -0
  76. package/src/debug/index.ts +32 -7
  77. package/src/debug/log-viewer.ts +111 -53
  78. package/src/debug/raw-sse.ts +68 -48
  79. package/src/discovery/codex.ts +13 -5
  80. package/src/edit/hashline/diff.ts +57 -4
  81. package/src/eval/__tests__/julia-prelude.test.ts +2 -2
  82. package/src/eval/js/shared/local-module-loader.ts +23 -1
  83. package/src/export/html/template.js +13 -7
  84. package/src/extensibility/extensions/loader.ts +5 -3
  85. package/src/extensibility/extensions/wrapper.ts +9 -3
  86. package/src/extensibility/hooks/loader.ts +3 -3
  87. package/src/extensibility/hooks/tool-wrapper.ts +13 -4
  88. package/src/extensibility/plugins/manager.ts +2 -1
  89. package/src/extensibility/tool-event-input.ts +23 -0
  90. package/src/extensibility/utils.ts +74 -0
  91. package/src/internal-urls/docs-index.generated.txt +1 -1
  92. package/src/mcp/client.ts +3 -1
  93. package/src/mcp/manager.ts +12 -5
  94. package/src/mcp/transports/index.ts +1 -0
  95. package/src/mcp/transports/sse.ts +377 -0
  96. package/src/memories/index.ts +130 -15
  97. package/src/memory-backend/local-backend.ts +5 -3
  98. package/src/modes/components/advisor-config.ts +555 -0
  99. package/src/modes/components/advisor-message.ts +9 -2
  100. package/src/modes/components/agent-hub.ts +9 -4
  101. package/src/modes/components/index.ts +2 -0
  102. package/src/modes/components/model-selector.ts +79 -48
  103. package/src/modes/components/settings-selector.ts +1 -0
  104. package/src/modes/components/status-line/component.ts +150 -5
  105. package/src/modes/components/status-line/segments.ts +46 -21
  106. package/src/modes/components/status-line/types.ts +13 -1
  107. package/src/modes/components/tool-execution.ts +47 -6
  108. package/src/modes/controllers/command-controller.ts +27 -36
  109. package/src/modes/controllers/event-controller.ts +113 -1
  110. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  111. package/src/modes/controllers/input-controller.ts +61 -61
  112. package/src/modes/controllers/selector-controller.ts +100 -9
  113. package/src/modes/interactive-mode.ts +74 -25
  114. package/src/modes/skill-command.ts +116 -0
  115. package/src/modes/types.ts +9 -3
  116. package/src/modes/utils/ui-helpers.ts +41 -23
  117. package/src/prompts/agents/reviewer.md +11 -10
  118. package/src/prompts/goals/goal-todo-context.md +12 -0
  119. package/src/prompts/review-custom-request.md +1 -2
  120. package/src/prompts/review-request.md +1 -2
  121. package/src/prompts/system/interrupted-thinking.md +7 -0
  122. package/src/prompts/system/recap-user.md +9 -0
  123. package/src/prompts/system/subagent-system-prompt.md +8 -5
  124. package/src/prompts/system/subagent-yield-reminder.md +6 -5
  125. package/src/prompts/system/system-prompt.md +0 -1
  126. package/src/prompts/tools/irc.md +2 -2
  127. package/src/prompts/tools/read.md +2 -2
  128. package/src/sdk.ts +28 -24
  129. package/src/session/agent-session.ts +899 -429
  130. package/src/session/indexed-session-storage.ts +86 -13
  131. package/src/session/messages.test.ts +125 -0
  132. package/src/session/messages.ts +172 -9
  133. package/src/session/redis-session-storage.ts +49 -2
  134. package/src/session/session-entries.ts +39 -2
  135. package/src/session/session-history-format.ts +29 -2
  136. package/src/session/session-listing.ts +54 -24
  137. package/src/session/session-loader.ts +66 -3
  138. package/src/session/session-manager.ts +115 -22
  139. package/src/session/session-persistence.ts +95 -1
  140. package/src/session/session-storage.ts +36 -0
  141. package/src/session/session-title-slot.ts +141 -0
  142. package/src/session/sql-session-storage.ts +71 -11
  143. package/src/slash-commands/builtin-registry.ts +23 -24
  144. package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
  145. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
  146. package/src/ssh/connection-manager.ts +139 -12
  147. package/src/ssh/file-transfer.ts +23 -18
  148. package/src/ssh/ssh-executor.ts +2 -13
  149. package/src/ssh/utils.ts +19 -0
  150. package/src/task/executor.ts +21 -23
  151. package/src/task/render.ts +162 -20
  152. package/src/task/renderer.ts +14 -0
  153. package/src/task/types.ts +17 -0
  154. package/src/task/yield-assembly.ts +207 -0
  155. package/src/tiny/text.ts +23 -0
  156. package/src/tools/ask.ts +55 -4
  157. package/src/tools/render-utils.ts +2 -0
  158. package/src/tools/renderers.ts +8 -2
  159. package/src/tools/review.ts +17 -7
  160. package/src/tools/ssh.ts +8 -4
  161. package/src/tools/todo.ts +17 -1
  162. package/src/tools/yield.ts +140 -31
  163. package/src/utils/thinking-display.ts +15 -0
  164. 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 createHttpTransport(config as MCPHttpServerConfig | MCPSseServerConfig);
83
+ return createSseTransport(config as MCPSseServerConfig);
82
84
  default:
83
85
  throw new Error(`Unknown server type: ${serverType}`);
84
86
  }
@@ -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 instanceof HttpTransport &&
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 instanceof HttpTransport && lookupMcpOAuthCredential(this.#authStorage, config)) {
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") {
@@ -3,4 +3,5 @@
3
3
  */
4
4
 
5
5
  export * from "./http";
6
+ export * from "./sse";
6
7
  export * from "./stdio";
@@ -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
+ }
@@ -147,16 +147,42 @@ export function startMemoryStartupTask(options: {
147
147
  });
148
148
  }
149
149
 
150
- /**
151
- * Build memory usage instructions for prompt injection.
152
- */
153
- export async function buildMemoryToolDeveloperInstructions(
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<string | undefined> {
182
+ ): Promise<MemoryToolDeveloperInstructionsSnapshot | undefined> {
157
183
  const cfg = loadMemoryConfig(settings);
158
184
  if (!cfg.enabled) return undefined;
159
- const memoryRoot = getMemoryRoot(agentDir, settings.getCwd());
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
- if (!summary && !learned) return undefined;
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 ? truncateByApproxTokens(summary, cfg.summaryInjectionTokenLimit).trim() : "";
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 = learned && learnedBudget > 0 ? truncateByApproxTokens(learned, learnedBudget).trim() : "";
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
- const firstLine = fileText.split("\n", 1)[0] ?? "";
533
- const parsed = parseJsonlLenient(firstLine);
534
- const header = Array.isArray(parsed) && parsed.length > 0 ? (parsed[0] as Record<string, unknown>) : undefined;
535
- if (header && header.type === "session") {
536
- if (typeof header.cwd === "string") cwd = header.cwd;
537
- if (typeof header.id === "string") id = header.id;
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) {