@oh-my-pi/pi-coding-agent 15.10.4 → 15.10.5

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 (141) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/types/capability/rule-buckets.d.ts +1 -1
  3. package/dist/types/capability/rule.d.ts +6 -1
  4. package/dist/types/cli/update-cli.d.ts +11 -1
  5. package/dist/types/config/model-registry.d.ts +18 -1
  6. package/dist/types/discovery/at-imports.d.ts +15 -0
  7. package/dist/types/edit/diff.d.ts +3 -2
  8. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +1 -0
  9. package/dist/types/eval/backend.d.ts +7 -0
  10. package/dist/types/eval/js/context-manager.d.ts +1 -0
  11. package/dist/types/eval/js/executor.d.ts +2 -0
  12. package/dist/types/eval/js/index.d.ts +1 -1
  13. package/dist/types/eval/js/shared/helpers.d.ts +6 -0
  14. package/dist/types/eval/js/shared/runtime.d.ts +5 -0
  15. package/dist/types/eval/js/worker-protocol.d.ts +6 -0
  16. package/dist/types/eval/py/executor.d.ts +7 -0
  17. package/dist/types/eval/py/index.d.ts +1 -1
  18. package/dist/types/export/ttsr.d.ts +14 -0
  19. package/dist/types/extensibility/extensions/types.d.ts +8 -1
  20. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +1 -1
  21. package/dist/types/internal-urls/local-protocol.d.ts +10 -0
  22. package/dist/types/mcp/oauth-flow.d.ts +2 -2
  23. package/dist/types/modes/components/custom-editor.d.ts +3 -0
  24. package/dist/types/modes/components/{status-line.d.ts → status-line/component.d.ts} +2 -32
  25. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  26. package/dist/types/modes/components/status-line/types.d.ts +31 -2
  27. package/dist/types/modes/image-references.d.ts +8 -3
  28. package/dist/types/modes/interactive-mode.d.ts +1 -1
  29. package/dist/types/modes/theme/theme.d.ts +2 -1
  30. package/dist/types/modes/types.d.ts +2 -1
  31. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  32. package/dist/types/session/agent-session.d.ts +0 -2
  33. package/dist/types/tools/ask.d.ts +1 -0
  34. package/dist/types/tools/browser/tab-worker.d.ts +15 -0
  35. package/dist/types/tools/index.d.ts +17 -0
  36. package/dist/types/tools/render-utils.d.ts +1 -1
  37. package/dist/types/tools/tool-timeouts.d.ts +1 -1
  38. package/dist/types/utils/block-context.d.ts +35 -0
  39. package/dist/types/utils/image-loading.d.ts +12 -0
  40. package/package.json +29 -9
  41. package/src/capability/rule-buckets.ts +4 -2
  42. package/src/capability/rule.ts +10 -1
  43. package/src/cli/auth-broker-cli.ts +6 -7
  44. package/src/cli/auth-gateway-cli.ts +1 -1
  45. package/src/cli/list-models.ts +5 -0
  46. package/src/cli/update-cli.ts +138 -16
  47. package/src/config/model-registry.ts +81 -2
  48. package/src/debug/index.ts +4 -8
  49. package/src/discovery/at-imports.ts +273 -0
  50. package/src/discovery/builtin-rules/index.ts +4 -0
  51. package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
  52. package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
  53. package/src/discovery/helpers.ts +2 -1
  54. package/src/edit/diff.ts +114 -4
  55. package/src/edit/hashline/diff.ts +1 -1
  56. package/src/edit/hashline/execute.ts +1 -1
  57. package/src/edit/modes/patch.ts +6 -2
  58. package/src/edit/modes/replace.ts +1 -1
  59. package/src/edit/renderer.ts +12 -2
  60. package/src/eval/__tests__/helpers-local-roots.test.ts +58 -0
  61. package/src/eval/backend.ts +15 -0
  62. package/src/eval/js/context-manager.ts +4 -2
  63. package/src/eval/js/executor.ts +3 -0
  64. package/src/eval/js/index.ts +7 -1
  65. package/src/eval/js/shared/helpers.ts +53 -6
  66. package/src/eval/js/shared/runtime.ts +8 -0
  67. package/src/eval/js/worker-core.ts +1 -0
  68. package/src/eval/js/worker-protocol.ts +6 -0
  69. package/src/eval/py/executor.ts +12 -0
  70. package/src/eval/py/index.ts +7 -1
  71. package/src/eval/py/prelude.py +43 -4
  72. package/src/eval/py/runner.py +1 -0
  73. package/src/exa/render.ts +1 -1
  74. package/src/export/ttsr.ts +122 -1
  75. package/src/extensibility/extensions/types.ts +8 -1
  76. package/src/extensibility/legacy-pi-ai-shim.ts +1 -1
  77. package/src/extensibility/plugins/doctor.ts +1 -1
  78. package/src/extensibility/plugins/legacy-pi-compat.ts +6 -5
  79. package/src/goals/tools/goal-tool.ts +1 -1
  80. package/src/internal-urls/docs-index.generated.ts +6 -5
  81. package/src/internal-urls/local-protocol.ts +13 -0
  82. package/src/lsp/render.ts +8 -6
  83. package/src/mcp/oauth-flow.ts +3 -3
  84. package/src/mcp/render.ts +7 -1
  85. package/src/modes/components/custom-editor.ts +12 -6
  86. package/src/modes/components/login-dialog.ts +1 -1
  87. package/src/modes/components/oauth-selector.ts +4 -4
  88. package/src/modes/components/read-tool-group.ts +10 -3
  89. package/src/modes/components/{status-line.ts → status-line/component.ts} +18 -40
  90. package/src/modes/components/status-line/index.ts +1 -0
  91. package/src/modes/components/status-line/types.ts +23 -8
  92. package/src/modes/components/tool-execution.ts +1 -1
  93. package/src/modes/components/transcript-container.ts +17 -10
  94. package/src/modes/components/user-message.ts +6 -3
  95. package/src/modes/components/welcome.ts +1 -1
  96. package/src/modes/controllers/extension-ui-controller.ts +143 -127
  97. package/src/modes/controllers/input-controller.ts +36 -10
  98. package/src/modes/controllers/mcp-command-controller.ts +28 -12
  99. package/src/modes/controllers/selector-controller.ts +4 -11
  100. package/src/modes/controllers/ssh-command-controller.ts +2 -2
  101. package/src/modes/image-references.ts +13 -7
  102. package/src/modes/interactive-mode.ts +2 -2
  103. package/src/modes/rpc/rpc-mode.ts +1 -1
  104. package/src/modes/setup-wizard/scenes/sign-in.ts +3 -11
  105. package/src/modes/theme/theme.ts +95 -1
  106. package/src/modes/types.ts +2 -1
  107. package/src/modes/utils/ui-helpers.ts +14 -5
  108. package/src/prompts/tools/bash.md +1 -1
  109. package/src/prompts/tools/eval.md +4 -4
  110. package/src/sdk.ts +31 -14
  111. package/src/session/agent-session.ts +213 -155
  112. package/src/session/session-manager.ts +1 -1
  113. package/src/slash-commands/builtin-registry.ts +1 -1
  114. package/src/system-prompt.ts +15 -9
  115. package/src/task/render.ts +20 -8
  116. package/src/tools/ask.ts +14 -5
  117. package/src/tools/bash-interactive.ts +1 -1
  118. package/src/tools/bash.ts +14 -2
  119. package/src/tools/browser/render.ts +5 -2
  120. package/src/tools/browser/tab-worker.ts +211 -91
  121. package/src/tools/debug.ts +5 -2
  122. package/src/tools/eval-render.ts +6 -3
  123. package/src/tools/eval.ts +1 -1
  124. package/src/tools/gh-renderer.ts +29 -15
  125. package/src/tools/index.ts +32 -0
  126. package/src/tools/inspect-image-renderer.ts +12 -5
  127. package/src/tools/job.ts +9 -6
  128. package/src/tools/memory-render.ts +19 -5
  129. package/src/tools/read.ts +165 -18
  130. package/src/tools/render-utils.ts +3 -1
  131. package/src/tools/resolve.ts +1 -1
  132. package/src/tools/review.ts +1 -1
  133. package/src/tools/ssh.ts +4 -1
  134. package/src/tools/todo.ts +8 -1
  135. package/src/tools/tool-timeouts.ts +1 -1
  136. package/src/tools/write.ts +1 -1
  137. package/src/tui/code-cell.ts +1 -1
  138. package/src/utils/block-context.ts +312 -0
  139. package/src/utils/image-loading.ts +31 -1
  140. package/src/web/search/providers/codex.ts +1 -1
  141. package/src/web/search/render.ts +14 -6
@@ -43,6 +43,7 @@ export type SymbolKey =
43
43
  | "status.running"
44
44
  | "status.shadowed"
45
45
  | "status.aborted"
46
+ | "status.done"
46
47
  // Navigation
47
48
  | "nav.cursor"
48
49
  | "nav.selected"
@@ -197,7 +198,29 @@ export type SymbolKey =
197
198
  | "tab.tools"
198
199
  | "tab.memory"
199
200
  | "tab.tasks"
200
- | "tab.providers";
201
+ | "tab.providers"
202
+ // Tool identity icons
203
+ | "tool.write"
204
+ | "tool.edit"
205
+ | "tool.bash"
206
+ | "tool.ssh"
207
+ | "tool.lsp"
208
+ | "tool.gh"
209
+ | "tool.webSearch"
210
+ | "tool.exa"
211
+ | "tool.browser"
212
+ | "tool.eval"
213
+ | "tool.debug"
214
+ | "tool.mcp"
215
+ | "tool.job"
216
+ | "tool.task"
217
+ | "tool.todo"
218
+ | "tool.memory"
219
+ | "tool.ask"
220
+ | "tool.resolve"
221
+ | "tool.review"
222
+ | "tool.inspectImage"
223
+ | "tool.goal";
201
224
 
202
225
  type SymbolMap = Record<SymbolKey, string>;
203
226
 
@@ -213,6 +236,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
213
236
  "status.running": "⟳",
214
237
  "status.shadowed": "◌",
215
238
  "status.aborted": "⏹",
239
+ "status.done": "•",
216
240
  // Navigation
217
241
  "nav.cursor": "❯",
218
242
  "nav.selected": "➤",
@@ -368,6 +392,28 @@ const UNICODE_SYMBOLS: SymbolMap = {
368
392
  "tab.memory": "🧠",
369
393
  "tab.tasks": "📦",
370
394
  "tab.providers": "🌐",
395
+ // Tool identity icons (per-tool signature glyph on the success header)
396
+ "tool.write": "✎",
397
+ "tool.edit": "✎",
398
+ "tool.bash": "❯",
399
+ "tool.ssh": "⇄",
400
+ "tool.lsp": "💡",
401
+ "tool.gh": "⎇",
402
+ "tool.webSearch": "⌕",
403
+ "tool.exa": "🔭",
404
+ "tool.browser": "🌐",
405
+ "tool.eval": "▶",
406
+ "tool.debug": "🐞",
407
+ "tool.mcp": "🔌",
408
+ "tool.job": "⚙",
409
+ "tool.task": "⇶",
410
+ "tool.todo": "☑",
411
+ "tool.memory": "🧠",
412
+ "tool.ask": "?",
413
+ "tool.resolve": "✓",
414
+ "tool.review": "◉",
415
+ "tool.inspectImage": "🖼",
416
+ "tool.goal": "◎",
371
417
  };
372
418
 
373
419
  const NERD_SYMBOLS: SymbolMap = {
@@ -392,6 +438,8 @@ const NERD_SYMBOLS: SymbolMap = {
392
438
  "status.shadowed": "◐",
393
439
  // pick:  | alt:  
394
440
  "status.aborted": "\uf04d",
441
+ // pick: • | alt: ● ·
442
+ "status.done": "•",
395
443
  // Navigation
396
444
  // pick:  | alt:  
397
445
  "nav.cursor": "\uf054",
@@ -638,6 +686,28 @@ const NERD_SYMBOLS: SymbolMap = {
638
686
  "tab.memory": "󰧑",
639
687
  "tab.tasks": "󰐱",
640
688
  "tab.providers": "󰖟",
689
+ // Tool identity icons (per-tool signature glyph on the success header)
690
+ "tool.write": "\uEA7F",
691
+ "tool.edit": "\uEA73",
692
+ "tool.bash": "\uEBCA",
693
+ "tool.ssh": "\uEB3A",
694
+ "tool.lsp": "\uEA61",
695
+ "tool.gh": "\uEA84",
696
+ "tool.webSearch": "\uEB01",
697
+ "tool.exa": "\uEB68",
698
+ "tool.browser": "\uEAAE",
699
+ "tool.eval": "\uEBAF",
700
+ "tool.debug": "\uEAD8",
701
+ "tool.mcp": "\uEB2D",
702
+ "tool.job": "\uEBA2",
703
+ "tool.task": "\uEA7E",
704
+ "tool.todo": "\uEAB3",
705
+ "tool.memory": "\uEACE",
706
+ "tool.ask": "\uEAC7",
707
+ "tool.resolve": "\uEBB1",
708
+ "tool.review": "\uEA70",
709
+ "tool.inspectImage": "\uEAEA",
710
+ "tool.goal": "\uEBF8",
641
711
  };
642
712
 
643
713
  const ASCII_SYMBOLS: SymbolMap = {
@@ -652,6 +722,7 @@ const ASCII_SYMBOLS: SymbolMap = {
652
722
  "status.running": "[~]",
653
723
  "status.shadowed": "[/]",
654
724
  "status.aborted": "[-]",
725
+ "status.done": "*",
655
726
  // Navigation
656
727
  "nav.cursor": ">",
657
728
  "nav.selected": "->",
@@ -805,6 +876,28 @@ const ASCII_SYMBOLS: SymbolMap = {
805
876
  "tab.memory": "[Y]",
806
877
  "tab.tasks": "[K]",
807
878
  "tab.providers": "[P]",
879
+ // Tool identity icons (per-tool signature glyph on the success header)
880
+ "tool.write": "+f",
881
+ "tool.edit": "~",
882
+ "tool.bash": "$",
883
+ "tool.ssh": "ssh",
884
+ "tool.lsp": "lsp",
885
+ "tool.gh": "gh",
886
+ "tool.webSearch": "web",
887
+ "tool.exa": "exa",
888
+ "tool.browser": "[w]",
889
+ "tool.eval": ">_",
890
+ "tool.debug": "dbg",
891
+ "tool.mcp": "<>",
892
+ "tool.job": "job",
893
+ "tool.task": ">>>",
894
+ "tool.todo": "[x]",
895
+ "tool.memory": "mem",
896
+ "tool.ask": "[?]",
897
+ "tool.resolve": "[v]",
898
+ "tool.review": "rev",
899
+ "tool.inspectImage": "[i]",
900
+ "tool.goal": "(o)",
808
901
  };
809
902
 
810
903
  const SYMBOL_PRESETS: Record<SymbolPreset, SymbolMap> = {
@@ -1485,6 +1578,7 @@ export class Theme {
1485
1578
  running: this.#symbols["status.running"],
1486
1579
  shadowed: this.#symbols["status.shadowed"],
1487
1580
  aborted: this.#symbols["status.aborted"],
1581
+ done: this.#symbols["status.done"],
1488
1582
  };
1489
1583
  }
1490
1584
 
@@ -35,6 +35,7 @@ import type { Theme } from "./theme/theme";
35
35
  export type CompactionQueuedMessage = {
36
36
  text: string;
37
37
  mode: "steer" | "followUp";
38
+ images?: ImageContent[];
38
39
  };
39
40
 
40
41
  export type SubmittedUserInput = {
@@ -180,7 +181,7 @@ export interface InteractiveModeContext {
180
181
  showNewVersionNotification(newVersion: string): void;
181
182
  clearEditor(): void;
182
183
  updatePendingMessagesDisplay(): void;
183
- queueCompactionMessage(text: string, mode: "steer" | "followUp"): void;
184
+ queueCompactionMessage(text: string, mode: "steer" | "followUp", images?: ImageContent[]): void;
184
185
  flushCompactionQueue(options?: { willRetry?: boolean }): Promise<void>;
185
186
  flushPendingBashComponents(): void;
186
187
  flushPendingModelSwitch(): Promise<void>;
@@ -161,7 +161,7 @@ export class UiHelpers {
161
161
  const typeLabel = job.type ? `[${job.type}]` : "[job]";
162
162
  const duration = typeof job.durationMs === "number" ? formatDuration(job.durationMs) : undefined;
163
163
  const line = [
164
- theme.fg("success", `${theme.status.success} Background job completed`),
164
+ theme.fg("success", `${theme.status.done} Background job completed`),
165
165
  theme.fg("dim", typeLabel),
166
166
  theme.fg("accent", jobId),
167
167
  duration ? theme.fg("dim", `(${duration})`) : undefined,
@@ -630,12 +630,18 @@ export class UiHelpers {
630
630
  }
631
631
  }
632
632
 
633
- queueCompactionMessage(text: string, mode: "steer" | "followUp"): void {
634
- this.ctx.compactionQueuedMessages.push({ text, mode } as CompactionQueuedMessage);
633
+ queueCompactionMessage(text: string, mode: "steer" | "followUp", images?: ImageContent[]): void {
634
+ const queuedImages = images && images.length > 0 ? images : undefined;
635
+ this.ctx.compactionQueuedMessages.push({ text, mode, images: queuedImages } as CompactionQueuedMessage);
635
636
  this.ctx.editor.addToHistory(text);
636
637
  this.ctx.editor.setText("");
638
+ this.ctx.editor.imageLinks = undefined;
639
+ this.ctx.pendingImages = [];
640
+ this.ctx.pendingImageLinks = [];
637
641
  this.ctx.updatePendingMessagesDisplay();
638
- this.ctx.showStatus("Queued message for after compaction");
642
+ this.ctx.showStatus(
643
+ queuedImages ? "Queued message with image for after compaction" : "Queued message for after compaction",
644
+ );
639
645
  }
640
646
 
641
647
  async #deliverQueuedMessage(message: CompactionQueuedMessage): Promise<void> {
@@ -644,7 +650,9 @@ export class UiHelpers {
644
650
  return;
645
651
  }
646
652
  await this.ctx.withLocalSubmission(message.text, () =>
647
- message.mode === "followUp" ? this.ctx.session.followUp(message.text) : this.ctx.session.steer(message.text),
653
+ message.mode === "followUp"
654
+ ? this.ctx.session.followUp(message.text, message.images)
655
+ : this.ctx.session.steer(message.text, message.images),
648
656
  );
649
657
  }
650
658
 
@@ -738,6 +746,7 @@ export class UiHelpers {
738
746
  const promptPromise = this.ctx.session
739
747
  .prompt(firstPrompt.text, {
740
748
  streamingBehavior: firstPrompt.mode === "followUp" ? "followUp" : "steer",
749
+ images: firstPrompt.images,
741
750
  })
742
751
  .catch((error: unknown) => {
743
752
  disposeFirstPrompt();
@@ -29,7 +29,7 @@ Executes bash command in shell session for terminal operations like git, bun, ca
29
29
 
30
30
  - `timeout` (seconds) caps the **wall-clock duration** of the command. When it elapses the process is killed and the call returns with a timeout annotation. Range: `1`–`3600`s; default `300`s (see `clampTimeout("bash", …)` in `tool-timeouts.ts`).
31
31
  - `async: true` only defers **reporting** of the result — it does NOT disable, extend, or detach the timeout. A daemon started with `async: true` is still killed when `timeout` elapses, regardless of how long the agent waits before reading the result.
32
- - For long-running daemons (dev servers, watchers): either pass an explicit large `timeout` (up to `3600`), or fully detach the process from this shell using `nohup … &` / `setsid &` / `disown` so it survives independent of the bash call's lifecycle.
32
+ - For long-running daemons (dev servers, watchers): pass an explicit large `timeout` (up to `3600`). The shell session persists across calls, so a backgrounded job (`cmd &`) keeps running between bash calls on its own.
33
33
  {{/if}}
34
34
  {{#if autoBackgroundEnabled}}
35
35
 
@@ -8,7 +8,7 @@ Cell fields:
8
8
  - `language` — {{#if py}}`"py"` for the IPython kernel{{/if}}{{#ifAll py js}}, {{/ifAll}}{{#if js}}`"js"` for the persistent JavaScript VM{{/if}}.
9
9
  - `code` — cell body, verbatim. Newlines, quotes, and indentation are JSON-encoded; no fences, no headers.
10
10
  - `title` (optional) — short label shown in the transcript (e.g. `"imports"`, `"load config"`).
11
- - `timeout` (optional) — per-cell wall-clock budget in seconds (1-600). Default 30. It bounds the cell's **own** work, but is paused while an `agent()`/`parallel()`/`completion()` call is in flight — so a long fanout or a slow completion runs to completion, while the cell itself is still bounded. Compute, `print`/stdout, `log()`/`phase()`, and ordinary tool calls all count against the budget; raise `timeout` for a cell that does heavy local work or long non-agent tool calls.
11
+ - `timeout` (optional) — per-cell wall-clock budget in seconds (1-3600). Default 30. It bounds the cell's **own** work, but is paused while an `agent()`/`parallel()`/`completion()` call is in flight — so a long fanout or a slow completion runs to completion, while the cell itself is still bounded. Compute, `print`/stdout, `log()`/`phase()`, and ordinary tool calls all count against the budget; raise `timeout` for a cell that does heavy local work or long non-agent tool calls.
12
12
  - `reset` (optional) — wipe this cell's language kernel before running.{{#ifAll py js}} Reset is per-language: a `py` cell's reset does not touch the JavaScript VM and vice versa.{{/ifAll}}
13
13
 
14
14
  **Work incrementally:**
@@ -29,11 +29,11 @@ display(value) → None
29
29
  print(value, ...) → None
30
30
  Print to the cell's text output.
31
31
  read(path, offset?=1, limit?=None) → str
32
- Read file contents as text. offset/limit are 1-indexed line bounds.
32
+ Read file contents as text. offset/limit are 1-indexed line bounds. Accepts `local://…` (resolved to the session-local root, same place `read local://…` reads).
33
33
  write(path, content) → str
34
- Write content to a file (creates parent directories). Returns the resolved path.
34
+ Write content to a file (creates parent directories). Returns the resolved path. Accepts `local://…` to persist artifacts across turns / share with subagents.
35
35
  append(path, content) → str
36
- Append content to a file. Returns the resolved path.
36
+ Append content to a file. Returns the resolved path. Accepts `local://…`.
37
37
  tree(path?=".", max_depth?=3, show_hidden?=False) → str
38
38
  Render a directory tree.
39
39
  diff(a, b) → str
package/src/sdk.ts CHANGED
@@ -154,6 +154,7 @@ import {
154
154
  EditTool,
155
155
  EvalTool,
156
156
  FindTool,
157
+ filterInitialToolsForDiscoveryAll,
157
158
  getSearchTools,
158
159
  HIDDEN_TOOLS,
159
160
  isImageProviderPreference,
@@ -1427,7 +1428,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1427
1428
  const getArtifactsDir = () => sessionManager.getArtifactsDir();
1428
1429
  if (!options.parentTaskPrefix) {
1429
1430
  setActiveSkills(skills);
1430
- setActiveRules([...rulebookRules, ...alwaysApplyRules]);
1431
+ // Include TTSR rules so `rule://<name>` can resolve them too. They are
1432
+ // registered with the manager and bucketed out before rulebook/always,
1433
+ // so without this a TTSR-only rule (e.g. a triggered builtin) is not
1434
+ // addressable and `rule://` reports "Available: none".
1435
+ setActiveRules([...rulebookRules, ...alwaysApplyRules, ...ttsrManager.getRules()]);
1431
1436
  if (asyncJobManager) AsyncJobManager.setInstance(asyncJobManager);
1432
1437
  }
1433
1438
  const localProtocolOptions = options.localProtocolOptions ?? {
@@ -1570,6 +1575,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1570
1575
  }
1571
1576
  extensionsResult.runtime.pendingProviderRegistrations = [];
1572
1577
  }
1578
+ // Discover runtime (extension) provider catalogs now that they are
1579
+ // registered. The startup refreshInBackground() ran before extensions
1580
+ // loaded, so dynamic extension providers are only discovered here. Runs in
1581
+ // the background (cache-aware) so startup is never blocked on the fetch; the
1582
+ // model list re-renders when the catalog arrives, like other dynamic providers.
1583
+ void modelRegistry.refreshRuntimeProviders().catch(error => {
1584
+ logger.warn("runtime provider discovery failed", {
1585
+ error: error instanceof Error ? error.message : String(error),
1586
+ });
1587
+ });
1573
1588
 
1574
1589
  // Retry session-model candidates now that extension providers are
1575
1590
  // registered. The initial restore runs before extensions load, so a role
@@ -1951,19 +1966,21 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1951
1966
  // from the initial set unless they were explicitly requested or restored from persistence.
1952
1967
  // The model finds them via search_tool_bm25 and activates them on demand.
1953
1968
  if (effectiveDiscoveryMode === "all") {
1954
- const essentialBuiltinNames = new Set(computeEssentialBuiltinNames(settings));
1955
- const explicitlyRequestedToolNames = new Set(options.toolNames?.map(name => name.toLowerCase()) ?? []);
1956
- // Back-compat: persisted activations live under selectedMCPToolNames today (built-in
1957
- // activation persistence is a follow-up). MCP names won't collide with built-in names.
1958
- const restoredDiscoveredNames = new Set(existingSession.selectedMCPToolNames);
1959
- initialToolNames = initialToolNames.filter(name => {
1960
- const tool = toolRegistry.get(name);
1961
- if (!tool?.loadMode) return true; // not a built-in — leave MCP/custom/extension to existing logic
1962
- if (tool.loadMode === "essential") return true;
1963
- if (essentialBuiltinNames.has(name)) return true;
1964
- if (explicitlyRequestedToolNames.has(name)) return true;
1965
- if (restoredDiscoveredNames.has(name)) return true;
1966
- return false;
1969
+ // Tools a forced tool_choice will target must stay active, or the named
1970
+ // choice references a tool absent from the request (provider 400). Eager
1971
+ // todos force a named `todo` choice on the first turn.
1972
+ const forceActive = new Set<string>();
1973
+ if (settings.get("todo.eager") && settings.get("todo.enabled") && toolRegistry.has("todo")) {
1974
+ forceActive.add("todo");
1975
+ }
1976
+ initialToolNames = filterInitialToolsForDiscoveryAll(initialToolNames, {
1977
+ loadModeOf: name => toolRegistry.get(name)?.loadMode,
1978
+ essentialNames: new Set(computeEssentialBuiltinNames(settings)),
1979
+ explicitlyRequested: new Set(options.toolNames?.map(name => name.toLowerCase()) ?? []),
1980
+ // Back-compat: persisted activations live under selectedMCPToolNames today (built-in
1981
+ // activation persistence is a follow-up). MCP names won't collide with built-in names.
1982
+ restored: new Set(existingSession.selectedMCPToolNames),
1983
+ forceActive,
1967
1984
  });
1968
1985
  }
1969
1986