create-walle 0.9.21 → 0.9.23

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 (500) hide show
  1. package/README.md +27 -5
  2. package/package.json +2 -2
  3. package/template/CLAUDE.md +2 -2
  4. package/template/LICENSE +1 -1
  5. package/template/bin/ctm-dev-cleanup.js +24 -3
  6. package/template/bin/ctm-launch.sh +13 -0
  7. package/template/bin/dev.sh +156 -18
  8. package/template/bin/node-bin.sh +84 -0
  9. package/template/bin/pin-node.sh +51 -0
  10. package/template/claude-task-manager/api-prompts.js +1203 -182
  11. package/template/claude-task-manager/api-reviews.js +109 -15
  12. package/template/claude-task-manager/approval-agent.js +1360 -280
  13. package/template/claude-task-manager/bin/restart-ctm.sh +64 -23
  14. package/template/claude-task-manager/bin/storage-migration-supervisor.js +338 -0
  15. package/template/claude-task-manager/db.js +4417 -295
  16. package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
  17. package/template/claude-task-manager/docs/approval-ai-refinement.md +138 -0
  18. package/template/claude-task-manager/docs/approval-rescue-loop.md +74 -0
  19. package/template/claude-task-manager/docs/codex-operational-warning-health.md +107 -0
  20. package/template/claude-task-manager/docs/codex-resume-state-guard-design.md +17 -12
  21. package/template/claude-task-manager/docs/codex-terminal-render-controller-handoff.md +311 -0
  22. package/template/claude-task-manager/docs/coding-agent-hooks-architecture.md +418 -0
  23. package/template/claude-task-manager/docs/conversation-import-freshness.md +20 -0
  24. package/template/claude-task-manager/docs/google-workspace-auth-health.md +77 -0
  25. package/template/claude-task-manager/docs/image-paste-ux.md +13 -0
  26. package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
  27. package/template/claude-task-manager/docs/main-loop-offload-architecture.md +66 -0
  28. package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +274 -519
  29. package/template/claude-task-manager/docs/mobile-live-streaming.md +27 -5
  30. package/template/claude-task-manager/docs/mobile-remote-submission-lifecycle.md +69 -0
  31. package/template/claude-task-manager/docs/phone-access-design.md +53 -15
  32. package/template/claude-task-manager/docs/phone-passkey-identity.md +122 -0
  33. package/template/claude-task-manager/docs/phone-setup.md +3 -0
  34. package/template/claude-task-manager/docs/prompt-editing-tree-design.md +25 -1
  35. package/template/claude-task-manager/docs/remote-desktop-access-design.md +268 -0
  36. package/template/claude-task-manager/docs/restart-lifecycle-architecture.md +95 -0
  37. package/template/claude-task-manager/docs/runtime-work-control-plane.md +53 -0
  38. package/template/claude-task-manager/docs/session-interactive-wait-surfaces.md +38 -0
  39. package/template/claude-task-manager/docs/session-needs-you-dismissal.md +84 -0
  40. package/template/claude-task-manager/docs/session-render-state-management-design.md +91 -3
  41. package/template/claude-task-manager/docs/session-standup-command-center-design.md +25 -1
  42. package/template/claude-task-manager/docs/session-title-authority.md +32 -0
  43. package/template/claude-task-manager/docs/session-workspace-binding.md +33 -0
  44. package/template/claude-task-manager/docs/skill-intent-resolution-design.md +72 -0
  45. package/template/claude-task-manager/docs/walle-mcp-supervisor-health.md +86 -0
  46. package/template/claude-task-manager/docs/walle-relay-phone-access-design.md +24 -15
  47. package/template/claude-task-manager/docs/walle-session-history-hydration.md +114 -0
  48. package/template/claude-task-manager/docs/walle-session-input-queue.md +104 -0
  49. package/template/claude-task-manager/docs/walle-session-model-catalog.md +90 -0
  50. package/template/claude-task-manager/docs/walle-session-model-preferences.md +15 -6
  51. package/template/claude-task-manager/git-utils.js +897 -27
  52. package/template/claude-task-manager/lib/agent-capabilities.js +33 -0
  53. package/template/claude-task-manager/lib/agent-cli-cache.js +37 -7
  54. package/template/claude-task-manager/lib/agent-hooks-installer.js +26 -2
  55. package/template/claude-task-manager/lib/agent-presets.js +17 -1
  56. package/template/claude-task-manager/lib/all-sessions-query.js +108 -0
  57. package/template/claude-task-manager/lib/approval-ai-refinement.js +488 -0
  58. package/template/claude-task-manager/lib/approval-self-adapt.js +168 -0
  59. package/template/claude-task-manager/lib/async-semaphore.js +44 -0
  60. package/template/claude-task-manager/lib/auth-context.js +5 -0
  61. package/template/claude-task-manager/lib/auth-rate-limit.js +47 -4
  62. package/template/claude-task-manager/lib/auth-rules.js +29 -2
  63. package/template/claude-task-manager/lib/auto-approval-verifier.js +129 -16
  64. package/template/claude-task-manager/lib/background-llm.js +144 -17
  65. package/template/claude-task-manager/lib/branch-inventory.js +212 -0
  66. package/template/claude-task-manager/lib/claude-desktop-sessions.js +15 -3
  67. package/template/claude-task-manager/lib/coalesce-sync-frames.js +151 -0
  68. package/template/claude-task-manager/lib/codex-launch-health.js +762 -0
  69. package/template/claude-task-manager/lib/codex-transcript-pager.js +51 -0
  70. package/template/claude-task-manager/lib/codex-zst.js +124 -0
  71. package/template/claude-task-manager/lib/coding-agent-models.js +233 -30
  72. package/template/claude-task-manager/lib/connection-health.js +232 -0
  73. package/template/claude-task-manager/lib/conversation-blob-parser.js +42 -0
  74. package/template/claude-task-manager/lib/conversation-tail-merge.js +89 -26
  75. package/template/claude-task-manager/lib/ctm-session-context-api.js +39 -10
  76. package/template/claude-task-manager/lib/cursor-conversation-store.js +354 -0
  77. package/template/claude-task-manager/lib/db-owner-worker-client.js +315 -0
  78. package/template/claude-task-manager/lib/document-review.js +141 -6
  79. package/template/claude-task-manager/lib/escalation-review.js +152 -0
  80. package/template/claude-task-manager/lib/graceful-shutdown.js +159 -0
  81. package/template/claude-task-manager/lib/headless-term-service.js +678 -0
  82. package/template/claude-task-manager/lib/heavy-worker-fallback.js +38 -0
  83. package/template/claude-task-manager/lib/jsonl-conversation-parser.js +542 -0
  84. package/template/claude-task-manager/lib/jsonl-range-reader.js +112 -0
  85. package/template/claude-task-manager/lib/main-db-census.js +216 -0
  86. package/template/claude-task-manager/lib/message-pagination.js +106 -4
  87. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +750 -26
  88. package/template/claude-task-manager/lib/mobile-auth-api.js +274 -7
  89. package/template/claude-task-manager/lib/mobile-auth-store.js +592 -10
  90. package/template/claude-task-manager/lib/mobile-notification-dispatcher.js +15 -0
  91. package/template/claude-task-manager/lib/model-overview-brain-fallback.js +311 -0
  92. package/template/claude-task-manager/lib/model-overview-cache.js +141 -0
  93. package/template/claude-task-manager/lib/models-health-routing-notice.js +126 -0
  94. package/template/claude-task-manager/lib/node-pin-guard.js +93 -0
  95. package/template/claude-task-manager/lib/perf-tracker.js +242 -6
  96. package/template/claude-task-manager/lib/permission-match.js +76 -0
  97. package/template/claude-task-manager/lib/permission-sync.js +133 -20
  98. package/template/claude-task-manager/lib/process-title.js +35 -0
  99. package/template/claude-task-manager/lib/prompt-executions-query.js +25 -0
  100. package/template/claude-task-manager/lib/prompt-index-disk-cache.js +44 -0
  101. package/template/claude-task-manager/lib/prompt-intent.js +132 -0
  102. package/template/claude-task-manager/lib/provider-user-context.js +34 -0
  103. package/template/claude-task-manager/lib/read-pool-client.js +313 -0
  104. package/template/claude-task-manager/lib/readpool-breaker.js +31 -0
  105. package/template/claude-task-manager/lib/recent-sessions-breaker.js +12 -0
  106. package/template/claude-task-manager/lib/remote-feedback-client.js +72 -0
  107. package/template/claude-task-manager/lib/remote-relay-protocol.js +37 -4
  108. package/template/claude-task-manager/lib/remote-relay-store.js +159 -0
  109. package/template/claude-task-manager/lib/remote-submission-observer.js +278 -0
  110. package/template/claude-task-manager/lib/restart-guard.js +109 -0
  111. package/template/claude-task-manager/lib/restore-interruption-detector.js +439 -0
  112. package/template/claude-task-manager/lib/restore-policy.js +13 -0
  113. package/template/claude-task-manager/lib/restore-resume-batch.js +74 -0
  114. package/template/claude-task-manager/lib/restore-runtime.js +68 -0
  115. package/template/claude-task-manager/lib/restore-storm.js +34 -0
  116. package/template/claude-task-manager/lib/resume-cwd.js +36 -0
  117. package/template/claude-task-manager/lib/resume-preflight.js +313 -0
  118. package/template/claude-task-manager/lib/runtime-work-registry.js +444 -0
  119. package/template/claude-task-manager/lib/sanitize-openai-auth.js +31 -0
  120. package/template/claude-task-manager/lib/scheduler.js +21 -1
  121. package/template/claude-task-manager/lib/scrollback-snapshot-store.js +159 -0
  122. package/template/claude-task-manager/lib/serial-task-queue.js +64 -0
  123. package/template/claude-task-manager/lib/server-listeners.js +239 -0
  124. package/template/claude-task-manager/lib/session-capture.js +42 -7
  125. package/template/claude-task-manager/lib/session-content-backfill.js +131 -0
  126. package/template/claude-task-manager/lib/session-history.js +388 -43
  127. package/template/claude-task-manager/lib/session-host-manager.js +287 -0
  128. package/template/claude-task-manager/lib/session-image-refs.js +209 -0
  129. package/template/claude-task-manager/lib/session-jobs.js +399 -59
  130. package/template/claude-task-manager/lib/session-prompt-index.js +137 -0
  131. package/template/claude-task-manager/lib/session-restore.js +53 -0
  132. package/template/claude-task-manager/lib/session-standup.js +123 -23
  133. package/template/claude-task-manager/lib/session-state-bus.js +14 -0
  134. package/template/claude-task-manager/lib/session-stream.js +64 -16
  135. package/template/claude-task-manager/lib/session-timeline-summary.js +260 -0
  136. package/template/claude-task-manager/lib/session-token-usage.js +494 -0
  137. package/template/claude-task-manager/lib/session-workspace-binding.js +356 -0
  138. package/template/claude-task-manager/lib/setup-network-config.js +9 -0
  139. package/template/claude-task-manager/lib/size-cap.js +45 -0
  140. package/template/claude-task-manager/lib/size-cap.test.js +62 -0
  141. package/template/claude-task-manager/lib/skill-autocomplete.js +180 -1
  142. package/template/claude-task-manager/lib/skill-intent-resolver.js +304 -0
  143. package/template/claude-task-manager/lib/sqlite-driver.js +19 -3
  144. package/template/claude-task-manager/lib/standup-attention.js +7 -3
  145. package/template/claude-task-manager/lib/status-authority.js +39 -0
  146. package/template/claude-task-manager/lib/status-hooks.js +4 -0
  147. package/template/claude-task-manager/lib/storage-migration.js +235 -0
  148. package/template/claude-task-manager/lib/structured-capture.js +298 -0
  149. package/template/claude-task-manager/lib/sync-io-census.js +163 -0
  150. package/template/claude-task-manager/lib/tailscale-setup.js +6 -0
  151. package/template/claude-task-manager/lib/terminal-activity-evidence.js +33 -0
  152. package/template/claude-task-manager/lib/terminal-choice.js +364 -0
  153. package/template/claude-task-manager/lib/terminal-control-sanitize.js +17 -0
  154. package/template/claude-task-manager/lib/terminal-fingerprint.js +48 -0
  155. package/template/claude-task-manager/lib/terminal-output-flush.js +84 -0
  156. package/template/claude-task-manager/lib/timeline-order.js +122 -0
  157. package/template/claude-task-manager/lib/transcript-store.js +348 -43
  158. package/template/claude-task-manager/lib/transport-security.js +84 -1
  159. package/template/claude-task-manager/lib/wait-state.js +184 -0
  160. package/template/claude-task-manager/lib/walle-client.js +47 -5
  161. package/template/claude-task-manager/lib/walle-ctm-history.js +564 -4
  162. package/template/claude-task-manager/lib/walle-external-actions.js +135 -16
  163. package/template/claude-task-manager/lib/walle-history-hydration.js +46 -0
  164. package/template/claude-task-manager/lib/walle-native-health.js +403 -0
  165. package/template/claude-task-manager/lib/walle-repair.js +701 -0
  166. package/template/claude-task-manager/lib/walle-session-cache.js +109 -0
  167. package/template/claude-task-manager/lib/walle-session-context.js +57 -21
  168. package/template/claude-task-manager/lib/walle-session-model-catalog.js +34 -0
  169. package/template/claude-task-manager/lib/walle-supervisor.js +539 -63
  170. package/template/claude-task-manager/lib/walle-transcript.js +52 -0
  171. package/template/claude-task-manager/lib/worktree-active-sync.js +11 -7
  172. package/template/claude-task-manager/lib/worktree-cwd.js +32 -1
  173. package/template/claude-task-manager/package.json +1 -1
  174. package/template/claude-task-manager/prompt-harvest.js +89 -66
  175. package/template/claude-task-manager/providers/claude-code.js +51 -3
  176. package/template/claude-task-manager/providers/cursor.js +140 -45
  177. package/template/claude-task-manager/public/css/reviews.css +551 -61
  178. package/template/claude-task-manager/public/css/setup.css +191 -0
  179. package/template/claude-task-manager/public/css/walle-session.css +865 -10
  180. package/template/claude-task-manager/public/css/walle.css +154 -0
  181. package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
  182. package/template/claude-task-manager/public/index.html +18516 -2058
  183. package/template/claude-task-manager/public/ipad.html +363 -0
  184. package/template/claude-task-manager/public/js/document-review-links.js +301 -0
  185. package/template/claude-task-manager/public/js/image-normalize.js +69 -36
  186. package/template/claude-task-manager/public/js/message-renderer.js +1265 -77
  187. package/template/claude-task-manager/public/js/prompts.js +66 -29
  188. package/template/claude-task-manager/public/js/reviews.js +901 -133
  189. package/template/claude-task-manager/public/js/session-activity-utils.js +11 -1
  190. package/template/claude-task-manager/public/js/session-search-utils.js +94 -10
  191. package/template/claude-task-manager/public/js/session-status-precedence.js +23 -5
  192. package/template/claude-task-manager/public/js/setup.js +1273 -176
  193. package/template/claude-task-manager/public/js/stream-view.js +691 -73
  194. package/template/claude-task-manager/public/js/terminal-reconciler.js +210 -0
  195. package/template/claude-task-manager/public/js/walle-session.js +2455 -158
  196. package/template/claude-task-manager/public/js/walle.js +455 -28
  197. package/template/claude-task-manager/public/m/app.css +2909 -262
  198. package/template/claude-task-manager/public/m/app.js +6601 -398
  199. package/template/claude-task-manager/public/m/claim.html +224 -17
  200. package/template/claude-task-manager/public/m/index.html +117 -21
  201. package/template/claude-task-manager/public/m/sw.js +3 -1
  202. package/template/claude-task-manager/public/manifest.json +2 -2
  203. package/template/claude-task-manager/public/prompts.html +30 -14
  204. package/template/claude-task-manager/queue-engine.js +507 -28
  205. package/template/claude-task-manager/scripts/repair-claude-session-images.js +27 -8
  206. package/template/claude-task-manager/server.js +14341 -2197
  207. package/template/claude-task-manager/session-integrity.js +160 -18
  208. package/template/claude-task-manager/session-search-ranking.js +1 -0
  209. package/template/claude-task-manager/session-utils.js +25 -5
  210. package/template/claude-task-manager/workers/approval-blocklist.js +96 -6
  211. package/template/claude-task-manager/workers/approval-widget-validator.js +14 -8
  212. package/template/claude-task-manager/workers/conversation-import-worker.js +11 -50
  213. package/template/claude-task-manager/workers/db-owner-worker.js +386 -0
  214. package/template/claude-task-manager/workers/harvest-worker.js +9 -55
  215. package/template/claude-task-manager/workers/headless-term-worker.js +9 -530
  216. package/template/claude-task-manager/workers/read-pool-worker.js +387 -0
  217. package/template/claude-task-manager/workers/scrollback-worker.js +11 -72
  218. package/template/claude-task-manager/workers/session-host-process.js +146 -0
  219. package/template/claude-task-manager/workers/session-integrity-worker.js +10 -54
  220. package/template/claude-task-manager/workers/state-detectors/base.js +18 -1
  221. package/template/claude-task-manager/workers/state-detectors/claude-code.js +182 -9
  222. package/template/claude-task-manager/workers/state-detectors/codex.js +150 -2
  223. package/template/claude-task-manager/workers/state-detectors/cursor.js +127 -0
  224. package/template/claude-task-manager/workers/state-detectors/gemini.js +21 -0
  225. package/template/claude-task-manager/workers/state-detectors/index.js +29 -0
  226. package/template/claude-task-manager/workers/state-detectors/opencode.js +103 -0
  227. package/template/docs/design/markdown-review-pane.md +206 -0
  228. package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +129 -38
  229. package/template/docs/designs/2026-05-20-mobile-worktree-finish-command.md +27 -0
  230. package/template/docs/designs/2026-05-22-ai-configuration-consolidation.md +248 -0
  231. package/template/docs/designs/ai-configuration-consolidation-mock.html +812 -0
  232. package/template/docs/private-memory-and-pii-policy.md +69 -0
  233. package/template/package.json +2 -1
  234. package/template/scripts/check-private-data.js +201 -0
  235. package/template/shared/sqlite-owner-guard.js +30 -0
  236. package/template/shared/sqlite-owner-write-queue.js +225 -0
  237. package/template/shared/sqlite-storage-policy.js +111 -0
  238. package/template/shared/sqlite-write-lock.js +428 -0
  239. package/template/wall-e/agent-runners/claude-code.js +5 -0
  240. package/template/wall-e/agent.js +166 -22
  241. package/template/wall-e/api-walle.js +524 -70
  242. package/template/wall-e/auth/provider-flows.js +11 -1
  243. package/template/wall-e/bin/walle-mcp-stdio.js +341 -17
  244. package/template/wall-e/brain.js +1614 -141
  245. package/template/wall-e/chat/attachment-blocks.js +96 -0
  246. package/template/wall-e/chat/attachments.js +2 -1
  247. package/template/wall-e/chat/capability-resolver.js +7 -7
  248. package/template/wall-e/chat/context-messages.js +28 -0
  249. package/template/wall-e/chat/conversation-frame.js +630 -0
  250. package/template/wall-e/chat/provider-messages.js +125 -0
  251. package/template/wall-e/chat.js +1002 -233
  252. package/template/wall-e/coding/acceptance-contract.js +170 -0
  253. package/template/wall-e/coding/acp-adapter.js +1 -1
  254. package/template/wall-e/coding/agent-catalog.js +3 -0
  255. package/template/wall-e/coding/artifact-store.js +93 -0
  256. package/template/wall-e/coding/capability-router.js +120 -0
  257. package/template/wall-e/coding/coding-run-controller.js +423 -0
  258. package/template/wall-e/coding/compaction-service.js +157 -12
  259. package/template/wall-e/coding/frontend-verification.js +258 -0
  260. package/template/wall-e/coding/lifecycle-hooks.js +75 -0
  261. package/template/wall-e/coding/local-preview-contract.js +157 -0
  262. package/template/wall-e/coding/permission-service.js +57 -13
  263. package/template/wall-e/coding/prompt-bundle.js +19 -1
  264. package/template/wall-e/coding/prompt-section-registry.js +227 -0
  265. package/template/wall-e/coding/provider-compat.js +15 -0
  266. package/template/wall-e/coding/runtime-events.js +224 -0
  267. package/template/wall-e/coding/runtime-mode.js +3 -0
  268. package/template/wall-e/coding/side-git-snapshot.js +160 -4
  269. package/template/wall-e/coding/snapshot-service.js +143 -1
  270. package/template/wall-e/coding/stream-processor.js +388 -34
  271. package/template/wall-e/coding/task-tool.js +141 -4
  272. package/template/wall-e/coding/tool-execution-controller.js +365 -0
  273. package/template/wall-e/coding/tool-registry.js +43 -5
  274. package/template/wall-e/coding/user-hooks.js +217 -0
  275. package/template/wall-e/coding-orchestrator.js +1330 -221
  276. package/template/wall-e/coding-prompts.js +20 -4
  277. package/template/wall-e/context/context-builder.js +15 -2
  278. package/template/wall-e/decision/confidence.js +1 -1
  279. package/template/wall-e/docs/coding-acceptance-contract.md +41 -0
  280. package/template/wall-e/docs/external-action-controller.md +26 -6
  281. package/template/wall-e/docs/telemetry-lifecycle.md +8 -2
  282. package/template/wall-e/embeddings.js +591 -53
  283. package/template/wall-e/external-action-controller.js +12 -0
  284. package/template/wall-e/http/auth.js +1 -0
  285. package/template/wall-e/http/chat-api.js +46 -11
  286. package/template/wall-e/http/model-admin.js +836 -34
  287. package/template/wall-e/lib/boot-profile.js +88 -0
  288. package/template/wall-e/lib/event-loop-monitor.js +93 -0
  289. package/template/wall-e/lib/service-health.js +194 -0
  290. package/template/wall-e/llm/anthropic.js +130 -5
  291. package/template/wall-e/llm/client.js +266 -63
  292. package/template/wall-e/llm/default-fallback.js +382 -0
  293. package/template/wall-e/llm/health.js +19 -0
  294. package/template/wall-e/llm/message-guard.js +78 -0
  295. package/template/wall-e/llm/model-catalog.js +252 -1
  296. package/template/wall-e/llm/openai.js +26 -4
  297. package/template/wall-e/llm/portkey-sync.js +654 -0
  298. package/template/wall-e/llm/provider-error.js +30 -2
  299. package/template/wall-e/llm/registry.js +5 -1
  300. package/template/wall-e/llm/request-compat.js +67 -0
  301. package/template/wall-e/loops/backfill.js +79 -23
  302. package/template/wall-e/loops/brain-optimize.js +67 -0
  303. package/template/wall-e/loops/ingest.js +25 -10
  304. package/template/wall-e/loops/question-digest.js +160 -0
  305. package/template/wall-e/loops/reflect.js +6 -4
  306. package/template/wall-e/loops/think.js +39 -12
  307. package/template/wall-e/mcp-server.js +318 -36
  308. package/template/wall-e/memory/ctm-context-client.js +52 -14
  309. package/template/wall-e/memory/ctm-operational-context.js +237 -0
  310. package/template/wall-e/memory/ctm-prompt-executions-client.js +128 -0
  311. package/template/wall-e/memory/ctm-session-context.js +111 -63
  312. package/template/wall-e/prompts/coding/deepseek.txt +3 -0
  313. package/template/wall-e/prompts/coding/gemini.txt +6 -0
  314. package/template/wall-e/prompts/coding/gpt.txt +6 -0
  315. package/template/wall-e/prompts/coding/local.txt +7 -0
  316. package/template/wall-e/runtime/decision-hooks.js +115 -0
  317. package/template/wall-e/runtime/devbox-gateway.js +82 -8
  318. package/template/wall-e/runtime/prompt-manifest.js +86 -0
  319. package/template/wall-e/runtime/tool-executor.js +269 -0
  320. package/template/wall-e/runtime/tool-result-envelope.js +138 -0
  321. package/template/wall-e/runtime/transcript-projection.js +60 -0
  322. package/template/wall-e/runtime/walle-runtime.js +224 -0
  323. package/template/wall-e/scripts/db-optimize/migrate.js +162 -0
  324. package/template/wall-e/scripts/db-optimize/recall-eval.js +117 -0
  325. package/template/wall-e/server.js +15 -0
  326. package/template/wall-e/session-files.js +9 -0
  327. package/template/wall-e/skills/_bundled/google-calendar/run.js +1 -1
  328. package/template/wall-e/skills/_bundled/gws-workspace/run.js +1 -1
  329. package/template/wall-e/skills/_bundled/slack-mentions/run.js +76 -6
  330. package/template/wall-e/skills/claude-code-reader.js +7 -3
  331. package/template/wall-e/skills/script-skill-runner.js +10 -0
  332. package/template/wall-e/skills/skill-planner.js +38 -0
  333. package/template/wall-e/tools/builtin-middleware.js +19 -9
  334. package/template/wall-e/tools/local-tools.js +1428 -16
  335. package/template/wall-e/tools/permission-checker.js +73 -5
  336. package/template/wall-e/tools/question-manager.js +117 -7
  337. package/template/wall-e/training/harvester.js +12 -28
  338. package/template/wall-e/training/replay.js +25 -80
  339. package/template/website/index.html +10 -10
  340. package/template/wall-e/eval/ab-test.js +0 -203
  341. package/template/wall-e/eval/agent-runner.js +0 -772
  342. package/template/wall-e/eval/agent-scorer.js +0 -461
  343. package/template/wall-e/eval/aggregator.js +0 -414
  344. package/template/wall-e/eval/allowed-test-commands.js +0 -34
  345. package/template/wall-e/eval/benchmark-generator.js +0 -113
  346. package/template/wall-e/eval/benchmarks/chat-eval.json +0 -1662
  347. package/template/wall-e/eval/benchmarks/chat.json +0 -82
  348. package/template/wall-e/eval/benchmarks/coding-agent-real.json +0 -1
  349. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -1581
  350. package/template/wall-e/eval/benchmarks/coding.json +0 -122
  351. package/template/wall-e/eval/benchmarks/memory-retrieval.json +0 -234
  352. package/template/wall-e/eval/benchmarks/reasoning.json +0 -82
  353. package/template/wall-e/eval/benchmarks/swebench-lite-30.json +0 -212
  354. package/template/wall-e/eval/benchmarks.js +0 -669
  355. package/template/wall-e/eval/cc-replay.js +0 -719
  356. package/template/wall-e/eval/chat-eval.js +0 -525
  357. package/template/wall-e/eval/check-keys.js +0 -15
  358. package/template/wall-e/eval/check-providers.js +0 -42
  359. package/template/wall-e/eval/codex-cli-baseline.js +0 -669
  360. package/template/wall-e/eval/coding-agent-real.js +0 -570
  361. package/template/wall-e/eval/context-compactor.js +0 -251
  362. package/template/wall-e/eval/debug-agent003.js +0 -68
  363. package/template/wall-e/eval/diagnostics.js +0 -216
  364. package/template/wall-e/eval/eval-orchestrator.js +0 -642
  365. package/template/wall-e/eval/evaluate.js +0 -202
  366. package/template/wall-e/eval/evaluator.js +0 -373
  367. package/template/wall-e/eval/exporter.js +0 -212
  368. package/template/wall-e/eval/fixtures/express-basic/package.json +0 -9
  369. package/template/wall-e/eval/fixtures/express-basic/server.js +0 -115
  370. package/template/wall-e/eval/fixtures/express-basic/test.js +0 -83
  371. package/template/wall-e/eval/fixtures/express-buggy/package.json +0 -9
  372. package/template/wall-e/eval/fixtures/express-buggy/server.js +0 -113
  373. package/template/wall-e/eval/fixtures/express-buggy/test.js +0 -83
  374. package/template/wall-e/eval/fixtures/express-buggy-items/package.json +0 -9
  375. package/template/wall-e/eval/fixtures/express-buggy-items/server.js +0 -112
  376. package/template/wall-e/eval/fixtures/express-buggy-items/test.js +0 -83
  377. package/template/wall-e/eval/fixtures/express-buggy-search/package.json +0 -9
  378. package/template/wall-e/eval/fixtures/express-buggy-search/server.js +0 -121
  379. package/template/wall-e/eval/fixtures/express-buggy-search/test.js +0 -83
  380. package/template/wall-e/eval/fixtures/express-rename-data/data.js +0 -34
  381. package/template/wall-e/eval/fixtures/express-rename-data/package.json +0 -9
  382. package/template/wall-e/eval/fixtures/express-rename-data/server.js +0 -97
  383. package/template/wall-e/eval/fixtures/express-rename-data/test.js +0 -88
  384. package/template/wall-e/eval/fixtures/express-xss/package.json +0 -12
  385. package/template/wall-e/eval/fixtures/express-xss/server.js +0 -90
  386. package/template/wall-e/eval/fixtures/express-xss/test.js +0 -67
  387. package/template/wall-e/eval/fixtures/express-xss/views/profile.ejs +0 -9
  388. package/template/wall-e/eval/fixtures/fullstack-app/config/default.js +0 -9
  389. package/template/wall-e/eval/fixtures/fullstack-app/config/test.js +0 -13
  390. package/template/wall-e/eval/fixtures/fullstack-app/package.json +0 -11
  391. package/template/wall-e/eval/fixtures/fullstack-app/public/css/style.css +0 -137
  392. package/template/wall-e/eval/fixtures/fullstack-app/public/index.html +0 -46
  393. package/template/wall-e/eval/fixtures/fullstack-app/public/js/app.js +0 -121
  394. package/template/wall-e/eval/fixtures/fullstack-app/public/js/auth.js +0 -71
  395. package/template/wall-e/eval/fixtures/fullstack-app/public/js/items.js +0 -80
  396. package/template/wall-e/eval/fixtures/fullstack-app/public/js/users.js +0 -46
  397. package/template/wall-e/eval/fixtures/fullstack-app/public/login.html +0 -45
  398. package/template/wall-e/eval/fixtures/fullstack-app/public/register.html +0 -38
  399. package/template/wall-e/eval/fixtures/fullstack-app/scripts/migrate.js +0 -23
  400. package/template/wall-e/eval/fixtures/fullstack-app/scripts/seed.js +0 -46
  401. package/template/wall-e/eval/fixtures/fullstack-app/server/db.js +0 -99
  402. package/template/wall-e/eval/fixtures/fullstack-app/server/index.js +0 -94
  403. package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/auth.js +0 -19
  404. package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/logger.js +0 -19
  405. package/template/wall-e/eval/fixtures/fullstack-app/server/router.js +0 -50
  406. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/auth.js +0 -69
  407. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/health.js +0 -23
  408. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/items.js +0 -88
  409. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/users.js +0 -75
  410. package/template/wall-e/eval/fixtures/fullstack-app/server/test.js +0 -198
  411. package/template/wall-e/eval/fixtures/fullstack-app/server/utils/response.js +0 -34
  412. package/template/wall-e/eval/fixtures/fullstack-app/server/utils/validate.js +0 -26
  413. package/template/wall-e/eval/fixtures/fullstack-app/server.js +0 -8
  414. package/template/wall-e/eval/fixtures/fullstack-app/test.js +0 -12
  415. package/template/wall-e/eval/fixtures/monorepo-basic/package.json +0 -8
  416. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/data.js +0 -58
  417. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/middleware.js +0 -46
  418. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/package.json +0 -8
  419. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/routes.js +0 -64
  420. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/server.js +0 -56
  421. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/test.js +0 -116
  422. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/commands.js +0 -61
  423. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/index.js +0 -62
  424. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/output.js +0 -43
  425. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/package.json +0 -11
  426. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/test.js +0 -44
  427. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/formatters.js +0 -43
  428. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/index.js +0 -12
  429. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/package.json +0 -5
  430. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/test.js +0 -55
  431. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/validators.js +0 -29
  432. package/template/wall-e/eval/fixtures/monorepo-basic/test.js +0 -46
  433. package/template/wall-e/eval/fixtures/node-cli/index.js +0 -78
  434. package/template/wall-e/eval/fixtures/node-cli/package.json +0 -10
  435. package/template/wall-e/eval/fixtures/node-cli/test.js +0 -57
  436. package/template/wall-e/eval/fixtures/node-typed/package.json +0 -8
  437. package/template/wall-e/eval/fixtures/node-typed/src/handlers.js +0 -31
  438. package/template/wall-e/eval/fixtures/node-typed/src/utils.js +0 -33
  439. package/template/wall-e/eval/fixtures/node-typed/test.js +0 -36
  440. package/template/wall-e/eval/fixtures/python-flask/app.py +0 -14
  441. package/template/wall-e/eval/fixtures/python-flask/requirements.txt +0 -2
  442. package/template/wall-e/eval/fixtures/python-flask/test_app.py +0 -25
  443. package/template/wall-e/eval/fixtures/wall-e-subset/brain.js +0 -105
  444. package/template/wall-e/eval/fixtures/wall-e-subset/eval/aggregator.js +0 -101
  445. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/chat.json +0 -20
  446. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/coding.json +0 -32
  447. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks.js +0 -64
  448. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/package.json +0 -6
  449. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/server.js +0 -31
  450. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/test.js +0 -18
  451. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/utils.js +0 -34
  452. package/template/wall-e/eval/fixtures/wall-e-subset/eval/runner.js +0 -104
  453. package/template/wall-e/eval/fixtures/wall-e-subset/eval/scorer.js +0 -73
  454. package/template/wall-e/eval/fixtures/wall-e-subset/eval/test.js +0 -134
  455. package/template/wall-e/eval/fixtures/wall-e-subset/llm/client.js +0 -99
  456. package/template/wall-e/eval/fixtures/wall-e-subset/llm/providers.js +0 -63
  457. package/template/wall-e/eval/fixtures/wall-e-subset/llm/test.js +0 -70
  458. package/template/wall-e/eval/fixtures/wall-e-subset/package.json +0 -10
  459. package/template/wall-e/eval/fixtures/wall-e-subset/test.js +0 -86
  460. package/template/wall-e/eval/harvester.js +0 -685
  461. package/template/wall-e/eval/head-to-head.js +0 -388
  462. package/template/wall-e/eval/humaneval-adapter.js +0 -321
  463. package/template/wall-e/eval/list-models.js +0 -31
  464. package/template/wall-e/eval/livecodebench-adapter.js +0 -291
  465. package/template/wall-e/eval/mail-integration.js +0 -443
  466. package/template/wall-e/eval/manifest.js +0 -186
  467. package/template/wall-e/eval/meta-harness/adapters/coding-agent.js +0 -57
  468. package/template/wall-e/eval/meta-harness/bootstrap-snapshot.js +0 -149
  469. package/template/wall-e/eval/meta-harness/candidate-store.js +0 -117
  470. package/template/wall-e/eval/meta-harness/cli.js +0 -86
  471. package/template/wall-e/eval/meta-harness/domain-spec.js +0 -154
  472. package/template/wall-e/eval/meta-harness/domains/coding-agent.domain.json +0 -84
  473. package/template/wall-e/eval/meta-harness/examples/env-bootstrap-candidate.js +0 -29
  474. package/template/wall-e/eval/meta-harness/experience-store.js +0 -174
  475. package/template/wall-e/eval/meta-harness/frontier.js +0 -96
  476. package/template/wall-e/eval/meta-harness/harness-interface.js +0 -90
  477. package/template/wall-e/eval/meta-harness/leakage-guard.js +0 -80
  478. package/template/wall-e/eval/meta-harness/optimizer.js +0 -207
  479. package/template/wall-e/eval/meta-harness/proposer-runner.js +0 -110
  480. package/template/wall-e/eval/meta-harness/reporting.js +0 -58
  481. package/template/wall-e/eval/meta-harness/telemetry.js +0 -27
  482. package/template/wall-e/eval/meta-harness/validation.js +0 -81
  483. package/template/wall-e/eval/promoter.js +0 -228
  484. package/template/wall-e/eval/provider-normalizer.js +0 -33
  485. package/template/wall-e/eval/replay.js +0 -395
  486. package/template/wall-e/eval/run-agent-benchmarks.js +0 -386
  487. package/template/wall-e/eval/run-codex-cli-baseline.js +0 -177
  488. package/template/wall-e/eval/run-coding-agent-real.js +0 -187
  489. package/template/wall-e/eval/run-eval.js +0 -435
  490. package/template/wall-e/eval/run-model-comparison.js +0 -142
  491. package/template/wall-e/eval/session-evaluator.js +0 -187
  492. package/template/wall-e/eval/session-miner.js +0 -207
  493. package/template/wall-e/eval/session-retrieval-benchmark.js +0 -150
  494. package/template/wall-e/eval/session-transcripts.js +0 -509
  495. package/template/wall-e/eval/shadow.js +0 -161
  496. package/template/wall-e/eval/swebench-adapter.js +0 -345
  497. package/template/wall-e/eval/swebench-docker.js +0 -192
  498. package/template/wall-e/eval/train.py +0 -320
  499. package/template/wall-e/eval/trainer.js +0 -232
  500. package/template/wall-e/eval/weekly-eval-loop.js +0 -241
@@ -6,12 +6,34 @@ const {
6
6
  getDefaultClient,
7
7
  getDefaultModelForProvider,
8
8
  getDefaultProviderType,
9
+ getProviderRuntimeConfig,
9
10
  resolveCompatibleModel,
11
+ withProviderMessageGuard,
10
12
  } = require('./llm/client');
13
+ const { canonicalModelId: canonicalSessionModelId } = require('./llm/model-catalog');
11
14
  const { routeLabel } = require('./llm/portkey');
12
15
  const { executeLocalTool, LOCAL_TOOL_DEFINITIONS, resolveProjectPath } = require('./tools/local-tools');
16
+ const { buildAttachmentBlocks } = require('./chat/attachment-blocks');
17
+ const ctmContextClient = require('./memory/ctm-context-client');
18
+ const ctmOperationalContext = require('./memory/ctm-operational-context');
13
19
  const slackMcp = require('./tools/slack-mcp');
14
20
  const { estimateTokens, estimateMessagesTokens } = require('./context/token-counter');
21
+
22
+ // search_memories returns total_memories / total_slack for context. These were two
23
+ // count(*) scans of the memories table on EVERY search (a hot MCP path). The totals
24
+ // barely move between searches, so cache them briefly.
25
+ let _searchTotalsCache = { at: 0, total_memories: 0, total_slack: 0 };
26
+ const _SEARCH_TOTALS_TTL_MS = 60 * 1000;
27
+ function _searchTotals(db) {
28
+ const now = Date.now();
29
+ if (now - _searchTotalsCache.at < _SEARCH_TOTALS_TTL_MS) return _searchTotalsCache;
30
+ try {
31
+ const total_memories = db.prepare('SELECT count(*) as c FROM memories').get().c;
32
+ const total_slack = db.prepare('SELECT count(*) as c FROM memories WHERE source = ?').get('slack').c;
33
+ _searchTotalsCache = { at: now, total_memories, total_slack };
34
+ } catch { /* keep last good cache on error */ }
35
+ return _searchTotalsCache;
36
+ }
15
37
  const { shouldCompact, compactToolResult, compactMessages } = require('./context/compactor');
16
38
  const { buildSystemPrompt } = require('./context/context-builder');
17
39
  const {
@@ -29,9 +51,19 @@ const { sanitizeQuery } = require('./context/query-sanitizer');
29
51
  const { checkPermission, recordDecision, configure: configurePermissions } = require('./tools/permission-checker');
30
52
  const {
31
53
  inputForExternalActionEnvelope,
54
+ isExternalActionTool,
32
55
  reviewExternalAction,
33
56
  } = require('./external-action-controller');
34
57
  const { reviewExternalActionGateway } = require('./external-action-gateway');
58
+ const {
59
+ hasVerificationEvidence,
60
+ normalizeToolCallEvidence,
61
+ } = require('./coding/acceptance-contract');
62
+ const {
63
+ analyzeLocalPreviewClaims,
64
+ buildLocalPreviewVerificationNudge,
65
+ buildUnsupportedLocalPreviewReply,
66
+ } = require('./coding/local-preview-contract');
35
67
  const { runShadow } = require('./eval/shadow');
36
68
  const {
37
69
  buildCodeReviewContextBlock,
@@ -40,6 +72,15 @@ const {
40
72
  looksLikePrematureCodeReviewReply,
41
73
  } = require('./chat/code-review-context');
42
74
  const { createSessionRecorder } = require('./session-files');
75
+ const { normalizeContextMessagesForChat } = require('./chat/context-messages');
76
+ const { buildProviderMessagesForChat } = require('./chat/provider-messages');
77
+ const {
78
+ buildConversationFramePrompt,
79
+ buildConversationRuntime,
80
+ buildFrameRepairPrompt,
81
+ parseSessionMetadata,
82
+ validateFrameAnswer,
83
+ } = require('./chat/conversation-frame');
43
84
  const {
44
85
  decorateProviderError,
45
86
  recordProviderFailureAlert,
@@ -49,8 +90,17 @@ const { extractTextToolCalls, normalizeToolCall } = require('./llm/text-tool-cal
49
90
  const {
50
91
  emitAgentRunContextWarnings,
51
92
  isCodingAgentContext,
52
- resolveAgentRunContext,
53
93
  } = require('./runtime/agent-run-context');
94
+ const { classifyCodingTurnIntent } = require('./coding/coding-run-controller');
95
+ const {
96
+ createWallEToolRegistry,
97
+ executeRuntimeTool,
98
+ getRuntimeToolDefinitions,
99
+ RuntimeToolExecutor,
100
+ resolveWallERuntimeProfile,
101
+ runtimeToolContext,
102
+ } = require('./runtime/walle-runtime');
103
+ const { buildRuntimePromptManifest } = require('./runtime/prompt-manifest');
54
104
  let _telemetry;
55
105
  try { _telemetry = require('./telemetry'); } catch { _telemetry = { trackError() {}, track() {} }; }
56
106
  let _embeddings;
@@ -62,6 +112,28 @@ function providerTypeOf(provider) {
62
112
  return provider.type || provider.provider || provider.id || '';
63
113
  }
64
114
 
115
+ // Native-tool-call providers never legitimately emit text-form tool markup.
116
+ // Anthropic (Claude) returns `tool_use` blocks; if an "anthropic" route returns
117
+ // DeepSeek-style DSML instead, the route is almost certainly NOT serving real
118
+ // Claude (e.g. a Portkey catalog slug mapped to a DSML-emitting backend). Warn
119
+ // once per (provider, model, format) so the misconfiguration is visible without
120
+ // log spam. Detection only — finalization already strips/recovers the markup.
121
+ const NATIVE_TOOL_CALL_PROVIDERS = new Set(['anthropic', 'google', 'openai']);
122
+ const _routeFormatMismatchWarned = new Set();
123
+ function warnOnTextToolCallFormatMismatch(providerType, model, format) {
124
+ const type = String(providerType || '').toLowerCase();
125
+ if (!format || !NATIVE_TOOL_CALL_PROVIDERS.has(type)) return;
126
+ const key = `${type}|${model || ''}|${format}`;
127
+ if (_routeFormatMismatchWarned.has(key)) return;
128
+ _routeFormatMismatchWarned.add(key);
129
+ console.warn(
130
+ `[chat] Route mismatch: provider "${type}" (model ${model || 'unknown'}) returned ` +
131
+ `text-form tool calls (${format}) instead of native tool_use. A genuine ${type} ` +
132
+ `model does not emit ${format}; this route is likely proxying to a different ` +
133
+ `backend (e.g. a misconfigured Portkey catalog slug). Verify the route's underlying model.`,
134
+ );
135
+ }
136
+
65
137
  function isDeepSeekV4Pro(provider, model) {
66
138
  const type = providerTypeOf(provider).toLowerCase();
67
139
  return (type === 'deepseek' || type.includes('deepseek'))
@@ -104,6 +176,10 @@ function looksLikePrematureToolFollowupReply(text) {
104
176
  || value.length > 500;
105
177
  if (hasFinalShape) return false;
106
178
 
179
+ const deferredToolOffer = /\b(?:would you like me to|want me to|should i)\s+(?:look up|search|get|check|inspect|read|pull|fetch|find)\b/i.test(value)
180
+ && /[??]/.test(value);
181
+ if (concise && deferredToolOffer) return true;
182
+
107
183
  const progressCue = /\b(?:content inventory confirmed|inventory confirmed|analysis complete|context confirmed|now|next|then)\b/i.test(value);
108
184
  const sequenceCue = /\b(?:now|next)\b[\s:,-—;]+[\s\S]{0,160}\bthen\b/i.test(value);
109
185
  if (firstPersonFuture && executionFutureVerb && concise) return true;
@@ -112,6 +188,26 @@ function looksLikePrematureToolFollowupReply(text) {
112
188
  return concise && progressCue && (sequenceCue || executionVerb);
113
189
  }
114
190
 
191
+ function _chatToolCallHistoryForActionGuard(toolCalls = []) {
192
+ return (toolCalls || [])
193
+ .map((call) => {
194
+ const name = call?.name || call?.tool || '';
195
+ if (!name) return null;
196
+ return normalizeToolCallEvidence({ ...call, name });
197
+ })
198
+ .filter(Boolean);
199
+ }
200
+
201
+ function _chatNoActionContinuation(args) {
202
+ try {
203
+ const { getNoActionContinuation } = require('./coding-orchestrator');
204
+ return getNoActionContinuation(args);
205
+ } catch (err) {
206
+ console.warn('[chat] Action completion guard unavailable:', err.message);
207
+ return null;
208
+ }
209
+ }
210
+
115
211
  function _hasExplicitWeatherLocation(message) {
116
212
  const text = String(message || '').trim();
117
213
  if (!text) return false;
@@ -272,7 +368,7 @@ function hasOwn(obj, key) {
272
368
  }
273
369
 
274
370
  function isWallECodingMode({ opts = {}, channel = '' } = {}) {
275
- return isCodingAgentContext(resolveAgentRunContext({ ...opts, channel }));
371
+ return isCodingAgentContext(resolveWallERuntimeProfile({ ...opts, channel }).context);
276
372
  }
277
373
 
278
374
  function resolveReasoningOptions({
@@ -305,36 +401,6 @@ function resolveReasoningOptions({
305
401
  return out;
306
402
  }
307
403
 
308
- const PARALLEL_SAFE_CHAT_TOOLS = new Set([
309
- 'think',
310
- 'search_memories',
311
- 'lookup_person',
312
- 'list_mcp_tools',
313
- 'list_tasks',
314
- 'calendar_events',
315
- 'calendar_list',
316
- 'system_info',
317
- 'clipboard_read',
318
- 'mail_messages',
319
- 'mail_read',
320
- 'mail_search',
321
- 'read_project_file',
322
- 'search_project',
323
- 'read_file',
324
- 'search_files',
325
- 'web_fetch',
326
- 'glean_search',
327
- 'glean_people',
328
- 'glean_chat',
329
- 'glob',
330
- 'grep_files',
331
- 'list_directory',
332
- ]);
333
-
334
- function _shouldRunToolCallsSequentially(toolCalls) {
335
- return toolCalls.some((tu) => !PARALLEL_SAFE_CHAT_TOOLS.has(tu.name));
336
- }
337
-
338
404
  let _codingOrchestrator;
339
405
  function getCodingOrchestrator() {
340
406
  if (!_codingOrchestrator) _codingOrchestrator = require('./coding-orchestrator');
@@ -521,90 +587,6 @@ function _requestedSkillProgressEvents(resolution = {}) {
521
587
  });
522
588
  }
523
589
 
524
- /**
525
- * Convert validated ParsedAttachments into the LLM content-block shape
526
- * that the active provider expects. The provider adapter is responsible
527
- * for any further reshaping (e.g. OpenAI's `image_url` envelope).
528
- *
529
- * Anthropic:
530
- * image → { type:'image', source:{type:'base64', media_type, data} }
531
- * pdf → { type:'document', source:{type:'base64', media_type:'application/pdf', data} }
532
- * audio → throws (Anthropic vision API doesn't accept audio).
533
- * OpenAI (vision/audio-capable models):
534
- * image → internal {type:'image',source:...} — messagesToOpenAI
535
- * rewrites to {type:'image_url',image_url:{url:'data:...'}}.
536
- * audio → internal {type:'audio',source:...} — messagesToOpenAI
537
- * rewrites to {type:'input_audio',input_audio:{data,format}}.
538
- * pdf → throws (OpenAI doesn't natively support PDF in chat).
539
- * Google Gemini:
540
- * image/pdf/audio → {type:<kind>,source:{type:'base64',media_type,data}}
541
- * — Gemini provider's messagesToGemini (TBD) rewrites to
542
- * {inline_data:{mime_type,data}}. For now Phase 4 wires the
543
- * internal shape only; audio reaches OpenAI today, Gemini
544
- * lands when its adapter ships.
545
- *
546
- * @throws Error with .code='MULTIMODAL_NOT_AVAILABLE' when the active
547
- * provider can't carry the requested attachment kinds.
548
- */
549
- function buildAttachmentBlocks(providerType, attachments) {
550
- if (!attachments || attachments.length === 0) return null;
551
-
552
- // Internal canonical content-block shape (Anthropic-flavored). Each
553
- // adapter's messages converter rewrites to its native shape on the
554
- // way out. New attachment kinds add a `type` and optionally a
555
- // `format` field for adapter convenience.
556
- const toInternalBlock = (a) => {
557
- const baseSrc = { type: 'base64', media_type: a.mediaType, data: a.base64 };
558
- if (a.type === 'image') return { type: 'image', source: baseSrc };
559
- if (a.type === 'pdf') return { type: 'document', source: { ...baseSrc, media_type: 'application/pdf' } };
560
- if (a.type === 'audio') {
561
- // OpenAI's input_audio API requires a `format` like 'wav'|'mp3'.
562
- // Derive from the media type so the adapter can wire it up.
563
- const fmt = a.mediaType.split('/')[1] || 'mp3';
564
- return { type: 'audio', source: baseSrc, format: fmt };
565
- }
566
- const err = new Error(`Unknown attachment kind "${a.type}"`);
567
- err.code = 'MULTIMODAL_NOT_AVAILABLE';
568
- throw err;
569
- };
570
-
571
- if (providerType === 'anthropic') {
572
- return attachments.map((a) => {
573
- if (a.type === 'audio') {
574
- const e = new Error('Anthropic chat does not accept audio attachments. Switch to OpenAI or Gemini for audio.');
575
- e.code = 'MULTIMODAL_NOT_AVAILABLE';
576
- throw e;
577
- }
578
- return toInternalBlock(a);
579
- });
580
- }
581
-
582
- if (providerType === 'openai') {
583
- return attachments.map((a) => {
584
- if (a.type === 'pdf') {
585
- const e = new Error('OpenAI chat does not natively support PDF attachments. Switch to Anthropic for PDF support.');
586
- e.code = 'MULTIMODAL_NOT_AVAILABLE';
587
- throw e;
588
- }
589
- return toInternalBlock(a); // image and audio
590
- });
591
- }
592
-
593
- if (providerType === 'google' || providerType === 'gemini') {
594
- // All three kinds are accepted; the Google adapter will rewrite to
595
- // Gemini's `inline_data` shape on the way out.
596
- return attachments.map(toInternalBlock);
597
- }
598
-
599
- // Other providers (Ollama, MLX, etc.) reach this only when the
600
- // capability resolver was bypassed or has a gap. Surface clearly.
601
- const e = new Error(
602
- `Attachments not supported on provider "${providerType}". Switch to a configured Anthropic/OpenAI/Gemini provider for this turn.`
603
- );
604
- e.code = 'MULTIMODAL_NOT_AVAILABLE';
605
- throw e;
606
- }
607
-
608
590
  /**
609
591
  * Format a coding progress event as a human-readable chat message.
610
592
  * Used by both inline execution and SSE streaming.
@@ -703,11 +685,12 @@ function _providerConfigFromRegistryRow(row) {
703
685
  }
704
686
 
705
687
  function _registryRouteFromRow(row, input, explicitProvider) {
688
+ const runtimeType = _providerRuntimeType(row) || row.provider_type || explicitProvider || null;
706
689
  return {
707
690
  input,
708
691
  model: row.model_id,
709
- provider: row.provider_type || explicitProvider || null,
710
- providerConfig: _providerConfigFromRegistryRow(row),
692
+ provider: runtimeType,
693
+ providerConfig: _providerRuntimeConfig(row),
711
694
  registryId: row.id,
712
695
  providerId: row.provider_id,
713
696
  displayName: row.display_name,
@@ -734,19 +717,37 @@ function _ambiguousModelRoute(input, rows, explicitProvider) {
734
717
  };
735
718
  }
736
719
 
720
+ function _preferredRouteRowForBareModel(rows = [], explicitProvider) {
721
+ if (!rows.length) return null;
722
+ const type = explicitProvider || rows[0].provider_type;
723
+ const policy = (type && typeof brain.getProviderRoutePolicy === 'function')
724
+ ? brain.getProviderRoutePolicy(type)
725
+ : 'auto';
726
+ if (typeof brain.sortModelProvidersByRoutePolicy !== 'function') return null;
727
+ const sorted = brain.sortModelProvidersByRoutePolicy(
728
+ rows.map((row) => ({ ...row, type: row.provider_type || row.type })),
729
+ policy,
730
+ );
731
+ return sorted[0] || null;
732
+ }
733
+
737
734
  function _createChatProvider(type, config = {}, opts = {}) {
738
- if (typeof opts.providerFactory === 'function') return opts.providerFactory(type, config);
739
- return createClient(type, config);
735
+ const provider = typeof opts.providerFactory === 'function'
736
+ ? opts.providerFactory(type, config)
737
+ : createClient(type, config);
738
+ return withProviderMessageGuard(provider);
740
739
  }
741
740
 
742
741
  function _providerRuntimeType(row = {}) {
743
- if (row.type === 'anthropic' && row.auth_method === 'claude_cli') return 'claude-cli';
744
- if (row.type === 'openai' && row.auth_method === 'codex_cli') return 'codex-cli';
745
- return row.type;
742
+ const type = row.type || row.provider_type;
743
+ if (type === 'anthropic' && row.auth_method === 'claude_cli') return 'claude-cli';
744
+ if (type === 'openai' && row.auth_method === 'codex_cli') return 'codex-cli';
745
+ return type;
746
746
  }
747
747
 
748
748
  function _providerRuntimeConfig(row = {}) {
749
- if (row.type === 'anthropic' && row.auth_method === 'oauth_proxy') {
749
+ const type = row.type || row.provider_type;
750
+ if (type === 'anthropic' && row.auth_method === 'oauth_proxy') {
750
751
  return {
751
752
  apiKey: 'oauth-proxy-placeholder',
752
753
  baseUrl: `http://127.0.0.1:${process.env.OAUTH_PROXY_PORT || '3458'}`,
@@ -861,30 +862,35 @@ function _toolArtifactSummary(toolCalls = []) {
861
862
  }
862
863
 
863
864
  function _hasSuccessfulWrite(toolCalls = []) {
864
- return (toolCalls || []).some(call => call && !call.error && call.tool === 'write_file');
865
+ return (toolCalls || []).some(call => (
866
+ call
867
+ && (call.tool === 'write_file' || call.name === 'write_file')
868
+ && normalizeToolCallEvidence(call).ok === true
869
+ ));
865
870
  }
866
871
 
867
872
  function _hasSuccessfulScreenshotAfterLastWrite(toolCalls = []) {
868
873
  let lastWriteIndex = -1;
869
874
  for (let i = 0; i < (toolCalls || []).length; i++) {
870
875
  const call = toolCalls[i];
871
- if (call && !call.error && call.tool === 'write_file') lastWriteIndex = i;
876
+ if (
877
+ call
878
+ && (call.tool === 'write_file' || call.name === 'write_file')
879
+ && normalizeToolCallEvidence(call).ok === true
880
+ ) {
881
+ lastWriteIndex = i;
882
+ }
872
883
  }
873
884
  if (lastWriteIndex < 0) return false;
874
- return (toolCalls || []).some((call, index) => (
875
- index > lastWriteIndex
876
- && call
877
- && !call.error
878
- && call.tool === 'browser_screenshot'
879
- ));
885
+ return hasVerificationEvidence(_chatToolCallHistoryForActionGuard((toolCalls || []).slice(lastWriteIndex + 1)));
880
886
  }
881
887
 
882
888
  function _postToolProgressFailureReply(toolCalls = []) {
883
889
  if (_hasSuccessfulWrite(toolCalls)) {
884
- const verificationText = _hasSuccessfulScreenshotAfterLastWrite(toolCalls)
885
- ? '\n\nVerified with browser screenshots; detailed tool output is preserved in the activity panel.'
886
- : '\n\nDetailed tool output is preserved in the activity panel.';
887
- return `Completed the requested changes.${_toolArtifactSummary(toolCalls)}${verificationText}`;
890
+ if (_hasSuccessfulScreenshotAfterLastWrite(toolCalls)) {
891
+ return `Completed the requested changes.${_toolArtifactSummary(toolCalls)}\n\nVerified with tool evidence; detailed tool output is preserved in the activity panel.`;
892
+ }
893
+ return `I made file changes, but final answer generation failed before successful verification evidence was captured.${_toolArtifactSummary(toolCalls)}\n\nThe detailed tool outputs are preserved in the activity panel. Please treat this as incomplete until tests, build, check_url, or browser_screenshot succeeds.`;
888
894
  }
889
895
  return `I ran the requested tools, but final answer generation kept returning progress/tool-control text instead of a clean final answer.${_toolArtifactSummary(toolCalls)}\n\nThe detailed tool outputs are preserved in the activity panel.`;
890
896
  }
@@ -1375,6 +1381,7 @@ async function _tryProviderFinalizationFallback({
1375
1381
  const decorated = decorateProviderError(err, {
1376
1382
  provider: fallback.providerType,
1377
1383
  model: fallback.model,
1384
+ providerId: fallback.providerId || null,
1378
1385
  });
1379
1386
  recordProviderFailureAlert(decorated.providerError, brain);
1380
1387
  console.warn('[chat] Finalization provider fallback failed, trying next candidate:', fallback.providerType, decorated.providerError.type);
@@ -1383,16 +1390,213 @@ async function _tryProviderFinalizationFallback({
1383
1390
  return null;
1384
1391
  }
1385
1392
 
1393
+ function _truncateFinalizationEvidence(value, maxChars) {
1394
+ const text = String(value || '').trim();
1395
+ if (text.length <= maxChars) return text;
1396
+ return text.slice(0, Math.max(0, maxChars - 80)).trimEnd() + '\n[... evidence truncated ...]';
1397
+ }
1398
+
1399
+ function _jsonForFinalizationEvidence(value, maxChars = 4000) {
1400
+ try {
1401
+ return _truncateFinalizationEvidence(JSON.stringify(value, null, 2), maxChars);
1402
+ } catch {
1403
+ return _truncateFinalizationEvidence(String(value || ''), maxChars);
1404
+ }
1405
+ }
1406
+
1407
+ function _mailMessageLine(message = {}) {
1408
+ const parts = [];
1409
+ if (message.subject) parts.push(`Subject: ${message.subject}`);
1410
+ if (message.from) parts.push(`From: ${message.from}`);
1411
+ if (message.to) parts.push(`To: ${message.to}`);
1412
+ if (message.date) parts.push(`Date: ${message.date}`);
1413
+ if (message.messageId) parts.push(`Message ID: ${message.messageId}`);
1414
+ if (message.snippet) parts.push(`Snippet: ${message.snippet}`);
1415
+ return parts.join(' | ');
1416
+ }
1417
+
1418
+ function _finalizationEvidenceForToolCall(call, index) {
1419
+ const name = call?.name || call?.tool || 'tool';
1420
+ const result = call?.result;
1421
+ const evidence = normalizeToolCallEvidence(call);
1422
+ const ok = evidence.ok === true;
1423
+ const lines = [`Tool ${index + 1}: ${name} (${ok ? 'ok' : 'error'})`];
1424
+ if (call?.args || call?.input) {
1425
+ lines.push(`Input: ${_jsonForFinalizationEvidence(call.args || call.input, 1200)}`);
1426
+ }
1427
+ if (!result) return lines.join('\n');
1428
+
1429
+ if (name === 'mail_search') {
1430
+ if (result.count != null) lines.push(`Count: ${result.count}`);
1431
+ if (result.query) lines.push(`Query: ${result.query}`);
1432
+ if (Array.isArray(result.accounts) && result.accounts.length) {
1433
+ lines.push(`Accounts searched: ${result.accounts.join(', ')}`);
1434
+ }
1435
+ if (Array.isArray(result.messages) && result.messages.length) {
1436
+ lines.push('Messages:');
1437
+ for (const message of result.messages.slice(0, 6)) {
1438
+ lines.push(`- ${_mailMessageLine(message)}`);
1439
+ }
1440
+ if (result.messages.length > 6) lines.push(`- ... ${result.messages.length - 6} more message(s) omitted`);
1441
+ }
1442
+ if (result.error) lines.push(`Error: ${result.error}`);
1443
+ return _truncateFinalizationEvidence(lines.join('\n'), 6500);
1444
+ }
1445
+
1446
+ if (name === 'mail_read') {
1447
+ for (const field of ['account', 'mailbox', 'subject', 'from', 'to', 'cc', 'date', 'messageId', 'threadId']) {
1448
+ if (result[field]) lines.push(`${field}: ${result[field]}`);
1449
+ }
1450
+ if (result.content) {
1451
+ lines.push('Content:');
1452
+ lines.push(_truncateFinalizationEvidence(result.content, 10000));
1453
+ } else if (result.snippet) {
1454
+ lines.push(`Snippet: ${result.snippet}`);
1455
+ }
1456
+ if (result.error) lines.push(`Error: ${result.error}`);
1457
+ return _truncateFinalizationEvidence(lines.join('\n'), 12000);
1458
+ }
1459
+
1460
+ if (name === 'web_fetch') {
1461
+ if (result.url) lines.push(`URL: ${result.url}`);
1462
+ if (result.title) lines.push(`Title: ${result.title}`);
1463
+ if (result.error) lines.push(`Error: ${result.error}`);
1464
+ const body = result.text || result.content || result.body || result.markdown || result.html;
1465
+ if (body) {
1466
+ lines.push('Fetched content:');
1467
+ lines.push(_truncateFinalizationEvidence(body, 7000));
1468
+ }
1469
+ return _truncateFinalizationEvidence(lines.join('\n'), 8500);
1470
+ }
1471
+
1472
+ if (result.error) lines.push(`Error: ${result.error}`);
1473
+ lines.push(`Result: ${_jsonForFinalizationEvidence(result, 5000)}`);
1474
+ return _truncateFinalizationEvidence(lines.join('\n'), 6500);
1475
+ }
1476
+
1477
+ function _buildEvidenceOnlyFinalizationMessages({ originalRequest, toolCalls = [] }) {
1478
+ const parts = [];
1479
+ let used = 0;
1480
+ const maxTotal = 22000;
1481
+ for (const [index, call] of toolCalls.entries()) {
1482
+ const block = _finalizationEvidenceForToolCall(call, index);
1483
+ if (!block.trim()) continue;
1484
+ if (used + block.length > maxTotal && parts.length > 0) {
1485
+ parts.push('[... additional tool evidence omitted to keep finalization focused ...]');
1486
+ break;
1487
+ }
1488
+ parts.push(block);
1489
+ used += block.length;
1490
+ }
1491
+ const evidence = parts.join('\n\n');
1492
+ return [{
1493
+ role: 'user',
1494
+ content: [
1495
+ 'Original user request:',
1496
+ String(originalRequest || '').trim(),
1497
+ '',
1498
+ 'Tool evidence already gathered:',
1499
+ evidence || 'No structured tool evidence is available.',
1500
+ '',
1501
+ 'Write the final answer now. Use only this evidence. Do not mention tool calls, JSON, protocols, activity panels, or future inspection. If the request asks for drafts, write the drafts directly.',
1502
+ ].join('\n'),
1503
+ }];
1504
+ }
1505
+
1506
+ async function _tryEvidenceOnlyFinalization({
1507
+ provider,
1508
+ model,
1509
+ originalRequest,
1510
+ toolCalls = [],
1511
+ opts = {},
1512
+ resetFinalizationTimeout,
1513
+ resolveCurrentReasoningOptions,
1514
+ signal,
1515
+ getSignal,
1516
+ } = {}) {
1517
+ if (typeof opts.finalizationFailureReply === 'string') return null;
1518
+ if (!provider || !Array.isArray(toolCalls) || toolCalls.length === 0) return null;
1519
+ try {
1520
+ resetFinalizationTimeout?.();
1521
+ const activeSignal = typeof getSignal === 'function' ? getSignal() : signal;
1522
+ const reasoning = typeof resolveCurrentReasoningOptions === 'function'
1523
+ ? resolveCurrentReasoningOptions(provider, model)
1524
+ : {};
1525
+ const response = await provider.chat({
1526
+ model,
1527
+ maxTokens: Math.max(2048, Math.min(4096, Number(opts.finalizationMaxTokens) || 4096)),
1528
+ system: [
1529
+ 'You are Wall-E final-answer synthesizer.',
1530
+ 'The main agent has already gathered tool evidence.',
1531
+ 'You cannot call tools and must not emit tool markup or progress notes.',
1532
+ 'Answer the original user request directly from the supplied evidence.',
1533
+ ].join(' '),
1534
+ messages: _buildEvidenceOnlyFinalizationMessages({ originalRequest, toolCalls }),
1535
+ tools: [],
1536
+ ...reasoning,
1537
+ thinking: 'disabled',
1538
+ signal: activeSignal,
1539
+ });
1540
+ const parsed = extractTextToolCalls(response);
1541
+ const text = String(parsed.content || response.content || '').trim();
1542
+ if (text && !_finalizationResponseIsProtocol(parsed, text)) {
1543
+ try {
1544
+ _telemetry.track('chat_evidence_only_finalization', {
1545
+ status: 'success',
1546
+ provider: response.provider || provider.type || '',
1547
+ model: response.model || model || '',
1548
+ });
1549
+ } catch {}
1550
+ return {
1551
+ text,
1552
+ response,
1553
+ meta: {
1554
+ model: response.model || model,
1555
+ provider: response.provider || provider.type,
1556
+ usage: response.usage || null,
1557
+ stopReason: response.stopReason || null,
1558
+ evidenceOnlyFinalization: true,
1559
+ },
1560
+ };
1561
+ }
1562
+ try {
1563
+ _telemetry.track('chat_evidence_only_finalization', {
1564
+ status: 'protocol_text',
1565
+ provider: response.provider || provider.type || '',
1566
+ model: response.model || model || '',
1567
+ });
1568
+ } catch {}
1569
+ } catch (err) {
1570
+ if (_isUserCancellation(opts, err)) throw _cancelledError();
1571
+ console.warn('[chat] Evidence-only finalization failed:', err.message);
1572
+ try {
1573
+ _telemetry.track('chat_evidence_only_finalization', {
1574
+ status: 'error',
1575
+ provider: provider.type || '',
1576
+ model: model || '',
1577
+ error_type: err.name || err.code || 'error',
1578
+ });
1579
+ } catch {}
1580
+ }
1581
+ return null;
1582
+ }
1583
+
1386
1584
  function _findChatProviderFallback(args) {
1387
1585
  return (_listChatProviderFallbacks(args) || [])[0] || null;
1388
1586
  }
1389
1587
 
1390
1588
  function resolveModelSelection(model, explicitProvider) {
1391
1589
  if (!model) {
1392
- return { input: model, model: null, provider: explicitProvider || null, providerConfig: null, registryId: null };
1590
+ return {
1591
+ input: model,
1592
+ model: null,
1593
+ provider: explicitProvider || null,
1594
+ providerConfig: explicitProvider ? getProviderRuntimeConfig(explicitProvider) : null,
1595
+ registryId: null,
1596
+ };
1393
1597
  }
1394
1598
 
1395
- const raw = String(model);
1599
+ const input = String(model);
1396
1600
  try {
1397
1601
  const exact = brain.getDb().prepare(`
1398
1602
  SELECT
@@ -1412,11 +1616,12 @@ function resolveModelSelection(model, explicitProvider) {
1412
1616
  AND mr.enabled = 1
1413
1617
  AND mp.enabled = 1
1414
1618
  LIMIT 1
1415
- `).get(raw);
1619
+ `).get(input);
1416
1620
  if (exact) {
1417
- return _registryRouteFromRow(exact, raw, explicitProvider);
1621
+ return _registryRouteFromRow(exact, input, explicitProvider);
1418
1622
  }
1419
1623
 
1624
+ const raw = canonicalSessionModelId(input, explicitProvider || detectProviderForModel(input) || '');
1420
1625
  const rows = brain.getDb().prepare(`
1421
1626
  SELECT
1422
1627
  mr.id,
@@ -1445,7 +1650,7 @@ function resolveModelSelection(model, explicitProvider) {
1445
1650
  const compatibleModel = resolveCompatibleModel(row.model_id, explicitProvider);
1446
1651
  if (compatibleModel !== row.model_id) {
1447
1652
  return {
1448
- input: raw,
1653
+ input,
1449
1654
  model: compatibleModel,
1450
1655
  provider: explicitProvider,
1451
1656
  providerConfig: null,
@@ -1455,36 +1660,39 @@ function resolveModelSelection(model, explicitProvider) {
1455
1660
  };
1456
1661
  }
1457
1662
  }
1458
- return _registryRouteFromRow(row, raw, explicitProvider);
1663
+ return _registryRouteFromRow(row, input, explicitProvider);
1459
1664
  }
1460
1665
 
1461
1666
  if (rows.length > 1) {
1462
1667
  const defaultRegistryId = (brain.getKv?.('walle_model_registry_id') || (explicitProvider ? brain.getKv?.(`walle_model_registry_${explicitProvider}`) : null) || '').trim();
1463
1668
  const defaultRow = defaultRegistryId ? rows.find((row) => row.id === defaultRegistryId) : null;
1464
- if (defaultRow) return _registryRouteFromRow(defaultRow, raw, explicitProvider);
1465
- return _ambiguousModelRoute(raw, rows, explicitProvider);
1669
+ if (defaultRow) return _registryRouteFromRow(defaultRow, input, explicitProvider);
1670
+ const preferredRow = _preferredRouteRowForBareModel(rows, explicitProvider);
1671
+ if (preferredRow) return _registryRouteFromRow(preferredRow, input, explicitProvider);
1672
+ return _ambiguousModelRoute(input, rows, explicitProvider);
1466
1673
  }
1467
1674
  } catch {}
1468
1675
 
1676
+ const raw = canonicalSessionModelId(input, explicitProvider || detectProviderForModel(input) || '');
1469
1677
  const detectedProvider = detectProviderForModel(raw);
1470
1678
  if (explicitProvider) {
1471
1679
  const compatibleModel = resolveCompatibleModel(raw, explicitProvider);
1472
1680
  return {
1473
- input: raw,
1681
+ input,
1474
1682
  model: compatibleModel,
1475
1683
  provider: explicitProvider,
1476
- providerConfig: null,
1684
+ providerConfig: getProviderRuntimeConfig(explicitProvider),
1477
1685
  registryId: null,
1478
- coercedFrom: compatibleModel === raw ? null : raw,
1686
+ coercedFrom: compatibleModel === input ? null : input,
1479
1687
  detectedProvider,
1480
1688
  };
1481
1689
  }
1482
1690
 
1483
1691
  return {
1484
- input: raw,
1692
+ input,
1485
1693
  model: raw,
1486
1694
  provider: detectedProvider || null,
1487
- providerConfig: null,
1695
+ providerConfig: detectedProvider ? getProviderRuntimeConfig(detectedProvider) : null,
1488
1696
  registryId: null,
1489
1697
  };
1490
1698
  }
@@ -1571,8 +1779,9 @@ async function chat(message, opts = {}) {
1571
1779
  let usedProvider = null;
1572
1780
  const allToolCalls = [];
1573
1781
  const channel = opts.channel || 'ctm';
1574
- const agentRunContext = resolveAgentRunContext({ ...opts, channel });
1575
- emitAgentRunContextWarnings(agentRunContext, { telemetry: _telemetry });
1782
+ const wallERuntimeProfile = resolveWallERuntimeProfile({ ...opts, channel });
1783
+ const agentRunContext = wallERuntimeProfile.context;
1784
+ emitAgentRunContextWarnings({ ...agentRunContext, warnings: wallERuntimeProfile.warnings }, { telemetry: _telemetry });
1576
1785
  const wallECodingMode = isCodingAgentContext(agentRunContext);
1577
1786
 
1578
1787
  // Scorecard-aware model selection (computed once per chat, used by all turns)
@@ -1590,6 +1799,11 @@ async function chat(message, opts = {}) {
1590
1799
  .join(', ');
1591
1800
  throw new Error(`Ambiguous model route for "${selectedRoute.input}". Choose a specific model route: ${choices}`);
1592
1801
  }
1802
+ if (selectedRoute.model) {
1803
+ // Registry routes own the executable model id. The picker may pass an alias
1804
+ // or display id, but provider calls must use the route's canonical model.
1805
+ selectedModel = selectedRoute.model;
1806
+ }
1593
1807
  const effectiveCwd = opts.cwd || opts.context?.cwd || opts.context?.projectPath || null;
1594
1808
 
1595
1809
  // Plug-provider-routing Item B+E: log routing decision via
@@ -1618,6 +1832,7 @@ async function chat(message, opts = {}) {
1618
1832
  const routingMessage = String(opts.routingMessage || opts.originalMessage || message || '');
1619
1833
  const queryTopics = classifyTopics(routingMessage);
1620
1834
  const intent = classifyIntent(queryTopics);
1835
+ const codingTurnIntent = classifyCodingTurnIntent(routingMessage);
1621
1836
 
1622
1837
  // Item F (multi-agent deep-dive): conversation collapse policy.
1623
1838
  // 'main' (default) keeps the historical 'default' bucket;
@@ -1631,6 +1846,7 @@ async function chat(message, opts = {}) {
1631
1846
  brain,
1632
1847
  });
1633
1848
  const existingSession = brain.getSession(sessionId);
1849
+ const existingSessionMetadata = parseSessionMetadata(existingSession?.metadata);
1634
1850
  const sessionRecorder = createSessionRecorder({
1635
1851
  sessionId,
1636
1852
  channel,
@@ -1640,6 +1856,8 @@ async function chat(message, opts = {}) {
1640
1856
  source: opts.source || channel || 'chat',
1641
1857
  model: selectedModel,
1642
1858
  provider: selectedRoute.provider || opts.provider || null,
1859
+ runtimeProfile: wallERuntimeProfile.profileId,
1860
+ permissionProfile: wallERuntimeProfile.permissionProfile,
1643
1861
  },
1644
1862
  });
1645
1863
  const recordSessionMessage = (role, content, extra) => {
@@ -1650,6 +1868,52 @@ async function chat(message, opts = {}) {
1650
1868
  return null;
1651
1869
  }
1652
1870
  };
1871
+ const recordUsageLedger = (response, feature, extra = {}) => {
1872
+ if (!response?.usage) return null;
1873
+ try {
1874
+ const raw = response.raw && typeof response.raw === 'object' ? response.raw : {};
1875
+ const providerType = response.provider || extra.provider || usedProvider || targetProviderType || null;
1876
+ const modelId = response.model || extra.model || selectedModel || null;
1877
+ const routeProvider = selectedRoute?.provider || null;
1878
+ const routeModel = selectedRoute?.model || null;
1879
+ const routeMatches = !!selectedRoute
1880
+ && (!providerType || !routeProvider || String(providerType).toLowerCase() === String(routeProvider).toLowerCase())
1881
+ && (!modelId || !routeModel || String(modelId).toLowerCase() === String(routeModel).toLowerCase());
1882
+ const requestId = response.requestId
1883
+ || response.responseId
1884
+ || response.id
1885
+ || raw.id
1886
+ || raw.response_id
1887
+ || raw.request_id
1888
+ || null;
1889
+ return brain.recordModelUsage?.({
1890
+ source: 'wall-e.chat',
1891
+ feature,
1892
+ sessionId,
1893
+ providerType,
1894
+ providerId: extra.providerId || (routeMatches ? selectedRoute.providerId : null) || null,
1895
+ modelId,
1896
+ modelRegistryId: extra.modelRegistryId || (routeMatches ? selectedRoute.registryId : null) || null,
1897
+ gatewayType: extra.gatewayType || (routeMatches ? selectedRoute.gatewayType : null) || null,
1898
+ routeLabel: extra.routeLabel || (routeMatches ? selectedRoute.routeLabel : null) || null,
1899
+ requestId,
1900
+ usage: response.usage,
1901
+ latencyMs: response.latencyMs,
1902
+ stopReason: response.stopReason || null,
1903
+ metadata: {
1904
+ channel,
1905
+ taskType: opts.taskType || 'chat',
1906
+ turn: extra.turn ?? null,
1907
+ fallback: !!extra.fallback,
1908
+ repair: extra.repair || null,
1909
+ runtimeProfile: wallERuntimeProfile.profileId,
1910
+ },
1911
+ });
1912
+ } catch (err) {
1913
+ console.warn('[chat] Failed to record model usage ledger:', err.message);
1914
+ return null;
1915
+ }
1916
+ };
1653
1917
  const progressSink = opts.onProgress || (() => {});
1654
1918
  const onProgress = (event) => {
1655
1919
  try { sessionRecorder.appendProgress(event); } catch (err) {
@@ -1657,6 +1921,16 @@ async function chat(message, opts = {}) {
1657
1921
  }
1658
1922
  return progressSink(event);
1659
1923
  };
1924
+ const recordSessionFrame = (frame, extra = {}) => {
1925
+ try {
1926
+ return typeof sessionRecorder.appendFrame === 'function'
1927
+ ? sessionRecorder.appendFrame(frame, extra)
1928
+ : sessionRecorder.appendProgress({ type: 'conversation_frame', frame, ...(extra || {}) });
1929
+ } catch (err) {
1930
+ console.error('[chat] Failed to append conversation frame:', err.message);
1931
+ return null;
1932
+ }
1933
+ };
1660
1934
  const returnSystemReply = (text, meta = {}) => {
1661
1935
  const persistStart = Date.now();
1662
1936
  if (!opts._fallbackRetry) {
@@ -1688,7 +1962,11 @@ async function chat(message, opts = {}) {
1688
1962
  metadata: JSON.stringify({ channel }),
1689
1963
  });
1690
1964
  }
1691
- const assistantMessage = brain.insertChatMessage({ role: 'assistant', content: text, channel, session_id: sessionId });
1965
+ const assistantMessage = brain.insertChatMessage({
1966
+ role: 'assistant', content: text, channel, session_id: sessionId,
1967
+ model_id: meta.model || null,
1968
+ model_provider: meta.provider || null,
1969
+ });
1692
1970
  recordSessionMessage('assistant', text, {
1693
1971
  dbMessageId: assistantMessage.id,
1694
1972
  provider: meta.provider || 'system',
@@ -1740,6 +2018,35 @@ async function chat(message, opts = {}) {
1740
2018
  });
1741
2019
  }
1742
2020
 
2021
+ const runtimeFrameContextMessages = (() => {
2022
+ const override = normalizeContextMessagesForChat(opts.contextMessages, message);
2023
+ if (override.length > 0) return override;
2024
+ try {
2025
+ const recentChat = brain.listChatMessages({ session_id: sessionId, limit: 20 });
2026
+ return normalizeContextMessagesForChat(
2027
+ recentChat.map(m => ({ role: m.role, content: m.content })),
2028
+ message,
2029
+ );
2030
+ } catch {
2031
+ return normalizeContextMessagesForChat([], message);
2032
+ }
2033
+ })();
2034
+ const conversationRuntime = buildConversationRuntime({
2035
+ messages: runtimeFrameContextMessages,
2036
+ currentMessage: message,
2037
+ previousFrame: existingSessionMetadata.conversationFrame || existingSessionMetadata.activeFrame || null,
2038
+ channel,
2039
+ cwd: effectiveCwd || opts.cwd || process.cwd(),
2040
+ taskType: opts.taskType || 'chat',
2041
+ });
2042
+ const conversationFrameContextBlock = buildConversationFramePrompt(conversationRuntime.frame);
2043
+ if (conversationFrameContextBlock) {
2044
+ recordSessionFrame(conversationRuntime.frame, {
2045
+ reason: 'turn_start',
2046
+ validators: conversationRuntime.validators,
2047
+ });
2048
+ }
2049
+
1743
2050
  let currentLocationSummary = null;
1744
2051
  if (shouldUseLocationForMessage(routingMessage, queryTopics)) {
1745
2052
  currentLocationSummary = await resolveLocationForMessage(opts.locationRequest || opts.req || null, brain, routingMessage, {
@@ -1841,7 +2148,7 @@ async function chat(message, opts = {}) {
1841
2148
  intent,
1842
2149
  topics: queryTopics,
1843
2150
  currentLocation: currentLocationContext,
1844
- }) + reviewContextBlock + codeReviewContextBlock + workspaceContextBlock + promptCapabilityContextBlock;
2151
+ }) + conversationFrameContextBlock + reviewContextBlock + codeReviewContextBlock + workspaceContextBlock + promptCapabilityContextBlock;
1845
2152
  // Item D (multi-agent deep-dive): opt-in workspace directives override
1846
2153
  // for the chat surface. Tutorial-style or persona-tweak directives can
1847
2154
  // live in `wall-e/loops/chat.directives.md` (or
@@ -1851,11 +2158,14 @@ async function chat(message, opts = {}) {
1851
2158
  const wallECodingScopeReminder = wallECodingMode
1852
2159
  ? '\n\n## Wall-E Coding Scope Reminder\nMatch the phase the user asked for. If the user asks to design, plan, analyze, review, or discuss without asking for changes, deliver that artifact and stop. If the user combines review/analyze/design with improve, polish, enhance, upgrade, fix, build, implement, refactor, code, edit, update, or change, then actually perform the requested change or start the coding agent; do not stop at a review-only answer.'
1853
2160
  : '';
2161
+ const wallECodingContinuationReminder = wallECodingMode && codingTurnIntent.readOnly && !codingTurnIntent.expectsChange
2162
+ ? '\n\n## Wall-E Coding Conversation Continuation\nThis turn is read-only or conversational. Do not start a coding task, do not call start_coding, and do not evaluate code-change completion. Use the existing conversation, supplied context, and any read-only tool results to answer directly. If the user asks to refine, update, or correct a prior summary, produce the updated summary now. Ask for more input only when the missing detail is truly required and say what evidence you checked.'
2163
+ : '';
1854
2164
  const finalLanguageReminder = buildResponseLanguagePolicy({
1855
2165
  heading: '## Final Response Language Reminder',
1856
2166
  userMessage: message,
1857
2167
  });
1858
- const systemPrompt = `${augmentedSystemPrompt}${wallECodingScopeReminder}\n\n${finalLanguageReminder}`;
2168
+ const systemPrompt = `${augmentedSystemPrompt}${wallECodingScopeReminder}${wallECodingContinuationReminder}\n\n${finalLanguageReminder}`;
1859
2169
  timings.promptBuildMs = Date.now() - promptStart;
1860
2170
 
1861
2171
  // Expire old sessions occasionally (at most once per hour)
@@ -1875,14 +2185,22 @@ async function chat(message, opts = {}) {
1875
2185
  let provider = _clientOverride || null;
1876
2186
  if (!provider) {
1877
2187
  const routeConfig = selectedRoute.providerConfig || {};
1878
- const providerConfig = { ...routeConfig, ...(opts.providerConfig || {}) };
1879
- const hasSpecificConfig = !!(providerConfig.apiKey || providerConfig.baseUrl || providerConfig.customHeaders);
2188
+ const suppliedProviderConfig = { ...routeConfig, ...(opts.providerConfig || {}) };
2189
+ const hasSpecificConfig = !!(
2190
+ suppliedProviderConfig.apiKey
2191
+ || suppliedProviderConfig.baseUrl
2192
+ || suppliedProviderConfig.customHeaders
2193
+ || suppliedProviderConfig.authMode
2194
+ || suppliedProviderConfig.refreshToken
2195
+ );
1880
2196
  if (opts.providerFactory || targetProviderType !== defaultProviderType || opts.provider || hasSpecificConfig) {
2197
+ const providerConfig = getProviderRuntimeConfig(targetProviderType, suppliedProviderConfig);
1881
2198
  provider = _createChatProvider(targetProviderType, providerConfig, opts);
1882
2199
  } else {
1883
2200
  provider = getDefaultClient();
1884
2201
  }
1885
2202
  }
2203
+ provider = withProviderMessageGuard(provider);
1886
2204
 
1887
2205
  // Engine swap: if using ollama and user prefers MLX, transparently switch
1888
2206
  if (!_clientOverride && (provider.type === 'ollama' || opts.provider === 'ollama')) {
@@ -1890,6 +2208,7 @@ async function chat(message, opts = {}) {
1890
2208
  const localEngine = brain.getDb().prepare("SELECT value FROM brain_metadata WHERE key = 'local_engine'").get()?.value;
1891
2209
  if (localEngine === 'mlx') {
1892
2210
  provider = _createChatProvider('mlx', {}, opts);
2211
+ provider = withProviderMessageGuard(provider);
1893
2212
  }
1894
2213
  } catch (e) {
1895
2214
  console.warn('[chat] MLX engine swap failed, staying on Ollama:', e.message);
@@ -1897,6 +2216,20 @@ async function chat(message, opts = {}) {
1897
2216
  }
1898
2217
 
1899
2218
  selectedModel = resolveCompatibleModel(selectedModel || getDefaultModelForProvider(targetProviderType), provider.type || targetProviderType);
2219
+ const primaryProviderErrorContext = (providerType, model) => {
2220
+ const resolvedProvider = providerType || targetProviderType || getDefaultProviderType();
2221
+ const resolvedModel = model || selectedModel;
2222
+ const routeApplies = selectedRoute
2223
+ && (!selectedRoute.provider || selectedRoute.provider === resolvedProvider)
2224
+ && (!selectedRoute.model || selectedRoute.model === resolvedModel);
2225
+ return {
2226
+ provider: resolvedProvider,
2227
+ model: resolvedModel,
2228
+ providerId: routeApplies ? (selectedRoute.providerId || null) : null,
2229
+ registryId: routeApplies ? (selectedRoute.registryId || null) : null,
2230
+ routeLabel: routeApplies ? (selectedRoute.routeLabel || null) : null,
2231
+ };
2232
+ };
1900
2233
 
1901
2234
  const codeReviewFastPath = codeReviewRequested
1902
2235
  && isUsableCodeReviewSnapshot(codeReviewContextBlock)
@@ -1965,6 +2298,35 @@ async function chat(message, opts = {}) {
1965
2298
  description: `Run one of my skills to fetch data or perform an action. PREFER skills over raw mcp_call when a matching skill exists — skills parse data correctly and store results in brain. Available skills: ${(() => { try { const { loadAllSkills } = require('./skills/skill-loader'); const bundled = loadAllSkills().map(s => s.name + ': ' + (s.description || '').slice(0, 60)); const db = brain.listSkills({ enabled: 1 }).map(s => s.name + ': ' + (s.description || '').slice(0, 60)); const all = [...new Set([...bundled, ...db])]; return all.join('; ') || 'none'; } catch { return 'unknown'; } })()}`,
1966
2299
  input_schema: { type: 'object', properties: { skill_name: { type: 'string', description: 'Name of the skill to run' } }, required: ['skill_name'] },
1967
2300
  },
2301
+ {
2302
+ name: 'ctm_context',
2303
+ description: 'Authoritative CTM operational/session-memory lookup for CTM primary, mobile phone URL, /m/, Dev Tunnel, Tailscale, and "CTM session memory" questions. Use this before generic memories for CTM remote-access answers.',
2304
+ input_schema: {
2305
+ type: 'object',
2306
+ properties: {
2307
+ query: { type: 'string', description: 'The CTM/mobile/session-memory question to resolve. Defaults to the user message.' },
2308
+ limit: { type: 'number', default: 5 },
2309
+ force: { type: 'boolean', default: true },
2310
+ },
2311
+ },
2312
+ },
2313
+ {
2314
+ name: 'ctm_remote_access_status',
2315
+ description: 'Read current CTM setup/network status and return active phone URLs and tunnel state. Use for "what URL should my phone use?"',
2316
+ input_schema: { type: 'object', properties: {} },
2317
+ },
2318
+ {
2319
+ name: 'ctm_session_search',
2320
+ description: 'Search cached CTM coding-session messages. Use for questions that explicitly ask for CTM session memory, prior coding sessions, or remembered CTM operational fixes.',
2321
+ input_schema: {
2322
+ type: 'object',
2323
+ properties: {
2324
+ query: { type: 'string', description: 'Search query' },
2325
+ limit: { type: 'number', default: 5 },
2326
+ },
2327
+ required: ['query'],
2328
+ },
2329
+ },
1968
2330
  {
1969
2331
  name: 'search_memories',
1970
2332
  description: 'Hybrid search (BM25 + semantic vectors) across private/user memory. Use before public web search for remembered context, prior discussions, decisions, preferences, people, projects, tools, or Slack/email/calendar work context. Call MULTIPLE searches in ONE turn to batch them.',
@@ -2126,7 +2488,14 @@ async function chat(message, opts = {}) {
2126
2488
  ]);
2127
2489
 
2128
2490
  // Intent-based tool filtering — fewer tools = fewer wrong choices
2129
- const CONVERSATIONAL_TOOLS = new Set(['think', 'remember_fact', 'search_memories']);
2491
+ const CONVERSATIONAL_TOOLS = new Set([
2492
+ 'think',
2493
+ 'remember_fact',
2494
+ 'search_memories',
2495
+ 'ctm_context',
2496
+ 'ctm_remote_access_status',
2497
+ 'ctm_session_search',
2498
+ ]);
2130
2499
  const DIRECT_ACTION_EXCLUDE = new Set([
2131
2500
  'search_memories', 'lookup_person', 'remember_fact', 'think',
2132
2501
  'pull_slack', 'slack_connect', 'slack_search', 'slack_read_channel', 'slack_send_message',
@@ -2148,6 +2517,7 @@ async function chat(message, opts = {}) {
2148
2517
 
2149
2518
  function isCodingActionRequest() {
2150
2519
  const text = String(routingMessage || '');
2520
+ if (codingTurnIntent.readOnly && !codingTurnIntent.expectsChange) return false;
2151
2521
  if (CODING_AGENT_RE.test(text)) return true;
2152
2522
  if (STRONG_CODE_CHANGE_RE.test(text)) return queryTopics.includes('technical') || CODE_TARGET_RE.test(text);
2153
2523
  return GENERIC_CODE_CHANGE_RE.test(text) && CODE_TARGET_RE.test(text);
@@ -2194,6 +2564,20 @@ async function chat(message, opts = {}) {
2194
2564
  return filtered;
2195
2565
  }
2196
2566
 
2567
+ const chatToolRegistry = createWallEToolRegistry({
2568
+ runtimeProfile: wallERuntimeProfile,
2569
+ builtinTools: chatTools,
2570
+ prepareBuiltinArgs: (toolName, args) => normalizeToolCall({ name: toolName, input: args }).input,
2571
+ executeBuiltinTool: (toolName, args, ctx) => executeChatTool(toolName, args, ctx),
2572
+ unknownToolHandler: (toolName, args, ctx) => executeChatTool(toolName, args, ctx),
2573
+ });
2574
+ const chatToolContextBase = runtimeToolContext(wallERuntimeProfile, {
2575
+ provider: usedProvider || targetProviderType,
2576
+ model: selectedModel,
2577
+ sessionId,
2578
+ cwd: effectiveCwd || opts.cwd || process.cwd(),
2579
+ });
2580
+
2197
2581
  for (const event of requestedSkillEvents) onProgress(event);
2198
2582
 
2199
2583
  // Execute a chat tool call
@@ -2220,14 +2604,16 @@ async function chat(message, opts = {}) {
2220
2604
  if (effectiveCwd && input && typeof input === 'object') {
2221
2605
  if (name === 'run_shell' && !input.cwd) {
2222
2606
  input = { ...input, cwd: effectiveCwd };
2223
- } else if (name === 'search_files' && !input.directory) {
2224
- input = { ...input, directory: effectiveCwd };
2607
+ } else if (name === 'search_files') {
2608
+ input = { ...input, directory: input.directory || effectiveCwd, projectRoot: input.projectRoot || effectiveCwd };
2225
2609
  } else if (name === 'glob') {
2226
2610
  input = { ...input, directory: input.directory || effectiveCwd, projectRoot: input.projectRoot || effectiveCwd };
2227
2611
  } else if (name === 'grep_files') {
2228
2612
  input = { ...input, directory: input.directory || effectiveCwd, projectRoot: input.projectRoot || effectiveCwd };
2229
2613
  } else if ((name === 'read_file' || name === 'write_file' || name === 'edit_file') && !input.projectRoot) {
2230
2614
  input = { ...input, projectRoot: effectiveCwd };
2615
+ } else if (name === 'start_static_server') {
2616
+ input = { ...input, directory: input.directory || effectiveCwd, projectRoot: input.projectRoot || effectiveCwd };
2231
2617
  } else if (name === 'list_directory') {
2232
2618
  input = { ...input, directory: input.directory || effectiveCwd, projectRoot: input.projectRoot || effectiveCwd };
2233
2619
  }
@@ -2242,9 +2628,18 @@ async function chat(message, opts = {}) {
2242
2628
  if (!externalActionReview.admitted) {
2243
2629
  return externalActionReview.result;
2244
2630
  }
2631
+ if (externalActionReview.envelope && isExternalActionTool(name)) {
2632
+ const executeExternalAction = typeof opts.externalActionExecutor === 'function'
2633
+ ? opts.externalActionExecutor
2634
+ : executeLocalTool;
2635
+ return executeExternalAction(name, input, {
2636
+ envelope: externalActionReview.envelope,
2637
+ approval: externalActionReview.envelope.approval || null,
2638
+ });
2639
+ }
2245
2640
 
2246
2641
  // Permission check — skip for internal-only tools
2247
- const SKIP_PERM_CHECK = new Set(['think', 'search_memories', 'lookup_person', 'remember_fact', 'list_mcp_tools', 'list_tasks', 'calendar_events', 'calendar_list', 'system_info', 'clipboard_read', 'mail_messages', 'mail_search', 'read_project_file', 'search_project', 'read_file', 'glob', 'grep_files', 'list_directory']);
2642
+ const SKIP_PERM_CHECK = new Set(['think', 'search_memories', 'ctm_context', 'ctm_remote_access_status', 'ctm_session_search', 'lookup_person', 'remember_fact', 'list_mcp_tools', 'list_tasks', 'calendar_events', 'calendar_list', 'system_info', 'clipboard_read', 'mail_messages', 'mail_search', 'read_project_file', 'search_project', 'read_file', 'glob', 'grep_files', 'list_directory', 'check_url', 'start_static_server', 'stop_static_server']);
2248
2643
  if (!SKIP_PERM_CHECK.has(name)) {
2249
2644
  const permCommand = name === 'run_shell' ? (input.command || '')
2250
2645
  : name === 'write_file' || name === 'read_file' ? (input.file_path || '')
@@ -2336,6 +2731,39 @@ async function chat(message, opts = {}) {
2336
2731
  return { error: err.message };
2337
2732
  }
2338
2733
  }
2734
+ if (name === 'ctm_context') {
2735
+ const query = String(input.query || routingMessage || message || '').trim();
2736
+ return ctmOperationalContext.lookupCtmOperationalContext({
2737
+ query,
2738
+ limit: input.limit || 5,
2739
+ force: input.force !== false,
2740
+ }, {
2741
+ timeoutMs: input.timeout_ms || 6000,
2742
+ cooldownMs: input.cooldown_ms || 0,
2743
+ });
2744
+ }
2745
+ if (name === 'ctm_remote_access_status') {
2746
+ const status = await ctmContextClient.getRemoteAccessStatus({}, {
2747
+ timeoutMs: input.timeout_ms || 6000,
2748
+ cooldownMs: input.cooldown_ms || 0,
2749
+ });
2750
+ return {
2751
+ ok: !status.reason,
2752
+ source: status.source || 'ctm-api',
2753
+ reason: status.reason || '',
2754
+ api_transport: status.api_transport || null,
2755
+ remote_access: status.reason ? null : ctmOperationalContext.summarizeRemoteAccess(status),
2756
+ };
2757
+ }
2758
+ if (name === 'ctm_session_search') {
2759
+ return ctmContextClient.searchSessions({
2760
+ query: input.query,
2761
+ limit: input.limit || 5,
2762
+ }, {
2763
+ timeoutMs: input.timeout_ms || 6000,
2764
+ cooldownMs: input.cooldown_ms || 0,
2765
+ });
2766
+ }
2339
2767
  if (name === 'search_memories') {
2340
2768
  const sanitizedInput = { ...input, query: sanitizeQuery(input.query) || input.query };
2341
2769
  // Hybrid search: FTS5/LIKE + vector similarity (if available)
@@ -2422,16 +2850,15 @@ async function chat(message, opts = {}) {
2422
2850
  }
2423
2851
  }
2424
2852
 
2425
- // Touch accessed memories for importance decay tracking
2426
- for (const m of results) {
2427
- if (m.id) try { brain.touchMemory(m.id); } catch {}
2428
- }
2853
+ // Touch accessed memories for importance decay tracking — one batched UPDATE
2854
+ // instead of an N+1 of write-locked statements (one per result row).
2855
+ try { brain.touchMemories(results.map(m => m.id)); } catch {}
2429
2856
 
2430
- const totalSlack = db.prepare('SELECT count(*) as c FROM memories WHERE source = ?').get('slack').c;
2857
+ const totals = _searchTotals(db);
2431
2858
  return {
2432
2859
  count: results.length,
2433
- total_memories: db.prepare('SELECT count(*) as c FROM memories').get().c,
2434
- total_slack: totalSlack,
2860
+ total_memories: totals.total_memories,
2861
+ total_slack: totals.total_slack,
2435
2862
  search_method: searchMethod,
2436
2863
  memories: results.map(m => ({
2437
2864
  source: m.source,
@@ -2585,6 +3012,17 @@ async function chat(message, opts = {}) {
2585
3012
  return { created: true, id: result.id, title: input.title, status: 'pending', due_at: input.due_at || 'immediate' };
2586
3013
  }
2587
3014
  if (name === 'start_coding') {
3015
+ const requestedCodingIntent = classifyCodingTurnIntent(input?.request || '');
3016
+ if (requestedCodingIntent.readOnly && !requestedCodingIntent.expectsChange) {
3017
+ return {
3018
+ success: true,
3019
+ skipped: true,
3020
+ read_only: true,
3021
+ intent: requestedCodingIntent,
3022
+ message: 'This is a conversational/read-only follow-up, not a coding task. Answer from the existing conversation and context; do not run a coding task or append a no-code-changes blocker.',
3023
+ };
3024
+ }
3025
+
2588
3026
  ensureBrainInit();
2589
3027
 
2590
3028
  // Inline execution with progress streaming (default)
@@ -2649,9 +3087,18 @@ async function chat(message, opts = {}) {
2649
3087
  progress: progressLog.join('\n'),
2650
3088
  };
2651
3089
  } catch (err) {
3090
+ // Classify provider/LLM failures so a coding task that couldn't reach the model
3091
+ // reports a clear message ("AI provider network error: …could not reach the
3092
+ // provider endpoint…") instead of a raw "fetch failed".
3093
+ let friendly = (err && err.message) || 'Coding task failed';
3094
+ try {
3095
+ const { decorateProviderError } = require('./llm/provider-error');
3096
+ const pe = decorateProviderError(err, { model: (input && input.model) || undefined }).providerError;
3097
+ if (pe && pe.userMessage) friendly = pe.userMessage;
3098
+ } catch {}
2652
3099
  return {
2653
3100
  success: false,
2654
- error: err.message,
3101
+ error: friendly,
2655
3102
  progress: progressLog.join('\n'),
2656
3103
  };
2657
3104
  }
@@ -2815,7 +3262,10 @@ async function chat(message, opts = {}) {
2815
3262
  // Load recent chat history, resuming from compacted state if available
2816
3263
  const chatSessionId = sessionId;
2817
3264
  let historyMessages;
2818
- if (existingSession?.compacted_messages) {
3265
+ const contextOverride = normalizeContextMessagesForChat(opts.contextMessages, message);
3266
+ if (contextOverride.length > 0) {
3267
+ historyMessages = contextOverride;
3268
+ } else if (existingSession?.compacted_messages) {
2819
3269
  // Resume from compacted state + recent messages
2820
3270
  try {
2821
3271
  const compacted = JSON.parse(existingSession.compacted_messages);
@@ -2831,36 +3281,17 @@ async function chat(message, opts = {}) {
2831
3281
  const recentChat = brain.listChatMessages({ session_id: chatSessionId, limit: 10 });
2832
3282
  historyMessages = recentChat.map(m => ({ role: m.role, content: m.content }));
2833
3283
  }
2834
- if (!persistUserTurn) {
2835
- const last = historyMessages[historyMessages.length - 1];
2836
- const lastContent = typeof last?.content === 'string' ? last.content : JSON.stringify(last?.content || '');
2837
- if (!last || last.role !== 'user' || lastContent !== message) {
2838
- historyMessages.push({ role: 'user', content: message });
2839
- }
2840
- }
2841
- // Agentic chat loop — history already includes the new user message
2842
- const messages = [...historyMessages];
2843
- // Attachment hydration: when the current turn carries attachments, the
2844
- // last user message in `messages` is the row we just inserted with
2845
- // string content. Replace its `content` with a content-block array so
2846
- // the LLM actually sees the image(s).
2847
- if (_attachmentBlocks && messages.length > 0) {
2848
- // Walk back to the last user-role entry (should be the one at -1
2849
- // for normal flows; defensive scan handles compacted resume edge).
2850
- for (let i = messages.length - 1; i >= 0; i--) {
2851
- if (messages[i].role === 'user') {
2852
- const text = typeof messages[i].content === 'string' ? messages[i].content : message;
2853
- messages[i] = {
2854
- role: 'user',
2855
- content: [
2856
- { type: 'text', text },
2857
- ..._attachmentBlocks,
2858
- ],
2859
- };
2860
- break;
2861
- }
2862
- }
2863
- }
3284
+ // Provider-boundary assembly is the authority for the current turn.
3285
+ // Persistence happens before the model call, but a DB reread can still be
3286
+ // stale or compacted. Always reassert the accepted user input here so no
3287
+ // provider can receive an empty message list.
3288
+ const messages = buildProviderMessagesForChat({
3289
+ historyMessages,
3290
+ currentMessage: message,
3291
+ attachmentBlocks: _attachmentBlocks,
3292
+ provider: provider.type || targetProviderType,
3293
+ model: selectedModel,
3294
+ });
2864
3295
  let finalText = '';
2865
3296
  let lastTurnText = ''; // Only the last turn's text (prevents error accumulation across turns)
2866
3297
  let lastTurn = 0;
@@ -2868,6 +3299,8 @@ async function chat(message, opts = {}) {
2868
3299
  let forceFinalNoTools = false;
2869
3300
  let toolLimitNoticeSent = false;
2870
3301
  let postToolProgressContinuationCount = 0;
3302
+ let actionCompletionContinuationCount = 0;
3303
+ let localPreviewVerificationContinuationCount = 0;
2871
3304
 
2872
3305
  // Adaptive limits based on intent classification
2873
3306
  const INTENT_LIMITS = {
@@ -2996,13 +3429,36 @@ async function chat(message, opts = {}) {
2996
3429
 
2997
3430
  resetTurnTimeout();
2998
3431
  const { withRetry } = require('./llm/retry');
2999
- const toolsForTurn = (() => {
3432
+ const toolsForTurn = await (async () => {
3000
3433
  if (codeReviewFastPath || forceFinalNoTools) return [];
3001
- let tools = opts.allowedTools ? chatTools.filter(t => opts.allowedTools.includes(t.name)) : chatTools;
3434
+ const registryTools = await getRuntimeToolDefinitions(chatToolRegistry, wallERuntimeProfile, {
3435
+ ...chatToolContextBase,
3436
+ provider: usedProvider || targetProviderType,
3437
+ model: selectedModel,
3438
+ });
3439
+ let tools = opts.allowedTools ? registryTools.filter(t => opts.allowedTools.includes(t.name)) : registryTools;
3002
3440
  tools = filterToolsForIntent(tools, intent);
3003
3441
  if (channel === 'task' || channel === 'eval') tools = tools.filter(t => !TASK_CHANNEL_EXCLUDE.has(t.name));
3004
3442
  return tools;
3005
3443
  })();
3444
+ const runtimePromptManifest = buildRuntimePromptManifest({
3445
+ runtimeProfile: wallERuntimeProfile,
3446
+ systemPrompt,
3447
+ userTask: message,
3448
+ messages,
3449
+ tools: toolsForTurn,
3450
+ provider: usedProvider || targetProviderType,
3451
+ model: selectedModel,
3452
+ metadata: { channel, turn },
3453
+ });
3454
+ try {
3455
+ _telemetry.track('prompt_manifest', {
3456
+ runtime_profile: wallERuntimeProfile.profileId,
3457
+ stable_sections: runtimePromptManifest.stableSectionCount,
3458
+ dynamic_sections: runtimePromptManifest.dynamicSectionCount,
3459
+ token_estimate: runtimePromptManifest.tokenEstimate,
3460
+ });
3461
+ } catch {}
3006
3462
  const primaryRetries = (_allowChatProviderFallback(opts)
3007
3463
  && _allowCrossProviderFallbackForTurn({
3008
3464
  opts,
@@ -3037,8 +3493,7 @@ async function chat(message, opts = {}) {
3037
3493
  if (registeredProv) providerAvailability.recordFailure(registeredProv.providerId, llmErr.message);
3038
3494
  } catch {}
3039
3495
  const decorated = decorateProviderError(llmErr, {
3040
- provider: usedProvider || targetProviderType || getDefaultProviderType(),
3041
- model: selectedModel,
3496
+ ...primaryProviderErrorContext(usedProvider || targetProviderType || getDefaultProviderType(), selectedModel),
3042
3497
  });
3043
3498
  const allowCrossProviderFallback = _allowCrossProviderFallbackForTurn({
3044
3499
  opts,
@@ -3060,7 +3515,7 @@ async function chat(message, opts = {}) {
3060
3515
  let lastFallbackDecorated = null;
3061
3516
  for (const fallback of fallbacks) {
3062
3517
  if (attemptedProviderTypes.has(fallback.providerType) || attemptedProviderTypes.has(fallback.provider?.type)) continue;
3063
- provider = fallback.provider;
3518
+ provider = withProviderMessageGuard(fallback.provider);
3064
3519
  selectedModel = fallback.model;
3065
3520
  usedProvider = fallback.provider.type || fallback.providerType;
3066
3521
  attemptedProviderTypes.add(fallback.providerType);
@@ -3098,6 +3553,7 @@ async function chat(message, opts = {}) {
3098
3553
  const fallbackDecorated = decorateProviderError(fallbackErr, {
3099
3554
  provider: fallback.providerType,
3100
3555
  model: fallback.model,
3556
+ providerId: fallback.providerId || null,
3101
3557
  });
3102
3558
  lastFallbackDecorated = fallbackDecorated;
3103
3559
  recordProviderFailureAlert(fallbackDecorated.providerError, brain);
@@ -3153,6 +3609,11 @@ async function chat(message, opts = {}) {
3153
3609
 
3154
3610
  const textToolResponse = extractTextToolCalls(response);
3155
3611
  if (textToolResponse.textToolCallFormat) {
3612
+ warnOnTextToolCallFormatMismatch(
3613
+ response.provider || usedProvider || targetProviderType,
3614
+ response.model || selectedModel,
3615
+ textToolResponse.textToolCallFormat,
3616
+ );
3156
3617
  const allowedToolNames = new Set((Array.isArray(toolsForTurn) ? toolsForTurn : []).map((tool) => tool.name));
3157
3618
  const allowedToolCalls = textToolResponse.toolCalls.filter((call) => allowedToolNames.has(call.name));
3158
3619
  response = { ...textToolResponse, toolCalls: allowedToolCalls };
@@ -3175,6 +3636,7 @@ async function chat(message, opts = {}) {
3175
3636
  if (response.usage) {
3176
3637
  totalInputTokens += response.usage.input || 0;
3177
3638
  totalOutputTokens += response.usage.output || 0;
3639
+ recordUsageLedger(response, 'turn', { turn });
3178
3640
  }
3179
3641
  if (!usedModel) { usedModel = response.model; usedProvider = response.provider; }
3180
3642
 
@@ -3266,6 +3728,84 @@ async function chat(message, opts = {}) {
3266
3728
  console.log('[chat] Premature post-tool progress text — continuing tool loop');
3267
3729
  continue;
3268
3730
  }
3731
+
3732
+ const shouldRunActionCompletionGuard = (wallECodingMode || isCodingActionRequest())
3733
+ && !forceFinalNoTools
3734
+ && !(codingTurnIntent.readOnly && !codingTurnIntent.expectsChange);
3735
+ if (shouldRunActionCompletionGuard) {
3736
+ let hasTurnBudgetForActionContinuation = turn + 1 < allowedTurns;
3737
+ if (!hasTurnBudgetForActionContinuation) {
3738
+ hasTurnBudgetForActionContinuation = extendCodingTurnBudget('action completion guard');
3739
+ }
3740
+ const toolsAvailableForActionContinuation = Array.isArray(toolsForTurn)
3741
+ && toolsForTurn.length > 0
3742
+ && toolCallCount < MAX_TOOL_CALLS
3743
+ && hasTurnBudgetForActionContinuation;
3744
+ const actionContinuation = _chatNoActionContinuation({
3745
+ prompt: routingMessage || message,
3746
+ content: finalText,
3747
+ toolCallHistory: _chatToolCallHistoryForActionGuard(allToolCalls),
3748
+ toolsAvailable: toolsAvailableForActionContinuation,
3749
+ nudges: actionCompletionContinuationCount,
3750
+ maxNudges: wallECodingMode ? 3 : 1,
3751
+ cwd: effectiveCwd || opts.cwd || process.cwd(),
3752
+ codingIntent: codingTurnIntent,
3753
+ });
3754
+ if (actionContinuation?.action === 'continue') {
3755
+ actionCompletionContinuationCount += 1;
3756
+ const assistantHistoryContent = [];
3757
+ if (response.reasoningContent && typeof response.reasoningContent === 'string') {
3758
+ assistantHistoryContent.push({ type: 'reasoning', text: response.reasoningContent });
3759
+ }
3760
+ if (finalText) assistantHistoryContent.push({ type: 'text', text: finalText });
3761
+ messages.push({
3762
+ role: 'assistant',
3763
+ content: assistantHistoryContent.length > 0 ? assistantHistoryContent : finalText,
3764
+ });
3765
+ messages.push({
3766
+ role: 'user',
3767
+ content: `${actionContinuation.message}\n` +
3768
+ 'If the target project is outside the current working directory, do not run broad filesystem scans. Use start_coding with cwd set to the explicit target project directory, or use project-scoped file tools against that directory. For website/UI/UX requests, finish only after concrete edits and verification evidence.',
3769
+ });
3770
+ console.log('[chat] Action completion guard — continuing tool loop:', actionContinuation.reason);
3771
+ continue;
3772
+ }
3773
+ if (actionContinuation?.action === 'fail') {
3774
+ finalText = `I could not complete the requested code change: ${actionContinuation.reason}`;
3775
+ }
3776
+ }
3777
+ const localPreviewClaim = analyzeLocalPreviewClaims(finalText, allToolCalls);
3778
+ if (!forceFinalNoTools
3779
+ && localPreviewClaim.hasPositiveClaim
3780
+ && !localPreviewClaim.ok
3781
+ && localPreviewVerificationContinuationCount < 2) {
3782
+ let hasTurnBudgetForLocalPreviewVerification = turn + 1 < allowedTurns;
3783
+ if (!hasTurnBudgetForLocalPreviewVerification) {
3784
+ hasTurnBudgetForLocalPreviewVerification = extendCodingTurnBudget('local preview verification guard');
3785
+ }
3786
+ const toolsAvailableForLocalPreviewVerification = Array.isArray(toolsForTurn)
3787
+ && toolsForTurn.length > 0
3788
+ && toolCallCount < MAX_TOOL_CALLS
3789
+ && hasTurnBudgetForLocalPreviewVerification;
3790
+ if (toolsAvailableForLocalPreviewVerification) {
3791
+ localPreviewVerificationContinuationCount += 1;
3792
+ const assistantHistoryContent = [];
3793
+ if (response.reasoningContent && typeof response.reasoningContent === 'string') {
3794
+ assistantHistoryContent.push({ type: 'reasoning', text: response.reasoningContent });
3795
+ }
3796
+ if (finalText) assistantHistoryContent.push({ type: 'text', text: finalText });
3797
+ messages.push({
3798
+ role: 'assistant',
3799
+ content: assistantHistoryContent.length > 0 ? assistantHistoryContent : finalText,
3800
+ });
3801
+ messages.push({
3802
+ role: 'user',
3803
+ content: buildLocalPreviewVerificationNudge(localPreviewClaim),
3804
+ });
3805
+ console.warn('[chat] Local preview claim lacked tool evidence; continuing tool loop');
3806
+ continue;
3807
+ }
3808
+ }
3269
3809
  finalResponseMeta = {
3270
3810
  model: response.model || selectedModel,
3271
3811
  provider: response.provider || usedProvider || targetProviderType,
@@ -3280,6 +3820,12 @@ async function chat(message, opts = {}) {
3280
3820
  for (const tu of response.toolCalls) {
3281
3821
  const summary = tu.name === 'search_memories'
3282
3822
  ? `Searching: "${tu.input.query}"${tu.input.source ? ` (${tu.input.source})` : ''}`
3823
+ : tu.name === 'ctm_context'
3824
+ ? 'Checking CTM operational context...'
3825
+ : tu.name === 'ctm_remote_access_status'
3826
+ ? 'Checking CTM phone access...'
3827
+ : tu.name === 'ctm_session_search'
3828
+ ? `Searching CTM sessions: "${tu.input.query}"`
3283
3829
  : tu.name === 'think'
3284
3830
  ? 'Analyzing evidence...'
3285
3831
  : tu.name === 'lookup_person'
@@ -3338,7 +3884,13 @@ async function chat(message, opts = {}) {
3338
3884
  const executeToolCall = async (tu) => {
3339
3885
  const t0 = Date.now();
3340
3886
  console.log('[chat] Tool call:', tu.name, JSON.stringify(tu.input).slice(0, 150));
3341
- const result = await executeChatTool(tu.name, tu.input);
3887
+ const result = await executeRuntimeTool(chatToolRegistry, wallERuntimeProfile, tu.name, tu.input, {
3888
+ ...chatToolContextBase,
3889
+ provider: usedProvider || targetProviderType,
3890
+ model: selectedModel,
3891
+ sessionId,
3892
+ cwd: effectiveCwd || opts.cwd || process.cwd(),
3893
+ });
3342
3894
  const resultStr = JSON.stringify(result);
3343
3895
  const elapsed = Date.now() - t0;
3344
3896
  timings.toolMs += elapsed;
@@ -3346,18 +3898,25 @@ async function chat(message, opts = {}) {
3346
3898
  // Per-tool-call telemetry: name + status + latency only. No tool args
3347
3899
  // or result content (those can carry user data); error_type is the
3348
3900
  // top-level shape only, not the message.
3901
+ const evidence = normalizeToolCallEvidence({ name: tu.name, input: tu.input }, result);
3902
+ const isError = evidence.ok === false;
3349
3903
  try {
3350
- const isError = !!(result && result.error);
3351
3904
  _telemetry.track('tool_call', {
3352
3905
  name: tu.name,
3353
3906
  status: isError ? 'error' : 'success',
3354
3907
  duration_ms: elapsed,
3355
- error_type: isError ? (typeof result.error === 'string' ? 'message' : (result.auth_required ? 'auth_required' : 'object')) : undefined,
3908
+ error_type: isError ? (typeof result?.error === 'string' ? 'message' : (result?.auth_required ? 'auth_required' : 'object')) : undefined,
3356
3909
  });
3357
3910
  } catch {}
3358
3911
  // Emit tool completion with result summary
3359
3912
  const resultSummary = tu.name === 'search_memories'
3360
3913
  ? `Found ${result.count || 0} results (${result.search_method || 'search'})`
3914
+ : tu.name === 'ctm_context'
3915
+ ? `CTM context ${result.ok ? 'available' : 'unavailable'}`
3916
+ : tu.name === 'ctm_remote_access_status'
3917
+ ? `CTM phone access ${result.ok ? 'available' : 'unavailable'}`
3918
+ : tu.name === 'ctm_session_search'
3919
+ ? `Found ${(result.results || []).length} CTM session result(s)`
3361
3920
  : tu.name === 'think'
3362
3921
  ? 'Done thinking'
3363
3922
  : `Completed in ${elapsed}ms`;
@@ -3371,23 +3930,62 @@ async function chat(message, opts = {}) {
3371
3930
  name: tu.name,
3372
3931
  result: _progressToolResultPayload(result, resultStr),
3373
3932
  summary: resultSummary,
3933
+ error: isError ? (result?.error || result?.stderr || 'Tool returned unsuccessful evidence') : null,
3374
3934
  fullSizeBytes: Buffer.byteLength(resultStr, 'utf8'),
3375
3935
  });
3376
3936
  return {
3377
3937
  message: { type: 'tool_result', tool_use_id: tu.id, content: compactedResult },
3378
- call: { id: tu.id, tool: tu.name, args: tu.input, result_summary: resultSummary, error: !!result?.error },
3938
+ call: {
3939
+ id: tu.id,
3940
+ tool: tu.name,
3941
+ name: tu.name,
3942
+ args: tu.input,
3943
+ input: tu.input,
3944
+ result,
3945
+ ok: evidence.ok === true,
3946
+ result_summary: resultSummary,
3947
+ error: isError ? (result?.error || true) : false,
3948
+ },
3379
3949
  };
3380
3950
  };
3381
3951
 
3382
- let toolOutputs;
3383
- if (_shouldRunToolCallsSequentially(response.toolCalls)) {
3384
- toolOutputs = [];
3385
- for (const tu of response.toolCalls) {
3386
- toolOutputs.push(await executeToolCall(tu));
3387
- }
3388
- } else {
3389
- toolOutputs = await Promise.all(response.toolCalls.map(executeToolCall));
3390
- }
3952
+ const runtimeToolExecutor = new RuntimeToolExecutor({
3953
+ registry: chatToolRegistry,
3954
+ runtimeProfile: wallERuntimeProfile,
3955
+ executeToolCall,
3956
+ });
3957
+ const toolExecutionRows = await runtimeToolExecutor.run(response.toolCalls, {
3958
+ ...chatToolContextBase,
3959
+ provider: usedProvider || targetProviderType,
3960
+ model: selectedModel,
3961
+ sessionId,
3962
+ cwd: effectiveCwd || opts.cwd || process.cwd(),
3963
+ });
3964
+ const toolOutputs = toolExecutionRows.map((row) => {
3965
+ if (row.rawResult?.message && row.rawResult?.call) return row.rawResult;
3966
+ const call = row.call || {};
3967
+ const envelope = row.envelope || {};
3968
+ const result = envelope.outputForModel || row.result || { error: envelope.error || 'Tool execution failed' };
3969
+ const content = typeof result === 'string' ? result : JSON.stringify(result);
3970
+ return {
3971
+ message: {
3972
+ type: 'tool_result',
3973
+ tool_use_id: call.id,
3974
+ content,
3975
+ is_error: row.ok === false,
3976
+ },
3977
+ call: {
3978
+ id: call.id,
3979
+ tool: call.name,
3980
+ name: call.name,
3981
+ args: call.input,
3982
+ input: call.input,
3983
+ result,
3984
+ ok: row.ok === true,
3985
+ error: row.ok === true ? false : (envelope.error || true),
3986
+ },
3987
+ };
3988
+ });
3391
3989
 
3392
3990
  for (const output of toolOutputs) allToolCalls.push(output.call);
3393
3991
  const toolResults = toolOutputs.map((output) => output.message);
@@ -3516,6 +4114,7 @@ async function chat(message, opts = {}) {
3516
4114
  if (repairResponse.usage) {
3517
4115
  totalInputTokens += repairResponse.usage.input || 0;
3518
4116
  totalOutputTokens += repairResponse.usage.output || 0;
4117
+ recordUsageLedger(repairResponse, 'code_review_repair', { repair: 'code_review' });
3519
4118
  }
3520
4119
  timings.repairMs += Date.now() - repairStart;
3521
4120
  if (!usedModel) { usedModel = repairResponse.model; usedProvider = repairResponse.provider; }
@@ -3538,8 +4137,7 @@ async function chat(message, opts = {}) {
3538
4137
  console.error('[chat] Code-review repair call failed:', repairErr.message);
3539
4138
  try {
3540
4139
  const decorated = decorateProviderError(repairErr, {
3541
- provider: usedProvider || targetProviderType || getDefaultProviderType(),
3542
- model: selectedModel,
4140
+ ...primaryProviderErrorContext(usedProvider || targetProviderType || getDefaultProviderType(), selectedModel),
3543
4141
  });
3544
4142
  recordProviderFailureAlert(decorated.providerError, brain);
3545
4143
  } catch {}
@@ -3572,6 +4170,7 @@ async function chat(message, opts = {}) {
3572
4170
  if (repairResponse.usage) {
3573
4171
  totalInputTokens += repairResponse.usage.input || 0;
3574
4172
  totalOutputTokens += repairResponse.usage.output || 0;
4173
+ recordUsageLedger(repairResponse, 'post_tool_repair', { repair: 'tool_followup' });
3575
4174
  }
3576
4175
  timings.repairMs += Date.now() - repairStart;
3577
4176
  if (!usedModel) { usedModel = repairResponse.model; usedProvider = repairResponse.provider; }
@@ -3597,14 +4196,23 @@ async function chat(message, opts = {}) {
3597
4196
  console.error('[chat] Post-tool repair call failed:', repairErr.message);
3598
4197
  try {
3599
4198
  const decorated = decorateProviderError(repairErr, {
3600
- provider: usedProvider || targetProviderType || getDefaultProviderType(),
3601
- model: selectedModel,
4199
+ ...primaryProviderErrorContext(usedProvider || targetProviderType || getDefaultProviderType(), selectedModel),
3602
4200
  });
3603
4201
  recordProviderFailureAlert(decorated.providerError, brain);
3604
4202
  } catch {}
3605
4203
  }
3606
4204
  if (repairFailed && hasPrematureToolControlText(null, finalText)) {
3607
- const fallback = await _tryProviderFinalizationFallback({
4205
+ const evidenceFallback = await _tryEvidenceOnlyFinalization({
4206
+ provider,
4207
+ model: selectedModel,
4208
+ originalRequest: routingMessage || message,
4209
+ toolCalls: allToolCalls,
4210
+ opts,
4211
+ resetFinalizationTimeout,
4212
+ resolveCurrentReasoningOptions,
4213
+ getSignal: () => controller.signal,
4214
+ });
4215
+ const fallback = evidenceFallback || await _tryProviderFinalizationFallback({
3608
4216
  messages,
3609
4217
  systemPrompt,
3610
4218
  attemptedProviderTypes,
@@ -3623,13 +4231,19 @@ async function chat(message, opts = {}) {
3623
4231
  if (fallback.response?.usage) {
3624
4232
  totalInputTokens += fallback.response.usage.input || 0;
3625
4233
  totalOutputTokens += fallback.response.usage.output || 0;
4234
+ recordUsageLedger(fallback.response, evidenceFallback ? 'tool_followup_evidence_only' : 'tool_followup_provider_fallback', {
4235
+ fallback: true,
4236
+ repair: evidenceFallback ? 'tool_followup_evidence_only' : 'tool_followup_provider_fallback',
4237
+ provider: fallback.meta?.provider || null,
4238
+ model: fallback.meta?.model || null,
4239
+ });
3626
4240
  }
3627
4241
  usedModel = fallback.meta.model || usedModel;
3628
4242
  usedProvider = fallback.meta.provider || usedProvider;
3629
4243
  finalResponseMeta = {
3630
4244
  ...(finalResponseMeta || {}),
3631
4245
  ...fallback.meta,
3632
- repair: 'tool_followup_provider_fallback',
4246
+ repair: evidenceFallback ? 'tool_followup_evidence_only' : 'tool_followup_provider_fallback',
3633
4247
  };
3634
4248
  } else {
3635
4249
  finalText = _postToolFinalizationFailureReply(opts, allToolCalls);
@@ -3667,14 +4281,25 @@ async function chat(message, opts = {}) {
3667
4281
  if (summaryResponse.usage) {
3668
4282
  totalInputTokens += summaryResponse.usage.input || 0;
3669
4283
  totalOutputTokens += summaryResponse.usage.output || 0;
4284
+ recordUsageLedger(summaryResponse, 'summary_fallback', { repair: 'summary' });
3670
4285
  }
3671
4286
  const summaryTextResponse = extractTextToolCalls(summaryResponse);
3672
4287
  const summaryText = summaryTextResponse.content || '';
3673
4288
  if (summaryText && !hasPrematureToolControlText(summaryTextResponse, summaryText)) {
3674
4289
  finalText = summaryText;
3675
4290
  } else {
3676
- console.warn('[chat] Summary fallback returned progress/tool-control text; trying provider finalization fallback');
3677
- const fallback = await _tryProviderFinalizationFallback({
4291
+ console.warn('[chat] Summary fallback returned progress/tool-control text; trying evidence-only finalization');
4292
+ const evidenceFallback = await _tryEvidenceOnlyFinalization({
4293
+ provider,
4294
+ model: selectedModel,
4295
+ originalRequest: routingMessage || message,
4296
+ toolCalls: allToolCalls,
4297
+ opts,
4298
+ resetFinalizationTimeout,
4299
+ resolveCurrentReasoningOptions,
4300
+ getSignal: () => controller.signal,
4301
+ });
4302
+ const fallback = evidenceFallback || await _tryProviderFinalizationFallback({
3678
4303
  messages,
3679
4304
  systemPrompt,
3680
4305
  attemptedProviderTypes,
@@ -3693,6 +4318,12 @@ async function chat(message, opts = {}) {
3693
4318
  if (fallback.response?.usage) {
3694
4319
  totalInputTokens += fallback.response.usage.input || 0;
3695
4320
  totalOutputTokens += fallback.response.usage.output || 0;
4321
+ recordUsageLedger(fallback.response, evidenceFallback ? 'summary_evidence_only' : 'summary_provider_fallback', {
4322
+ fallback: true,
4323
+ repair: evidenceFallback ? 'summary_evidence_only' : 'summary_provider_fallback',
4324
+ provider: fallback.meta?.provider || null,
4325
+ model: fallback.meta?.model || null,
4326
+ });
3696
4327
  }
3697
4328
  usedModel = fallback.meta.model || usedModel;
3698
4329
  usedProvider = fallback.meta.provider || usedProvider;
@@ -3722,8 +4353,7 @@ async function chat(message, opts = {}) {
3722
4353
  let summaryProviderError = null;
3723
4354
  try {
3724
4355
  const decorated = decorateProviderError(summaryErr, {
3725
- provider: usedProvider || targetProviderType || getDefaultProviderType(),
3726
- model: selectedModel,
4356
+ ...primaryProviderErrorContext(usedProvider || targetProviderType || getDefaultProviderType(), selectedModel),
3727
4357
  });
3728
4358
  summaryProviderError = decorated.providerError;
3729
4359
  recordProviderFailureAlert(summaryProviderError, brain);
@@ -3736,7 +4366,19 @@ async function chat(message, opts = {}) {
3736
4366
  const summaryFailureText = summaryProviderError
3737
4367
  ? `${summaryProviderError.title}: ${summaryProviderError.userMessage}`
3738
4368
  : `Summary generation failed: ${summaryErr.message}`;
3739
- const fallback = allToolCalls.length > 0
4369
+ const evidenceFallback = allToolCalls.length > 0
4370
+ ? await _tryEvidenceOnlyFinalization({
4371
+ provider,
4372
+ model: selectedModel,
4373
+ originalRequest: routingMessage || message,
4374
+ toolCalls: allToolCalls,
4375
+ opts,
4376
+ resetFinalizationTimeout,
4377
+ resolveCurrentReasoningOptions,
4378
+ getSignal: () => controller.signal,
4379
+ })
4380
+ : null;
4381
+ const fallback = evidenceFallback || (allToolCalls.length > 0
3740
4382
  ? await _tryProviderFinalizationFallback({
3741
4383
  messages,
3742
4384
  systemPrompt,
@@ -3750,13 +4392,19 @@ async function chat(message, opts = {}) {
3750
4392
  fromProvider: usedProvider || targetProviderType,
3751
4393
  fromModel: usedModel || selectedModel,
3752
4394
  })
3753
- : null;
4395
+ : null);
3754
4396
  if (fallback) {
3755
4397
  finalText = fallback.text;
3756
4398
  lastTurnText = finalText;
3757
4399
  if (fallback.response?.usage) {
3758
4400
  totalInputTokens += fallback.response.usage.input || 0;
3759
4401
  totalOutputTokens += fallback.response.usage.output || 0;
4402
+ recordUsageLedger(fallback.response, evidenceFallback ? 'summary_error_evidence_only' : 'summary_error_provider_fallback', {
4403
+ fallback: true,
4404
+ repair: evidenceFallback ? 'summary_error_evidence_only' : 'summary_error_provider_fallback',
4405
+ provider: fallback.meta?.provider || null,
4406
+ model: fallback.meta?.model || null,
4407
+ });
3760
4408
  }
3761
4409
  usedModel = fallback.meta.model || usedModel;
3762
4410
  usedProvider = fallback.meta.provider || usedProvider;
@@ -3838,6 +4486,91 @@ async function chat(message, opts = {}) {
3838
4486
  console.error('[chat] Self-critique error:', critiqueErr.message);
3839
4487
  }
3840
4488
 
4489
+ const frameValidation = validateFrameAnswer(text, conversationRuntime.frame);
4490
+ if (!frameValidation.ok && !opts.disableConversationFrameRepair) {
4491
+ try {
4492
+ console.warn('[chat] Conversation frame validation failed; forcing scoped final-answer repair:', frameValidation.violations.map(v => v.code).join(','));
4493
+ resetFinalizationTimeout();
4494
+ const repairStart = Date.now();
4495
+ const frameRepairResponse = await provider.chat({
4496
+ model: selectedModel,
4497
+ maxTokens: Math.min(2048, Math.max(512, estimateTokens(text) + 512)),
4498
+ system: systemPrompt,
4499
+ messages: [
4500
+ ...messages,
4501
+ { role: 'assistant', content: text },
4502
+ {
4503
+ role: 'user',
4504
+ content: buildFrameRepairPrompt({
4505
+ frame: conversationRuntime.frame,
4506
+ violations: frameValidation.violations,
4507
+ originalUserMessage: message,
4508
+ }),
4509
+ },
4510
+ ],
4511
+ tools: [],
4512
+ thinking: 'disabled',
4513
+ signal: controller.signal,
4514
+ });
4515
+ if (frameRepairResponse.usage) {
4516
+ totalInputTokens += frameRepairResponse.usage.input || 0;
4517
+ totalOutputTokens += frameRepairResponse.usage.output || 0;
4518
+ recordUsageLedger(frameRepairResponse, 'conversation_frame_repair', { repair: 'conversation_frame' });
4519
+ }
4520
+ timings.repairMs += Date.now() - repairStart;
4521
+ const frameRepairSanitization = _sanitizeProtocolFinalText(frameRepairResponse.content || '', opts, allToolCalls);
4522
+ const repairedText = frameRepairSanitization.text || '';
4523
+ const repairedValidation = validateFrameAnswer(repairedText, conversationRuntime.frame);
4524
+ if (repairedText && !frameRepairSanitization.finalizationFailed && repairedValidation.ok) {
4525
+ text = repairedText;
4526
+ lastTurnText = text;
4527
+ finalResponseMeta = {
4528
+ ...(finalResponseMeta || {}),
4529
+ model: frameRepairResponse.model || selectedModel,
4530
+ provider: frameRepairResponse.provider || usedProvider || targetProviderType,
4531
+ usage: frameRepairResponse.usage || finalResponseMeta?.usage || null,
4532
+ stopReason: frameRepairResponse.stopReason || finalResponseMeta?.stopReason || null,
4533
+ conversationFrameRepair: true,
4534
+ conversationFrameValidation: {
4535
+ ok: true,
4536
+ repairedFrom: frameValidation.violations.map(v => v.code),
4537
+ },
4538
+ ...(frameRepairSanitization.changed ? { conversationFrameRepairProtocolSanitized: true } : {}),
4539
+ };
4540
+ } else {
4541
+ console.warn('[chat] Conversation frame repair did not satisfy validation; keeping original final response');
4542
+ finalResponseMeta = {
4543
+ ...(finalResponseMeta || {}),
4544
+ conversationFrameValidation: {
4545
+ ok: false,
4546
+ violations: frameValidation.violations.map(v => ({ code: v.code, message: v.message })),
4547
+ repairOk: false,
4548
+ repairedViolations: repairedValidation.violations.map(v => ({ code: v.code, message: v.message })),
4549
+ },
4550
+ };
4551
+ }
4552
+ } catch (repairErr) {
4553
+ if (_isUserCancellation(opts, repairErr)) throw _cancelledError();
4554
+ console.error('[chat] Conversation frame repair call failed:', repairErr.message);
4555
+ finalResponseMeta = {
4556
+ ...(finalResponseMeta || {}),
4557
+ conversationFrameValidation: {
4558
+ ok: false,
4559
+ violations: frameValidation.violations.map(v => ({ code: v.code, message: v.message })),
4560
+ repairError: repairErr.message,
4561
+ },
4562
+ };
4563
+ }
4564
+ } else {
4565
+ finalResponseMeta = {
4566
+ ...(finalResponseMeta || {}),
4567
+ conversationFrameValidation: {
4568
+ ok: true,
4569
+ validators: conversationRuntime.validators,
4570
+ },
4571
+ };
4572
+ }
4573
+
3841
4574
  const languageMismatch = detectResponseLanguageMismatch(message, text);
3842
4575
  if (languageMismatch && !opts.disableLanguageRepair) {
3843
4576
  try {
@@ -3860,6 +4593,7 @@ async function chat(message, opts = {}) {
3860
4593
  if (languageRepairResponse.usage) {
3861
4594
  totalInputTokens += languageRepairResponse.usage.input || 0;
3862
4595
  totalOutputTokens += languageRepairResponse.usage.output || 0;
4596
+ recordUsageLedger(languageRepairResponse, 'language_repair', { repair: 'language' });
3863
4597
  }
3864
4598
  timings.repairMs += Date.now() - repairStart;
3865
4599
  const languageRepairSanitization = _sanitizeProtocolFinalText(languageRepairResponse.content || '', opts, allToolCalls);
@@ -3898,15 +4632,34 @@ async function chat(message, opts = {}) {
3898
4632
  };
3899
4633
  }
3900
4634
 
4635
+ const finalLocalPreviewClaim = analyzeLocalPreviewClaims(text, allToolCalls);
4636
+ if (finalLocalPreviewClaim.hasPositiveClaim && !finalLocalPreviewClaim.ok) {
4637
+ console.warn('[chat] Blocking unsupported local preview claim before persistence');
4638
+ text = buildUnsupportedLocalPreviewReply(finalLocalPreviewClaim);
4639
+ lastTurnText = text;
4640
+ finalResponseMeta = {
4641
+ ...(finalResponseMeta || {}),
4642
+ localPreviewClaimBlocked: true,
4643
+ unsupportedLocalPreviewUrls: finalLocalPreviewClaim.unsupportedUrls.map((url) => url.raw),
4644
+ };
4645
+ }
4646
+
3901
4647
  // Save enriched session state
3902
4648
  try {
3903
4649
  const metadata = {
4650
+ ...existingSessionMetadata,
3904
4651
  lastTopic: message.slice(0, 200),
3905
4652
  turnCount: lastTurn + 1,
3906
4653
  toolsUsed: [...new Set(messages
3907
4654
  .filter(m => m.role === 'assistant' && Array.isArray(m.content))
3908
4655
  .flatMap(m => m.content.filter(b => b.type === 'tool_use').map(b => b.name))
3909
4656
  )],
4657
+ conversationFrame: conversationRuntime.frame,
4658
+ conversationRuntime: {
4659
+ schemaVersion: conversationRuntime.schemaVersion,
4660
+ validators: conversationRuntime.validators,
4661
+ lastValidation: finalResponseMeta?.conversationFrameValidation || null,
4662
+ },
3910
4663
  };
3911
4664
  brain.upsertSession({
3912
4665
  id: sessionId, channel,
@@ -3919,9 +4672,16 @@ async function chat(message, opts = {}) {
3919
4672
  console.error('[chat] Failed to save session:', sessionErr.message);
3920
4673
  }
3921
4674
 
3922
- // Save assistant response (user message was already saved before calling Claude)
4675
+ // Save assistant response (user message was already saved before calling Claude).
4676
+ // Record which model/provider actually produced this turn (finalResponseMeta is
4677
+ // the synthesis turn's meta; fall back to the last used/selected model) so the
4678
+ // conversation log can attribute it instead of showing a blank model.
3923
4679
  const persistStart = Date.now();
3924
- const assistantMessage = brain.insertChatMessage({ role: 'assistant', content: text, channel, session_id: sessionId });
4680
+ const assistantMessage = brain.insertChatMessage({
4681
+ role: 'assistant', content: text, channel, session_id: sessionId,
4682
+ model_id: finalResponseMeta?.model || usedModel || selectedModel || null,
4683
+ model_provider: finalResponseMeta?.provider || usedProvider || targetProviderType || null,
4684
+ });
3925
4685
  recordSessionMessage('assistant', text, {
3926
4686
  ...(finalResponseMeta || {}),
3927
4687
  dbMessageId: assistantMessage.id,
@@ -3961,7 +4721,7 @@ async function chat(message, opts = {}) {
3961
4721
  timings.persistMs += Date.now() - persistStart;
3962
4722
 
3963
4723
  const latencyMs = Date.now() - chatStartTime;
3964
- const cost = calculateCost(usedModel, totalInputTokens, totalOutputTokens);
4724
+ const cost = calculateCost(usedModel, totalInputTokens, totalOutputTokens, usedProvider);
3965
4725
 
3966
4726
  // Telemetry (anonymous — no message content)
3967
4727
  try {
@@ -4079,7 +4839,16 @@ async function chat(message, opts = {}) {
4079
4839
  }
4080
4840
  }
4081
4841
 
4082
- function calculateCost(model, inputTokens, outputTokens) {
4842
+ function calculateCost(model, inputTokens, outputTokens, providerType) {
4843
+ try {
4844
+ const estimate = brain.estimateModelUsageCost?.({
4845
+ providerType,
4846
+ modelId: model,
4847
+ inputTokens,
4848
+ outputTokens,
4849
+ });
4850
+ if (Number.isFinite(estimate?.costUsd)) return estimate.costUsd;
4851
+ } catch {}
4083
4852
  const pricing = {
4084
4853
  'claude-sonnet-4-6': { input: 3, output: 15 },
4085
4854
  'claude-opus-4-6': { input: 15, output: 75 },