@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
package/src/sdk.ts CHANGED
@@ -119,7 +119,7 @@ import {
119
119
  obfuscateProviderContext,
120
120
  SecretObfuscator,
121
121
  } from "./secrets";
122
- import { AgentSession, type PlanYolo, type Prewalk } from "./session/agent-session";
122
+ import { AgentSession, type InitialRetryFallbackState, type PlanYolo, type Prewalk } from "./session/agent-session";
123
123
  import { discoverAuthStorage as discoverAuthStorageFromConfig } from "./session/auth-broker-config";
124
124
  import type { AuthStorage } from "./session/auth-storage";
125
125
  import { createInterruptedTurnAbortMessage } from "./session/exit-diagnostics";
@@ -907,7 +907,7 @@ function registerEvalCleanup(): void {
907
907
  postmortem.register("julia-cleanup", disposeAllJuliaKernelSessions);
908
908
  }
909
909
 
910
- function customToolToDefinition(tool: CustomTool): ToolDefinition {
910
+ export function customToolToDefinition(tool: CustomTool): ToolDefinition {
911
911
  const definition: ToolDefinition & { [TOOL_DEFINITION_MARKER]: true } = {
912
912
  name: tool.name,
913
913
  label: tool.label,
@@ -917,6 +917,9 @@ function customToolToDefinition(tool: CustomTool): ToolDefinition {
917
917
  loadMode: defaultLoadModeForToolName(tool.name, tool.loadMode),
918
918
  deferrable: tool.deferrable,
919
919
  approval: typeof tool.approval === "function" ? tool.approval.bind(tool) : tool.approval,
920
+ // Preserved through RegisteredToolAdapter so MCP-backed tools' explicit
921
+ // `strict: false` (#4336/#4340) survives the custom-tool → definition bridge.
922
+ strict: tool.strict,
920
923
  mcpServerName: tool.mcpServerName,
921
924
  mcpToolName: tool.mcpToolName,
922
925
  execute: (toolCallId, params, signal, onUpdate, ctx) =>
@@ -1387,6 +1390,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1387
1390
  );
1388
1391
  let model = options.model;
1389
1392
  let modelFallbackMessage: string | undefined;
1393
+ let initialRetryFallback: InitialRetryFallbackState | undefined;
1390
1394
  // Identify session model strings to restore in fallback order. We do an
1391
1395
  // initial pass here so model-dependent setup (thinking-level resolution,
1392
1396
  // host preconnect) can use the restored model; extension-registered
@@ -1692,6 +1696,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1692
1696
  getAgentId: () => resolvedAgentId,
1693
1697
  getToolByName: name => session?.getToolByName(name),
1694
1698
  agentRegistry,
1699
+ // The global lifecycle releases through AgentRegistry.global(); wiring it
1700
+ // onto a caller-supplied registry would report a cancel while releasing an
1701
+ // unrelated global ref. With no lifecycle, hub cancel falls back to
1702
+ // dispose + unregister on the session's own registry.
1703
+ agentLifecycle: options.agentRegistry ? undefined : () => AgentLifecycleManager.global(),
1695
1704
  getSessionSpawns: () => options.spawns ?? "*",
1696
1705
  getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
1697
1706
  getActiveModelString,
@@ -2022,8 +2031,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2022
2031
  // the background.
2023
2032
  await modelRegistry.refreshRuntimeProviders("offline");
2024
2033
  // Continue runtime discovery in the background (cache-aware) so startup is
2025
- // only blocked on local cache reads, not provider network fetches.
2026
- void modelRegistry.refreshRuntimeProviders().catch(error => {
2034
+ // only blocked on local cache reads, not provider network fetches. Stash
2035
+ // the promise so the deferred `--model` retry below can await it instead
2036
+ // of starting a second concurrent discovery pass (the unfiltered
2037
+ // `refresh()` also covers runtime model managers).
2038
+ const runtimeDiscoveryPromise = modelRegistry.refreshRuntimeProviders().catch(error => {
2027
2039
  logger.warn("runtime provider discovery failed", {
2028
2040
  error: error instanceof Error ? error.message : String(error),
2029
2041
  });
@@ -2072,8 +2084,42 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2072
2084
  // registered. Use the same CLI resolver as the immediate path so bare role
2073
2085
  // names, exact model names, and provider selectors keep one precedence rule.
2074
2086
  if (!model && deferredModelPatterns.length > 0) {
2075
- const availableModels = modelRegistry.getAll();
2087
+ // Deferred `--model` patterns almost always failed at the immediate
2088
+ // path (main.ts:881) precisely because discovery-backed providers
2089
+ // hadn't populated yet. Await the in-flight runtime discovery
2090
+ // already kicked off above (stash + reuse avoids a second concurrent
2091
+ // `#refreshRuntimeDiscoveries` pass for the same runtime model
2092
+ // managers; it resolves instantly when no runtime managers are
2093
+ // registered). `refreshRuntimeProviders()` only covers runtime model
2094
+ // managers, not config-discovery providers (e.g. user-configured
2095
+ // ollama); fall back to a full cache-aware refresh only when the
2096
+ // runtime pass didn't surface a match AND config-discovery providers
2097
+ // exist to fetch from. By then runtime managers short-circuit on the
2098
+ // fresh cache written by the awaited pass, closing the double-fetch
2099
+ // window.
2100
+ await logger.time("resolveModelDiscoveryDeferredRetry", () => runtimeDiscoveryPromise);
2076
2101
  const matchPreferences = getModelMatchPreferences(settings);
2102
+ const runtimeResolved = deferredModelPatterns.some(pattern =>
2103
+ pattern.split(",").some(selector => {
2104
+ const trimmedSelector = selector.trim();
2105
+ if (!trimmedSelector) return false;
2106
+ const resolved = resolveCliModel({
2107
+ cliModel: trimmedSelector,
2108
+ modelRegistry,
2109
+ settings,
2110
+ preferences: matchPreferences,
2111
+ });
2112
+ return Boolean(
2113
+ resolved.model || (resolved.configuredPatterns && resolved.configuredPatterns.length > 0),
2114
+ );
2115
+ }),
2116
+ );
2117
+ if (!runtimeResolved && modelRegistry.getDiscoverableProviders().length > 0) {
2118
+ await logger.time("resolveModelDiscoveryFallbackNonRuntime", () =>
2119
+ modelRegistry.refresh("online-if-uncached"),
2120
+ );
2121
+ }
2122
+ const availableModels = modelRegistry.getAll();
2077
2123
  const expandedModelPatterns = deferredModelPatterns.flatMap(pattern =>
2078
2124
  pattern.split(",").flatMap(selector => {
2079
2125
  const trimmedSelector = selector.trim();
@@ -2085,26 +2131,70 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2085
2131
  preferences: matchPreferences,
2086
2132
  });
2087
2133
  if (resolved.configuredPatterns && resolved.configuredPatterns.length > 0) {
2088
- return resolved.configuredPatterns;
2134
+ const primaryPatterns: Array<{
2135
+ pattern: string;
2136
+ retryFallback: InitialRetryFallbackState | undefined;
2137
+ }> = resolved.configuredPatterns.map(pattern => ({
2138
+ pattern,
2139
+ retryFallback: undefined,
2140
+ }));
2141
+ if (!resolved.configuredRole || !settings.get("retry.modelFallback")) {
2142
+ return primaryPatterns;
2143
+ }
2144
+ const fallbackChains = settings.get("retry.fallbackChains");
2145
+ const roleFallbacks =
2146
+ fallbackChains[resolved.configuredRole] ??
2147
+ (resolved.configuredRole === "default" ? undefined : fallbackChains.default);
2148
+ if (!Array.isArray(roleFallbacks)) return primaryPatterns;
2149
+ const originalSelector = resolved.configuredPatterns[0];
2150
+ const parsedOriginal = parseModelString(originalSelector, {
2151
+ allowMaxSuffix: true,
2152
+ allowAutoAlias: true,
2153
+ isLiteralModelId: (provider, id) => modelRegistry.find(provider, id) !== undefined,
2154
+ });
2155
+ const retryFallback: InitialRetryFallbackState = {
2156
+ role: resolved.configuredRole,
2157
+ originalSelector,
2158
+ originalThinkingLevel: parsedOriginal?.thinkingLevel,
2159
+ };
2160
+ return [
2161
+ ...primaryPatterns,
2162
+ ...roleFallbacks
2163
+ .filter((pattern): pattern is string => typeof pattern === "string")
2164
+ .map(pattern => ({ pattern, retryFallback })),
2165
+ ];
2089
2166
  }
2090
2167
  if (resolved.model) {
2091
2168
  return [
2092
- formatModelSelectorValue(
2093
- resolved.selector ?? formatModelStringWithRouting(resolved.model),
2094
- resolved.thinkingLevel,
2095
- ),
2169
+ {
2170
+ pattern: formatModelSelectorValue(
2171
+ resolved.selector ?? formatModelStringWithRouting(resolved.model),
2172
+ resolved.thinkingLevel,
2173
+ ),
2174
+ retryFallback: undefined,
2175
+ },
2096
2176
  ];
2097
2177
  }
2098
- return resolveConfiguredModelPatterns([trimmedSelector], settings);
2178
+ return resolveConfiguredModelPatterns([trimmedSelector], settings).map(pattern => ({
2179
+ pattern,
2180
+ retryFallback: undefined,
2181
+ }));
2099
2182
  }),
2100
2183
  );
2101
2184
  for (let patternIndex = 0; patternIndex < expandedModelPatterns.length; patternIndex += 1) {
2102
- const pattern = expandedModelPatterns[patternIndex];
2185
+ const { pattern, retryFallback } = expandedModelPatterns[patternIndex];
2103
2186
  const primary = parseModelPattern(pattern, availableModels, matchPreferences);
2104
- if (!primary.model) continue;
2187
+ if (!primary.model || (retryFallback && !hasModelAuth(primary.model))) continue;
2105
2188
  let selectedModel = primary.model;
2106
2189
  let selectedThinkingLevel = primary.thinkingLevel;
2107
2190
  let selectedExplicitThinkingLevel = primary.explicitThinkingLevel;
2191
+ // A chain entry without its own `:level` suffix inherits the
2192
+ // unavailable primary's configured thinking level, matching
2193
+ // runtime fallback-chain semantics.
2194
+ if (retryFallback && !selectedExplicitThinkingLevel && retryFallback.originalThinkingLevel !== undefined) {
2195
+ selectedThinkingLevel = retryFallback.originalThinkingLevel;
2196
+ selectedExplicitThinkingLevel = true;
2197
+ }
2108
2198
  let authFallbackUsed = false;
2109
2199
  if (options.modelPatternAuthFallback) {
2110
2200
  const primaryKey = await modelRegistry.getApiKey(primary.model);
@@ -2132,8 +2222,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2132
2222
  );
2133
2223
  const seenSelectors = new Set<string>([primarySelector]);
2134
2224
  const fallbackSelectors: string[] = [];
2135
- for (const fallbackPattern of expandedModelPatterns.slice(patternIndex + 1)) {
2136
- const fallback = parseModelPattern(fallbackPattern, availableModels, matchPreferences);
2225
+ for (const fallbackEntry of expandedModelPatterns.slice(patternIndex + 1)) {
2226
+ const fallback = parseModelPattern(fallbackEntry.pattern, availableModels, matchPreferences);
2137
2227
  if (!fallback.model) continue;
2138
2228
  const fallbackSelector = formatModelSelectorValue(
2139
2229
  formatModelStringWithRouting(fallback.model),
@@ -2174,6 +2264,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2174
2264
  }
2175
2265
  }
2176
2266
  model = selectedModel;
2267
+ initialRetryFallback = retryFallback;
2177
2268
  modelFallbackMessage = undefined;
2178
2269
  if (selectedExplicitThinkingLevel) {
2179
2270
  restoredSessionThinkingLevel = selectedThinkingLevel;
@@ -2202,48 +2293,84 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2202
2293
  // path-scoped `enabledModels` allow-list when configured. Skip when the
2203
2294
  // user explicitly requested a model via --model that wasn't found.
2204
2295
  if (!model && deferredModelPatterns.length === 0) {
2205
- // Re-resolve the allowed set: extension factories above may have
2206
- // registered providers/models that weren't visible at startup.
2207
- const fallbackCandidates = await resolveAllowedModels(modelRegistry, settings, modelMatchPreferences);
2208
-
2209
- // Retry the default-role lookup against the post-extension allowed
2210
- // set. Extension factories register providers AFTER the early
2211
- // `defaultRoleSpec` resolution, so a role pointing at an extension
2212
- // model (e.g. an openai-compat plugin's `posthog/claude-opus-4-8`)
2213
- // returned `undefined` there. Without this retry the next step's
2214
- // `pickDefaultAvailableModel` happily replaces the user's configured
2215
- // default with a bundled provider's default whenever a stray
2216
- // `OPENAI_API_KEY`/`ANTHROPIC_API_KEY` is in the environment.
2217
- // (issue #3569)
2218
- if (!hasExplicitModel && !defaultRoleSpec.model) {
2296
+ // Retry the configured default role against the current catalog,
2297
+ // setting `model` (+ thinking level) when it resolves. Extension
2298
+ // factories register providers AFTER the early `defaultRoleSpec`
2299
+ // resolution, and configured discovery providers may still be
2300
+ // mid-discovery, so a role pointing at such a model (an openai-compat
2301
+ // plugin's `posthog/claude-opus-4-8`, a models.yml `openai-models-list`
2302
+ // endpoint) returned `undefined` there. Without this retry the
2303
+ // `pickDefaultAvailableModel` fallback below happily replaces the
2304
+ // user's configured default with a bundled provider's default whenever
2305
+ // a stray `OPENAI_API_KEY`/`ANTHROPIC_API_KEY` is in the environment.
2306
+ // (issues #3569, #6162)
2307
+ const tryResolveDefaultRole = async (): Promise<boolean> => {
2308
+ if (hasExplicitModel) return false;
2309
+ // Re-resolve the allowed set: extension factories and discovery
2310
+ // refreshes above may have registered models not visible earlier.
2311
+ const fallbackCandidates = await resolveAllowedModels(modelRegistry, settings, modelMatchPreferences);
2219
2312
  const reResolvedRoleSpec = resolveModelRoleValue(settings.getModelRole("default"), fallbackCandidates, {
2220
2313
  settings,
2221
2314
  matchPreferences: modelMatchPreferences,
2222
2315
  });
2223
- if (reResolvedRoleSpec.model) {
2224
- defaultRoleSpec = reResolvedRoleSpec;
2225
- const resolvedDefaultModel = reResolvedRoleSpec.model;
2226
- model = resolvedDefaultModel;
2227
- modelFallbackMessage = undefined;
2228
- // Recompute the thinking level against the now-real model.
2229
- // `pickInitialThinkingLevel` closes over `defaultRoleSpec`,
2230
- // so the role's explicit selector (e.g. `:max`) now applies.
2231
- thinkingLevel = pickInitialThinkingLevel(resolvedDefaultModel);
2232
- autoThinking = thinkingLevel === AUTO_THINKING;
2233
- effectiveThinkingLevel = concreteThinkingLevel(thinkingLevel);
2234
- effectiveThinkingLevel = logger.time("resolveThinkingLevelForModel", () =>
2235
- autoThinking
2236
- ? resolveProvisionalAutoLevel(resolvedDefaultModel)
2237
- : resolveThinkingLevelForModel(resolvedDefaultModel, effectiveThinkingLevel),
2238
- );
2239
- preconnectModelHost(resolvedDefaultModel.baseUrl);
2240
- }
2241
- }
2316
+ if (!reResolvedRoleSpec.model) return false;
2317
+ defaultRoleSpec = reResolvedRoleSpec;
2318
+ const resolvedDefaultModel = reResolvedRoleSpec.model;
2319
+ model = resolvedDefaultModel;
2320
+ modelFallbackMessage = undefined;
2321
+ // Recompute the thinking level against the now-real model.
2322
+ // `pickInitialThinkingLevel` closes over `defaultRoleSpec`,
2323
+ // so the role's explicit selector (e.g. `:max`) now applies.
2324
+ thinkingLevel = pickInitialThinkingLevel(resolvedDefaultModel);
2325
+ autoThinking = thinkingLevel === AUTO_THINKING;
2326
+ effectiveThinkingLevel = concreteThinkingLevel(thinkingLevel);
2327
+ effectiveThinkingLevel = logger.time("resolveThinkingLevelForModel", () =>
2328
+ autoThinking
2329
+ ? resolveProvisionalAutoLevel(resolvedDefaultModel)
2330
+ : resolveThinkingLevelForModel(resolvedDefaultModel, effectiveThinkingLevel),
2331
+ );
2332
+ preconnectModelHost(resolvedDefaultModel.baseUrl);
2333
+ return true;
2334
+ };
2335
+
2336
+ await tryResolveDefaultRole();
2242
2337
 
2243
2338
  if (!model) {
2244
- const defaultModel = pickDefaultAvailableModel(fallbackCandidates.filter(hasModelAuth));
2245
- if (defaultModel) {
2246
- model = defaultModel;
2339
+ const fallbackCandidates = await resolveAllowedModels(modelRegistry, settings, modelMatchPreferences);
2340
+ let pick = pickDefaultAvailableModel(fallbackCandidates.filter(hasModelAuth));
2341
+
2342
+ // Cold-cache discovery race (issues #6114, #6162): a discovery
2343
+ // provider (models.yml `openai-models-list`, LM Studio/Ollama/
2344
+ // llama.cpp, or an openai-compat proxy) ships no static models, so
2345
+ // the static+cached catalog resolved nothing above. Background
2346
+ // discovery in main.ts fires only AFTER createAgentSession returns,
2347
+ // so on a cache-cold boot the configured default stays unresolved
2348
+ // and `pick` silently degrades to an unrelated authed provider's
2349
+ // default (#6162) or "No models available" (#6114) — even though
2350
+ // `omp models` (which awaits discovery) lists the model. Await one
2351
+ // cache-aware discovery pass and retry when a default role is
2352
+ // configured (must win over `pick`) or nothing resolved at all.
2353
+ // The common path — role already resolved, or a `pick` with no
2354
+ // configured default — never pays for it.
2355
+ const defaultRoleConfigured = Boolean(settings.getModelRole("default"));
2356
+ if (
2357
+ !hasExplicitModel &&
2358
+ (defaultRoleConfigured || !pick) &&
2359
+ modelRegistry.getDiscoverableProviders().length > 0
2360
+ ) {
2361
+ await logger.time("resolveModelDiscoveryFallback", () => modelRegistry.refresh("online-if-uncached"));
2362
+ if (!(await tryResolveDefaultRole()) && !model) {
2363
+ const refreshedCandidates = await resolveAllowedModels(
2364
+ modelRegistry,
2365
+ settings,
2366
+ modelMatchPreferences,
2367
+ );
2368
+ pick = pickDefaultAvailableModel(refreshedCandidates.filter(hasModelAuth));
2369
+ }
2370
+ }
2371
+
2372
+ if (!model && pick) {
2373
+ model = pick;
2247
2374
  }
2248
2375
  }
2249
2376
  if (model) {
@@ -2918,6 +3045,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2918
3045
  agent,
2919
3046
  pruneToolDescriptions: inlineToolDescriptors,
2920
3047
  thinkingLevel: autoThinking ? AUTO_THINKING : effectiveThinkingLevel,
3048
+ initialRetryFallback,
2921
3049
  prewalk: options.prewalk,
2922
3050
  planYolo: options.planYolo,
2923
3051
  serviceTierByFamily: initialServiceTierByFamily,
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Contract: a turn ending in a provider error surfaces one warn-level log
3
+ * carrying `provider`/`model`/`errorMessage`/`errorStatus`/`errorId`, so
4
+ * recurring provider stream failures are diagnosable from the main log alone
5
+ * (issue #6177). Non-error stops must not emit it.
6
+ */
7
+ import { afterAll, afterEach, describe, expect, it } from "bun:test";
8
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
9
+ import { logger } from "@oh-my-pi/pi-utils";
10
+ import { logProviderTurnError } from "./agent-session";
11
+
12
+ function makeMessage(overrides: Partial<AssistantMessage>): AssistantMessage {
13
+ return {
14
+ role: "assistant",
15
+ content: [],
16
+ api: "cursor",
17
+ provider: "cursor",
18
+ model: "composer-2.5",
19
+ usage: {} as AssistantMessage["usage"],
20
+ stopReason: "error",
21
+ timestamp: 1,
22
+ ...overrides,
23
+ };
24
+ }
25
+
26
+ describe("logProviderTurnError", () => {
27
+ const events: logger.LogEvent[] = [];
28
+ const dispose = logger.registerLogSink(event => events.push(event));
29
+
30
+ afterEach(() => {
31
+ events.length = 0;
32
+ });
33
+ afterAll(() => dispose());
34
+
35
+ it("emits a warn log with the provider error fields for stopReason:error", () => {
36
+ logProviderTurnError(
37
+ makeMessage({
38
+ errorMessage: "stream stall: idle watchdog fired",
39
+ errorStatus: 500,
40
+ errorId: 42,
41
+ }),
42
+ );
43
+
44
+ const warn = events.find(e => e.level === "warn" && e.message === "agent turn ended with provider error");
45
+ expect(warn).toBeDefined();
46
+ expect(warn?.context).toMatchObject({
47
+ provider: "cursor",
48
+ model: "composer-2.5",
49
+ errorMessage: "stream stall: idle watchdog fired",
50
+ errorStatus: 500,
51
+ errorId: 42,
52
+ });
53
+ });
54
+
55
+ it("does not emit for a successful stop", () => {
56
+ logProviderTurnError(makeMessage({ stopReason: "stop", errorMessage: undefined }));
57
+ expect(events.some(e => e.message === "agent turn ended with provider error")).toBe(false);
58
+ });
59
+ });