@oh-my-pi/pi-coding-agent 17.0.7 → 17.0.9

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 (153) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/cli.js +5025 -4930
  3. package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
  4. package/dist/types/config/model-registry.d.ts +28 -1
  5. package/dist/types/config/model-resolver.d.ts +5 -1
  6. package/dist/types/config/settings-schema.d.ts +50 -3
  7. package/dist/types/dap/client.d.ts +19 -0
  8. package/dist/types/extensibility/extensions/runner.d.ts +4 -2
  9. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  10. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
  11. package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
  12. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +1 -1
  13. package/dist/types/extensibility/typebox.d.ts +4 -0
  14. package/dist/types/hindsight/client.d.ts +10 -0
  15. package/dist/types/hindsight/config.d.ts +8 -0
  16. package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
  17. package/dist/types/lsp/client.d.ts +2 -0
  18. package/dist/types/mcp/manager.d.ts +6 -2
  19. package/dist/types/mcp/render.d.ts +2 -2
  20. package/dist/types/mcp/tool-bridge.d.ts +16 -3
  21. package/dist/types/mcp/types.d.ts +6 -0
  22. package/dist/types/mnemopi/state.d.ts +2 -0
  23. package/dist/types/modes/components/oauth-selector.d.ts +8 -0
  24. package/dist/types/modes/components/settings-defs.d.ts +1 -0
  25. package/dist/types/modes/components/user-message.d.ts +25 -1
  26. package/dist/types/modes/controllers/extension-ui-controller.d.ts +8 -0
  27. package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
  28. package/dist/types/modes/interactive-mode.d.ts +7 -1
  29. package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
  30. package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
  31. package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
  32. package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
  33. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  34. package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
  35. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  36. package/dist/types/modes/types.d.ts +14 -1
  37. package/dist/types/modes/utils/ui-helpers.d.ts +12 -8
  38. package/dist/types/sdk.d.ts +1 -0
  39. package/dist/types/session/agent-session-error-log.test.d.ts +1 -0
  40. package/dist/types/session/agent-session.d.ts +86 -3
  41. package/dist/types/session/session-loader.d.ts +6 -4
  42. package/dist/types/task/executor.d.ts +10 -0
  43. package/dist/types/task/index.d.ts +1 -0
  44. package/dist/types/task/prewalk.d.ts +3 -0
  45. package/dist/types/task/types.d.ts +14 -0
  46. package/dist/types/tools/ask.d.ts +10 -0
  47. package/dist/types/tools/hub/jobs.d.ts +1 -1
  48. package/dist/types/tools/index.d.ts +3 -0
  49. package/dist/types/tools/report-tool-issue.d.ts +24 -9
  50. package/dist/types/tools/tool-timeouts.d.ts +8 -2
  51. package/dist/types/utils/changelog.d.ts +4 -6
  52. package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
  53. package/dist/types/web/search/types.d.ts +1 -1
  54. package/package.json +13 -13
  55. package/scripts/legacy-pi-virtual-module.ts +1 -1
  56. package/src/cli/grievances-cli.ts +2 -2
  57. package/src/cli/models-cli.ts +37 -17
  58. package/src/cli/update-cli.ts +14 -1
  59. package/src/cli/usage-cli.ts +1 -1
  60. package/src/config/__tests__/model-registry.test.ts +147 -0
  61. package/src/config/model-registry.ts +87 -24
  62. package/src/config/model-resolver.ts +56 -32
  63. package/src/config/settings-schema.ts +46 -3
  64. package/src/config/settings.ts +68 -5
  65. package/src/dap/client.ts +86 -7
  66. package/src/edit/diff.ts +4 -4
  67. package/src/eval/py/prelude.py +5 -5
  68. package/src/export/html/template.js +23 -9
  69. package/src/extensibility/extensions/runner.ts +9 -4
  70. package/src/extensibility/extensions/types.ts +3 -0
  71. package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
  72. package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
  73. package/src/extensibility/plugins/legacy-pi-compat.ts +851 -310
  74. package/src/extensibility/typebox.ts +22 -9
  75. package/src/hindsight/client.test.ts +42 -0
  76. package/src/hindsight/client.ts +40 -3
  77. package/src/hindsight/config.ts +18 -0
  78. package/src/internal-urls/registry-helpers.ts +40 -0
  79. package/src/launch/broker.ts +31 -5
  80. package/src/lsp/client.ts +23 -0
  81. package/src/lsp/clients/biome-client.ts +3 -4
  82. package/src/lsp/index.ts +26 -8
  83. package/src/main.ts +16 -5
  84. package/src/mcp/manager.ts +39 -8
  85. package/src/mcp/render.ts +94 -35
  86. package/src/mcp/tool-bridge.ts +116 -11
  87. package/src/mcp/types.ts +7 -0
  88. package/src/mnemopi/state.ts +70 -3
  89. package/src/modes/components/agent-dashboard.ts +6 -2
  90. package/src/modes/components/agent-hub.ts +26 -9
  91. package/src/modes/components/chat-transcript-builder.ts +13 -2
  92. package/src/modes/components/diff.ts +2 -2
  93. package/src/modes/components/oauth-selector.ts +24 -7
  94. package/src/modes/components/plan-review-overlay.ts +20 -4
  95. package/src/modes/components/settings-defs.ts +5 -2
  96. package/src/modes/components/settings-selector.ts +9 -5
  97. package/src/modes/components/tool-execution.test.ts +63 -2
  98. package/src/modes/components/user-message.ts +100 -1
  99. package/src/modes/controllers/command-controller.ts +36 -12
  100. package/src/modes/controllers/event-controller.ts +4 -1
  101. package/src/modes/controllers/extension-ui-controller.ts +18 -0
  102. package/src/modes/controllers/input-controller.ts +47 -12
  103. package/src/modes/controllers/mcp-command-controller.ts +70 -40
  104. package/src/modes/controllers/selector-controller.ts +82 -6
  105. package/src/modes/interactive-mode.ts +39 -4
  106. package/src/modes/rpc/rpc-client.ts +94 -3
  107. package/src/modes/rpc/rpc-frame.ts +164 -4
  108. package/src/modes/rpc/rpc-messages.ts +127 -0
  109. package/src/modes/rpc/rpc-mode.ts +79 -7
  110. package/src/modes/rpc/rpc-types.ts +34 -2
  111. package/src/modes/setup-wizard/scenes/model.ts +5 -7
  112. package/src/modes/setup-wizard/scenes/providers.ts +3 -2
  113. package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
  114. package/src/modes/setup-wizard/scenes/theme.ts +13 -3
  115. package/src/modes/setup-wizard/scenes/types.ts +9 -1
  116. package/src/modes/setup-wizard/scenes/web-search.ts +6 -1
  117. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  118. package/src/modes/types.ts +16 -3
  119. package/src/modes/utils/ui-helpers.ts +40 -20
  120. package/src/prompts/goals/guided-goal-system.md +24 -3
  121. package/src/prompts/system/workflow-notice.md +1 -1
  122. package/src/prompts/tools/task.md +12 -2
  123. package/src/sdk.ts +179 -51
  124. package/src/session/agent-session-error-log.test.ts +59 -0
  125. package/src/session/agent-session.ts +351 -46
  126. package/src/session/session-context.test.ts +224 -1
  127. package/src/session/session-context.ts +41 -2
  128. package/src/session/session-loader.ts +10 -5
  129. package/src/slash-commands/helpers/usage-report.ts +3 -1
  130. package/src/system-prompt.ts +1 -2
  131. package/src/task/executor.ts +44 -27
  132. package/src/task/index.ts +63 -8
  133. package/src/task/prewalk.ts +6 -0
  134. package/src/task/structured-subagent.ts +3 -1
  135. package/src/task/types.ts +16 -0
  136. package/src/task/worktree.ts +25 -11
  137. package/src/tools/ask.ts +15 -0
  138. package/src/tools/bash.ts +14 -6
  139. package/src/tools/browser.ts +1 -1
  140. package/src/tools/debug.ts +1 -1
  141. package/src/tools/eval.ts +4 -5
  142. package/src/tools/fetch.ts +1 -1
  143. package/src/tools/hub/index.ts +1 -1
  144. package/src/tools/hub/jobs.ts +67 -8
  145. package/src/tools/hub/launch.ts +7 -0
  146. package/src/tools/index.ts +3 -0
  147. package/src/tools/report-tool-issue.ts +79 -28
  148. package/src/tools/tool-timeouts.ts +10 -3
  149. package/src/tools/write.ts +31 -0
  150. package/src/utils/changelog.ts +54 -58
  151. package/src/utils/git.ts +57 -49
  152. package/src/web/search/providers/firecrawl.ts +46 -13
  153. package/src/web/search/types.ts +5 -1
@@ -1,7 +1,8 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
3
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
3
4
  import * as snapcompact from "@oh-my-pi/snapcompact";
4
- import type { CompactionSummaryMessage } from "./messages";
5
+ import { type CompactionSummaryMessage, INTERRUPTED_THINKING_MESSAGE_TYPE } from "./messages";
5
6
  import { buildSessionContext, type StrippedToolCallsMarker } from "./session-context";
6
7
  import type { SessionEntry } from "./session-entries";
7
8
 
@@ -159,3 +160,225 @@ describe("buildSessionContext dangling toolCalls", () => {
159
160
  expect(context.messages.some(message => message.role === "assistant")).toBe(false);
160
161
  });
161
162
  });
163
+
164
+ const assistantUsage: AssistantMessage["usage"] = {
165
+ input: 0,
166
+ output: 0,
167
+ cacheRead: 0,
168
+ cacheWrite: 0,
169
+ totalTokens: 0,
170
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
171
+ };
172
+
173
+ function userEntry(id: string, parentId: string | null, content: string, messageTimestamp: number): SessionEntry {
174
+ return {
175
+ type: "message",
176
+ id,
177
+ parentId,
178
+ timestamp,
179
+ message: { role: "user", content, timestamp: messageTimestamp } as AgentMessage,
180
+ };
181
+ }
182
+
183
+ function assistantEntry(
184
+ id: string,
185
+ parentId: string | null,
186
+ stopReason: AssistantMessage["stopReason"],
187
+ text: string,
188
+ messageTimestamp: number,
189
+ ): SessionEntry {
190
+ return {
191
+ type: "message",
192
+ id,
193
+ parentId,
194
+ timestamp,
195
+ message: {
196
+ role: "assistant",
197
+ content: [{ type: "text", text }],
198
+ api: "anthropic-messages",
199
+ provider: "anthropic",
200
+ model: "claude-sonnet-4-5",
201
+ usage: assistantUsage,
202
+ stopReason,
203
+ timestamp: messageTimestamp,
204
+ } satisfies AssistantMessage,
205
+ };
206
+ }
207
+
208
+ function toolCallAssistantEntry(
209
+ id: string,
210
+ parentId: string | null,
211
+ stopReason: AssistantMessage["stopReason"],
212
+ toolCallId: string,
213
+ messageTimestamp: number,
214
+ ): SessionEntry {
215
+ return {
216
+ type: "message",
217
+ id,
218
+ parentId,
219
+ timestamp,
220
+ message: {
221
+ role: "assistant",
222
+ content: [{ type: "toolCall", id: toolCallId, name: "write", arguments: { path: "plan.md", content: "x" } }],
223
+ api: "anthropic-messages",
224
+ provider: "anthropic",
225
+ model: "claude-sonnet-4-5",
226
+ usage: assistantUsage,
227
+ stopReason,
228
+ timestamp: messageTimestamp,
229
+ } satisfies AssistantMessage,
230
+ };
231
+ }
232
+
233
+ function syntheticToolResultEntry(
234
+ id: string,
235
+ parentId: string | null,
236
+ toolCallId: string,
237
+ messageTimestamp: number,
238
+ ): SessionEntry {
239
+ return {
240
+ type: "message",
241
+ id,
242
+ parentId,
243
+ timestamp,
244
+ message: {
245
+ role: "toolResult",
246
+ toolCallId,
247
+ toolName: "write",
248
+ content: [
249
+ { type: "text", text: "Tool call was not executed because the provider stream ended with an error." },
250
+ ],
251
+ details: { __synthetic: true, source: "assistant_stop_error", executed: false },
252
+ isError: true,
253
+ timestamp: messageTimestamp,
254
+ } as AgentMessage,
255
+ };
256
+ }
257
+
258
+ function hiddenContinuityEntry(id: string, parentId: string | null): SessionEntry {
259
+ return {
260
+ type: "custom_message",
261
+ id,
262
+ parentId,
263
+ timestamp,
264
+ customType: INTERRUPTED_THINKING_MESSAGE_TYPE,
265
+ content: "preserved interrupted thinking",
266
+ display: false,
267
+ attribution: "agent",
268
+ };
269
+ }
270
+
271
+ function expectUserTail(messages: AgentMessage[], content: string): void {
272
+ const tail = messages.at(-1);
273
+ expect(tail?.role).toBe("user");
274
+ if (tail?.role !== "user") {
275
+ throw new Error(`Expected user tail, received ${tail?.role ?? "none"}`);
276
+ }
277
+ expect(tail.content).toBe(content);
278
+ }
279
+
280
+ describe("buildSessionContext failed replay tails", () => {
281
+ it("terminates on cyclic parent links and includes each reachable message once", () => {
282
+ const entries = [userEntry("A", "B", "from A", 1), userEntry("B", "A", "from B", 2)];
283
+
284
+ const context = buildSessionContext(entries, "A");
285
+
286
+ expect(context.messages.map(message => (message.role === "user" ? message.content : message.role))).toEqual([
287
+ "from B",
288
+ "from A",
289
+ ]);
290
+ });
291
+
292
+ it("omits a terminal aborted assistant from normal context", () => {
293
+ const context = buildSessionContext([
294
+ userEntry("user", null, "continue", 1),
295
+ assistantEntry("assistant", "user", "aborted", "partial unsafe replay", 2),
296
+ ]);
297
+
298
+ expect(context.messages.some(message => message.role === "assistant")).toBe(false);
299
+ expectUserTail(context.messages, "continue");
300
+ });
301
+
302
+ it("omits an earlier aborted assistant before a later user from normal context", () => {
303
+ const context = buildSessionContext([
304
+ userEntry("user-1", null, "first prompt", 1),
305
+ assistantEntry("assistant", "user-1", "aborted", "partial unsafe replay", 2),
306
+ userEntry("user-2", "assistant", "retry", 3),
307
+ ]);
308
+
309
+ expect(context.messages.some(message => message.role === "assistant")).toBe(false);
310
+ expectUserTail(context.messages, "retry");
311
+ });
312
+
313
+ it("preserves a terminal aborted assistant in transcript mode", () => {
314
+ const context = buildSessionContext(
315
+ [
316
+ userEntry("user", null, "continue", 1),
317
+ assistantEntry("assistant", "user", "aborted", "visible transcript error", 2),
318
+ ],
319
+ undefined,
320
+ undefined,
321
+ { transcript: true },
322
+ );
323
+
324
+ const assistant = context.messages.find(message => message.role === "assistant");
325
+ expect(assistant?.role).toBe("assistant");
326
+ if (assistant?.role !== "assistant") {
327
+ throw new Error(`Expected transcript assistant, received ${assistant?.role ?? "none"}`);
328
+ }
329
+ expect(assistant.stopReason).toBe("aborted");
330
+ expect(assistant.content).toEqual([{ type: "text", text: "visible transcript error" }]);
331
+ });
332
+
333
+ it("omits a terminal error assistant from normal context", () => {
334
+ const context = buildSessionContext([
335
+ userEntry("user", null, "retry with smaller input", 1),
336
+ assistantEntry("assistant", "user", "error", "provider rejected the request", 2),
337
+ ]);
338
+
339
+ expect(context.messages.some(message => message.role === "assistant")).toBe(false);
340
+ expectUserTail(context.messages, "retry with smaller input");
341
+ });
342
+
343
+ it("keeps an aborted assistant when hidden interrupted-thinking continuity follows it", () => {
344
+ const context = buildSessionContext([
345
+ userEntry("user", null, "keep reasoning continuity", 1),
346
+ assistantEntry("assistant", "user", "aborted", "partial answer before interrupt", 2),
347
+ hiddenContinuityEntry("continuity", "assistant"),
348
+ ]);
349
+
350
+ const assistant = context.messages.find(message => message.role === "assistant");
351
+ expect(assistant?.role).toBe("assistant");
352
+ if (assistant?.role !== "assistant") {
353
+ throw new Error(`Expected assistant before continuity, received ${assistant?.role ?? "none"}`);
354
+ }
355
+ expect(assistant.stopReason).toBe("aborted");
356
+ expect(context.messages.at(-1)?.role).toBe("custom");
357
+ });
358
+
359
+ it("drops synthetic tool results paired with a dropped failed tool-call turn", () => {
360
+ const context = buildSessionContext([
361
+ userEntry("user", null, "write the plan", 1),
362
+ toolCallAssistantEntry("assistant", "user", "error", "call-1", 2),
363
+ syntheticToolResultEntry("result", "assistant", "call-1", 3),
364
+ ]);
365
+
366
+ expect(context.messages.map(message => message.role)).toEqual(["user"]);
367
+ expectUserTail(context.messages, "write the plan");
368
+ });
369
+
370
+ it("keeps the failed tool-call turn and its result in transcript mode", () => {
371
+ const context = buildSessionContext(
372
+ [
373
+ userEntry("user", null, "write the plan", 1),
374
+ toolCallAssistantEntry("assistant", "user", "error", "call-1", 2),
375
+ syntheticToolResultEntry("result", "assistant", "call-1", 3),
376
+ ],
377
+ undefined,
378
+ undefined,
379
+ { transcript: true, keepDanglingToolCalls: true },
380
+ );
381
+
382
+ expect(context.messages.map(message => message.role)).toEqual(["user", "assistant", "toolResult"]);
383
+ });
384
+ });
@@ -5,6 +5,7 @@ import {
5
5
  createBranchSummaryMessage,
6
6
  createCompactionSummaryMessage,
7
7
  createCustomMessage,
8
+ INTERRUPTED_THINKING_MESSAGE_TYPE,
8
9
  isCustomMessageContent,
9
10
  normalizeCustomMessagePayload,
10
11
  } from "./messages";
@@ -197,10 +198,13 @@ export function buildSessionContext(
197
198
  };
198
199
  }
199
200
 
200
- // Walk from leaf to root, collecting path
201
+ // Walk from leaf to root, collecting path. Corrupt/pre-fix files can contain
202
+ // parent cycles; stop at the first repeat so session load is bounded.
201
203
  const path: SessionEntry[] = [];
204
+ const seenPathIds = new Set<string>();
202
205
  let current: SessionEntry | undefined = leaf;
203
- while (current) {
206
+ while (current && !seenPathIds.has(current.id)) {
207
+ seenPathIds.add(current.id);
204
208
  path.push(current);
205
209
  current = current.parentId ? byId.get(current.parentId) : undefined;
206
210
  }
@@ -498,6 +502,41 @@ export function buildSessionContext(
498
502
  }
499
503
  }
500
504
 
505
+ // Error/abort assistant turns are transcript events, not safe assistant
506
+ // turns to replay into the next provider request. Drop them even when a
507
+ // later user message follows through non-context entries (`session_exit`,
508
+ // labels, etc.); otherwise a resumed session replays a dead partial turn
509
+ // and can spend minutes reprocessing old context before the new prompt.
510
+ // Keep the interrupted-thinking continuity pair: convertToLlm strips the
511
+ // unsafe trailing thinking from that assistant and sends the hidden
512
+ // continuity note instead.
513
+ if (!options?.transcript) {
514
+ for (let i = messages.length - 1; i >= 0; i--) {
515
+ const message = messages[i];
516
+ if (message?.role !== "assistant") continue;
517
+ if (message.stopReason !== "aborted" && message.stopReason !== "error") continue;
518
+ const next = messages[i + 1];
519
+ if (next?.role === "custom" && next.customType === INTERRUPTED_THINKING_MESSAGE_TYPE) continue;
520
+ // A failed turn that emitted tool calls persists paired synthetic
521
+ // tool_result placeholders after it. Dropping only the assistant would
522
+ // strand those results with no preceding tool_use — a shape providers
523
+ // reject — so remove the paired results alongside the turn.
524
+ const droppedToolCallIds = new Set<string>();
525
+ for (const block of message.content) {
526
+ if (block.type === "toolCall") droppedToolCallIds.add(block.id);
527
+ }
528
+ messages.splice(i, 1);
529
+ if (droppedToolCallIds.size > 0) {
530
+ for (let j = messages.length - 1; j >= i; j--) {
531
+ const candidate = messages[j];
532
+ if (candidate?.role === "toolResult" && droppedToolCallIds.has(candidate.toolCallId)) {
533
+ messages.splice(j, 1);
534
+ }
535
+ }
536
+ }
537
+ }
538
+ }
539
+
501
540
  return {
502
541
  messages,
503
542
  cacheMissExplainedAt: options?.transcript ? cacheMissExplainedAt : undefined,
@@ -306,10 +306,12 @@ export async function resolveBlobRefsInEntries(entries: FileEntry[], blobStore:
306
306
  }
307
307
 
308
308
  /**
309
- * Read-only message view of a session file: load entries, migrate to the
310
- * current version, resolve blob refs, and build the context along the
311
- * persisted leaf path (last entry). Does NOT create a writer or take the
312
- * session lock safe to call against a file another session is writing.
309
+ * Read-only transcript view of a session file: load entries, migrate to the
310
+ * current version, resolve blob refs, and build the display transcript along
311
+ * the persisted leaf path (last entry). Uses transcript mode (collapsed to the
312
+ * latest compaction) so failed/aborted tail turns stay visible, unlike the
313
+ * provider-context builder which drops them. Does NOT create a writer or take
314
+ * the session lock — safe to call against a file another session is writing.
313
315
  */
314
316
  export async function loadSessionMessagesReadOnly(filePath: string): Promise<AgentMessage[]> {
315
317
  const entries = await loadEntriesFromFile(filePath);
@@ -317,5 +319,8 @@ export async function loadSessionMessagesReadOnly(filePath: string): Promise<Age
317
319
  migrateToCurrentVersion(entries);
318
320
  await resolveBlobRefsInEntries(entries, new BlobStore(getBlobsDir()));
319
321
  const sessionEntries = entries.filter((e): e is SessionEntry => e.type !== "session");
320
- return buildSessionContext(sessionEntries).messages;
322
+ return buildSessionContext(sessionEntries, undefined, undefined, {
323
+ transcript: true,
324
+ collapseCompactedHistory: true,
325
+ }).messages;
321
326
  }
@@ -124,7 +124,9 @@ function renderUsageReports(
124
124
  );
125
125
  lines.push(` ${renderAsciiBar(limit.amount.usedFraction)}`);
126
126
  if (limit.window?.resetsAt && limit.window.resetsAt > nowMs) {
127
- lines.push(` resets in ${formatDuration(limit.window.resetsAt - nowMs)}`);
127
+ lines.push(
128
+ ` ${limit.window.resetLabel ?? "resets"} in ${formatDuration(limit.window.resetsAt - nowMs)}`,
129
+ );
128
130
  }
129
131
  if (limit.notes && limit.notes.length > 0)
130
132
  lines.push(
@@ -24,7 +24,6 @@ import projectPromptTemplate from "./prompts/system/project-prompt.md" with { ty
24
24
  import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
25
25
  import { normalizeConcurrencyLimit } from "./task/parallel";
26
26
  import { usesCodexTaskPrompt } from "./task/prompt-policy";
27
- import { shortenPath } from "./tools/render-utils";
28
27
  import { type ActiveRepoContext, resolveActiveRepoContext } from "./utils/active-repo-context";
29
28
  import { formatLocalCalendarDate } from "./utils/local-date";
30
29
  import { normalizePromptPath } from "./utils/prompt-path";
@@ -710,7 +709,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
710
709
 
711
710
  const date = formatLocalCalendarDate();
712
711
  const dateTime = date;
713
- const promptCwd = shortenPath(normalizePromptPath(resolvedCwd));
712
+ const promptCwd = normalizePromptPath(resolvedCwd);
714
713
  const activeRepoContextPrompt = renderActiveRepoContextPrompt(activeRepoContext);
715
714
 
716
715
  // Build tool metadata for system prompt rendering.
@@ -30,7 +30,6 @@ import { getSessionSlashCommands } from "../extensibility/extensions/get-command
30
30
  import { buildSkillPromptMessage, type Skill } from "../extensibility/skills";
31
31
  import type { HindsightSessionState } from "../hindsight/state";
32
32
  import type { LocalProtocolOptions } from "../internal-urls";
33
- import { callTool } from "../mcp/client";
34
33
  import type { MCPManager } from "../mcp/manager";
35
34
  import type { MnemopiSessionState } from "../mnemopi/state";
36
35
  import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
@@ -55,6 +54,7 @@ import type { EventBus } from "../utils/event-bus";
55
54
  import { buildNamedToolChoice } from "../utils/tool-choice";
56
55
  import type { WorkspaceTree } from "../workspace-tree";
57
56
  import { generateTaskLabel } from "./label";
57
+ import { resolveAgentPrewalkDefault } from "./prewalk";
58
58
  import { subprocessToolRegistry } from "./subprocess-tool-registry";
59
59
  import {
60
60
  type AgentDefinition,
@@ -729,46 +729,54 @@ function getUsageTokens(usage: unknown): number {
729
729
 
730
730
  /**
731
731
  * Create proxy tools that reuse the parent's MCP connections.
732
+ *
733
+ * Each proxy delegates to the current source `MCPTool`/`DeferredMCPTool` rather
734
+ * than rebuilding a raw `tools/call` request, so the Task/subagent path shares
735
+ * the source tool's authoritative outbound boundary: harness-intent (`i`)
736
+ * stripping, optional-placeholder pruning, local-URL resolution, reconnect
737
+ * retry, abort handling, and result/provider metadata. The source tool is
738
+ * re-resolved on every call by raw MCP server/tool metadata (not the normalized
739
+ * display name), so a reconnect that swaps the instance in `getTools()` is
740
+ * always honored. The proxy adds only the Task-specific 60s call timeout,
741
+ * combining its abort signal with the caller's around source execution.
732
742
  */
733
743
  export function createMCPProxyTools(mcpManager: MCPManager): CustomTool[] {
734
744
  return mcpManager.getTools().map(tool => {
735
- const mcpTool = tool as { mcpToolName?: string; mcpServerName?: string };
745
+ const serverName = tool.mcpServerName ?? "";
746
+ const mcpToolName = tool.mcpToolName ?? "";
736
747
  return {
737
748
  name: tool.name,
738
749
  label: tool.label ?? tool.name,
739
750
  description: tool.description ?? "",
740
751
  parameters: tool.parameters,
741
- execute: async (_toolCallId, params, _onUpdate, _ctx, signal) => {
752
+ strict: tool.strict,
753
+ mcpServerName: serverName,
754
+ mcpToolName,
755
+ execute: async (toolCallId, params, onUpdate, ctx, signal) => {
742
756
  if (signal?.aborted) {
743
757
  throw new ToolAbortError();
744
758
  }
745
- const serverName = mcpTool.mcpServerName ?? "";
746
- const mcpToolName = mcpTool.mcpToolName ?? "";
759
+ // Re-resolve by raw MCP metadata so a reconnect that replaced the
760
+ // source instance is picked up; the display name alone is not enough.
761
+ const source = mcpManager
762
+ .getTools()
763
+ .find(t => t.mcpServerName === serverName && t.mcpToolName === mcpToolName);
764
+ if (!source?.execute) {
765
+ return {
766
+ content: [{ type: "text" as const, text: `MCP error: tool ${mcpToolName} no longer available` }],
767
+ details: { serverName, mcpToolName, isError: true },
768
+ };
769
+ }
747
770
  try {
748
771
  const timeoutController = new AbortController();
749
772
  const timeoutSignal = timeoutController.signal;
750
773
  const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
751
- const result = await withAbortTimeout(
752
- (async () => {
753
- const connection = await untilAborted(combinedSignal, () =>
754
- mcpManager.waitForConnection(serverName),
755
- );
756
- return callTool(connection, mcpToolName, params as Record<string, unknown>, {
757
- signal: combinedSignal,
758
- });
759
- })(),
774
+ return await withAbortTimeout(
775
+ Promise.resolve(source.execute(toolCallId, params, onUpdate, ctx, combinedSignal)),
760
776
  MCP_CALL_TIMEOUT_MS,
761
777
  signal,
762
778
  timeoutController,
763
779
  );
764
- return {
765
- content: (result.content ?? []).map(item =>
766
- item.type === "text"
767
- ? { type: "text" as const, text: item.text ?? "" }
768
- : { type: "text" as const, text: JSON.stringify(item) },
769
- ),
770
- details: { serverName, mcpToolName, isError: result.isError },
771
- };
772
780
  } catch (error) {
773
781
  if (error instanceof ToolAbortError) {
774
782
  throw error;
@@ -1519,9 +1527,16 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1519
1527
  scheduleProgress(flushProgress);
1520
1528
  };
1521
1529
 
1522
- const attach = (session: AgentSession): (() => void) =>
1523
- session.subscribe(event => {
1530
+ const attach = (session: AgentSession): (() => void) => {
1531
+ let activeModel = session.model ? formatModelStringWithRouting(session.model) : undefined;
1532
+ return session.subscribe(event => {
1524
1533
  emitSubagentEvent(event);
1534
+ const nextModel = session.model ? formatModelStringWithRouting(session.model) : undefined;
1535
+ if (nextModel && nextModel !== activeModel) {
1536
+ activeModel = nextModel;
1537
+ progress.resolvedModel = nextModel;
1538
+ scheduleProgress(true);
1539
+ }
1525
1540
  if (event.type === "auto_retry_start") {
1526
1541
  progress.retryState = {
1527
1542
  attempt: event.attempt,
@@ -1563,15 +1578,18 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1563
1578
  }
1564
1579
  if (event.type === "retry_fallback_applied") {
1565
1580
  progress.resolvedModel = event.to;
1581
+ progress.resolvedModelIsFallback = true;
1566
1582
  scheduleProgress(true);
1567
1583
  return;
1568
1584
  }
1569
1585
  if (event.type === "retry_fallback_succeeded") {
1570
1586
  progress.resolvedModel = event.model;
1587
+ progress.resolvedModelIsFallback = true;
1571
1588
  scheduleProgress(true);
1572
1589
  return;
1573
1590
  }
1574
1591
  });
1592
+ };
1575
1593
 
1576
1594
  const captureSalvage = (session: AgentSession): void => {
1577
1595
  // Best-effort salvage: capture the last assistant text so
@@ -1973,6 +1991,7 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
1973
1991
  contextWindow: progress.contextWindow,
1974
1992
  modelOverride,
1975
1993
  resolvedModel: progress.resolvedModel,
1994
+ resolvedModelIsFallback: progress.resolvedModelIsFallback,
1976
1995
  error: exitCode !== 0 && stderr ? stderr : undefined,
1977
1996
  aborted: wasAborted,
1978
1997
  abortReason: finalAbortReason,
@@ -2451,11 +2470,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2451
2470
  // frontmatter default; the `task.prewalk` toggle (default off) arms it.
2452
2471
  // Resolution failures skip prewalk instead of failing the spawn.
2453
2472
  let prewalk: Prewalk | undefined;
2454
- const genericTaskPrewalk =
2455
- agent.source === "bundled" && agent.name === "task" && settings.get("task.prewalk") ? true : undefined;
2456
2473
  const prewalkPattern = resolveAgentPrewalkPattern({
2457
2474
  settingsOverride: settings.get("task.agentPrewalk")[agent.name],
2458
- agentPrewalk: agent.prewalk ?? genericTaskPrewalk,
2475
+ agentPrewalk: resolveAgentPrewalkDefault(agent, settings.get("task.prewalk")),
2459
2476
  });
2460
2477
  if (prewalkPattern) {
2461
2478
  const resolvedPrewalk = resolveModelOverride([prewalkPattern], modelRegistry, settings);