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/index.ts CHANGED
@@ -6,6 +6,7 @@ import { accessSync, constants as fsConstants, existsSync, lstatSync, readFileSy
6
6
  import type { SelfDevRunParticipant, SelfDevRunState } from './self-dev/run-store.js';
7
7
  import { applyConfigDirFlagFromArgv } from './cli/config-dir-flag.js';
8
8
  import { LOGS_SINCE_OPTION, registerLogsCommands } from './cli/logs-cli.js';
9
+ import { registerSimCommands } from './cli/sim-cli.js';
9
10
  import { registerDocsCommands } from './cli/docs-cli.js';
10
11
  import { registerBenchCommands } from './cli/bench-cli.js';
11
12
  import { registerRetroCommands } from './cli/retro-cli.js';
@@ -4370,7 +4371,7 @@ selfCmd
4370
4371
  }
4371
4372
  };
4372
4373
  try {
4373
- const { dispatchHeavyCheck } = await import('./self-implement/gate-remote.js');
4374
+ const { dispatchHeavyCheck, remoteSelfGateArgv } = await import('./self-implement/gate-remote.js');
4374
4375
  const { runGitCommand } = await import('./git-fs/runner.js');
4375
4376
  const topLevel = runGitCommand(process.cwd(), ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });
4376
4377
  if (topLevel.status !== 0) throw new Error(`git rev-parse --show-toplevel failed: ${topLevel.stderr.trim() || `rc=${topLevel.status}`}`);
@@ -4389,13 +4390,13 @@ selfCmd
4389
4390
  const { remoteCommitSupportsArtifactDir, newRemoteArtifactDir } = await import('./self-implement/gate-artifacts.js');
4390
4391
  remoteArtifacts = remoteCommitSupportsArtifactDir(repo) ? { dir: newRemoteArtifactDir() } : { unsupported: true };
4391
4392
  }
4392
- const remoteArtifactArgs = remoteArtifacts && 'dir' in remoteArtifacts ? ['--artifact-dir', remoteArtifacts.dir] : [];
4393
4393
  // origin/main is pinned to this machine's origin/main on the host; any other base is passed as its resolved sha.
4394
- const remoteArgv = (sha: (ref: string) => string) => ['bun', 'bin/elanous.mjs', 'self', 'gate', '--local', ...(opts.changed ? ['--changed'] : []),
4395
- ...(opts.base ? ['--base', opts.base === 'origin/main' ? opts.base : sha(opts.base)] : []), ...(shards !== undefined ? ['--shards', String(shards)] : []),
4394
+ const remoteArgv = (sha: (ref: string) => string) => remoteSelfGateArgv({
4395
+ changed: opts.changed, base: opts.base, shards, fixedShardDistribution: opts.fixedShardDistribution,
4396
+ includeImporters: opts.includeImporters,
4396
4397
  // A measured commit without this option rejects it (exit ≠ 0) — read as not passed, never as a silent measured run.
4397
- ...(opts.fixedShardDistribution ? ['--fixed-shard-distribution'] : []),
4398
- ...(opts.includeImporters ? ['--include-importers'] : []), ...remoteArtifactArgs];
4398
+ artifactDir: remoteArtifacts && 'dir' in remoteArtifacts ? remoteArtifacts.dir : undefined,
4399
+ }, sha);
4399
4400
  const { gateHostReservationRoot } = await import('./release-loop/gate-host-reservation.js');
4400
4401
  process.exitCode = await dispatchHeavyCheck({
4401
4402
  tool: 'self-gate', repo, installPwa: true, remoteArgv, refs: opts.base ? [opts.base] : [], reservationRoot: gateHostReservationRoot(),
@@ -6175,6 +6176,7 @@ async function loadJevAccessOrExit(): Promise<import('./decide/jev.js').JevAcces
6175
6176
  registerIntakeCommands(program);
6176
6177
 
6177
6178
  registerLogsCommands(program);
6179
+ registerSimCommands(program);
6178
6180
 
6179
6181
  // ── ad (marketing-ad pipeline entrance) ──
6180
6182
  /** ⛔ 「있다」와 「읽을 수 있는 파일이다」는 다른 값 — 디렉토리·권한 없는 파일을 실사로 읽지 않는다. */
@@ -15,6 +15,11 @@
15
15
  // - ⏹ 정확히 `/stop`·`/esc` 인 글(TUI-STOP-INPUT-BYPASS · #68): 대기열을 «건너뛰고» 지금 턴의 ESC 중단 경로
16
16
  // (`requestStop` → 대시보드 escGate)를 바로 부른다. 대기 중인 일반 항목은 그대로·순서대로 남는다.
17
17
  // 턴이 없으면 아무것도 안 하고 `tui-stop-no-active-turn`(멈출 길이 없으면 `tui-stop-undelivered`)으로 던져 바깥 호출자(rc≠0)에게 알린다.
18
+ // - ⏹ 원시 pty 길(TUI-STOP-RAW-PTY): `pty text pty_<id> /stop --enter`·tmux 처럼 TUI 를 띄운 «PTY 자체»에 쓰면
19
+ // 이 라우터를 안 지나 stdin 으로 들어오고, 턴 중 typeahead 가 «대기(응답 뒤)»에 쌓았다(10-17 재점검).
20
+ // ⇒ 턴 중 스트리밍 리스너가 stdin «한 청크»가 정확히 `/stop⏎`·`/esc⏎` 이면 `routeRawPtyStop` 으로 같은 `routeStop` 을 부른다.
21
+ // 키보드는 한 키씩 오고 붙여넣기는 봉투에 싸이므로, 한 청크에 온 «글 한 줄 ⊕ Enter» 는 바깥 주입이다.
22
+ // 턴 밖·Enter 없음·그 밖의 글은 종전대로 키로 흐른다(관측 `path: 'raw-pty'` · 제어 IPC 는 `'control-ipc'`).
18
23
  //
19
24
  // ⚠️ herdr/tmux 처럼 «터미널 자체»에 쓰는 길은 키보드와 구별할 수 없다 — 이 라우터는
20
25
  // elanous 제어 IPC(`pty text`) 경로만 가른다.
@@ -40,11 +45,13 @@ export interface ExternalInputHost {
40
45
  requestStop?(): boolean;
41
46
  /** 외부 stop 의 결과 표지(채팅 로그 한 줄). */
42
47
  onExternalStop?(result: 'stopped' | 'no-turn' | 'failed'): void;
43
- /** 대기열이 바뀌었다 — 화면 표지를 다시 그린다. */
44
- onQueueChange?(pending: readonly ExternalInputItem[], event: 'queued' | 'delivered'): void;
48
+ /** 대기열의 현재 상태가 바뀌었다 — 화면 표지를 다시 그린다. */
49
+ onQueueChange?(pending: readonly ExternalInputItem[]): void;
45
50
  }
46
51
 
47
52
  export type ExternalInputRoute = 'passthrough' | 'submitted' | 'queued' | 'stopped';
53
+ /** 외부 stop 이 들어온 길 — 제어 IPC(`tui:<pid>` · 이 라우터) / 원시 pty(stdin 한 청크). */
54
+ export type ExternalStopPath = 'control-ipc' | 'raw-pty';
48
55
  export type ExternalInputDecision = 'passthrough' | 'submit-now' | 'queue' | 'reject';
49
56
 
50
57
  const EXTERNAL_INPUT_SOURCE: InputSourceRef = { kind: 'terminal', provider: 'pty' };
@@ -133,8 +140,8 @@ export function createExternalInputQueue(deps: ExternalInputQueueDeps = {}) {
133
140
  // 경고는 머리 항목 «하나»에 한 번 — 머리가 실제로 바뀔 때만 다시 경고할 수 있다(제출 실패로 남은 같은 머리는 유지).
134
141
  let staleTurnWarnedHead: ExternalInputItem | null = null;
135
142
 
136
- const notify = (event: 'queued' | 'delivered'): void => {
137
- try { host?.onQueueChange?.(pending, event); } catch { /* fail-soft — 표지 실패가 입력을 막지 않는다 */ }
143
+ const notify = (): void => {
144
+ try { host?.onQueueChange?.(pending); } catch { /* fail-soft — 표지 실패가 입력을 막지 않는다 */ }
138
145
  };
139
146
  const ensurePump = (): void => {
140
147
  if (pending.length > 0 && !stopPump) stopPump = schedule(pump, PUMP_MS);
@@ -163,20 +170,20 @@ export function createExternalInputQueue(deps: ExternalInputQueueDeps = {}) {
163
170
  if (!ok) { log('deliver-failed', { length: head.text.length, pending: pending.length }); return false; }
164
171
  pending = pending.slice(1);
165
172
  log('delivered', { length: head.text.length, waitedMs: now() - head.at, pending: pending.length });
166
- notify('delivered');
173
+ notify();
167
174
  ensurePump();
168
175
  return true;
169
176
  }
170
177
 
171
178
  /** ⏹ 외부 stop — 대기열을 건너뛰고 지금 턴의 escGate 로. 대기열은 건드리지 않는다. */
172
- function routeStop(h: ExternalInputHost, text: string): ExternalInputRoute {
179
+ function routeStop(h: ExternalInputHost, text: string, path: ExternalStopPath): ExternalInputRoute {
173
180
  let turnActive = false;
174
181
  try { turnActive = h.isTurnActive(); } catch { turnActive = false; }
175
182
  let stopped = false;
176
183
  if (turnActive && h.requestStop) {
177
184
  try { stopped = h.requestStop(); } catch { stopped = false; }
178
185
  }
179
- logInput('external-stop', { hadTurn: turnActive, stopped, command: text, pending: pending.length });
186
+ logInput('external-stop', { hadTurn: turnActive, stopped, command: text, pending: pending.length, path });
180
187
  const result = stopped ? 'stopped' : turnActive ? 'failed' : 'no-turn';
181
188
  try { h.onExternalStop?.(result); } catch { /* fail-soft */ }
182
189
  if (result === 'no-turn') throw new Error('tui-stop-no-active-turn');
@@ -189,6 +196,7 @@ export function createExternalInputQueue(deps: ExternalInputQueueDeps = {}) {
189
196
  host = next;
190
197
  log('host-registered', { pending: pending.length });
191
198
  ensurePump();
199
+ if (pending.length > 0) notify();
192
200
  return () => {
193
201
  if (host !== next) return;
194
202
  host = null;
@@ -200,7 +208,7 @@ export function createExternalInputQueue(deps: ExternalInputQueueDeps = {}) {
200
208
  // `pty key`(input-key)는 «키»다 — 공백·`j` 처럼 출력 가능한 키도 종전대로 넣는다.
201
209
  if (keyCommandDepth > 0) return 'passthrough';
202
210
  const line = parseExternalTextLine(chars);
203
- if (line && host && isExternalStopCommand(line.text)) return routeStop(host, line.text);
211
+ if (line && host && isExternalStopCommand(line.text)) return routeStop(host, line.text, 'control-ipc');
204
212
  let turnActive = false; let composer: string | undefined; let pendingKeys = false;
205
213
  if (host && line) {
206
214
  try { turnActive = host.isTurnActive(); composer = host.composerText(); pendingKeys = host.hasPendingKeys(); }
@@ -229,10 +237,26 @@ export function createExternalInputQueue(deps: ExternalInputQueueDeps = {}) {
229
237
  }
230
238
  pending = [...pending, { text: line!.text, at: now() }];
231
239
  log('route', { ...base, route: 'queued', pending: pending.length });
232
- notify('queued');
240
+ notify();
233
241
  ensurePump();
234
242
  return 'queued';
235
243
  },
244
+ /**
245
+ * ⏹ 원시 pty 길 — 턴 중 stdin «한 청크»가 정확히 `/stop⏎`·`/esc⏎` 이면 `routeStop` 으로 보내고 true(키로 흘리지 않는다).
246
+ * 턴 밖·호스트 없음·Enter 없음·그 밖의 글은 false — 호출측이 종전대로 키를 넣는다.
247
+ */
248
+ routeRawStop(chunk: string): boolean {
249
+ const h = host;
250
+ if (!h) return false;
251
+ const line = parseExternalTextLine(chunk);
252
+ if (!line?.submit || !isExternalStopCommand(line.text)) return false;
253
+ let turnActive = false;
254
+ try { turnActive = h.isTurnActive(); } catch { turnActive = false; }
255
+ if (!turnActive) return false;
256
+ // 'failed'(중단 경로 없음)도 소비한다 — 표지(Esc 로 멈춰 주세요)는 routeStop 이 이미 남겼다.
257
+ try { routeStop(h, line.text, 'raw-pty'); } catch { /* 결과는 이벤트·표지로 남았다 */ }
258
+ return true;
259
+ },
236
260
  pump,
237
261
  pending: (): readonly ExternalInputItem[] => pending,
238
262
  };
@@ -249,3 +273,4 @@ const shared = createExternalInputQueue();
249
273
 
250
274
  export function registerExternalInputHost(host: ExternalInputHost): () => void { return shared.register(host); }
251
275
  export function routeExternalInput(chars: string): ExternalInputRoute { return shared.route(chars); }
276
+ export function routeRawPtyStop(chunk: string): boolean { return shared.routeRawStop(chunk); }
@@ -40,12 +40,48 @@ const lastBySurface = new Map<string, RouteDecision>();
40
40
 
41
41
  export function recordCurrentRouteDecision(surface: string, decision: RouteDecision): void {
42
42
  lastBySurface.set(surface, decision);
43
+ // 새 턴의 결정이 서면 지난 턴의 «실제 실행»은 더는 이 턴의 것이 아니다.
44
+ executionBySurface.delete(surface);
43
45
  }
44
46
 
45
47
  export function currentRouteDecision(surface: string): RouteDecision | null {
46
48
  return lastBySurface.get(surface) ?? null;
47
49
  }
48
50
 
51
+ /** 이번 턴에 «실제로» 응답한 공급자·모델(TUI-MODEL-BADGE-ACTUAL · 10-16).
52
+ * 결정(`RouteDecision`)은 턴 «시작»에 고른 값이고, 폴백 체인이 다른 공급자로 넘기면 실제는 달라진다.
53
+ * ACP 사용량 봉투(`routeProvider`·`model`)로 들어온다 — 이 표면이 결정을 세운 in-process 세션만 적는다.
54
+ * ⚠️ 지금 읽는 곳은 완료 배지뿐이다. 상태줄·`/model` «지금:» 의 원천 통일은 후속 칸 TUI-MODEL-BADGE-SOURCE. */
55
+ export interface RouteExecution {
56
+ provider?: string;
57
+ model: string;
58
+ }
59
+
60
+ const executionBySurface = new Map<string, RouteExecution>();
61
+
62
+ export function recordRouteExecution(surface: string, execution: RouteExecution): void {
63
+ executionBySurface.set(surface, execution);
64
+ }
65
+
66
+ export function currentRouteExecution(surface: string): RouteExecution | null {
67
+ return executionBySurface.get(surface) ?? null;
68
+ }
69
+
70
+ /** 표면의 결정·실행을 비운다(세션 핀은 사람의 선택이라 남긴다) — 짝이 안 맞는 턴(원격 등)에서 지난 «A → B» 가
71
+ * 다시 보이지 않게 한다(r5 리뷰). */
72
+ export function clearSurfaceRouteDecision(surface: string): void {
73
+ lastBySurface.delete(surface);
74
+ executionBySurface.delete(surface);
75
+ }
76
+
77
+ /** 턴이 고른 모델과 실제로 응답한 모델이 다르면 «처음 → 실제», 아니면 null(꾸미지 않는다). */
78
+ export function routeFallbackLabel(surface: string): string | null {
79
+ const decision = lastBySurface.get(surface);
80
+ const execution = executionBySurface.get(surface);
81
+ if (!decision?.model || !execution?.model || execution.model === decision.model) return null;
82
+ return `${decision.model} → ${execution.model}`;
83
+ }
84
+
49
85
  /** A `/model` choice scoped to this process's surface (TUI-MODEL-SESSION-SCOPE · 10-14).
50
86
  * It reaches the turn as `explicitModel`, so it wins over the Codex tier policy —
51
87
  * a config-only `llm.model` change is ignored by that policy (10-14 OPT: `/model luna`
@@ -18,6 +18,12 @@ export const REPAIR_PHASE_READ_ONLY_OPS_TOOLS = ['MsgList', 'ChecklistList'] as
18
18
  * rejects the steer target loops forever (#62 repair · #67 verify — MsgPost).
19
19
  * - Agent is allowed in every action phase, so its result (AgentOutput/AgentList) must be
20
20
  * too, or a delegated result is dropped and the parent redoes the work (#41).
21
+ * - Read-only observation of the run itself (TUI-OPS-TURN-GUARD gap ②, old-build samples where
22
+ * repair/verify rejected these mid-ops-turn): `logs_query` (logs.db READ-ONLY) and
23
+ * `PtyShellPoll` (drains output of a PTY the turn already started — no input, no spawn).
24
+ * Deliberately NOT here: `schedule_manage` (create/update/delete mutate crontab) and
25
+ * `se_build` (`stop` SIGTERMs a mission) — the allow list is by name, so a tool with any
26
+ * write action stays phase-governed.
21
27
  * Source edits stay governed by each phase's own Edit/Write entries — nothing here edits files. */
22
28
  export const ACTION_PHASE_OPS_TOOLS = [
23
29
  'MsgList',
@@ -30,4 +36,6 @@ export const ACTION_PHASE_OPS_TOOLS = [
30
36
  'ChecklistMove',
31
37
  'AgentOutput',
32
38
  'AgentList',
39
+ 'logs_query',
40
+ 'PtyShellPoll',
33
41
  ] as const;
@@ -0,0 +1,209 @@
1
+ // ── Codex(Responses API) 요청 «모양» 요약 — 4xx 거절 관측용 (TUI-OBS-400-SHAPE · 결함 #69) ──
2
+ //
3
+ // 왜: Codex 백엔드는 400 을 `{"detail":"Bad Request"}` 한 줄로만 돌려준다. 그래서 «요청이 너무 컸나 ·
4
+ // 메시지가 몇 개였나 · tool_call ↔ tool_result 짝이 깨졌나 · 첨부가 섞였나» 를 로그로 가를 수 없었다
5
+ // (10-16 OPT 실측: 같은 문면이 «합쳤을 때만» 400 — 원인 추정 불가).
6
+ //
7
+ // 무엇을 싣나: 수·바이트·종류·짝 깨짐 수 «만». ⛔ 본문·프롬프트·도구 인자·도구 출력·사용자 텍스트·키는
8
+ // 싣지 않는다. 짝 깨진 call_id 는 sha256 앞 8자로만(상한 5개) — 같은 id 가 다음 런에도 보이는지 대조용.
9
+ // 순수 함수 — 호출부(streamCodexResponsesEvents)가 실제 전송 객체와 전송 문자열을 넘긴다.
10
+
11
+ import { createHash } from 'node:crypto';
12
+
13
+ const HASH_SAMPLE_CAP = 5;
14
+
15
+ // ⛔ 카운트 «키» 도 요청에서 온 문자열이다 — 알려진 종류만 키로 쓰고 나머지는 `unknown` 으로 접는다
16
+ // (잘못된 type/role 문자열에 내용이 들어 있어도 이벤트에 새지 않게 · 리뷰 r1).
17
+ const KNOWN_ROLES = new Set(['user', 'assistant', 'system', 'developer', 'tool']);
18
+ const KNOWN_ITEM_TYPES = new Set([
19
+ 'message', 'function_call', 'function_call_output', 'reasoning', 'custom_tool_call', 'custom_tool_call_output',
20
+ 'local_shell_call', 'local_shell_call_output', 'web_search_call', 'image_generation_call', 'item_reference',
21
+ ]);
22
+ const KNOWN_CONTENT_TYPES = new Set([
23
+ 'input_text', 'output_text', 'input_image', 'input_file', 'input_audio', 'refusal', 'summary_text', 'reasoning_text', 'string',
24
+ ]);
25
+ const KNOWN_EFFORTS = new Set(['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
26
+ const KNOWN_SUMMARIES = new Set(['auto', 'concise', 'detailed', 'none']);
27
+ const KNOWN_TOOL_CHOICES = new Set(['auto', 'required', 'none', 'function', 'allowed_tools', 'custom']);
28
+ const MODEL_ID_RE = /^[A-Za-z0-9._:/@+-]{1,80}$/;
29
+
30
+ function known(set: Set<string>, value: unknown): string {
31
+ return typeof value === 'string' && set.has(value) ? value : 'unknown';
32
+ }
33
+
34
+ function hash8(id: string): string {
35
+ return createHash('sha256').update(id).digest('hex').slice(0, 8);
36
+ }
37
+
38
+ function byteLen(value: unknown): number {
39
+ try {
40
+ const text = typeof value === 'string' ? value : JSON.stringify(value ?? null);
41
+ return Buffer.byteLength(text ?? '', 'utf8');
42
+ } catch {
43
+ return -1;
44
+ }
45
+ }
46
+
47
+ function bump(counts: Record<string, number>, key: string): void {
48
+ counts[key] = (counts[key] ?? 0) + 1;
49
+ }
50
+
51
+ function countContentBlocks(content: unknown, counts: Record<string, number>, empty: { n: number }): void {
52
+ if (typeof content === 'string') {
53
+ bump(counts, 'string');
54
+ if (content.length === 0) empty.n += 1;
55
+ return;
56
+ }
57
+ if (!Array.isArray(content)) return;
58
+ for (const block of content) {
59
+ if (!block || typeof block !== 'object') { bump(counts, 'unknown'); continue; }
60
+ const b = block as Record<string, unknown>;
61
+ const type = typeof b.type === 'string' ? b.type : '';
62
+ bump(counts, known(KNOWN_CONTENT_TYPES, type));
63
+ if ((type === 'input_text' || type === 'output_text') && typeof b.text === 'string' && b.text.length === 0) empty.n += 1;
64
+ }
65
+ }
66
+
67
+ export interface CodexRequestShape {
68
+ status: number;
69
+ /** UTF-8 bytes of the exact JSON string sent on the wire. */
70
+ bytes: number;
71
+ instructionsBytes: number;
72
+ inputBytes: number;
73
+ toolsBytes: number;
74
+ /** Largest single input item, bytes — one huge tool output vs. many small items. */
75
+ maxInputItemBytes: number;
76
+ inputItemCount: number;
77
+ /** Items carrying a `role` (user/assistant/system/developer messages). */
78
+ messageCount: number;
79
+ roleCounts: Record<string, number>;
80
+ /** Top-level item kinds: message / function_call / function_call_output / reasoning / … */
81
+ itemTypeCounts: Record<string, number>;
82
+ /** Content block kinds across messages and array-form tool outputs (input_text / input_image / …). */
83
+ contentTypeCounts: Record<string, number>;
84
+ emptyTextBlockCount: number;
85
+ toolCallCount: number;
86
+ toolResultCount: number;
87
+ /** function_call «items» whose call_id has no function_call_output anywhere (counted per item, duplicates included). */
88
+ unpairedToolCallCount: number;
89
+ /** function_call_output «items» whose call_id has no function_call anywhere (counted per item). */
90
+ orphanToolResultCount: number;
91
+ /** function_call_output items whose call_id has a function_call only «after» them. */
92
+ toolResultBeforeCallCount: number;
93
+ /** function_call items beyond the first for the same call_id. */
94
+ duplicateToolCallIdCount: number;
95
+ missingCallIdCount: number;
96
+ unpairedToolCallIdHashes: string[];
97
+ orphanToolResultIdHashes: string[];
98
+ toolDefinitionCount: number;
99
+ model?: string;
100
+ reasoningEffort?: string;
101
+ reasoningSummary?: string;
102
+ store: boolean;
103
+ hasPreviousResponseId: boolean;
104
+ toolChoice?: string;
105
+ hasPromptCacheKey: boolean;
106
+ /** Size of the provider's error body — the body itself is not recorded. */
107
+ errorBodyBytes?: number;
108
+ }
109
+
110
+ /** Summarise a Codex Responses request body for a rejected (4xx) call. Never copies text content. */
111
+ export function summarizeCodexRequestShape(
112
+ wireBody: Record<string, unknown>,
113
+ wireJson: string,
114
+ status: number,
115
+ errorBodyText?: string,
116
+ ): CodexRequestShape {
117
+ const input = Array.isArray(wireBody.input) ? wireBody.input : [];
118
+ const roleCounts: Record<string, number> = {};
119
+ const itemTypeCounts: Record<string, number> = {};
120
+ const contentTypeCounts: Record<string, number> = {};
121
+ const empty = { n: 0 };
122
+ // 항목별로 센다(리뷰 r2) — 같은 call_id 가 여러 번 나와도 짝 깨짐 수는 «항목 수» 다.
123
+ const calls: Array<{ id: string; index: number }> = [];
124
+ const results: Array<{ id: string; index: number }> = [];
125
+ let toolCallCount = 0;
126
+ let toolResultCount = 0;
127
+ let missingCallIdCount = 0;
128
+ let maxInputItemBytes = 0;
129
+
130
+ input.forEach((raw, index) => {
131
+ maxInputItemBytes = Math.max(maxInputItemBytes, byteLen(raw));
132
+ if (!raw || typeof raw !== 'object') { bump(itemTypeCounts, 'unknown'); return; }
133
+ const item = raw as Record<string, unknown>;
134
+ const type = typeof item.type === 'string' ? item.type : (typeof item.role === 'string' ? 'message' : 'unknown');
135
+ bump(itemTypeCounts, known(KNOWN_ITEM_TYPES, type));
136
+ if (typeof item.role === 'string') {
137
+ bump(roleCounts, known(KNOWN_ROLES, item.role));
138
+ countContentBlocks(item.content, contentTypeCounts, empty);
139
+ }
140
+ if (type === 'function_call') {
141
+ toolCallCount += 1;
142
+ const id = typeof item.call_id === 'string' && item.call_id ? item.call_id : '';
143
+ if (!id) { missingCallIdCount += 1; return; }
144
+ calls.push({ id, index });
145
+ } else if (type === 'function_call_output') {
146
+ toolResultCount += 1;
147
+ countContentBlocks(item.output, contentTypeCounts, empty);
148
+ const id = typeof item.call_id === 'string' && item.call_id ? item.call_id : '';
149
+ if (!id) { missingCallIdCount += 1; return; }
150
+ results.push({ id, index });
151
+ }
152
+ });
153
+
154
+ const firstCallAt = new Map<string, number>();
155
+ for (const c of calls) if (!firstCallAt.has(c.id)) firstCallAt.set(c.id, c.index);
156
+ const resultIds = new Set(results.map((r) => r.id));
157
+ const unpairedItems = calls.filter((c) => !resultIds.has(c.id));
158
+ const orphanItems = results.filter((r) => !firstCallAt.has(r.id));
159
+ const resultBeforeCall = results.filter((r) => firstCallAt.has(r.id) && r.index < (firstCallAt.get(r.id) as number)).length;
160
+ const duplicateToolCallIdCount = calls.length - firstCallAt.size;
161
+ const unpaired = [...new Set(unpairedItems.map((c) => c.id))];
162
+ const orphan = [...new Set(orphanItems.map((r) => r.id))];
163
+
164
+ const reasoning = wireBody.reasoning && typeof wireBody.reasoning === 'object'
165
+ ? wireBody.reasoning as Record<string, unknown> : undefined;
166
+ const toolChoiceRaw = wireBody.tool_choice;
167
+ const toolChoice = toolChoiceRaw === undefined || toolChoiceRaw === null
168
+ ? undefined
169
+ : known(KNOWN_TOOL_CHOICES, typeof toolChoiceRaw === 'object' ? (toolChoiceRaw as Record<string, unknown>).type : toolChoiceRaw);
170
+ const tools = Array.isArray(wireBody.tools) ? wireBody.tools : [];
171
+
172
+ return {
173
+ status,
174
+ bytes: Buffer.byteLength(wireJson, 'utf8'),
175
+ instructionsBytes: typeof wireBody.instructions === 'string' ? Buffer.byteLength(wireBody.instructions, 'utf8') : 0,
176
+ inputBytes: byteLen(input),
177
+ toolsBytes: byteLen(tools),
178
+ maxInputItemBytes,
179
+ inputItemCount: input.length,
180
+ messageCount: Object.values(roleCounts).reduce((a, b) => a + b, 0),
181
+ roleCounts,
182
+ itemTypeCounts,
183
+ contentTypeCounts,
184
+ emptyTextBlockCount: empty.n,
185
+ toolCallCount,
186
+ toolResultCount,
187
+ unpairedToolCallCount: unpairedItems.length,
188
+ orphanToolResultCount: orphanItems.length,
189
+ toolResultBeforeCallCount: resultBeforeCall,
190
+ duplicateToolCallIdCount,
191
+ missingCallIdCount,
192
+ unpairedToolCallIdHashes: unpaired.slice(0, HASH_SAMPLE_CAP).map(hash8),
193
+ orphanToolResultIdHashes: orphan.slice(0, HASH_SAMPLE_CAP).map(hash8),
194
+ toolDefinitionCount: tools.length,
195
+ ...(typeof wireBody.model === 'string' ? { model: MODEL_ID_RE.test(wireBody.model) ? wireBody.model : 'unknown' } : {}),
196
+ ...(reasoning?.effort !== undefined ? { reasoningEffort: known(KNOWN_EFFORTS, reasoning.effort) } : {}),
197
+ ...(reasoning?.summary !== undefined ? { reasoningSummary: known(KNOWN_SUMMARIES, reasoning.summary) } : {}),
198
+ store: wireBody.store === true,
199
+ hasPreviousResponseId: typeof wireBody.previous_response_id === 'string' && wireBody.previous_response_id.length > 0,
200
+ ...(toolChoice ? { toolChoice } : {}),
201
+ hasPromptCacheKey: typeof wireBody.prompt_cache_key === 'string',
202
+ ...(typeof errorBodyText === 'string' ? { errorBodyBytes: Buffer.byteLength(errorBodyText, 'utf8') } : {}),
203
+ };
204
+ }
205
+
206
+ /** 4xx rejections whose cause can be the request shape. Auth (401/403) and quota (429) are not shape problems. */
207
+ export function isShapeRelevantRejection(status: number): boolean {
208
+ return status >= 400 && status < 500 && status !== 401 && status !== 403 && status !== 429;
209
+ }