@mono-agent/agent-app 0.20.11 → 0.21.0

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 (369) hide show
  1. package/README.md +260 -98
  2. package/dist/adapter-send-tools.d.ts +3 -2
  3. package/dist/adapter-send-tools.d.ts.map +1 -1
  4. package/dist/adapter-send-tools.js +15 -5
  5. package/dist/adapter-send-tools.js.map +1 -1
  6. package/dist/app-controller-channels.d.ts +6 -0
  7. package/dist/app-controller-channels.d.ts.map +1 -1
  8. package/dist/app-controller-channels.js +29 -2
  9. package/dist/app-controller-channels.js.map +1 -1
  10. package/dist/app-controller-continuation.d.ts.map +1 -1
  11. package/dist/app-controller-continuation.js +2 -3
  12. package/dist/app-controller-continuation.js.map +1 -1
  13. package/dist/app-controller-lifecycle.d.ts +4 -0
  14. package/dist/app-controller-lifecycle.d.ts.map +1 -1
  15. package/dist/app-controller-lifecycle.js +7 -0
  16. package/dist/app-controller-lifecycle.js.map +1 -1
  17. package/dist/app-controller-maintenance.d.ts +3 -1
  18. package/dist/app-controller-maintenance.d.ts.map +1 -1
  19. package/dist/app-controller-maintenance.js +3 -0
  20. package/dist/app-controller-maintenance.js.map +1 -1
  21. package/dist/app-controller-monitors.d.ts +26 -0
  22. package/dist/app-controller-monitors.d.ts.map +1 -0
  23. package/dist/app-controller-monitors.js +127 -0
  24. package/dist/app-controller-monitors.js.map +1 -0
  25. package/dist/app-controller-responder.d.ts +9 -17
  26. package/dist/app-controller-responder.d.ts.map +1 -1
  27. package/dist/app-controller-responder.js +54 -86
  28. package/dist/app-controller-responder.js.map +1 -1
  29. package/dist/app-controller-traceability.d.ts.map +1 -1
  30. package/dist/app-controller-traceability.js +3 -0
  31. package/dist/app-controller-traceability.js.map +1 -1
  32. package/dist/app-controller-types.d.ts +1 -0
  33. package/dist/app-controller-types.d.ts.map +1 -1
  34. package/dist/app-controller-utils.d.ts +1 -5
  35. package/dist/app-controller-utils.d.ts.map +1 -1
  36. package/dist/app-controller-utils.js +4 -21
  37. package/dist/app-controller-utils.js.map +1 -1
  38. package/dist/app-controller.d.ts +26 -10
  39. package/dist/app-controller.d.ts.map +1 -1
  40. package/dist/app-controller.js +39 -1
  41. package/dist/app-controller.js.map +1 -1
  42. package/dist/artifact-retention.js +11 -0
  43. package/dist/artifact-retention.js.map +1 -1
  44. package/dist/background.d.ts +0 -6
  45. package/dist/background.d.ts.map +1 -1
  46. package/dist/background.js +5 -58
  47. package/dist/background.js.map +1 -1
  48. package/dist/channel-drivers/slack.d.ts.map +1 -1
  49. package/dist/channel-drivers/slack.js +18 -0
  50. package/dist/channel-drivers/slack.js.map +1 -1
  51. package/dist/channel-drivers/telegram.d.ts.map +1 -1
  52. package/dist/channel-drivers/telegram.js +26 -0
  53. package/dist/channel-drivers/telegram.js.map +1 -1
  54. package/dist/channel-drivers/tui.d.ts +68 -8
  55. package/dist/channel-drivers/tui.d.ts.map +1 -1
  56. package/dist/channel-drivers/tui.js +407 -89
  57. package/dist/channel-drivers/tui.js.map +1 -1
  58. package/dist/channel-runtime-controls.d.ts +2 -1
  59. package/dist/channel-runtime-controls.d.ts.map +1 -1
  60. package/dist/channel-runtime-controls.js +79 -22
  61. package/dist/channel-runtime-controls.js.map +1 -1
  62. package/dist/cli-args.d.ts +5 -8
  63. package/dist/cli-args.d.ts.map +1 -1
  64. package/dist/cli-args.js +33 -52
  65. package/dist/cli-args.js.map +1 -1
  66. package/dist/cli-background-command.d.ts.map +1 -1
  67. package/dist/cli-background-command.js +4 -0
  68. package/dist/cli-background-command.js.map +1 -1
  69. package/dist/cli-commands.d.ts.map +1 -1
  70. package/dist/cli-commands.js +24 -2
  71. package/dist/cli-commands.js.map +1 -1
  72. package/dist/cli-help.d.ts +26 -0
  73. package/dist/cli-help.d.ts.map +1 -1
  74. package/dist/cli-help.js +58 -53
  75. package/dist/cli-help.js.map +1 -1
  76. package/dist/cli-init-command.d.ts +1 -2
  77. package/dist/cli-init-command.d.ts.map +1 -1
  78. package/dist/cli-init-command.js +31 -71
  79. package/dist/cli-init-command.js.map +1 -1
  80. package/dist/cli-install-skill-command.d.ts.map +1 -1
  81. package/dist/cli-install-skill-command.js +12 -3
  82. package/dist/cli-install-skill-command.js.map +1 -1
  83. package/dist/config-reference.d.ts +1 -0
  84. package/dist/config-reference.d.ts.map +1 -1
  85. package/dist/config-reference.js +217 -60
  86. package/dist/config-reference.js.map +1 -1
  87. package/dist/configured-agent.d.ts +27 -4
  88. package/dist/configured-agent.d.ts.map +1 -1
  89. package/dist/configured-agent.js +159 -112
  90. package/dist/configured-agent.js.map +1 -1
  91. package/dist/conversation-title.d.ts.map +1 -1
  92. package/dist/conversation-title.js +9 -2
  93. package/dist/conversation-title.js.map +1 -1
  94. package/dist/cron-control-store.d.ts.map +1 -1
  95. package/dist/cron-control-store.js +4 -2
  96. package/dist/cron-control-store.js.map +1 -1
  97. package/dist/doctor.d.ts +7 -13
  98. package/dist/doctor.d.ts.map +1 -1
  99. package/dist/doctor.js +406 -770
  100. package/dist/doctor.js.map +1 -1
  101. package/dist/first-run-readiness.d.ts.map +1 -1
  102. package/dist/first-run-readiness.js +0 -6
  103. package/dist/first-run-readiness.js.map +1 -1
  104. package/dist/index.d.ts +2 -0
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +1 -0
  107. package/dist/index.js.map +1 -1
  108. package/dist/interaction-bridge.d.ts +3 -3
  109. package/dist/interaction-bridge.d.ts.map +1 -1
  110. package/dist/interaction-bridge.js +27 -10
  111. package/dist/interaction-bridge.js.map +1 -1
  112. package/dist/launchd.d.ts +2 -0
  113. package/dist/launchd.d.ts.map +1 -1
  114. package/dist/launchd.js +3 -0
  115. package/dist/launchd.js.map +1 -1
  116. package/dist/local-tui-session.d.ts +14 -0
  117. package/dist/local-tui-session.d.ts.map +1 -0
  118. package/dist/local-tui-session.js +100 -0
  119. package/dist/local-tui-session.js.map +1 -0
  120. package/dist/mcp-apps.d.ts.map +1 -1
  121. package/dist/mcp-apps.js +3 -0
  122. package/dist/mcp-apps.js.map +1 -1
  123. package/dist/memory-journal.d.ts +53 -0
  124. package/dist/memory-journal.d.ts.map +1 -0
  125. package/dist/memory-journal.js +586 -0
  126. package/dist/memory-journal.js.map +1 -0
  127. package/dist/memory-recall.d.ts.map +1 -1
  128. package/dist/memory-recall.js +16 -2
  129. package/dist/memory-recall.js.map +1 -1
  130. package/dist/memory-remember.d.ts +49 -0
  131. package/dist/memory-remember.d.ts.map +1 -0
  132. package/dist/memory-remember.js +190 -0
  133. package/dist/memory-remember.js.map +1 -0
  134. package/dist/memory-retrieval.d.ts +33 -0
  135. package/dist/memory-retrieval.d.ts.map +1 -1
  136. package/dist/memory-retrieval.js +43 -0
  137. package/dist/memory-retrieval.js.map +1 -1
  138. package/dist/model-effort-capabilities.d.ts +8 -9
  139. package/dist/model-effort-capabilities.d.ts.map +1 -1
  140. package/dist/model-effort-capabilities.js +17 -35
  141. package/dist/model-effort-capabilities.js.map +1 -1
  142. package/dist/modules/base.d.ts +3 -4
  143. package/dist/modules/base.d.ts.map +1 -1
  144. package/dist/modules/base.js +4 -6
  145. package/dist/modules/base.js.map +1 -1
  146. package/dist/modules/catalog.d.ts.map +1 -1
  147. package/dist/modules/catalog.js +11 -9
  148. package/dist/modules/catalog.js.map +1 -1
  149. package/dist/modules/known-tools.d.ts +2 -2
  150. package/dist/modules/known-tools.d.ts.map +1 -1
  151. package/dist/modules/known-tools.js +7 -0
  152. package/dist/modules/known-tools.js.map +1 -1
  153. package/dist/monitor-channel-routing.d.ts +20 -0
  154. package/dist/monitor-channel-routing.d.ts.map +1 -0
  155. package/dist/monitor-channel-routing.js +73 -0
  156. package/dist/monitor-channel-routing.js.map +1 -0
  157. package/dist/monitors-command.d.ts +23 -0
  158. package/dist/monitors-command.d.ts.map +1 -0
  159. package/dist/monitors-command.js +217 -0
  160. package/dist/monitors-command.js.map +1 -0
  161. package/dist/monitors-config.d.ts +77 -0
  162. package/dist/monitors-config.d.ts.map +1 -0
  163. package/dist/monitors-config.js +150 -0
  164. package/dist/monitors-config.js.map +1 -0
  165. package/dist/monitors-context.d.ts +58 -0
  166. package/dist/monitors-context.d.ts.map +1 -0
  167. package/dist/monitors-context.js +194 -0
  168. package/dist/monitors-context.js.map +1 -0
  169. package/dist/monitors-runtime.d.ts +36 -0
  170. package/dist/monitors-runtime.d.ts.map +1 -0
  171. package/dist/monitors-runtime.js +103 -0
  172. package/dist/monitors-runtime.js.map +1 -0
  173. package/dist/monitors-service.d.ts +71 -0
  174. package/dist/monitors-service.d.ts.map +1 -0
  175. package/dist/monitors-service.js +1796 -0
  176. package/dist/monitors-service.js.map +1 -0
  177. package/dist/monitors-store.d.ts +93 -0
  178. package/dist/monitors-store.d.ts.map +1 -0
  179. package/dist/monitors-store.js +313 -0
  180. package/dist/monitors-store.js.map +1 -0
  181. package/dist/notify-destinations.d.ts +3 -2
  182. package/dist/notify-destinations.d.ts.map +1 -1
  183. package/dist/notify-destinations.js +6 -7
  184. package/dist/notify-destinations.js.map +1 -1
  185. package/dist/posted-reply-history.d.ts.map +1 -1
  186. package/dist/posted-reply-history.js +40 -3
  187. package/dist/posted-reply-history.js.map +1 -1
  188. package/dist/proactive-notify.d.ts +20 -1
  189. package/dist/proactive-notify.d.ts.map +1 -1
  190. package/dist/proactive-notify.js +56 -3
  191. package/dist/proactive-notify.js.map +1 -1
  192. package/dist/process-job-channel-routing.js +2 -1
  193. package/dist/process-job-channel-routing.js.map +1 -1
  194. package/dist/process-job-output-tail.d.ts +53 -0
  195. package/dist/process-job-output-tail.d.ts.map +1 -0
  196. package/dist/process-job-output-tail.js +257 -0
  197. package/dist/process-job-output-tail.js.map +1 -0
  198. package/dist/process-jobs-config.d.ts +1 -1
  199. package/dist/process-jobs-config.js +1 -1
  200. package/dist/process-jobs-config.js.map +1 -1
  201. package/dist/process-jobs-context.d.ts +11 -1
  202. package/dist/process-jobs-context.d.ts.map +1 -1
  203. package/dist/process-jobs-context.js +55 -11
  204. package/dist/process-jobs-context.js.map +1 -1
  205. package/dist/process-jobs-protection.js +1 -1
  206. package/dist/process-jobs-protection.js.map +1 -1
  207. package/dist/process-jobs-runtime.d.ts.map +1 -1
  208. package/dist/process-jobs-runtime.js +15 -12
  209. package/dist/process-jobs-runtime.js.map +1 -1
  210. package/dist/process-jobs-service.d.ts.map +1 -1
  211. package/dist/process-jobs-service.js +118 -172
  212. package/dist/process-jobs-service.js.map +1 -1
  213. package/dist/process-jobs-store.d.ts +3 -1
  214. package/dist/process-jobs-store.d.ts.map +1 -1
  215. package/dist/process-jobs-store.js +4 -1
  216. package/dist/process-jobs-store.js.map +1 -1
  217. package/dist/process-output-redaction.d.ts +58 -0
  218. package/dist/process-output-redaction.d.ts.map +1 -0
  219. package/dist/process-output-redaction.js +311 -0
  220. package/dist/process-output-redaction.js.map +1 -0
  221. package/dist/project-skills.d.ts +11 -5
  222. package/dist/project-skills.d.ts.map +1 -1
  223. package/dist/project-skills.js +88 -39
  224. package/dist/project-skills.js.map +1 -1
  225. package/dist/provider-auth-checks.d.ts +27 -0
  226. package/dist/provider-auth-checks.d.ts.map +1 -0
  227. package/dist/provider-auth-checks.js +349 -0
  228. package/dist/provider-auth-checks.js.map +1 -0
  229. package/dist/provider-auth-observations.d.ts +22 -0
  230. package/dist/provider-auth-observations.d.ts.map +1 -0
  231. package/dist/provider-auth-observations.js +139 -0
  232. package/dist/provider-auth-observations.js.map +1 -0
  233. package/dist/provider-auth-operator.d.ts +19 -0
  234. package/dist/provider-auth-operator.d.ts.map +1 -0
  235. package/dist/provider-auth-operator.js +560 -0
  236. package/dist/provider-auth-operator.js.map +1 -0
  237. package/dist/provider-auth-status.d.ts +20 -0
  238. package/dist/provider-auth-status.d.ts.map +1 -0
  239. package/dist/provider-auth-status.js +235 -0
  240. package/dist/provider-auth-status.js.map +1 -0
  241. package/dist/provider-model-catalog.d.ts +95 -0
  242. package/dist/provider-model-catalog.d.ts.map +1 -0
  243. package/dist/provider-model-catalog.js +550 -0
  244. package/dist/provider-model-catalog.js.map +1 -0
  245. package/dist/provider-setup.d.ts +26 -13
  246. package/dist/provider-setup.d.ts.map +1 -1
  247. package/dist/provider-setup.js +106 -130
  248. package/dist/provider-setup.js.map +1 -1
  249. package/dist/readiness-probe-worker.d.ts +1 -2
  250. package/dist/readiness-probe-worker.d.ts.map +1 -1
  251. package/dist/readiness-probe-worker.js +3 -8
  252. package/dist/readiness-probe-worker.js.map +1 -1
  253. package/dist/readiness-probe.d.ts +1 -1
  254. package/dist/readiness-probe.d.ts.map +1 -1
  255. package/dist/readiness-probe.js +3 -33
  256. package/dist/readiness-probe.js.map +1 -1
  257. package/dist/reply-artifacts.d.ts +3 -1
  258. package/dist/reply-artifacts.d.ts.map +1 -1
  259. package/dist/reply-artifacts.js +45 -10
  260. package/dist/reply-artifacts.js.map +1 -1
  261. package/dist/request-model-override.d.ts +14 -37
  262. package/dist/request-model-override.d.ts.map +1 -1
  263. package/dist/request-model-override.js +105 -143
  264. package/dist/request-model-override.js.map +1 -1
  265. package/dist/request-scoped-mcp.d.ts +3 -0
  266. package/dist/request-scoped-mcp.d.ts.map +1 -1
  267. package/dist/request-scoped-mcp.js +1 -1
  268. package/dist/request-scoped-mcp.js.map +1 -1
  269. package/dist/run-history.d.ts.map +1 -1
  270. package/dist/run-history.js +56 -13
  271. package/dist/run-history.js.map +1 -1
  272. package/dist/runtime-option-extensions.d.ts +2 -4
  273. package/dist/runtime-option-extensions.d.ts.map +1 -1
  274. package/dist/runtime-option-extensions.js +4 -8
  275. package/dist/runtime-option-extensions.js.map +1 -1
  276. package/dist/runtime-routes.d.ts +5 -5
  277. package/dist/runtime-routes.d.ts.map +1 -1
  278. package/dist/runtime-routes.js +29 -18
  279. package/dist/runtime-routes.js.map +1 -1
  280. package/dist/sandbox-settings-cleanup.d.ts +15 -0
  281. package/dist/sandbox-settings-cleanup.d.ts.map +1 -0
  282. package/dist/sandbox-settings-cleanup.js +88 -0
  283. package/dist/sandbox-settings-cleanup.js.map +1 -0
  284. package/dist/seen-conversations.d.ts +4 -3
  285. package/dist/seen-conversations.d.ts.map +1 -1
  286. package/dist/seen-conversations.js +6 -5
  287. package/dist/seen-conversations.js.map +1 -1
  288. package/dist/session-history.d.ts +14 -0
  289. package/dist/session-history.d.ts.map +1 -1
  290. package/dist/session-history.js +120 -5
  291. package/dist/session-history.js.map +1 -1
  292. package/dist/systemd-command.d.ts +8 -0
  293. package/dist/systemd-command.d.ts.map +1 -0
  294. package/dist/systemd-command.js +206 -0
  295. package/dist/systemd-command.js.map +1 -0
  296. package/dist/systemd.d.ts +51 -0
  297. package/dist/systemd.d.ts.map +1 -0
  298. package/dist/systemd.js +299 -0
  299. package/dist/systemd.js.map +1 -0
  300. package/dist/tool-output-artifacts.d.ts +4 -0
  301. package/dist/tool-output-artifacts.d.ts.map +1 -0
  302. package/dist/tool-output-artifacts.js +17 -0
  303. package/dist/tool-output-artifacts.js.map +1 -0
  304. package/dist/trigger-overrides.d.ts +6 -0
  305. package/dist/trigger-overrides.d.ts.map +1 -1
  306. package/dist/trigger-overrides.js +36 -4
  307. package/dist/trigger-overrides.js.map +1 -1
  308. package/dist/tui-command.d.ts +0 -45
  309. package/dist/tui-command.d.ts.map +1 -1
  310. package/dist/tui-command.js +27 -264
  311. package/dist/tui-command.js.map +1 -1
  312. package/dist/untrusted-text.d.ts +36 -0
  313. package/dist/untrusted-text.d.ts.map +1 -0
  314. package/dist/untrusted-text.js +150 -0
  315. package/dist/untrusted-text.js.map +1 -0
  316. package/dist/web-command.d.ts +3 -0
  317. package/dist/web-command.d.ts.map +1 -1
  318. package/dist/web-command.js +112 -7
  319. package/dist/web-command.js.map +1 -1
  320. package/dist/web-request-coordinator.d.ts +19 -0
  321. package/dist/web-request-coordinator.d.ts.map +1 -0
  322. package/dist/web-request-coordinator.js +242 -0
  323. package/dist/web-request-coordinator.js.map +1 -0
  324. package/dist/wizard/answers.d.ts +3 -9
  325. package/dist/wizard/answers.d.ts.map +1 -1
  326. package/dist/wizard/answers.js +11 -36
  327. package/dist/wizard/answers.js.map +1 -1
  328. package/dist/wizard/from-flags.d.ts +1 -2
  329. package/dist/wizard/from-flags.d.ts.map +1 -1
  330. package/dist/wizard/from-flags.js +8 -3
  331. package/dist/wizard/from-flags.js.map +1 -1
  332. package/dist/wizard/model-discovery.d.ts +7 -8
  333. package/dist/wizard/model-discovery.d.ts.map +1 -1
  334. package/dist/wizard/model-discovery.js +58 -431
  335. package/dist/wizard/model-discovery.js.map +1 -1
  336. package/dist/wizard/presets.js +2 -2
  337. package/dist/wizard/presets.js.map +1 -1
  338. package/dist/wizard/prompts.d.ts +13 -12
  339. package/dist/wizard/prompts.d.ts.map +1 -1
  340. package/dist/wizard/prompts.js +25 -45
  341. package/dist/wizard/prompts.js.map +1 -1
  342. package/dist/wizard/run.d.ts.map +1 -1
  343. package/dist/wizard/run.js +30 -153
  344. package/dist/wizard/run.js.map +1 -1
  345. package/package.json +18 -18
  346. package/schema/mono-agent.config.schema.json +301 -53
  347. package/skills/mono-agent-composer/SKILL.md +5 -5
  348. package/skills/mono-agent-composer/references/config-blueprint.md +61 -47
  349. package/skills/mono-agent-composer/references/discovery-questions.md +18 -18
  350. package/skills/mono-agent-composer/references/feature-coverage.md +39 -21
  351. package/skills/mono-agent-composer/references/package-map.md +26 -17
  352. package/skills/mono-agent-composer/references/playbooks.md +39 -28
  353. package/skills/mono-agent-composer/references/validation.md +4 -3
  354. package/dist/codex-model-catalog.d.ts +0 -15
  355. package/dist/codex-model-catalog.d.ts.map +0 -1
  356. package/dist/codex-model-catalog.js +0 -220
  357. package/dist/codex-model-catalog.js.map +0 -1
  358. package/dist/configuration-proposal-main.d.ts +0 -3
  359. package/dist/configuration-proposal-main.d.ts.map +0 -1
  360. package/dist/configuration-proposal-main.js +0 -13
  361. package/dist/configuration-proposal-main.js.map +0 -1
  362. package/dist/configuration-proposal-tool.d.ts +0 -33
  363. package/dist/configuration-proposal-tool.d.ts.map +0 -1
  364. package/dist/configuration-proposal-tool.js +0 -188
  365. package/dist/configuration-proposal-tool.js.map +0 -1
  366. package/dist/local-configuration.d.ts +0 -99
  367. package/dist/local-configuration.d.ts.map +0 -1
  368. package/dist/local-configuration.js +0 -1330
  369. package/dist/local-configuration.js.map +0 -1
package/dist/doctor.js CHANGED
@@ -1,7 +1,7 @@
1
+ import { inspectWebControl } from "./web-request-coordinator.js";
1
2
  import { execFile as execFileCallback } from "node:child_process";
2
3
  import { constants } from "node:fs";
3
4
  import { access, lstat, mkdir, readdir, readFile, realpath, stat } from "node:fs/promises";
4
- import { homedir } from "node:os";
5
5
  import { isIP } from "node:net";
6
6
  import { basename, join, relative, resolve, sep } from "node:path";
7
7
  import { promisify } from "node:util";
@@ -12,10 +12,12 @@ import { promisify } from "node:util";
12
12
  import { getBuiltinModels as getPiBuiltinModels, getBuiltinProviders as getPiBuiltinProviders, } from "@earendil-works/pi-ai/providers/all";
13
13
  import { validateCronExpression } from "@mono-agent/cron-adapter";
14
14
  import { classifyContinuationMcpServerTransport, isStdioMcpServerSpec, loadToolPolicyFromJsonFile, } from "@mono-agent/agent-harness";
15
- import { defaultExecutionModeForModel, describeMonoRuntimeSupport, inspectCodexSubscriptionSearch, isValidMcpServerName, modelReferenceKey, networkPolicyAllowsUrl, parseMonoRuntimeModelReference, resolveModelEffortLevels, } from "@mono-agent/runtime-adapter";
16
- import { buildMonoAgentConfigView, findJsonSecretConfigWarnings, findRemovedConfigWarnings, readMonoAgentConfigJson, redactMonoAgentConfig, resolveSupermemoryContainer, } from "@mono-agent/config";
17
- import { describeSandboxEffectiveState, resolveSandboxEffectiveState, sandboxEffectiveStateWarning, } from "@mono-agent/runtime-adapter";
15
+ import { describeMonoRuntimeSupport, inspectCodexSubscriptionSearch, isValidMcpServerName, modelReferenceKey, networkPolicyAllowsUrl, parseMonoRuntimeModelReference, } from "@mono-agent/runtime-adapter";
16
+ import { buildMonoAgentConfigView, EFFORT_LEVELS, findJsonSecretConfigWarnings, findRemovedConfigWarnings, readMonoAgentConfigJson, redactMonoAgentConfig, resolveSupermemoryContainer, } from "@mono-agent/config";
17
+ import { describeSandboxEffectiveState, MODEL_REFERENCE_ECHO_MAX_BYTES, MODEL_REFERENCE_REASON_MAX_BYTES, resolveSandboxEffectiveState, RuntimeAdapterError, sandboxEffectiveStateWarning, sanitizeModelReferenceText, } from "@mono-agent/runtime-adapter";
18
18
  import { isAppCoreConfigError, loadAppCoreConfig, } from "./app-config.js";
19
+ import { isRememberToolAllowed, isRememberToolPolicyName } from "./memory-remember.js";
20
+ import { isMemoryJournalToolAllowed, isMemoryJournalToolPolicyName } from "./memory-journal.js";
19
21
  import { adapterSendToolNames, isAdapterSendToolAllowed, resolveAdapterSendToolsSettings } from "./adapter-send-tools.js";
20
22
  import { canonicalToolName, isAllowAllTools, isKnownToolName, isMcpToolName, suggestToolName } from "./modules/known-tools.js";
21
23
  import { collectChannelConfigViews } from "./channel-config-view.js";
@@ -37,19 +39,26 @@ import { formatInteractionBridgeUrl, loadInteractionSettings } from "./interacti
37
39
  import { FIRST_RUN_MEMORY_INITIALIZING_MARKER } from "./first-run-managed-memory.js";
38
40
  import { DEFAULT_MEMORY_EMBEDDING_ENDPOINTS, probeMemoryEmbeddingSelection, } from "./memory-embedding-service.js";
39
41
  import { piAuthRecoveryCommand } from "./provider-setup.js";
42
+ import { collectUsedProviderReferences } from "./provider-auth-status.js";
40
43
  import { inspectPiAuthStore } from "./pi-auth-store-inspection.js";
41
- import { checkManagedProjectSkills, managedProjectSkillsExist } from "./project-skills.js";
42
- import { configuredRuntimeFallbackModels, configuredRuntimeModels } from "./runtime-routes.js";
44
+ import { activeProjectSkillSelections, checkManagedProjectSkills, isRetiredProjectSkillName, managedProjectSkillsExist, } from "./project-skills.js";
43
45
  import { runtimeProvenanceDetail } from "./runtime-provenance.js";
46
+ import { resolveAdvertisedModelEffort } from "./model-effort-capabilities.js";
44
47
  import { loadSupermemoryPlugin } from "./supermemory-plugin.js";
45
48
  import { DEFAULT_LAUNCHD_LOG_POLICY, inspectLaunchdLogs, LAUNCHD_LOG_MONITOR_INTERVAL_SECONDS, launchdLogPathsForConfig, } from "./launchd-logs.js";
46
49
  import { readLaunchdLogMonitorStatus } from "./launchd-log-monitor-status.js";
47
50
  import { exporterSection, runsSection } from "./doctor-observability.js";
48
51
  import { sessionToolHistorySection } from "./doctor-session-history.js";
49
- import { runtimeRouteContainsUnsupportedHistoryTool } from "./app-controller-utils.js";
50
52
  const execFile = promisify(execFileCallback);
51
53
  const CONTINUATION_V2_ROLLBACK_GUARD = "UPGRADED-TO-RECORDS-V3";
52
54
  const CONTINUATION_V2_ROLLBACK_GUARD_CONTENT = "This state directory uses continuation records v3. Older runtimes must not open records-v2.\n";
55
+ const DOCTOR_STATUS_TIMEOUT_MS = 5_000;
56
+ const DOCTOR_STATUS_MAX_BUFFER_BYTES = 64 * 1024;
57
+ const OLLAMA_WEB_SEARCH_PROBE_MAX_BYTES = 2 * 1024 * 1024;
58
+ const defaultDoctorStatusExecFile = async (file, args, options) => {
59
+ const { stdout } = await execFile(file, [...args], options);
60
+ return { stdout };
61
+ };
53
62
  /**
54
63
  * Loads every config section the app would use at start and reports it
55
64
  * per-section, so an engineer can see exactly what would run, wait, or fail —
@@ -73,7 +82,7 @@ export async function validateMonoAgentFolder(options) {
73
82
  }
74
83
  sections.push(await runtimeProvenanceSection(options.verifiedRuntimeProvenanceDetail));
75
84
  if (coreConfig !== undefined) {
76
- const staticTriggerCredentialRefs = await collectStaticTriggerCredentialRefs(drivers, options);
85
+ const staticTriggerCredentialRefs = await collectStaticTriggerCredentialRefs(coreConfig, drivers, options);
77
86
  const jsonResult = await readMonoAgentConfigJson(options.configPath);
78
87
  // Channel secrets (bot tokens, API keys) live outside the core view, so the
79
88
  // placement check spans both: core sections + every channel's config view.
@@ -92,13 +101,13 @@ export async function validateMonoAgentFolder(options) {
92
101
  sections.push({ id: "secret-placement", label: "Config warnings", status: "waiting", details: configWarnings });
93
102
  }
94
103
  sections.push(runtimeSection(coreConfig));
95
- sections.push(await credentialsSection(coreConfig, options.env, options.cwd, liveness, options.verifiedCredentialModelRefs, options.sdkAuthStatusExecFile, staticTriggerCredentialRefs));
104
+ sections.push(await credentialsSection(coreConfig, options.env, options.verifiedCredentialModelRefs, staticTriggerCredentialRefs));
96
105
  sections.push(await contextSection(coreConfig, options.cwd));
97
106
  sections.push(await memorySection(coreConfig, options.cwd, options.env, liveness, allowFilesystemWrites, options.preferAppPluginInstall === true));
98
107
  sections.push(await toolsSection(coreConfig, options));
99
108
  sections.push(await sessionToolHistorySection({
100
109
  historyRoot: join(coreConfig.artifacts.dir, "..", "history"),
101
- requestScopedToolSupported: !runtimeRouteContainsUnsupportedHistoryTool(coreConfig),
110
+ requestScopedToolSupported: true,
102
111
  }));
103
112
  sections.push(await webToolsSection(coreConfig, options, liveness));
104
113
  sections.push(await continuationSection(coreConfig, options));
@@ -233,73 +242,13 @@ function staticTriggerConfigEntries(driverId, loaded) {
233
242
  * Collect static trigger model overrides for credential readiness. Dynamic
234
243
  * webhook request-body overrides are intentionally unavailable at validate time.
235
244
  */
236
- async function collectStaticTriggerCredentialRefs(drivers, input) {
237
- const refs = [];
238
- const seen = new Set();
239
- for (const driver of drivers) {
240
- if (driver.id !== "webhook" && driver.id !== "cron")
241
- continue;
242
- let loaded;
243
- try {
244
- loaded = await driver.loadConfig(input);
245
- }
246
- catch {
247
- // The channel section owns malformed/unreadable config diagnostics.
248
- continue;
249
- }
250
- for (const { entryPath, entry } of staticTriggerConfigEntries(driver.id, loaded)) {
251
- if (typeof entry.model !== "string")
252
- continue;
253
- try {
254
- const ref = parseMonoRuntimeModelReference(entry.model);
255
- const key = `${entryPath}\u0000${modelReferenceKey(ref)}`;
256
- if (seen.has(key))
257
- continue;
258
- seen.add(key);
259
- refs.push({ label: entryPath, ref });
260
- }
261
- catch {
262
- // The channel section owns invalid model-reference syntax.
263
- }
264
- }
265
- }
266
- return refs;
245
+ async function collectStaticTriggerCredentialRefs(config, drivers, input) {
246
+ return (await collectUsedProviderReferences(config, drivers, input))
247
+ .filter(({ usage }) => usage.kind === "cron" || usage.kind === "webhook")
248
+ .map(({ usage, ref }) => ({ label: usage.label, ref }));
267
249
  }
268
250
  async function applyRequestModelOverrideCompatibilityChecks(sections, config, drivers, input) {
269
- const baseIsDirectCodex = config.runtime.model.sdk === "codex";
270
- const routeSafety = config.runtime.routeSafety ?? "uniform";
271
- const directBoundaryConflicts = [];
272
- const sandboxBypasses = [];
273
- const toolPolicyBypasses = [];
274
- const effortBypasses = [];
275
- const turnCapBypasses = [];
276
- const mcpBypasses = [];
277
- const skillBypasses = [];
278
251
  const piModelResolutionFailures = [];
279
- const monoSandboxActive = config.sandbox !== undefined && config.sandbox.mode !== "off";
280
- const restrictiveToolPolicy = !hasAllowAllOnlyToolPolicy(config.tools);
281
- let configuredMcpServerNames = [];
282
- if (config.tools.mcpConfigPath !== undefined) {
283
- try {
284
- configuredMcpServerNames = Object.keys((await loadToolPolicyFromJsonFile(config.tools.mcpConfigPath)).mcpServers ?? {});
285
- }
286
- catch {
287
- // The tools section owns missing/malformed MCP-file diagnostics.
288
- }
289
- }
290
- let adapterToolNames = [];
291
- try {
292
- const settings = await resolveAdapterSendToolsSettings(input, {
293
- allowedTools: config.tools.allowedTools,
294
- disallowedTools: config.tools.disallowedTools,
295
- });
296
- if (settings !== undefined)
297
- adapterToolNames = adapterSendToolNames(settings);
298
- }
299
- catch {
300
- // Channel/tools sections own adapter config diagnostics.
301
- }
302
- const effectiveMcpSources = effectiveMcpRuntimeSources(config, configuredMcpServerNames, adapterToolNames);
303
252
  for (const driver of drivers) {
304
253
  if (driver.id !== "webhook" && driver.id !== "cron")
305
254
  continue;
@@ -313,50 +262,19 @@ async function applyRequestModelOverrideCompatibilityChecks(sections, config, dr
313
262
  }
314
263
  for (const { entryPath, entry } of staticTriggerConfigEntries(driver.id, loaded)) {
315
264
  const hasModelOverride = typeof entry.model === "string";
316
- const hasEffortOverride = typeof entry.effort === "string";
317
- if (!hasModelOverride && !hasEffortOverride)
265
+ if (!hasModelOverride)
318
266
  continue;
319
267
  try {
320
- const model = hasModelOverride
321
- ? parseMonoRuntimeModelReference(entry.model)
322
- : config.runtime.model;
323
- const location = hasModelOverride
324
- ? `${entryPath}.model=${entry.model}`
325
- : `${entryPath}.effort=${entry.effort}`;
326
- if (hasModelOverride) {
327
- const resolutionIssue = piModelResolutionIssue(config, model);
328
- if (resolutionIssue !== undefined) {
329
- piModelResolutionFailures.push(`${location}: ${resolutionIssue}.`);
330
- }
331
- }
332
- if (routeSafety === "uniform" && hasModelOverride && (model.sdk === "codex") !== baseIsDirectCodex) {
333
- directBoundaryConflicts.push(location);
334
- }
335
- if (routeSafety === "uniform" && hasModelOverride && monoSandboxActive && (model.sdk === "claude" || model.sdk === "opencode" || model.sdk === "codex")) {
336
- sandboxBypasses.push(location);
337
- }
338
- if (hasModelOverride && restrictiveToolPolicy && model.sdk === "opencode") {
339
- toolPolicyBypasses.push(location);
340
- }
341
- const effectiveEffort = hasEffortOverride ? entry.effort : config.runtime.effort;
342
- const legacyFallbacks = (config.runtime.fallbacks?.length ?? 0) > 0
343
- ? []
344
- : config.runtime.fallbackModels ?? [];
345
- const directOpenCodeModels = [model, ...legacyFallbacks]
346
- .filter((candidate) => candidate.sdk === "opencode");
347
- if (directOpenCodeModels.length > 0 && effectiveEffort !== undefined) {
348
- effortBypasses.push(`${location} (effective effort=${effectiveEffort}) (direct OpenCode route=${directOpenCodeModels.map(referenceOf).join(", ")})`);
349
- }
350
- if (directOpenCodeModels.length > 0 && Number(config.runtime.maxTurns) > 0) {
351
- turnCapBypasses.push(`${location} (runtime.maxTurns=${config.runtime.maxTurns}; direct OpenCode route=${directOpenCodeModels.map(referenceOf).join(", ")})`);
352
- }
353
- if (directOpenCodeModels.length > 0 && effectiveMcpSources.length > 0) {
354
- mcpBypasses.push(`${location} (direct OpenCode route=${directOpenCodeModels.map(referenceOf).join(", ")}; MCP sources=${effectiveMcpSources.join("; ")})`);
355
- }
356
- if (directOpenCodeModels.length > 0
357
- && config.context.skillDisclosure === "index"
358
- && config.context.skillsRoot !== undefined) {
359
- skillBypasses.push(location);
268
+ const model = parseMonoRuntimeModelReference(entry.model);
269
+ const resolutionIssue = piModelResolutionIssue(config, model);
270
+ if (resolutionIssue !== undefined) {
271
+ // The parser accepted this value, but acceptance is no longer a length
272
+ // guarantee: the byte ceiling was removed because no constant could
273
+ // hold for every provider, so a legitimately long id parses. This is a
274
+ // diagnostic, not the protocol string, so it is bounded here rather
275
+ // than at the grammar.
276
+ const echoed = sanitizeModelReferenceText(entry.model, MODEL_REFERENCE_ECHO_MAX_BYTES);
277
+ piModelResolutionFailures.push(`${entryPath}.model=${echoed}: ${resolutionIssue}.`);
360
278
  }
361
279
  }
362
280
  catch {
@@ -364,14 +282,7 @@ async function applyRequestModelOverrideCompatibilityChecks(sections, config, dr
364
282
  }
365
283
  }
366
284
  }
367
- if (directBoundaryConflicts.length === 0
368
- && sandboxBypasses.length === 0
369
- && toolPolicyBypasses.length === 0
370
- && effortBypasses.length === 0
371
- && turnCapBypasses.length === 0
372
- && mcpBypasses.length === 0
373
- && skillBypasses.length === 0
374
- && piModelResolutionFailures.length === 0)
285
+ if (piModelResolutionFailures.length === 0)
375
286
  return;
376
287
  const index = sections.findIndex((section) => section.id === "runtime");
377
288
  if (index < 0)
@@ -382,63 +293,14 @@ async function applyRequestModelOverrideCompatibilityChecks(sections, config, dr
382
293
  status: "error",
383
294
  details: [
384
295
  ...runtime.details,
385
- ...(directBoundaryConflicts.length === 0
386
- ? []
387
- : [
388
- "Uniform route safety cannot cross the direct-Codex runtime boundary because tool and sandbox contracts would change mid-agent. Choose per-route-native to opt into explicit route-local contracts.",
389
- ...directBoundaryConflicts,
390
- ]),
391
- ...(sandboxBypasses.length === 0
392
- ? []
393
- : [
394
- "Claude or direct OpenCode model overrides cannot run under uniform route safety while mono-agent SRT is active; direct Codex is also provider-owned. Choose per-route-native only after reviewing the explicit route-local contracts.",
395
- ...sandboxBypasses,
396
- ]),
397
- ...(toolPolicyBypasses.length === 0
398
- ? []
399
- : [
400
- "Per-trigger direct OpenCode model overrides require an effective allow-all policy because OpenCode's provider-owned tool loop does not consume mono-agent allowedTools/disallowedTools.",
401
- ...toolPolicyBypasses,
402
- ]),
403
- ...(effortBypasses.length === 0
404
- ? []
405
- : [
406
- "Per-trigger direct OpenCode routes cannot receive runtime effort because the OpenCode SDK does not expose effort control.",
407
- ...effortBypasses,
408
- ]),
409
- ...(turnCapBypasses.length === 0
410
- ? []
411
- : [
412
- "Per-trigger direct OpenCode routes cannot enforce runtime.maxTurns; omit it, set it to 0, or use a runtime with a hard turn cap.",
413
- ...turnCapBypasses,
414
- ]),
415
- ...(mcpBypasses.length === 0
416
- ? []
417
- : [
418
- "Per-trigger direct OpenCode routes cannot receive configured or auto-provisioned MCP runtime options.",
419
- ...mcpBypasses,
420
- ]),
421
- ...(skillBypasses.length === 0
422
- ? []
423
- : [
424
- "Per-trigger direct OpenCode routes cannot use index skill disclosure because the bridge disables external/runtime skills; use full disclosure or a Pi runtime.",
425
- ...skillBypasses,
426
- ]),
427
- ...(piModelResolutionFailures.length === 0
428
- ? []
429
- : [
430
- "Per-trigger Pi model overrides must resolve through providers.local or Pi's exact built-in catalog before execution.",
431
- ...piModelResolutionFailures,
432
- ]),
296
+ "Per-trigger Pi model overrides must resolve through providers.local or Pi's exact built-in catalog before execution.",
297
+ ...piModelResolutionFailures,
433
298
  ],
434
299
  };
435
300
  }
436
301
  function isUnknownRecord(value) {
437
302
  return typeof value === "object" && value !== null && !Array.isArray(value);
438
303
  }
439
- function hasAllowAllOnlyToolPolicy(tools) {
440
- return isAllowAllTools(tools.allowedTools) && tools.disallowedTools.length === 0;
441
- }
442
304
  /** Adapter send tools each channel owns; an allowed entry needs BOTH the tool AND the enabled channel. */
443
305
  const CHANNEL_OWNED_SEND_TOOLS = {
444
306
  slack: ["SlackSendMessage"],
@@ -511,45 +373,28 @@ function runtimeSection(config) {
511
373
  const details = [];
512
374
  let status = "ok";
513
375
  const routes = configuredRuntimeRouteChecks(config);
514
- const routeSafety = config.runtime.routeSafety ?? "uniform";
515
- details.push(`Route safety: ${routeSafety}.`);
516
- if (routes.length > 1) {
517
- details.push(routeSafety === "per-route-native"
518
- ? "Mixed runtime families are allowed; every attempt uses its explicit route-native safety contract."
519
- : "Fallback routes use the uniform compatibility contract; validation fails closed when any route cannot represent a required capability.");
520
- }
521
- const directOpenCodeModels = routes.filter((route) => route.model.sdk === "opencode");
522
- if (Number(config.runtime.maxTurns) > 0 && directOpenCodeModels.length > 0) {
523
- status = "error";
524
- details.push(`Direct OpenCode model${directOpenCodeModels.length === 1 ? "" : "s"} ${directOpenCodeModels.map((route) => referenceOf(route.model)).join(", ")} cannot enforce runtime.maxTurns=${config.runtime.maxTurns}; omit it, set it to 0, or use a runtime with a hard turn cap.`);
525
- }
526
- if (directOpenCodeModels.length > 0
527
- && config.context.skillDisclosure === "index"
528
- && config.context.skillsRoot !== undefined) {
529
- status = "error";
530
- details.push(`Direct OpenCode model${directOpenCodeModels.length === 1 ? "" : "s"} ${directOpenCodeModels.map((route) => referenceOf(route.model)).join(", ")} cannot use context.skillDisclosure=index because runtime skills are disabled; use full disclosure or a Pi runtime.`);
531
- }
532
376
  for (const route of routes) {
533
377
  try {
534
- const support = describeMonoRuntimeSupport(route.model, route.executionMode);
378
+ const support = describeMonoRuntimeSupport(route.model);
535
379
  const resolutionIssue = piModelResolutionIssue(config, route.model);
536
- const effortIssue = runtimeRouteEffortIssue(config, route);
537
- if (support.compatible && resolutionIssue === undefined) {
538
- details.push(`${route.label} ${referenceOf(route.model)} runs on ${support.backend?.label ?? "unknown backend"} ` +
380
+ const effortWarning = runtimeRouteEffortWarning(config, route);
381
+ if (resolutionIssue === undefined) {
382
+ details.push(`${route.label} ${displayReferenceOf(route.model)} runs on ${support.backend.label} ` +
539
383
  `(effort: ${route.effort ?? "provider default"}).`);
540
384
  }
541
385
  else {
542
386
  status = "error";
543
- details.push(`${route.label} ${referenceOf(route.model)}: ${resolutionIssue ?? support.incompatibilityReason ?? "unsupported"}.`);
387
+ details.push(`${route.label} ${displayReferenceOf(route.model)}: ${resolutionIssue}.`);
544
388
  }
545
- if (effortIssue !== undefined) {
546
- status = "error";
547
- details.push(`${route.label} ${referenceOf(route.model)}: ${effortIssue}`);
389
+ if (effortWarning !== undefined) {
390
+ if (status !== "error")
391
+ status = "waiting";
392
+ details.push(effortWarning);
548
393
  }
549
394
  }
550
395
  catch (error) {
551
396
  status = "error";
552
- details.push(`${route.label} ${referenceOf(route.model)}: ${error instanceof Error ? error.message : String(error)}.`);
397
+ details.push(`${route.label} ${displayReferenceOf(route.model)}: ${displayReason(error)}.`);
553
398
  }
554
399
  }
555
400
  return { id: "runtime", label: "Runtime", status, details };
@@ -557,48 +402,52 @@ function runtimeSection(config) {
557
402
  function configuredRuntimeRouteChecks(config) {
558
403
  const primary = {
559
404
  label: "Primary model",
405
+ configPath: "runtime.effort",
560
406
  model: config.runtime.model,
561
407
  ...(config.runtime.effort === undefined ? {} : { effort: config.runtime.effort }),
562
- executionMode: config.runtime.executionMode,
563
408
  };
564
- if ((config.runtime.fallbacks?.length ?? 0) > 0) {
565
- return [
566
- primary,
567
- ...(config.runtime.fallbacks ?? []).map((fallback) => ({
568
- label: "Fallback model",
569
- model: fallback.model,
570
- ...(fallback.effort === undefined ? {} : { effort: fallback.effort }),
571
- })),
572
- ];
573
- }
574
409
  return [
575
410
  primary,
576
- ...(config.runtime.fallbackModels ?? []).map((model) => ({
411
+ ...(config.runtime.fallbacks ?? []).map((fallback, index) => ({
577
412
  label: "Fallback model",
578
- model,
579
- ...(config.runtime.effort === undefined ? {} : { effort: config.runtime.effort }),
413
+ configPath: `runtime.fallbacks[${index}].effort`,
414
+ model: fallback.model,
415
+ ...(fallback.effort === undefined ? {} : { effort: fallback.effort }),
580
416
  })),
581
417
  ];
582
418
  }
583
- function runtimeRouteEffortIssue(config, route) {
419
+ function runtimeRouteEffortWarning(config, route) {
584
420
  if (route.effort === undefined)
585
421
  return undefined;
586
- if (route.model.sdk === "opencode") {
587
- return `Direct OpenCode model ${referenceOf(route.model)} cannot receive runtime.effort=${route.effort}; the OpenCode SDK exposes no reasoning-effort input. Omit the route effort.`;
588
- }
589
- const metadata = resolveModelEffortLevels(route.model, config.providers?.local);
590
- if (metadata.effortLevels !== undefined && !metadata.effortLevels.includes(route.effort)) {
591
- return `effort=${route.effort} is unsupported by known model metadata; choose ${metadata.effortLevels.join(", ")}, or omit it for provider default.`;
592
- }
593
- if (metadata.reasoning === false && route.effort !== "none") {
594
- return `effort=${route.effort} is unsupported because known model metadata marks this route as non-reasoning; use none or provider default.`;
595
- }
596
- if (route.model.sdk === "claude"
597
- && (route.executionMode ?? defaultExecutionModeForModel(route.model)) === "sdk"
598
- && !["low", "medium", "high", "xhigh", "max"].includes(route.effort)) {
599
- return `effort=${route.effort} is unsupported by the Claude Agent SDK; choose low, medium, high, xhigh, max, or provider default.`;
600
- }
601
- return undefined;
422
+ // Always go through the shared resolver. Deriving the ladder from
423
+ // `thinkingLevelMap`'s KEYS treated an override table as the complete
424
+ // supported set: claude-fable-5 maps only {off, xhigh, max}, which yielded
425
+ // [none, xhigh, max] and made a perfectly valid `effort: medium` warn and
426
+ // recommend xhigh. The resolver reports [minimal, low, medium, high, xhigh,
427
+ // max] for that model, and it is the same one the catalog and pickers use --
428
+ // doctor disagreeing with what the selector offers is its own bug.
429
+ const resolved = resolveAdvertisedModelEffort(route.model, {
430
+ ...(config.providers?.local === undefined ? {} : { localProviders: config.providers.local }),
431
+ });
432
+ // A NON-REASONING route has no advertised ladder at all, so the level-list
433
+ // check below returns clean and `effort: high` was reported ready. Keep this
434
+ // ahead of that check: a local model can declare `reasoning_mode: "none"`
435
+ // WITH `reasoning_levels`, and "this route does not reason" is the accurate
436
+ // diagnosis there, not "pick another level".
437
+ if (resolved.reasoning === false && route.effort !== "none") {
438
+ return `[WARN] ${route.configPath}=${route.effort} is unsupported because known model metadata marks ${displayReferenceOf(route.model)} as non-reasoning; use none, or omit it for the provider default. The runtime remains permissive and will forward the configured value.`;
439
+ }
440
+ const advertised = resolved.effortLevels
441
+ ?.filter((level) => EFFORT_LEVELS.includes(level));
442
+ if (advertised === undefined || advertised.length === 0 || advertised.includes(route.effort))
443
+ return undefined;
444
+ const configuredIndex = EFFORT_LEVELS.indexOf(route.effort);
445
+ const nearest = advertised.reduce((best, candidate) => {
446
+ const candidateIndex = EFFORT_LEVELS.indexOf(candidate);
447
+ const bestIndex = EFFORT_LEVELS.indexOf(best);
448
+ return Math.abs(candidateIndex - configuredIndex) < Math.abs(bestIndex - configuredIndex) ? candidate : best;
449
+ });
450
+ return `[WARN] ${route.configPath}=${route.effort} is outside ${displayReferenceOf(route.model)}'s advertised effort levels (${advertised.join(", ")}); nearest advertised level: ${nearest}. The runtime remains permissive and will forward the configured value.`;
602
451
  }
603
452
  /**
604
453
  * Mirrors the Pi runtime's actual model-resolution boundary. Mono-agent only
@@ -607,17 +456,14 @@ function runtimeRouteEffortIssue(config, route) {
607
456
  * provider, `resolvePiRuntimeModel` performs an exact built-in catalog lookup.
608
457
  */
609
458
  function piModelResolutionIssue(config, model) {
610
- if (model.sdk !== "pi" || model.provider === undefined) {
611
- return undefined;
612
- }
613
459
  const localProvider = config.providers?.local?.find((provider) => provider.id === model.provider);
614
460
  if (localProvider !== undefined) {
615
461
  if (localProvider.enabled === false) {
616
- return `provider \`${model.provider}\` is disabled in providers.local`;
462
+ return `provider \`${displayText(model.provider)}\` is disabled in providers.local`;
617
463
  }
618
464
  const localModel = localProvider.models?.find((candidate) => candidate.name === model.model || candidate.alias === model.model);
619
465
  if (localModel?.enabled === false) {
620
- return `model \`${model.model}\` is disabled in providers.local for provider \`${model.provider}\``;
466
+ return `model \`${displayText(model.model)}\` is disabled in providers.local for provider \`${displayText(model.provider)}\``;
621
467
  }
622
468
  return undefined;
623
469
  }
@@ -625,7 +471,14 @@ function piModelResolutionIssue(config, model) {
625
471
  && getPiBuiltinModels(model.provider).some((candidate) => candidate.id === model.model)) {
626
472
  return undefined;
627
473
  }
628
- return (`pi model not found: ${model.provider}:${model.model}; no matching providers.local entry exists and Pi's built-in catalog has no exact model. ` +
474
+ // This is doctor's own validate-time diagnostic, not the runtime failure
475
+ // string. The one that must stay byte-identical for `NON_RETRYABLE_PROVIDER_RE`
476
+ // is built in agent-runtime's `ai/providers/pi-models.js`; nothing matches
477
+ // against this text. So the reference is bounded here, which matters now that
478
+ // the parser imposes no length ceiling and a legitimately long id reaches this
479
+ // line whole.
480
+ const reference = displayText(`${model.provider}:${model.model}`);
481
+ return (`pi model not found: ${reference}; no matching providers.local entry exists and Pi's built-in catalog has no exact model. ` +
629
482
  "The sibling Pi CLI models.json is not a mono-agent runtime source; add providers.local for a self-hosted provider or choose a built-in Pi model");
630
483
  }
631
484
  function isPiBuiltinProvider(provider) {
@@ -641,229 +494,21 @@ async function readPiAuthProviders(path) {
641
494
  return inspection;
642
495
  return { status: "ok", providers: inspection.auth };
643
496
  }
644
- /**
645
- * What each SDK-authenticated backend truthfully reads for credentials. Only the
646
- * env vars are statically checkable; the login paths are recorded so the warning
647
- * can stay honest (a logged-in user is fine and we must not claim otherwise).
648
- *
649
- * - claude (`claude:*`, sdk + cli): the Claude Code process authenticates from
650
- * `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, or `CLAUDE_CODE_OAUTH_TOKEN` in
651
- * the env, OR from a `claude /login` subscription session stored outside the
652
- * environment (macOS Keychain / `~/.claude`), OR from a Bedrock/Vertex
653
- * configuration (`CLAUDE_CODE_USE_BEDROCK`/`CLAUDE_CODE_USE_VERTEX` + cloud
654
- * credentials) — none of which the env-key check can see, hence the hedge in
655
- * the warning. Its own error string is verbatim: "Claude Code authentication
656
- * failed. Run `claude /login` or configure ANTHROPIC_API_KEY,
657
- * ANTHROPIC_AUTH_TOKEN, or CLAUDE_CODE_OAUTH_TOKEN."
658
- * - codex (`codex:*`, cli): the Codex app-server authenticates from
659
- * `OPENAI_API_KEY` in the env, OR from a `codex login` ChatGPT session stored
660
- * in `~/.codex/auth.json` — also outside the environment.
661
- */
662
- const SDK_AUTH_SCHEMES = {
663
- claude: {
664
- envKeys: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN"],
665
- loginCommand: "claude /login",
666
- loginDetail: "a `claude /login` subscription session or a Bedrock/Vertex configuration (CLAUDE_CODE_USE_BEDROCK/VERTEX)",
667
- failureHint: 'the first turn fails with an opaque "Claude Code process exited with code 1" that names nothing',
668
- statusCommand: "claude auth status --json",
669
- statusArgs: ["auth", "status", "--json"],
670
- },
671
- codex: {
672
- envKeys: ["OPENAI_API_KEY"],
673
- loginCommand: "codex login",
674
- loginDetail: "a `codex login` ChatGPT session (`~/.codex/auth.json`, outside the environment)",
675
- failureHint: "the first turn fails to authenticate",
676
- statusCommand: "codex login status",
677
- statusArgs: ["login", "status"],
678
- },
679
- };
680
- const SDK_AUTH_STATUS_TIMEOUT_MS = 5_000;
681
- const SDK_AUTH_STATUS_MAX_BUFFER_BYTES = 64 * 1024;
682
- function isSdkAuthName(value) {
683
- return value === "claude" || value === "codex";
684
- }
685
- const defaultSdkAuthStatusExecFile = async (file, args, options) => {
686
- const { stdout } = await execFile(file, [...args], options);
687
- return { stdout };
688
- };
689
- /**
690
- * Performs the SDK's local, read-only login-status command. A zero Codex exit
691
- * confirms its external login; Claude additionally requires strict JSON with
692
- * `loggedIn: true`. Missing binaries, timeouts, non-zero exits, and malformed
693
- * output all fail closed without leaking command output into validation.
694
- */
695
- async function checkSdkExternalLoginStatus(sdk, env, cwd, run) {
696
- const scheme = SDK_AUTH_SCHEMES[sdk];
697
- try {
698
- const { stdout } = await run(sdk, scheme.statusArgs, {
699
- cwd,
700
- env,
701
- timeout: SDK_AUTH_STATUS_TIMEOUT_MS,
702
- maxBuffer: SDK_AUTH_STATUS_MAX_BUFFER_BYTES,
703
- encoding: "utf8",
704
- });
705
- if (sdk === "codex") {
706
- return true;
707
- }
708
- const parsed = JSON.parse(stdout);
709
- return parsed !== null
710
- && typeof parsed === "object"
711
- && !Array.isArray(parsed)
712
- && parsed.loggedIn === true;
713
- }
714
- catch {
715
- return false;
716
- }
717
- }
718
- /** Read provider IDs directly from auth.json without launching mutation-capable OpenCode middleware. */
719
- async function inspectOpenCodeCredentialProviders(env) {
720
- if (env.OPENCODE_AUTH_CONTENT !== undefined) {
721
- return { status: "inline_auth_unsupported" };
722
- }
723
- const dataHome = typeof env.XDG_DATA_HOME === "string" && env.XDG_DATA_HOME.length > 0
724
- ? env.XDG_DATA_HOME
725
- : join(typeof env.HOME === "string" && env.HOME.length > 0 ? env.HOME : homedir(), ".local", "share");
726
- const opencodeData = join(dataHome, "opencode");
727
- const marker = await regularCurrentUserFile(join(opencodeData, "opencode.db"));
728
- if (!marker)
729
- return { status: "migration_required" };
730
- const authPath = join(opencodeData, "auth.json");
731
- if (!(await regularCurrentUserFile(authPath)))
732
- return { status: "auth_missing" };
733
- try {
734
- const parsed = JSON.parse(await readFile(authPath, "utf8"));
735
- if (!isUnknownRecord(parsed))
736
- return { status: "auth_invalid" };
737
- const providers = new Set();
738
- for (const [provider, credential] of Object.entries(parsed)) {
739
- if (provider.length === 0 || provider.trim() !== provider || !isOpenCodeCredentialEntry(credential)) {
740
- return { status: "auth_invalid" };
741
- }
742
- providers.add(provider);
743
- }
744
- return { status: "ok", providers };
745
- }
746
- catch {
747
- return { status: "auth_invalid" };
748
- }
749
- }
750
- async function regularCurrentUserFile(path) {
751
- try {
752
- const info = await lstat(path);
753
- return info.isFile() && (typeof process.getuid !== "function" || info.uid === process.getuid());
754
- }
755
- catch {
756
- return false;
757
- }
758
- }
759
- function isOpenCodeCredentialEntry(value) {
760
- if (!isUnknownRecord(value))
761
- return false;
762
- if (value.type === "oauth") {
763
- return typeof value.refresh === "string"
764
- && typeof value.access === "string"
765
- && (value.refresh.trim().length > 0 || value.access.trim().length > 0)
766
- && typeof value.expires === "number"
767
- && Number.isSafeInteger(value.expires)
768
- && value.expires >= 0
769
- && (value.accountId === undefined || typeof value.accountId === "string")
770
- && (value.enterpriseUrl === undefined || typeof value.enterpriseUrl === "string");
771
- }
772
- if (value.type === "api") {
773
- return typeof value.key === "string"
774
- && value.key.trim().length > 0
775
- && (value.metadata === undefined
776
- || (isUnknownRecord(value.metadata) && Object.values(value.metadata).every((entry) => typeof entry === "string")));
777
- }
778
- if (value.type === "wellknown") {
779
- return typeof value.key === "string" && value.key.trim().length > 0
780
- && typeof value.token === "string" && value.token.trim().length > 0;
781
- }
782
- return false;
783
- }
784
- async function checkOpenCodeVersion(env, cwd, run) {
785
- const versionEnv = {};
786
- for (const key of [
787
- "PATH",
788
- "LANG",
789
- "LC_ALL",
790
- "LC_CTYPE",
791
- "SHELL",
792
- "TMPDIR",
793
- "TMP",
794
- "TEMP",
795
- "SystemRoot",
796
- "WINDIR",
797
- "ComSpec",
798
- "PATHEXT",
799
- ]) {
800
- const value = env[key];
801
- if (typeof value === "string" && value.length > 0)
802
- versionEnv[key] = value;
803
- }
804
- try {
805
- const { stdout } = await run("opencode", ["--version"], {
806
- cwd,
807
- env: versionEnv,
808
- timeout: SDK_AUTH_STATUS_TIMEOUT_MS,
809
- maxBuffer: SDK_AUTH_STATUS_MAX_BUFFER_BYTES,
810
- encoding: "utf8",
811
- });
812
- const match = /^v?(\d+)\.(\d+)\.(\d+)(?:\+[0-9A-Za-z.-]+)?$/u.exec(stdout.trim());
813
- if (match === null)
814
- return false;
815
- const major = Number(match[1]);
816
- const minor = Number(match[2]);
817
- const patch = Number(match[3]);
818
- return Number.isSafeInteger(major)
819
- && Number.isSafeInteger(minor)
820
- && Number.isSafeInteger(patch)
821
- && (major > 1 || (major === 1 && minor >= 15));
822
- }
823
- catch {
824
- return false;
825
- }
826
- }
827
- /** First env key whose value is present and non-blank, or undefined when none are set. */
828
- function firstPresentEnvKey(env, keys) {
829
- for (const key of keys) {
830
- const value = env[key];
831
- if (typeof value === "string" && value.trim().length > 0) {
832
- return key;
833
- }
834
- }
835
- return undefined;
836
- }
837
497
  /**
838
498
  * Checks that every referenced model (primary, fallbacks, and the agent-host memory LLM)
839
499
  * has discoverable credentials, so a keyless or expired-OAuth provider is caught at
840
500
  * `validate` time instead of degrading crons/memory silently at runtime (the failure mode
841
- * that broke memory capture for ~10 days: the auth store's OAuth token had quietly expired,
842
- * and the E1 fresh-instance case where `claude:*` with no `ANTHROPIC_API_KEY` "validates"
843
- * clean but the first turn dies with an opaque "process exited with code 1").
501
+ * that broke memory capture for ~10 days when an auth-store OAuth token quietly expired.
844
502
  *
845
- * The check is read-only. Static validation never launches a process; live validation may
846
- * run bounded, local SDK login-status commands, which inspect durable external login state
847
- * without making a model turn or mutating the auth store.
848
- *
849
- * - Pi providers: inspect the auth store (`piAuthPath`) and the config's own
503
+ * The read-only check inspects the auth store (`piAuthPath`) and the config's own
850
504
  * `providers.local` custom providers. A custom/local provider follows its declared
851
505
  * `apiKey` / `apiKeyEnv` contract instead of Pi OAuth; an OAuth provider absent from
852
506
  * the store, or whose access token has expired, is flagged `waiting` with a re-auth hint.
853
- * - SDK-authenticated providers (`claude:*` / `codex:*`): inspect the RESOLVED ENV (process
854
- * env + loaded `.env`) for the backend's accepted keys. During live validation only, a
855
- * missing env credential falls back to `claude auth status --json` / `codex login status`.
856
- * The commands use the same resolved environment (including PATH and HOME), are bounded,
857
- * cached once per SDK, and never make a model turn. Static validation remains process-free.
858
- * - Direct OpenCode (`opencode:<provider>:<model>`): inspect exact provider IDs in the
859
- * standard auth.json and require the native DB migration marker. Static validation launches
860
- * no process. Live validation runs only a bounded `opencode --version` preflight; it never
861
- * runs the mutation-capable auth middleware or makes a model turn.
862
507
  *
863
508
  * `waiting` (never `error`) keeps the verdict non-fatal, mirroring the Ollama/Phoenix
864
509
  * probes — the goal is visibility, not blocking start.
865
510
  */
866
- async function credentialsSection(config, env, cwd, liveness, verifiedCredentialModelRefs = [], sdkAuthStatusExecFile = defaultSdkAuthStatusExecFile, staticTriggerRefs = []) {
511
+ async function credentialsSection(config, env, verifiedCredentialModelRefs = [], staticTriggerRefs = []) {
867
512
  const refs = [
868
513
  { label: "Primary", ref: config.runtime.model },
869
514
  ...configuredRuntimeRouteChecks(config).slice(1).map((route) => ({
@@ -872,7 +517,7 @@ async function credentialsSection(config, env, cwd, liveness, verifiedCredential
872
517
  })),
873
518
  ...staticTriggerRefs,
874
519
  ];
875
- if (config.memory?.llm !== undefined && config.memory.llm.provider !== "ollama") {
520
+ if (config.memory?.llm?.provider === "agent-host") {
876
521
  try {
877
522
  refs.push({ label: "Memory LLM", ref: parseMonoRuntimeModelReference(config.memory.llm.model) });
878
523
  }
@@ -880,9 +525,7 @@ async function credentialsSection(config, env, cwd, liveness, verifiedCredential
880
525
  // A malformed memory model reference is surfaced by the memory/runtime shape checks.
881
526
  }
882
527
  }
883
- const authenticatedRefs = refs.filter((r) => (r.ref.sdk === "pi" && typeof r.ref.provider === "string")
884
- || (r.ref.sdk === "opencode" && typeof r.ref.provider === "string")
885
- || isSdkAuthName(r.ref.sdk));
528
+ const authenticatedRefs = refs;
886
529
  if (authenticatedRefs.length === 0) {
887
530
  return {
888
531
  id: "credentials",
@@ -895,102 +538,19 @@ async function credentialsSection(config, env, cwd, liveness, verifiedCredential
895
538
  let status = "ok";
896
539
  const verified = new Set(verifiedCredentialModelRefs);
897
540
  for (const { label, ref } of authenticatedRefs) {
898
- const refStr = referenceOf(ref);
899
- if (verified.has(refStr)) {
900
- details.push(`${label} ${refStr}: credentials verified by a successful live model check.`);
541
+ // The set holds whole canonical references, so the LOOKUP key stays whole;
542
+ // only the printed form is bounded.
543
+ if (verified.has(referenceOf(ref))) {
544
+ details.push(`${label} ${displayReferenceOf(ref)}: credentials verified by a successful live model check.`);
901
545
  }
902
546
  }
903
547
  const unverifiedRefs = authenticatedRefs.filter(({ ref }) => !verified.has(referenceOf(ref)));
904
- const piRefs = unverifiedRefs.filter((r) => r.ref.sdk === "pi" && typeof r.ref.provider === "string");
905
- const openCodeRefs = unverifiedRefs.filter((r) => r.ref.sdk === "opencode" && typeof r.ref.provider === "string");
906
- const sdkRefs = unverifiedRefs.filter((r) => isSdkAuthName(r.ref.sdk));
907
- if (piRefs.length > 0) {
908
- const piStatus = await appendPiCredentialDetails(config, piRefs, details, env);
548
+ if (unverifiedRefs.length > 0) {
549
+ const piStatus = await appendPiCredentialDetails(config, unverifiedRefs, details, env);
909
550
  if (piStatus === "waiting") {
910
551
  status = "waiting";
911
552
  }
912
553
  }
913
- const openCodeInspection = openCodeRefs.length > 0
914
- ? inspectOpenCodeCredentialProviders(env)
915
- : undefined;
916
- const openCodeVersion = liveness && openCodeRefs.length > 0
917
- ? checkOpenCodeVersion(env, cwd, sdkAuthStatusExecFile)
918
- : undefined;
919
- const sdkAuthStatuses = new Map();
920
- const externalLoginStatus = (sdk) => {
921
- const cached = sdkAuthStatuses.get(sdk);
922
- if (cached !== undefined) {
923
- return cached;
924
- }
925
- const pending = checkSdkExternalLoginStatus(sdk, env, cwd, sdkAuthStatusExecFile);
926
- sdkAuthStatuses.set(sdk, pending);
927
- return pending;
928
- };
929
- // Start each unique local status check before awaiting details so two SDKs
930
- // cost one bounded timeout window rather than running serially.
931
- if (liveness) {
932
- for (const { ref } of sdkRefs) {
933
- const scheme = SDK_AUTH_SCHEMES[ref.sdk];
934
- if (scheme !== undefined && firstPresentEnvKey(env, scheme.envKeys) === undefined) {
935
- void externalLoginStatus(ref.sdk);
936
- }
937
- }
938
- }
939
- for (const { label, ref } of sdkRefs) {
940
- const refStr = referenceOf(ref);
941
- const scheme = SDK_AUTH_SCHEMES[ref.sdk];
942
- if (scheme === undefined) {
943
- continue;
944
- }
945
- const present = firstPresentEnvKey(env, scheme.envKeys);
946
- if (present !== undefined) {
947
- details.push(`${label} ${refStr}: SDK credential present in the resolved env (${present}); credential detected, live model verification is still pending.`);
948
- continue;
949
- }
950
- if (liveness && await externalLoginStatus(ref.sdk)) {
951
- details.push(`${label} ${refStr}: external sign-in detected by read-only \`${scheme.statusCommand}\`; ` +
952
- "credentials are not verified until a live model turn succeeds.");
953
- continue;
954
- }
955
- status = "waiting";
956
- details.push(`[WARN] ${label} ${refStr}: no SDK credential in the resolved env (checked ${scheme.envKeys.join(", ")}). ` +
957
- (liveness
958
- ? `External login was not verified by \`${scheme.statusCommand}\`; `
959
- : `If you authenticated via ${scheme.loginDetail} this is fine and can't be verified during static validation; `) +
960
- `otherwise ${scheme.failureHint} — set ${scheme.envKeys[0]} or run \`${scheme.loginCommand}\`.`);
961
- }
962
- if (openCodeRefs.length > 0) {
963
- const inspection = openCodeInspection === undefined ? undefined : await openCodeInspection;
964
- const supportedVersion = openCodeVersion === undefined ? false : await openCodeVersion;
965
- for (const { label, ref } of openCodeRefs) {
966
- const refStr = referenceOf(ref);
967
- const provider = ref.provider;
968
- const credentialPresent = inspection?.status === "ok" && inspection.providers.has(provider);
969
- if (credentialPresent && liveness && supportedVersion) {
970
- details.push(`${label} ${refStr}: provider \`${provider}\` credential present in the standard OpenCode auth store; stable OpenCode CLI >=1.15.0 detected without a model turn. Credential detected; live model verification is still pending.`);
971
- }
972
- else {
973
- status = "waiting";
974
- const warning = inspection?.status === "migration_required"
975
- ? "the native OpenCode database migration marker is missing or invalid; run `opencode db migrate --pure` once"
976
- : inspection?.status === "inline_auth_unsupported"
977
- ? "OPENCODE_AUTH_CONTENT is unsupported for direct runs; persist credentials with `opencode auth login` and unset it"
978
- : inspection?.status === "auth_invalid"
979
- ? "the standard OpenCode auth.json is malformed or contains an unsupported credential entry"
980
- : credentialPresent && !liveness
981
- ? "credentials and migration marker are present, but the required stable OpenCode CLI >=1.15.0 is unverified during static validation"
982
- : credentialPresent && !supportedVersion
983
- ? "credentials are present, but stable OpenCode CLI >=1.15.0 could not be verified"
984
- : inspection?.status === "ok"
985
- ? `no exact credential entry exists for provider \`${provider}\`; run \`opencode auth login\` for that provider`
986
- : "the standard OpenCode auth.json is missing; run `opencode auth login`";
987
- const safetyNote = liveness
988
- ? "No model turn or mutation-capable OpenCode command was run."
989
- : "No OpenCode process was launched.";
990
- details.push(`[WARN] ${label} ${refStr}: ${warning}. ${safetyNote}`);
991
- }
992
- }
993
- }
994
554
  return { id: "credentials", label: "Provider credentials", status, details };
995
555
  }
996
556
  /**
@@ -1014,34 +574,38 @@ async function appendPiCredentialDetails(config, piRefs, details, env) {
1014
574
  }
1015
575
  for (const { label, ref } of piRefs) {
1016
576
  const provider = ref.provider;
1017
- const refStr = referenceOf(ref);
1018
- const loginCommand = piAuthRecoveryCommand(provider, authPath);
577
+ // Display-only from here down: `provider` is still the lookup key into the
578
+ // auth store and providers.local, `providerLabel` and `refStr` are what the
579
+ // detail lines quote back at the operator.
580
+ const providerLabel = displayText(provider);
581
+ const refStr = displayReferenceOf(ref);
582
+ const loginCommand = piAuthRecoveryCommand(providerLabel, authPath);
1019
583
  const localProvider = localProviders.get(provider);
1020
584
  if (localProvider !== undefined) {
1021
585
  if (localProvider.enabled === false) {
1022
586
  status = "waiting";
1023
- details.push(`[WARN] ${label} ${refStr}: provider \`${provider}\` is configured in providers.local but disabled (\`enabled: false\`); the runtime will throw \`provider disabled: ${provider}\` on the first turn. Set \`enabled: true\` on that providers.local entry.`);
587
+ details.push(`[WARN] ${label} ${refStr}: provider \`${providerLabel}\` is configured in providers.local but disabled (\`enabled: false\`); the runtime will throw \`provider disabled: ${providerLabel}\` on the first turn. Set \`enabled: true\` on that providers.local entry.`);
1024
588
  }
1025
589
  else if (localProvider.apiKey !== undefined) {
1026
- details.push(`${label} ${refStr}: provider \`${provider}\` configured via config providers.local (API key configured); credential detected, live model verification is still pending.`);
590
+ details.push(`${label} ${refStr}: provider \`${providerLabel}\` configured via config providers.local (API key configured); credential detected, live model verification is still pending.`);
1027
591
  }
1028
592
  else if (localProvider.apiKeyEnv !== undefined) {
1029
593
  if (hasNonEmptyCredentialValue(env[localProvider.apiKeyEnv])) {
1030
- details.push(`${label} ${refStr}: provider \`${provider}\` configured via config providers.local with ${localProvider.apiKeyEnv} present in the resolved environment; credential detected, live model verification is still pending.`);
594
+ details.push(`${label} ${refStr}: provider \`${providerLabel}\` configured via config providers.local with ${localProvider.apiKeyEnv} present in the resolved environment; credential detected, live model verification is still pending.`);
1031
595
  }
1032
596
  else {
1033
597
  status = "waiting";
1034
- details.push(`[WARN] ${label} ${refStr}: provider \`${provider}\` declares apiKeyEnv \`${localProvider.apiKeyEnv}\`, but the resolved environment has no non-empty value and no inline apiKey fallback. Set ${localProvider.apiKeyEnv} before starting.`);
598
+ details.push(`[WARN] ${label} ${refStr}: provider \`${providerLabel}\` declares apiKeyEnv \`${localProvider.apiKeyEnv}\`, but the resolved environment has no non-empty value and no inline apiKey fallback. Set ${localProvider.apiKeyEnv} before starting.`);
1035
599
  }
1036
600
  }
1037
601
  else {
1038
- details.push(`${label} ${refStr}: provider \`${provider}\` configured via config providers.local (keyless local provider; no API key declared).`);
602
+ details.push(`${label} ${refStr}: provider \`${providerLabel}\` configured via config providers.local (keyless local provider; no API key declared).`);
1039
603
  }
1040
604
  continue;
1041
605
  }
1042
606
  const apiKeyEnv = PI_API_KEY_ENV_BY_PROVIDER[provider];
1043
607
  if (apiKeyEnv !== undefined && hasNonEmptyCredentialValue(env[apiKeyEnv])) {
1044
- details.push(`${label} ${refStr}: Pi API-key credential for \`${provider}\` present in the resolved environment (${apiKeyEnv}); credential detected, live model verification is still pending.`);
608
+ details.push(`${label} ${refStr}: Pi API-key credential for \`${providerLabel}\` present in the resolved environment (${apiKeyEnv}); credential detected, live model verification is still pending.`);
1045
609
  continue;
1046
610
  }
1047
611
  if (authInspection?.status === "unsafe") {
@@ -1056,37 +620,37 @@ async function appendPiCredentialDetails(config, piRefs, details, env) {
1056
620
  if (entry === undefined) {
1057
621
  status = "waiting";
1058
622
  details.push(apiKeyEnv === undefined
1059
- ? `[WARN] ${label} ${refStr}: no Pi credentials found for provider \`${provider}\` in the auth store. Authenticate it with \`${loginCommand}\`, or set providers.piAuthPath.`
1060
- : `[WARN] ${label} ${refStr}: no Pi API key credentials found for provider \`${provider}\` in the auth store or resolved environment. Run \`${loginCommand}\`, or set ${apiKeyEnv}.`);
623
+ ? `[WARN] ${label} ${refStr}: no Pi credentials found for provider \`${providerLabel}\` in the auth store. Authenticate it with \`${loginCommand}\`, or set providers.piAuthPath.`
624
+ : `[WARN] ${label} ${refStr}: no Pi API key credentials found for provider \`${providerLabel}\` in the auth store or resolved environment. Run \`${loginCommand}\`, or set ${apiKeyEnv}.`);
1061
625
  continue;
1062
626
  }
1063
627
  const isOAuth = entry.type === "oauth";
1064
628
  const isApiKey = entry.type === "api_key";
1065
629
  if (isApiKey && !hasNonEmptyCredentialValue(entry.key)) {
1066
630
  status = "waiting";
1067
- details.push(`[WARN] ${label} ${refStr}: stored API-key credential for \`${provider}\` has no usable key. Run \`${loginCommand}\`; no secret value was displayed.`);
631
+ details.push(`[WARN] ${label} ${refStr}: stored API-key credential for \`${providerLabel}\` has no usable key. Run \`${loginCommand}\`; no secret value was displayed.`);
1068
632
  continue;
1069
633
  }
1070
634
  if (isOAuth && !hasNonEmptyCredentialValue(entry.access) && !hasNonEmptyCredentialValue(entry.refresh)) {
1071
635
  status = "waiting";
1072
- details.push(`[WARN] ${label} ${refStr}: stored OAuth credential for \`${provider}\` has no usable access or refresh token. Re-authenticate with \`${loginCommand}\`; no secret value was displayed.`);
636
+ details.push(`[WARN] ${label} ${refStr}: stored OAuth credential for \`${providerLabel}\` has no usable access or refresh token. Re-authenticate with \`${loginCommand}\`; no secret value was displayed.`);
1073
637
  continue;
1074
638
  }
1075
639
  if (!isOAuth && !isApiKey) {
1076
640
  status = "waiting";
1077
- details.push(`[WARN] ${label} ${refStr}: stored credential for \`${provider}\` has an unsupported or missing type. Re-authenticate with \`${loginCommand}\`; no secret value was displayed.`);
641
+ details.push(`[WARN] ${label} ${refStr}: stored credential for \`${providerLabel}\` has an unsupported or missing type. Re-authenticate with \`${loginCommand}\`; no secret value was displayed.`);
1078
642
  continue;
1079
643
  }
1080
644
  const expired = typeof entry.expires === "number" && entry.expires < now;
1081
645
  const whenNote = typeof entry.expires === "number" ? ` ${new Date(entry.expires).toISOString()}` : "";
1082
646
  if (isOAuth && expired) {
1083
647
  status = "waiting";
1084
- details.push(`[WARN] ${label} ${refStr}: OAuth token for \`${provider}\` expired${whenNote} — the runtime may auto-refresh, but this credential is not ready until a request succeeds; if runs fail with "No API key for provider: ${provider}" re-authenticate with \`${loginCommand}\`.`);
648
+ details.push(`[WARN] ${label} ${refStr}: OAuth token for \`${providerLabel}\` expired${whenNote} — the runtime may auto-refresh, but this credential is not ready until a request succeeds; if runs fail with "No API key for provider: ${providerLabel}" re-authenticate with \`${loginCommand}\`.`);
1085
649
  continue;
1086
650
  }
1087
651
  details.push(isOAuth
1088
- ? `${label} ${refStr}: OAuth credentials for \`${provider}\` present (token valid${whenNote}); credential detected, live model verification is still pending.`
1089
- : `${label} ${refStr}: API key credentials for \`${provider}\` present; credential detected, live model verification is still pending.`);
652
+ ? `${label} ${refStr}: OAuth credentials for \`${providerLabel}\` present (token valid${whenNote}); credential detected, live model verification is still pending.`
653
+ : `${label} ${refStr}: API key credentials for \`${providerLabel}\` present; credential detected, live model verification is still pending.`);
1090
654
  }
1091
655
  return status;
1092
656
  }
@@ -1110,6 +674,8 @@ function hasNonEmptyCredentialValue(value) {
1110
674
  async function contextSection(config, cwd) {
1111
675
  const details = [];
1112
676
  let status = "ok";
677
+ const activeSelectedSkills = activeProjectSkillSelections(config.context.selectedSkills);
678
+ const retiredSelectedSkills = config.context.selectedSkills.filter(isRetiredProjectSkillName);
1113
679
  if (await pathExists(config.context.identityPath)) {
1114
680
  details.push(`Identity: ${config.context.identityPath}`);
1115
681
  }
@@ -1121,10 +687,19 @@ async function contextSection(config, cwd) {
1121
687
  status = "error";
1122
688
  details.push(`Soul file is missing: ${config.context.soulPath}`);
1123
689
  }
1124
- if (config.context.skillsRoot !== undefined) {
690
+ for (const skill of retiredSelectedSkills) {
691
+ if (status === "ok")
692
+ status = "waiting";
693
+ details.push(`Skill \`${skill}\` is retired and ignored. Remove it from context.selectedSkills or ` +
694
+ "MONO_AGENT_SELECTED_SKILLS, then run `mono-agent install-skill --project --check`." +
695
+ (config.context.skillDisclosure === "index"
696
+ ? " Skill disclosure runs as `full` until the retired selector is removed."
697
+ : ""));
698
+ }
699
+ if (activeSelectedSkills.length > 0 && config.context.skillsRoot !== undefined) {
1125
700
  if (await pathExists(config.context.skillsRoot)) {
1126
701
  details.push(`Skills root: ${config.context.skillsRoot}`);
1127
- for (const skill of config.context.selectedSkills) {
702
+ for (const skill of activeSelectedSkills) {
1128
703
  const skillPath = join(config.context.skillsRoot, skill, "SKILL.md");
1129
704
  if (await pathExists(skillPath)) {
1130
705
  details.push(`Skill \`${skill}\`: ${skillPath}`);
@@ -1140,7 +715,7 @@ async function contextSection(config, cwd) {
1140
715
  details.push(`Skills root is missing: ${config.context.skillsRoot}`);
1141
716
  }
1142
717
  }
1143
- else if (config.context.selectedSkills.length > 0) {
718
+ else if (activeSelectedSkills.length > 0) {
1144
719
  status = "error";
1145
720
  details.push("Skills are selected but context.skillsRoot is not set.");
1146
721
  }
@@ -1175,7 +750,15 @@ function memoryConsolidationCronIssue(expression) {
1175
750
  }
1176
751
  async function memorySection(config, cwd, env, liveness, allowFilesystemWrites, preferAppPluginInstall) {
1177
752
  if (config.memory === undefined) {
1178
- return { id: "memory", label: "Memory", status: "disabled", details: ["No memory configured."] };
753
+ return {
754
+ id: "memory",
755
+ label: "Memory",
756
+ status: "disabled",
757
+ details: [
758
+ "No memory configured.",
759
+ "Chronological journal: disabled because no local memory backend is configured.",
760
+ ],
761
+ };
1179
762
  }
1180
763
  // External backend (e.g. supermemory): mode/embeddings/llm are bujo-only and
1181
764
  // ignored, so validate the plugin-owned shape before any soft liveness probe.
@@ -1221,6 +804,9 @@ async function memorySection(config, cwd, env, liveness, allowFilesystemWrites,
1221
804
  sm.apiKey === undefined
1222
805
  ? "Auth: no API key configured (keyless — works only if the instance allows it)."
1223
806
  : "Auth: API key configured.",
807
+ config.memory.recallTool?.enabled === false
808
+ ? "Explicit memory read tools are disabled by memory.recallTool.enabled. Chronological journal browsing is also unsupported by Supermemory."
809
+ : "Chronological journal: unsupported by Supermemory; MemoryJournal is not offered.",
1224
810
  ];
1225
811
  if (!liveness) {
1226
812
  details.push("Supermemory liveness probe skipped; ingestion is async.");
@@ -1238,6 +824,9 @@ async function memorySection(config, cwd, env, liveness, allowFilesystemWrites,
1238
824
  }
1239
825
  const details = [
1240
826
  `Mode: ${config.memory.mode}, path: ${config.memory.path}, writeMode: ${config.memory.writeMode}.`,
827
+ config.memory.recallTool?.enabled === false
828
+ ? `Explicit memory read tools are disabled by memory.recallTool.enabled; the local ${config.memory.mode} tier supports chronology but MemoryJournal is not offered.`
829
+ : `Chronological journal: supported by the local ${config.memory.mode} tier when tool policy allows MemoryJournal.`,
1241
830
  ];
1242
831
  let status = "ok";
1243
832
  if (config.memory.llm !== undefined) {
@@ -1248,12 +837,12 @@ async function memorySection(config, cwd, env, liveness, allowFilesystemWrites,
1248
837
  const resolutionIssue = piModelResolutionIssue(config, model);
1249
838
  if (resolutionIssue !== undefined) {
1250
839
  status = "error";
1251
- details.push(`Agent-host memory LLM ${referenceOf(model)}: ${resolutionIssue}.`);
840
+ details.push(`Agent-host memory LLM ${displayReferenceOf(model)}: ${resolutionIssue}.`);
1252
841
  }
1253
842
  }
1254
843
  catch (error) {
1255
844
  status = "error";
1256
- details.push(`Agent-host memory LLM ${config.memory.llm.model}: ${error instanceof Error ? error.message : String(error)}.`);
845
+ details.push(`Agent-host memory LLM ${displayText(config.memory.llm.model)}: ${displayReason(error)}.`);
1257
846
  }
1258
847
  }
1259
848
  }
@@ -1573,12 +1162,12 @@ async function localEmbeddingLivenessWarnings(embeddings, apiKey) {
1573
1162
  }
1574
1163
  if (provider === "ollama" && /HTTP 404/u.test(reason)) {
1575
1164
  return [
1576
- `[WARN] Ollama embedding model ${embeddings.model} could not be proved at ${endpoint} (${reason}); ` +
1577
- `run \`ollama pull ${embeddings.model}\` and verify its embedding capability.`,
1165
+ `[WARN] Ollama embedding model ${displayText(embeddings.model)} could not be proved at ${endpoint} (${reason}); ` +
1166
+ `run \`ollama pull ${displayText(embeddings.model)}\` and verify its embedding capability.`,
1578
1167
  ];
1579
1168
  }
1580
1169
  return [
1581
- `[WARN] ${label} embedding readiness failed for ${embeddings.model} at ${endpoint} (${reason}). ` +
1170
+ `[WARN] ${label} embedding readiness failed for ${displayText(embeddings.model)} at ${endpoint} (${reason}). ` +
1582
1171
  "Verify the selected model, authentication, and configured dimension.",
1583
1172
  ];
1584
1173
  }
@@ -1620,8 +1209,8 @@ function memoryLlmLabel(llm) {
1620
1209
  return "none";
1621
1210
  }
1622
1211
  return llm.provider === "ollama"
1623
- ? `ollama:${llm.model}`
1624
- : `agent-host:${llm.model}${llm.executionMode === undefined ? "" : ` (${llm.executionMode})`}`;
1212
+ ? `ollama:${displayText(llm.model)}`
1213
+ : `agent-host:${displayText(llm.model)}`;
1625
1214
  }
1626
1215
  async function toolsSection(config, input) {
1627
1216
  const details = [];
@@ -1650,6 +1239,53 @@ async function toolsSection(config, input) {
1650
1239
  details.push(`Allowed tools: ${allowedTools.join(", ")}.`);
1651
1240
  let mcpNoteAdded = false;
1652
1241
  for (const name of allowedTools) {
1242
+ if (isMemoryJournalToolPolicyName(name)) {
1243
+ // This app-owned MCP surface must be checked before the generic MCP
1244
+ // branch, using the same deny-wins spellings as runtime composition.
1245
+ if (!isMemoryJournalToolAllowed(config.tools)) {
1246
+ status = "waiting";
1247
+ details.push(`${name} is listed but tool policy still resolves to denied (deny wins, including the mcp__mono-agent-memory-journal__* spelling) - the tool will not be offered.`);
1248
+ }
1249
+ else if (config.memory === undefined) {
1250
+ status = "waiting";
1251
+ details.push(`${name} is in allowedTools but no memory block is configured - chronological journal browsing will not work.`);
1252
+ }
1253
+ else if (config.memory.recallTool?.enabled === false) {
1254
+ status = "waiting";
1255
+ details.push(`${name} is in allowedTools but memory.recallTool.enabled is off - explicit memory read tools will not work.`);
1256
+ }
1257
+ else if ((config.memory.backend ?? "bujo") === "supermemory") {
1258
+ status = "waiting";
1259
+ details.push(`${name} is in allowedTools but the Supermemory backend exposes no chronological journal surface - the tool will not be offered.`);
1260
+ }
1261
+ continue;
1262
+ }
1263
+ if (isRememberToolPolicyName(name)) {
1264
+ // Reconcile with the runtime's own resolution, which honours the
1265
+ // `mcp__mono-agent-memory-write__*` spellings and deny-wins. This check
1266
+ // must precede the generic MCP branch below.
1267
+ if (!isRememberToolAllowed(config.tools)) {
1268
+ status = "waiting";
1269
+ details.push(`${name} is listed but tool policy still resolves to denied (deny wins, including the mcp__mono-agent-memory-write__* spellings) - the tool will not be offered.`);
1270
+ continue;
1271
+ }
1272
+ // Unlike MemoryRecall, Remember IS allowlist-gated, so listing it is
1273
+ // correct and required under a restrictive policy. It still needs a
1274
+ // configured memory block with the write surface left enabled.
1275
+ if (config.memory === undefined) {
1276
+ status = "waiting";
1277
+ details.push(`${name} is in allowedTools but no memory block is configured - durable writes will not work. Configure memory (or remove this entry).`);
1278
+ }
1279
+ else if (config.memory.rememberTool?.enabled === false) {
1280
+ status = "waiting";
1281
+ details.push(`${name} is in allowedTools but memory.rememberTool.enabled is off - durable writes will not work. Enable memory.rememberTool (or remove this entry).`);
1282
+ }
1283
+ else if (config.memory.backend === "supermemory") {
1284
+ status = "waiting";
1285
+ details.push(`${name} is in allowedTools but the supermemory backend exposes no durable write surface - the tool will not be offered.`);
1286
+ }
1287
+ continue;
1288
+ }
1653
1289
  if (isMcpToolName(name)) {
1654
1290
  // MCP tool names are owned by their servers; we cannot verify them offline.
1655
1291
  if (!mcpNoteAdded) {
@@ -1685,45 +1321,6 @@ async function toolsSection(config, input) {
1685
1321
  // Under allow-all the disallow list is already folded into the "except" clause above.
1686
1322
  details.push(`Disallowed tools: ${config.tools.disallowedTools.join(", ")}.`);
1687
1323
  }
1688
- const runtimeModels = configuredRuntimeModels(config.runtime);
1689
- const directCodexModels = runtimeModels
1690
- .filter((model) => model.sdk === "codex")
1691
- .map(referenceOf);
1692
- const directOpenCodeModels = runtimeModels
1693
- .filter((model) => model.sdk === "opencode")
1694
- .map(referenceOf);
1695
- const claudeCliModels = [
1696
- { model: config.runtime.model, executionMode: config.runtime.executionMode },
1697
- ...configuredRuntimeFallbackModels(config.runtime).map((model) => ({
1698
- model,
1699
- executionMode: defaultExecutionModeForModel(model),
1700
- })),
1701
- ]
1702
- .filter(({ model, executionMode }) => model.sdk === "claude" && executionMode === "cli")
1703
- .map(({ model }) => referenceOf(model));
1704
- const allowAllOnly = hasAllowAllOnlyToolPolicy(config.tools);
1705
- const dedicatedNoToolsProbe = input.codexNoToolsProbe === true
1706
- && allowedTools.length === 0
1707
- && config.tools.disallowedTools.length === 0;
1708
- if (directCodexModels.length > 0
1709
- && !dedicatedNoToolsProbe
1710
- && !allowAllOnly) {
1711
- status = "error";
1712
- details.push(`Direct Codex model${directCodexModels.length === 1 ? "" : "s"} ${directCodexModels.join(", ")} cannot enforce ` +
1713
- "tools.allowedTools/tools.disallowedTools. Use allow-all-only (allowedTools omitted or containing \"*\", with no disallowedTools), " +
1714
- "or select a runtime that supports restrictive tool policies.");
1715
- }
1716
- if (directOpenCodeModels.length > 0 && !allowAllOnly) {
1717
- status = "error";
1718
- details.push(`Direct OpenCode model${directOpenCodeModels.length === 1 ? "" : "s"} ${directOpenCodeModels.join(", ")} cannot enforce ` +
1719
- "tools.allowedTools/tools.disallowedTools. Use allow-all-only (allowedTools omitted or containing \"*\", with no disallowedTools), " +
1720
- "or use a Pi runtime (including pi:opencode-go:*).");
1721
- }
1722
- if (claudeCliModels.length > 0 && allowedTools.length === 0) {
1723
- status = "error";
1724
- details.push(`Claude CLI model${claudeCliModels.length === 1 ? "" : "s"} ${claudeCliModels.join(", ")} cannot enforce an empty ` +
1725
- "tools.allowedTools list because omitting --tools enables Claude Code's default tool set. Use Claude SDK for a chat-only agent, or configure a non-empty enforceable tool list.");
1726
- }
1727
1324
  let configuredMcpServerNames = [];
1728
1325
  let configuredMcpServers = {};
1729
1326
  if (config.tools.mcpConfigPath !== undefined) {
@@ -1781,7 +1378,6 @@ async function toolsSection(config, input) {
1781
1378
  const adapterSendTools = await resolveAdapterSendToolsSettings(input, {
1782
1379
  allowedTools: config.tools.allowedTools,
1783
1380
  disallowedTools: config.tools.disallowedTools,
1784
- suppressInteractionTools: directOpenCodeModels.length > 0,
1785
1381
  });
1786
1382
  if (adapterSendTools === undefined) {
1787
1383
  details.push("No adapter-derived send tools enabled.");
@@ -1789,19 +1385,13 @@ async function toolsSection(config, input) {
1789
1385
  else {
1790
1386
  details.push(`Adapter send tools: ${adapterSendToolNames(adapterSendTools).join(", ")}.`);
1791
1387
  }
1792
- const blockedAdapterEndpoints = await adapterSendToolNetworkPolicyWarnings(config, input, adapterSendTools, directOpenCodeModels.length > 0);
1388
+ const blockedAdapterEndpoints = await adapterSendToolNetworkPolicyWarnings(config, input, adapterSendTools);
1793
1389
  if (blockedAdapterEndpoints.length > 0) {
1794
1390
  if (status !== "error") {
1795
1391
  status = "waiting";
1796
1392
  }
1797
1393
  details.push(...blockedAdapterEndpoints);
1798
1394
  }
1799
- const effectiveMcpSources = effectiveMcpRuntimeSources(config, configuredMcpServerNames, adapterSendTools === undefined ? [] : adapterSendToolNames(adapterSendTools));
1800
- if (directOpenCodeModels.length > 0 && effectiveMcpSources.length > 0) {
1801
- status = "error";
1802
- details.push(`Direct OpenCode model${directOpenCodeModels.length === 1 ? "" : "s"} ${directOpenCodeModels.join(", ")} cannot safely consume ` +
1803
- `MCP runtime options from ${effectiveMcpSources.join("; ")}. Disable those MCP sources or use a Pi runtime (including pi:opencode-go:*).`);
1804
- }
1805
1395
  // Subagents: the Agent tool is registered only when BOTH the capability is
1806
1396
  // enabled and the tool is allowed, so surface either half being missing.
1807
1397
  const subagents = config.subagents;
@@ -1824,14 +1414,6 @@ async function toolsSection(config, input) {
1824
1414
  }
1825
1415
  else {
1826
1416
  details.push("Subagents inherit the parent's skill index and the ReadSkill tool.");
1827
- // The parent's own chain is capability-checked elsewhere; a profile that
1828
- // pins its own model is not, so a direct-OpenCode profile would silently
1829
- // run without the index it was told it had.
1830
- for (const definition of subagents.definitions ?? []) {
1831
- if (definition.model?.sdk === "opencode") {
1832
- details.push(`Subagent "${definition.name}" is pinned to a direct OpenCode model, which cannot use ReadSkill; it runs without the skill index.`);
1833
- }
1834
- }
1835
1417
  }
1836
1418
  for (const tool of subagents.inline?.allowedTools ?? []) {
1837
1419
  if (!isKnownToolName(tool) && !isMcpToolName(tool)) {
@@ -1881,31 +1463,61 @@ async function toolsSection(config, input) {
1881
1463
  return { id: "tools", label: "Tools & MCP", status, details };
1882
1464
  }
1883
1465
  const MIN_AGENT_BROWSER_VERSION = [0, 33, 1];
1466
+ function formatSearchBackendChain(backends) {
1467
+ if (backends.length <= 1)
1468
+ return backends[0] ?? "none";
1469
+ return `${backends.slice(0, -1).join(", ")}, then ${backends.at(-1)}`;
1470
+ }
1884
1471
  async function webToolsSection(config, input, liveness) {
1885
1472
  const web = config.tools.web;
1886
1473
  const search = web?.search ?? {
1887
1474
  backend: "auto",
1475
+ maxRequestsPerRun: 4,
1888
1476
  codex: { model: "gpt-5.6-luna" },
1889
1477
  };
1890
1478
  const fetchConfig = web?.fetch ?? { render: "never", browserCommand: "agent-browser" };
1891
- const details = [`WebSearch backend: ${search.backend}.`];
1479
+ const details = [
1480
+ `WebSearch backend: ${search.backend}.`,
1481
+ `WebSearch request budget: ${search.maxRequestsPerRun ?? 4} per logical run.`,
1482
+ `Web coordination: ${web?.coordination ?? "process"}.`,
1483
+ ];
1892
1484
  let status = "ok";
1893
- if (search.endpoint === undefined) {
1485
+ if (web?.coordination === "host") {
1486
+ try {
1487
+ const shared = await inspectWebControl();
1488
+ details.push(shared.exists ? "Host web control state is valid." : "Host web control state will be initialized on first request.");
1489
+ }
1490
+ catch {
1491
+ status = "waiting";
1492
+ details.push("Host web control state is unavailable; web requests will fail closed. Inspect with mono-agent web-control status.");
1493
+ }
1494
+ }
1495
+ const searxngEndpoint = search.searxng?.endpoint ?? search.endpoint;
1496
+ const autoEligibleBackends = [
1497
+ ...(search.ollama === undefined ? [] : ["configured Ollama"]),
1498
+ ...(searxngEndpoint === undefined ? [] : ["configured SearXNG"]),
1499
+ "Codex",
1500
+ "keyless",
1501
+ ];
1502
+ const autoAfterOllama = formatSearchBackendChain(autoEligibleBackends.slice(1));
1503
+ if (searxngEndpoint === undefined) {
1894
1504
  details.push(search.backend === "keyless"
1895
1505
  ? "SearXNG is not configured; keyless search is enabled."
1896
1506
  : search.backend === "codex"
1897
1507
  ? "SearXNG is not configured; strict Codex subscription search is enabled."
1898
1508
  : search.backend === "searxng"
1899
1509
  ? "SearXNG is not configured."
1900
- : "SearXNG is not configured; auto mode starts with Codex subscription search, then keyless search.");
1510
+ : search.ollama === undefined
1511
+ ? "SearXNG is not configured; auto mode starts with Codex subscription search, then keyless search."
1512
+ : "SearXNG is not configured; auto mode uses configured Ollama, then Codex subscription search, then keyless search.");
1901
1513
  }
1902
1514
  else {
1903
- details.push(`SearXNG endpoint: ${search.endpoint}.`);
1515
+ details.push(`SearXNG endpoint: ${searxngEndpoint}.`);
1904
1516
  if (!liveness) {
1905
1517
  details.push("SearXNG liveness was not probed.");
1906
1518
  }
1907
1519
  else {
1908
- const probe = await probeSearxngEndpoint(search.endpoint);
1520
+ const probe = await probeSearxngEndpoint(searxngEndpoint);
1909
1521
  if (probe.ok) {
1910
1522
  details.push("SearXNG JSON search probe succeeded.");
1911
1523
  }
@@ -1913,14 +1525,42 @@ async function webToolsSection(config, input, liveness) {
1913
1525
  status = "waiting";
1914
1526
  details.push(`[WARN] SearXNG JSON search probe failed (${probe.reason}). ` +
1915
1527
  (search.backend === "auto"
1916
- ? "Start the local companion or fix tools.web.search.endpoint; auto mode can still fall back to Codex subscription search, then keyless search."
1917
- : "Start the local companion or fix tools.web.search.endpoint; strict SearXNG mode has no fallback."));
1528
+ ? "Start the local companion or fix tools.web.search.searxng.endpoint; auto mode can still fall back to Codex subscription search, then keyless search."
1529
+ : "Start the local companion or fix tools.web.search.searxng.endpoint; strict SearXNG mode has no fallback."));
1918
1530
  }
1919
1531
  }
1920
1532
  }
1533
+ if (search.backend === "ollama" || (search.backend === "auto" && search.ollama !== undefined)) {
1534
+ const ollama = search.ollama;
1535
+ if (ollama === undefined) {
1536
+ status = "waiting";
1537
+ details.push("[WARN] Ollama Web Search is selected but its resolved configuration is missing.");
1538
+ }
1539
+ else if (!liveness) {
1540
+ details.push(`Ollama Web Search origin: ${ollama.baseUrl}. ${search.backend === "ollama" ? "Strict Ollama mode has no fallback." : `Auto mode advances to ${autoAfterOllama} when Ollama is unavailable.`}`);
1541
+ details.push("Ollama Web Search liveness was not probed.");
1542
+ }
1543
+ else {
1544
+ details.push(`Ollama Web Search origin: ${ollama.baseUrl}. ${search.backend === "ollama" ? "Strict Ollama mode has no fallback." : `Auto mode advances to ${autoAfterOllama} when Ollama is unavailable.`}`);
1545
+ const probe = await probeOllamaWebSearch(ollama);
1546
+ if (probe.ok)
1547
+ details.push("Ollama Web Search JSON probe succeeded.");
1548
+ else {
1549
+ status = "waiting";
1550
+ details.push(`[WARN] Ollama Web Search probe failed (${probe.reason}). ${search.backend === "ollama" ? "Strict Ollama mode has no fallback." : `Auto mode can still advance to ${autoAfterOllama}.`}`);
1551
+ }
1552
+ }
1553
+ }
1554
+ else if (search.ollama !== undefined) {
1555
+ details.push(`Ollama Web Search is configured but inactive because backend ${search.backend} is strict.`);
1556
+ }
1921
1557
  if (search.backend === "auto") {
1922
1558
  const codexModel = search.codex?.model ?? "gpt-5.6-luna";
1923
- details.push(`Codex subscription fallback model: ${codexModel}; readiness is checked lazily when auto mode reaches that fallback.`);
1559
+ const beforeCodex = autoEligibleBackends.slice(0, autoEligibleBackends.indexOf("Codex"));
1560
+ const codexPosition = beforeCodex.length === 0
1561
+ ? "as the first eligible backend"
1562
+ : `after ${formatSearchBackendChain(beforeCodex)}`;
1563
+ details.push(`Codex subscription fallback model: ${codexModel}; readiness is checked lazily when auto mode reaches it ${codexPosition}.`);
1924
1564
  }
1925
1565
  else if (search.backend === "codex") {
1926
1566
  const codexModel = search.codex?.model ?? "gpt-5.6-luna";
@@ -1960,6 +1600,95 @@ async function webToolsSection(config, input, liveness) {
1960
1600
  }
1961
1601
  return { id: "web-tools", label: "Web search & fetch", status, details };
1962
1602
  }
1603
+ async function probeOllamaWebSearch(config) {
1604
+ const ctrl = new AbortController();
1605
+ const timer = setTimeout(() => { ctrl.abort(); }, LIVENESS_PROBE_TIMEOUT_MS);
1606
+ const official = config.baseUrl === "https://ollama.com";
1607
+ const paths = official ? ["/api/web_search"] : ["/api/experimental/web_search", "/api/web_search"];
1608
+ try {
1609
+ for (let index = 0; index < paths.length; index += 1) {
1610
+ const response = await fetch(`${config.baseUrl}${paths[index]}`, {
1611
+ method: "POST",
1612
+ headers: {
1613
+ Accept: "application/json",
1614
+ "Content-Type": "application/json",
1615
+ ...(official ? { Authorization: `Bearer ${config.apiKey}` } : {}),
1616
+ },
1617
+ body: JSON.stringify({ query: "mono-agent doctor", max_results: 1 }),
1618
+ redirect: "error",
1619
+ signal: ctrl.signal,
1620
+ });
1621
+ if (!official && index === 0 && [404, 405].includes(response.status)) {
1622
+ await response.body?.cancel();
1623
+ continue;
1624
+ }
1625
+ if ([401, 403].includes(response.status)) {
1626
+ await response.body?.cancel();
1627
+ return { ok: false, reason: official
1628
+ ? `hosted authentication failed; check ${config.apiKeyEnv}`
1629
+ : "the local proxy requires sign-in; run ollama signin or repair its proxy authentication" };
1630
+ }
1631
+ if (!response.ok) {
1632
+ await response.body?.cancel();
1633
+ return { ok: false, reason: `HTTP ${response.status}` };
1634
+ }
1635
+ const declared = Number(response.headers.get("content-length"));
1636
+ if (Number.isFinite(declared) && declared > OLLAMA_WEB_SEARCH_PROBE_MAX_BYTES) {
1637
+ await response.body?.cancel();
1638
+ return { ok: false, reason: `response exceeded ${OLLAMA_WEB_SEARCH_PROBE_MAX_BYTES} bytes` };
1639
+ }
1640
+ const bytes = await readBoundedProbeResponse(response, OLLAMA_WEB_SEARCH_PROBE_MAX_BYTES);
1641
+ let body;
1642
+ try {
1643
+ body = JSON.parse(new TextDecoder().decode(bytes));
1644
+ }
1645
+ catch {
1646
+ return { ok: false, reason: "response was not valid JSON" };
1647
+ }
1648
+ if (!body || typeof body !== "object" || !Array.isArray(body.results)) {
1649
+ return { ok: false, reason: "response did not contain a results array" };
1650
+ }
1651
+ return { ok: true, reason: "" };
1652
+ }
1653
+ return { ok: false, reason: "this Ollama version supports neither local Web Search endpoint" };
1654
+ }
1655
+ catch (error) {
1656
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
1657
+ }
1658
+ finally {
1659
+ clearTimeout(timer);
1660
+ }
1661
+ }
1662
+ async function readBoundedProbeResponse(response, maxBytes) {
1663
+ const reader = response.body?.getReader();
1664
+ if (reader === undefined)
1665
+ throw new Error("response body could not be read safely");
1666
+ const chunks = [];
1667
+ let total = 0;
1668
+ try {
1669
+ while (true) {
1670
+ const next = await reader.read();
1671
+ if (next.done)
1672
+ break;
1673
+ total += next.value.byteLength;
1674
+ if (total > maxBytes) {
1675
+ await reader.cancel();
1676
+ throw new Error(`response exceeded ${maxBytes} bytes`);
1677
+ }
1678
+ chunks.push(next.value);
1679
+ }
1680
+ }
1681
+ finally {
1682
+ reader.releaseLock();
1683
+ }
1684
+ const joined = new Uint8Array(total);
1685
+ let offset = 0;
1686
+ for (const chunk of chunks) {
1687
+ joined.set(chunk, offset);
1688
+ offset += chunk.byteLength;
1689
+ }
1690
+ return joined;
1691
+ }
1963
1692
  async function probeSearxngEndpoint(endpoint) {
1964
1693
  const ctrl = new AbortController();
1965
1694
  const timer = setTimeout(() => { ctrl.abort(); }, LIVENESS_PROBE_TIMEOUT_MS);
@@ -2001,14 +1730,14 @@ async function probeSearxngEndpoint(endpoint) {
2001
1730
  }
2002
1731
  }
2003
1732
  async function readAgentBrowserVersion(command, input) {
2004
- const run = input.sdkAuthStatusExecFile ?? defaultSdkAuthStatusExecFile;
1733
+ const run = input.statusExecFile ?? defaultDoctorStatusExecFile;
2005
1734
  try {
2006
1735
  const { stdout } = await run(command, ["--version"], {
2007
1736
  cwd: input.cwd,
2008
1737
  env: Object.fromEntries(["PATH", "LANG", "LC_ALL", "LC_CTYPE", "SHELL", "TMPDIR", "TMP", "TEMP"]
2009
1738
  .flatMap((key) => typeof input.env[key] === "string" ? [[key, input.env[key]]] : [])),
2010
- timeout: SDK_AUTH_STATUS_TIMEOUT_MS,
2011
- maxBuffer: SDK_AUTH_STATUS_MAX_BUFFER_BYTES,
1739
+ timeout: DOCTOR_STATUS_TIMEOUT_MS,
1740
+ maxBuffer: DOCTOR_STATUS_MAX_BUFFER_BYTES,
2012
1741
  encoding: "utf8",
2013
1742
  });
2014
1743
  const match = /(?:^|\s)v?(\d+)\.(\d+)\.(\d+)(?:\s|$)/u.exec(stdout.trim());
@@ -2166,7 +1895,7 @@ async function processJobsSection(config, input) {
2166
1895
  `Owner-only local state: ${settings.stateDir}.`,
2167
1896
  `Concurrency: ${String(settings.maxConcurrent)} global, ${String(settings.maxActivePerConversation)} per conversation, ${String(settings.maxQueued)} queued.`,
2168
1897
  `Caps: runtime=${String(settings.maxRuntimeMs)}ms, queue-age=${String(settings.maxQueueAgeMs)}ms, output=${String(settings.maxOutputBytes)} bytes, chain-depth=${String(settings.maxChainDepth)}.`,
2169
- `Runtime availability: Pi-native Exec/Bash only; configured primary is ${config.runtime.model.sdk}.`,
1898
+ `Runtime availability: Pi-native Exec/Bash only; configured primary provider is ${displayText(config.runtime.model.provider)}.`,
2170
1899
  ];
2171
1900
  const inspection = await inspectProcessJobState(input.cwd, settings.stateDir);
2172
1901
  return {
@@ -3205,7 +2934,7 @@ function formatContinuationHost(host) {
3205
2934
  * needs the remote adapter API (or the app-owned interaction bridge) admitted
3206
2935
  * by sandbox.network.
3207
2936
  */
3208
- async function adapterSendToolNetworkPolicyWarnings(config, input, settings, suppressInteractionTools) {
2937
+ async function adapterSendToolNetworkPolicyWarnings(config, input, settings) {
3209
2938
  if (config.sandbox === undefined || config.sandbox.mode !== "native") {
3210
2939
  return [];
3211
2940
  }
@@ -3225,7 +2954,7 @@ async function adapterSendToolNetworkPolicyWarnings(config, input, settings, sup
3225
2954
  url: settings.telegram.apiRoot ?? "https://api.telegram.org",
3226
2955
  });
3227
2956
  }
3228
- const askUserAllowed = !suppressInteractionTools && isAdapterSendToolAllowed("AskUser", {
2957
+ const askUserAllowed = isAdapterSendToolAllowed("AskUser", {
3229
2958
  allowedTools: config.tools.allowedTools,
3230
2959
  disallowedTools: config.tools.disallowedTools,
3231
2960
  });
@@ -3273,100 +3002,7 @@ function endpointHost(url) {
3273
3002
  return url;
3274
3003
  }
3275
3004
  }
3276
- function effectiveMcpRuntimeSources(config, configuredMcpServerNames, adapterToolNames) {
3277
- const sources = [];
3278
- if (configuredMcpServerNames.length > 0) {
3279
- sources.push(`tools.mcpConfigPath (${configuredMcpServerNames.join(", ")})`);
3280
- }
3281
- if (config.memory?.recallTool?.enabled === true) {
3282
- sources.push("memory.recallTool");
3283
- }
3284
- if (config.memory?.backend === "supermemory"
3285
- && config.memory.supermemory?.exposeMcpServer === true
3286
- && config.memory.supermemory.apiKey !== undefined) {
3287
- sources.push("memory.supermemory.exposeMcpServer");
3288
- }
3289
- if (adapterToolNames.length > 0) {
3290
- sources.push(`adapter send tools (${adapterToolNames.join(", ")})`);
3291
- }
3292
- return sources;
3293
- }
3294
3005
  async function sandboxSection(config, engine) {
3295
- const runtimeModels = configuredRuntimeModels(config.runtime);
3296
- const directCodexRefs = runtimeModels
3297
- .filter((model) => model.sdk === "codex")
3298
- .map((model) => model.reference ?? `codex:${model.model}`);
3299
- const claudeRefs = runtimeModels
3300
- .filter((model) => model.sdk === "claude")
3301
- .map((model) => model.reference ?? `claude:${model.model}`);
3302
- const directOpenCodeRefs = runtimeModels
3303
- .filter((model) => model.sdk === "opencode")
3304
- .map((model) => model.reference ?? `opencode:${model.provider ?? "unknown"}:${model.model}`);
3305
- const routeSafety = config.runtime.routeSafety ?? "uniform";
3306
- if (routeSafety === "per-route-native") {
3307
- const monoSandboxActive = config.sandbox !== undefined && config.sandbox.mode !== "off";
3308
- const piRoutes = runtimeModels.filter((model) => model.sdk === "pi");
3309
- const details = runtimeModels.map((model, index) => routeNativeSafetyDetail(model, index, config));
3310
- let status = !monoSandboxActive && piRoutes.length > 0 ? "disabled" : "ok";
3311
- if (monoSandboxActive && piRoutes.length > 0) {
3312
- const state = await resolveSandboxEffectiveState({
3313
- policy: config.sandbox,
3314
- ...(engine === undefined ? {} : { engine }),
3315
- });
3316
- const warning = sandboxEffectiveStateWarning(state);
3317
- details.push(`Pi route SRT policy: mode ${config.sandbox.mode}, network ${config.sandbox.network.mode}, fallback ${config.sandbox.fallback}.`, describeSandboxEffectiveState(state), ...(warning === undefined ? [] : [warning]));
3318
- if (warning !== undefined || state.effective === "blocked")
3319
- status = "waiting";
3320
- }
3321
- else if (piRoutes.length > 0) {
3322
- details.push("Pi route SRT policy: disabled; Bash and stdio MCP subprocesses run unsandboxed.");
3323
- }
3324
- else if (monoSandboxActive) {
3325
- details.push("The configured mono-agent SRT policy has no Pi route to enforce it; provider-owned route contracts below apply instead.");
3326
- status = "waiting";
3327
- }
3328
- if (monoSandboxActive && runtimeModels.some((model) => model.sdk !== "pi")) {
3329
- details.push("[WARN] Per-route-native explicitly does not project mono-agent readableRoots, writableRoots, denyWrite, or network rules onto non-Pi routes; review each route contract before start.");
3330
- status = "waiting";
3331
- }
3332
- if (runtimeModels.some((model) => model.sdk === "codex") && config.runtime.permissionMode === "bypassPermissions") {
3333
- status = "waiting";
3334
- }
3335
- return { id: "sandbox", label: "Sandbox", status, details };
3336
- }
3337
- const incompatibleDetails = [];
3338
- if (config.sandbox !== undefined && config.sandbox.mode !== "off" && directCodexRefs.length > 0) {
3339
- const codexPosture = directCodexSandboxPosture(config.runtime.permissionMode);
3340
- incompatibleDetails.push(`Native mono-agent sandbox policy cannot govern direct Codex runtime${directCodexRefs.length === 1 ? "" : "s"}: ${directCodexRefs.join(", ")}.`, `${codexPosture.detail} Remove the mono-agent sandbox block or use Pi when exact srt roots, denyWrite, or network policy are required.`);
3341
- }
3342
- if (config.sandbox !== undefined && config.sandbox.mode !== "off" && claudeRefs.length > 0) {
3343
- incompatibleDetails.push(`Native mono-agent sandbox policy cannot govern Claude runtime${claudeRefs.length === 1 ? "" : "s"}: ${claudeRefs.join(", ")}.`, "Claude's provider-owned tool loop does not consume mono-agent sandboxPolicy. Set sandbox.mode to off, remove the sandbox block, or use a Pi runtime when mono-agent srt enforcement is required.");
3344
- }
3345
- if (config.sandbox !== undefined && config.sandbox.mode !== "off" && directOpenCodeRefs.length > 0) {
3346
- incompatibleDetails.push(`Native mono-agent sandbox policy cannot govern direct OpenCode runtime${directOpenCodeRefs.length === 1 ? "" : "s"}: ${directOpenCodeRefs.join(", ")}.`, "Direct OpenCode's provider-owned tool loop does not consume mono-agent sandboxPolicy. Set sandbox.mode to off, remove the sandbox block, or use a pi:opencode-go:* runtime when mono-agent srt enforcement is required.");
3347
- }
3348
- if (incompatibleDetails.length > 0) {
3349
- return {
3350
- id: "sandbox",
3351
- label: "Sandbox",
3352
- status: "error",
3353
- details: incompatibleDetails,
3354
- };
3355
- }
3356
- if (directCodexRefs.length > 0 && (config.sandbox === undefined || config.sandbox.mode === "off")) {
3357
- const posture = directCodexSandboxPosture(config.runtime.permissionMode);
3358
- return {
3359
- id: "sandbox",
3360
- label: "Sandbox",
3361
- status: posture.status,
3362
- details: [
3363
- posture.detail,
3364
- config.sandbox === undefined
3365
- ? "No mono-agent native srt policy is configured."
3366
- : "The mono-agent native srt policy is explicitly off; the Codex-native posture still applies.",
3367
- ],
3368
- };
3369
- }
3370
3006
  if (config.sandbox === undefined) {
3371
3007
  return { id: "sandbox", label: "Sandbox", status: "disabled", details: ["No sandbox policy configured."] };
3372
3008
  }
@@ -3394,44 +3030,6 @@ async function sandboxSection(config, engine) {
3394
3030
  details,
3395
3031
  };
3396
3032
  }
3397
- function routeNativeSafetyDetail(model, index, config) {
3398
- const label = index === 0 ? "Primary" : `Fallback ${index}`;
3399
- const ref = referenceOf(model);
3400
- if (model.sdk === "pi") {
3401
- if (config.sandbox === undefined || config.sandbox.mode === "off") {
3402
- return `${label} ${ref}: Pi-owned tools use mono-agent tool policy; SRT is disabled, so Bash and stdio MCP subprocesses run unsandboxed.`;
3403
- }
3404
- if (config.sandbox.fallback === "unsafe-host-process") {
3405
- return `${label} ${ref}: Pi-owned tools use the configured mono-agent SRT policy; its explicit unsafe-host-process fallback can run subprocesses unsandboxed when SRT is unavailable.`;
3406
- }
3407
- return `${label} ${ref}: Pi-owned tools use the configured mono-agent SRT policy and fail closed when it is unavailable.`;
3408
- }
3409
- if (model.sdk === "claude") {
3410
- return `${label} ${ref}: Claude provider-owned permissions apply; mono-agent SRT filesystem/network rules are not projected.`;
3411
- }
3412
- if (model.sdk === "codex") {
3413
- return `${label} ${ref}: ${directCodexSandboxPosture(config.runtime.permissionMode).detail}`;
3414
- }
3415
- return `${label} ${ref}: OpenCode provider-owned execution with an effective allow-all tool policy applies; mono-agent SRT rules are not projected.`;
3416
- }
3417
- function directCodexSandboxPosture(permissionMode) {
3418
- if (permissionMode === "bypassPermissions") {
3419
- return {
3420
- status: "waiting",
3421
- detail: "[WARN] Direct Codex bypassPermissions uses native danger-full-access with no filesystem or network sandbox; unattended approval prompts are still disabled.",
3422
- };
3423
- }
3424
- if (permissionMode === "plan") {
3425
- return {
3426
- status: "ok",
3427
- detail: "Direct Codex plan mode uses its native read-only sandbox with network disabled; unattended escalation requests are denied.",
3428
- };
3429
- }
3430
- return {
3431
- status: "ok",
3432
- detail: "Direct Codex default/acceptEdits mode uses its native workspace-write sandbox with network disabled; unattended escalation requests are denied.",
3433
- };
3434
- }
3435
3033
  async function channelSection(driver, input) {
3436
3034
  const id = `channel:${driver.id}`;
3437
3035
  try {
@@ -3464,6 +3062,44 @@ async function channelSection(driver, input) {
3464
3062
  function referenceOf(model) {
3465
3063
  return modelReferenceKey(model);
3466
3064
  }
3065
+ /**
3066
+ * Bounded, single-line rendering of operator-supplied text for a diagnostic.
3067
+ *
3068
+ * `referenceOf` is the canonical KEY -- it is compared against
3069
+ * `verifiedCredentialModelRefs`, which carries whole references -- so it must
3070
+ * stay unbounded. What gets PRINTED must not be. The parser deliberately has no
3071
+ * length ceiling any more (a reference cannot be truncated into validity, so a
3072
+ * ceiling there could only refuse a model that runs), which means acceptance
3073
+ * stopped being a length guarantee: a 500,000-byte reference parses today and
3074
+ * every one of these lines is echoed by `mono-agent validate`, the daemon log
3075
+ * and launchd's captured stdout. Escaping comes with the clamp, so a reference
3076
+ * carrying a newline also cannot forge a detail line that reads as doctor's own.
3077
+ */
3078
+ function displayText(value) {
3079
+ return sanitizeModelReferenceText(value, MODEL_REFERENCE_ECHO_MAX_BYTES);
3080
+ }
3081
+ /** Bounded display form of a model reference. Never use it as a map or set key. */
3082
+ function displayReferenceOf(model) {
3083
+ return displayText(referenceOf(model));
3084
+ }
3085
+ /**
3086
+ * Bounded display form of a thrown reason. A reason gets the larger budget because it is one
3087
+ * fixed repair sentence plus at most one echo of the value.
3088
+ *
3089
+ * Unwrap one layer first, as `modelReferenceReason` in @mono-agent/config and `reasonOf` in
3090
+ * trigger-overrides already do: the adapter's `message` wraps a 32-byte generic prefix around
3091
+ * a reason ALREADY at the full reason budget, so clamping the wrapped form spends the budget
3092
+ * on the prefix and pays for it out of the tail -- which is where the kernel parser puts the
3093
+ * concrete repair whenever it quotes the operator's value first.
3094
+ */
3095
+ function displayReason(error) {
3096
+ const reason = error instanceof RuntimeAdapterError && typeof error.details.reason === "string"
3097
+ ? error.details.reason
3098
+ : error instanceof Error
3099
+ ? error.message
3100
+ : String(error);
3101
+ return sanitizeModelReferenceText(reason, MODEL_REFERENCE_REASON_MAX_BYTES);
3102
+ }
3467
3103
  async function pathExists(path) {
3468
3104
  try {
3469
3105
  await stat(path);