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
@@ -39,6 +39,8 @@ import {
39
39
  } from '../agent/task-notification.js';
40
40
  import type { getUserConfig } from '../user-config.js';
41
41
  import { isNativeStructureEnabledForProvider, resolveActiveProvider } from '../user-config.js';
42
+ import { measureInputLagTurnStart } from '../tui/input-lag-probe.js';
43
+ import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
42
44
 
43
45
  type UserConfig = ReturnType<typeof getUserConfig>;
44
46
 
@@ -61,8 +63,6 @@ export interface DashboardTurnPreambleContext {
61
63
  modelFamily?: ModelFamily;
62
64
  /** Current request session ID. It is injected only while this preamble is built for the turn. */
63
65
  sessionId?: string;
64
- /** Catalog richness for non-dashboard callers; the essential dashboard passes false. */
65
- rich?: boolean;
66
66
  /** Active tool names for session-specific guidance (Wave 3,
67
67
  * 2026-05-04). When provided, the universal preamble emits per-tool
68
68
  * one-liner directives (e.g. AskUserQuestion → use for tool-deny
@@ -85,40 +85,108 @@ export function buildControllerSystemMessage(controller: string, channels?: stri
85
85
  };
86
86
  }
87
87
 
88
+ /** How old a git snapshot the TUI preamble may serve while it rebuilds in the background.
89
+ * Edits (post-Edit force refresh) and HEAD moves invalidate it earlier; the dashboard's
90
+ * 5s git tick keeps it younger than this so an idle-then-type turn still hits. */
91
+ const DASHBOARD_GIT_SNAPSHOT_STALE_MS = 60_000;
92
+
93
+ /** TUI-TYPING-LAG-OBS 시험 seam — 이 ms 만큼 turn-preamble 을 «동기로» 막아 타이핑 지연을 일으킨다.
94
+ * 운영 기본 0. 켜는 길 둘: ux-sim·단위 시험은 `setPreambleSyncBlockMsForTesting` · 격리 TUI 는
95
+ * env `ELANOUS_TEST_PREAMBLE_SYNC_BLOCK_MS`(⛔ test 우주에서만 읽는다 — 운영 우주에선 무시).
96
+ * test 우주 = 이 프로세스의 실효 뿌리가 운영 집이 아닌 것. `--test` 플래그만 보면 pty-drive 가
97
+ * `--test` 를 `--config-dir <격리 뿌리>` 로 바꿔 띄운 시험 자식에서 seam 이 안 켜진다. */
98
+ export const PREAMBLE_SYNC_BLOCK_ENV = 'ELANOUS_TEST_PREAMBLE_SYNC_BLOCK_MS';
99
+ let preambleSyncBlockOverrideMs: number | null = null;
100
+
101
+ export function setPreambleSyncBlockMsForTesting(ms: number | null): void {
102
+ preambleSyncBlockOverrideMs = ms;
103
+ }
104
+
105
+ export function preambleSyncBlockMs(
106
+ env: NodeJS.ProcessEnv = process.env,
107
+ instanceRoot: () => string = effectiveInstanceRoot,
108
+ ): number {
109
+ if (preambleSyncBlockOverrideMs !== null) return Math.max(0, preambleSyncBlockOverrideMs);
110
+ const raw = env[PREAMBLE_SYNC_BLOCK_ENV];
111
+ if (!raw || instanceRoot() === prodInstanceRoot()) return 0;
112
+ const ms = Number(raw);
113
+ return Number.isFinite(ms) && ms > 0 ? ms : 0;
114
+ }
115
+
116
+ /** 일부러 이벤트 루프를 동기로 막는다(시험 seam). 시계를 돌며 도는 바쁜 루프 대신 `Atomics.wait` —
117
+ * CPU 를 태우지 않고, 시험이 시계를 가짜로 바꿔도 끝난다. */
118
+ function blockPreambleSync(ms: number): void {
119
+ if (ms <= 0) return;
120
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
121
+ }
122
+
88
123
  export function buildDashboardTurnPreamble(
89
124
  ctx: DashboardTurnPreambleContext,
90
125
  ): LLMMessage[] {
126
+ return measureInputLagTurnStart(() => {
127
+ blockPreambleSync(preambleSyncBlockMs());
128
+ return buildDashboardTurnPreambleUnmeasured(ctx);
129
+ });
130
+ }
131
+
132
+ /** TURN-START-BLOCK-REST 1단계 — 빌더마다 걸린 ms(0.1ms 반올림)를 `chat.turn-preamble built` 의
133
+ * `builderMs` 칸에 싣는다. 시계는 빌더마다 앞뒤 한 번씩이고, `built` 이벤트가 안 나가는
134
+ * (`debug.enabled` 꺼짐) 턴에서는 재지 않는다. 이 함수의 빌더는 전부 동기라 잰 ms 가 곧
135
+ * 이벤트 루프를 막은 시간이다(`builderMsKind: 'sync'`). */
136
+ type BuilderMs = Record<string, number>;
137
+
138
+ function timed<T>(ms: BuilderMs | null, name: string, fn: () => T): T {
139
+ if (ms === null) return fn();
140
+ const t0 = performance.now();
141
+ try {
142
+ return fn();
143
+ } finally {
144
+ ms[name] = Math.round((performance.now() - t0) * 10) / 10;
145
+ }
146
+ }
147
+
148
+ function buildDashboardTurnPreambleUnmeasured(
149
+ ctx: DashboardTurnPreambleContext,
150
+ ): LLMMessage[] {
151
+ const builderMs: BuilderMs | null = debug.enabled ? {} : null;
152
+ const startedAt = builderMs === null ? 0 : performance.now();
91
153
  // Universal preamble — surface-agnostic anchor (AGENTS.md +
92
154
  // CLAUDE.md). Spread first so the project context lands BEFORE any
93
155
  // surface-specific guidance, mirroring Claude Code's Phase 0
94
156
  // ordering. Returns [] when neither anchor file exists, so the
95
157
  // spread is a no-op outside actual project repos (tests etc.).
96
- const universalPreamble = buildUniversalPreamble({
158
+ const universalPreamble = timed(builderMs, 'universal', () => buildUniversalPreamble({
97
159
  cwd: ctx.cwd,
98
160
  ...(ctx.modelFamily !== undefined ? { modelFamily: ctx.modelFamily } : {}),
99
161
  ...(ctx.enabledTools !== undefined ? { enabledTools: ctx.enabledTools } : {}),
100
- });
162
+ }));
101
163
  // 🪪 L1 — 자리 부트 카드(세션마다 한 번 만든 스냅샷). 정체성(앵커) 바로 뒤. 자리가 아니면 [].
102
- const seatBootSystemMsgs = ctx.seatBoot
103
- ? buildSeatBootSystemMessages(
164
+ const seatBoot = ctx.seatBoot;
165
+ const seatBootSystemMsgs = seatBoot
166
+ ? timed(builderMs, 'seatBoot', () => buildSeatBootSystemMessages(
104
167
  { cwd: ctx.cwd, ...(ctx.sessionId !== undefined ? { sessionId: ctx.sessionId } : {}) },
105
- ctx.seatBoot === true ? {} : ctx.seatBoot,
106
- )
168
+ seatBoot === true ? {} : seatBoot,
169
+ ))
107
170
  : [];
108
- const planModeSystemMsgs = buildPlanModeSystemMessages();
109
- const askSystemMsgs = buildExecutionAskSystemMessages() as LLMMessage[];
110
- const approvalSystemMsgs = buildApprovalPolicySystemMessages() as LLMMessage[];
111
- const sandboxSystemMsgs = buildSandboxEscalationSystemMessages() as LLMMessage[];
171
+ const planModeSystemMsgs = timed(builderMs, 'planMode', () => buildPlanModeSystemMessages());
172
+ const askSystemMsgs = timed(builderMs, 'ask', () => buildExecutionAskSystemMessages() as LLMMessage[]);
173
+ const approvalSystemMsgs = timed(builderMs, 'approval', () => buildApprovalPolicySystemMessages() as LLMMessage[]);
174
+ const sandboxSystemMsgs = timed(builderMs, 'sandbox', () => buildSandboxEscalationSystemMessages() as LLMMessage[]);
112
175
  // Session-scoped (built once, rebuilt on change) — see turn-session-layers.ts.
113
176
  const sessionKey = ctx.sessionId ?? 'tui';
114
- const memorySystemMsgs = ctx.memory === false
177
+ const memory = ctx.memory;
178
+ const memorySystemMsgs = memory === false
115
179
  ? []
116
- : sessionMemoryLayer({ sessionKey, cwd: ctx.cwd, ...(ctx.memory ?? {}) });
117
- const gitSnapshotSystemMsgs = buildSessionRuntimeSystemMessages({
180
+ : timed(builderMs, 'memory', () => sessionMemoryLayer({ sessionKey, cwd: ctx.cwd, ...(memory ?? {}) }));
181
+ // TUI-TYPING-LAG — the snapshot is built twice per turn (dashboard ⊕ ACP) with 5+ git
182
+ // subprocesses each; a recent one is served and rebuilt off the event loop instead.
183
+ const gitSnapshotSystemMsgs = timed(builderMs, 'gitSnapshot', () => buildSessionRuntimeSystemMessages({
118
184
  cwd: ctx.cwd,
119
- });
120
- const surfaceSystemMsgs = ctx.turnProfile
121
- ? buildSessionRuntimeTurnSystemMessages(ctx.turnProfile, ctx.userText)
185
+ gitSnapshotStaleMs: DASHBOARD_GIT_SNAPSHOT_STALE_MS,
186
+ }));
187
+ const turnProfile = ctx.turnProfile;
188
+ const surfaceSystemMsgs = turnProfile
189
+ ? timed(builderMs, 'surface', () => buildSessionRuntimeTurnSystemMessages(turnProfile, ctx.userText))
122
190
  : ([] as LLMMessage[]);
123
191
  // ⭐⭐ `tools.nativeStructure` 는 «어느 provider 에 켤지»를 목록으로 고를 수 있다(2026-08-18).
124
192
  // ⛔ 목록이 없으면 종전대로 «전 provider» — 기존 설정을 쓰는 사람의 값이 안 바뀐다.
@@ -134,43 +202,46 @@ export function buildDashboardTurnPreamble(
134
202
  && isNativeStructureEnabledForProvider(nativeStructureConfig, resolveActiveProvider(ctx.userConfig));
135
203
  let nativeTools: NativeToolCatalogEntry[] | undefined;
136
204
  if (nativeStructureEnabled) {
137
- // eslint-disable-next-line @typescript-eslint/no-var-requires
138
- const { findNativeTool } = require('../native-tool-catalog.js') as typeof import('../native-tool-catalog.js');
139
- nativeTools = [...new Map((ctx.enabledTools ?? [])
140
- .map(findNativeTool)
141
- .filter((tool): tool is NativeToolCatalogEntry => tool !== undefined)
142
- .map(tool => [tool.id, tool])).values()];
205
+ nativeTools = timed(builderMs, 'nativeTools', () => {
206
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
207
+ const { findNativeTool } = require('../native-tool-catalog.js') as typeof import('../native-tool-catalog.js');
208
+ return [...new Map((ctx.enabledTools ?? [])
209
+ .map(findNativeTool)
210
+ .filter((tool): tool is NativeToolCatalogEntry => tool !== undefined)
211
+ .map(tool => [tool.id, tool])).values()];
212
+ });
143
213
  }
144
- const implDisciplineSystemMsgs = buildImplDisciplineSystemMessages({
214
+ const implDisciplineSystemMsgs = timed(builderMs, 'implDiscipline', () => buildImplDisciplineSystemMessages({
145
215
  text: ctx.userText,
146
216
  }, {
147
217
  nativeStructureEnabled,
148
218
  ...(nativeTools !== undefined ? { enabledTools: nativeTools } : {}),
149
- }) as LLMMessage[];
150
- const concisenessSystemMsgs = buildConcisenessSystemMessages(
219
+ }) as LLMMessage[]);
220
+ const concisenessSystemMsgs = timed(builderMs, 'conciseness', () => buildConcisenessSystemMessages(
151
221
  ctx.userConfig.chat.conciseness,
152
- ) as LLMMessage[];
222
+ ) as LLMMessage[]);
153
223
  // ★ elanous 자기접근 규율 + 자기인지 ambient(P3 · 2026-07-13) — 텔레그램/디스코드
154
224
  // (makeElanousAgentRunTurn)와 단일 출처(agent/self-ambient.ts). TUI 채팅에서도 미션
155
225
  // 진단("P2 왜 실패?")·자율 시스템 관측·회상이 규율+ambient+툴(self-ops family) 3박자로
156
226
  // 성립한다. fail-soft: ambient 조회 실패가 턴을 막지 않는다.
157
227
  let selfOpsSystemMsgs: LLMMessage[] = [];
158
228
  try {
159
- selfOpsSystemMsgs = [
229
+ selfOpsSystemMsgs = timed(builderMs, 'selfOps', () => [
160
230
  elanousSelfAccessPrompt(ctx.sessionId),
161
231
  ...sessionSelfAmbientParts(sessionKey),
162
232
  investigationRecallAmbient(ctx.userText),
163
233
  localRefGroundingAmbient(ctx.userText),
164
234
  ]
165
235
  .filter(Boolean)
166
- .map((content): LLMMessage => ({ role: 'system', content }));
236
+ .map((content): LLMMessage => ({ role: 'system', content })));
167
237
  } catch { /* fail-soft */ }
168
238
  const controlEnv = ctx.controlEnv ?? {
169
239
  controller: process.env.ELANOUS_CONTROLLER,
170
240
  channels: process.env.ELANOUS_CONTROL_CHANNELS,
171
241
  };
172
- const controllerSystemMsgs = controlEnv.controller
173
- ? [buildControllerSystemMessage(controlEnv.controller, controlEnv.channels)]
242
+ const controller = controlEnv.controller;
243
+ const controllerSystemMsgs = controller
244
+ ? timed(builderMs, 'controller', () => [buildControllerSystemMessage(controller, controlEnv.channels)])
174
245
  : [];
175
246
  const preamble = [
176
247
  ...universalPreamble,
@@ -196,24 +267,25 @@ export function buildDashboardTurnPreamble(
196
267
  // or (c) the model is simply ignoring what we sent.
197
268
  let taskNotificationCount = 0;
198
269
  try {
199
- const pendingNotifications = globalTaskNotificationQueue.drain();
200
- taskNotificationCount = pendingNotifications.length;
201
- const taskNotificationsXml = renderTaskNotificationsXml(pendingNotifications);
202
- if (taskNotificationsXml) {
203
- preamble.push({ role: 'user', content: taskNotificationsXml });
204
- }
270
+ timed(builderMs, 'taskNotifications', () => {
271
+ const pendingNotifications = globalTaskNotificationQueue.drain();
272
+ taskNotificationCount = pendingNotifications.length;
273
+ const taskNotificationsXml = renderTaskNotificationsXml(pendingNotifications);
274
+ if (taskNotificationsXml) {
275
+ preamble.push({ role: 'user', content: taskNotificationsXml });
276
+ }
277
+ });
205
278
  } catch {
206
279
  // Fail-soft: a task-notification delivery failure must not block the turn.
207
280
  }
208
281
 
209
- if (debug.enabled) {
282
+ if (debug.enabled && builderMs !== null) {
210
283
  const total = preamble.reduce((sum, m) => {
211
284
  const c = m.content;
212
285
  return sum + (typeof c === 'string' ? c.length : JSON.stringify(c).length);
213
286
  }, 0);
214
287
  debug.log('chat.turn-preamble', 'built', {
215
288
  hasTurnProfile: ctx.turnProfile != null,
216
- rich: ctx.rich === true,
217
289
  builderCounts: {
218
290
  universal: universalPreamble.length,
219
291
  seatBoot: seatBootSystemMsgs.length,
@@ -229,6 +301,10 @@ export function buildDashboardTurnPreamble(
229
301
  implDiscipline: implDisciplineSystemMsgs.length,
230
302
  conciseness: concisenessSystemMsgs.length,
231
303
  },
304
+ // 실린 빌더만 키가 있다(seatBoot·memory·surface·nativeTools·controller 는 조건부로 안 불린다).
305
+ builderMs,
306
+ builderMsKind: 'sync',
307
+ builderTotalMs: Math.round((performance.now() - startedAt) * 10) / 10,
232
308
  totalMessageCount: preamble.length,
233
309
  taskNotificationCount,
234
310
  totalChars: total,
@@ -16,7 +16,7 @@ import { join } from 'node:path';
16
16
  import type { LLMMessage } from '../llm.js';
17
17
  import { debug } from '../debug/log.js';
18
18
  import { buildMemoryInjection, memoryRoot, type MemoryEntry } from '../memory.js';
19
- import { elanousSelfAmbientParts } from '../agent/self-ambient.js';
19
+ import { cachedSelfAmbientBaseParts } from '../agent/self-ambient-cache.js';
20
20
 
21
21
  /** Token budget for the TUI memory layer — the same default chat.ts gives buildMemoryInjection. */
22
22
  export const TUI_MEMORY_BUDGET_TOKENS = 1500;
@@ -157,7 +157,9 @@ export function sessionSelfAmbientParts(sessionKey: string, opts: SessionSelfAmb
157
157
  return cached.parts;
158
158
  }
159
159
  try {
160
- const parts = (opts.build ?? (() => elanousSelfAmbientParts()))();
160
+ // TUI-TYPING-LAG — the stores are scanned by a worker (self-ambient-cache.ts); expiry here must
161
+ // not bring a synchronous `ps`/crontab/sqlite scan back onto the TUI turn path.
162
+ const parts = (opts.build ?? (() => cachedSelfAmbientBaseParts()))();
161
163
  const hash = contentHash(parts.join('\0'));
162
164
  if (cached && cached.hash === hash) {
163
165
  remember(ambientCache, sessionKey, { ...cached, at: now });
@@ -0,0 +1,222 @@
1
+ // TURN-START-BLOCK-REST 2단계 — 턴 시작 덩어리 관측(`tui.turn-start segments`).
2
+ //
3
+ // 1단계(#28821)가 turn-preamble 빌더 전체 ≤10ms 인데 같은 창의 `tui.input-lag turn-start` loopDelayMs.max 는
4
+ // 21~112ms 임을 보였다 — 막는 것은 preamble «밖»이다. 이 모듈은 턴 시작(대시보드 턴 본문 첫 콜백)에 트레이스를
5
+ // 열고, 실제 호출 순서를 따라 심은 경계(`markTurnStartSegment`)마다 시계를 «한 번» 읽어 앞 경계와의 ms 를
6
+ // 덩어리로 쌓는다. 첫 스트림 요청 경계(`endTurnStartSegments`)에서 닫고, 박동 두 번 간격 뒤 같은 창의 루프
7
+ // 지연과 함께 debug.log('tui.turn-start', 'segments', …) 한 줄을 남긴다. 기존 `tui.input-lag` 줄은 창·칸 그대로다.
8
+ //
9
+ // - 턴 키: 경계는 트레이스를 연 «그 턴»의 키(ACP sessionId)를 같이 준다. 키가 다른 경계(다른 세션의 턴 ·
10
+ // 서브 에이전트)는 문자열 비교 한 번으로 버린다 — 시계를 안 읽고 덩어리도 안 섞는다. 한 번에 열린 트레이스는
11
+ // 하나(TUI 는 한 번에 한 턴)이고, 다른 키로 새로 열면 앞 것은 `superseded` 로 닫는다.
12
+ // - 박동: 덩어리 안에서 박동이 돌았으면(`yielded`) 루프가 돌았다 = 순수 동기 아님. 박동 없이 이어진 덩어리 합의
13
+ // 최대(`syncBlockMaxMs`)가 박동 하나가 본 막힘의 추정이다(오차 ≤ 박동 간격). 박동은 input-lag 프로브의 것을
14
+ // 읽는다(`InputLagBeatView` — 새 타이머 0).
15
+ // - debug 가 꺼진 턴은 트레이스를 안 연다 — 그 턴의 경계는 null 검사 한 번이고 시계를 안 읽는다.
16
+ // - 줄을 내는 시점은 기존 turn-start 와 같은 «박동 두 번 간격» 타이머다(두 줄의 창이 같은 규칙으로 닫힌다).
17
+ // 실제 박동 두 번을 센 뒤 내보내는 전환은 TURN-START-BLOCK-REST 3단계 앞 조각.
18
+ // - 결선(대시보드·ACP·코어 턴·LLM 경계 한 줄씩)은 후속 PR — 이 모듈은 등록이 없으면 아무 일도 안 한다.
19
+
20
+ import type { InputLagBeatView, LoopDelayMs } from '../tui/input-lag-probe.js';
21
+
22
+ export const TURN_START_CATEGORY = 'tui.turn-start';
23
+ export const TURN_START_SEGMENTS_EVENT = 'segments';
24
+ /** 첫 스트림 요청 경계가 끝내 안 오면(원격 데몬 세션 · 중단 · 실패) 이만큼 뒤 쌓인 덩어리로 닫는다. */
25
+ export const TURN_START_TRACE_CAP_MS = 15_000;
26
+
27
+ export interface TurnStartSegments {
28
+ /** 덩어리 이름 → ms(0.1ms 반올림 · 앞 경계부터 이 경계까지의 벽시계). 같은 이름이 또 오면 `#2`… */
29
+ segmentsMs: Record<string, number>;
30
+ /** 경계가 온 순서. */
31
+ order: string[];
32
+ /** 그 덩어리 안에서 박동이 돌았다 — 루프가 돌았으니(await·I/O) 벽시계 ms 가 곧 막힘은 아니다. */
33
+ yielded: string[];
34
+ /** 덩어리 합(공개된 segmentsMs 값의 합 · 0.1ms). */
35
+ totalMs: number;
36
+ /** 트레이스 시작 → 닫힘. stream-open 이면 totalMs 와 같고, cap·superseded 면 마지막 경계 뒤 기다린 시간까지 든다
37
+ * (debug 가 꺼진 새 턴이 닫으면 시계를 안 읽으므로 마지막 경계까지). */
38
+ closedAfterMs: number;
39
+ /** 트레이스를 열 때 마지막 박동부터 흐른 ms — 열기 «전» 동기 일이 이미 루프를 막고 있었으면 크다(평상시 ≤ 박동 간격). */
40
+ preTraceMs: number;
41
+ /** 박동 없이 이어진 덩어리 합의 최대 — 루프를 한 번에 막은 동기 덩어리의 추정(박동이 돈 덩어리는 마지막 박동 뒤
42
+ * 꼬리만 · 첫 덩어리까지 박동이 안 돌았으면 `preTrace` 를 머리로 포함). */
43
+ syncBlockMaxMs: number;
44
+ /** 그 최대 동기 덩어리를 이룬 덩어리 이름(순서대로). ⚠️ 첫 이름이 `yielded` 에 든 덩어리면 그 덩어리는 «마지막 박동 뒤
45
+ * 꼬리만» syncBlockMaxMs 에 들었다 — 그 덩어리 segmentsMs 전체가 동기 블록이라는 뜻이 아니다. */
46
+ syncBlockSegments: string[];
47
+ /** 트레이스 시작 → 닫힘 ⊕ 박동 두 번 간격 창의 루프 지연. */
48
+ loopDelayMs: LoopDelayMs;
49
+ endedBy: 'stream-open' | 'cap' | 'superseded';
50
+ /** 트레이스 키(ACP sessionId). */
51
+ key: string;
52
+ turnId?: string;
53
+ busy: boolean;
54
+ }
55
+
56
+ interface TimerLike { unref?: () => void }
57
+
58
+ export interface TurnStartSegmentsOptions {
59
+ beats: InputLagBeatView;
60
+ log: (category: string, event: string, data: TurnStartSegments) => void;
61
+ isBusy: () => boolean;
62
+ /** 트레이스를 열지 — 대시보드는 `debug.enabled` 를 준다. 없으면 연다. */
63
+ isEnabled?: () => boolean;
64
+ now?: () => number;
65
+ setTimeoutFn?: (fn: () => void, ms: number) => TimerLike;
66
+ clearTimeoutFn?: (timer: TimerLike) => void;
67
+ }
68
+
69
+ export interface TurnStartSegmentsRecorder {
70
+ /** 키 `key` 의 턴 시작 트레이스를 연다(열린 것이 있으면 `superseded` 로 닫고). */
71
+ begin(key: string, turnId?: string): void;
72
+ /** 키가 같은 열린 트레이스에 덩어리 경계 하나 — 시계 한 번. 아니면 null 검사·비교뿐. */
73
+ mark(key: string, name: string): void;
74
+ /** 첫 스트림 요청 경계 — 마지막 덩어리를 찍고 닫는다(줄은 박동 두 번 간격 뒤). */
75
+ end(key: string, name: string): void;
76
+ dispose(): void;
77
+ }
78
+
79
+ const round1 = (ms: number): number => Math.round(Math.max(0, ms) * 10) / 10;
80
+
81
+ interface Trace {
82
+ key: string;
83
+ turnId: string | undefined;
84
+ startAt: number;
85
+ lastAt: number;
86
+ preTraceMs: number;
87
+ /** 공개 덩어리(반올림 값)의 누적 합 — totalMs 가 segmentsMs 의 합과 어긋나지 않게. */
88
+ sumMs: number;
89
+ segmentsMs: Record<string, number>;
90
+ order: string[];
91
+ yielded: string[];
92
+ blockMs: number;
93
+ blockSegments: string[];
94
+ maxBlockMs: number;
95
+ maxBlockSegments: string[];
96
+ capTimer: TimerLike | null;
97
+ }
98
+
99
+ export function createTurnStartSegments(opts: TurnStartSegmentsOptions): TurnStartSegmentsRecorder {
100
+ const now = opts.now ?? (() => performance.now());
101
+ const isEnabled = opts.isEnabled ?? (() => true);
102
+ const setTimeoutFn = opts.setTimeoutFn ?? ((fn, ms) => setTimeout(fn, ms));
103
+ const clearTimeoutFn = opts.clearTimeoutFn ?? ((t) => clearTimeout(t as ReturnType<typeof setTimeout>));
104
+ const beats = opts.beats;
105
+ let trace: Trace | null = null;
106
+ let disposed = false;
107
+
108
+ const markAt = (tr: Trace, name: string): void => {
109
+ const t = now();
110
+ let key = name;
111
+ for (let n = 2; key in tr.segmentsMs; n++) key = `${name}#${n}`;
112
+ const ms = round1(t - tr.lastAt);
113
+ tr.segmentsMs[key] = ms;
114
+ tr.sumMs += ms;
115
+ tr.order.push(key);
116
+ // 앞 경계 뒤에 박동이 돌았으면 루프가 돌았다 — 동기 덩어리는 마지막 박동 «뒤» 꼬리부터 다시 센다.
117
+ const lastBeatAt = beats.lastBeatAt();
118
+ if (lastBeatAt > tr.lastAt) {
119
+ tr.yielded.push(key);
120
+ tr.blockMs = t - lastBeatAt;
121
+ tr.blockSegments = [key];
122
+ } else {
123
+ tr.blockMs += t - tr.lastAt;
124
+ tr.blockSegments.push(key);
125
+ }
126
+ if (tr.blockMs >= tr.maxBlockMs) {
127
+ tr.maxBlockMs = tr.blockMs;
128
+ tr.maxBlockSegments = tr.blockSegments.slice();
129
+ }
130
+ tr.lastAt = t;
131
+ };
132
+
133
+ const close = (tr: Trace, endedBy: TurnStartSegments['endedBy'], closedAt: number): void => {
134
+ if (trace === tr) trace = null;
135
+ // 닫힌 트레이스를 상한 타이머가 붙잡지 않게 푼다.
136
+ if (tr.capTimer !== null) { clearTimeoutFn(tr.capTimer); tr.capTimer = null; }
137
+ const emit = (): void => {
138
+ if (disposed) return;
139
+ opts.log(TURN_START_CATEGORY, TURN_START_SEGMENTS_EVENT, {
140
+ segmentsMs: tr.segmentsMs,
141
+ order: tr.order,
142
+ yielded: tr.yielded,
143
+ totalMs: Math.round(tr.sumMs * 10) / 10,
144
+ closedAfterMs: round1(closedAt - tr.startAt),
145
+ preTraceMs: round1(tr.preTraceMs),
146
+ syncBlockMaxMs: round1(tr.maxBlockMs),
147
+ syncBlockSegments: tr.maxBlockSegments,
148
+ loopDelayMs: beats.loopDelaySince(tr.startAt),
149
+ endedBy,
150
+ key: tr.key,
151
+ ...(tr.turnId !== undefined ? { turnId: tr.turnId } : {}),
152
+ busy: opts.isBusy(),
153
+ });
154
+ };
155
+ // 방금 끝난 동기 막힘은 루프가 풀린 «뒤» 박동에 찍힌다 — 첫 스트림 요청이면 박동 두 번 간격 기다렸다 읽는다.
156
+ if (endedBy === 'stream-open') setTimeoutFn(emit, beats.heartbeatMs * 2).unref?.();
157
+ else emit();
158
+ };
159
+
160
+ return {
161
+ begin(key: string, turnId?: string): void {
162
+ if (disposed) return;
163
+ const open = trace;
164
+ if (!isEnabled()) {
165
+ // debug 가 꺼진 턴은 시계를 안 읽는다 — 열려 있던 트레이스는 마지막 경계 시각으로 닫는다.
166
+ if (open !== null) close(open, 'superseded', open.lastAt);
167
+ return;
168
+ }
169
+ const t = now();
170
+ if (open !== null) close(open, 'superseded', t);
171
+ // 열기 전 마지막 박동 뒤 흐른 시간 — 열기 전 동기 일이 같은 막힘의 머리다(시계는 이 한 번).
172
+ const preTraceMs = Math.max(0, t - beats.lastBeatAt());
173
+ const tr: Trace = {
174
+ key, turnId, startAt: t, lastAt: t, preTraceMs, sumMs: 0,
175
+ segmentsMs: {}, order: [], yielded: [],
176
+ blockMs: preTraceMs, blockSegments: ['preTrace'], maxBlockMs: preTraceMs, maxBlockSegments: ['preTrace'],
177
+ capTimer: null,
178
+ };
179
+ trace = tr;
180
+ tr.capTimer = setTimeoutFn(() => { tr.capTimer = null; if (trace === tr) close(tr, 'cap', now()); }, TURN_START_TRACE_CAP_MS);
181
+ tr.capTimer.unref?.();
182
+ },
183
+ mark(key: string, name: string): void {
184
+ const tr = trace;
185
+ if (tr !== null && tr.key === key) markAt(tr, name);
186
+ },
187
+ end(key: string, name: string): void {
188
+ const tr = trace;
189
+ if (tr === null || tr.key !== key) return;
190
+ markAt(tr, name);
191
+ close(tr, 'stream-open', tr.lastAt);
192
+ },
193
+ dispose(): void {
194
+ if (disposed) return;
195
+ disposed = true;
196
+ if (trace !== null && trace.capTimer !== null) { clearTimeoutFn(trace.capTimer); trace.capTimer = null; }
197
+ trace = null;
198
+ },
199
+ };
200
+ }
201
+
202
+ // ── 등록 자리 — 대시보드가 하나 등록하고, 경계(대시보드·ACP·코어 턴·LLM)는 부르기만 한다. ──
203
+ let active: TurnStartSegmentsRecorder | null = null;
204
+
205
+ export function setTurnStartSegments(recorder: TurnStartSegmentsRecorder | null): void {
206
+ active = recorder;
207
+ }
208
+
209
+ /** 키 `key`(ACP sessionId) 의 턴 시작 트레이스를 연다 — 대시보드 턴 본문의 첫 콜백. */
210
+ export function beginTurnStartSegments(key: string, turnId?: string): void {
211
+ active?.begin(key, turnId);
212
+ }
213
+
214
+ /** 턴 시작 덩어리 경계 — 등록·트레이스가 없거나 키가 다르면 null 검사·비교뿐(시계를 안 읽는다). */
215
+ export function markTurnStartSegment(key: string | undefined, name: string): void {
216
+ if (key !== undefined) active?.mark(key, name);
217
+ }
218
+
219
+ /** 첫 스트림 요청 경계 — 같은 키의 트레이스를 닫는다. */
220
+ export function endTurnStartSegments(key: string | undefined, name: string): void {
221
+ if (key !== undefined) active?.end(key, name);
222
+ }
@@ -0,0 +1,146 @@
1
+ /** 대기 중 프롬프트 띠의 «주인 하나» (TUI-PROMPT-SINGLE-OWNER · 2026-10-17).
2
+ *
3
+ * 종전엔 띠를 둘이 그렸다 — frame 렌더러 zone 은 `❯ ` 를, textInput `drawInput` 은
4
+ * 실제 버퍼를 직접 stdout 에 썼다. 직접 쓰기가 행 캐시를 무효화하므로 대기 중 매 draw
5
+ * (2Hz 상태줄 틱 포함)가 «zone 이 `❯ ` 로 지움 → afterRender 덧칠이 버퍼로 다시 씀»을
6
+ * 되풀이했고, 프롬프트·구분선·상태줄이 매 프레임 `\x1b[2K` 로 다시 나갔다.
7
+ *
8
+ * ⭐ 이제 textInput 은 그릴 줄을 여기 **올리기만** 하고(publish), 실제 쓰기는 frame
9
+ * 렌더러 zone 하나가 한다. 행 캐시 diff 가 바뀐 줄만 내보내므로 대기 중 재출력은 0 이다. */
10
+ import { debug } from '../debug/log.js';
11
+ import type { LayoutZone } from '../layout/composer.js';
12
+ import type { CursorState } from './cursor-state.js';
13
+
14
+ export interface PromptRowPaint {
15
+ /** 위→아래 순서의 보이는 프롬프트 줄(접두·패딩 포함 · 행 지움 없음). */
16
+ rows: string[];
17
+ /** 프롬프트 아래 둘째 줄의 일시 힌트(Ctrl+C 등). 없으면 null. */
18
+ hint: string | null;
19
+ caret: CursorState | null;
20
+ source: 'text-input' | 'reverse-search';
21
+ }
22
+
23
+ export interface PromptRowOwner {
24
+ /** 같은 그림이면 false(프레임 요청 없음) — 대기 중 외부 repaint 가 no-op 이 되는 자리. */
25
+ publish(paint: PromptRowPaint): boolean;
26
+ clear(): void;
27
+ current(): PromptRowPaint | null;
28
+ }
29
+
30
+ export type PromptRowOwnerLog = (category: string, event: string, data: Record<string, unknown>) => void;
31
+
32
+ const CATEGORY = 'dashboard.prompt-owner';
33
+
34
+ const paintKey = (paint: PromptRowPaint): string => JSON.stringify(paint);
35
+
36
+ export function createPromptRowOwner(deps: {
37
+ requestFrame: () => void;
38
+ isDebugEnabled?: () => boolean;
39
+ log?: PromptRowOwnerLog;
40
+ }): PromptRowOwner {
41
+ let paint: PromptRowPaint | null = null;
42
+ let key = '';
43
+ const enabled = (): boolean => (deps.isDebugEnabled ? deps.isDebugEnabled() : debug.enabled);
44
+ const log: PromptRowOwnerLog = deps.log ?? ((c, e, d) => debug.log(c, e, d));
45
+ return {
46
+ publish(next) {
47
+ const nextKey = paintKey(next);
48
+ if (nextKey === key) return false;
49
+ paint = next;
50
+ key = nextKey;
51
+ if (enabled()) {
52
+ log(CATEGORY, 'publish', {
53
+ source: next.source,
54
+ rows: next.rows.length,
55
+ hint: next.hint !== null,
56
+ caretRow: next.caret?.row ?? null,
57
+ caretCol: next.caret?.col ?? null,
58
+ });
59
+ }
60
+ deps.requestFrame();
61
+ return true;
62
+ },
63
+ clear() {
64
+ if (!paint) return;
65
+ paint = null;
66
+ key = '';
67
+ if (enabled()) log(CATEGORY, 'clear', {});
68
+ deps.requestFrame();
69
+ },
70
+ current: () => paint,
71
+ };
72
+ }
73
+
74
+ /** 바닥 기준으로 `height` 줄에 맞춘다 — 입력 줄이 늘면 위로 자란다(textInput 과 같은 앵커). */
75
+ export function renderPromptOwnerRows(paint: PromptRowPaint, height: number): string[] {
76
+ // ⚠️ `slice(-0)` is the whole array — a zero-height zone must paint nothing.
77
+ if (height <= 0) return [];
78
+ const visible = paint.rows.slice(-height);
79
+ return [...Array.from({ length: height - visible.length }, () => ''), ...visible];
80
+ }
81
+
82
+ export type PromptBandOwner = 'text-input' | 'turn-typeahead' | 'suppressed';
83
+
84
+ /** 누가 띠를 그렸나 — 주인·그림(줄·힌트·caret)이 바뀔 때 한 줄, 같은 상태(대기)는 10초에 한 줄로 샘플링. */
85
+ export function createPromptBandRenderObserver(deps: {
86
+ isDebugEnabled?: () => boolean;
87
+ log?: PromptRowOwnerLog;
88
+ now?: () => number;
89
+ sampleMs?: number;
90
+ } = {}): (owner: PromptBandOwner, frameKey: string) => void {
91
+ const now = deps.now ?? Date.now;
92
+ const sampleMs = deps.sampleMs ?? 10_000;
93
+ let lastOwner: PromptBandOwner | null = null;
94
+ let lastFrameKey = '';
95
+ let lastLoggedAt = 0;
96
+ let renders = 0;
97
+ return (owner, frameKey) => {
98
+ renders++;
99
+ const enabled = deps.isDebugEnabled ? deps.isDebugEnabled() : debug.enabled;
100
+ if (!enabled) return;
101
+ const changed = owner !== lastOwner || frameKey !== lastFrameKey;
102
+ const at = now();
103
+ if (!changed && at - lastLoggedAt < sampleMs) return;
104
+ (deps.log ?? ((c, e, d) => debug.log(c, e, d)))(CATEGORY, 'frame-render', {
105
+ owner,
106
+ changed,
107
+ rendersSinceLast: renders,
108
+ });
109
+ lastOwner = owner;
110
+ lastFrameKey = frameKey;
111
+ lastLoggedAt = at;
112
+ renders = 0;
113
+ };
114
+ }
115
+
116
+ /** 프롬프트 zone 을 주인 하나로 감싼다 — textInput 이 그림을 올렸으면 그것을, 아니면(턴 중)
117
+ * `inner`(turn-typeahead 에코·placeholder)를 그린다. caret 도 같은 그림에서 나온다. */
118
+ export function withPromptRowOwner(inner: LayoutZone, owner: PromptRowOwner, opts: {
119
+ suppressPromptArea: boolean;
120
+ onCaret?: (caret: CursorState | null) => void;
121
+ /** `frameKey` covers everything the band paints — rows, hint and caret. */
122
+ observe?: (owner: PromptBandOwner, frameKey: string) => void;
123
+ }): LayoutZone {
124
+ return {
125
+ ...inner,
126
+ render: (height, width) => {
127
+ const paint = opts.suppressPromptArea ? null : owner.current();
128
+ if (!paint) {
129
+ const rows = inner.render(height, width);
130
+ opts.observe?.(opts.suppressPromptArea ? 'suppressed' : 'turn-typeahead', rows.join('\n'));
131
+ return rows;
132
+ }
133
+ const rows = renderPromptOwnerRows(paint, height);
134
+ opts.onCaret?.(paint.caret);
135
+ opts.observe?.('text-input', JSON.stringify({ rows, hint: paint.hint, caret: paint.caret }));
136
+ return rows;
137
+ },
138
+ };
139
+ }
140
+
141
+ /** 프롬프트 아래 둘째 줄(status-gap-top) — textInput 힌트가 있으면 그것, 아니면 빈 줄.
142
+ * 프롬프트가 억제된 프레임(전면 모달 등)에는 힌트도 내지 않는다. */
143
+ export function renderPromptOwnerHintRow(owner: PromptRowOwner, opts: { suppressPromptArea: boolean }): string {
144
+ if (opts.suppressPromptArea) return '';
145
+ return owner.current()?.hint ?? '';
146
+ }