@adhdev/daemon-core 0.9.82-rc.49 → 0.9.82-rc.490

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