@adhdev/daemon-core 0.9.82-rc.45 → 0.9.82-rc.450

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