@oh-my-pi/pi-coding-agent 16.2.1 → 16.2.3

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 (187) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist/cli.js +3621 -3579
  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/runtime.d.ts +15 -1
  8. package/dist/types/advisor/transcript-recorder.d.ts +13 -2
  9. package/dist/types/advisor/watchdog.d.ts +20 -0
  10. package/dist/types/collab/guest.d.ts +29 -0
  11. package/dist/types/config/model-roles.d.ts +1 -1
  12. package/dist/types/config/settings-schema.d.ts +113 -12
  13. package/dist/types/debug/log-viewer.d.ts +1 -0
  14. package/dist/types/debug/raw-sse.d.ts +1 -0
  15. package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
  16. package/dist/types/edit/hashline/diff.d.ts +0 -11
  17. package/dist/types/edit/index.d.ts +18 -0
  18. package/dist/types/edit/streaming.d.ts +30 -0
  19. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  20. package/dist/types/extensibility/shared-events.d.ts +1 -0
  21. package/dist/types/extensibility/tool-event-input.d.ts +7 -0
  22. package/dist/types/extensibility/utils.d.ts +12 -0
  23. package/dist/types/mcp/oauth-discovery.d.ts +0 -11
  24. package/dist/types/mcp/transports/index.d.ts +1 -0
  25. package/dist/types/mcp/transports/sse.d.ts +20 -0
  26. package/dist/types/modes/components/advisor-config.d.ts +59 -0
  27. package/dist/types/modes/components/index.d.ts +1 -0
  28. package/dist/types/modes/components/model-selector.d.ts +9 -1
  29. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  30. package/dist/types/modes/components/status-line/component.d.ts +30 -3
  31. package/dist/types/modes/components/status-line/types.d.ts +13 -1
  32. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  33. package/dist/types/modes/interactive-mode.d.ts +10 -4
  34. package/dist/types/modes/skill-command.d.ts +32 -0
  35. package/dist/types/modes/types.d.ts +7 -2
  36. package/dist/types/sdk.d.ts +1 -1
  37. package/dist/types/session/agent-session.d.ts +84 -12
  38. package/dist/types/session/indexed-session-storage.d.ts +7 -1
  39. package/dist/types/session/messages.d.ts +32 -7
  40. package/dist/types/session/messages.test.d.ts +1 -0
  41. package/dist/types/session/session-entries.d.ts +31 -3
  42. package/dist/types/session/session-history-format.d.ts +6 -0
  43. package/dist/types/session/session-loader.d.ts +9 -1
  44. package/dist/types/session/session-manager.d.ts +6 -4
  45. package/dist/types/session/session-storage.d.ts +11 -0
  46. package/dist/types/session/session-title-slot.d.ts +19 -0
  47. package/dist/types/session/settings-stream-fn.d.ts +21 -0
  48. package/dist/types/session/turn-persistence.d.ts +88 -0
  49. package/dist/types/ssh/connection-manager.d.ts +47 -0
  50. package/dist/types/ssh/utils.d.ts +16 -0
  51. package/dist/types/task/executor.d.ts +3 -16
  52. package/dist/types/task/render.d.ts +0 -5
  53. package/dist/types/task/renderer.d.ts +13 -0
  54. package/dist/types/task/types.d.ts +16 -0
  55. package/dist/types/task/yield-assembly.d.ts +28 -0
  56. package/dist/types/tiny/text.d.ts +8 -0
  57. package/dist/types/tools/render-utils.d.ts +2 -0
  58. package/dist/types/tools/review.d.ts +6 -4
  59. package/dist/types/tools/ssh.d.ts +1 -1
  60. package/dist/types/tools/todo.d.ts +6 -0
  61. package/dist/types/tools/yield.d.ts +8 -3
  62. package/dist/types/utils/thinking-display.d.ts +4 -0
  63. package/package.json +12 -12
  64. package/src/advisor/__tests__/advisor.test.ts +438 -10
  65. package/src/advisor/__tests__/config.test.ts +173 -0
  66. package/src/advisor/advise-tool.ts +11 -6
  67. package/src/advisor/config.ts +256 -0
  68. package/src/advisor/index.ts +1 -0
  69. package/src/advisor/runtime.ts +77 -4
  70. package/src/advisor/transcript-recorder.ts +25 -2
  71. package/src/advisor/watchdog.ts +57 -31
  72. package/src/auto-thinking/classifier.ts +2 -2
  73. package/src/autoresearch/index.ts +7 -2
  74. package/src/cli/gc-cli.ts +17 -10
  75. package/src/collab/guest.ts +43 -7
  76. package/src/config/model-registry.ts +80 -18
  77. package/src/config/model-resolver.ts +5 -1
  78. package/src/config/model-roles.ts +3 -3
  79. package/src/config/settings-schema.ts +107 -8
  80. package/src/debug/index.ts +32 -7
  81. package/src/debug/log-viewer.ts +111 -53
  82. package/src/debug/raw-sse.ts +68 -48
  83. package/src/discovery/codex.ts +13 -5
  84. package/src/discovery/omp-extension-roots.ts +38 -13
  85. package/src/edit/hashline/diff.ts +57 -4
  86. package/src/edit/index.ts +21 -0
  87. package/src/edit/streaming.ts +170 -0
  88. package/src/eval/js/shared/local-module-loader.ts +23 -1
  89. package/src/export/html/template.js +13 -7
  90. package/src/extensibility/custom-tools/types.ts +1 -0
  91. package/src/extensibility/extensions/loader.ts +5 -3
  92. package/src/extensibility/extensions/wrapper.ts +9 -3
  93. package/src/extensibility/hooks/loader.ts +3 -3
  94. package/src/extensibility/hooks/tool-wrapper.ts +13 -4
  95. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
  96. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
  97. package/src/extensibility/plugins/manager.ts +76 -5
  98. package/src/extensibility/shared-events.ts +1 -0
  99. package/src/extensibility/tool-event-input.ts +23 -0
  100. package/src/extensibility/utils.ts +74 -0
  101. package/src/internal-urls/docs-index.generated.txt +1 -1
  102. package/src/mcp/client.ts +3 -1
  103. package/src/mcp/manager.ts +12 -5
  104. package/src/mcp/oauth-discovery.ts +5 -29
  105. package/src/mcp/transports/http.ts +3 -1
  106. package/src/mcp/transports/index.ts +1 -0
  107. package/src/mcp/transports/sse.ts +377 -0
  108. package/src/memories/index.ts +15 -6
  109. package/src/mnemopi/backend.ts +2 -2
  110. package/src/modes/acp/acp-agent.ts +1 -1
  111. package/src/modes/components/advisor-config.ts +555 -0
  112. package/src/modes/components/advisor-message.ts +9 -2
  113. package/src/modes/components/agent-hub.ts +9 -4
  114. package/src/modes/components/assistant-message.ts +5 -5
  115. package/src/modes/components/index.ts +2 -0
  116. package/src/modes/components/model-selector.ts +79 -48
  117. package/src/modes/components/settings-selector.ts +1 -0
  118. package/src/modes/components/status-line/component.ts +145 -14
  119. package/src/modes/components/status-line/segments.ts +47 -22
  120. package/src/modes/components/status-line/types.ts +13 -1
  121. package/src/modes/components/tool-execution.ts +47 -6
  122. package/src/modes/controllers/command-controller.ts +23 -2
  123. package/src/modes/controllers/event-controller.ts +114 -11
  124. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  125. package/src/modes/controllers/input-controller.ts +61 -61
  126. package/src/modes/controllers/selector-controller.ts +100 -9
  127. package/src/modes/interactive-mode.ts +65 -10
  128. package/src/modes/print-mode.ts +1 -1
  129. package/src/modes/skill-command.ts +116 -0
  130. package/src/modes/types.ts +7 -2
  131. package/src/modes/utils/transcript-render-helpers.ts +2 -2
  132. package/src/modes/utils/ui-helpers.ts +46 -27
  133. package/src/prompts/agents/reviewer.md +11 -10
  134. package/src/prompts/review-custom-request.md +1 -2
  135. package/src/prompts/review-request.md +1 -2
  136. package/src/prompts/system/interrupted-thinking.md +7 -0
  137. package/src/prompts/system/recap-user.md +9 -0
  138. package/src/prompts/system/subagent-system-prompt.md +8 -5
  139. package/src/prompts/system/subagent-yield-reminder.md +6 -5
  140. package/src/prompts/system/system-prompt.md +0 -1
  141. package/src/prompts/tools/irc.md +2 -2
  142. package/src/prompts/tools/read.md +2 -2
  143. package/src/sdk.ts +40 -50
  144. package/src/session/agent-session.ts +1139 -600
  145. package/src/session/indexed-session-storage.ts +86 -13
  146. package/src/session/messages.test.ts +125 -0
  147. package/src/session/messages.ts +192 -21
  148. package/src/session/redis-session-storage.ts +49 -2
  149. package/src/session/session-entries.ts +39 -2
  150. package/src/session/session-history-format.ts +29 -2
  151. package/src/session/session-listing.ts +54 -24
  152. package/src/session/session-loader.ts +66 -3
  153. package/src/session/session-manager.ts +113 -19
  154. package/src/session/session-persistence.ts +96 -3
  155. package/src/session/session-storage.ts +36 -0
  156. package/src/session/session-title-slot.ts +141 -0
  157. package/src/session/settings-stream-fn.ts +49 -0
  158. package/src/session/sql-session-storage.ts +71 -11
  159. package/src/session/turn-persistence.ts +142 -0
  160. package/src/session/unexpected-stop-classifier.ts +2 -2
  161. package/src/slash-commands/builtin-registry.ts +16 -3
  162. package/src/slash-commands/helpers/mcp.ts +2 -1
  163. package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
  164. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
  165. package/src/ssh/connection-manager.ts +139 -12
  166. package/src/ssh/file-transfer.ts +23 -18
  167. package/src/ssh/ssh-executor.ts +2 -13
  168. package/src/ssh/utils.ts +19 -0
  169. package/src/task/executor.ts +21 -23
  170. package/src/task/render.ts +162 -20
  171. package/src/task/renderer.ts +14 -0
  172. package/src/task/types.ts +17 -0
  173. package/src/task/yield-assembly.ts +207 -0
  174. package/src/tiny/models.ts +8 -6
  175. package/src/tiny/text.ts +37 -7
  176. package/src/tools/ask.ts +55 -4
  177. package/src/tools/image-gen.ts +2 -1
  178. package/src/tools/render-utils.ts +2 -0
  179. package/src/tools/renderers.ts +8 -2
  180. package/src/tools/review.ts +17 -7
  181. package/src/tools/ssh.ts +8 -4
  182. package/src/tools/todo.ts +17 -1
  183. package/src/tools/tts.ts +2 -1
  184. package/src/tools/yield.ts +140 -31
  185. package/src/utils/thinking-display.ts +15 -0
  186. package/src/utils/title-generator.ts +1 -1
  187. package/src/web/search/providers/tavily.ts +36 -19
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") {
@@ -4,6 +4,7 @@
4
4
  * Automatically detects OAuth requirements from MCP server responses
5
5
  * and extracts authentication endpoints.
6
6
  */
7
+ import * as AIError from "@oh-my-pi/pi-ai/error";
7
8
  import type { FetchImpl } from "@oh-my-pi/pi-ai/types";
8
9
 
9
10
  export interface OAuthEndpoints {
@@ -23,8 +24,8 @@ export interface AuthDetectionResult {
23
24
  message?: string;
24
25
  }
25
26
 
26
- function parseMcpAuthServerUrl(errorMessage: string, serverUrl?: string): string | undefined {
27
- const match = errorMessage.match(/Mcp-Auth-Server:\s*([^;\]\s]+)/i);
27
+ export function extractMcpAuthServerUrl(error: Error, serverUrl?: string): string | undefined {
28
+ const match = error.message.match(/Mcp-Auth-Server:\s*([^;\]\s]+)/i);
28
29
  if (!match?.[1]) return undefined;
29
30
 
30
31
  try {
@@ -34,32 +35,6 @@ function parseMcpAuthServerUrl(errorMessage: string, serverUrl?: string): string
34
35
  }
35
36
  }
36
37
 
37
- export function extractMcpAuthServerUrl(error: Error, serverUrl?: string): string | undefined {
38
- return parseMcpAuthServerUrl(error.message, serverUrl);
39
- }
40
-
41
- /**
42
- * Detect if an error indicates authentication is required.
43
- * Checks for common auth error patterns.
44
- */
45
- export function detectAuthError(error: Error): boolean {
46
- const errorMsg = error.message.toLowerCase();
47
-
48
- // Check for HTTP auth status codes
49
- if (
50
- errorMsg.includes("401") ||
51
- errorMsg.includes("403") ||
52
- errorMsg.includes("unauthorized") ||
53
- errorMsg.includes("forbidden") ||
54
- errorMsg.includes("authentication required") ||
55
- errorMsg.includes("authentication failed")
56
- ) {
57
- return true;
58
- }
59
-
60
- return false;
61
- }
62
-
63
38
  /**
64
39
  * Extract OAuth endpoints from error response.
65
40
  * Looks for WWW-Authenticate header format or JSON error bodies.
@@ -200,7 +175,8 @@ export function extractOAuthEndpoints(error: Error): OAuthEndpoints | null {
200
175
  * Returns structured info about what auth is needed.
201
176
  */
202
177
  export function analyzeAuthError(error: Error, serverUrl?: string): AuthDetectionResult {
203
- if (!detectAuthError(error)) {
178
+ // No auth required unless the error carries an HTTP auth status / auth-failure phrasing.
179
+ if (!AIError.is(AIError.classify(error), AIError.Flag.AuthFailed)) {
204
180
  return { requiresAuth: false };
205
181
  }
206
182
 
@@ -4,6 +4,7 @@
4
4
  * Implements JSON-RPC 2.0 over HTTP POST with optional SSE streaming.
5
5
  * Based on MCP spec 2025-03-26.
6
6
  */
7
+ import * as AIError from "@oh-my-pi/pi-ai/error";
7
8
  import { logger, readSseJson, Snowflake } from "@oh-my-pi/pi-utils";
8
9
  import type {
9
10
  JsonRpcError,
@@ -186,7 +187,8 @@ export class HttpTransport implements MCPTransport {
186
187
  return await this.#executeRequest<T>(method, params, options);
187
188
  } catch (error) {
188
189
  // Retry once on auth failure if onAuthError is wired
189
- if (this.onAuthError && error instanceof Error && /^HTTP (401|403):/.test(error.message)) {
190
+ const status = error instanceof Error ? AIError.status(error) : undefined;
191
+ if (this.onAuthError && (status === 401 || status === 403)) {
190
192
  const newHeaders = await this.onAuthError();
191
193
  if (newHeaders) {
192
194
  // Persist refreshed headers so subsequent requests use them directly
@@ -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
+ }
@@ -529,12 +529,21 @@ async function collectThreads(session: AgentSession, currentThreadId?: string):
529
529
  let id = name.slice(0, -6);
530
530
  try {
531
531
  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;
532
+ let sawTitleSlot = false;
533
+ for (const rawLine of fileText.split(/\r?\n/)) {
534
+ const line = rawLine.trim();
535
+ if (!line) continue;
536
+ const parsed = parseJsonlLenient<Record<string, unknown>>(line);
537
+ const header = Array.isArray(parsed) && parsed.length > 0 ? parsed[0] : undefined;
538
+ if (!sawTitleSlot && header?.type === "title") {
539
+ sawTitleSlot = true;
540
+ continue;
541
+ }
542
+ if (header?.type === "session") {
543
+ if (typeof header.cwd === "string") cwd = header.cwd;
544
+ if (typeof header.id === "string") id = header.id;
545
+ }
546
+ break;
538
547
  }
539
548
  } catch {
540
549
  // ignore malformed session files
@@ -511,10 +511,10 @@ async function resolveMnemopiProviderOptions(
511
511
  }
512
512
 
513
513
  try {
514
- const resolved = resolveRoleSelection(["smol"], settings, modelRegistry.getAvailable(), modelRegistry);
514
+ const resolved = resolveRoleSelection(["tiny", "smol"], settings, modelRegistry.getAvailable(), modelRegistry);
515
515
  const model = resolved?.model;
516
516
  if (!model) {
517
- logger.warn("Mnemopi: llmMode=smol but no smol model resolved; continuing without LLM.");
517
+ logger.warn("Mnemopi: llmMode=smol but no tiny/smol model resolved; continuing without LLM.");
518
518
  return base;
519
519
  }
520
520
  return {
@@ -2014,7 +2014,7 @@ export class AcpAgent implements Agent {
2014
2014
  }
2015
2015
  }
2016
2016
  }
2017
- if (notifications.length === 0 && message.errorMessage && !isSilentAbort(message.errorMessage)) {
2017
+ if (notifications.length === 0 && message.errorMessage && !isSilentAbort(message)) {
2018
2018
  notifications.push({
2019
2019
  sessionId,
2020
2020
  update: {