@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.2

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 (198) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/cli.js +3556 -3504
  3. package/dist/types/advisor/advise-tool.d.ts +12 -1
  4. package/dist/types/advisor/runtime.d.ts +41 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/update-cli.d.ts +4 -1
  7. package/dist/types/cli/usage-cli.d.ts +3 -0
  8. package/dist/types/cli/usage-error.d.ts +4 -0
  9. package/dist/types/config/api-key-resolver.d.ts +2 -2
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/model-resolver.d.ts +8 -1
  12. package/dist/types/config/models-config.d.ts +1 -1
  13. package/dist/types/config/settings-schema.d.ts +10 -0
  14. package/dist/types/discovery/substitute-plugin-root.d.ts +22 -0
  15. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
  16. package/dist/types/eval/backend.d.ts +3 -3
  17. package/dist/types/eval/bridge-timeout.d.ts +9 -1
  18. package/dist/types/eval/js/context-manager.d.ts +5 -3
  19. package/dist/types/eval/js/process-entry.d.ts +6 -0
  20. package/dist/types/eval/js/worker-core.d.ts +15 -1
  21. package/dist/types/eval/py/spawn-options.d.ts +10 -0
  22. package/dist/types/eval/py/tool-bridge.d.ts +1 -0
  23. package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
  24. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  25. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  26. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -6
  27. package/dist/types/extensibility/plugins/bun-git-cache.d.ts +3 -0
  28. package/dist/types/goals/guided-setup.d.ts +12 -0
  29. package/dist/types/internal-urls/history-protocol.d.ts +3 -2
  30. package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
  31. package/dist/types/internal-urls/registry-helpers.d.ts +19 -0
  32. package/dist/types/main.d.ts +1 -0
  33. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  34. package/dist/types/mcp/oauth-flow.d.ts +2 -0
  35. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +1 -0
  36. package/dist/types/modes/components/agent-hub.d.ts +10 -0
  37. package/dist/types/modes/components/dynamic-border.d.ts +5 -3
  38. package/dist/types/modes/components/login-dialog.d.ts +2 -0
  39. package/dist/types/modes/components/mcp-add-wizard.d.ts +1 -0
  40. package/dist/types/modes/components/read-tool-group.d.ts +0 -2
  41. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  42. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  43. package/dist/types/modes/interactive-mode.d.ts +1 -0
  44. package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
  45. package/dist/types/modes/orchestrate.d.ts +1 -1
  46. package/dist/types/modes/rpc/host-tools.d.ts +2 -0
  47. package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
  48. package/dist/types/modes/types.d.ts +1 -0
  49. package/dist/types/modes/ultrathink.d.ts +1 -1
  50. package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
  51. package/dist/types/modes/workflow.d.ts +1 -1
  52. package/dist/types/session/agent-session.d.ts +6 -0
  53. package/dist/types/session/exit-diagnostics.d.ts +11 -0
  54. package/dist/types/session/messages.d.ts +15 -0
  55. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
  56. package/dist/types/subprocess/worker-client.d.ts +6 -0
  57. package/dist/types/tools/bash-skill-urls.d.ts +1 -0
  58. package/dist/types/tools/grep.d.ts +0 -2
  59. package/dist/types/tools/read.d.ts +0 -4
  60. package/dist/types/web/search/provider.d.ts +10 -3
  61. package/dist/types/web/search/providers/codex.d.ts +5 -4
  62. package/package.json +12 -12
  63. package/src/advisor/__tests__/advisor.test.ts +917 -42
  64. package/src/advisor/advise-tool.ts +17 -1
  65. package/src/advisor/runtime.ts +270 -67
  66. package/src/autolearn/controller.ts +15 -3
  67. package/src/autoresearch/dashboard.ts +2 -2
  68. package/src/cli/args.ts +12 -0
  69. package/src/cli/auth-broker-cli.ts +30 -11
  70. package/src/cli/auth-gateway-cli.ts +5 -1
  71. package/src/cli/config-cli.ts +15 -3
  72. package/src/cli/dry-balance-cli.ts +14 -4
  73. package/src/cli/flag-tables.ts +21 -7
  74. package/src/cli/update-cli.ts +62 -11
  75. package/src/cli/usage-cli.ts +58 -5
  76. package/src/cli/usage-error.ts +7 -0
  77. package/src/cli.ts +23 -1
  78. package/src/commands/acp.ts +11 -2
  79. package/src/commands/launch.ts +12 -3
  80. package/src/commands/token.ts +3 -1
  81. package/src/config/api-key-resolver.ts +12 -3
  82. package/src/config/config-file.ts +30 -12
  83. package/src/config/model-registry.ts +7 -7
  84. package/src/config/model-resolver.ts +21 -7
  85. package/src/config/models-config.ts +1 -1
  86. package/src/config/settings-schema.ts +10 -0
  87. package/src/cursor.ts +2 -0
  88. package/src/discovery/claude-plugins.ts +9 -3
  89. package/src/discovery/omp-plugins.ts +6 -2
  90. package/src/discovery/substitute-plugin-root.ts +32 -0
  91. package/src/eval/__tests__/agent-bridge.test.ts +19 -14
  92. package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
  93. package/src/eval/__tests__/js-context-manager.test.ts +158 -1
  94. package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
  95. package/src/eval/__tests__/prelude-agent.test.ts +20 -0
  96. package/src/eval/__tests__/process-entry-import.test.ts +27 -0
  97. package/src/eval/agent-bridge.ts +121 -116
  98. package/src/eval/backend.ts +3 -3
  99. package/src/eval/bridge-timeout.ts +20 -2
  100. package/src/eval/executor-base.ts +85 -7
  101. package/src/eval/jl/kernel.ts +2 -1
  102. package/src/eval/js/context-manager.ts +109 -32
  103. package/src/eval/js/process-entry.ts +27 -0
  104. package/src/eval/js/shared/runtime.ts +1 -1
  105. package/src/eval/js/worker-core.ts +70 -9
  106. package/src/eval/js/worker-entry.ts +1 -1
  107. package/src/eval/py/__tests__/prelude.test.ts +72 -0
  108. package/src/eval/py/kernel.ts +2 -1
  109. package/src/eval/py/prelude.py +28 -1
  110. package/src/eval/py/spawn-options.ts +13 -0
  111. package/src/eval/py/tool-bridge.ts +13 -14
  112. package/src/eval/rb/kernel.ts +2 -1
  113. package/src/exec/bash-executor.ts +30 -43
  114. package/src/extensibility/custom-tools/types.ts +3 -0
  115. package/src/extensibility/extensions/runner.ts +3 -0
  116. package/src/extensibility/extensions/types.ts +3 -0
  117. package/src/extensibility/extensions/wrapper.ts +18 -18
  118. package/src/extensibility/plugins/bun-git-cache.ts +91 -0
  119. package/src/extensibility/plugins/legacy-pi-compat.ts +32 -16
  120. package/src/extensibility/plugins/manager.ts +28 -7
  121. package/src/goals/guided-setup.ts +29 -1
  122. package/src/internal-urls/history-protocol.ts +95 -15
  123. package/src/internal-urls/memory-protocol.ts +13 -9
  124. package/src/internal-urls/registry-helpers.ts +50 -1
  125. package/src/launch/broker.ts +38 -25
  126. package/src/lsp/client.ts +7 -1
  127. package/src/main.ts +29 -0
  128. package/src/mcp/oauth-discovery.ts +20 -1
  129. package/src/mcp/oauth-flow.ts +3 -1
  130. package/src/mcp/tool-bridge.ts +57 -6
  131. package/src/modes/components/__tests__/dynamic-border.test.ts +55 -0
  132. package/src/modes/components/agent-dashboard.ts +2 -2
  133. package/src/modes/components/agent-hub.ts +15 -2
  134. package/src/modes/components/agent-transcript-viewer.ts +2 -2
  135. package/src/modes/components/chat-transcript-builder.ts +26 -4
  136. package/src/modes/components/dynamic-border.ts +9 -6
  137. package/src/modes/components/extensions/extension-list.ts +2 -2
  138. package/src/modes/components/hook-selector.ts +10 -4
  139. package/src/modes/components/login-dialog.ts +5 -0
  140. package/src/modes/components/mcp-add-wizard.ts +5 -0
  141. package/src/modes/components/plan-review-overlay.ts +11 -11
  142. package/src/modes/components/read-tool-group.ts +1 -8
  143. package/src/modes/components/status-line/component.ts +10 -1
  144. package/src/modes/components/transcript-container.ts +110 -7
  145. package/src/modes/controllers/command-controller.ts +12 -4
  146. package/src/modes/controllers/event-controller.ts +80 -15
  147. package/src/modes/controllers/input-controller.ts +4 -2
  148. package/src/modes/controllers/mcp-command-controller.ts +6 -7
  149. package/src/modes/controllers/selector-controller.ts +24 -5
  150. package/src/modes/controllers/todo-command-controller.ts +18 -14
  151. package/src/modes/interactive-mode.ts +7 -3
  152. package/src/modes/magic-keyword-boundary.ts +23 -0
  153. package/src/modes/orchestrate.ts +6 -5
  154. package/src/modes/print-mode.ts +9 -0
  155. package/src/modes/prompt-action-autocomplete.ts +6 -1
  156. package/src/modes/rpc/host-tools.ts +15 -0
  157. package/src/modes/rpc/rpc-mode.ts +123 -48
  158. package/src/modes/types.ts +1 -1
  159. package/src/modes/ultrathink.ts +6 -5
  160. package/src/modes/utils/transcript-render-helpers.ts +54 -0
  161. package/src/modes/utils/ui-helpers.ts +27 -1
  162. package/src/modes/workflow.ts +6 -5
  163. package/src/prompts/advisor/system.md +1 -0
  164. package/src/prompts/system/system-prompt.md +1 -0
  165. package/src/prompts/tools/eval.md +2 -2
  166. package/src/prompts/tools/grep.md +1 -2
  167. package/src/prompts/tools/read.md +2 -4
  168. package/src/sdk.ts +61 -34
  169. package/src/session/agent-session.ts +283 -39
  170. package/src/session/exit-diagnostics.ts +108 -0
  171. package/src/session/messages.test.ts +66 -0
  172. package/src/session/messages.ts +37 -0
  173. package/src/session/streaming-output.ts +40 -12
  174. package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
  175. package/src/slash-commands/helpers/logout.ts +23 -3
  176. package/src/slash-commands/helpers/usage-report.ts +14 -2
  177. package/src/subprocess/worker-client.ts +9 -2
  178. package/src/system-prompt.test.ts +36 -0
  179. package/src/system-prompt.ts +1 -1
  180. package/src/task/executor.ts +8 -0
  181. package/src/task/render.test.ts +36 -0
  182. package/src/task/render.ts +55 -43
  183. package/src/tools/bash-skill-urls.ts +4 -1
  184. package/src/tools/bash.ts +1 -0
  185. package/src/tools/browser/registry.ts +17 -3
  186. package/src/tools/eval.ts +14 -9
  187. package/src/tools/gh.ts +3 -1
  188. package/src/tools/grep.ts +5 -45
  189. package/src/tools/path-utils.ts +7 -1
  190. package/src/tools/read.ts +23 -74
  191. package/src/tools/write.ts +82 -9
  192. package/src/tools/yield.ts +29 -1
  193. package/src/utils/title-generator.ts +10 -6
  194. package/src/web/search/index.ts +39 -22
  195. package/src/web/search/provider.ts +33 -16
  196. package/src/web/search/providers/codex.ts +68 -21
  197. package/src/web/search/providers/perplexity-auth.ts +20 -11
  198. package/src/web/search/providers/perplexity.ts +14 -2
@@ -1,6 +1,7 @@
1
1
  import { prompt } from "@oh-my-pi/pi-utils";
2
2
  import workflowNoticeTemplate from "../prompts/system/workflow-notice.md" with { type: "text" };
3
3
  import { createGradientHighlighter, type KeywordHighlighter } from "./gradient-highlight";
4
+ import { magicKeywordRegex } from "./magic-keyword-boundary";
4
5
  import { keywordInProse } from "./markdown-prose";
5
6
 
6
7
  /**
@@ -10,13 +11,13 @@ import { keywordInProse } from "./markdown-prose";
10
11
  * amber→green gradient ({@link highlightWorkflow}); submitting a message that
11
12
  * mentions it appends a hidden workflow notice that steers the model to author
12
13
  * a deterministic multi-subagent workflow through the active task schema.
13
- * Matching is whitespace-delimited and case-sensitive (lowercase only) —
14
+ * Matching is prose-delimited and case-sensitive (lowercase only) —
14
15
  * "workflowz" triggers, but "workflowzed", "Workflowz", and "workflowz.ts"
15
16
  * never do.
16
17
  */
17
18
 
18
- // Detection: lowercase keyword flanked by whitespace or a string edge. Non-global so `.test` stays stateless.
19
- const WORKFLOW_WORD = /(?<!\S)workflowz(?!\S)/;
19
+ // Detection: lowercase keyword flanked by prose punctuation, whitespace, or a string edge.
20
+ const WORKFLOW_WORD = magicKeywordRegex("workflowz");
20
21
 
21
22
  /** WORKFLOW_NOTICE is the default hidden notice for sessions with batched task calls enabled. */
22
23
  export const WORKFLOW_NOTICE: string = renderWorkflowNotice({ taskBatch: true });
@@ -28,7 +29,7 @@ export function renderWorkflowNotice({ taskBatch }: { taskBatch: boolean }): str
28
29
 
29
30
  /**
30
31
  * Whether `text` contains the standalone keyword "workflowz"
31
- * (lowercase, whitespace-delimited) in prose — never inside a code block, inline
32
+ * (lowercase, prose-delimited) in prose — never inside a code block, inline
32
33
  * code span, or XML/HTML section.
33
34
  */
34
35
  export function containsWorkflow(text: string): boolean {
@@ -42,7 +43,7 @@ export function containsWorkflow(text: string): boolean {
42
43
  */
43
44
  export const highlightWorkflow: KeywordHighlighter = createGradientHighlighter({
44
45
  probe: /workflowz/,
45
- highlight: /(?<!\S)workflowz(?!\S)/g,
46
+ highlight: magicKeywordRegex("workflowz", "g"),
46
47
  stops: 14,
47
48
  hue: t => 30 + t * 120,
48
49
  });
@@ -28,6 +28,7 @@ Keep exploration lean:
28
28
  - NEVER restate information the agent already has, including errors they have seen.
29
29
  - Examples: type errors, LSP diagnostics, failed builds, failing tests, lint.
30
30
  - NEVER repeat advice you already gave, and NEVER send the same advice twice; give the agent room to act on prior advice before raising the same theme again.
31
+ - When an update heading is tagged `[in progress — more steps follow]`, the agent is mid-turn and has not finished yet. Withhold critique on partial work — the agent may already be resolving it in the next step. Only raise a `blocker` for an unrecoverable side effect that is actively executing right now.
31
32
  - NEVER nitpick about things user stated they are okay with. You are the advocate for the user.
32
33
  - You are user-aligned: treat the user's word as truth, their frustration as justified, their stated requirements as binding.
33
34
  </communication>
@@ -57,6 +57,7 @@ Special URLs for internal resources; with most FS/bash tools they auto-resolve t
57
57
  - `memory://root`: project memory summary
58
58
  {{/if}}
59
59
  - `agent://<id>`: agent output artifact; `/<path>` extracts a JSON field
60
+ - `history://<id>`: read-only markdown transcript of an agent (live, parked, or released); bare `history://` lists all agents. Serves any agent whose session file persists on disk, not just registered peers.
60
61
  - `artifact://<id>`: artifact content
61
62
  - `local://<name>.md`: plan artifacts or shared content for subagents
62
63
  {{#if hasObsidian}}
@@ -10,7 +10,7 @@ Fields:
10
10
  - `language` — {{#if py}}`"py"` IPython kernel{{/if}}{{#ifAll py js}}, {{/ifAll}}{{#if js}}`"js"` persistent JavaScript VM{{/if}}{{#if rb}}{{#ifAny py js}}, {{/ifAny}}`"rb"` persistent Ruby kernel{{/if}}{{#if jl}}{{#ifAny py js rb}}, {{/ifAny}}`"jl"` persistent Julia kernel{{/if}}.
11
11
  - `code` — cell body, verbatim. Newlines/quotes JSON-encoded; no fences, no headers.
12
12
  - `title` (optional) — short transcript label (e.g. `"imports"`).
13
- - `timeout` (optional) — seconds. Raise only for heavy compute or long non-agent tool calls.
13
+ - `timeout` (optional) — seconds; `0` disables the cell timeout. Raise only for heavy compute or long non-agent tool calls.
14
14
  - `reset` (optional) — wipe this language's kernel first.{{#ifAll py js}} Per-language: a `py` reset never touches the JS VM.{{/ifAll}}
15
15
 
16
16
  {{#if py}}Live event loop: use top-level `await` directly; `asyncio.run(…)` raises "cannot be called from a running event loop".{{/if}}
@@ -28,7 +28,7 @@ display(value) → None
28
28
  print(value, ...) → None
29
29
  Text output.
30
30
  read(path, offset?=1, limit?=None) → str
31
- File as text; offset/limit 1-indexed lines. Accepts `local://…`.
31
+ File/resource text; offset/limit = 1-indexed lines. `local://…` works everywhere; Python/JS also accept top-level `read` URI schemes.
32
32
  write(path, content) → str
33
33
  Write file (creates parents) → resolved path. `local://…` persists across turns/subagents.
34
34
  env(key?=None, value?=None) → str | None | dict
@@ -2,8 +2,7 @@ Greps files using regex.
2
2
 
3
3
  <instruction>
4
4
  - Supports Rust regex and PCRE2 syntax.
5
- - `path`: SHOULD scope to a known path (e.g. `src`); pass several as a delimited list (`src; tests`). Use `selector` only for line-number filtering, never path/root selection (`"/"` belongs in `path`).
6
- - Literal colon filename + line range? Use `selector` (e.g. `{"path":"test:1-2","selector":"1-2"}`), not recursive `path:"test:1-2:1-2"`.
5
+ - `path`: SHOULD scope to a known path (e.g. `src`); pass several as a delimited list (`src; tests`). Append a line selector to one file path (e.g. `src/foo.ts:50-100`); selectors never choose the search root.
7
6
  - Cross-line patterns detected from literal `\n` or `\\n` in `pattern`.
8
7
  </instruction>
9
8
 
@@ -1,4 +1,4 @@
1
- Read files, directories, archives, SQLite, images, documents, internal resources, and web URLs via `path` plus optional `selector`.
1
+ Read files, directories, archives, SQLite, images, documents, internal resources, and web URLs via `path`.
2
2
 
3
3
  <instruction>
4
4
  - SHOULD parallelize independent reads.
@@ -7,8 +7,7 @@ Read files, directories, archives, SQLite, images, documents, internal resources
7
7
 
8
8
  ## Parameters
9
9
 
10
- - `path` — required. Local path, internal URI (`skill://`, `agent://`, `artifact://`, `memory://`, `rule://`, `local://`, `vault://`, `mcp://`, `omp://`, `issue://`, `pr://`, `ssh://`), or URL. Inline `:<sel>` still works for ranges/modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).
11
- - `selector` — optional selector without leading `:` (e.g. `"50-200"`, `"raw"`, `"raw:50-100"`, `"conflicts"`). Use when `path` contains literal colons: `{"path":"test:1-2","selector":"1-2"}`.
10
+ - `path` — required. Local path, internal URI (`skill://`, `agent://`, `artifact://`, `memory://`, `rule://`, `local://`, `vault://`, `mcp://`, `omp://`, `issue://`, `pr://`, `ssh://`), or URL. Append `:<sel>` for ranges/modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).
12
11
 
13
12
  ## Selectors
14
13
 
@@ -73,6 +72,5 @@ All URI schemes take the same line selectors. `artifact://<id>` recovers spilled
73
72
  `ssh://host/<absolute-path>` reads a remote text file (UTF-8, ≤1 MiB) or lists a directory one level deep, on a pre-configured SSH host or `~/.ssh/config` alias; `ssh://host/` lists the remote root and bare `ssh://` lists the configured hosts. Files are also writable via `write` and searchable via `search`; a directory only lists (`search` refuses a directory, `write` refuses to overwrite one). A literal `:`, `?`, or `#` in the remote path must be percent-encoded (`%3A`/`%3F`/`%23`) — a trailing `:sel` is read as a line selector, and `?`/`#` start a URL query/fragment. Requires a POSIX login shell (`sh`/`bash`/`zsh`); a Windows host or a non-POSIX shell (fish, csh/tcsh) is rejected — use the `ssh` tool there.
74
73
 
75
74
  <critical>
76
- - Literal colon filename + selector? Use `selector`, not recursive `path:"file:sel:sel"`.
77
75
  - Summary footer names elided ranges? Re-issue ONLY those ranges. NEVER guess `..`/`…` content.
78
76
  </critical>
package/src/sdk.ts CHANGED
@@ -112,10 +112,12 @@ import {
112
112
  import { AgentSession, type PlanYolo, type Prewalk } from "./session/agent-session";
113
113
  import { discoverAuthStorage as discoverAuthStorageFromConfig } from "./session/auth-broker-config";
114
114
  import type { AuthStorage } from "./session/auth-storage";
115
+ import { createInterruptedTurnAbortMessage } from "./session/exit-diagnostics";
115
116
  import {
116
117
  type CustomMessage,
117
118
  convertToLlm,
118
119
  LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE,
120
+ replaceLlmImagesWithText,
119
121
  USER_INTERRUPT_LABEL,
120
122
  wrapSteeringForModel,
121
123
  } from "./session/messages";
@@ -864,6 +866,7 @@ function createCustomToolContext(ctx: ExtensionContext): CustomToolContext {
864
866
  isIdle: ctx.isIdle,
865
867
  hasQueuedMessages: ctx.hasPendingMessages,
866
868
  abort: ctx.abort,
869
+ localProtocolOptions: ctx.localProtocolOptions,
867
870
  };
868
871
  }
869
872
 
@@ -1271,11 +1274,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1271
1274
  }
1272
1275
  const secretsEnabled = obfuscator?.hasSecrets() === true;
1273
1276
 
1274
- // Check if session has existing data to restore
1275
- const existingSession = logger.time("loadSessionContext", () =>
1277
+ // An abnormal process exit after a non-terminal message tail is durable
1278
+ // evidence that the old process can no longer finish that turn. Preserve the
1279
+ // partial transcript and append one terminal aborted assistant record before
1280
+ // rebuilding runtime context. The helper is idempotent once that record exists.
1281
+ let existingBranch = logger.time("getSessionBranch", () => sessionManager.getBranch());
1282
+ const interruptedTurnAbort = createInterruptedTurnAbortMessage(existingBranch);
1283
+ if (interruptedTurnAbort) {
1284
+ sessionManager.appendMessage(interruptedTurnAbort);
1285
+ existingBranch = logger.time("getRecoveredSessionBranch", () => sessionManager.getBranch());
1286
+ }
1287
+ let existingSession = logger.time("loadSessionContext", () =>
1276
1288
  deobfuscateSessionContext(sessionManager.buildSessionContext(), obfuscator),
1277
1289
  );
1278
- const existingBranch = logger.time("getSessionBranch", () => sessionManager.getBranch());
1279
1290
  const hasExistingSession = existingBranch.length > 0;
1280
1291
  const hasThinkingEntry = existingBranch.some(entry => entry.type === "thinking_level_change");
1281
1292
  const hasServiceTierEntry = existingBranch.some(entry => entry.type === "service_tier_change");
@@ -1816,10 +1827,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1816
1827
  // to mirror the AsyncJobManager ownership rule.
1817
1828
  if (mcpManager && !options.parentTaskPrefix) MCPManager.setInstance(mcpManager);
1818
1829
 
1819
- // Add image tools when the active model or configured image providers can generate images.
1820
- const imageGenTools = await logger.time("getImageGenTools", () => getImageGenTools(modelRegistry, model));
1821
- if (imageGenTools.length > 0) {
1822
- customTools.push(...(imageGenTools as unknown as CustomTool[]));
1830
+ // Add image tools when generation is enabled and either no explicit tool
1831
+ // whitelist was given or it names `generate_image`. Unlike built-in tools
1832
+ // (filtered in `createTools`), custom tools are force-activated via
1833
+ // `alwaysInclude` below, so an explicit `--no-tools`/whitelist must be
1834
+ // honored here or image-gen would leak past every filter (issue #5305).
1835
+ const imageGenRequested = !options.toolNames || options.toolNames.includes("generate_image");
1836
+ if (settings.get("generate_image.enabled") && imageGenRequested) {
1837
+ const imageGenTools = await logger.time("getImageGenTools", () => getImageGenTools(modelRegistry, model));
1838
+ if (imageGenTools.length > 0) {
1839
+ customTools.push(...(imageGenTools as unknown as CustomTool[]));
1840
+ }
1823
1841
  }
1824
1842
 
1825
1843
  if (settings.get("speechgen.enabled")) {
@@ -2166,6 +2184,24 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2166
2184
  }
2167
2185
  }
2168
2186
 
2187
+ // A first-turn user tail has no assistant metadata to copy. Once startup
2188
+ // has selected its final model, use that model to terminate the
2189
+ // interrupted turn before the live agent consumes the restored context.
2190
+ if (model) {
2191
+ const selectedModelAbort = createInterruptedTurnAbortMessage(existingBranch, {
2192
+ api: model.api,
2193
+ provider: model.provider,
2194
+ model: model.id,
2195
+ });
2196
+ if (selectedModelAbort) {
2197
+ sessionManager.appendMessage(selectedModelAbort);
2198
+ existingBranch = logger.time("getRecoveredUserTailBranch", () => sessionManager.getBranch());
2199
+ existingSession = logger.time("loadRecoveredUserTailContext", () =>
2200
+ deobfuscateSessionContext(sessionManager.buildSessionContext(), obfuscator),
2201
+ );
2202
+ }
2203
+ }
2204
+
2169
2205
  // Discover custom commands (TypeScript slash commands)
2170
2206
  const customCommandsResult: CustomCommandsLoadResult = options.disableExtensionDiscovery
2171
2207
  ? { commands: [], errors: [] }
@@ -2191,6 +2227,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2191
2227
  modelRegistry,
2192
2228
  () => (hasSession ? createSessionMemoryRuntimeContext(session, agentDir, cwd) : undefined),
2193
2229
  settings,
2230
+ localProtocolOptions,
2194
2231
  );
2195
2232
 
2196
2233
  credentialDisabledTarget = extensionRunner;
@@ -2209,6 +2246,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2209
2246
  session.abort({ reason: USER_INTERRUPT_LABEL });
2210
2247
  },
2211
2248
  settings,
2249
+ localProtocolOptions,
2212
2250
  autoApprove: options.autoApprove ?? false,
2213
2251
  });
2214
2252
  const toolContextStore = new ToolContextStore(getSessionContext);
@@ -2618,36 +2656,25 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2618
2656
 
2619
2657
  const slashCommands = await slashCommandsPromise;
2620
2658
 
2621
- // Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)
2659
+ // Keep image blocks off the wire when they'd be rejected: either the user
2660
+ // disabled images (`images.blockImages`) or the active model has no vision
2661
+ // support. The latter covers switching from a vision model to a text-only
2662
+ // one mid-session — historical image blocks would otherwise be replayed to
2663
+ // a provider that 400s on them (#5400). Read both dynamically so a `/model`
2664
+ // switch or setting change takes effect on the next turn.
2622
2665
  const convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {
2623
2666
  const converted = convertToLlm(messages);
2624
- // Check setting dynamically so mid-session changes take effect
2625
- if (!settings.get("images.blockImages")) {
2626
- return converted;
2667
+ if (settings.get("images.blockImages")) {
2668
+ return replaceLlmImagesWithText(converted, "Image reading is disabled.");
2627
2669
  }
2628
- // Filter out ImageContent from all messages, replacing with text placeholder
2629
- return converted.map(msg => {
2630
- if (msg.role === "user" || msg.role === "toolResult") {
2631
- const content = msg.content;
2632
- if (Array.isArray(content)) {
2633
- const hasImages = content.some(c => c.type === "image");
2634
- if (hasImages) {
2635
- const filteredContent = content
2636
- .map(c =>
2637
- c.type === "image" ? { type: "text" as const, text: "Image reading is disabled." } : c,
2638
- )
2639
- .filter((c, i, arr) => {
2640
- // Dedupe consecutive "Image reading is disabled." texts
2641
- if (!(c.type === "text" && c.text === "Image reading is disabled." && i > 0)) return true;
2642
- const prev = arr[i - 1];
2643
- return !(prev.type === "text" && prev.text === "Image reading is disabled.");
2644
- });
2645
- return { ...msg, content: filteredContent };
2646
- }
2647
- }
2648
- }
2649
- return msg;
2650
- });
2670
+ const activeModel = agent?.state.model ?? model;
2671
+ if (activeModel && !activeModel.input.includes("image")) {
2672
+ return replaceLlmImagesWithText(
2673
+ converted,
2674
+ "[image omitted: the active model does not support image input]",
2675
+ );
2676
+ }
2677
+ return converted;
2651
2678
  };
2652
2679
 
2653
2680
  // Final convertToLlm: live provider replay drops API-level refusal errors,