@adhdev/daemon-core 0.9.82-rc.44 → 0.9.82-rc.440

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 (425) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +9 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/build-info.d.ts +37 -0
  4. package/dist/chat/source-machine.d.ts +166 -0
  5. package/dist/chat/source-resolver.d.ts +104 -0
  6. package/dist/chat/subscription-updates.d.ts +1 -0
  7. package/dist/cli-adapter-types.d.ts +68 -2
  8. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  9. package/dist/cli-adapters/cli-state-engine.d.ts +200 -0
  10. package/dist/cli-adapters/provider-cli-adapter.d.ts +126 -64
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +5 -0
  12. package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
  13. package/dist/cli-adapters/provider-cli-shared.d.ts +74 -0
  14. package/dist/cli-adapters/pty-write-chunking.d.ts +34 -0
  15. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  16. package/dist/cli-adapters/resolve-executable.d.ts +69 -0
  17. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  18. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  19. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  20. package/dist/commands/chat-commands-debug-bundle.d.ts +14 -0
  21. package/dist/commands/chat-commands-read.d.ts +7 -0
  22. package/dist/commands/chat-commands-scope.d.ts +39 -0
  23. package/dist/commands/chat-commands-shared.d.ts +33 -0
  24. package/dist/commands/chat-commands-write.d.ts +14 -0
  25. package/dist/commands/chat-commands.d.ts +10 -23
  26. package/dist/commands/cli-manager.d.ts +15 -1
  27. package/dist/commands/handler.d.ts +125 -0
  28. package/dist/commands/high-family/index.d.ts +3 -0
  29. package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
  30. package/dist/commands/high-family/mesh-events.d.ts +2 -0
  31. package/dist/commands/high-family/mesh-status.d.ts +2 -0
  32. package/dist/commands/high-family/types.d.ts +60 -0
  33. package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
  34. package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
  35. package/dist/commands/low-family/diagnostics.d.ts +2 -0
  36. package/dist/commands/low-family/index.d.ts +3 -0
  37. package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
  38. package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
  39. package/dist/commands/low-family/notification.d.ts +2 -0
  40. package/dist/commands/low-family/refine-config.d.ts +2 -0
  41. package/dist/commands/low-family/session-host.d.ts +2 -0
  42. package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
  43. package/dist/commands/low-family/status-meta.d.ts +2 -0
  44. package/dist/commands/low-family/types.d.ts +33 -0
  45. package/dist/commands/med-family/cli-agent.d.ts +2 -0
  46. package/dist/commands/med-family/fast-forward.d.ts +2 -0
  47. package/dist/commands/med-family/ide.d.ts +10 -0
  48. package/dist/commands/med-family/index.d.ts +3 -0
  49. package/dist/commands/med-family/mesh-crud.d.ts +29 -0
  50. package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
  51. package/dist/commands/med-family/mesh-queue.d.ts +2 -0
  52. package/dist/commands/med-family/mesh-restart.d.ts +2 -0
  53. package/dist/commands/med-family/types.d.ts +142 -0
  54. package/dist/commands/mesh-coordinator.d.ts +85 -1
  55. package/dist/commands/router.d.ts +276 -2
  56. package/dist/commands/upgrade-helper.d.ts +41 -1
  57. package/dist/config/chat-history.d.ts +9 -0
  58. package/dist/config/config.d.ts +5 -0
  59. package/dist/config/mesh-config.d.ts +120 -1
  60. package/dist/config/mesh-json-config.d.ts +199 -0
  61. package/dist/config/repo-settings.d.ts +77 -0
  62. package/dist/daemon/dev-server.d.ts +0 -2
  63. package/dist/detection/ide-detector.d.ts +13 -0
  64. package/dist/detection/win32-ide-version.d.ts +37 -0
  65. package/dist/git/change-impact-config.d.ts +159 -0
  66. package/dist/git/git-commands.d.ts +11 -1
  67. package/dist/git/git-diff.d.ts +6 -0
  68. package/dist/git/git-executor.d.ts +11 -0
  69. package/dist/git/git-status.d.ts +57 -0
  70. package/dist/git/git-types.d.ts +2 -50
  71. package/dist/git/git-worktree.d.ts +71 -1
  72. package/dist/git/index.d.ts +3 -1
  73. package/dist/index.d.ts +64 -14
  74. package/dist/index.js +50679 -20122
  75. package/dist/index.js.map +1 -1
  76. package/dist/index.mjs +47546 -17153
  77. package/dist/index.mjs.map +1 -1
  78. package/dist/installer.d.ts +1 -4
  79. package/dist/ipc/local-ipc-server.d.ts +91 -0
  80. package/dist/launch.d.ts +1 -1
  81. package/dist/logging/async-batch-writer.d.ts +10 -0
  82. package/dist/logging/log-redactor.d.ts +24 -0
  83. package/dist/logging/log-tail-reader.d.ts +81 -0
  84. package/dist/logging/logger.d.ts +1 -1
  85. package/dist/mesh/contracts.d.ts +164 -0
  86. package/dist/mesh/coordinator-prompt.d.ts +89 -0
  87. package/dist/mesh/coordinator-registry.d.ts +59 -0
  88. package/dist/mesh/mesh-active-work.d.ts +174 -0
  89. package/dist/mesh/mesh-clone-grace.d.ts +8 -0
  90. package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
  91. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  92. package/dist/mesh/mesh-event-classify.d.ts +5 -0
  93. package/dist/mesh/mesh-event-forwarding.d.ts +41 -0
  94. package/dist/mesh/mesh-event-trace.d.ts +21 -0
  95. package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
  96. package/dist/mesh/mesh-events-pending.d.ts +69 -0
  97. package/dist/mesh/mesh-events-stale.d.ts +61 -0
  98. package/dist/mesh/mesh-events-utils.d.ts +75 -0
  99. package/dist/mesh/mesh-events.d.ts +6 -49
  100. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  101. package/dist/mesh/mesh-host-ownership.d.ts +29 -0
  102. package/dist/mesh/mesh-init.d.ts +128 -0
  103. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
  104. package/dist/mesh/mesh-ledger.d.ts +77 -1
  105. package/dist/mesh/mesh-magi-status.d.ts +63 -0
  106. package/dist/mesh/mesh-missions.d.ts +189 -0
  107. package/dist/mesh/mesh-node-identity.d.ts +289 -0
  108. package/dist/mesh/mesh-queue-assignment.d.ts +113 -0
  109. package/dist/mesh/mesh-reconcile-loop.d.ts +65 -0
  110. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  111. package/dist/mesh/mesh-refine-gates.d.ts +428 -0
  112. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  113. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  114. package/dist/mesh/mesh-routing.d.ts +70 -0
  115. package/dist/mesh/mesh-runtime-store.d.ts +450 -0
  116. package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
  117. package/dist/mesh/mesh-task-inflight.d.ts +46 -0
  118. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  119. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
  120. package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
  121. package/dist/mesh/mesh-work-queue.d.ts +300 -5
  122. package/dist/mesh/preview-freshness.d.ts +18 -0
  123. package/dist/mesh/refine-config.d.ts +216 -0
  124. package/dist/mesh/worktree-bootstrap-config.d.ts +145 -0
  125. package/dist/providers/acp-provider-instance.d.ts +5 -0
  126. package/dist/providers/approval-utils.d.ts +20 -0
  127. package/dist/providers/chat-message-normalization.d.ts +31 -1
  128. package/dist/providers/cli-provider-instance.d.ts +252 -3
  129. package/dist/providers/contracts.d.ts +149 -6
  130. package/dist/providers/external-sources.d.ts +71 -0
  131. package/dist/providers/manual-attendance.d.ts +63 -0
  132. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  133. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  134. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  135. package/dist/providers/native-history/constants.d.ts +12 -0
  136. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  137. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  138. package/dist/providers/native-history/index.d.ts +13 -0
  139. package/dist/providers/provider-instance-manager.d.ts +29 -0
  140. package/dist/providers/provider-instance.d.ts +23 -1
  141. package/dist/providers/provider-loader.d.ts +26 -4
  142. package/dist/providers/provider-trust.d.ts +31 -0
  143. package/dist/providers/read-chat-contract.d.ts +29 -0
  144. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  145. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
  146. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  147. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  148. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  149. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  150. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  151. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  152. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  153. package/dist/providers/sdk/v1/index.d.ts +30 -0
  154. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  155. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  156. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  157. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  158. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  159. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  160. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  161. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  162. package/dist/providers/spec/adapter.d.ts +91 -0
  163. package/dist/providers/spec/cli-adapter.d.ts +223 -0
  164. package/dist/providers/spec/evaluator.d.ts +45 -0
  165. package/dist/providers/spec/fsm-driver.d.ts +422 -0
  166. package/dist/providers/spec/fsm-evaluator.d.ts +76 -0
  167. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  168. package/dist/providers/spec/fsm-types.d.ts +174 -0
  169. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  170. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  171. package/dist/providers/spec/route.d.ts +4 -0
  172. package/dist/providers/spec/types.d.ts +222 -0
  173. package/dist/providers/status-monitor.d.ts +7 -7
  174. package/dist/providers/transcript-v2.d.ts +176 -0
  175. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  176. package/dist/providers/working-dir.d.ts +17 -0
  177. package/dist/repo-mesh-types.d.ts +509 -13
  178. package/dist/runtime-defaults.d.ts +2 -0
  179. package/dist/sessions/registry.d.ts +6 -0
  180. package/dist/shared-types-extra.d.ts +2 -4
  181. package/dist/shared-types.d.ts +59 -2
  182. package/dist/status/normalize.d.ts +1 -1
  183. package/dist/status/normalize.js +1 -0
  184. package/dist/status/normalize.js.map +1 -1
  185. package/dist/status/normalize.mjs +1 -0
  186. package/dist/status/normalize.mjs.map +1 -1
  187. package/dist/status/reporter.d.ts +2 -0
  188. package/dist/status/snapshot.d.ts +26 -0
  189. package/dist/system/hash.d.ts +8 -0
  190. package/dist/system/load-better-sqlite3.d.ts +21 -0
  191. package/dist/types.d.ts +5 -0
  192. package/package.json +7 -3
  193. package/src/agent-stream/poller.ts +2 -3
  194. package/src/agent-stream/provider-adapter.ts +1 -1
  195. package/src/boot/daemon-lifecycle.ts +63 -12
  196. package/src/boot/process-hardening.ts +89 -0
  197. package/src/build-info.ts +73 -0
  198. package/src/chat/source-machine.ts +534 -0
  199. package/src/chat/source-resolver.ts +0 -0
  200. package/src/chat/subscription-updates.ts +20 -1
  201. package/src/cli-adapter-types.d.ts +3 -1
  202. package/src/cli-adapter-types.ts +68 -2
  203. package/src/cli-adapters/cli-script-runner.ts +421 -0
  204. package/src/cli-adapters/cli-state-engine.ts +1246 -0
  205. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  206. package/src/cli-adapters/provider-cli-adapter.ts +914 -1144
  207. package/src/cli-adapters/provider-cli-parse.d.ts +2 -0
  208. package/src/cli-adapters/provider-cli-parse.ts +19 -0
  209. package/src/cli-adapters/provider-cli-runtime.ts +56 -7
  210. package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
  211. package/src/cli-adapters/provider-cli-shared.ts +121 -11
  212. package/src/cli-adapters/pty-transport.ts +2 -1
  213. package/src/cli-adapters/pty-write-chunking.ts +106 -0
  214. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  215. package/src/cli-adapters/resolve-executable.ts +204 -0
  216. package/src/cli-adapters/session-host-transport.ts +2 -1
  217. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +63 -29
  218. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  219. package/src/cli-adapters/terminal-screen.ts +16 -81
  220. package/src/commands/WINDOWS-UPGRADE-LOCK-FAILURE.md +198 -0
  221. package/src/commands/chat-commands-debug-bundle.ts +398 -0
  222. package/src/commands/chat-commands-read.ts +2327 -0
  223. package/src/commands/chat-commands-scope.ts +54 -0
  224. package/src/commands/chat-commands-shared.ts +114 -0
  225. package/src/commands/chat-commands-write.ts +891 -0
  226. package/src/commands/chat-commands.ts +19 -1841
  227. package/src/commands/cli-manager.ts +572 -27
  228. package/src/commands/handler.ts +841 -2
  229. package/src/commands/high-family/index.ts +28 -0
  230. package/src/commands/high-family/mesh-coordinator-launch.ts +678 -0
  231. package/src/commands/high-family/mesh-events.ts +80 -0
  232. package/src/commands/high-family/mesh-status.ts +714 -0
  233. package/src/commands/high-family/types.ts +76 -0
  234. package/src/commands/low-family/coordinator-prompt.ts +72 -0
  235. package/src/commands/low-family/daemon-lifecycle.ts +107 -0
  236. package/src/commands/low-family/diagnostics.ts +57 -0
  237. package/src/commands/low-family/index.ts +37 -0
  238. package/src/commands/low-family/mesh-ledger.ts +62 -0
  239. package/src/commands/low-family/mesh-node-logs.ts +87 -0
  240. package/src/commands/low-family/notification.ts +116 -0
  241. package/src/commands/low-family/refine-config.ts +106 -0
  242. package/src/commands/low-family/session-host.ts +274 -0
  243. package/src/commands/low-family/spec-providerdev.ts +217 -0
  244. package/src/commands/low-family/status-meta.ts +112 -0
  245. package/src/commands/low-family/types.ts +39 -0
  246. package/src/commands/med-family/cli-agent.ts +238 -0
  247. package/src/commands/med-family/fast-forward.ts +230 -0
  248. package/src/commands/med-family/ide.ts +163 -0
  249. package/src/commands/med-family/index.ts +37 -0
  250. package/src/commands/med-family/mesh-crud.ts +1242 -0
  251. package/src/commands/med-family/mesh-host-pairing.ts +234 -0
  252. package/src/commands/med-family/mesh-queue.ts +167 -0
  253. package/src/commands/med-family/mesh-restart.ts +92 -0
  254. package/src/commands/med-family/types.ts +143 -0
  255. package/src/commands/mesh-coordinator.ts +334 -124
  256. package/src/commands/router.ts +2846 -3484
  257. package/src/commands/stream-commands.ts +8 -0
  258. package/src/commands/upgrade-helper.ts +310 -45
  259. package/src/config/chat-history.ts +493 -24
  260. package/src/config/config.ts +12 -0
  261. package/src/config/mesh-config.ts +593 -22
  262. package/src/config/mesh-json-config.ts +376 -0
  263. package/src/config/recent-activity.ts +8 -2
  264. package/src/config/repo-settings.ts +111 -0
  265. package/src/daemon/dev-auto-implement.ts +3 -2
  266. package/src/daemon/dev-cli-debug.ts +10 -1
  267. package/src/daemon/dev-server.ts +0 -541
  268. package/src/detection/cli-detector.ts +28 -9
  269. package/src/detection/ide-detector.ts +55 -16
  270. package/src/detection/win32-ide-version.ts +106 -0
  271. package/src/git/change-impact-config.ts +354 -0
  272. package/src/git/git-commands.ts +59 -15
  273. package/src/git/git-diff.ts +81 -11
  274. package/src/git/git-executor.ts +12 -0
  275. package/src/git/git-status.ts +767 -48
  276. package/src/git/git-types.ts +14 -62
  277. package/src/git/git-worktree.ts +261 -4
  278. package/src/git/index.ts +17 -0
  279. package/src/index.ts +196 -12
  280. package/src/installer.d.ts +1 -1
  281. package/src/installer.ts +8 -6
  282. package/src/ipc/local-ipc-server.ts +278 -0
  283. package/src/launch.d.ts +1 -1
  284. package/src/launch.ts +37 -28
  285. package/src/logging/async-batch-writer.ts +55 -0
  286. package/src/logging/command-log.ts +7 -5
  287. package/src/logging/log-redactor.ts +100 -0
  288. package/src/logging/log-tail-reader.ts +341 -0
  289. package/src/logging/logger.ts +14 -7
  290. package/src/mesh/contracts.ts +338 -0
  291. package/src/mesh/coordinator-prompt.ts +415 -34
  292. package/src/mesh/coordinator-registry.ts +121 -0
  293. package/src/mesh/mesh-active-work.ts +645 -0
  294. package/src/mesh/mesh-clone-grace.ts +68 -0
  295. package/src/mesh/mesh-coordinator-config.ts +97 -0
  296. package/src/mesh/mesh-delivery-policy.ts +315 -0
  297. package/src/mesh/mesh-event-classify.ts +51 -0
  298. package/src/mesh/mesh-event-forwarding.ts +1897 -0
  299. package/src/mesh/mesh-event-trace.ts +67 -0
  300. package/src/mesh/mesh-events-coordinator.ts +32 -0
  301. package/src/mesh/mesh-events-pending.ts +656 -0
  302. package/src/mesh/mesh-events-stale.ts +389 -0
  303. package/src/mesh/mesh-events-utils.ts +443 -0
  304. package/src/mesh/mesh-events.ts +33 -1035
  305. package/src/mesh/mesh-fast-forward.ts +843 -0
  306. package/src/mesh/mesh-host-ownership.ts +111 -0
  307. package/src/mesh/mesh-init.ts +350 -0
  308. package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
  309. package/src/mesh/mesh-ledger.ts +656 -102
  310. package/src/mesh/mesh-magi-status.ts +223 -0
  311. package/src/mesh/mesh-missions.ts +449 -0
  312. package/src/mesh/mesh-node-identity.ts +1887 -0
  313. package/src/mesh/mesh-queue-assignment.ts +1924 -0
  314. package/src/mesh/mesh-reconcile-loop.ts +1997 -0
  315. package/src/mesh/mesh-refine-batch.ts +205 -0
  316. package/src/mesh/mesh-refine-gates.ts +1673 -0
  317. package/src/mesh/mesh-refine-status.ts +231 -0
  318. package/src/mesh/mesh-review-inbox.ts +307 -0
  319. package/src/mesh/mesh-routing.ts +291 -0
  320. package/src/mesh/mesh-runtime-store.ts +1853 -0
  321. package/src/mesh/mesh-scheduling-runtime.ts +198 -0
  322. package/src/mesh/mesh-task-inflight.ts +70 -0
  323. package/src/mesh/mesh-task-stats.ts +161 -0
  324. package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
  325. package/src/mesh/mesh-warmup-deadline.ts +152 -0
  326. package/src/mesh/mesh-work-queue.ts +1254 -141
  327. package/src/mesh/preview-freshness.ts +118 -0
  328. package/src/mesh/refine-config.ts +423 -0
  329. package/src/mesh/worktree-bootstrap-config.ts +450 -0
  330. package/src/providers/acp-provider-instance.ts +43 -10
  331. package/src/providers/approval-utils.d.ts +5 -0
  332. package/src/providers/approval-utils.ts +57 -5
  333. package/src/providers/chat-message-normalization.ts +92 -4
  334. package/src/providers/cli-provider-instance.ts +2001 -106
  335. package/src/providers/contracts.d.ts +55 -0
  336. package/src/providers/contracts.ts +160 -6
  337. package/src/providers/extension-provider-instance.ts +12 -7
  338. package/src/providers/external-sources.ts +218 -0
  339. package/src/providers/ide-provider-instance.ts +35 -12
  340. package/src/providers/manual-attendance.ts +85 -0
  341. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  342. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  343. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  344. package/src/providers/native-history/constants.ts +19 -0
  345. package/src/providers/native-history/dispatcher.ts +345 -0
  346. package/src/providers/native-history/hermes-cli-transcript.ts +260 -0
  347. package/src/providers/native-history/index.ts +30 -0
  348. package/src/providers/provider-instance-manager.ts +71 -0
  349. package/src/providers/provider-instance.ts +19 -1
  350. package/src/providers/provider-loader.ts +668 -50
  351. package/src/providers/provider-schema.ts +87 -14
  352. package/src/providers/provider-trust.ts +114 -0
  353. package/src/providers/read-chat-contract.ts +76 -16
  354. package/src/providers/sdk/README.md +49 -0
  355. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  356. package/src/providers/sdk/v1/builders/cli/detect-status.ts +313 -0
  357. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  358. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  359. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  360. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  361. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  362. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  363. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  364. package/src/providers/sdk/v1/index.ts +152 -0
  365. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  366. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  367. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  368. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +509 -0
  369. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  370. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  371. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  372. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  373. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  374. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  375. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  376. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  377. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  378. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  379. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  380. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  381. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  382. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  383. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  384. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  385. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  386. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  387. package/src/providers/sdk/v1/validators/index.ts +19 -0
  388. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  389. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  390. package/src/providers/spec/adapter.ts +235 -0
  391. package/src/providers/spec/cli-adapter.ts +1056 -0
  392. package/src/providers/spec/evaluator.ts +407 -0
  393. package/src/providers/spec/fsm-driver.ts +1410 -0
  394. package/src/providers/spec/fsm-evaluator.ts +272 -0
  395. package/src/providers/spec/fsm-loader.ts +120 -0
  396. package/src/providers/spec/fsm-types.ts +273 -0
  397. package/src/providers/spec/native-history-executor.ts +1143 -0
  398. package/src/providers/spec/pre-launch-trust.ts +104 -0
  399. package/src/providers/spec/route.ts +51 -0
  400. package/src/providers/spec/types.ts +262 -0
  401. package/src/providers/status-monitor.d.ts +7 -7
  402. package/src/providers/status-monitor.ts +37 -22
  403. package/src/providers/transcript-v2.ts +567 -0
  404. package/src/providers/types/interactive-prompt.ts +536 -0
  405. package/src/providers/version-archive.ts +64 -24
  406. package/src/providers/working-dir.ts +23 -0
  407. package/src/repo-mesh-types.ts +748 -14
  408. package/src/runtime-defaults.ts +39 -0
  409. package/src/sessions/registry.ts +6 -0
  410. package/src/shared-types-extra.ts +2 -4
  411. package/src/shared-types.d.ts +8 -0
  412. package/src/shared-types.ts +64 -1
  413. package/src/status/builders.ts +26 -6
  414. package/src/status/normalize.ts +2 -0
  415. package/src/status/reporter.ts +19 -1
  416. package/src/status/snapshot.ts +95 -26
  417. package/src/system/hash.ts +23 -0
  418. package/src/system/host-memory.ts +29 -12
  419. package/src/system/load-better-sqlite3.ts +68 -0
  420. package/src/types.ts +5 -0
  421. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  422. package/dist/mesh/mesh-sync.d.ts +0 -53
  423. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  424. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  425. package/src/mesh/mesh-sync.ts +0 -111
@@ -0,0 +1,2327 @@
1
+ /**
2
+ * Chat Commands — read side: handleReadChat, handleChatHistory and the
3
+ * native-history / source-resolution / normalization helpers they use.
4
+ */
5
+
6
+ import * as path from 'node:path';
7
+ import type { CommandResult, CommandHelpers } from './handler.js';
8
+ import type { CliAdapter } from '../cli-adapter-types.js';
9
+ import { flattenContent, type ProviderModule, type ProviderScripts } from '../providers/contracts.js';
10
+ import { validateReadChatResultPayload } from '../providers/read-chat-contract.js';
11
+ import { isNativeSourceCanonicalHistory, readChatHistory, readProviderChatHistory } from '../config/chat-history.js';
12
+ import { getCoordinatorForSession } from '../mesh/coordinator-registry.js';
13
+ import { LOG } from '../logging/logger.js';
14
+ import { recordDebugTrace } from '../logging/debug-trace.js';
15
+ import { hashSignatureParts } from '../chat/chat-signatures.js';
16
+ import {
17
+ CHAT_SOURCE_REGISTRY,
18
+ buildV1NativePresentObservation,
19
+ chatSourceSessionKey,
20
+ type ChatSourceDecision,
21
+ type ChatSourceObservation,
22
+ type ChatSourceTransitionCause,
23
+ } from '../chat/source-resolver.js';
24
+ import type { ChatMessage } from '../types.js';
25
+ import { filterUserFacingChatMessages, isActivityChatMessage, isUserFacingChatMessage, normalizeChatMessages } from '../providers/chat-message-normalization.js';
26
+ import {
27
+ READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS,
28
+ type RuntimeChatMessageMerger,
29
+ getCurrentProviderType,
30
+ getTargetedCliAdapter,
31
+ getTargetInstance,
32
+ getTargetTransport,
33
+ isCliLikeTransport,
34
+ isExtensionTransport,
35
+ parseMaybeJson,
36
+ } from './chat-commands-shared.js';
37
+ import { evaluateReadChatNodeWorkspaceScope, resolveTargetSessionActualWorkspace } from './chat-commands-scope.js';
38
+
39
+ // Minimum tail floor for hot-path history/mirror reads. The dashboard requests a
40
+ // bounded tail (~60); we keep a small floor so a tiny requested tailLimit still
41
+ // has enough surrounding context for seed/mirror dedup correctness, but it must
42
+ // NOT dominate the hot subscribe/poll path the way the previous 200 floor did.
43
+ // readChatHistory now serves this as an O(tail) bounded read, so the cost scales
44
+ // with this floor, not with total accumulated history.
45
+ const HOT_TAIL_MIN_LIMIT = 60;
46
+ // (A2.2) CLI_NATIVE_HISTORY_FRESH_MS removed with isNativeHistoryFreshEnough.
47
+ // Hardcoded native-transcript provider allow-list. Deprecated. Kept only as a
48
+ // last-resort fallback when ProviderModule is not yet loaded; on every hit we
49
+ // warn so the dependency on this set is visible. A2 deletes the set entirely
50
+ // and routes solely through canonicalHistory.contractVersion +
51
+ // isNativeSourceCanonicalHistory().
52
+ const CLI_NATIVE_TRANSCRIPT_PROVIDERS = new Set(['codex-cli', 'claude-cli', 'hermes-cli', 'antigravity-cli']);
53
+ const warnedLegacyNativeAllowlistHits = new Set<string>();
54
+ function warnLegacyNativeAllowlistHit(providerType: string): void {
55
+ if (warnedLegacyNativeAllowlistHits.has(providerType)) return;
56
+ warnedLegacyNativeAllowlistHits.add(providerType);
57
+ // eslint-disable-next-line no-console
58
+ console.warn(
59
+ `[chat-commands] supportsCliNativeTranscript fell back to the hardcoded `
60
+ + `CLI_NATIVE_TRANSCRIPT_PROVIDERS set for "${providerType}". `
61
+ + `The provider module was unavailable or did not declare canonicalHistory. `
62
+ + `Set canonicalHistory.contractVersion in the provider.json to remove this dependency.`,
63
+ );
64
+ }
65
+ function getExplicitHistorySessionId(args: any): string | undefined {
66
+ const explicit = typeof args?.historySessionId === 'string' ? args.historySessionId.trim() : '';
67
+ if (explicit) return explicit;
68
+
69
+ const explicitProviderSessionId = typeof args?.providerSessionId === 'string' ? args.providerSessionId.trim() : '';
70
+ if (explicitProviderSessionId) return explicitProviderSessionId;
71
+
72
+ return undefined;
73
+ }
74
+ function getHistorySessionId(h: CommandHelpers, args: any): string | undefined {
75
+ const explicit = getExplicitHistorySessionId(args);
76
+ if (explicit) return explicit;
77
+
78
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
79
+ if (!targetSessionId) return undefined;
80
+
81
+ const session = h.ctx.sessionRegistry?.get(targetSessionId) as any;
82
+ const registeredProviderSessionId = typeof session?.providerSessionId === 'string' ? session.providerSessionId.trim() : '';
83
+ if (registeredProviderSessionId) return registeredProviderSessionId;
84
+
85
+ const instance = getTargetInstance(h, args);
86
+ const state = instance?.getState?.();
87
+ const providerSessionId = typeof state?.providerSessionId === 'string' ? state.providerSessionId.trim() : '';
88
+ if (providerSessionId) return providerSessionId;
89
+
90
+ const currentSession = h.currentSession as any;
91
+ if (currentSession?.sessionId === targetSessionId) {
92
+ const currentProviderSessionId = typeof currentSession.providerSessionId === 'string'
93
+ ? currentSession.providerSessionId.trim()
94
+ : '';
95
+ if (currentProviderSessionId) return currentProviderSessionId;
96
+ }
97
+
98
+ return targetSessionId;
99
+ }
100
+
101
+ function resolveCliNativeHistorySessionId(args: any, currentHistorySessionId: string | undefined, parsedProviderSessionId: string | undefined): string | undefined {
102
+ const explicit = getExplicitHistorySessionId(args);
103
+ if (explicit) return explicit;
104
+
105
+ const parsed = typeof parsedProviderSessionId === 'string' ? parsedProviderSessionId.trim() : '';
106
+ const current = typeof currentHistorySessionId === 'string' ? currentHistorySessionId.trim() : '';
107
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
108
+
109
+ // getHistorySessionId falls back to the runtime session id when no native
110
+ // handle has been registered yet. For live CLI adapters the parser may
111
+ // already know the provider-native handle; prefer it over the runtime id so
112
+ // exact native reads do not miss the worker transcript and fall back to PTY
113
+ // or same-workspace history.
114
+ if (parsed && (!current || current === targetSessionId)) return parsed;
115
+ return current || parsed || undefined;
116
+ }
117
+
118
+ function shouldSkipLiveCliNativeHistoryWithoutProviderSession(args: {
119
+ adapter?: CliAdapter | null;
120
+ providerType?: string;
121
+ readChatArgs: any;
122
+ nativeHistorySessionId?: string;
123
+ parsedProviderSessionId?: string;
124
+ }): boolean {
125
+ const explicit = getExplicitHistorySessionId(args.readChatArgs);
126
+ if (explicit) return false;
127
+
128
+ const targetSessionId = typeof args.readChatArgs?.targetSessionId === 'string'
129
+ ? args.readChatArgs.targetSessionId.trim()
130
+ : '';
131
+ if (!targetSessionId) return false;
132
+
133
+ const resolved = typeof args.nativeHistorySessionId === 'string'
134
+ ? args.nativeHistorySessionId.trim()
135
+ : '';
136
+ if (!resolved || resolved !== targetSessionId) return false;
137
+
138
+ const parsed = typeof args.parsedProviderSessionId === 'string'
139
+ ? args.parsedProviderSessionId.trim()
140
+ : '';
141
+ if (parsed) return false;
142
+
143
+ const cliType = args.adapter?.cliType || args.providerType || '';
144
+ if (cliType !== 'codex-cli') return false;
145
+
146
+ // A live Codex session starts with only the daemon runtime UUID. That UUID
147
+ // is not the provider-native rollout id, so using it for native history
148
+ // lets the file picker fall back to the newest same-workspace transcript
149
+ // and makes concurrent fresh sessions all show the same old conversation.
150
+ return !!args.adapter;
151
+ }
152
+
153
+ function getInteractionId(args: any): string | undefined {
154
+ return typeof args?._interactionId === 'string' && args._interactionId.trim()
155
+ ? args._interactionId.trim()
156
+ : undefined;
157
+ }
158
+
159
+ function traceProviderEvent(
160
+ args: any,
161
+ category: 'provider' | 'parser',
162
+ stage: string,
163
+ options: {
164
+ h: CommandHelpers;
165
+ provider?: ProviderModule;
166
+ payload?: Record<string, unknown>;
167
+ level?: 'debug' | 'info' | 'warn' | 'error';
168
+ },
169
+ ): void {
170
+ recordDebugTrace({
171
+ interactionId: getInteractionId(args),
172
+ category,
173
+ stage,
174
+ level: options.level || 'info',
175
+ sessionId: typeof args?.targetSessionId === 'string' ? args.targetSessionId : options.h.currentSession?.sessionId,
176
+ providerType: options.provider?.type || options.h.currentProviderType || options.h.currentSession?.providerType,
177
+ payload: options.payload,
178
+ });
179
+ }
180
+ function normalizeReadChatTailLimit(args: any): number {
181
+ const value = Number(args?.tailLimit || 0);
182
+ return Number.isFinite(value) ? Math.max(0, value) : 0;
183
+ }
184
+
185
+ function normalizeReadChatMessages(payload: Record<string, any>): ChatMessage[] {
186
+ const messages = Array.isArray(payload.messages) ? payload.messages as ChatMessage[] : [];
187
+ return normalizeChatMessages(messages);
188
+ }
189
+
190
+ function getMessageNewestReceivedAt(messages: Array<{ receivedAt?: unknown; timestamp?: unknown }>): number {
191
+ let newest = 0;
192
+ for (const message of messages) {
193
+ const receivedAt = Number(message?.receivedAt ?? message?.timestamp ?? 0);
194
+ if (Number.isFinite(receivedAt) && receivedAt > newest) newest = receivedAt;
195
+ }
196
+ return newest;
197
+ }
198
+
199
+ function readHistorySessionIdFromMessages(messages: ChatMessage[]): string | undefined {
200
+ for (const message of messages as Array<ChatMessage & { historySessionId?: unknown }>) {
201
+ const historySessionId = typeof message?.historySessionId === 'string' ? message.historySessionId.trim() : '';
202
+ if (historySessionId) return historySessionId;
203
+ }
204
+ return undefined;
205
+ }
206
+
207
+ function shouldPreserveNativeIdentity(providerType: string, sessionId: string, message: ChatMessage): boolean {
208
+ const providerUnitKey = typeof message.providerUnitKey === 'string' ? message.providerUnitKey.trim() : '';
209
+ const turnKey = typeof message._turnKey === 'string' ? message._turnKey.trim() : '';
210
+ if (!providerUnitKey) return false;
211
+ // (A2.3) v2 stamped identity is producer-owned and globally stable; trust it
212
+ // unconditionally. Producers may omit _turnKey (the daemon recomputes it
213
+ // from the current ordering), so do not require turnKey for v2 messages.
214
+ if (providerUnitKey.startsWith('v2:') || providerUnitKey.startsWith('v2-pty:')) {
215
+ return true;
216
+ }
217
+ // v1 identity always required both keys to be present.
218
+ if (!turnKey) return false;
219
+ if (providerType === 'hermes-cli' && sessionId) {
220
+ return providerUnitKey.startsWith(`${providerType}:native:${sessionId}:`)
221
+ && turnKey.startsWith(`${providerType}:native-turn:${sessionId}:`);
222
+ }
223
+ return true;
224
+ }
225
+
226
+ /**
227
+ * Drop the synthetic "user" message some CLIs surface in their native
228
+ * transcript when the daemon injects a coordinator system prompt
229
+ * (codex puts the AGENTS.md / developer_instructions block in as
230
+ * role=user; agy/claude/hermes have similar artifacts). The user can
231
+ * opt back into seeing it via the provider setting
232
+ * `showCoordinatorSystemPrompt`. Default is off — the prompt is still
233
+ * fully visible from the chat-header ⓘ "Session info" dialog.
234
+ *
235
+ * Matching rules:
236
+ * 1. Setting must be off (default).
237
+ * 2. There must be a registered coordinator entry for the session.
238
+ * 3. The candidate message is filtered when its role is user OR
239
+ * system and its content either contains the prompt body verbatim,
240
+ * OR contains the well-known coordinator marker
241
+ * `adhdev-mesh-coordinator-prompt`. The marker covers context-file
242
+ * cases (agy AGENTS.md / gemini GEMINI.md) where the CLI may wrap
243
+ * its own preamble around our block. Verbatim-content covers
244
+ * codex's developer_instructions echo.
245
+ *
246
+ * Returns the messages array unchanged when none of the rules match,
247
+ * so this is safe to apply unconditionally to every read_chat result.
248
+ */
249
+ function maybeHideCoordinatorPromptMessage(
250
+ h: CommandHelpers,
251
+ providerType: string,
252
+ sessionId: string | undefined,
253
+ messages: ChatMessage[],
254
+ ): ChatMessage[] {
255
+ if (!Array.isArray(messages) || messages.length === 0) return messages;
256
+ if (!sessionId) return messages;
257
+ const loader = h.ctx?.providerLoader;
258
+ if (!loader) return messages;
259
+ let showSetting: unknown = undefined;
260
+ try {
261
+ showSetting = (loader as any).getSettingValue?.(providerType, 'showCoordinatorSystemPrompt');
262
+ } catch { /* unknown setting key for this provider — fall through */ }
263
+ if (showSetting === true) return messages;
264
+ const coord = getCoordinatorForSession(sessionId);
265
+ if (!coord) return messages;
266
+ const promptBody = typeof coord.systemPrompt === 'string' ? coord.systemPrompt : '';
267
+ const MARKER = 'adhdev-mesh-coordinator-prompt';
268
+ const filtered = messages.filter(m => {
269
+ const role = String((m as any)?.role || '').toLowerCase();
270
+ if (role !== 'user' && role !== 'system') return true;
271
+ const content = flattenContent((m as any)?.content);
272
+ if (!content) return true;
273
+ if (content.includes(MARKER)) return false;
274
+ if (promptBody && content.includes(promptBody.slice(0, Math.min(400, promptBody.length)))) return false;
275
+ return true;
276
+ });
277
+ if (filtered.length !== messages.length) {
278
+ LOG.debug('ChatFilter', `[${providerType}] hid ${messages.length - filtered.length} coordinator-prompt message(s) from ${sessionId}`);
279
+ }
280
+ return filtered;
281
+ }
282
+
283
+ /**
284
+ * Convenience wrapper used at every native-history call site: normalize +
285
+ * conditionally drop the coordinator system-prompt message. Avoids
286
+ * duplicating the filter at four read_chat code paths.
287
+ */
288
+ function normalizeAndFilterNativeHistory(
289
+ h: CommandHelpers,
290
+ providerType: string,
291
+ args: any,
292
+ messages: ChatMessage[],
293
+ nativeSessionId?: string,
294
+ ): ChatMessage[] {
295
+ const normalized = normalizeNativeHistoryMessages(providerType, messages, nativeSessionId);
296
+ const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId
297
+ : typeof args?.sessionId === 'string' ? args.sessionId
298
+ : undefined;
299
+ return maybeHideCoordinatorPromptMessage(h, providerType, sessionId, normalized);
300
+ }
301
+
302
+ function normalizeNativeHistoryMessages(providerType: string, messages: ChatMessage[], nativeSessionId?: string): ChatMessage[] {
303
+ let turnIndex = 0;
304
+ return normalizeChatMessages(messages).map((message, index) => {
305
+ const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
306
+ const kind = typeof message.kind === 'string' && message.kind.trim() ? message.kind.trim() : (role === 'system' ? 'system' : 'standard');
307
+ if ((role === 'user' || role === 'human') && index > 0) turnIndex += 1;
308
+ const historySessionId = typeof (message as any).historySessionId === 'string'
309
+ ? (message as any).historySessionId.trim()
310
+ : '';
311
+ const contentHash = hashSignatureParts([
312
+ providerType,
313
+ historySessionId,
314
+ String(message.receivedAt || message.timestamp || index),
315
+ role,
316
+ kind,
317
+ flattenContent(message.content),
318
+ ]).slice(0, 12);
319
+ const nativeIdentitySessionId = historySessionId || (typeof nativeSessionId === 'string' ? nativeSessionId.trim() : '');
320
+ const preserveNativeIdentity = shouldPreserveNativeIdentity(providerType, nativeIdentitySessionId, message);
321
+ const existingProviderUnitKey = typeof message.providerUnitKey === 'string' ? message.providerUnitKey.trim() : '';
322
+ const existingTurnKey = typeof message._turnKey === 'string' ? message._turnKey.trim() : '';
323
+ const providerUnitKey = preserveNativeIdentity
324
+ ? existingProviderUnitKey
325
+ : `${providerType}:native:${nativeIdentitySessionId || 'workspace'}:${index}:${role || 'message'}:${kind}:${contentHash}`;
326
+ const meta = message.meta && typeof message.meta === 'object' ? message.meta as Record<string, unknown> : undefined;
327
+ const isSystemSessionStart = role === 'system' || kind === 'system' || kind === 'session_start';
328
+ const isActivity = role === 'assistant' && (kind === 'tool' || kind === 'terminal' || kind === 'thought');
329
+ // (A2.3) sequence emit. Producer-supplied wins (v2-stamped messages
330
+ // bring their own monotonic sequence); otherwise derive from
331
+ // receivedAt/timestamp; otherwise positional. Always present on the
332
+ // output so consumers (ChatSourceMachine) have a stable ordering key.
333
+ const existingSequence = typeof (message as any).sequence === 'number'
334
+ && Number.isFinite((message as any).sequence)
335
+ ? (message as any).sequence
336
+ : null;
337
+ const tsCandidate = Number(message.receivedAt || message.timestamp || 0);
338
+ const sequence = existingSequence !== null
339
+ ? existingSequence
340
+ : (tsCandidate > 0 ? tsCandidate : index);
341
+ return {
342
+ ...message,
343
+ role: role === 'human' ? 'user' : (role || 'assistant'),
344
+ kind: isSystemSessionStart ? 'system' : kind,
345
+ ...(nativeIdentitySessionId ? { historySessionId: nativeIdentitySessionId } : {}),
346
+ providerUnitKey,
347
+ bubbleId: typeof message.bubbleId === 'string' && message.bubbleId.trim()
348
+ && preserveNativeIdentity
349
+ ? message.bubbleId.trim()
350
+ : `bubble:${providerUnitKey}`,
351
+ sequence,
352
+ _turnKey: preserveNativeIdentity
353
+ ? existingTurnKey
354
+ : `${providerType}:native-turn:${nativeIdentitySessionId || 'workspace'}:${turnIndex}`,
355
+ bubbleState: message.bubbleState || 'final',
356
+ ...(isSystemSessionStart ? {
357
+ visibility: message.visibility || 'hidden',
358
+ transcriptVisibility: message.transcriptVisibility || 'hidden',
359
+ audience: message.audience || 'internal',
360
+ source: message.source || 'runtime_status',
361
+ } : isActivity ? {
362
+ source: message.source || (kind === 'terminal' ? 'terminal_command' : 'tool_call'),
363
+ meta: { ...meta, label: message.senderName || meta?.label || (kind === 'terminal' ? 'Terminal' : 'Tool') },
364
+ } : {
365
+ source: message.source || (role === 'assistant' ? 'assistant_text' : undefined),
366
+ }),
367
+ } as ChatMessage;
368
+ });
369
+ }
370
+
371
+ function buildCliMessageSourceProvenance(args: {
372
+ selected: 'native-history' | 'pty-parser';
373
+ provider: string;
374
+ nativeHandle?: string;
375
+ sessionWorkspace?: string;
376
+ intendedWorkspace?: string;
377
+ transcriptWorkspace?: string;
378
+ fallbackReason?: string;
379
+ nativeSource?: string;
380
+ sourcePath?: string;
381
+ sourceMtimeMs?: number;
382
+ nativeHistoryCoverage?: string;
383
+ partialReason?: string;
384
+ unavailableReason?: string;
385
+ nativeMessages?: ChatMessage[];
386
+ ptyMessages?: ChatMessage[];
387
+ returnedMessages?: ChatMessage[];
388
+ safeMapping?: boolean;
389
+ freshEnough?: boolean;
390
+ ptyStatusApprovalOnly?: boolean;
391
+ }): Record<string, unknown> {
392
+ const sourceMtimeMs = Number(args.sourceMtimeMs || 0);
393
+ const sourceMtimeAgeMs = sourceMtimeMs > 0 ? Math.max(0, Date.now() - sourceMtimeMs) : undefined;
394
+ const nativeMessages = args.nativeMessages || [];
395
+ const ptyMessages = args.ptyMessages || [];
396
+ const returnedMessages = args.returnedMessages || [];
397
+ const identityStatus = args.selected === 'native-history'
398
+ ? 'safe'
399
+ : args.fallbackReason === 'native_history_not_safely_mapped'
400
+ ? 'ambiguous_session_identity'
401
+ : args.fallbackReason?.startsWith('native_history_unavailable')
402
+ ? 'transcript_unmapped'
403
+ : undefined;
404
+ return {
405
+ selected: args.selected,
406
+ provider: args.provider,
407
+ providerType: args.provider,
408
+ ...(identityStatus ? { identityStatus } : {}),
409
+ ...(args.nativeHandle ? { nativeHandle: args.nativeHandle } : {}),
410
+ ...(args.nativeHandle ? { nativeSessionId: args.nativeHandle } : {}),
411
+ ...(args.sessionWorkspace ? { sessionWorkspace: args.sessionWorkspace } : {}),
412
+ ...(args.intendedWorkspace ? { intendedWorkspace: args.intendedWorkspace } : {}),
413
+ ...(args.transcriptWorkspace ? { transcriptWorkspace: args.transcriptWorkspace } : {}),
414
+ ...(args.fallbackReason ? { fallbackReason: args.fallbackReason } : {}),
415
+ ...(args.nativeSource ? { nativeSource: args.nativeSource } : {}),
416
+ ...(args.sourcePath ? { sourcePath: args.sourcePath } : {}),
417
+ ...(args.nativeHistoryCoverage ? { nativeHistoryCoverage: args.nativeHistoryCoverage } : {}),
418
+ ...(args.partialReason ? { partialReason: args.partialReason } : {}),
419
+ ...(args.unavailableReason ? { unavailableReason: args.unavailableReason } : {}),
420
+ ptyStatusApprovalOnly: args.ptyStatusApprovalOnly === true,
421
+ staleness: {
422
+ sourceMtimeMs: sourceMtimeMs || undefined,
423
+ sourceMtimeAgeMs,
424
+ nativeNewestMessageAt: getMessageNewestReceivedAt(nativeMessages),
425
+ ptyNewestMessageAt: getMessageNewestReceivedAt(ptyMessages),
426
+ freshEnough: args.freshEnough === true,
427
+ },
428
+ coverage: {
429
+ nativeMessageCount: nativeMessages.length,
430
+ ptyMessageCount: ptyMessages.length,
431
+ returnedMessageCount: returnedMessages.length,
432
+ safeMapping: args.safeMapping === true,
433
+ // true when PTY message bodies are suppressed and must not be treated as
434
+ // chat content. PTY may still contribute status/approval/screen evidence.
435
+ ptyMessagesSuppressed: args.selected === 'native-history' || args.ptyStatusApprovalOnly === true,
436
+ },
437
+ };
438
+ }
439
+
440
+ /**
441
+ * Map a ChatSourceMachine transition cause back to the v1 messageSource
442
+ * `fallbackReason` vocabulary so legacy consumers (web-cloud, tests, mesh
443
+ * debug bundles) keep parsing strings they already know. A3 replaces the
444
+ * caller surface with stateTransition/lockState, after which this map can be
445
+ * deleted.
446
+ *
447
+ * Returns undefined when the cause does not correspond to a fallback (i.e.
448
+ * the source is native-history and there is nothing to explain).
449
+ */
450
+ function causeToLegacyFallbackReason(
451
+ cause: ChatSourceTransitionCause,
452
+ selected: 'native-history' | 'pty-parser',
453
+ extraDetail?: { unavailableReason?: string; nativeSource?: string },
454
+ ): string | undefined {
455
+ if (selected === 'native-history') return undefined;
456
+ switch (cause) {
457
+ case 'initial':
458
+ return 'native_history_not_checked';
459
+ case 'native_progressed':
460
+ // Selected pty-parser despite a progressed observation — that
461
+ // means we held PtyOnly stickily (peak unmet or non-superset).
462
+ return 'native_history_not_selected';
463
+ case 'native_regressed_shrunk':
464
+ return 'native_history_empty';
465
+ case 'native_regressed_unsafe_mapping':
466
+ return 'native_history_not_safely_mapped';
467
+ case 'native_regressed_coverage_partial':
468
+ return 'native_history_partial';
469
+ case 'native_regressed_coverage_unavailable':
470
+ return 'native_history_unavailable';
471
+ case 'native_unavailable_read_error':
472
+ return extraDetail?.unavailableReason
473
+ ? `native_history_unavailable:${extraDetail.unavailableReason}`
474
+ : 'native_history_unavailable';
475
+ case 'native_unavailable_provider_unsupported':
476
+ return 'provider_native_transcript_not_supported';
477
+ case 'native_unavailable_empty':
478
+ return 'native_history_empty';
479
+ case 'native_unavailable_not_native_source':
480
+ return extraDetail?.nativeSource
481
+ ? `native_history_source_${extraDetail.nativeSource}`
482
+ : 'native_history_unavailable';
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Translate a native-history fetch result + provider/adapter context into a
488
+ * ChatSourceObservation and drive ChatSourceRegistry. Returns the decision
489
+ * together with the legacy messageSource payload so call sites can produce
490
+ * a v1-compatible response without duplicating the registry plumbing.
491
+ *
492
+ * This is the replacement for the 300-line if-ladder that previously lived
493
+ * inline in handleReadChat. It is intentionally split out for two reasons:
494
+ * (1) we will call it from two places (CLI adapter branch + history-only
495
+ * branch) instead of duplicating the ladder, (2) tests can drive it with
496
+ * synthetic native-history results to verify the cause→fallbackReason
497
+ * mapping without booting the whole readChat pipeline.
498
+ */
499
+ function decideCliReadChatSource(args: {
500
+ providerType: string;
501
+ provider?: ProviderModule;
502
+ sessionId: string;
503
+ nativeHistoryResult: any | null;
504
+ nativeHistoryError?: unknown;
505
+ safeMapping: boolean;
506
+ trustedExactNativeIdentity?: boolean;
507
+ sessionWorkspace?: string;
508
+ intendedWorkspace?: string;
509
+ ptyMessages: ChatMessage[];
510
+ ptyStatusApprovalOnly: boolean;
511
+ }): {
512
+ decision: ChatSourceDecision;
513
+ messageSource: Record<string, unknown>;
514
+ nativeMessages: ChatMessage[];
515
+ nativeSelected: boolean;
516
+ } {
517
+ const supportsNative = supportsCliNativeTranscript(args.providerType, args.provider);
518
+ const observation = buildObservationForCli(args, supportsNative);
519
+ const sessionKey = chatSourceSessionKey(args.providerType, args.sessionId);
520
+ let decision = CHAT_SOURCE_REGISTRY.observe(sessionKey, observation);
521
+
522
+ // A restored runtime can briefly expose different native slices while the
523
+ // provider transcript settles (for example, startup/system rows may be
524
+ // filtered after the first read). The source machine correctly treats a
525
+ // shrinking slice as regression, but an exact provider-session lookup with
526
+ // no PTY transcript has no safer fallback. Re-bootstrap only this proven
527
+ // identity so the chat does not disappear after daemon restart.
528
+ if (
529
+ decision.selected === 'pty-parser'
530
+ && args.trustedExactNativeIdentity === true
531
+ && args.safeMapping
532
+ && args.ptyMessages.length === 0
533
+ && observation.kind === 'native_present'
534
+ && observation.coverage !== 'partial'
535
+ && observation.messages.length > 0
536
+ ) {
537
+ CHAT_SOURCE_REGISTRY.clear(sessionKey);
538
+ decision = CHAT_SOURCE_REGISTRY.observe(sessionKey, observation);
539
+ }
540
+
541
+ const nativeMessages: ChatMessage[] = observation.kind === 'native_present'
542
+ ? extractNativeMessagesFromResult(args.providerType, args.nativeHistoryResult)
543
+ : [];
544
+
545
+ const nativeSource = typeof args.nativeHistoryResult?.source === 'string'
546
+ ? args.nativeHistoryResult.source
547
+ : undefined;
548
+ const sourcePath = typeof args.nativeHistoryResult?.sourcePath === 'string'
549
+ ? args.nativeHistoryResult.sourcePath
550
+ : undefined;
551
+ const sourceMtimeMs = typeof args.nativeHistoryResult?.sourceMtimeMs === 'number'
552
+ ? args.nativeHistoryResult.sourceMtimeMs
553
+ : undefined;
554
+ const coverageHint = typeof args.nativeHistoryResult?.nativeHistoryCoverage === 'string'
555
+ ? args.nativeHistoryResult.nativeHistoryCoverage
556
+ : undefined;
557
+ const partialReason = typeof args.nativeHistoryResult?.partialReason === 'string'
558
+ ? args.nativeHistoryResult.partialReason
559
+ : undefined;
560
+ const unavailableReason = typeof args.nativeHistoryResult?.unavailableReason === 'string'
561
+ ? args.nativeHistoryResult.unavailableReason
562
+ : args.nativeHistoryError
563
+ ? `error:${(args.nativeHistoryError as any)?.message || String(args.nativeHistoryError)}`
564
+ : undefined;
565
+ const nativeHandle = typeof args.nativeHistoryResult?.providerSessionId === 'string'
566
+ ? args.nativeHistoryResult.providerSessionId
567
+ : undefined;
568
+ const transcriptWorkspace = typeof args.nativeHistoryResult?.workspace === 'string'
569
+ ? args.nativeHistoryResult.workspace
570
+ : nativeMessages.map((m: any) => typeof m?.workspace === 'string' ? m.workspace.trim() : '').find(Boolean);
571
+
572
+ const fallbackReason = causeToLegacyFallbackReason(decision.transition.cause, decision.selected, {
573
+ unavailableReason,
574
+ nativeSource: nativeSource && nativeSource !== 'provider-native' ? nativeSource : undefined,
575
+ });
576
+
577
+ // ptyStatusApprovalOnly: when the machine selected native-history we
578
+ // suppress PTY content so the dashboard does not double-show messages
579
+ // already in the native transcript. When the machine selected
580
+ // pty-parser, PTY is the authoritative source — do NOT suppress it.
581
+ // Callers used to hard-code this to `nativeSelected first = true` which
582
+ // suppressed PTY content even when native was empty/unavailable, leaving
583
+ // the dashboard with zero visible messages (the codex generating/waiting
584
+ // approval stuck state). Trust the machine here, not the caller hint.
585
+ const ptyStatusApprovalOnly = decision.selected === 'native-history'
586
+ ? true
587
+ : args.ptyStatusApprovalOnly;
588
+
589
+ const messageSource = buildCliMessageSourceProvenance({
590
+ selected: decision.selected,
591
+ provider: args.providerType,
592
+ nativeHandle,
593
+ sessionWorkspace: args.sessionWorkspace,
594
+ intendedWorkspace: args.intendedWorkspace,
595
+ transcriptWorkspace,
596
+ fallbackReason,
597
+ nativeSource,
598
+ sourcePath,
599
+ sourceMtimeMs,
600
+ nativeHistoryCoverage: coverageHint,
601
+ partialReason,
602
+ unavailableReason,
603
+ nativeMessages,
604
+ ptyMessages: args.ptyMessages,
605
+ returnedMessages: decision.selected === 'native-history' ? nativeMessages : args.ptyMessages,
606
+ safeMapping: args.safeMapping,
607
+ // freshEnough is a v1 concept the machine does not model directly.
608
+ // We surface lockState.locked here so v1 consumers reading
609
+ // staleness.freshEnough still get a meaningful boolean.
610
+ freshEnough: decision.lockState.locked,
611
+ ptyStatusApprovalOnly,
612
+ });
613
+
614
+ return {
615
+ decision,
616
+ messageSource,
617
+ nativeMessages,
618
+ nativeSelected: decision.selected === 'native-history',
619
+ };
620
+ }
621
+
622
+ function buildObservationForCli(
623
+ args: {
624
+ providerType: string;
625
+ sessionId: string;
626
+ nativeHistoryResult: any | null;
627
+ nativeHistoryError?: unknown;
628
+ safeMapping: boolean;
629
+ },
630
+ supportsNative: boolean,
631
+ ): ChatSourceObservation {
632
+ if (!supportsNative) {
633
+ return { kind: 'native_unavailable', reason: 'provider_not_supported' };
634
+ }
635
+ if (args.nativeHistoryError) {
636
+ return { kind: 'native_unavailable', reason: 'read_error' };
637
+ }
638
+ const result = args.nativeHistoryResult;
639
+ if (!result || typeof result !== 'object') {
640
+ return { kind: 'native_unavailable', reason: 'read_error' };
641
+ }
642
+ const source = typeof result.source === 'string' ? result.source : '';
643
+ if (source && source !== 'provider-native') {
644
+ // 'native-unavailable' or other producer-side declined source.
645
+ return { kind: 'native_unavailable', reason: source === 'native-unavailable' ? 'empty' : 'not_native_source' };
646
+ }
647
+ const messages = Array.isArray(result.messages) ? result.messages : [];
648
+ if (messages.length === 0) {
649
+ return { kind: 'native_unavailable', reason: 'empty' };
650
+ }
651
+ const coverage = typeof result.nativeHistoryCoverage === 'string'
652
+ ? result.nativeHistoryCoverage
653
+ : 'tail';
654
+ if (coverage === 'unavailable') {
655
+ return { kind: 'native_unavailable', reason: 'coverage_unavailable' };
656
+ }
657
+ return buildV1NativePresentObservation({
658
+ providerType: args.providerType,
659
+ sessionId: args.sessionId,
660
+ messages,
661
+ coverage: coverage === 'full' || coverage === 'tail' || coverage === 'current-turn' || coverage === 'partial'
662
+ ? coverage
663
+ : 'tail',
664
+ safeMapping: args.safeMapping,
665
+ });
666
+ }
667
+
668
+ function extractNativeMessagesFromResult(providerType: string, result: any): ChatMessage[] {
669
+ if (!result || !Array.isArray(result.messages)) return [];
670
+ return normalizeNativeHistoryMessages(
671
+ providerType,
672
+ result.messages as ChatMessage[],
673
+ typeof result.providerSessionId === 'string' ? result.providerSessionId : undefined,
674
+ );
675
+ }
676
+
677
+ /**
678
+ * ptyStatusApprovalOnly is true when the daemon should treat PTY content as
679
+ * status/approval signal only (not as chat messages). v1 set this to `true`
680
+ * whenever native-history was selected as the source, and `false` otherwise.
681
+ * The machine equivalent: when native is the source we want PTY suppressed.
682
+ */
683
+ function primaryPtyApprovalOnlyFor(_cliType: string, nativeSelected: boolean): boolean {
684
+ return nativeSelected;
685
+ }
686
+
687
+ /**
688
+ * Codex-only unsafe-native fallback: when the primary native fetch produced
689
+ * unsafe-mapping data, v1 attempted to recover by reading exact runtime
690
+ * mirror messages, runtime input ACK messages, or by trusting the current-
691
+ * runtime PTY when safely attributed. None of this is the machine's
692
+ * responsibility — the machine already decided pty-parser. This helper
693
+ * preserves the daemon-side message selection and annotates messageSource.
694
+ */
695
+ function applyUnsafeNativeDaemonFallback(args: {
696
+ providerType: string;
697
+ adapter: CliAdapter;
698
+ helpers: CommandHelpers;
699
+ readChatArgs: any;
700
+ sessionWorkspace?: string;
701
+ intendedWorkspace?: string;
702
+ ptyMessages: ChatMessage[];
703
+ nativeHistoryLimit: number;
704
+ provider?: ProviderModule;
705
+ messageSourceRef: { set(value: Record<string, unknown>): void; get(): Record<string, unknown> };
706
+ apply(selection: {
707
+ messages: ChatMessage[];
708
+ transcriptAuthority?: 'provider' | 'daemon';
709
+ coverage?: 'full' | 'tail' | 'current-turn';
710
+ status?: string;
711
+ }): void;
712
+ activeModal: unknown;
713
+ returnedStatus: string;
714
+ coverage?: 'full' | 'tail' | 'current-turn';
715
+ }): void {
716
+ if (args.adapter.cliType !== 'codex-cli') {
717
+ // Only codex-cli had v1 daemon mirror recovery. Other providers skip.
718
+ return;
719
+ }
720
+ const ms = args.messageSourceRef.get();
721
+ const fallbackReason = typeof ms.fallbackReason === 'string' ? ms.fallbackReason : '';
722
+ if (!isUnsafeNativeTranscriptFallback(fallbackReason)) {
723
+ return;
724
+ }
725
+ const safeCurrentRuntimePtyMessages = isCurrentRuntimePtySafelyAttributed({
726
+ adapter: args.adapter,
727
+ helpers: args.helpers,
728
+ readChatArgs: args.readChatArgs,
729
+ sessionWorkspace: args.sessionWorkspace,
730
+ intendedWorkspace: args.intendedWorkspace,
731
+ ptyMessages: args.ptyMessages,
732
+ });
733
+ if (safeCurrentRuntimePtyMessages) {
734
+ args.apply({
735
+ messages: args.ptyMessages,
736
+ transcriptAuthority: 'daemon',
737
+ coverage: args.coverage || 'current-turn',
738
+ status: args.returnedStatus,
739
+ });
740
+ const next = { ...ms, selectedDaemonSource: 'current-runtime-pty', transcriptAuthority: 'daemon', runtimeMappingSafe: true };
741
+ args.messageSourceRef.set(next);
742
+ return;
743
+ }
744
+ const safeRuntimeAckMessages = selectRuntimeInputAckMessages(args.ptyMessages);
745
+ if (safeRuntimeAckMessages.length > 0) {
746
+ args.apply({
747
+ messages: safeRuntimeAckMessages,
748
+ transcriptAuthority: 'daemon',
749
+ coverage: 'tail',
750
+ status: coerceUnsafeNativeFallbackStatus(args.returnedStatus, args.activeModal),
751
+ });
752
+ const next = { ...ms, ptyStatusApprovalOnly: true };
753
+ args.messageSourceRef.set(next);
754
+ return;
755
+ }
756
+ const exactRuntimeMirrorMessages = readExactRuntimeMirrorMessages({
757
+ providerType: args.providerType,
758
+ targetSessionId: typeof args.readChatArgs?.targetSessionId === 'string' ? args.readChatArgs.targetSessionId : undefined,
759
+ currentSessionId: typeof (args.helpers.currentSession as any)?.sessionId === 'string' ? (args.helpers.currentSession as any).sessionId : undefined,
760
+ tailLimit: args.nativeHistoryLimit,
761
+ historyBehavior: args.provider?.historyBehavior,
762
+ });
763
+ if (exactRuntimeMirrorMessages.length > 0) {
764
+ args.apply({
765
+ messages: exactRuntimeMirrorMessages,
766
+ transcriptAuthority: 'daemon',
767
+ coverage: 'tail',
768
+ status: coerceUnsafeNativeFallbackStatus(args.returnedStatus, args.activeModal),
769
+ });
770
+ const next = { ...ms, selectedDaemonSource: 'exact-runtime-mirror', transcriptAuthority: 'daemon', ptyStatusApprovalOnly: true };
771
+ args.messageSourceRef.set(next);
772
+ return;
773
+ }
774
+ // No daemon mirror available — keep PTY messages as-is (still pty-parser
775
+ // selection); just coerce status for waiting_approval consistency.
776
+ args.apply({
777
+ messages: args.ptyMessages,
778
+ coverage: args.coverage,
779
+ status: coerceUnsafeNativeFallbackStatus(args.returnedStatus, args.activeModal),
780
+ });
781
+ const next = { ...ms, ptyStatusApprovalOnly: true };
782
+ args.messageSourceRef.set(next);
783
+ }
784
+
785
+ // (A2.2) buildNativeHistoryFallbackReason removed. ChatSourceMachine emits a
786
+ // ChatSourceTransitionCause; causeToLegacyFallbackReason maps it back to the
787
+ // v1 vocabulary for response compatibility. A3 deletes the v1 vocabulary
788
+ // entirely and surfaces stateTransition/lockState directly.
789
+
790
+ function isUnsafeNativeTranscriptFallback(reason?: string): boolean {
791
+ const value = String(reason || '').trim();
792
+ return value.startsWith('native_history_unavailable')
793
+ || value === 'native_history_not_safely_mapped'
794
+ || value === 'native_history_stale'
795
+ || value === 'native_history_partial';
796
+ }
797
+
798
+ function coerceUnsafeNativeFallbackStatus(status: string, activeModal: unknown): string {
799
+ if (status === 'waiting_approval' && activeModal) return status;
800
+ return 'idle';
801
+ }
802
+
803
+ function isRuntimeInputAckMessage(message: ChatMessage | undefined): boolean {
804
+ if (!message || typeof message !== 'object') return false;
805
+ const role = String((message as any).role || '').trim().toLowerCase();
806
+ if (role !== 'user' && role !== 'human') return false;
807
+ const meta = (message as any).meta;
808
+ return !!meta && typeof meta === 'object' && !Array.isArray(meta) && meta.runtimeInputAck === true;
809
+ }
810
+
811
+ function selectRuntimeInputAckMessages(messages: ChatMessage[]): ChatMessage[] {
812
+ return messages.filter((message) => isRuntimeInputAckMessage(message));
813
+ }
814
+
815
+ function readExactRuntimeMirrorMessages(args: {
816
+ providerType: string;
817
+ targetSessionId?: string;
818
+ currentSessionId?: string;
819
+ tailLimit: number;
820
+ historyBehavior?: ProviderModule['historyBehavior'];
821
+ }): ChatMessage[] {
822
+ const targetSessionId = String(args.targetSessionId || '').trim();
823
+ const currentSessionId = String(args.currentSessionId || '').trim();
824
+ if (!targetSessionId || targetSessionId !== currentSessionId) return [];
825
+
826
+ const history = readChatHistory(
827
+ args.providerType,
828
+ 0,
829
+ Math.max(args.tailLimit || 0, HOT_TAIL_MIN_LIMIT),
830
+ targetSessionId,
831
+ 0,
832
+ args.historyBehavior,
833
+ );
834
+ return normalizeChatMessages((history.messages || []) as ChatMessage[])
835
+ .filter((message) => {
836
+ const historySessionId = String((message as any).historySessionId || '').trim();
837
+ const instanceId = String((message as any).instanceId || '').trim();
838
+ return historySessionId === targetSessionId || instanceId === targetSessionId;
839
+ });
840
+ }
841
+ function normalizeComparableWorkspace(value: unknown): string {
842
+ const text = typeof value === 'string' ? value.trim() : '';
843
+ if (!text) return '';
844
+ return path.resolve(text);
845
+ }
846
+ function isCurrentRuntimePtySafelyAttributed(args: {
847
+ adapter: CliAdapter;
848
+ helpers: CommandHelpers;
849
+ readChatArgs: any;
850
+ sessionWorkspace?: string;
851
+ intendedWorkspace?: string;
852
+ ptyMessages: ChatMessage[];
853
+ }): boolean {
854
+ if (args.adapter.cliType !== 'codex-cli') return false;
855
+ if (!Array.isArray(args.ptyMessages) || args.ptyMessages.length === 0) return false;
856
+ const targetSessionId = typeof args.readChatArgs?.targetSessionId === 'string'
857
+ ? args.readChatArgs.targetSessionId.trim()
858
+ : '';
859
+ const currentSession = args.helpers.currentSession as any;
860
+ const currentSessionId = typeof currentSession?.sessionId === 'string'
861
+ ? currentSession.sessionId.trim()
862
+ : '';
863
+ if (!targetSessionId || !currentSessionId || targetSessionId !== currentSessionId) return false;
864
+
865
+ const runtimeMeta = typeof (args.adapter as any).getRuntimeMetadata === 'function'
866
+ ? (args.adapter as any).getRuntimeMetadata()
867
+ : null;
868
+ const runtimeId = typeof runtimeMeta?.runtimeId === 'string' ? runtimeMeta.runtimeId.trim() : '';
869
+ if (!runtimeId || runtimeId !== targetSessionId) return false;
870
+ const surfaceKind = typeof runtimeMeta?.surfaceKind === 'string' ? runtimeMeta.surfaceKind : '';
871
+ if (surfaceKind === 'inactive_record' || surfaceKind === 'recovery_snapshot') return false;
872
+
873
+ const sessionWorkspace = normalizeComparableWorkspace(args.sessionWorkspace);
874
+ const adapterWorkspace = normalizeComparableWorkspace(args.adapter.workingDir);
875
+ if (!sessionWorkspace || !adapterWorkspace || sessionWorkspace !== adapterWorkspace) return false;
876
+ const intendedWorkspace = normalizeComparableWorkspace(args.intendedWorkspace);
877
+ if (intendedWorkspace && intendedWorkspace !== sessionWorkspace) return false;
878
+
879
+ const registryEntry = args.helpers.ctx?.sessionRegistry?.get?.(targetSessionId) as any;
880
+ const registryInstanceKey = typeof registryEntry?.adapterKey === 'string' && registryEntry.adapterKey.trim()
881
+ ? registryEntry.adapterKey.trim()
882
+ : typeof registryEntry?.instanceKey === 'string' && registryEntry.instanceKey.trim()
883
+ ? registryEntry.instanceKey.trim()
884
+ : '';
885
+ if (registryInstanceKey) {
886
+ const targetInstance = args.helpers.ctx?.instanceManager?.getInstance?.(registryInstanceKey);
887
+ if (targetInstance) {
888
+ const instanceType = typeof (targetInstance as any).type === 'string' ? (targetInstance as any).type : '';
889
+ if (instanceType && instanceType !== args.adapter.cliType) return false;
890
+ }
891
+ }
892
+
893
+ return true;
894
+ }
895
+
896
+ function supportsCliNativeTranscript(providerType: string, provider?: ProviderModule): boolean {
897
+ // Preferred path: the provider module declares canonicalHistory in its
898
+ // provider.json. We trust that declaration regardless of the legacy
899
+ // allow-list. A2 will additionally require canonicalHistory.contractVersion
900
+ // to be a supported value (transcript-v2.ts).
901
+ if (provider?.category === 'cli' && isNativeSourceCanonicalHistory(provider?.nativeHistory)) {
902
+ return true;
903
+ }
904
+ // Last-resort fallback for early call sites where the provider module is
905
+ // not yet loaded. Warn once per provider type so this dependency is visible
906
+ // and can be removed in A2.
907
+ if (CLI_NATIVE_TRANSCRIPT_PROVIDERS.has(providerType)) {
908
+ warnLegacyNativeAllowlistHit(providerType);
909
+ return true;
910
+ }
911
+ return false;
912
+ }
913
+
914
+ function getComparableVisibleText(message: ChatMessage | undefined): string {
915
+ if (!message) return '';
916
+ const role = String((message as any).role || '').trim().toLowerCase();
917
+ if (role !== 'user' && role !== 'assistant') return '';
918
+ const kind = String((message as any).kind || 'standard').trim().toLowerCase();
919
+ if (kind && kind !== 'standard') return '';
920
+ const content = flattenContent((message as any).content).replace(/\s+/g, ' ').trim();
921
+ return content;
922
+ }
923
+
924
+ function hasOverlappingVisibleConversationText(nativeMessages: ChatMessage[], ptyMessages: ChatMessage[]): boolean {
925
+ const nativeTexts = nativeMessages.map(getComparableVisibleText).filter(Boolean);
926
+ const ptyTexts = ptyMessages.map(getComparableVisibleText).filter(Boolean);
927
+ if (nativeTexts.length === 0 || ptyTexts.length === 0) return false;
928
+ for (const nativeText of nativeTexts) {
929
+ for (const ptyText of ptyTexts) {
930
+ if (nativeText === ptyText) return true;
931
+ const shorter = nativeText.length <= ptyText.length ? nativeText : ptyText;
932
+ const longer = nativeText.length <= ptyText.length ? ptyText : nativeText;
933
+ if (shorter.length >= 32 && longer.includes(shorter)) return true;
934
+ }
935
+ }
936
+ return false;
937
+ }
938
+
939
+ function hasSafeNativeHistoryMapping(args: {
940
+ historySessionId?: string;
941
+ providerSessionId?: string;
942
+ workspace?: string;
943
+ nativeMessages: ChatMessage[];
944
+ ptyMessages?: ChatMessage[];
945
+ requireWorkspaceContentOverlap?: boolean;
946
+ }): boolean {
947
+ const isCoordinatorTranscript = args.nativeMessages.some((m: any) => {
948
+ const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '');
949
+ return text.includes('mesh_send_task') || text.includes('mesh_status') || text.includes('mesh_read_chat') || text.includes('mesh_launch_session');
950
+ });
951
+
952
+ const explicitSessionId = String(args.historySessionId || args.providerSessionId || '').trim();
953
+ if (explicitSessionId) {
954
+ const expectedWorkspace = normalizeComparableWorkspace(args.workspace);
955
+ const declaredWorkspaces = args.nativeMessages
956
+ .map((message: any) => normalizeComparableWorkspace(message?.workspace))
957
+ .filter(Boolean);
958
+ if (
959
+ expectedWorkspace
960
+ && declaredWorkspaces.length > 0
961
+ && !declaredWorkspaces.some((workspace) => workspace === expectedWorkspace)
962
+ ) {
963
+ return false;
964
+ }
965
+ const messageSessionIds = args.nativeMessages
966
+ .map((message: any) => typeof message?.historySessionId === 'string' ? message.historySessionId.trim() : '')
967
+ .filter(Boolean);
968
+ if (messageSessionIds.length > 0) {
969
+ return messageSessionIds.some((id) => id === explicitSessionId);
970
+ }
971
+
972
+ // Messages carry no historySessionId — cannot confirm they belong to the requested session.
973
+ // Only allow a coordinator transcript that is also confirmed by the PTY side; otherwise
974
+ // fail closed so a same-workspace session's history is never silently accepted.
975
+ if (isCoordinatorTranscript && args.ptyMessages && args.ptyMessages.length > 0) {
976
+ const ptyHasCoordinator = args.ptyMessages.some((m: any) => {
977
+ const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '');
978
+ return text.includes('mesh_send_task') || text.includes('mesh_status') || text.includes('mesh_read_chat');
979
+ });
980
+ return ptyHasCoordinator;
981
+ }
982
+
983
+ // No historySessionId in messages and no coordinator cross-check: fail closed.
984
+ // Workspace-only matching must not override an explicit session identity.
985
+ return false;
986
+ }
987
+ const workspace = String(args.workspace || '').trim();
988
+ if (!workspace) return false;
989
+ const workspaceMatches = args.nativeMessages.some((message: any) => String(message?.workspace || '').trim() === workspace);
990
+ if (!workspaceMatches) return false;
991
+
992
+ if (isCoordinatorTranscript && args.ptyMessages && args.ptyMessages.length > 0) {
993
+ const ptyHasCoordinator = args.ptyMessages.some((m: any) => {
994
+ const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '');
995
+ return text.includes('mesh_send_task') || text.includes('mesh_status') || text.includes('mesh_read_chat');
996
+ });
997
+ if (!ptyHasCoordinator) {
998
+ return false;
999
+ }
1000
+ }
1001
+
1002
+ if (!args.requireWorkspaceContentOverlap) return true;
1003
+ return hasOverlappingVisibleConversationText(args.nativeMessages, args.ptyMessages || []);
1004
+ }
1005
+
1006
+ // Provenance boundary: workspace-only native history lookup is never safe
1007
+ // because multiple concurrent sessions sharing the same cwd would alias each
1008
+ // other. historySessionId (the provider-native session key) is required to
1009
+ // establish ownership. hasSafeNativeHistoryMapping() enforces the same
1010
+ // invariant after the read; both guards must hold for native history to be used.
1011
+ /**
1012
+ * Pull the session's spawnedAtMs out of the registry. Native-history
1013
+ * file pickers use it as a "files older than this can't be from this
1014
+ * session" floor; without it a fresh dashboard view would inherit the
1015
+ * previous session's transcript whenever its file happened to be the
1016
+ * newest match. Returns undefined when the session isn't registered
1017
+ * (e.g. read_chat before the live session was wired up) — the executor
1018
+ * treats undefined as "no floor".
1019
+ */
1020
+ function sessionStartedAtMsFromRegistry(h: CommandHelpers, targetSessionId: string | undefined): number | undefined {
1021
+ const sid = typeof targetSessionId === 'string' ? targetSessionId.trim() : '';
1022
+ if (!sid) return undefined;
1023
+ const target = h.ctx?.sessionRegistry?.get?.(sid);
1024
+ return typeof target?.spawnedAtMs === 'number' ? target.spawnedAtMs : undefined;
1025
+ }
1026
+
1027
+ /**
1028
+ * Pull the env vars the daemon set when it spawned this session's CLI.
1029
+ * Mesh coordinator points hermes at a per-coordinator HERMES_HOME so
1030
+ * the native-history reader needs that override to find the right
1031
+ * state.db; without it the reader sees ~/.hermes/state.db and misses
1032
+ * every coordinator-session transcript.
1033
+ *
1034
+ * Returns undefined when no SpecCliAdapter is in play (legacy
1035
+ * providers / CDP) or when the adapter exposes no spawn env.
1036
+ */
1037
+ function sessionSpawnEnvFromAdapter(h: CommandHelpers, targetSessionId: string | undefined): Record<string, string> | undefined {
1038
+ const adapter = getTargetedCliAdapter(h, { targetSessionId }, undefined);
1039
+ if (!adapter || typeof adapter.getRuntimeMetadata !== 'function') return undefined;
1040
+ const meta = adapter.getRuntimeMetadata() as Record<string, unknown> | undefined;
1041
+ const env = meta && typeof meta === 'object' ? (meta as Record<string, unknown>).spawnedEnv : undefined;
1042
+ return env && typeof env === 'object' ? env as Record<string, string> : undefined;
1043
+ }
1044
+
1045
+
1046
+ function readCliProviderNativeHistory(agentStr: string, args: {
1047
+ canonicalHistory?: ProviderModule['canonicalHistory'];
1048
+ historySessionId?: string;
1049
+ workspace?: string;
1050
+ offset: number;
1051
+ limit: number;
1052
+ excludeRecentCount: number;
1053
+ historyBehavior?: ProviderModule['historyBehavior'];
1054
+ scripts?: ProviderScripts;
1055
+ excludeInProgressTurn?: boolean;
1056
+ sessionStartedAtMs?: number;
1057
+ envOverrides?: Record<string, string>;
1058
+ }): ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' } {
1059
+ const canBindFromLiveSession = !args.historySessionId
1060
+ && typeof args.sessionStartedAtMs === 'number'
1061
+ && args.sessionStartedAtMs > 0
1062
+ && typeof args.workspace === 'string'
1063
+ && args.workspace.trim().length > 0;
1064
+ if (!args.historySessionId && !canBindFromLiveSession) {
1065
+ return {
1066
+ messages: [],
1067
+ hasMore: false,
1068
+ source: 'native-unavailable',
1069
+ unavailableReason: 'native_history_workspace_only_lookup_unsafe',
1070
+ lookup: 'session',
1071
+ } as ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' };
1072
+ }
1073
+ const sessionHistory = readProviderChatHistory(agentStr, {
1074
+ canonicalHistory: args.canonicalHistory,
1075
+ historySessionId: args.historySessionId,
1076
+ workspace: args.workspace,
1077
+ offset: args.offset,
1078
+ limit: args.limit,
1079
+ excludeRecentCount: args.excludeRecentCount,
1080
+ historyBehavior: args.historyBehavior,
1081
+ scripts: args.scripts as any,
1082
+ excludeInProgressTurn: args.excludeInProgressTurn,
1083
+ sessionStartedAtMs: args.sessionStartedAtMs,
1084
+ envOverrides: args.envOverrides,
1085
+ });
1086
+ const boundProviderSessionId = typeof (sessionHistory as any)?.providerSessionId === 'string'
1087
+ ? (sessionHistory as any).providerSessionId.trim()
1088
+ : '';
1089
+ // A fresh live session can be bound without a provider id when the native
1090
+ // reader matched both cwd and session_meta.timestamp to spawnedAtMs.
1091
+ return {
1092
+ ...(sessionHistory as any),
1093
+ lookup: args.historySessionId || (canBindFromLiveSession && boundProviderSessionId)
1094
+ ? 'session'
1095
+ : 'workspace',
1096
+ };
1097
+ }
1098
+
1099
+ function readLiveCodexWorkspaceNativeHistory(agentStr: string, args: {
1100
+ canonicalHistory?: ProviderModule['canonicalHistory'];
1101
+ workspace?: string;
1102
+ offset: number;
1103
+ limit: number;
1104
+ excludeRecentCount: number;
1105
+ historyBehavior?: ProviderModule['historyBehavior'];
1106
+ scripts?: ProviderScripts;
1107
+ }): (ReturnType<typeof readProviderChatHistory> & { lookup: 'workspace' }) | null {
1108
+ if (agentStr !== 'codex-cli') return null;
1109
+ const workspace = typeof args.workspace === 'string' ? args.workspace.trim() : '';
1110
+ if (!workspace) return null;
1111
+ const history = readProviderChatHistory(agentStr, {
1112
+ canonicalHistory: args.canonicalHistory,
1113
+ workspace,
1114
+ offset: args.offset,
1115
+ limit: args.limit,
1116
+ excludeRecentCount: args.excludeRecentCount,
1117
+ historyBehavior: args.historyBehavior,
1118
+ scripts: args.scripts as any,
1119
+ });
1120
+ return { ...(history as any), lookup: 'workspace' };
1121
+ }
1122
+
1123
+ // (A2.2) isNativeHistoryFreshEnough removed. The v1 freshness comparison
1124
+ // (native_newest vs pty_newest with a 5-minute mtime grace window) was the
1125
+ // direct cause of the plipping behaviour: PTY arrived every turn so native
1126
+ // looked stale by default. ChatSourceMachine never compares native vs PTY
1127
+ // freshness — the lock holds across arbitrary PTY arrival. See
1128
+ // chat/source-machine.ts for the new semantics.
1129
+
1130
+ function shouldPreserveReadChatPayloadField(key: string): boolean {
1131
+ return key === 'messageSource' || key === 'transcriptProvenance';
1132
+ }
1133
+
1134
+ function updateMessageSourceReturnedCount(value: unknown, returnedMessageCount: number): unknown {
1135
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return value;
1136
+ const record = value as Record<string, unknown>;
1137
+ const coverage = record.coverage && typeof record.coverage === 'object' && !Array.isArray(record.coverage)
1138
+ ? record.coverage as Record<string, unknown>
1139
+ : undefined;
1140
+ if (!coverage) return value;
1141
+ return {
1142
+ ...record,
1143
+ coverage: {
1144
+ ...coverage,
1145
+ returnedMessageCount,
1146
+ },
1147
+ };
1148
+ }
1149
+
1150
+ function deriveHistoryDedupKey(message: ChatMessage & { _unitKey?: string; _turnKey?: string }): string | undefined {
1151
+ const unitKey = typeof message._unitKey === 'string' ? message._unitKey.trim() : '';
1152
+ if (unitKey) return `read_chat:${unitKey}`;
1153
+
1154
+ const turnKey = typeof message._turnKey === 'string' ? message._turnKey.trim() : '';
1155
+ if (!turnKey) return undefined;
1156
+
1157
+ let content = '';
1158
+ try {
1159
+ content = JSON.stringify(message.content ?? '');
1160
+ } catch {
1161
+ content = String(message.content ?? '');
1162
+ }
1163
+ return `read_chat:${turnKey}:${String(message.role || '').toLowerCase()}:${content}`;
1164
+ }
1165
+
1166
+ function toHistoryPersistedMessages(messages: ChatMessage[]): Array<{
1167
+ role: string;
1168
+ content: string;
1169
+ receivedAt?: number;
1170
+ kind?: string;
1171
+ senderName?: string;
1172
+ historyDedupKey?: string;
1173
+ }> {
1174
+ return messages.map((message) => ({
1175
+ role: message.role,
1176
+ content: flattenContent(message.content),
1177
+ receivedAt: typeof message.receivedAt === 'number' ? message.receivedAt : undefined,
1178
+ kind: typeof message.kind === 'string' ? message.kind : undefined,
1179
+ senderName: typeof message.senderName === 'string' ? message.senderName : undefined,
1180
+ historyDedupKey: deriveHistoryDedupKey(message as ChatMessage & { _unitKey?: string; _turnKey?: string }),
1181
+ }));
1182
+ }
1183
+
1184
+ function buildFullTail(messages: ChatMessage[], tailLimit: number): {
1185
+ messages: ChatMessage[];
1186
+ totalMessages: number;
1187
+ } {
1188
+ const totalMessages = messages.length;
1189
+ const tailMessages = tailLimit > 0 ? messages.slice(-tailLimit) : messages;
1190
+ return {
1191
+ messages: tailMessages,
1192
+ totalMessages,
1193
+ };
1194
+ }
1195
+
1196
+ function hasNonEmptyModalButtons(activeModal: unknown): boolean {
1197
+ if (!activeModal || typeof activeModal !== 'object') return false;
1198
+ const buttons = (activeModal as { buttons?: unknown }).buttons;
1199
+ return Array.isArray(buttons) && buttons.some((button) => typeof button === 'string' && button.trim().length > 0);
1200
+ }
1201
+
1202
+ function normalizeReadChatCommandStatus(status: unknown, activeModal: unknown): string {
1203
+ const raw = typeof status === 'string' ? status.trim() : '';
1204
+ if (!raw) {
1205
+ return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'idle';
1206
+ }
1207
+ switch (raw) {
1208
+ case 'starting':
1209
+ return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'starting';
1210
+ case 'stopped':
1211
+ case 'disconnected':
1212
+ case 'not_monitored':
1213
+ return 'error';
1214
+ case 'waiting_approval':
1215
+ // The contract validator requires activeModal+buttons whenever
1216
+ // status is waiting_approval. If a producer/coercer set this
1217
+ // status without staging the modal yet (a race we hit with
1218
+ // codex-cli during tool approval setup), downgrade to a
1219
+ // generating-like status so readChat still returns successfully.
1220
+ // The next poll will pick up the modal once the provider has
1221
+ // emitted it.
1222
+ return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'generating';
1223
+ default:
1224
+ return raw;
1225
+ }
1226
+ }
1227
+
1228
+ function isGeneratingLikeStatus(status: unknown): boolean {
1229
+ return status === 'generating' || status === 'streaming' || status === 'no_progress' || status === 'long_generating' || status === 'starting';
1230
+ }
1231
+
1232
+ function hasVisibleAssistantMessage(messages: unknown[] | undefined): boolean {
1233
+ if (!Array.isArray(messages)) return false;
1234
+ return messages.some((message: any) => {
1235
+ if (!message || message.role !== 'assistant') return false;
1236
+ const kind = typeof message.kind === 'string' ? message.kind : 'standard';
1237
+ if (kind !== 'standard') return false;
1238
+ return String(message.content || '').trim().length > 0;
1239
+ });
1240
+ }
1241
+
1242
+ function hasFinalVisibleAssistantMessage(messages: unknown[] | undefined): boolean {
1243
+ if (!Array.isArray(messages)) return false;
1244
+ const visible = filterUserFacingChatMessages(messages as ChatMessage[]);
1245
+ const last = visible[visible.length - 1] as ChatMessage | undefined;
1246
+ const role = typeof last?.role === 'string' ? last.role.trim().toLowerCase() : '';
1247
+ const content = last ? flattenContent(last.content).trim() : '';
1248
+ return (role === 'assistant' || role === 'model') && content.length > 0;
1249
+ }
1250
+
1251
+ function shouldTrustCliAdapterTerminalStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): boolean {
1252
+ if (!isGeneratingLikeStatus(parsedStatus)) return false;
1253
+ if (hasNonEmptyModalButtons(activeModal)) return false;
1254
+ const adapterRawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
1255
+ if (adapterRawStatus !== 'idle') return false;
1256
+ if (typeof adapter.isProcessing === 'function' && adapter.isProcessing()) return false;
1257
+ return true;
1258
+ }
1259
+
1260
+ function normalizeCliReadChatStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any, parsedMessages?: unknown[]): string {
1261
+ const adapterRawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
1262
+ if (adapterRawStatus === 'starting'
1263
+ && isGeneratingLikeStatus(parsedStatus)
1264
+ && !hasNonEmptyModalButtons(activeModal)
1265
+ && Array.isArray(parsedMessages)
1266
+ && parsedMessages.length === 0
1267
+ && Array.isArray(adapterStatus?.messages)
1268
+ && adapterStatus.messages.length === 0
1269
+ && !(typeof adapter.isProcessing === 'function' && adapter.isProcessing())) {
1270
+ return 'starting';
1271
+ }
1272
+ if (
1273
+ isGeneratingLikeStatus(adapterRawStatus)
1274
+ && parsedStatus === 'idle'
1275
+ && !hasNonEmptyModalButtons(activeModal)
1276
+ && !hasVisibleAssistantMessage(parsedMessages)
1277
+ ) {
1278
+ return adapterRawStatus;
1279
+ }
1280
+ if (shouldTrustCliAdapterTerminalStatus(parsedStatus, activeModal, adapter, adapterStatus)) return 'idle';
1281
+ return typeof parsedStatus === 'string' && parsedStatus.trim() ? parsedStatus : 'idle';
1282
+ }
1283
+
1284
+ function finalizeStreamingMessagesWhenIdle(messages: ChatMessage[], status: string): ChatMessage[] {
1285
+ if (status !== 'idle') return messages;
1286
+ return messages.map((message) => {
1287
+ const meta = message.meta && typeof message.meta === 'object'
1288
+ ? message.meta as Record<string, unknown>
1289
+ : undefined;
1290
+ const hasStreamingMeta = meta?.streaming === true;
1291
+ if (message.bubbleState !== 'streaming' && !hasStreamingMeta) return message;
1292
+ return {
1293
+ ...message,
1294
+ ...(message.bubbleState === 'streaming' ? { bubbleState: 'final' as const } : {}),
1295
+ ...(hasStreamingMeta ? { meta: { ...meta, streaming: false } } : {}),
1296
+ };
1297
+ });
1298
+ }
1299
+
1300
+ /**
1301
+ * Collapse adjacent PTY messages whose canonical (whitespace-stripped)
1302
+ * content is identical, OR whose turn key + role/kind match.
1303
+ *
1304
+ * The PTY parser of some providers (hermes-cli observed in the wild)
1305
+ * emits the same logical assistant turn twice when the terminal re-wraps
1306
+ * the text at a different column. The two emissions differ in newline
1307
+ * position — and sometimes in a single inserted space next to punctuation
1308
+ * (e.g. `(수정 2개), upstream` vs `(수정 2개 ), upstream`), so a simple
1309
+ * `\s+ -> ' '` normalize cannot collapse them.
1310
+ *
1311
+ * Strategy:
1312
+ * 1. If both messages carry the same _turnKey + role + kind, they are
1313
+ * the same logical turn by construction. Collapse.
1314
+ * 2. Otherwise compare with all whitespace stripped — wrap variants
1315
+ * collapse to identical strings.
1316
+ *
1317
+ * Native-history paths run through pageHistoryRecords and already
1318
+ * collapse on a normalized signature; this helper is the PTY equivalent
1319
+ * the readChat sync path was missing.
1320
+ */
1321
+ function collapseAdjacentDuplicateChatMessages(messages: ChatMessage[]): ChatMessage[] {
1322
+ if (!Array.isArray(messages) || messages.length <= 1) return messages;
1323
+ const result: ChatMessage[] = [];
1324
+ let prevRoleKind = '';
1325
+ let prevStripped = '';
1326
+ for (const message of messages) {
1327
+ const role = typeof message.role === 'string' ? message.role : '';
1328
+ const kind = typeof message.kind === 'string' ? message.kind : 'standard';
1329
+ const content = typeof message.content === 'string'
1330
+ ? message.content
1331
+ : (Array.isArray(message.content) ? message.content.map((p: any) => typeof p?.text === 'string' ? p.text : '').join('') : '');
1332
+ const strippedContent = content.replace(/\s+/g, '');
1333
+ // Empty content or system messages are passed through untouched.
1334
+ if (!strippedContent || role === 'system') {
1335
+ result.push(message);
1336
+ prevRoleKind = '';
1337
+ prevStripped = '';
1338
+ continue;
1339
+ }
1340
+ const roleKind = `${role}:${kind}`;
1341
+ const sameStripped = strippedContent === prevStripped && roleKind === prevRoleKind;
1342
+ if (result.length > 0 && sameStripped) {
1343
+ // Adjacent duplicate after stripping all whitespace. Keep the
1344
+ // *later* copy because PTY's last emission usually has the most
1345
+ // complete formatting.
1346
+ result[result.length - 1] = message;
1347
+ prevRoleKind = roleKind;
1348
+ prevStripped = strippedContent;
1349
+ continue;
1350
+ }
1351
+ result.push(message);
1352
+ prevRoleKind = roleKind;
1353
+ prevStripped = strippedContent;
1354
+ }
1355
+ return result;
1356
+ }
1357
+
1358
+ function buildReadChatCommandResult(payload: Record<string, any>, args: any, h?: CommandHelpers): CommandResult {
1359
+ let validatedPayload: Record<string, any>;
1360
+ const debugReadChat = payload?.debugReadChat && typeof payload.debugReadChat === 'object'
1361
+ ? payload.debugReadChat
1362
+ : undefined;
1363
+ try {
1364
+ validatedPayload = validateReadChatResultPayload({
1365
+ ...payload,
1366
+ status: normalizeReadChatCommandStatus(payload?.status, payload?.activeModal),
1367
+ }, 'read_chat command result') as Record<string, any>;
1368
+ } catch (error: any) {
1369
+ return { success: false, error: error?.message || String(error) };
1370
+ }
1371
+ const messages = normalizeReadChatMessages(validatedPayload);
1372
+ // Last-mile coordinator-prompt filter. Different read_chat code paths
1373
+ // produce the final messages array (native-history main path, codex
1374
+ // exact-runtime-mirror fallback, daemon-side pty-parser, etc), so
1375
+ // applying it here means we don't have to thread the filter through
1376
+ // every one. Driven by the provider setting `showCoordinatorSystemPrompt`
1377
+ // + the coordinator-registry entry for the target session.
1378
+ const sessionIdHint = typeof args?.targetSessionId === 'string' ? args.targetSessionId
1379
+ : typeof args?.sessionId === 'string' ? args.sessionId
1380
+ : '';
1381
+ const providerHint = typeof args?.cliType === 'string' ? args.cliType
1382
+ : typeof args?.providerType === 'string' ? args.providerType
1383
+ : typeof args?.agentType === 'string' ? args.agentType
1384
+ : '';
1385
+ const filteredMessages = h
1386
+ ? maybeHideCoordinatorPromptMessage(h, providerHint, sessionIdHint, messages)
1387
+ : messages;
1388
+ // By default read_chat returns only user-facing prose turns. When the
1389
+ // caller opts in with `includeActivity`, tool/terminal/thought activity
1390
+ // bubbles (e.g. the native transcript's tool calls and results) are kept
1391
+ // inline too, in chronological order, so a restored conversation can show
1392
+ // what the agent actually did — not just the prose around it.
1393
+ const includeActivity = args?.includeActivity === true || args?.includeActivity === 'true';
1394
+ const visibleMessages = includeActivity
1395
+ ? filteredMessages.filter((m) => isUserFacingChatMessage(m) || isActivityChatMessage(m))
1396
+ : filterUserFacingChatMessages(filteredMessages);
1397
+ const sync = buildFullTail(visibleMessages, normalizeReadChatTailLimit(args));
1398
+ const hiddenMsgCount = Math.max(0, messages.length - visibleMessages.length);
1399
+ const preservedPayloadFields = Object.fromEntries(Object.entries(payload).filter(([key]) => shouldPreserveReadChatPayloadField(key)));
1400
+ if (preservedPayloadFields.messageSource) {
1401
+ preservedPayloadFields.messageSource = updateMessageSourceReturnedCount(preservedPayloadFields.messageSource, sync.messages.length);
1402
+ }
1403
+ if (preservedPayloadFields.transcriptProvenance) {
1404
+ preservedPayloadFields.transcriptProvenance = updateMessageSourceReturnedCount(preservedPayloadFields.transcriptProvenance, sync.messages.length);
1405
+ }
1406
+ const returnedDebugReadChat = debugReadChat
1407
+ ? {
1408
+ ...debugReadChat,
1409
+ fullMsgCount: typeof debugReadChat.fullMsgCount === 'number'
1410
+ ? debugReadChat.fullMsgCount
1411
+ : messages.length,
1412
+ visibleMsgCount: visibleMessages.length,
1413
+ hiddenMsgCount,
1414
+ returnedMsgCount: sync.messages.length,
1415
+ }
1416
+ : undefined;
1417
+ return {
1418
+ success: true,
1419
+ ...validatedPayload,
1420
+ ...preservedPayloadFields,
1421
+ messages: sync.messages,
1422
+ totalMessages: sync.totalMessages,
1423
+ ...(returnedDebugReadChat ? { debugReadChat: returnedDebugReadChat } : {}),
1424
+ };
1425
+ }
1426
+
1427
+
1428
+ function toNonNegativeNumber(value: any): number {
1429
+ const numeric = Number(value ?? 0);
1430
+ return Number.isFinite(numeric) ? Math.max(0, numeric) : 0;
1431
+ }
1432
+
1433
+ function getCliVisibleTranscriptCount(adapter: any): number {
1434
+ if (typeof adapter?.getScriptParsedStatus !== 'function') return 0;
1435
+ try {
1436
+ const parsed = parseMaybeJson(adapter.getScriptParsedStatus());
1437
+ return Array.isArray(parsed?.messages) ? parsed.messages.length : 0;
1438
+ } catch {
1439
+ return 0;
1440
+ }
1441
+ }
1442
+
1443
+ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<CommandResult> {
1444
+ const { agentType, offset, limit } = args;
1445
+ const historySessionId = getHistorySessionId(h, args);
1446
+ try {
1447
+ const provider = h.getProvider(agentType);
1448
+ const agentStr = provider?.type || agentType || getCurrentProviderType(h);
1449
+ const transport = getTargetTransport(h, provider);
1450
+ const hasExplicitExcludeRecentCount = args?.excludeRecentCount !== undefined && args?.excludeRecentCount !== null;
1451
+ let excludeRecentCount = toNonNegativeNumber(args?.excludeRecentCount);
1452
+ if (!hasExplicitExcludeRecentCount && isCliLikeTransport(transport)) {
1453
+ const adapter = getTargetedCliAdapter(h, args, provider?.type);
1454
+ const visibleCount = getCliVisibleTranscriptCount(adapter);
1455
+ if (visibleCount > excludeRecentCount) excludeRecentCount = visibleCount;
1456
+ }
1457
+ const workspace = typeof args?.workspace === 'string'
1458
+ ? args.workspace
1459
+ : typeof (h.currentSession as any)?.workspace === 'string'
1460
+ ? (h.currentSession as any).workspace
1461
+ : undefined;
1462
+ const exactNativeHistoryScope = Boolean(
1463
+ (typeof args?.targetSessionId === 'string' && args.targetSessionId.trim())
1464
+ || (typeof args?.historySessionId === 'string' && args.historySessionId.trim())
1465
+ || (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
1466
+ );
1467
+ const result = supportsCliNativeTranscript(agentStr, provider) && isNativeSourceCanonicalHistory(provider?.nativeHistory)
1468
+ ? readCliProviderNativeHistory(agentStr, {
1469
+ canonicalHistory: provider?.nativeHistory,
1470
+ historySessionId,
1471
+ workspace,
1472
+ offset: offset || 0,
1473
+ limit: limit || 30,
1474
+ excludeRecentCount,
1475
+ historyBehavior: provider?.historyBehavior,
1476
+ scripts: provider?.scripts as any,
1477
+ sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
1478
+ envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
1479
+ })
1480
+ : readProviderChatHistory(agentStr, {
1481
+ canonicalHistory: provider?.nativeHistory,
1482
+ historySessionId,
1483
+ workspace,
1484
+ offset: offset || 0,
1485
+ limit: limit || 30,
1486
+ excludeRecentCount,
1487
+ historyBehavior: provider?.historyBehavior,
1488
+ scripts: provider?.scripts as any,
1489
+ });
1490
+ if (supportsCliNativeTranscript(agentStr, provider) && isNativeSourceCanonicalHistory(provider?.nativeHistory)) {
1491
+ const lookup = (result as any).lookup === 'workspace' ? 'workspace' : 'session';
1492
+ const messages = Array.isArray((result as any).messages)
1493
+ ? normalizeAndFilterNativeHistory(h, agentStr, args, (result as any).messages as ChatMessage[], (result as any)?.providerSessionId)
1494
+ : [];
1495
+ const historyProviderSessionId = typeof (result as any)?.providerSessionId === 'string'
1496
+ ? (result as any).providerSessionId
1497
+ : readHistorySessionIdFromMessages(messages) || historySessionId;
1498
+ const safeMapping = hasSafeNativeHistoryMapping({
1499
+ historySessionId: lookup === 'workspace' ? undefined : historySessionId,
1500
+ providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
1501
+ workspace,
1502
+ nativeMessages: messages,
1503
+ });
1504
+ if ((result as any).source === 'provider-native' && messages.length > 0 && !safeMapping) {
1505
+ return {
1506
+ success: true,
1507
+ messages: [],
1508
+ hasMore: false,
1509
+ source: 'native-unavailable',
1510
+ agent: agentStr,
1511
+ };
1512
+ }
1513
+ }
1514
+ return { success: true, ...result, agent: agentStr };
1515
+ } catch (e: any) {
1516
+ return { success: false, error: e.message };
1517
+ }
1518
+ }
1519
+
1520
+ export async function handleReadChat(h: CommandHelpers, args: any): Promise<CommandResult> {
1521
+ // Node scope guard: a daemon hosting a base node + several worktree nodes must
1522
+ // not serve worktree A's transcript (or splice sibling worktree turns via the
1523
+ // native-history-by-workspace fallback) when a coordinator scoped the read to
1524
+ // worktree B. mesh_read_chat always passes the requested node's workspace as
1525
+ // args.workspace; refuse a CONFIRMED cross-workspace read rather than mix.
1526
+ {
1527
+ const guardSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
1528
+ if (guardSessionId && typeof args?.workspace === 'string' && args.workspace.trim()) {
1529
+ const verdict = evaluateReadChatNodeWorkspaceScope({
1530
+ targetSessionId: guardSessionId,
1531
+ intendedWorkspace: args.workspace,
1532
+ sessionWorkspace: resolveTargetSessionActualWorkspace(h, guardSessionId),
1533
+ });
1534
+ if (verdict.scoped) {
1535
+ LOG.info('Command', `[read_chat] node scope mismatch: session ${guardSessionId} workspace "${verdict.actual}" ≠ requested node workspace "${verdict.intended}" — refusing cross-worktree transcript`);
1536
+ return {
1537
+ success: false,
1538
+ code: 'read_chat_session_node_scope_mismatch',
1539
+ error: `Session ${guardSessionId} belongs to a different worktree (workspace "${verdict.actual}") than the requested node (workspace "${verdict.intended}"). Refusing to return a cross-worktree transcript — target the node that owns this session.`,
1540
+ };
1541
+ }
1542
+ }
1543
+ }
1544
+ // Resolve provider in order: explicit agentType/providerType > registered session.
1545
+ // Without this fallback, callers that only have a sessionId (e.g. a chat tail
1546
+ // controller that just got handed a session ID over WS) get an empty result
1547
+ // because getProvider(undefined) returns undefined and the rest of the pipeline
1548
+ // bails. This makes the UI look like the session "disappeared".
1549
+ let providerHint: string | undefined = args?.agentType || args?.providerType;
1550
+ if (!providerHint) {
1551
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
1552
+ if (targetSessionId) {
1553
+ const session = (h.ctx as any)?.sessionRegistry?.get?.(targetSessionId);
1554
+ if (session && typeof session.providerType === 'string') {
1555
+ providerHint = session.providerType;
1556
+ }
1557
+ }
1558
+ if (!providerHint && h.currentSession?.providerType) {
1559
+ providerHint = h.currentSession.providerType;
1560
+ }
1561
+ }
1562
+ const provider = h.getProvider(providerHint);
1563
+ const transport = getTargetTransport(h, provider);
1564
+ const historySessionId = getHistorySessionId(h, args);
1565
+
1566
+ const _log = (msg: string) => LOG.debug('Command', `[read_chat] ${msg}`);
1567
+
1568
+ // PTY / ACP transport: read from adapter
1569
+ if (isCliLikeTransport(transport)) {
1570
+ const adapter = getTargetedCliAdapter(h, args, provider?.type);
1571
+ if (adapter) {
1572
+ _log(`${transport} adapter: ${adapter.cliType}`);
1573
+ if (typeof adapter.getScriptParsedStatus !== 'function') {
1574
+ return { success: false, error: `${transport} adapter parseSession unavailable` };
1575
+ }
1576
+ let parsedStatus: any = null;
1577
+ try {
1578
+ parsedStatus = parseMaybeJson(adapter.getScriptParsedStatus());
1579
+ } catch (error: any) {
1580
+ return { success: false, error: error?.message || String(error) };
1581
+ }
1582
+ const parsedRecord = parsedStatus && typeof parsedStatus === 'object'
1583
+ ? parsedStatus as Record<string, any>
1584
+ : null;
1585
+ if (!parsedRecord || !Array.isArray(parsedRecord.messages)) {
1586
+ return { success: false, error: `${transport} parser did not return messages` };
1587
+ }
1588
+ const adapterStatus = typeof adapter.getStatus === 'function'
1589
+ ? adapter.getStatus()
1590
+ : {};
1591
+ const title = typeof parsedRecord.title === 'string' ? parsedRecord.title : undefined;
1592
+ const providerSessionId = typeof parsedRecord.providerSessionId === 'string'
1593
+ ? parsedRecord.providerSessionId
1594
+ : undefined;
1595
+ const transcriptAuthority = parsedRecord.transcriptAuthority === 'provider' || parsedRecord.transcriptAuthority === 'daemon'
1596
+ ? parsedRecord.transcriptAuthority
1597
+ : undefined;
1598
+ const coverage = parsedRecord.coverage === 'full' || parsedRecord.coverage === 'tail' || parsedRecord.coverage === 'current-turn'
1599
+ ? parsedRecord.coverage
1600
+ : undefined;
1601
+ const activeModal = parsedRecord.activeModal ?? parsedRecord.modal ?? null;
1602
+ const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus, parsedRecord.messages);
1603
+ const runtimeMessageMerger = getTargetInstance(h, args) as RuntimeChatMessageMerger | null;
1604
+ const parsedMessages = collapseAdjacentDuplicateChatMessages(
1605
+ finalizeStreamingMessagesWhenIdle(parsedRecord.messages as ChatMessage[], returnedStatus),
1606
+ );
1607
+ const returnedMessages = runtimeMessageMerger?.category === 'cli'
1608
+ && runtimeMessageMerger.type === adapter.cliType
1609
+ && typeof runtimeMessageMerger.mergeRuntimeChatMessages === 'function'
1610
+ ? runtimeMessageMerger.mergeRuntimeChatMessages(parsedMessages)
1611
+ : parsedMessages;
1612
+ const providerType = provider?.type || adapter.cliType;
1613
+ let selectedMessages = returnedMessages;
1614
+ let selectedTitle = title;
1615
+ let selectedProviderSessionId = providerSessionId;
1616
+ let selectedTranscriptAuthority = transcriptAuthority;
1617
+ let selectedCoverage = coverage;
1618
+ let selectedStatus = returnedStatus;
1619
+ const _targetSidForWs = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
1620
+ const _registryWs = _targetSidForWs
1621
+ ? (h.ctx?.sessionRegistry?.get?.(_targetSidForWs) as any)?.workspace
1622
+ : undefined;
1623
+ const _currentSessionWs = typeof (h.currentSession as any)?.workspace === 'string'
1624
+ ? (h.currentSession as any).workspace
1625
+ : typeof adapter.workingDir === 'string'
1626
+ ? adapter.workingDir
1627
+ : undefined;
1628
+ const sessionWorkspace = _targetSidForWs
1629
+ ? (typeof _registryWs === 'string' ? _registryWs : (typeof args?.workspace === 'string' ? args.workspace : undefined) ?? _currentSessionWs)
1630
+ : _currentSessionWs;
1631
+ const intendedWorkspace = typeof args?.workspace === 'string' ? args.workspace : undefined;
1632
+ // ───────────────────────────────────────────────────────────
1633
+ // Chat source decision via ChatSourceMachine (A2 big-bang).
1634
+ // Replaces the ~300-line if-ladder that mixed source decision
1635
+ // with native fetch, anchor mutation, and runtime mirror
1636
+ // selection. The machine decides only between native-history
1637
+ // and pty-parser; downstream selection of which message array
1638
+ // to surface stays here.
1639
+ //
1640
+ // Behavioural changes vs v1:
1641
+ // - No more nativeHistoryAnchoredAt mutation on the adapter.
1642
+ // Lock state lives in CHAT_SOURCE_REGISTRY keyed by
1643
+ // (providerType, sessionId).
1644
+ // - No PTY-vs-native freshness comparison. The lock holds
1645
+ // across arbitrary PTY arrival; only native regression /
1646
+ // unavailability unlocks. This is the plipping fix.
1647
+ // - 6 trigger strings (native_history_partial / _stale /
1648
+ // _not_safely_mapped / _empty / _error / _unavailable)
1649
+ // collapse to 3 events with diagnostic causes preserved
1650
+ // and mapped back to legacy fallbackReason strings for
1651
+ // response compatibility.
1652
+ // - Codex live-workspace native probe and unsafe-native
1653
+ // daemon mirror fallbacks are preserved as additional
1654
+ // input rounds to the machine; they were never the source
1655
+ // decision itself, they were retries.
1656
+ // ───────────────────────────────────────────────────────────
1657
+
1658
+ const supportsNative = supportsCliNativeTranscript(providerType, provider)
1659
+ && isNativeSourceCanonicalHistory(provider?.nativeHistory);
1660
+ const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h, adapter.cliType);
1661
+ const workspace = sessionWorkspace;
1662
+ const nativeHistoryLimit = Math.max(
1663
+ normalizeReadChatTailLimit(args) || 0,
1664
+ returnedMessages.length,
1665
+ HOT_TAIL_MIN_LIMIT,
1666
+ );
1667
+ const nativeHistorySessionId = supportsNative
1668
+ ? resolveCliNativeHistorySessionId(args, historySessionId, providerSessionId)
1669
+ : undefined;
1670
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
1671
+ const skipLiveNativeHistoryWithoutProviderSession = shouldSkipLiveCliNativeHistoryWithoutProviderSession({
1672
+ adapter,
1673
+ providerType,
1674
+ readChatArgs: args,
1675
+ nativeHistorySessionId,
1676
+ parsedProviderSessionId: providerSessionId,
1677
+ });
1678
+ const nativeHistoryReadSessionId = skipLiveNativeHistoryWithoutProviderSession
1679
+ ? undefined
1680
+ : nativeHistorySessionId;
1681
+ const exactNativeHistoryScope = Boolean(
1682
+ (typeof args?.historySessionId === 'string' && args.historySessionId.trim())
1683
+ || (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
1684
+ || providerSessionId
1685
+ || (nativeHistoryReadSessionId && nativeHistoryReadSessionId !== targetSessionId)
1686
+ || ((h.currentSession as any)?.sessionId === args?.targetSessionId && typeof (h.currentSession as any)?.providerSessionId === 'string' && (h.currentSession as any).providerSessionId.trim())
1687
+ );
1688
+
1689
+ // 1. Fetch native history (or skip if provider does not support it).
1690
+ let nativeHistory: any | null = null;
1691
+ let nativeHistoryError: unknown | undefined;
1692
+ if (supportsNative) {
1693
+ try {
1694
+ nativeHistory = readCliProviderNativeHistory(agentStr, {
1695
+ canonicalHistory: provider?.nativeHistory,
1696
+ historySessionId: nativeHistoryReadSessionId,
1697
+ workspace,
1698
+ offset: 0,
1699
+ limit: nativeHistoryLimit,
1700
+ excludeRecentCount: 0,
1701
+ historyBehavior: provider?.historyBehavior,
1702
+ scripts: provider?.scripts as any,
1703
+ excludeInProgressTurn: returnedStatus === 'waiting_approval',
1704
+ sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
1705
+ envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
1706
+ });
1707
+ } catch (error: any) {
1708
+ nativeHistoryError = error;
1709
+ nativeHistory = null;
1710
+ }
1711
+ }
1712
+
1713
+ // 2. Compute safeMapping with the same rules the v1 code used so the
1714
+ // machine sees the same observation it always would have.
1715
+ let nativeMessages: ChatMessage[] = nativeHistory && Array.isArray(nativeHistory.messages)
1716
+ ? normalizeAndFilterNativeHistory(h, agentStr, args, nativeHistory.messages as ChatMessage[], nativeHistory.providerSessionId)
1717
+ : [];
1718
+ const sessionStartedAtMs = sessionStartedAtMsFromRegistry(h, args?.targetSessionId);
1719
+ let historyProviderSessionId = typeof nativeHistory?.providerSessionId === 'string'
1720
+ ? nativeHistory.providerSessionId
1721
+ : readHistorySessionIdFromMessages(nativeMessages) || nativeHistoryReadSessionId || historySessionId;
1722
+ let lookup = nativeHistory?.lookup === 'workspace' ? 'workspace' : 'session';
1723
+ let nativeHistorySessionForMapping = adapter.cliType === 'antigravity-cli'
1724
+ && historyProviderSessionId
1725
+ && nativeHistoryReadSessionId
1726
+ && historyProviderSessionId !== nativeHistoryReadSessionId
1727
+ ? undefined
1728
+ : nativeHistoryReadSessionId;
1729
+ let safeMapping = supportsNative && nativeHistory
1730
+ ? hasSafeNativeHistoryMapping({
1731
+ historySessionId: lookup === 'workspace' ? undefined : nativeHistorySessionForMapping,
1732
+ providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId || providerSessionId,
1733
+ workspace,
1734
+ nativeMessages,
1735
+ ptyMessages: returnedMessages,
1736
+ requireWorkspaceContentOverlap: lookup === 'workspace' && !exactNativeHistoryScope,
1737
+ })
1738
+ : false;
1739
+ if (skipLiveNativeHistoryWithoutProviderSession && (!safeMapping || returnedMessages.length === 0)) {
1740
+ nativeHistory = null;
1741
+ nativeMessages = [];
1742
+ historyProviderSessionId = undefined;
1743
+ lookup = 'session';
1744
+ safeMapping = false;
1745
+ }
1746
+ const mayRetryUnsafeAutoDetectedCodexSession = adapter.cliType === 'codex-cli'
1747
+ && !getExplicitHistorySessionId(args)
1748
+ && Boolean(sessionStartedAtMs && sessionStartedAtMs > 0)
1749
+ && !skipLiveNativeHistoryWithoutProviderSession
1750
+ && !safeMapping;
1751
+ if (mayRetryUnsafeAutoDetectedCodexSession) {
1752
+ try {
1753
+ nativeHistory = readCliProviderNativeHistory(agentStr, {
1754
+ canonicalHistory: provider?.nativeHistory,
1755
+ historySessionId: undefined,
1756
+ workspace,
1757
+ offset: 0,
1758
+ limit: nativeHistoryLimit,
1759
+ excludeRecentCount: 0,
1760
+ historyBehavior: provider?.historyBehavior,
1761
+ scripts: provider?.scripts as any,
1762
+ excludeInProgressTurn: returnedStatus === 'waiting_approval',
1763
+ sessionStartedAtMs,
1764
+ envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
1765
+ });
1766
+ nativeHistoryError = undefined;
1767
+ nativeMessages = nativeHistory && Array.isArray(nativeHistory.messages)
1768
+ ? normalizeAndFilterNativeHistory(h, agentStr, args, nativeHistory.messages as ChatMessage[], nativeHistory.providerSessionId)
1769
+ : [];
1770
+ historyProviderSessionId = typeof nativeHistory?.providerSessionId === 'string'
1771
+ ? nativeHistory.providerSessionId
1772
+ : readHistorySessionIdFromMessages(nativeMessages);
1773
+ lookup = nativeHistory?.lookup === 'workspace' ? 'workspace' : 'session';
1774
+ nativeHistorySessionForMapping = undefined;
1775
+ safeMapping = supportsNative && nativeHistory
1776
+ ? hasSafeNativeHistoryMapping({
1777
+ historySessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
1778
+ providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
1779
+ workspace,
1780
+ nativeMessages,
1781
+ ptyMessages: returnedMessages,
1782
+ requireWorkspaceContentOverlap: lookup === 'workspace' && !exactNativeHistoryScope,
1783
+ })
1784
+ : false;
1785
+ } catch (error: any) {
1786
+ nativeHistoryError = error;
1787
+ nativeHistory = null;
1788
+ nativeMessages = [];
1789
+ historyProviderSessionId = undefined;
1790
+ safeMapping = false;
1791
+ }
1792
+ }
1793
+ const trustedExactNativeIdentity = lookup !== 'workspace'
1794
+ && Boolean(nativeHistoryReadSessionId)
1795
+ && Boolean(historyProviderSessionId)
1796
+ && nativeHistoryReadSessionId === historyProviderSessionId;
1797
+
1798
+ // 3. Drive ChatSourceMachine — one observation per readChat call,
1799
+ // keyed by (providerType, sessionKey-for-this-call). targetSessionId
1800
+ // is the most specific session anchor we have; fall back to
1801
+ // historySessionId so we never leak state across distinct sessions.
1802
+ const machineSessionKey = String(
1803
+ args?.targetSessionId
1804
+ || providerSessionId
1805
+ || historySessionId
1806
+ || (h.currentSession as any)?.sessionId
1807
+ || ''
1808
+ );
1809
+ const primary = decideCliReadChatSource({
1810
+ providerType,
1811
+ provider,
1812
+ sessionId: machineSessionKey,
1813
+ nativeHistoryResult: nativeHistory,
1814
+ nativeHistoryError,
1815
+ safeMapping,
1816
+ trustedExactNativeIdentity,
1817
+ sessionWorkspace,
1818
+ intendedWorkspace,
1819
+ ptyMessages: returnedMessages,
1820
+ // Start with PTY visible; decideCliReadChatSource flips this
1821
+ // to true when the machine actually selects native-history.
1822
+ ptyStatusApprovalOnly: false,
1823
+ });
1824
+ let messageSource: Record<string, unknown> = primary.messageSource;
1825
+
1826
+ if (primary.nativeSelected) {
1827
+ selectedMessages = finalizeStreamingMessagesWhenIdle(primary.nativeMessages, returnedStatus);
1828
+ selectedProviderSessionId = historyProviderSessionId || providerSessionId;
1829
+ selectedTranscriptAuthority = 'provider';
1830
+ selectedCoverage = nativeHistory?.hasMore ? 'tail' : 'full';
1831
+ if (selectedProviderSessionId && selectedProviderSessionId !== providerSessionId) {
1832
+ adapter.updateRuntimeMeta?.({ providerSessionId: selectedProviderSessionId });
1833
+ }
1834
+ } else if (supportsNative) {
1835
+ // Native not selected. Two preserved v1 fallbacks before settling
1836
+ // on PTY: (a) Codex-only live workspace native probe; (b) unsafe-
1837
+ // native daemon mirror selection. The machine sees each retry as
1838
+ // an additional observation.
1839
+ const liveCurrentRuntimePtySafe = isCurrentRuntimePtySafelyAttributed({
1840
+ adapter,
1841
+ helpers: h,
1842
+ readChatArgs: args,
1843
+ sessionWorkspace,
1844
+ intendedWorkspace,
1845
+ ptyMessages: returnedMessages,
1846
+ });
1847
+ const mayProbeLiveCodexWorkspaceNative = adapter.cliType === 'codex-cli'
1848
+ && liveCurrentRuntimePtySafe
1849
+ && !(typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
1850
+ && !(providerSessionId && providerSessionId.trim())
1851
+ && !nativeHistoryReadSessionId
1852
+ && (!historyProviderSessionId || historyProviderSessionId === nativeHistoryReadSessionId || historyProviderSessionId === historySessionId)
1853
+ && !skipLiveNativeHistoryWithoutProviderSession;
1854
+ const liveWorkspaceNativeHistory = mayProbeLiveCodexWorkspaceNative
1855
+ ? readLiveCodexWorkspaceNativeHistory(agentStr, {
1856
+ canonicalHistory: provider?.nativeHistory,
1857
+ workspace,
1858
+ offset: 0,
1859
+ limit: nativeHistoryLimit,
1860
+ excludeRecentCount: 0,
1861
+ historyBehavior: provider?.historyBehavior,
1862
+ scripts: provider?.scripts as any,
1863
+ })
1864
+ : null;
1865
+ const liveWorkspaceNativeMessages = Array.isArray((liveWorkspaceNativeHistory as any)?.messages)
1866
+ ? normalizeAndFilterNativeHistory(h, agentStr, args, (liveWorkspaceNativeHistory as any).messages as ChatMessage[], (liveWorkspaceNativeHistory as any)?.providerSessionId)
1867
+ : [];
1868
+ const liveWorkspaceNativeProviderSessionId = typeof (liveWorkspaceNativeHistory as any)?.providerSessionId === 'string'
1869
+ ? (liveWorkspaceNativeHistory as any).providerSessionId
1870
+ : readHistorySessionIdFromMessages(liveWorkspaceNativeMessages);
1871
+ const liveWorkspaceNativeSafeMapping = liveWorkspaceNativeMessages.length > 0
1872
+ && hasSafeNativeHistoryMapping({
1873
+ workspace,
1874
+ nativeMessages: liveWorkspaceNativeMessages,
1875
+ ptyMessages: returnedMessages,
1876
+ requireWorkspaceContentOverlap: true,
1877
+ });
1878
+ if (liveWorkspaceNativeHistory) {
1879
+ const liveDecision = decideCliReadChatSource({
1880
+ providerType,
1881
+ provider,
1882
+ // Distinct session key so a transient codex live-probe does not
1883
+ // clobber the primary session's lock. The machine treats this
1884
+ // as its own session; the primary session's state is untouched.
1885
+ sessionId: `${machineSessionKey}::live-workspace`,
1886
+ nativeHistoryResult: liveWorkspaceNativeHistory,
1887
+ safeMapping: liveWorkspaceNativeSafeMapping,
1888
+ sessionWorkspace,
1889
+ intendedWorkspace,
1890
+ ptyMessages: returnedMessages,
1891
+ ptyStatusApprovalOnly: true,
1892
+ });
1893
+ if (liveDecision.nativeSelected) {
1894
+ selectedMessages = finalizeStreamingMessagesWhenIdle(liveDecision.nativeMessages, returnedStatus);
1895
+ selectedProviderSessionId = liveWorkspaceNativeProviderSessionId || providerSessionId;
1896
+ selectedTranscriptAuthority = 'provider';
1897
+ selectedCoverage = (liveWorkspaceNativeHistory as any).hasMore ? 'tail' : 'full';
1898
+ if (selectedProviderSessionId && selectedProviderSessionId !== providerSessionId) {
1899
+ adapter.updateRuntimeMeta?.({ providerSessionId: selectedProviderSessionId });
1900
+ }
1901
+ messageSource = liveDecision.messageSource;
1902
+ (messageSource as any).selectedDaemonSource = 'live-workspace-native-history';
1903
+ (messageSource as any).runtimeMappingSafe = true;
1904
+ } else {
1905
+ // Live probe also rejected: apply unsafe-native daemon mirror
1906
+ // selection (codex-only) using the primary decision's
1907
+ // fallbackReason.
1908
+ applyUnsafeNativeDaemonFallback({
1909
+ providerType,
1910
+ adapter,
1911
+ helpers: h,
1912
+ readChatArgs: args,
1913
+ sessionWorkspace,
1914
+ intendedWorkspace,
1915
+ ptyMessages: returnedMessages,
1916
+ nativeHistoryLimit,
1917
+ provider,
1918
+ messageSourceRef: { set(value) { messageSource = value; }, get() { return messageSource; } },
1919
+ apply(selection) {
1920
+ selectedMessages = selection.messages;
1921
+ selectedTranscriptAuthority = selection.transcriptAuthority;
1922
+ selectedCoverage = selection.coverage ?? coverage;
1923
+ selectedStatus = selection.status ?? returnedStatus;
1924
+ },
1925
+ activeModal,
1926
+ returnedStatus,
1927
+ coverage,
1928
+ });
1929
+ }
1930
+ } else {
1931
+ applyUnsafeNativeDaemonFallback({
1932
+ providerType,
1933
+ adapter,
1934
+ helpers: h,
1935
+ readChatArgs: args,
1936
+ sessionWorkspace,
1937
+ intendedWorkspace,
1938
+ ptyMessages: returnedMessages,
1939
+ nativeHistoryLimit,
1940
+ provider,
1941
+ messageSourceRef: { set(value) { messageSource = value; }, get() { return messageSource; } },
1942
+ apply(selection) {
1943
+ selectedMessages = selection.messages;
1944
+ selectedTranscriptAuthority = selection.transcriptAuthority;
1945
+ selectedCoverage = selection.coverage ?? coverage;
1946
+ selectedStatus = selection.status ?? returnedStatus;
1947
+ },
1948
+ activeModal,
1949
+ returnedStatus,
1950
+ coverage,
1951
+ });
1952
+ }
1953
+ }
1954
+ if (
1955
+ isGeneratingLikeStatus(selectedStatus)
1956
+ && selectedTranscriptAuthority === 'provider'
1957
+ && !hasNonEmptyModalButtons(activeModal)
1958
+ && hasFinalVisibleAssistantMessage(selectedMessages)
1959
+ ) {
1960
+ selectedStatus = 'idle';
1961
+ selectedMessages = finalizeStreamingMessagesWhenIdle(selectedMessages, selectedStatus);
1962
+ messageSource = {
1963
+ ...messageSource,
1964
+ statusReconciled: {
1965
+ from: returnedStatus,
1966
+ to: 'idle',
1967
+ reason: 'provider_native_final_assistant',
1968
+ },
1969
+ };
1970
+ }
1971
+ LOG.debug('Command', `[read_chat] cli-like parsed provider=${adapter.cliType} target=${String(args?.targetSessionId || '')} adapterStatus=${String(adapterStatus.status || '')} parsedStatus=${String(parsedRecord.status || '')} parsedMsgCount=${parsedRecord.messages.length} returnedMsgCount=${returnedMessages.length}`);
1972
+ return buildReadChatCommandResult({
1973
+ messages: selectedMessages,
1974
+ status: selectedStatus,
1975
+ activeModal,
1976
+ messageSource,
1977
+ transcriptProvenance: messageSource,
1978
+ debugReadChat: {
1979
+ provider: adapter.cliType,
1980
+ targetSessionId: String(args?.targetSessionId || ''),
1981
+ adapterStatus: String(adapterStatus.status || ''),
1982
+ parsedStatus: String(parsedRecord.status || ''),
1983
+ returnedStatus: String(selectedStatus || ''),
1984
+ selectedMessageSource: (messageSource as any).selected,
1985
+ messageSource,
1986
+ shouldPreferAdapterMessages: supportsCliNativeTranscript(providerType, provider)
1987
+ && isNativeSourceCanonicalHistory(provider?.nativeHistory)
1988
+ && (messageSource as any).selected !== 'native-history'
1989
+ && typeof (messageSource as any).fallbackReason === 'string'
1990
+ && (messageSource as any).fallbackReason.startsWith('native_history_')
1991
+ && (messageSource as any).fallbackReason !== 'native_history_not_checked'
1992
+ && !isUnsafeNativeTranscriptFallback((messageSource as any).fallbackReason)
1993
+ && !(selectedTranscriptAuthority === 'provider' && selectedCoverage === 'full'),
1994
+ parsedMsgCount: parsedRecord.messages.length,
1995
+ returnedMsgCount: selectedMessages.length,
1996
+ },
1997
+ ...(selectedTitle ? { title: selectedTitle } : {}),
1998
+ ...(selectedProviderSessionId ? { providerSessionId: selectedProviderSessionId } : {}),
1999
+ ...(selectedTranscriptAuthority ? { transcriptAuthority: selectedTranscriptAuthority } : {}),
2000
+ ...(selectedCoverage ? { coverage: selectedCoverage } : {}),
2001
+ }, args, h);
2002
+ }
2003
+ // History-only path (no adapter). Same source-decision contract as
2004
+ // the adapter path above, but with no PTY messages — the machine
2005
+ // simply decides whether native is usable; if not we return the
2006
+ // history we have plus a `native_history_not_safely_available`
2007
+ // error response when the provider requires native source.
2008
+ const historyLimit = normalizeReadChatTailLimit(args);
2009
+ try {
2010
+ const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h);
2011
+ const targetSid = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
2012
+ const registrySessionWorkspace = targetSid
2013
+ ? (h.ctx?.sessionRegistry?.get?.(targetSid) as any)?.workspace
2014
+ : undefined;
2015
+ const currentSessionWorkspace = typeof (h.currentSession as any)?.workspace === 'string'
2016
+ ? (h.currentSession as any).workspace
2017
+ : undefined;
2018
+ const argsWorkspace = typeof args?.workspace === 'string' ? args.workspace : undefined;
2019
+ // When reading a different session (targetSid), prefer that session's registered
2020
+ // workspace (or the caller-supplied args.workspace) over the current (coordinator)
2021
+ // session's workspace — otherwise the coordinator's cwd shadows the worker's cwd
2022
+ // and history lookups find the wrong files.
2023
+ const workspace = targetSid
2024
+ ? (typeof registrySessionWorkspace === 'string' ? registrySessionWorkspace : argsWorkspace ?? currentSessionWorkspace)
2025
+ : (typeof currentSessionWorkspace === 'string' ? currentSessionWorkspace : undefined);
2026
+ const intendedWorkspace = argsWorkspace;
2027
+ const supportsNative = supportsCliNativeTranscript(agentStr, provider)
2028
+ && isNativeSourceCanonicalHistory(provider?.nativeHistory);
2029
+ const history = supportsNative
2030
+ ? readCliProviderNativeHistory(agentStr, {
2031
+ canonicalHistory: provider?.nativeHistory,
2032
+ historySessionId,
2033
+ workspace,
2034
+ offset: 0,
2035
+ limit: historyLimit,
2036
+ excludeRecentCount: 0,
2037
+ historyBehavior: provider?.historyBehavior,
2038
+ scripts: provider?.scripts as any,
2039
+ sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
2040
+ envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
2041
+ })
2042
+ : readProviderChatHistory(agentStr, {
2043
+ canonicalHistory: provider?.nativeHistory,
2044
+ historySessionId,
2045
+ workspace,
2046
+ offset: 0,
2047
+ limit: historyLimit,
2048
+ excludeRecentCount: 0,
2049
+ historyBehavior: provider?.historyBehavior,
2050
+ scripts: provider?.scripts as any,
2051
+ });
2052
+ const lookup = (history as any)?.lookup === 'workspace' ? 'workspace' : 'session';
2053
+ const historyMessages = Array.isArray((history as any)?.messages)
2054
+ ? normalizeAndFilterNativeHistory(h, agentStr, args, (history as any).messages as ChatMessage[], (history as any)?.providerSessionId)
2055
+ : [];
2056
+ const historyProviderSessionId = typeof (history as any)?.providerSessionId === 'string'
2057
+ ? (history as any).providerSessionId
2058
+ : readHistorySessionIdFromMessages(historyMessages) || historySessionId;
2059
+ const safeMapping = supportsNative
2060
+ ? hasSafeNativeHistoryMapping({
2061
+ historySessionId: lookup === 'workspace' ? undefined : historySessionId,
2062
+ providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
2063
+ workspace,
2064
+ nativeMessages: historyMessages,
2065
+ })
2066
+ : false;
2067
+ const trustedExactNativeIdentity = lookup !== 'workspace'
2068
+ && Boolean(historySessionId)
2069
+ && Boolean(historyProviderSessionId)
2070
+ && historySessionId === historyProviderSessionId;
2071
+
2072
+ const machineSessionKey = String(
2073
+ args?.targetSessionId
2074
+ || historyProviderSessionId
2075
+ || historySessionId
2076
+ || (h.currentSession as any)?.sessionId
2077
+ || ''
2078
+ );
2079
+ const decision = decideCliReadChatSource({
2080
+ providerType: agentStr,
2081
+ provider,
2082
+ sessionId: machineSessionKey,
2083
+ nativeHistoryResult: history,
2084
+ safeMapping,
2085
+ trustedExactNativeIdentity,
2086
+ sessionWorkspace: workspace,
2087
+ intendedWorkspace,
2088
+ ptyMessages: [],
2089
+ ptyStatusApprovalOnly: false,
2090
+ });
2091
+
2092
+ if (supportsNative && !decision.nativeSelected) {
2093
+ // Dead-end: we are in the history-only path (no live PTY/ACP
2094
+ // adapter was found for this target session) AND provider-native
2095
+ // history is not safely mappable to the requested session
2096
+ // (no historySessionId stamp / workspace mismatch). Previously
2097
+ // this returned `success:false`, which the command logger emits
2098
+ // at warn level on EVERY poll (handler.ts logCommandEnd) —
2099
+ // mesh coordinators poll read_chat continuously, so a worker whose
2100
+ // transcript can never be safely mapped produced a 100% warn-log
2101
+ // storm with no recovery. Switch to a SOFT response: success with
2102
+ // empty messages + pending:true so the coordinator treats it as
2103
+ // "no live messages readable yet" rather than a hard failure, and
2104
+ // carry the machine-readable reason for debuggability. The normal
2105
+ // live-adapter path (above) and the safe-native return (below) are
2106
+ // unaffected — this is strictly the both-absent dead end.
2107
+ LOG.debug('Command', `[read_chat] soft pending: no live adapter and native history not safely mappable target=${String(args?.targetSessionId || '')} provider=${agentStr} reason=native_history_not_safely_available`);
2108
+ return {
2109
+ success: true,
2110
+ pending: true,
2111
+ // Both signals are true here: we reached the history-only path
2112
+ // because no live adapter was found (`live_adapter_not_found`),
2113
+ // and native history is not safely mappable
2114
+ // (`native_history_not_safely_available`).
2115
+ reason: 'native_history_not_safely_available',
2116
+ reasons: ['live_adapter_not_found', 'native_history_not_safely_available'],
2117
+ code: 'native_history_not_safely_available',
2118
+ messages: [],
2119
+ status: 'idle',
2120
+ providerSessionId: historyProviderSessionId,
2121
+ messageSource: decision.messageSource,
2122
+ transcriptProvenance: decision.messageSource,
2123
+ };
2124
+ }
2125
+ return buildReadChatCommandResult({
2126
+ messages: historyMessages,
2127
+ status: 'idle',
2128
+ messageSource: decision.messageSource,
2129
+ transcriptProvenance: decision.messageSource,
2130
+ ...(typeof (history as any)?.title === 'string' ? { title: (history as any).title } : {}),
2131
+ ...(historyProviderSessionId ? { providerSessionId: historyProviderSessionId } : {}),
2132
+ ...(((provider?.historyBehavior as any)?.transcriptAuthority === 'provider' || (provider?.historyBehavior as any)?.transcriptAuthority === 'daemon')
2133
+ ? { transcriptAuthority: (provider?.historyBehavior as any).transcriptAuthority }
2134
+ : {}),
2135
+ coverage: 'tail',
2136
+ }, args, h);
2137
+ } catch (error: any) {
2138
+ return { success: false, error: error?.message || `${transport} adapter not found` };
2139
+ }
2140
+ }
2141
+
2142
+ // Extension transport: evaluateInSession
2143
+ if (isExtensionTransport(transport)) {
2144
+ let extensionReadChatError = '';
2145
+ try {
2146
+ const evalResult = await h.evaluateProviderScript('readChat', undefined, READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS);
2147
+ if (evalResult?.result) {
2148
+ let parsed = evalResult.result;
2149
+ if (typeof parsed === 'string') {
2150
+ try {
2151
+ parsed = JSON.parse(parsed);
2152
+ } catch (e: any) {
2153
+ extensionReadChatError = `extension read_chat parse failed: ${e?.message || String(e)}`;
2154
+ }
2155
+ }
2156
+ if (parsed && typeof parsed === 'object') {
2157
+ const validated = validateReadChatResultPayload(parsed, 'extension read_chat');
2158
+ _log(`Extension OK: ${validated.messages?.length || 0} msgs`);
2159
+ traceProviderEvent(args, 'provider', 'extension.read_chat.success', {
2160
+ h,
2161
+ provider,
2162
+ payload: {
2163
+ method: 'evaluateProviderScript',
2164
+ result: evalResult.result,
2165
+ parsed: validated,
2166
+ messageCount: Array.isArray(validated.messages) ? validated.messages.length : 0,
2167
+ },
2168
+ });
2169
+ h.historyWriter.appendNewMessages(
2170
+ provider?.type || 'unknown_extension',
2171
+ toHistoryPersistedMessages(normalizeReadChatMessages(validated)),
2172
+ validated.title,
2173
+ args?.targetSessionId,
2174
+ historySessionId,
2175
+ );
2176
+ return buildReadChatCommandResult(validated as Record<string, any>, args, h);
2177
+ }
2178
+ if (!extensionReadChatError) {
2179
+ extensionReadChatError = 'extension read_chat returned a non-object payload';
2180
+ }
2181
+ } else {
2182
+ extensionReadChatError = 'extension read_chat returned no payload';
2183
+ }
2184
+ } catch (e: any) {
2185
+ extensionReadChatError = `extension read_chat failed: ${e?.message || String(e)}`;
2186
+ _log(`Extension error: ${e.message}`);
2187
+ traceProviderEvent(args, 'provider', 'extension.read_chat.error', {
2188
+ h,
2189
+ provider,
2190
+ level: 'warn',
2191
+ payload: { method: 'evaluateProviderScript', error: e.message },
2192
+ });
2193
+ }
2194
+ // Alternative: AgentStreamManager (script fail when)
2195
+ if (h.agentStream) {
2196
+ const cdp = h.getCdp();
2197
+ const parentSessionId = h.currentSession?.parentSessionId;
2198
+ if (cdp && parentSessionId) {
2199
+ const stream = await h.agentStream.collectActiveSession(cdp, parentSessionId);
2200
+ if (stream && stream.agentType !== provider?.type) {
2201
+ return { success: false, error: `extension read_chat stream agent mismatch for ${provider?.type || 'unknown_extension'}` };
2202
+ }
2203
+ if (stream) {
2204
+ h.historyWriter.appendNewMessages(
2205
+ stream.agentType,
2206
+ toHistoryPersistedMessages(stream.messages || []),
2207
+ undefined,
2208
+ args?.targetSessionId,
2209
+ historySessionId,
2210
+ );
2211
+ return buildReadChatCommandResult({
2212
+ messages: stream.messages || [],
2213
+ status: stream.status,
2214
+ agentType: stream.agentType,
2215
+ }, args, h);
2216
+ }
2217
+ }
2218
+ }
2219
+ return { success: false, error: extensionReadChatError || 'extension read_chat unavailable' };
2220
+ }
2221
+
2222
+ // IDE category (default): cdp.evaluate
2223
+ const cdp = h.getCdp();
2224
+ if (!cdp?.isConnected) return { success: false, error: 'CDP not connected' };
2225
+
2226
+ // webview IDE (Kiro, PearAI) → evaluateInWebviewFrame directly use
2227
+ const webviewScript = h.getProviderScript('webviewReadChat') || h.getProviderScript('webview_read_chat');
2228
+ if (webviewScript) {
2229
+ let webviewReadChatError = '';
2230
+ try {
2231
+ const matchText = provider?.webviewMatchText;
2232
+ const matchFn = matchText
2233
+ ? (body: string) => body.includes(matchText)
2234
+ : undefined;
2235
+ const raw = await cdp.evaluateInWebviewFrame(webviewScript, matchFn);
2236
+ if (raw) {
2237
+ let parsed: any = raw;
2238
+ if (typeof parsed === 'string') {
2239
+ try {
2240
+ parsed = JSON.parse(parsed);
2241
+ } catch (e: any) {
2242
+ webviewReadChatError = `webview read_chat parse failed: ${e?.message || String(e)}`;
2243
+ }
2244
+ }
2245
+ if (parsed && typeof parsed === 'object') {
2246
+ const validated = validateReadChatResultPayload(parsed, 'webview read_chat');
2247
+ _log(`Webview OK: ${validated.messages?.length || 0} msgs`);
2248
+ h.historyWriter.appendNewMessages(
2249
+ provider?.type || getCurrentProviderType(h, 'unknown_webview'),
2250
+ toHistoryPersistedMessages(normalizeReadChatMessages(validated)),
2251
+ validated.title,
2252
+ args?.targetSessionId,
2253
+ historySessionId,
2254
+ );
2255
+ return buildReadChatCommandResult(validated as Record<string, any>, args, h);
2256
+ }
2257
+ if (!webviewReadChatError) {
2258
+ webviewReadChatError = 'webview read_chat returned a non-object payload';
2259
+ }
2260
+ } else {
2261
+ webviewReadChatError = 'webview read_chat returned no payload';
2262
+ }
2263
+ } catch (e: any) {
2264
+ webviewReadChatError = `webview read_chat failed: ${e?.message || String(e)}`;
2265
+ _log(`Webview readChat error: ${e.message}`);
2266
+ }
2267
+ return { success: false, error: webviewReadChatError || 'webview read_chat unavailable' };
2268
+ }
2269
+
2270
+ // Regular IDE (Cursor, Windsurf, Trae etc) → main DOM evaluate
2271
+ const script = h.getProviderScript('readChat') || h.getProviderScript('read_chat');
2272
+ if (script) {
2273
+ let ideReadChatError = '';
2274
+ try {
2275
+ const evalResult = await h.evaluateProviderScript('readChat', undefined, READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS);
2276
+ if (evalResult?.result) {
2277
+ let parsed: any = evalResult.result;
2278
+ if (typeof parsed === 'string') {
2279
+ try {
2280
+ parsed = JSON.parse(parsed);
2281
+ } catch (e: any) {
2282
+ ideReadChatError = `ide read_chat parse failed: ${e?.message || String(e)}`;
2283
+ }
2284
+ }
2285
+ if (parsed && typeof parsed === 'object') {
2286
+ const validated = validateReadChatResultPayload(parsed, 'ide read_chat');
2287
+ _log(`OK: ${validated.messages?.length || 0} msgs`);
2288
+ traceProviderEvent(args, 'provider', 'ide.read_chat.success', {
2289
+ h,
2290
+ provider,
2291
+ payload: {
2292
+ method: 'evaluate',
2293
+ result: evalResult.result,
2294
+ parsed: validated,
2295
+ messageCount: Array.isArray(validated.messages) ? validated.messages.length : 0,
2296
+ },
2297
+ });
2298
+ h.historyWriter.appendNewMessages(
2299
+ provider?.type || getCurrentProviderType(h, 'unknown_ide'),
2300
+ toHistoryPersistedMessages(normalizeReadChatMessages(validated)),
2301
+ validated.title,
2302
+ args?.targetSessionId,
2303
+ historySessionId,
2304
+ );
2305
+ return buildReadChatCommandResult(validated as Record<string, any>, args, h);
2306
+ }
2307
+ if (!ideReadChatError) {
2308
+ ideReadChatError = 'ide read_chat returned a non-object payload';
2309
+ }
2310
+ } else {
2311
+ ideReadChatError = 'ide read_chat returned no payload';
2312
+ }
2313
+ } catch (e: any) {
2314
+ ideReadChatError = `ide read_chat failed: ${e?.message || String(e)}`;
2315
+ LOG.info('Command', `[read_chat] Script error: ${e.message}`);
2316
+ traceProviderEvent(args, 'provider', 'ide.read_chat.error', {
2317
+ h,
2318
+ provider,
2319
+ level: 'warn',
2320
+ payload: { method: 'evaluate', error: e.message },
2321
+ });
2322
+ }
2323
+ return { success: false, error: ideReadChatError || 'ide read_chat unavailable' };
2324
+ }
2325
+
2326
+ return { success: false, error: 'read_chat unavailable' };
2327
+ }