@oh-my-pi/pi-coding-agent 17.2.13 → 17.2.15

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 (239) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/{CHANGELOG-d8xh7keh.md → CHANGELOG-fk29zh06.md} +30 -0
  3. package/dist/cli.js +5114 -5417
  4. package/dist/types/cleanse/agent.d.ts +3 -0
  5. package/dist/types/cleanse/checkers.d.ts +21 -1
  6. package/dist/types/cleanse/index.d.ts +4 -0
  7. package/dist/types/cleanse/parsers.d.ts +3 -1
  8. package/dist/types/cli/args.d.ts +1 -0
  9. package/dist/types/cli/cleanse-picker.d.ts +17 -0
  10. package/dist/types/cli/command-help.d.ts +3 -0
  11. package/dist/types/cli/progress-reporter.d.ts +19 -0
  12. package/dist/types/commands/cleanse.d.ts +11 -0
  13. package/dist/types/commands/compress.d.ts +37 -0
  14. package/dist/types/commands/launch-help.d.ts +3 -0
  15. package/dist/types/commands/launch.d.ts +3 -0
  16. package/dist/types/compress/index.d.ts +27 -0
  17. package/dist/types/compress/protocol.d.ts +49 -0
  18. package/dist/types/compress/session.d.ts +15 -0
  19. package/dist/types/compress/types.d.ts +54 -0
  20. package/dist/types/config/settings-schema.d.ts +10 -0
  21. package/dist/types/internal-urls/local-protocol.d.ts +8 -0
  22. package/dist/types/mcp/transports/header-policy.d.ts +8 -0
  23. package/dist/types/mcp/transports/http.d.ts +2 -0
  24. package/dist/types/mcp/types.d.ts +19 -0
  25. package/dist/types/modes/components/late-diagnostics-message.d.ts +2 -0
  26. package/dist/types/modes/components/todo-reminder.d.ts +2 -0
  27. package/dist/types/modes/components/tool-activity.d.ts +17 -0
  28. package/dist/types/modes/components/transcript-container.d.ts +2 -0
  29. package/dist/types/modes/components/ttsr-notification.d.ts +2 -0
  30. package/dist/types/modes/interactive-mode.d.ts +3 -1
  31. package/dist/types/modes/types.d.ts +3 -1
  32. package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -1
  33. package/dist/types/modes/utils/ui-helpers.d.ts +3 -1
  34. package/dist/types/session/agent-session-types.d.ts +2 -0
  35. package/dist/types/session/agent-session.d.ts +2 -0
  36. package/dist/types/session/session-tools.d.ts +13 -0
  37. package/dist/types/tools/builtin-names.d.ts +1 -2
  38. package/dist/types/tools/index.d.ts +1 -0
  39. package/dist/types/tools/think.d.ts +41 -0
  40. package/dist/types/utils/zip.d.ts +19 -9
  41. package/package.json +13 -13
  42. package/src/cleanse/agent.ts +67 -2
  43. package/src/cleanse/checkers.ts +252 -21
  44. package/src/cleanse/index.ts +90 -36
  45. package/src/cleanse/parsers.ts +243 -20
  46. package/src/cleanse/prompts/discovery.md +74 -0
  47. package/src/cli/args.ts +3 -0
  48. package/src/cli/cleanse-picker.ts +86 -0
  49. package/src/cli/command-help.ts +4 -0
  50. package/src/cli/flag-tables.ts +1 -0
  51. package/src/cli/gallery-fixtures/agentic.ts +16 -0
  52. package/src/{cleanse/progress.ts → cli/progress-reporter.ts} +12 -7
  53. package/src/cli-commands.ts +5 -0
  54. package/src/commands/cleanse.ts +19 -4
  55. package/src/commands/compress.ts +45 -0
  56. package/src/commands/launch-help.ts +3 -0
  57. package/src/commit/agentic/prompts/analyze-file.md +6 -6
  58. package/src/commit/agentic/prompts/session-user.md +4 -4
  59. package/src/compress/index.ts +318 -0
  60. package/src/compress/prompts/request.md +11 -0
  61. package/src/compress/prompts/review.md +17 -0
  62. package/src/compress/prompts/system.md +81 -0
  63. package/src/compress/protocol.ts +210 -0
  64. package/src/compress/session.ts +72 -0
  65. package/src/compress/types.ts +59 -0
  66. package/src/config/settings-schema.ts +11 -0
  67. package/src/discovery/builtin-rules/go-add-cleanup.md +8 -8
  68. package/src/discovery/builtin-rules/go-exp-promoted.md +8 -8
  69. package/src/discovery/builtin-rules/go-ioutil.md +12 -12
  70. package/src/discovery/builtin-rules/go-new-expr.md +8 -8
  71. package/src/discovery/builtin-rules/go-range-int.md +5 -5
  72. package/src/discovery/builtin-rules/rs-box-leak.md +7 -7
  73. package/src/discovery/builtin-rules/rs-future-prelude.md +4 -4
  74. package/src/discovery/builtin-rules/rs-parking-lot.md +6 -6
  75. package/src/discovery/builtin-rules/ts-no-any.md +5 -5
  76. package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +8 -8
  77. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +13 -14
  78. package/src/discovery/builtin-rules/ts-no-local-is-record.md +7 -7
  79. package/src/discovery/builtin-rules/ts-no-test-timers.md +3 -9
  80. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +7 -7
  81. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +2 -2
  82. package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +3 -9
  83. package/src/discovery/builtin-rules/ts-set-map.md +2 -4
  84. package/src/internal-urls/local-protocol.ts +42 -0
  85. package/src/live/prompts/live-instructions.md +10 -10
  86. package/src/main.ts +4 -0
  87. package/src/mcp/client.ts +7 -3
  88. package/src/mcp/transports/header-policy.ts +28 -0
  89. package/src/mcp/transports/http.ts +200 -35
  90. package/src/mcp/types.ts +21 -0
  91. package/src/modes/components/chat-transcript-builder.ts +17 -5
  92. package/src/modes/components/late-diagnostics-message.ts +12 -1
  93. package/src/modes/components/todo-reminder.ts +12 -0
  94. package/src/modes/components/tool-activity.ts +45 -0
  95. package/src/modes/components/transcript-container.ts +16 -0
  96. package/src/modes/components/ttsr-notification.ts +12 -0
  97. package/src/modes/controllers/event-controller.ts +3 -6
  98. package/src/modes/controllers/input-controller.ts +6 -6
  99. package/src/modes/controllers/selector-controller.ts +8 -6
  100. package/src/modes/interactive-mode.ts +5 -40
  101. package/src/modes/print-mode.ts +13 -54
  102. package/src/modes/types.ts +1 -1
  103. package/src/modes/utils/transcript-render-helpers.ts +3 -2
  104. package/src/modes/utils/ui-helpers.ts +17 -7
  105. package/src/prompts/advisor/active-repo-watchdog.md +3 -4
  106. package/src/prompts/advisor/advise-tool.md +3 -3
  107. package/src/prompts/advisor/context-files.md +1 -1
  108. package/src/prompts/advisor/system.md +45 -65
  109. package/src/prompts/agents/designer.md +44 -44
  110. package/src/prompts/agents/init.md +18 -18
  111. package/src/prompts/agents/librarian.md +31 -31
  112. package/src/prompts/agents/reviewer.md +34 -39
  113. package/src/prompts/agents/security-reviewer.md +3 -5
  114. package/src/prompts/agents/task.md +11 -12
  115. package/src/prompts/bench.md +2 -5
  116. package/src/prompts/ci-green-request.md +13 -15
  117. package/src/prompts/dry-balance-bench.md +5 -5
  118. package/src/prompts/goals/goal-budget-limit.md +4 -5
  119. package/src/prompts/goals/goal-continuation.md +11 -11
  120. package/src/prompts/goals/goal-mode-active.md +7 -7
  121. package/src/prompts/goals/goal-todo-context.md +2 -2
  122. package/src/prompts/goals/guided-goal-interview.md +18 -24
  123. package/src/prompts/memories/read-path.md +9 -9
  124. package/src/prompts/memories/stage_one_system.md +9 -11
  125. package/src/prompts/review-custom-request.md +8 -11
  126. package/src/prompts/review-headless-request.md +3 -10
  127. package/src/prompts/security/scan-coordinator.md +6 -5
  128. package/src/prompts/security/validate-request.md +4 -7
  129. package/src/prompts/skills/user-invocation.md +2 -2
  130. package/src/prompts/steering/parent-irc.md +1 -1
  131. package/src/prompts/steering/user-interjection.md +1 -3
  132. package/src/prompts/system/active-repo-context.md +4 -2
  133. package/src/prompts/system/agent-creation-architect.md +22 -37
  134. package/src/prompts/system/agent-creation-user.md +3 -3
  135. package/src/prompts/system/auto-continue.md +1 -1
  136. package/src/prompts/system/auto-thinking-difficulty-local.md +5 -7
  137. package/src/prompts/system/auto-thinking-difficulty.md +8 -10
  138. package/src/prompts/system/autolearn-guidance-learn.md +2 -1
  139. package/src/prompts/system/autolearn-guidance.md +5 -4
  140. package/src/prompts/system/autolearn-nudge-autocontinue.md +3 -3
  141. package/src/prompts/system/background-tan-dispatch.md +3 -3
  142. package/src/prompts/system/btw-user.md +2 -2
  143. package/src/prompts/system/commit-message-system.md +8 -6
  144. package/src/prompts/system/eager-task.md +3 -3
  145. package/src/prompts/system/empty-stop-retry.md +1 -1
  146. package/src/prompts/system/gemini-tool-call-reminder.md +5 -5
  147. package/src/prompts/system/interrupted-thinking.md +3 -3
  148. package/src/prompts/system/irc-autoreply.md +1 -1
  149. package/src/prompts/system/irc-incoming.md +3 -3
  150. package/src/prompts/system/manual-continue.md +3 -3
  151. package/src/prompts/system/mcp-xdev-guidance.md +2 -2
  152. package/src/prompts/system/memory-consolidation-system.md +2 -2
  153. package/src/prompts/system/mid-run-todo-nudge.md +1 -1
  154. package/src/prompts/system/orchestrate-notice.md +25 -25
  155. package/src/prompts/system/personalities/default.md +9 -9
  156. package/src/prompts/system/personalities/friendly.md +11 -11
  157. package/src/prompts/system/personalities/pragmatic.md +8 -8
  158. package/src/prompts/system/plan-mode-active.md +61 -64
  159. package/src/prompts/system/plan-mode-approved.md +10 -11
  160. package/src/prompts/system/plan-mode-compact-instructions.md +11 -11
  161. package/src/prompts/system/plan-mode-reference.md +5 -5
  162. package/src/prompts/system/plan-yolo-handoff.md +2 -2
  163. package/src/prompts/system/prewalk-checklist.md +5 -5
  164. package/src/prompts/system/prewalk-continue.md +1 -1
  165. package/src/prompts/system/prewalk-plan.md +8 -9
  166. package/src/prompts/system/project-prompt.md +11 -12
  167. package/src/prompts/system/recap-user.md +1 -1
  168. package/src/prompts/system/resolve-device-reminder.md +1 -1
  169. package/src/prompts/system/rewind-report.md +3 -3
  170. package/src/prompts/system/side-channel-no-tools.md +3 -1
  171. package/src/prompts/system/snapcompact-context-stub.md +1 -1
  172. package/src/prompts/system/snapcompact-system-frames-note.md +1 -1
  173. package/src/prompts/system/snapcompact-system-stub.md +1 -1
  174. package/src/prompts/system/snapcompact-toolresult-note.md +1 -1
  175. package/src/prompts/system/speech-rewrite.md +11 -13
  176. package/src/prompts/system/subagent-async-pending.md +5 -5
  177. package/src/prompts/system/subagent-system-prompt.md +5 -15
  178. package/src/prompts/system/subagent-user-prompt.md +1 -1
  179. package/src/prompts/system/subagent-yield-reminder.md +12 -12
  180. package/src/prompts/system/system-prompt.md +110 -136
  181. package/src/prompts/system/tan-context-switch.md +8 -14
  182. package/src/prompts/system/task-label.md +3 -3
  183. package/src/prompts/system/thinking-loop-redirect.md +6 -6
  184. package/src/prompts/system/title-marker-instruction.md +2 -1
  185. package/src/prompts/system/title-system.md +3 -5
  186. package/src/prompts/system/ttsr-interrupt.md +3 -3
  187. package/src/prompts/system/ttsr-tool-reminder.md +1 -1
  188. package/src/prompts/system/ultrathink-notice.md +1 -1
  189. package/src/prompts/system/unexpected-stop-classifier.md +3 -3
  190. package/src/prompts/system/vibe-mode-active.md +15 -15
  191. package/src/prompts/system/web-search.md +16 -16
  192. package/src/prompts/system/workflow-notice.md +35 -33
  193. package/src/prompts/system/xdev-mount-notice.md +4 -4
  194. package/src/prompts/tools/apply-patch.md +22 -23
  195. package/src/prompts/tools/approve.md +5 -0
  196. package/src/prompts/tools/ask.md +10 -10
  197. package/src/prompts/tools/checkpoint.md +7 -7
  198. package/src/prompts/tools/computer.md +16 -16
  199. package/src/prompts/tools/github.md +12 -12
  200. package/src/prompts/tools/goal.md +8 -9
  201. package/src/prompts/tools/grep.md +5 -5
  202. package/src/prompts/tools/image-attachment-describe-system.md +6 -6
  203. package/src/prompts/tools/image-attachment-describe.md +3 -8
  204. package/src/prompts/tools/image-gen.md +4 -4
  205. package/src/prompts/tools/inspect-image-system.md +10 -10
  206. package/src/prompts/tools/inspect-image.md +10 -13
  207. package/src/prompts/tools/learn.md +4 -4
  208. package/src/prompts/tools/manage-skill.md +9 -6
  209. package/src/prompts/tools/memory-edit.md +8 -8
  210. package/src/prompts/tools/recall.md +4 -4
  211. package/src/prompts/tools/reflect.md +2 -2
  212. package/src/prompts/tools/replace.md +13 -11
  213. package/src/prompts/tools/retain.md +3 -4
  214. package/src/prompts/tools/rewind.md +7 -8
  215. package/src/prompts/tools/rewrite.md +12 -0
  216. package/src/prompts/tools/security-publish.md +5 -1
  217. package/src/prompts/tools/security-scan.md +10 -1
  218. package/src/prompts/tools/task-async-contract.md +7 -1
  219. package/src/prompts/tools/todo.md +27 -25
  220. package/src/prompts/tools/vibe-kill.md +2 -2
  221. package/src/prompts/tools/vibe-list.md +2 -2
  222. package/src/prompts/tools/vibe-send.md +6 -7
  223. package/src/prompts/tools/vibe-spawn.md +8 -6
  224. package/src/prompts/tools/web-search.md +5 -5
  225. package/src/sdk.ts +10 -1
  226. package/src/session/agent-session-types.ts +2 -0
  227. package/src/session/agent-session.ts +28 -0
  228. package/src/session/session-handoff.ts +21 -0
  229. package/src/session/session-tools.ts +47 -0
  230. package/src/tools/builtin-names.ts +1 -2
  231. package/src/tools/index.ts +10 -0
  232. package/src/tools/read-format.ts +8 -3
  233. package/src/tools/read.ts +18 -7
  234. package/src/tools/renderers.ts +2 -0
  235. package/src/tools/think.ts +84 -0
  236. package/src/tools/write.ts +1 -1
  237. package/src/utils/file-mentions.ts +8 -2
  238. package/src/utils/zip.ts +858 -94
  239. package/dist/types/cleanse/progress.d.ts +0 -14
@@ -2,10 +2,11 @@
2
2
  * MCP HTTP transport (Streamable HTTP).
3
3
  *
4
4
  * Implements JSON-RPC 2.0 over HTTP POST with optional SSE streaming.
5
- * Based on MCP spec 2025-03-26.
5
+ * The negotiated protocol revision is carried in the `MCP-Protocol-Version`
6
+ * header on every request (see `MCP_PROTOCOL_VERSION`).
6
7
  */
7
8
  import * as AIError from "@oh-my-pi/pi-ai/error";
8
- import { logger, readSseJson } from "@oh-my-pi/pi-utils";
9
+ import { logger, readSseEvents, readSseJson } from "@oh-my-pi/pi-utils";
9
10
  import type {
10
11
  JsonRpcError,
11
12
  JsonRpcMessage,
@@ -19,9 +20,37 @@ import type {
19
20
  import { toJsonRpcError } from "../../mcp/types";
20
21
  import { RequestIdAllocator } from "../request-id";
21
22
  import { createMCPTimeout, getNeverAbortSignal, isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "../timeout";
22
- import { type MCPFetchInit, mcpFetch } from "./header-policy";
23
+ import { type MCPFetchInit, mcpFetch, withoutHeader } from "./header-policy";
23
24
 
24
25
  const HTTP_SSE_CONNECT_TIMEOUT_MS = 1_000;
26
+ const DEFAULT_SSE_RETRY_MS = 3_000;
27
+
28
+ interface SSEResumeState {
29
+ lastEventId: string | null;
30
+ retryMs: number;
31
+ }
32
+
33
+ /**
34
+ * Failure resuming an accepted request's logical SSE stream. Carries a
35
+ * never-replay contract: by resume time the server has accepted (and possibly
36
+ * executed) the originating POST, so auth-retry paths must not re-send it.
37
+ */
38
+ class SSEResumeError extends Error {}
39
+
40
+ /** Wait for the server-provided SSE retry interval while remaining abortable. */
41
+ async function waitForSSERetry(ms: number, signal: AbortSignal): Promise<void> {
42
+ if (signal.aborted) throw signal.reason;
43
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
44
+ const timer = setTimeout(resolve, ms);
45
+ const onAbort = (): void => reject(signal.reason);
46
+ signal.addEventListener("abort", onAbort, { once: true });
47
+ try {
48
+ await promise;
49
+ } finally {
50
+ clearTimeout(timer);
51
+ signal.removeEventListener("abort", onAbort);
52
+ }
53
+ }
25
54
  /**
26
55
  * Best-effort startup deadline for the optional Streamable HTTP GET SSE listener.
27
56
  *
@@ -45,6 +74,14 @@ export class HttpTransport implements MCPTransport {
45
74
  #sessionId: string | null = null;
46
75
  #sseConnection: AbortController | null = null;
47
76
  readonly #requestIds = new RequestIdAllocator();
77
+ /**
78
+ * Protocol version echoed in the `MCP-Protocol-Version` header. `null` until
79
+ * the `initialize` response is negotiated (via {@link setProtocolVersion}):
80
+ * the MCP spec requires the header only on requests *after* `initialize`, and
81
+ * a server that supports only an older revision may reject a header carrying
82
+ * a newer version sent before negotiation completes.
83
+ */
84
+ #protocolVersion: string | null = null;
48
85
 
49
86
  onClose?: () => void;
50
87
  onError?: (error: Error) => void;
@@ -55,16 +92,32 @@ export class HttpTransport implements MCPTransport {
55
92
 
56
93
  constructor(private config: MCPHttpServerConfig | MCPSseServerConfig) {}
57
94
 
58
- /** Fetch the configured endpoint with header precedence and origin policy. */
95
+ /**
96
+ * Fetch the configured endpoint with header precedence and origin policy.
97
+ *
98
+ * The transport fully owns `MCP-Protocol-Version`: it is stripped from
99
+ * configured headers so a user's `mcp.json` can never inject it, and added
100
+ * only once a version is negotiated (required by the MCP Streamable HTTP spec
101
+ * after `initialize`). Before negotiation — the `initialize` request itself —
102
+ * no protocol-version header is sent from either source.
103
+ */
59
104
  #fetch(init: MCPFetchInit, generated: Record<string, string>): Promise<Response> {
105
+ const configured = withoutHeader(this.config.headers, "MCP-Protocol-Version");
106
+ const withVersion =
107
+ this.#protocolVersion === null ? generated : { "MCP-Protocol-Version": this.#protocolVersion, ...generated };
60
108
  return mcpFetch(
61
109
  this.config.url,
62
110
  init,
63
- { generated, configured: this.config.headers },
111
+ { generated: withVersion, configured },
64
112
  this.config.headerPolicy === "origin-locked",
65
113
  );
66
114
  }
67
115
 
116
+ /** Record the protocol version negotiated during `initialize`. */
117
+ setProtocolVersion(version: string): void {
118
+ this.#protocolVersion = version;
119
+ }
120
+
68
121
  get connected(): boolean {
69
122
  return this.#connected;
70
123
  }
@@ -149,7 +202,7 @@ export class HttpTransport implements MCPTransport {
149
202
  // If the stream ends unexpectedly (server restart, network drop),
150
203
  // fire onClose so the manager can trigger reconnection.
151
204
  const signal = connection.signal;
152
- void this.#readSSEStream(response.body!, signal).finally(() => {
205
+ void this.#runSSEListener(response.body!, signal).finally(() => {
153
206
  const wasConnected = this.#connected;
154
207
  if (this.#sseConnection === connection) this.#sseConnection = null;
155
208
  if (wasConnected) this.onClose?.();
@@ -169,6 +222,95 @@ export class HttpTransport implements MCPTransport {
169
222
  }
170
223
  }
171
224
 
225
+ /**
226
+ * Read the long-lived GET SSE stream, resuming with `Last-Event-ID` when
227
+ * the server closes the physical connection mid-stream (2025-11-25 permits
228
+ * polling-style servers). Returns only when the logical stream ends — the
229
+ * caller fires `onClose` and the manager's reconnect path takes over. A
230
+ * resume cycle that delivers no events before dropping again ends the
231
+ * stream rather than retrying forever against a broken server.
232
+ */
233
+ async #runSSEListener(initialBody: ReadableStream<Uint8Array>, signal: AbortSignal): Promise<void> {
234
+ const resume: SSEResumeState = { lastEventId: null, retryMs: DEFAULT_SSE_RETRY_MS };
235
+ let body = initialBody;
236
+ let progressed = true;
237
+ for (;;) {
238
+ try {
239
+ for await (const event of readSseEvents(body, signal)) {
240
+ progressed = true;
241
+ if (event.id !== undefined) resume.lastEventId = event.id || null;
242
+ if (event.retry !== undefined) resume.retryMs = event.retry;
243
+ if (event.data === "") continue;
244
+ if (!this.#connected) return;
245
+ this.#dispatchSSEMessage(JSON.parse(event.data) as JsonRpcMessage | JsonRpcMessage[]);
246
+ }
247
+ } catch (error) {
248
+ if (error instanceof Error && error.name === "AbortError") return;
249
+ logger.debug("HTTP SSE stream error", {
250
+ url: this.config.url,
251
+ error: error instanceof Error ? error.message : String(error),
252
+ });
253
+ if (resume.lastEventId === null) {
254
+ if (error instanceof Error) this.onError?.(error);
255
+ return;
256
+ }
257
+ }
258
+ if (!this.#connected || signal.aborted || resume.lastEventId === null || !progressed) return;
259
+ progressed = false;
260
+ try {
261
+ const response = await this.#fetchSSEResume(resume, signal);
262
+ body = response.body as ReadableStream<Uint8Array>;
263
+ } catch (error) {
264
+ if (!(error instanceof Error && error.name === "AbortError")) {
265
+ logger.debug("HTTP SSE listener resume failed", {
266
+ url: this.config.url,
267
+ error: error instanceof Error ? error.message : String(error),
268
+ });
269
+ }
270
+ return;
271
+ }
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Resume a logical SSE stream via GET + `Last-Event-ID`, honoring the
277
+ * server-provided retry interval and refreshing auth once on 401/403.
278
+ * Failures throw {@link SSEResumeError} so `request()` never replays the
279
+ * originating POST in response.
280
+ */
281
+ async #fetchSSEResume(resume: SSEResumeState, signal: AbortSignal): Promise<Response> {
282
+ if (resume.lastEventId === null) {
283
+ throw new SSEResumeError("SSE stream ended without a resumable event ID");
284
+ }
285
+ await waitForSSERetry(resume.retryMs, signal);
286
+ const generated: Record<string, string> = {
287
+ Accept: "text/event-stream",
288
+ "Last-Event-ID": resume.lastEventId,
289
+ };
290
+ if (this.#sessionId) generated["Mcp-Session-Id"] = this.#sessionId;
291
+ let response = await this.#fetch({ method: "GET", signal }, generated);
292
+ if (this.onAuthError && (response.status === 401 || response.status === 403)) {
293
+ await response.body?.cancel();
294
+ const newHeaders = await this.onAuthError();
295
+ if (!newHeaders) {
296
+ throw new SSEResumeError(`HTTP ${response.status} resuming MCP SSE stream: auth refresh failed`);
297
+ }
298
+ // Persist refreshed headers so subsequent requests use them directly
299
+ this.config = { ...this.config, headers: newHeaders };
300
+ response = await this.#fetch({ method: "GET", signal }, generated);
301
+ }
302
+ if (!response.ok) {
303
+ const text = await response.text().catch(() => "");
304
+ throw new SSEResumeError(`HTTP ${response.status} resuming MCP SSE stream: ${text}`);
305
+ }
306
+ const contentType = response.headers.get("Content-Type") ?? "";
307
+ if (!contentType.includes("text/event-stream") || !response.body) {
308
+ await response.body?.cancel();
309
+ throw new SSEResumeError(`MCP SSE resume returned unsupported Content-Type: ${contentType || "(missing)"}`);
310
+ }
311
+ return response;
312
+ }
313
+
172
314
  /** Route an SSE message (or batch) to the appropriate handler. */
173
315
  #dispatchSSEMessage(message: JsonRpcMessage | JsonRpcMessage[]): void {
174
316
  if (Array.isArray(message)) {
@@ -194,9 +336,12 @@ export class HttpTransport implements MCPTransport {
194
336
  try {
195
337
  return await this.#executeRequest<T>(method, params, options);
196
338
  } catch (error) {
197
- // Retry once on auth failure if onAuthError is wired
339
+ // Retry once on auth failure if onAuthError is wired. Never replay
340
+ // after an SSE resume failure: the server already accepted the
341
+ // original POST and may have executed it — replaying could run a
342
+ // state-changing tool twice.
198
343
  const status = error instanceof Error ? AIError.status(error) : undefined;
199
- if (this.onAuthError && (status === 401 || status === 403)) {
344
+ if (!(error instanceof SSEResumeError) && this.onAuthError && (status === 401 || status === 403)) {
200
345
  const newHeaders = await this.onAuthError();
201
346
  if (newHeaders) {
202
347
  // Persist refreshed headers so subsequent requests use them directly
@@ -298,40 +443,60 @@ export class HttpTransport implements MCPTransport {
298
443
  const signal = operation.signal ?? getNeverAbortSignal();
299
444
 
300
445
  const { promise, resolve, reject } = Promise.withResolvers<T>();
446
+ const resume: SSEResumeState = { lastEventId: null, retryMs: DEFAULT_SSE_RETRY_MS };
301
447
  let captured = false;
302
448
 
303
- // Drain the SSE stream from a single iterator. We resolve the deferred
304
- // promise as soon as the matching response arrives, then keep iterating
305
- // in the background to pick up piggybacked notifications/requests.
306
- // Re-reading `response.body` after `for await` breaks would lock the
307
- // stream a second time and surface as "ReadableStream already has a
308
- // controller", so we must not exit the loop early.
449
+ // Drain each physical SSE connection without leaving its iterator early.
450
+ // A server may close a connection without terminating the logical stream;
451
+ // when it supplied an event ID, resume that stream via GET + Last-Event-ID.
309
452
  const drain = async (): Promise<void> => {
453
+ let current = response;
310
454
  try {
311
- for await (const raw of readSseJson<JsonRpcMessage | JsonRpcMessage[]>(response.body!, signal)) {
312
- const messages = Array.isArray(raw) ? raw : [raw];
313
- for (const message of messages) {
314
- if (
315
- !captured &&
316
- "id" in message &&
317
- message.id === expectedId &&
318
- ("result" in message || "error" in message)
319
- ) {
320
- captured = true;
321
- operation.clear();
322
- if (message.error) {
323
- reject(new Error(`MCP error ${message.error.code}: ${message.error.message}`));
324
- } else {
325
- resolve(message.result as T);
455
+ for (;;) {
456
+ if (!current.body) throw new Error("SSE response did not include a body");
457
+ try {
458
+ for await (const event of readSseEvents(current.body, signal)) {
459
+ if (event.id !== undefined) resume.lastEventId = event.id || null;
460
+ if (event.retry !== undefined) resume.retryMs = event.retry;
461
+ if (event.data === "") continue;
462
+ const raw = JSON.parse(event.data) as JsonRpcMessage | JsonRpcMessage[];
463
+ const messages = Array.isArray(raw) ? raw : [raw];
464
+ for (const message of messages) {
465
+ if (
466
+ !captured &&
467
+ "id" in message &&
468
+ message.id === expectedId &&
469
+ ("result" in message || "error" in message)
470
+ ) {
471
+ captured = true;
472
+ operation.clear();
473
+ if (message.error) {
474
+ reject(new Error(`MCP error ${message.error.code}: ${message.error.message}`));
475
+ } else {
476
+ resolve(message.result as T);
477
+ }
478
+ continue;
479
+ }
480
+ if (!this.#connected) continue;
481
+ this.#dispatchSSEMessage(message);
326
482
  }
327
- continue;
328
483
  }
329
- if (!this.#connected) continue;
330
- this.#dispatchSSEMessage(message);
484
+ } catch (error) {
485
+ // An abrupt drop (socket reset, body-read failure) is as
486
+ // resumable as a server-initiated close once an event ID
487
+ // exists; the request timeout still bounds the total wait.
488
+ if (captured) return;
489
+ if (signal.aborted || resume.lastEventId === null) throw error;
490
+ logger.debug("MCP SSE response stream dropped; resuming", {
491
+ url: this.config.url,
492
+ error: error instanceof Error ? error.message : String(error),
493
+ });
331
494
  }
332
- }
333
- if (!captured) {
334
- reject(new Error(`No response received for request ID ${expectedId}`));
495
+ if (captured) return;
496
+ if (resume.lastEventId === null) {
497
+ throw new Error(`No response received for request ID ${expectedId}`);
498
+ }
499
+ current = await this.#fetchSSEResume(resume, signal);
335
500
  }
336
501
  } catch (error) {
337
502
  if (captured) return;
package/src/mcp/types.ts CHANGED
@@ -151,6 +151,19 @@ export interface MCPConfigFile {
151
151
  // MCP Protocol Types
152
152
  // =============================================================================
153
153
 
154
+ /**
155
+ * Latest MCP protocol revision this client negotiates.
156
+ *
157
+ * Sent as `protocolVersion` in the `initialize` request and, for Streamable
158
+ * HTTP, echoed back to the server in the `MCP-Protocol-Version` header on every
159
+ * subsequent request (per the MCP HTTP transport spec). Must track the current
160
+ * stable revision: AWS Bedrock AgentCore Gateway refuses tool calls on an
161
+ * outbound per-user OAuth (`AUTHORIZATION_CODE`) target below `2025-11-25`, and
162
+ * it checks the version *before* consulting its token vault, so an older client
163
+ * is refused even for a caller whose consent is already stored.
164
+ */
165
+ export const MCP_PROTOCOL_VERSION = "2025-11-25";
166
+
154
167
  /** MCP implementation info */
155
168
  export interface MCPImplementation {
156
169
  name: string;
@@ -269,6 +282,14 @@ export interface MCPTransport {
269
282
  /** Close the transport */
270
283
  close(): Promise<void>;
271
284
 
285
+ /**
286
+ * Record the protocol version negotiated in the `initialize` response.
287
+ * Streamable HTTP transports echo it in the `MCP-Protocol-Version` header on
288
+ * every subsequent request; transports that need no per-request version
289
+ * (stdio) omit this.
290
+ */
291
+ setProtocolVersion?(version: string): void;
292
+
272
293
  /** Whether the transport is connected */
273
294
  readonly connected: boolean;
274
295
 
@@ -18,6 +18,7 @@ import type { AdvisorMessageDetails } from "../../advisor";
18
18
  import { COLLAB_PROMPT_MESSAGE_TYPE, type CollabPromptDetails } from "../../collab/protocol";
19
19
  import { settings } from "../../config/settings";
20
20
  import type { MessageRenderer } from "../../extensibility/extensions/types";
21
+ import { LAUNCH_COMPLETION_MESSAGE_TYPE } from "../../session/launch-completion";
21
22
  import {
22
23
  BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE,
23
24
  type CustomMessage,
@@ -53,6 +54,7 @@ import { EvalExecutionComponent } from "./eval-execution";
53
54
  import { type LateDiagnosticsFile, LateDiagnosticsMessageComponent } from "./late-diagnostics-message";
54
55
  import { groupedReadUsageCallIds, ReadToolGroupComponent, readArgsCollapseIntoGroup } from "./read-tool-group";
55
56
  import { SkillMessageComponent } from "./skill-message";
57
+ import { ToolActivityContainer } from "./tool-activity";
56
58
  import { ToolExecutionComponent } from "./tool-execution";
57
59
  import { TranscriptContainer } from "./transcript-container";
58
60
  import { createUsageRowBlock } from "./usage-row";
@@ -95,7 +97,9 @@ export class ChatTranscriptBuilder {
95
97
  #expandables: Array<{ setExpanded(expanded: boolean): void }> = [];
96
98
  #expanded = false;
97
99
 
98
- constructor(private readonly deps: ChatTranscriptBuilderDeps) {}
100
+ constructor(private readonly deps: ChatTranscriptBuilderDeps) {
101
+ this.container.setToolActivityVisible(!settings.get("display.hideToolActivity"));
102
+ }
99
103
 
100
104
  /** Whether the transcript currently holds any rendered rows. */
101
105
  get isEmpty(): boolean {
@@ -192,7 +196,6 @@ export class ChatTranscriptBuilder {
192
196
  this.#readGroup = new ReadToolGroupComponent({
193
197
  showContentPreview: settings.get("read.toolResultPreview"),
194
198
  });
195
- this.#readGroup.setToolActivityVisible(!settings.get("display.hideToolActivity"));
196
199
  this.#trackExpandable(this.#readGroup);
197
200
  this.container.addChild(this.#readGroup);
198
201
  }
@@ -407,7 +410,6 @@ export class ChatTranscriptBuilder {
407
410
  this.deps.cwd,
408
411
  content.id,
409
412
  );
410
- component.setToolActivityVisible(!settings.get("display.hideToolActivity"));
411
413
  this.#trackExpandable(component);
412
414
  this.container.addChild(component);
413
415
 
@@ -464,11 +466,11 @@ export class ChatTranscriptBuilder {
464
466
  this.#todoSnapshot = pending;
465
467
  }
466
468
  }
467
-
468
469
  #appendCustomMessage(message: Extract<AgentMessage, { role: "custom" | "hookMessage" }>): void {
469
470
  if (!message.display) return;
470
471
  if (message.customType === "async-result") {
471
- this.container.addChild(buildAsyncResultBlock(message));
472
+ const component = buildAsyncResultBlock(message);
473
+ this.container.addChild(component);
472
474
  return;
473
475
  }
474
476
  if (message.customType === LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE) {
@@ -501,6 +503,16 @@ export class ChatTranscriptBuilder {
501
503
  this.container.addChild(createAdvisorMessageCard(details, () => this.#expanded, theme));
502
504
  return;
503
505
  }
506
+ if (message.customType === LAUNCH_COMPLETION_MESSAGE_TYPE) {
507
+ const messageComponent = new CustomMessageComponent(
508
+ message as CustomMessage<unknown>,
509
+ this.deps.getMessageRenderer?.(message.customType),
510
+ );
511
+ this.#trackExpandable(messageComponent);
512
+ const component = new ToolActivityContainer(messageComponent);
513
+ this.container.addChild(component);
514
+ return;
515
+ }
504
516
  if (message.customType === BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE) {
505
517
  this.container.addChild(createBackgroundTanDispatchBlock(message as CustomMessage<unknown>));
506
518
  return;
@@ -17,7 +17,7 @@ export interface LateDiagnosticsFile {
17
17
  */
18
18
  export class LateDiagnosticsMessageComponent extends Container {
19
19
  #expanded = false;
20
-
20
+ #toolActivityVisible = true;
21
21
  constructor(private readonly files: LateDiagnosticsFile[]) {
22
22
  super();
23
23
  this.#rebuild();
@@ -29,6 +29,17 @@ export class LateDiagnosticsMessageComponent extends Container {
29
29
  this.#rebuild();
30
30
  }
31
31
 
32
+ setToolActivityVisible(visible: boolean): void {
33
+ if (this.#toolActivityVisible === visible) return;
34
+ this.#toolActivityVisible = visible;
35
+ this.invalidate();
36
+ }
37
+
38
+ override render(width: number): readonly string[] {
39
+ if (!this.#toolActivityVisible) return [];
40
+ return super.render(width);
41
+ }
42
+
32
43
  override invalidate(): void {
33
44
  super.invalidate();
34
45
  this.#rebuild();
@@ -10,6 +10,7 @@ import type { TodoItem } from "../../tools/todo";
10
10
  */
11
11
  export class TodoReminderComponent extends Container {
12
12
  #box: Box;
13
+ #toolActivityVisible = true;
13
14
 
14
15
  constructor(
15
16
  private readonly todos: TodoItem[],
@@ -27,6 +28,17 @@ export class TodoReminderComponent extends Container {
27
28
  this.#rebuild();
28
29
  }
29
30
 
31
+ setToolActivityVisible(visible: boolean): void {
32
+ if (this.#toolActivityVisible === visible) return;
33
+ this.#toolActivityVisible = visible;
34
+ this.invalidate();
35
+ }
36
+
37
+ override render(width: number): readonly string[] {
38
+ if (!this.#toolActivityVisible) return [];
39
+ return super.render(width);
40
+ }
41
+
30
42
  #rebuild(): void {
31
43
  this.#box.clear();
32
44
 
@@ -0,0 +1,45 @@
1
+ import { type Component, Container } from "@oh-my-pi/pi-tui";
2
+
3
+ export interface ToolActivityComponent {
4
+ setToolActivityVisible(visible: boolean): void;
5
+ }
6
+
7
+ export function isToolActivityComponent(component: Component): component is Component & ToolActivityComponent {
8
+ return typeof (component as Partial<ToolActivityComponent>).setToolActivityVisible === "function";
9
+ }
10
+
11
+ export class ToolActivityContainer extends Container implements ToolActivityComponent {
12
+ #visible = true;
13
+
14
+ constructor(component: Component | Component[]) {
15
+ super();
16
+ if (Array.isArray(component)) {
17
+ for (const child of component) this.addChild(child);
18
+ } else {
19
+ this.addChild(component);
20
+ }
21
+ }
22
+
23
+ setToolActivityVisible(visible: boolean): void {
24
+ if (this.#visible === visible) return;
25
+ this.#visible = visible;
26
+ this.invalidate();
27
+ }
28
+
29
+ /**
30
+ * Forward Ctrl+O expansion to wrapped children. The transcript's expansion
31
+ * traversal only visits top-level children, so the wrapper must proxy or
32
+ * wrapped renderers would freeze at their insertion-time expansion state.
33
+ */
34
+ setExpanded(expanded: boolean): void {
35
+ for (const child of this.children) {
36
+ const expandable = child as Partial<{ setExpanded(expanded: boolean): void }>;
37
+ if (typeof expandable.setExpanded === "function") expandable.setExpanded(expanded);
38
+ }
39
+ }
40
+
41
+ override render(width: number): readonly string[] {
42
+ if (!this.#visible) return [];
43
+ return super.render(width);
44
+ }
45
+ }
@@ -6,6 +6,7 @@ import {
6
6
  type RenderStablePrefix,
7
7
  type ViewportTailProvider,
8
8
  } from "@oh-my-pi/pi-tui";
9
+ import { isToolActivityComponent } from "./tool-activity";
9
10
 
10
11
  /**
11
12
  * A transcript block that is still mutating (a foreground tool awaiting its
@@ -160,6 +161,7 @@ export class TranscriptContainer
160
161
  extends Container
161
162
  implements NativeScrollbackLiveRegion, NativeScrollbackCommittedRows, RenderStablePrefix, ViewportTailProvider
162
163
  {
164
+ #toolActivityVisible = true;
163
165
  // Bumped to retire every block segment at once (theme change / clear); a
164
166
  // segment is only reused when its stored generation matches.
165
167
  #generation = 0;
@@ -182,6 +184,20 @@ export class TranscriptContainer
182
184
  // consumes the report and re-bases the baseline). Out-of-band renders
183
185
  // between engine frames lower it; they can never inflate it.
184
186
  #stableRowsFloor = 0;
187
+ override addChild(component: Component): void {
188
+ if (isToolActivityComponent(component)) component.setToolActivityVisible(this.#toolActivityVisible);
189
+ super.addChild(component);
190
+ }
191
+
192
+ setToolActivityVisible(visible: boolean): void {
193
+ if (this.#toolActivityVisible === visible) return;
194
+ this.#toolActivityVisible = visible;
195
+ for (const child of this.children) {
196
+ if (isToolActivityComponent(child)) child.setToolActivityVisible(visible);
197
+ }
198
+ this.invalidate();
199
+ }
200
+
185
201
  override invalidate(): void {
186
202
  // Theme/global invalidation: retire every diff snapshot so stale styling
187
203
  // is not diffed against the recolored render.
@@ -16,6 +16,7 @@ export class TtsrNotificationComponent extends Container {
16
16
  #box: Box;
17
17
  #expanded = false;
18
18
  #rules: Rule[];
19
+ #toolActivityVisible = true;
19
20
 
20
21
  constructor(rules: Rule[]) {
21
22
  super();
@@ -31,6 +32,17 @@ export class TtsrNotificationComponent extends Container {
31
32
  this.#rebuild();
32
33
  }
33
34
 
35
+ setToolActivityVisible(visible: boolean): void {
36
+ if (this.#toolActivityVisible === visible) return;
37
+ this.#toolActivityVisible = visible;
38
+ this.invalidate();
39
+ }
40
+
41
+ override render(width: number): readonly string[] {
42
+ if (!this.#toolActivityVisible) return [];
43
+ return super.render(width);
44
+ }
45
+
34
46
  /** Merge additional rules into this block (deduped by rule name). */
35
47
  addRules(rules: Rule[]): void {
36
48
  let changed = false;
@@ -345,7 +345,6 @@ export class EventController {
345
345
  showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
346
346
  });
347
347
  group.setExpanded(this.ctx.toolOutputExpanded);
348
- group.setToolActivityVisible(!this.ctx.hideToolActivity);
349
348
  this.ctx.chatContainer.addChild(group);
350
349
  this.#lastReadGroup = group;
351
350
  }
@@ -1131,7 +1130,6 @@ export class EventController {
1131
1130
  content.id,
1132
1131
  );
1133
1132
  component.setExpanded(this.ctx.toolOutputExpanded);
1134
- component.setToolActivityVisible(!this.ctx.hideToolActivity);
1135
1133
  this.ctx.chatContainer.addChild(component);
1136
1134
  this.ctx.pendingTools.set(content.id, component);
1137
1135
  this.#toolTimelineComponents.set(content.id, component);
@@ -1384,7 +1382,6 @@ export class EventController {
1384
1382
  );
1385
1383
  component.setArgsComplete(event.toolCallId);
1386
1384
  component.setExpanded(this.ctx.toolOutputExpanded);
1387
- component.setToolActivityVisible(!this.ctx.hideToolActivity);
1388
1385
  this.ctx.chatContainer.addChild(component);
1389
1386
  this.ctx.pendingTools.set(event.toolCallId, component);
1390
1387
  this.#toolTimelineComponents.set(event.toolCallId, component);
@@ -1624,8 +1621,8 @@ export class EventController {
1624
1621
  // This text can be a provider error copied verbatim off the wire (the
1625
1622
  // Cursor todo bridge forwards the server's string), so it may carry
1626
1623
  // ANSI escapes, other C0/C1 controls, tabs, newlines, or a line far
1627
- // wider than the terminal. `showWarning` renders through a plain
1628
- // `Text`, which strips none of that — an escape reaches the terminal
1624
+ // wider than the terminal. `showWarning` renders through a plain `Text`,
1625
+ // which strips none of that — an escape reaches the terminal
1629
1626
  // and can repaint outside the row. `sanitizeText` drops the control
1630
1627
  // sequences (and returns the same reference when there are none),
1631
1628
  // then `previewLine` collapses the remaining whitespace and bounds
@@ -1637,6 +1634,7 @@ export class EventController {
1637
1634
  const detail = textContent ? previewLine(sanitizeText(textContent), TRUNCATE_LENGTHS.LINE) : "";
1638
1635
  this.ctx.showWarning(
1639
1636
  `Todo update failed${detail ? `: ${detail}` : ". Progress may be stale until todo succeeds."}`,
1637
+ { hideWithToolActivity: true },
1640
1638
  );
1641
1639
  }
1642
1640
  // Plan approval rides a `write` to xd://propose: the dispatch metadata on
@@ -2027,7 +2025,6 @@ export class EventController {
2027
2025
  const component = new TodoReminderComponent(event.todos, event.attempt, event.maxAttempts);
2028
2026
  this.ctx.present(component);
2029
2027
  }
2030
-
2031
2028
  async #handleTodoAutoClear(_event: Extract<AgentSessionEvent, { type: "todo_auto_clear" }>): Promise<void> {
2032
2029
  await this.ctx.reloadTodos();
2033
2030
  }
@@ -10,7 +10,6 @@ import { AssistantMessageComponent } from "../../modes/components/assistant-mess
10
10
  import { extractImagePathFromText } from "../../modes/components/custom-editor";
11
11
  import { ReadToolGroupComponent } from "../../modes/components/read-tool-group";
12
12
  import { renderSegmentTrack } from "../../modes/components/segment-track";
13
- import { StrippedToolCallsPlaceholder } from "../../modes/components/stripped-tool-calls-placeholder";
14
13
  import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
15
14
  import { ToolExecutionComponent } from "../../modes/components/tool-execution";
16
15
  import { TreeSelectorComponent } from "../../modes/components/tree-selector";
@@ -1925,15 +1924,16 @@ export class InputController {
1925
1924
  }
1926
1925
 
1927
1926
  for (const child of this.ctx.chatContainer.children) {
1928
- if (child instanceof ToolExecutionComponent || child instanceof ReadToolGroupComponent) {
1929
- if (!this.ctx.hideToolActivity) child.setExpanded(false);
1930
- child.setToolActivityVisible(!this.ctx.hideToolActivity);
1927
+ if (
1928
+ !this.ctx.hideToolActivity &&
1929
+ (child instanceof ToolExecutionComponent || child instanceof ReadToolGroupComponent)
1930
+ ) {
1931
+ child.setExpanded(false);
1931
1932
  } else if (child instanceof AssistantMessageComponent) {
1932
1933
  child.setToolResultImagesVisible(!this.ctx.hideToolActivity);
1933
- } else if (child instanceof StrippedToolCallsPlaceholder) {
1934
- child.setToolActivityVisible(!this.ctx.hideToolActivity);
1935
1934
  }
1936
1935
  }
1936
+ this.ctx.chatContainer.setToolActivityVisible(!this.ctx.hideToolActivity);
1937
1937
 
1938
1938
  if (this.ctx.hideToolActivity) this.ctx.ui.clearInlineImages();
1939
1939
  this.ctx.ui.resetDisplay();