elanous 0.2.31 → 0.2.32

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 (282) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1834-50625904996a8fa8.js → 1834-98b64172bc1efe77.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/69-0a2c8209c6d4a158.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/8097-9530c03bcc550f02.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/approvals/page-504204b58c82c253.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3535a4a69f1fafaa.js → page-a9717a67c2eecbaf.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/board/page-5c9ec30aee62dfbf.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/botlab/page-584191533721fa16.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/bots/page-73c4b4bb3ac7ab5f.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/ceo/{page-5a503541353e60a8.js → page-c0f1ca371f710b06.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/chat/{page-6025101384e361a5.js → page-46430d45a9e04eac.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/consult/page-3b2ebf1dc64ff0be.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/control/{page-a645f00a06483e9a.js → page-a27b1e5a0d1b308f.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/decisions/page-25654b82094f7d22.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-e9bab7413b028b28.js → page-8ea49a4c6dd2c6e7.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/editor/page-ce5113aefea0a8d1.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-e547e6ce55f1fb33.js → page-0e5a41fac1c75396.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-d27e1b9850fd54bf.js → page-31aaf1ed37c6df9a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-3dcc1fa411e1f4e0.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-0fec29c7ba01894b.js → page-c69247e7134d329d.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-339a07410cd6f36b.js → layout-66dc5cd3a0999abe.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-26ff7ff651a7217d.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/live-run/page-617b3976981efa96.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/loops/{page-2fd93ed8c9f16580.js → page-d256f5d5d9552823.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/market/{page-9b564dbff0fefc35.js → page-ce8f1907b8a6101c.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-33e182a316263116.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/morning/{page-814ba51385225776.js → page-32866485913d6a04.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/{not-found-397d479626e5a459.js → not-found-7607cca9b84950c6.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-af1bba4e3251b728.js → page-eeab86176b03dc3b.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-3ad3be5e7d3fdce9.js → page-5b93dbae3432b996.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-77a6cb90a9cf2b10.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-4da93ad35d4a8d28.js → page-f47e768b5b242b5a.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/outputs/page-eb41aa3367fc2505.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/page-4d13ab0b8a267e3f.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/reflection/page-84dad32ff0fc0831.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-3a658d5fa6d16a25.js → page-95306f892aa2d0ee.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-0157de6e85f8bb6b.js → page-940fb6c217f6b470.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-0a07363b130182a1.js → page-8c66a3b24a745630.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/settings/{page-841d73d4ac44ed49.js → page-7b675dfd35c36fc9.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-6f43a3d1146e6377.js → page-8bd0d3cf7665bcfc.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/setup/{page-521ee23ac8c0abfa.js → page-c219f2ed171131be.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/share/{page-a9992ee285efaa83.js → page-40f8fa21bdae955b.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-3d48e85fa89ec04f.js → page-468f8313e4f1fbd9.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/tasks/page-7bbb8fa353a246c4.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/term/page-04d00ae9b0890349.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/today/{page-233c71cd5f68f0d7.js → page-4214b855d9233ea8.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/trace/{page-70151d78de2f375e.js → page-936752c7aa58762e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/vault/{page-dcac33a3e2cb72c0.js → page-ec052ad359df1797.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-8305570551091338.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-0448145ccd5c45c1.js → page-fad650e4793adb65.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workspace/page-d90e64743826e1d8.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-a16bcfd49af86a32.js +1 -0
  54. package/apps/pwa/out/_next/static/css/b5d88eba78a3d7bf.css +1 -0
  55. package/apps/pwa/out/approvals/index.html +1 -1
  56. package/apps/pwa/out/approvals/index.txt +14 -14
  57. package/apps/pwa/out/autopilot/index.html +1 -1
  58. package/apps/pwa/out/autopilot/index.txt +14 -14
  59. package/apps/pwa/out/board/index.html +1 -1
  60. package/apps/pwa/out/board/index.txt +14 -14
  61. package/apps/pwa/out/botlab/index.html +1 -1
  62. package/apps/pwa/out/botlab/index.txt +14 -14
  63. package/apps/pwa/out/bots/index.html +1 -1
  64. package/apps/pwa/out/bots/index.txt +14 -14
  65. package/apps/pwa/out/ceo/index.html +1 -1
  66. package/apps/pwa/out/ceo/index.txt +14 -14
  67. package/apps/pwa/out/chat/index.html +1 -1
  68. package/apps/pwa/out/chat/index.txt +14 -14
  69. package/apps/pwa/out/consult/index.html +1 -1
  70. package/apps/pwa/out/consult/index.txt +14 -14
  71. package/apps/pwa/out/control/index.html +1 -1
  72. package/apps/pwa/out/control/index.txt +14 -14
  73. package/apps/pwa/out/decisions/index.html +1 -1
  74. package/apps/pwa/out/decisions/index.txt +14 -14
  75. package/apps/pwa/out/design-check/index.html +1 -1
  76. package/apps/pwa/out/design-check/index.txt +14 -14
  77. package/apps/pwa/out/editor/index.html +1 -1
  78. package/apps/pwa/out/editor/index.txt +14 -14
  79. package/apps/pwa/out/exec/index.html +1 -1
  80. package/apps/pwa/out/exec/index.txt +14 -14
  81. package/apps/pwa/out/field/index.html +1 -1
  82. package/apps/pwa/out/field/index.txt +14 -14
  83. package/apps/pwa/out/healthz +1 -1
  84. package/apps/pwa/out/index.html +1 -1
  85. package/apps/pwa/out/index.txt +14 -14
  86. package/apps/pwa/out/inside/index.html +1 -1
  87. package/apps/pwa/out/inside/index.txt +14 -14
  88. package/apps/pwa/out/intake/index.html +1 -1
  89. package/apps/pwa/out/intake/index.txt +14 -14
  90. package/apps/pwa/out/live/index.html +1 -1
  91. package/apps/pwa/out/live/index.txt +17 -14
  92. package/apps/pwa/out/live-run/index.html +1 -1
  93. package/apps/pwa/out/live-run/index.txt +14 -14
  94. package/apps/pwa/out/loops/index.html +1 -1
  95. package/apps/pwa/out/loops/index.txt +14 -14
  96. package/apps/pwa/out/market/index.html +1 -1
  97. package/apps/pwa/out/market/index.txt +14 -14
  98. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  99. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  100. package/apps/pwa/out/morning/index.html +1 -1
  101. package/apps/pwa/out/morning/index.txt +14 -14
  102. package/apps/pwa/out/observatory/index.html +1 -1
  103. package/apps/pwa/out/observatory/index.txt +14 -14
  104. package/apps/pwa/out/ops/checklist/index.html +1 -1
  105. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  106. package/apps/pwa/out/ops/release/index.html +1 -1
  107. package/apps/pwa/out/ops/release/index.txt +14 -14
  108. package/apps/pwa/out/ops/seats/index.html +1 -1
  109. package/apps/pwa/out/ops/seats/index.txt +14 -14
  110. package/apps/pwa/out/outputs/index.html +1 -1
  111. package/apps/pwa/out/outputs/index.txt +14 -14
  112. package/apps/pwa/out/reflection/index.html +1 -1
  113. package/apps/pwa/out/reflection/index.txt +15 -15
  114. package/apps/pwa/out/scheduler/index.html +1 -1
  115. package/apps/pwa/out/scheduler/index.txt +14 -14
  116. package/apps/pwa/out/sessions/index.html +1 -1
  117. package/apps/pwa/out/sessions/index.txt +14 -14
  118. package/apps/pwa/out/settings/devices/index.html +1 -1
  119. package/apps/pwa/out/settings/devices/index.txt +14 -14
  120. package/apps/pwa/out/settings/index.html +1 -1
  121. package/apps/pwa/out/settings/index.txt +14 -14
  122. package/apps/pwa/out/setup/done/index.html +1 -1
  123. package/apps/pwa/out/setup/done/index.txt +14 -14
  124. package/apps/pwa/out/setup/index.html +1 -1
  125. package/apps/pwa/out/setup/index.txt +14 -14
  126. package/apps/pwa/out/share/index.html +1 -1
  127. package/apps/pwa/out/share/index.txt +14 -14
  128. package/apps/pwa/out/showroom/index.html +1 -1
  129. package/apps/pwa/out/showroom/index.txt +14 -14
  130. package/apps/pwa/out/tasks/index.html +1 -1
  131. package/apps/pwa/out/tasks/index.txt +14 -14
  132. package/apps/pwa/out/term/index.html +1 -1
  133. package/apps/pwa/out/term/index.txt +14 -14
  134. package/apps/pwa/out/today/index.html +1 -1
  135. package/apps/pwa/out/today/index.txt +14 -14
  136. package/apps/pwa/out/trace/index.html +1 -1
  137. package/apps/pwa/out/trace/index.txt +14 -14
  138. package/apps/pwa/out/vault/index.html +1 -1
  139. package/apps/pwa/out/vault/index.txt +14 -14
  140. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  141. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  142. package/apps/pwa/out/workflows/index.html +1 -1
  143. package/apps/pwa/out/workflows/index.txt +14 -14
  144. package/apps/pwa/out/workspace/index.html +1 -1
  145. package/apps/pwa/out/workspace/index.txt +14 -14
  146. package/apps/pwa/out/worktrees/index.html +1 -1
  147. package/apps/pwa/out/worktrees/index.txt +14 -14
  148. package/graphs/implement-loop.yaml +5 -0
  149. package/package.json +1 -1
  150. package/scripts/ci-typecheck-changed.ts +26 -1
  151. package/scripts/measure-si-task-cpu.ts +143 -0
  152. package/scripts/ratchets/test-interference-patterns.allowlist.txt +0 -2
  153. package/scripts/release-loop/prepare-node.ts +102 -4
  154. package/src/acp/core-turn-bridge.ts +16 -4
  155. package/src/acp/elanous-extensions.ts +3 -0
  156. package/src/acp/server.ts +3 -0
  157. package/src/acp/tool-trace.ts +186 -11
  158. package/src/agent/self-ambient-cache.ts +156 -0
  159. package/src/agent/self-ambient.ts +8 -1
  160. package/src/agent/task-notification.ts +5 -0
  161. package/src/agent-substrate/pr-reviewer.ts +3 -1
  162. package/src/ask-user-question/tool.ts +22 -0
  163. package/src/autopilot/build/llm-conflict-merge.ts +56 -10
  164. package/src/autopilot/pr-manager.ts +23 -5
  165. package/src/capture/frame-state-detect.ts +39 -0
  166. package/src/capture/tui-self-report.ts +134 -6
  167. package/src/chat/index.ts +61 -15
  168. package/src/chat/pickers/modal-runtime.ts +6 -0
  169. package/src/chat/pickers/modals.ts +8 -2
  170. package/src/cli/ask-bare.ts +1 -0
  171. package/src/cli/import-cli.ts +12 -0
  172. package/src/cli/log-event-names.ts +10 -0
  173. package/src/cli/msg-cli.ts +1 -0
  174. package/src/cli/pod-cli.ts +14 -3
  175. package/src/cli/pr-cli.ts +67 -32
  176. package/src/cli/pr-land-base-read.ts +34 -0
  177. package/src/cli/pr-land-heavy-remote.ts +81 -25
  178. package/src/cli/pr-land-overlap-evidence.ts +46 -2
  179. package/src/cli/pr-land-pod-shadow-job.ts +26 -3
  180. package/src/cli/pr-land-refusal.ts +20 -5
  181. package/src/cli/pty-takeover-cli.ts +88 -14
  182. package/src/cli/sim-cli.ts +18 -0
  183. package/src/context-bus/ops-events.ts +6 -2
  184. package/src/core-turn/run-core-turn.ts +1 -0
  185. package/src/core-turn/run-goal-loop.ts +21 -0
  186. package/src/core-turn/types.ts +3 -0
  187. package/src/dashboard/acp-active-model.ts +42 -1
  188. package/src/dashboard/background-jobs-status.ts +66 -0
  189. package/src/dashboard/index.ts +117 -44
  190. package/src/dashboard/input/agent-wake.ts +242 -0
  191. package/src/dashboard/input/chat-main-entry-runtime.ts +5 -0
  192. package/src/dashboard/input/external-input-host.ts +23 -9
  193. package/src/dashboard/input/mouse-wiring.ts +108 -11
  194. package/src/dashboard/log-click-runtime.ts +118 -2
  195. package/src/dashboard/turn-preamble.ts +116 -40
  196. package/src/dashboard/turn-session-layers.ts +4 -2
  197. package/src/dashboard/turn-start-segments.ts +222 -0
  198. package/src/display/prompt-row-owner.ts +146 -0
  199. package/src/display/resize-listener.ts +33 -2
  200. package/src/fold-stack.ts +4 -2
  201. package/src/git-fs/index.ts +263 -36
  202. package/src/harness/gate-reserve.ts +87 -0
  203. package/src/harness/harness-incidents.ts +13 -0
  204. package/src/harness/harness-queue.ts +15 -0
  205. package/src/index.ts +8 -6
  206. package/src/input/external-input-queue.ts +34 -9
  207. package/src/llm/route-decision.ts +36 -0
  208. package/src/llm/tool-loop-steered-rejection.ts +8 -0
  209. package/src/llm-request-shape.ts +209 -0
  210. package/src/llm.ts +166 -267
  211. package/src/loops/orchestrator/tick.ts +24 -2
  212. package/src/maturity/feature-maturity.ts +1 -1
  213. package/src/mss/logging/render-categories.ts +5 -0
  214. package/src/ops/overview.ts +59 -2
  215. package/src/pr-tool/census.ts +19 -4
  216. package/src/pr-tool/inflight-registry.ts +143 -0
  217. package/src/pty-shell/pty-manifest.ts +12 -3
  218. package/src/pty-shell/tui-run-state.ts +34 -3
  219. package/src/seat-loop/traffic-signal.ts +2 -1
  220. package/src/self-dev/ask-launch-flow.ts +13 -0
  221. package/src/self-dev/draft-residue.ts +243 -24
  222. package/src/self-dev/shard-plan.ts +11 -3
  223. package/src/self-dev/shard-run.ts +103 -10
  224. package/src/self-implement/elanous-tui-spawn.ts +14 -0
  225. package/src/self-implement/follow-up-placement.ts +79 -0
  226. package/src/self-implement/gate-artifacts.ts +15 -4
  227. package/src/self-implement/gate-baseline.ts +96 -5
  228. package/src/self-implement/gate-cli.ts +7 -2
  229. package/src/self-implement/gate-remote.ts +26 -1
  230. package/src/self-implement/host-regate.ts +36 -4
  231. package/src/self-implement/orchestrator.ts +101 -11
  232. package/src/self-implement/run-ledger.ts +16 -14
  233. package/src/session-runtime/index.ts +22 -267
  234. package/src/shell-runner/background-surface.ts +15 -5
  235. package/src/skills/tools/agent.ts +39 -6
  236. package/src/skills/tools/list-dir.ts +11 -0
  237. package/src/skills/tools/schedule-wakeup.ts +408 -0
  238. package/src/skills/tools/shell.ts +245 -3
  239. package/src/status/bar.ts +26 -0
  240. package/src/steward/digest-delivery.ts +118 -0
  241. package/src/steward/hold-hygiene.ts +120 -0
  242. package/src/steward/triage.ts +10 -0
  243. package/src/surface/shell-runner-boot.ts +5 -2
  244. package/src/task-agent/task-hand.ts +24 -0
  245. package/src/task-orchestrator/surfaces/pod-command-job.ts +65 -11
  246. package/src/task-orchestrator/surfaces/pod-lease.ts +98 -0
  247. package/src/task-orchestrator/surfaces/pod-ledger-events.ts +1 -1
  248. package/src/task-orchestrator/surfaces/pod-pool.ts +3 -1
  249. package/src/task-orchestrator/surfaces/pod-reuse-cleanup.ts +10 -6
  250. package/src/task-orchestrator/surfaces/pod-warm-worker-pod.ts +525 -0
  251. package/src/task-orchestrator/surfaces/pod-warm-worker-runner.ts +90 -0
  252. package/src/task-orchestrator/surfaces/pod-warm-worker-runtime.ts +156 -2
  253. package/src/task-orchestrator/surfaces/self-implement-pod.ts +7 -5
  254. package/src/tui/chat/execution-badge.ts +5 -1
  255. package/src/tui/input-lag-probe.ts +230 -0
  256. package/src/tui.ts +4 -0
  257. package/src/ux-sim/scenario.ts +255 -0
  258. package/src/version/packed-revision.json +1 -1
  259. package/apps/pwa/out/_next/static/chunks/8097-92f87bd108322f66.js +0 -1
  260. package/apps/pwa/out/_next/static/chunks/app/approvals/page-bf3634a117322df7.js +0 -1
  261. package/apps/pwa/out/_next/static/chunks/app/board/page-b9f51447f757fbf2.js +0 -1
  262. package/apps/pwa/out/_next/static/chunks/app/botlab/page-2ebb8fd59b9461f9.js +0 -1
  263. package/apps/pwa/out/_next/static/chunks/app/bots/page-a2fc4e49a4fa4ba8.js +0 -1
  264. package/apps/pwa/out/_next/static/chunks/app/consult/page-f4b630cc8b5273cd.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/app/decisions/page-61ac22cbab1ee4ec.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/app/editor/page-c992eafe028b642e.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/app/inside/page-805ac2e137b7a121.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/app/live/page-349f2752d8cd37ad.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/live-run/page-d5c6dcf31c25cecc.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-62f4475d3c38fb9b.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-f219ccdbc0bf27df.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/outputs/page-456666766e5ce6fc.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/page-7404e377e8adb8c5.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/reflection/page-14456240ed2a36ad.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d2ced71132fac9b2.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/term/page-f08358bdf251adb9.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-149dc1caba411bb9.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9cb0c444903da65d.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-17e504c396e0e389.js +0 -1
  280. package/apps/pwa/out/_next/static/css/3f5fc6f96e2cf7b2.css +0 -1
  281. /package/apps/pwa/out/_next/static/{LSXfY0u9QtfDbejY0Ukf_ → IVKr3AlRUVLKsw2fh7eXF}/_buildManifest.js +0 -0
  282. /package/apps/pwa/out/_next/static/{LSXfY0u9QtfDbejY0Ukf_ → IVKr3AlRUVLKsw2fh7eXF}/_ssgManifest.js +0 -0
package/src/llm.ts CHANGED
@@ -48,7 +48,8 @@ import {
48
48
  type ProviderFallbackAttempt,
49
49
  type ProviderFallbackTerminalVerdict,
50
50
  } from './session-runtime/retry-policy.js';
51
- import { fetchApiWithRetry, OverloadFailoverError, type FetchApiWithRetryOpts } from './session-runtime/retry-api.js';
51
+ import { ApiHttpError, fetchApiWithRetry, OverloadFailoverError, type FetchApiWithRetryOpts } from './session-runtime/retry-api.js';
52
+ import { isShapeRelevantRejection, summarizeCodexRequestShape } from './llm-request-shape.js';
52
53
  import { BedrockProvider, isBedrockCompatibleModel, makeBedrockProvider } from './llm/bedrock-provider.js';
53
54
  import { OPEN_WEIGHT_TOOL_DISCIPLINE } from './llm/open-weight-discipline.js';
54
55
  import { makeZaiCodingPlanProvider, zaiCodingPlanAvailable } from './llm/zai-coding-plan.js';
@@ -61,8 +62,6 @@ import { undoTurnRuntime } from './tool-runtime/undo-turn-runtime.js';
61
62
  import { listSnapshots } from './undo-turn/index.js';
62
63
  // Zero-dep spec module (no tool-runtime registry) — safe for this hot path.
63
64
  import { takeHydratedTools } from './skills/tools/tool-search-spec.js';
64
- import { dispatchAskUserQuestion } from './ask-user-question/index.js';
65
- import type { AskUserQuestionResult } from './ask-user-question/index.js';
66
65
  import { isPlanModeActive } from './plan-mode/session.js';
67
66
  import { mintTurnUri } from './mss/uri/builder.js';
68
67
  import type { TurnUri } from './mss/uri/brand.js';
@@ -406,6 +405,10 @@ export interface LLMOpts {
406
405
  * ③ 종결 신호를 받은 뒤의 빈 턴 포기에도 이미 쓴 답이 있으면 `[NO FINAL SYNTHESIS]` 를 붙이지 않는다.
407
406
  * 비우면(기본) 종전 동작 그대로. */
408
407
  terminalToolNames?: readonly string[];
408
+ /** TUI-H1-GOALLOOP-EMPTY-TAIL — 호출자가 이 스트림 «앞»에 사람에게 답을 이미 보였다(예: goal loop 의 앞 iteration).
409
+ * 그러면 종결 신호 뒤 빈 턴은 이 스트림에 쓴 답이 없어도 재촉·`[NO FINAL SYNTHESIS]` 없이 빈 답으로 끝낸다.
410
+ * 비우면(기본) 종전대로 — 답이 «어디에도» 없을 때의 경고는 그대로 보인다. */
411
+ terminalPriorAnswerVisible?: boolean;
409
412
  /** Prompt-caching toggle. Honored by Anthropic (attaches
410
413
  * cache_control markers on system + tools + 2nd-to-last history
411
414
  * message) and OpenAI (passes stream_options.include_usage so
@@ -425,12 +428,9 @@ export interface LLMOpts {
425
428
  * every tool-loop turn gets a stable checkpoint identity without
426
429
  * forcing existing callers to thread the brand through. */
427
430
  turnUri?: TurnUri;
428
- /** Chat session id forwarded to the AskUserQuestion bridge when the
429
- * tool-loop self-dispatches a doom-loop intervention prompt. Lets
430
- * PWA / iOS peers receive the prompt natively via the ACP
431
- * `elanous/ask/*` envelope instead of the raw `[ASK USER]` text
432
- * fallback. Absent for CLI / startup-script callers without a chat
433
- * session context. */
431
+ /** Chat session id (context compaction · provider sticky routing).
432
+ * Absent for CLI / startup-script callers without a chat session
433
+ * context. */
434
434
  sessionId?: string;
435
435
  /** Server-side built-in tools the provider should advertise to the
436
436
  * model alongside the caller-supplied `tools`. Each provider has
@@ -2195,6 +2195,31 @@ export async function* streamCodexResponsesEvents(
2195
2195
  // applies the same tool/SSE conventions
2196
2196
  const promptCache = extra.promptCache !== false;
2197
2197
  const cacheKey = promptCache ? computePromptCacheKey(body.model, body.instructions) : undefined;
2198
+ // TUI-OBS-400-SHAPE(#69) — 전송 객체·문자열을 한 번 만들어 둔다: 4xx 거절 때 «실제로 보낸 것» 의 모양을 잰다.
2199
+ const wireBody: Record<string, unknown> = {
2200
+ ...body,
2201
+ stream: true,
2202
+ // store=true wave: when caller opts in, the backend persists the
2203
+ // response and the next request can thread `previous_response_id`
2204
+ // to send only the delta input items. Default still false so any
2205
+ // caller that doesn't opt in (tests building bodies directly)
2206
+ // sees the legacy wire shape.
2207
+ store: body.store === true,
2208
+ ...(body.tool_choice ? { tool_choice: body.tool_choice } : {}),
2209
+ ...(body.previousResponseId ? { previous_response_id: body.previousResponseId } : {}),
2210
+ ...(cacheKey ? { prompt_cache_key: cacheKey } : {}),
2211
+ // Reasoning opt-in (gpt-5 family). When the caller sets
2212
+ // body.reasoning we mirror it onto the wire AND append the
2213
+ // include directive that lets multi-turn requests thread the
2214
+ // model's encrypted reasoning context. Omit both when absent
2215
+ // so non-reasoning models / opt-out users see the legacy
2216
+ // wire shape unchanged.
2217
+ ...(body.reasoning ? {
2218
+ reasoning: body.reasoning,
2219
+ include: ['reasoning.encrypted_content'],
2220
+ } : {}),
2221
+ };
2222
+ const wireJson = JSON.stringify(wireBody);
2198
2223
  const response = await fetchApiWithRetry(
2199
2224
  url,
2200
2225
  {
@@ -2212,33 +2237,14 @@ export async function* streamCodexResponsesEvents(
2212
2237
  // Codex". See getCodexUserAgent (oauth/codex.ts).
2213
2238
  'User-Agent': getCodexUserAgent(),
2214
2239
  },
2215
- body: JSON.stringify({
2216
- ...body,
2217
- stream: true,
2218
- // store=true wave: when caller opts in, the backend persists the
2219
- // response and the next request can thread `previous_response_id`
2220
- // to send only the delta input items. Default still false so any
2221
- // caller that doesn't opt in (tests building bodies directly)
2222
- // sees the legacy wire shape.
2223
- store: body.store === true,
2224
- ...(body.tool_choice ? { tool_choice: body.tool_choice } : {}),
2225
- ...(body.previousResponseId ? { previous_response_id: body.previousResponseId } : {}),
2226
- ...(cacheKey ? { prompt_cache_key: cacheKey } : {}),
2227
- // Reasoning opt-in (gpt-5 family). When the caller sets
2228
- // body.reasoning we mirror it onto the wire AND append the
2229
- // include directive that lets multi-turn requests thread the
2230
- // model's encrypted reasoning context. Omit both when absent
2231
- // so non-reasoning models / opt-out users see the legacy
2232
- // wire shape unchanged.
2233
- ...(body.reasoning ? {
2234
- reasoning: body.reasoning,
2235
- include: ['reasoning.encrypted_content'],
2236
- } : {}),
2237
- }),
2240
+ body: wireJson,
2238
2241
  signal,
2239
2242
  },
2240
2243
  retryOptsFor('codex', 'Codex API', extra.remainingFallbacks),
2241
- );
2244
+ ).catch((err: unknown) => {
2245
+ observeCodexRequestRejection(err, wireBody, wireJson);
2246
+ throw err;
2247
+ });
2242
2248
  debug.log('llm.response.status', `codex ${response.status}`, {
2243
2249
  url, status: response.status, elapsedMs: Date.now() - startedAt,
2244
2250
  });
@@ -2376,6 +2382,19 @@ export async function* streamCodexResponsesEvents(
2376
2382
  }
2377
2383
  }
2378
2384
 
2385
+ /** TUI-OBS-400-SHAPE(#69) — Codex 가 요청을 4xx(401·403·429 제외)로 거절하면 보낸 요청의 «모양» 만 남긴다.
2386
+ * Codex 400 은 `{"detail":"Bad Request"}` 뿐이라 크기·메시지 수·도구 짝 깨짐을 여기서 잰다.
2387
+ * ⛔ 본문·프롬프트·도구 인자/출력·키는 싣지 않는다(수·바이트·종류·call_id 해시 앞 8자만).
2388
+ * 관측 실패는 오류 계약을 바꾸지 않는다 — 호출부는 원래 오류를 그대로 던진다. */
2389
+ function observeCodexRequestRejection(err: unknown, wireBody: Record<string, unknown>, wireJson: string): void {
2390
+ if (!(err instanceof ApiHttpError) || !isShapeRelevantRejection(err.status)) return;
2391
+ try {
2392
+ debug.log('llm.codex', 'request-rejected-shape', summarizeCodexRequestShape(wireBody, wireJson, err.status, err.bodyText));
2393
+ } catch {
2394
+ // Observability must not interfere with the provider error contract.
2395
+ }
2396
+ }
2397
+
2379
2398
  function toolNames(raw: unknown): string[] | undefined {
2380
2399
  if (!Array.isArray(raw)) return undefined;
2381
2400
  return raw.map(item => {
@@ -4998,6 +5017,14 @@ export interface StreamWithToolsHandlers {
4998
5017
  * before the `onText('', enriched)` re-emit so presentation hosts can
4999
5018
  * keep the user answer to `answer` and send `supplement` to logs. */
5000
5019
  onFinalAnswerSupplement?(info: { answer: string; supplement: string; finalText: string }): void;
5020
+ /** 검증 보충 — final-answer supplement attachment point. Called after the
5021
+ * final answer is settled (and after any finalization-snapshot enrichment)
5022
+ * so callers can append verification-recency / loop-closure wording after
5023
+ * the final answer. Return a non-empty string to append it to the final
5024
+ * answer output (it will be joined after `finalText` and the combined
5025
+ * supplement surfaces via `onFinalAnswerSupplement`); return
5026
+ * undefined/null/empty to keep the prior behavior unchanged. */
5027
+ onAppendFinalAnswerSupplement?(req: FinalAnswerSupplementAttachmentRequest): string | null | undefined;
5001
5028
  /** Optional: notify when a tool_call is about to fire (for log line,
5002
5029
  * HUD segment, etc). */
5003
5030
  onToolCall?(call: { id: string; name: string; args: Record<string, unknown> }): void;
@@ -5856,44 +5883,15 @@ function buildAutoUndoRepairNotice(result: { output?: string; restoredId?: strin
5856
5883
  ].filter(Boolean).join('\n');
5857
5884
  }
5858
5885
 
5859
- // Fallback chat text used only when the AskUserQuestion bridge isn't
5860
- // reachable (no resolver, no sessionId, dispatch error). The marker
5861
- // `[ASK USER]` is preserved so `isUserInterventionRequired` downstream
5862
- // still skips force-synthesis — that gate predates the bridge route and
5863
- // other surfaces (TUI) still rely on the marker as a hard-stop signal.
5864
- function buildAutoUndoAskUserNotice(reason: DoomLoopReason, detail?: string | null): string {
5865
- const why = reason === 'plan-mode'
5866
- ? 'Auto-undo was skipped because plan mode is active.'
5867
- : reason === 'no-undo-available'
5868
- ? 'Auto-undo was skipped because no undo snapshot is available.'
5869
- : reason === 'undo-failed'
5870
- ? 'Auto-undo was attempted but the undo runtime failed.'
5871
- : 'Retry policy did not authorize auto-undo for this failure.';
5872
- return [
5873
- '[ASK USER] The same tool call kept failing — stopping to avoid wasting more turns.',
5874
- why,
5875
- detail ? `Detail: ${detail}` : '',
5876
- 'Reply with how to proceed (retry, change approach, or stop).',
5877
- ].filter(Boolean).join(' ');
5878
- }
5879
-
5880
- // ── Doom-loop → AskUserQuestion routing ─────────────────────────
5881
- //
5882
- // When the tool-loop trips the doom-loop gate (same tool error 3x) it
5883
- // previously emitted a raw `[ASK USER]` chat message describing what
5884
- // went wrong. With the AskUserQuestion bridge (PR #2611 PWA, #2612
5885
- // iOS, #2613 multi-option fan-out) live across PWA + iOS surfaces, we
5886
- // can self-dispatch a structured prompt instead so the user sees a
5887
- // modal with retry / different-approach / stop options rather than
5888
- // raw English instructions intended for the LLM.
5889
- //
5890
- // Falls back to the legacy `[ASK USER]` chat text when:
5891
- // - no `sessionId` plumbed (CLI / startup script callers),
5892
- // - no resolver wired (TUI-only or dispatch error), or
5893
- // - the bridge throws (peer disconnected, surface mismatch).
5886
+ // ── Doom-loop self-heal ─────────────────────────────────────────
5894
5887
  //
5895
- // The fallback marker `[ASK USER]` is preserved so the downstream
5896
- // `isUserInterventionRequired` gate continues to skip force-synthesis.
5888
+ // When the tool-loop trips the doom-loop gate (same tool error 3x) and
5889
+ // auto-undo cannot run, the loop heals itself instead of asking the user
5890
+ // (대표 10-17 — doom-loop is not a user-facing notice: in 40 h of runs it
5891
+ // tripped 3 times and the AskUser prompt was answered 0 times, each one
5892
+ // sitting 5 min until timeout). First trip → a «change approach» note for
5893
+ // the model and the turn continues; a second trip in the same turn → the
5894
+ // turn ends with a plain stop text (no question to the user).
5897
5895
 
5898
5896
  export type DoomLoopReason = 'plan-mode' | 'no-undo-available' | 'undo-failed' | 'policy';
5899
5897
 
@@ -5983,174 +5981,35 @@ function stringifyDoomFailureDetail(value: unknown): string {
5983
5981
  }
5984
5982
 
5985
5983
  export type DoomLoopRouteOutcome =
5986
- | { kind: 'retry'; interventionClass: DoomLoopInterventionClass }
5987
5984
  | { kind: 'guidance'; userMessage: string; interventionClass: DoomLoopInterventionClass }
5988
- | { kind: 'stop'; hardStopText: string; interventionClass: DoomLoopInterventionClass }
5989
- | { kind: 'fallback'; hardStopText: string; interventionClass: DoomLoopInterventionClass };
5990
-
5991
- export interface DoomLoopRouteOpts {
5992
- reason: DoomLoopReason;
5993
- /** Explicit evidence only. Omission means the intervention class is unknown. */
5985
+ | { kind: 'stop'; hardStopText: string; interventionClass: DoomLoopInterventionClass };
5986
+
5987
+ /** Visible text when the turn ends on a second doom trip. Deliberately not a
5988
+ * question and without the `[ASK USER]` marker — nobody is asked. */
5989
+ export const DOOM_LOOP_SELF_HEAL_STOP_TEXT =
5990
+ '같은 도구 호출이 방법을 바꾼 뒤에도 계속 실패해 이번 턴을 여기서 마칩니다.';
5991
+
5992
+ export interface DoomLoopSelfHealInput {
5993
+ /** 1 = first trip in this turn, 2+ = tripped again after the guidance. */
5994
+ attempt: number;
5995
+ tool?: string;
5996
+ fingerprint: string;
5994
5997
  interventionClass?: DoomLoopInterventionClass | null;
5995
- detail?: string | null;
5996
- doomWindow: string[];
5997
- sessionId?: string;
5998
- signal?: AbortSignal;
5999
- /** Override the AskUser gate timeout (ms). Defaults to
6000
- * DOOM_ASK_TIMEOUT_MS. Primarily a test seam. */
6001
- timeoutMs?: number;
6002
- }
6003
-
6004
- export function buildDoomLoopAskRequest(opts: DoomLoopRouteOpts): Record<string, unknown> {
6005
- const interventionClass = normalizeDoomLoopInterventionClass(opts.interventionClass);
6006
- const why = opts.reason === 'plan-mode'
6007
- ? 'plan mode is active so auto-undo was skipped'
6008
- : opts.reason === 'undo-failed'
6009
- ? `auto-undo was attempted but failed (${opts.detail ?? 'no detail'})`
6010
- : `auto-undo was not authorized by the retry policy (${opts.detail ?? 'no detail'})`;
6011
- const lastFingerprint = opts.doomWindow[opts.doomWindow.length - 1] ?? 'unknown';
6012
- return {
6013
- interventionClass,
6014
- questions: [
6015
- {
6016
- id: 'doom_loop_next_step',
6017
- header: 'Loop blocked',
6018
- question:
6019
- 'The same tool call kept failing after 3 attempts — '
6020
- + `${why}. How should I proceed?\n\nLast failure: ${lastFingerprint}`,
6021
- options: [
6022
- {
6023
- label: 'Retry once more',
6024
- description: 'Reset the retry tracker and let me try the same approach one more time.',
6025
- },
6026
- {
6027
- label: 'Different approach',
6028
- description: 'Type guidance in Other and I will follow it on the next turn.',
6029
- },
6030
- {
6031
- label: 'Stop here',
6032
- description: 'Halt this run. I will summarize what was tried and what was blocked.',
6033
- },
6034
- ],
6035
- includeOther: true,
6036
- },
6037
- ],
6038
- };
6039
- }
6040
-
6041
- export function interpretDoomLoopAskResult(
6042
- result: AskUserQuestionResult,
6043
- interventionClass: DoomLoopInterventionClass = 'unknown',
6044
- ): DoomLoopRouteOutcome {
6045
- if (result.cancelled === true) {
6046
- return {
6047
- kind: 'stop',
6048
- interventionClass,
6049
- hardStopText:
6050
- '[ASK USER] The same tool call kept failing and you cancelled the prompt — stopping this run.',
6051
- };
6052
- }
6053
- const answer = result.answers?.['doom_loop_next_step'];
6054
- const picked = Array.isArray(answer) ? (answer[0] ?? '') : (answer ?? '');
6055
- const otherText = result.otherText?.['doom_loop_next_step']?.trim();
6056
-
6057
- if (picked.startsWith('Retry')) {
6058
- return { kind: 'retry', interventionClass };
6059
- }
6060
- if (picked.startsWith('Different') || picked === 'Other') {
6061
- const guidance = otherText && otherText.length > 0
6062
- ? otherText
6063
- : 'Please try a different approach to the same goal — the previous tool call kept failing.';
6064
- return { kind: 'guidance', userMessage: guidance, interventionClass };
6065
- }
6066
- if (picked.startsWith('Stop')) {
6067
- return {
6068
- kind: 'stop',
6069
- interventionClass,
6070
- hardStopText: '[ASK USER] Stopped at your request after the repeated tool failure.',
6071
- };
6072
- }
6073
- return {
6074
- kind: 'stop',
6075
- interventionClass,
6076
- hardStopText:
6077
- '[ASK USER] The same tool call kept failing and the response was unrecognized — stopping this run.',
6078
- };
6079
5998
  }
6080
5999
 
6081
- // Doom-loop AskUser gate timeout — the doom-loop HITL prompt awaits an
6082
- // operator answer with NO timeout of its own (TUI modal `await modal.promise`
6083
- // and the ACP bridge `Promise.race([pushAskRequest, cancellation])` both lack
6084
- // a timer). In an autonomous drive there is no operator to answer, so the
6085
- // dispatch region freezes indefinitely — between tool-loop turns, invisible to
6086
- // the stream idle-watchdog (armed only inside the stream loop) and to the
6087
- // goal-loop (which awaits the hung iteration). This was the 16-minute freeze in
6088
- // RESEARCH-autonomous-runaway-discipline-2026-07-19 (R2). Bound the wait: after
6089
- // this budget, fall back to the same auto-undo notice used when no session /
6090
- // no cap-able peer is present. Generous enough that a present operator answers
6091
- // first; short enough that an absent one doesn't stall the run for long.
6092
- const DOOM_ASK_TIMEOUT_MS = 5 * 60_000;
6093
-
6094
- export async function routeDoomLoopToAskUser(opts: DoomLoopRouteOpts): Promise<DoomLoopRouteOutcome> {
6095
- const interventionClass = normalizeDoomLoopInterventionClass(opts.interventionClass);
6096
- if (!opts.sessionId) {
6097
- return {
6098
- kind: 'fallback',
6099
- interventionClass,
6100
- hardStopText: buildAutoUndoAskUserNotice(opts.reason, opts.detail),
6101
- };
6102
- }
6103
- try {
6104
- const dispatchCtx: { sessionId: string; signal?: AbortSignal } = { sessionId: opts.sessionId };
6105
- if (opts.signal) dispatchCtx.signal = opts.signal;
6106
- const timeoutMs = opts.timeoutMs ?? DOOM_ASK_TIMEOUT_MS;
6107
- const TIMED_OUT = Symbol('doom-ask-timeout');
6108
- let askTimer: ReturnType<typeof setTimeout> | undefined;
6109
- const raced = await Promise.race([
6110
- dispatchAskUserQuestion(buildDoomLoopAskRequest(opts), dispatchCtx),
6111
- new Promise<typeof TIMED_OUT>((res) => {
6112
- askTimer = setTimeout(() => res(TIMED_OUT), timeoutMs);
6113
- }),
6114
- ]);
6115
- if (askTimer) clearTimeout(askTimer);
6116
- if (raced === TIMED_OUT) {
6117
- debug.log('llm.tool-loop.retry', 'doom-loop-ask-timeout', {
6118
- reason: opts.reason,
6119
- interventionClass,
6120
- timeoutMs,
6121
- }, { level: 'warn' });
6122
- return {
6123
- kind: 'fallback',
6124
- interventionClass,
6125
- hardStopText: buildAutoUndoAskUserNotice(opts.reason, opts.detail),
6126
- };
6127
- }
6128
- const r = raced;
6129
- if (!r.result) {
6130
- debug.log('llm.tool-loop.retry', 'doom-loop-ask-route-failed', {
6131
- error: r.output,
6132
- reason: opts.reason,
6133
- interventionClass,
6134
- }, { level: 'error' });
6135
- return {
6136
- kind: 'fallback',
6137
- interventionClass,
6138
- hardStopText: buildAutoUndoAskUserNotice(opts.reason, opts.detail),
6139
- };
6140
- }
6141
- return interpretDoomLoopAskResult(r.result, interventionClass);
6142
- } catch (err) {
6143
- debug.log('llm.tool-loop.retry', 'doom-loop-ask-route-failed', {
6144
- error: err instanceof Error ? err.message : String(err),
6145
- reason: opts.reason,
6146
- interventionClass,
6147
- }, { level: 'error' });
6000
+ export function decideDoomLoopSelfHeal(input: DoomLoopSelfHealInput): DoomLoopRouteOutcome {
6001
+ const interventionClass = normalizeDoomLoopInterventionClass(input.interventionClass);
6002
+ if (input.attempt <= 1) {
6148
6003
  return {
6149
- kind: 'fallback',
6004
+ kind: 'guidance',
6150
6005
  interventionClass,
6151
- hardStopText: buildAutoUndoAskUserNotice(opts.reason, opts.detail),
6006
+ userMessage:
6007
+ `The same ${input.tool ?? 'tool'} call failed 3 times in a row (last failure: ${input.fingerprint}). `
6008
+ + 'Do not call it again with the same arguments. Change approach — a different tool or different '
6009
+ + 'arguments, or first verify the precondition that keeps failing — or state plainly what is blocked.',
6152
6010
  };
6153
6011
  }
6012
+ return { kind: 'stop', interventionClass, hardStopText: DOOM_LOOP_SELF_HEAL_STOP_TEXT };
6154
6013
  }
6155
6014
 
6156
6015
  interface AutoNarrowedInspection {
@@ -8164,9 +8023,10 @@ export function billingRouteForProvider(provider: string | undefined, model: str
8164
8023
  // local 은 호출이 «일어났다면» 언제나 local 이다 — 주소가 env 가 아니라 config rotation 에 있어도(자격 판정은 env 만 본다).
8165
8024
  if (name === 'local') return 'local';
8166
8025
  try {
8026
+ // A recorded rotation decision is the authoritative route for that Codex call;
8027
+ // auth-kind inference alone can otherwise relabel credit spend as subscription/API.
8028
+ if (name === 'openai-codex' && codexCreditSpendReason()) return 'credit';
8167
8029
  const auth = authKindForProvider(name, model);
8168
- // CREDIT-BILLING-LABEL(10-14) — codex 회전이 크레딧 소진을 정한 호출은 구독이 아니라 크레딧이다.
8169
- if (auth === 'oauth' && name === 'openai-codex' && codexCreditSpendReason()) return 'credit';
8170
8030
  return auth === 'oauth' ? 'subscription' : auth === 'local' ? 'local' : auth === 'apikey' ? 'api' : 'unknown';
8171
8031
  } catch {
8172
8032
  return 'unknown';
@@ -8196,6 +8056,41 @@ export function logAgentTurnUsage(model: string, usage: LLMUsage, providerNameOr
8196
8056
  } catch { /* usage observation must not change the turn */ }
8197
8057
  }
8198
8058
 
8059
+ /** 검증 보충 — final-answer supplement attachment point.
8060
+ * Request passed to `StreamWithToolsHandlers.onAppendFinalAnswerSupplement`
8061
+ * after the final answer is settled: `answer` is the visible answer text and
8062
+ * `finalText` is the current final answer output (including any
8063
+ * finalization-snapshot enrichment from the onFinalAnswerSupplement
8064
+ * foundation). */
8065
+ export interface FinalAnswerSupplementAttachmentRequest {
8066
+ answer: string;
8067
+ finalText: string;
8068
+ }
8069
+
8070
+ /** Callback type for the attachment point. Return a non-empty string to have
8071
+ * it appended after the final answer; undefined/null/empty keeps the final
8072
+ * answer output unchanged. */
8073
+ export type FinalAnswerSupplementAttachment = (
8074
+ req: FinalAnswerSupplementAttachmentRequest,
8075
+ ) => string | null | undefined;
8076
+
8077
+ /** Final-answer supplement attachment point: call the optional
8078
+ * `onAppendFinalAnswerSupplement` handler and return the final answer text
8079
+ * with its supplement appended (or the original text when the handler is
8080
+ * absent or yields nothing). No numeric thresholds are applied. */
8081
+ export function attachFinalAnswerSupplement(
8082
+ handlers: { onAppendFinalAnswerSupplement?: FinalAnswerSupplementAttachment },
8083
+ req: FinalAnswerSupplementAttachmentRequest,
8084
+ ): string {
8085
+ const provider = handlers.onAppendFinalAnswerSupplement;
8086
+ if (!provider) return req.finalText;
8087
+ const supplement = provider(req);
8088
+ if (typeof supplement !== 'string' || supplement.trim().length === 0) return req.finalText;
8089
+ const base = req.finalText;
8090
+ if (base.length === 0 || base.endsWith('\n')) return base + supplement.trim();
8091
+ return base + '\n\n' + supplement.trim();
8092
+ }
8093
+
8199
8094
  export async function streamLLMWithTools(
8200
8095
  messages: LLMMessage[],
8201
8096
  handlers: StreamWithToolsHandlers,
@@ -8519,6 +8414,8 @@ export async function streamLLMWithTools(
8519
8414
  // broken dispatch forever. Window=3 mirrors opencode's
8520
8415
  // session/processor.ts DOOM_LOOP_THRESHOLD.
8521
8416
  const toolErrorDoomTracker = new DoomLoopTracker(3);
8417
+ // Doom-loop self-heal attempts in this turn (guidance first, then stop).
8418
+ let doomSelfHealAttempts = 0;
8522
8419
  // A (RESEARCH-autonomous-runaway-discipline-2026-07-19 R1) — the error tracker
8523
8420
  // above only fires on *failing* calls; identical *successful* calls (the model
8524
8421
  // re-reading / re-grepping the same file over and over — the grep-thrash that
@@ -9022,13 +8919,17 @@ export async function streamLLMWithTools(
9022
8919
  verificationLoopState,
9023
8920
  finalization,
9024
8921
  );
9025
- if (finalText !== visibleText) {
8922
+ // 검증 보충 — final-answer supplement attachment point: let the caller
8923
+ // append verification-recency / loop-closure wording after the final
8924
+ // answer (on top of the onFinalAnswerSupplement foundation from PR #27520).
8925
+ const attachedText = attachFinalAnswerSupplement(handlers, { answer: visibleText.trim(), finalText });
8926
+ if (attachedText !== visibleText) {
9026
8927
  const answer = visibleText.trim();
9027
- handlers.onFinalAnswerSupplement?.({ answer, supplement: finalText.slice(answer.length).trim(), finalText });
8928
+ handlers.onFinalAnswerSupplement?.({ answer, supplement: attachedText.slice(answer.length).trim(), finalText: attachedText });
9028
8929
  }
9029
- if (finalText !== fullText) handlers.onText('', finalText);
9030
- emitTurnComplete(finalText);
9031
- return finalText;
8930
+ if (attachedText !== fullText) handlers.onText('', attachedText);
8931
+ emitTurnComplete(attachedText);
8932
+ return attachedText;
9032
8933
  }
9033
8934
 
9034
8935
  // No tool calls + empty text = model stalled. Re-prompt up to
@@ -9037,12 +8938,15 @@ export async function streamLLMWithTools(
9037
8938
  if (pendingCalls.length === 0 && turnText.length === 0) {
9038
8939
  // TUI-GOAL-LOOP-FRICTION(#63) — 종결 신호(update_goal)를 이미 받았고 답도 이미 썼다 = 할 말이 없는 게 맞다.
9039
8940
  // 빈 턴을 «정체»로 보고 재촉·`[NO FINAL SYNTHESIS]` 를 붙이지 않고 그 답으로 끝낸다.
9040
- if (terminalSignalDispatched && lastToolRoundText.trim().length > 0) {
9041
- const answer = lastToolRoundText;
8941
+ // TUI-H1-GOALLOOP-EMPTY-TAIL — 답이 이 스트림 «앞»에 이미 보였어도 같다(opts.terminalPriorAnswerVisible —
8942
+ // 예: Bash → «끝» → 재주입 → update_goal 만 → 빈 턴). 이 스트림에 답이 없으면 빈 문자열로 끝내고 화면은
8943
+ // 건드리지 않는다(onText 생략 · emitTurnComplete 는 빈 답을 이력에 안 싣는다). 마감은 호출자(goal loop) 몫.
8944
+ if (terminalSignalDispatched && (lastToolRoundText.trim().length > 0 || opts.terminalPriorAnswerVisible === true)) {
8945
+ const answer = lastToolRoundText.trim().length > 0 ? lastToolRoundText : '';
9042
8946
  debug.log('goal.loop', 'empty-turn-after-terminal-signal', {
9043
8947
  turn, emptyRetries, answerChars: answer.length,
9044
8948
  }, { level: 'info' });
9045
- if (answer !== fullText) handlers.onText('', answer);
8949
+ if (answer.length > 0 && answer !== fullText) handlers.onText('', answer);
9046
8950
  emitTurnComplete(answer);
9047
8951
  return answer;
9048
8952
  }
@@ -10659,41 +10563,36 @@ export async function streamLLMWithTools(
10659
10563
  debug.log('llm.tool-loop.retry', 'doom-loop-detected', {
10660
10564
  turn, tool: doomToolName, window: doomWindow,
10661
10565
  }, { level: 'warn' });
10662
- const applyAskOutcome = async (reason: DoomLoopReason, detail: string | null) => {
10566
+ const applySelfHeal = (reason: DoomLoopReason, detail: string | null) => {
10663
10567
  const interventionClass = classifyDoomLoopIntervention({
10664
10568
  detail: [detail, doomFailureDetail].filter(Boolean).join('\n'), doomWindow,
10665
10569
  });
10666
- const askDispatchOpts: DoomLoopRouteOpts = {
10667
- reason, detail, doomWindow, interventionClass,
10668
- };
10669
- if (opts.sessionId) askDispatchOpts.sessionId = opts.sessionId;
10670
- if (opts.signal) askDispatchOpts.signal = opts.signal;
10671
- const outcome = await routeDoomLoopToAskUser(askDispatchOpts);
10672
- if (outcome.kind === 'retry') {
10673
- toolErrorDoomTracker.reset();
10674
- postToolResultUserNote =
10675
- 'The previous tool call kept failing 3 times in a row. Retry the same approach once more — the user reset the doom-loop tracker.';
10676
- debug.log('llm.tool-loop.retry', 'doom-loop-ask-retry', {
10677
- turn, reason, interventionClass: outcome.interventionClass,
10678
- });
10679
- } else if (outcome.kind === 'guidance') {
10570
+ doomSelfHealAttempts += 1;
10571
+ const fingerprint = doomWindow[doomWindow.length - 1] ?? 'unknown';
10572
+ const outcome = decideDoomLoopSelfHeal({
10573
+ attempt: doomSelfHealAttempts, tool: doomToolName, fingerprint, interventionClass,
10574
+ });
10575
+ debug.log('llm.tool-loop.retry', 'doom-loop-self-heal', {
10576
+ turn,
10577
+ tool: doomToolName,
10578
+ attempt: doomSelfHealAttempts,
10579
+ action: outcome.kind,
10580
+ fingerprint,
10581
+ reason,
10582
+ interventionClass: outcome.interventionClass,
10583
+ }, { level: 'warn' });
10584
+ if (outcome.kind === 'guidance') {
10680
10585
  toolErrorDoomTracker.reset();
10681
10586
  postToolResultUserNote = outcome.userMessage;
10682
- debug.log('llm.tool-loop.retry', 'doom-loop-ask-guidance', {
10683
- turn, reason, interventionClass: outcome.interventionClass,
10684
- });
10685
10587
  } else {
10686
10588
  hardStopText = outcome.hardStopText;
10687
- debug.log('llm.tool-loop.retry', outcome.kind === 'fallback' ? 'doom-loop-ask-fallback' : 'doom-loop-ask-stop', {
10688
- turn, reason, interventionClass: outcome.interventionClass, window: doomWindow,
10689
- });
10690
10589
  }
10691
10590
  };
10692
10591
  if (retryDecision.action === 'auto-undo') {
10693
10592
  if (isPlanModeActive()) {
10694
- await applyAskOutcome('plan-mode', retryDecision.reason);
10593
+ applySelfHeal('plan-mode', retryDecision.reason);
10695
10594
  } else if (listSnapshots().length === 0) {
10696
- await applyAskOutcome('no-undo-available', 'UndoTurn: no snapshots available');
10595
+ applySelfHeal('no-undo-available', 'UndoTurn: no snapshots available');
10697
10596
  } else {
10698
10597
  try {
10699
10598
  const undoResult = await undoTurnRuntime.run({}, { surface: 'tui' });
@@ -10707,11 +10606,11 @@ export async function streamLLMWithTools(
10707
10606
  window: doomWindow,
10708
10607
  });
10709
10608
  } catch (err) {
10710
- await applyAskOutcome('undo-failed', err instanceof Error ? err.message : String(err));
10609
+ applySelfHeal('undo-failed', err instanceof Error ? err.message : String(err));
10711
10610
  }
10712
10611
  }
10713
10612
  } else {
10714
- await applyAskOutcome('policy', retryDecision.reason);
10613
+ applySelfHeal('policy', retryDecision.reason);
10715
10614
  }
10716
10615
  }
10717
10616
  if (executionDoomDetectedThisTurn && !hardStopText) {
@@ -10911,8 +10810,8 @@ export async function streamLLMWithTools(
10911
10810
  if (postToolResultUserNote) {
10912
10811
  // Appended after the tool_result user turn so the wire order is
10913
10812
  // assistant(tool_use) → user(tool_result) → user(intervention).
10914
- // This is the "retry" or "guidance" branch of the doom-loop ASK
10915
- // route — see routeDoomLoopToAskUser above.
10813
+ // Also carries the doom-loop self-heal guidance — see
10814
+ // decideDoomLoopSelfHeal above.
10916
10815
  history.push({ role: LOOP_SELF_NOTE_ROLE, content: postToolResultUserNote });
10917
10816
  }
10918
10817
  // 신호뿐인 턴 · 섞인 턴(정상 디스패치 · 부분 거부 · 통째 거부의 수신 확인) 모두 — 종결 신호가 이 턴에 «받아들여졌다».
@@ -3,7 +3,9 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renam
3
3
  import { dirname, join } from 'node:path';
4
4
  import { CardStore, taskCardsDir, type TaskCard } from '../../task-cards/card-store.js';
5
5
  import { Database } from 'bun:sqlite';
6
- import { type ChecklistItem } from '../../release-loop/checklist.js';
6
+ import { checklistDevVersion, listChecklist, type ChecklistItem } from '../../release-loop/checklist.js';
7
+ import { readTrafficSignal } from '../../seat-loop/traffic-signal.js';
8
+ import { runHoldHygiene, type HygieneCell, type HygieneDeps } from '../../steward/hold-hygiene.js';
7
9
  import { effectiveInstanceRoot } from '../../instance/resolve.js';
8
10
  import { getUserConfig, ORCHESTRATOR_DEFAULTS, resolveConfiguredAgentRoleLlm, type OrchestratorLoopConfig } from '../../user-config.js';
9
11
  import { rubricPriority } from '../../release-loop/rubric.js';
@@ -63,6 +65,8 @@ export type RebalanceAdapter = (version: string) => Promise<{ decisions: unknown
63
65
  export interface TickDeps {
64
66
  root?: string; now?: Date | (() => Date); mode?: Mode; runId?: string; window?: Window;
65
67
  heartbeatIntervalMs?: number;
68
+ hygieneCells?: () => HygieneCell[];
69
+ hygienePaths?: Pick<HygieneDeps, 'trafficPath' | 'metaPath' | 'dispatcherDir'>;
66
70
  cards?: () => TaskCard[]; split?: SplitAdapter; placeCell?: PlaceAdapter;
67
71
  degradation?: () => { llm: boolean; board: boolean };
68
72
  gridHosts?: readonly GridHost[];
@@ -888,7 +892,25 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
888
892
  }
889
893
  }
890
894
  }
891
- } else if (node === 'report') (deps.print ?? console.log)(reportLine(state));
895
+ } else if (node === 'report') {
896
+ if (mode === 'live') {
897
+ try {
898
+ const signal = readTrafficSignal(deps.hygienePaths?.trafficPath);
899
+ const versions = signal && !('unreadable' in signal) && signal.versions?.length ? signal.versions : [checklistDevVersion()];
900
+ let cells: HygieneCell[] = [];
901
+ if (deps.hygieneCells) cells = deps.hygieneCells();
902
+ else if (signal && !('unreadable' in signal) && signal.hold?.length) {
903
+ for (const version of versions) {
904
+ try { cells.push(...listChecklist(version).items); }
905
+ catch (error) { observe('hold-hygiene-checklist-unreadable', `${version}:${String(error).slice(0, 100)}`, 0); }
906
+ }
907
+ }
908
+ const sent = runHoldHygiene({ root, now, cells, ...deps.hygienePaths });
909
+ for (const notice of sent) observe('exchange', `hold-hygiene:${notice.key}`, 1, SEAT_LOOP_IDS[notice.to]);
910
+ } catch (error) { observe('hold-hygiene-failed', String(error).slice(0, 160), 0); }
911
+ }
912
+ (deps.print ?? console.log)(reportLine(state));
913
+ }
892
914
  state.nodes[node] = 'ok';
893
915
  } else if (state.nodes[node] !== 'ok') {
894
916
  // A rerun of a node that already completed keeps its result (review round 3): only never-run nodes become skipped.
@@ -82,7 +82,7 @@ export const FEATURE_MATURITY = {
82
82
  onboarding: 'beta', login: 'stable', session: 'beta', chat: 'stable', repl: 'stable',
83
83
  agent: 'stable', registry: 'beta', tier: 'stable', local: 'beta', scheduler: 'beta',
84
84
  attach: 'beta', wf: 'beta', nexus: 'stable', phone: 'beta', token: 'beta',
85
- acp: 'beta', voice: 'beta',
85
+ acp: 'beta', voice: 'beta', sim: 'beta',
86
86
  'run-detached': 'system', dogfood: 'system', run: 'system',
87
87
  },
88
88
  // Canonical names only; aliases inherit their command's grade at lookup.