@adhdev/daemon-core 0.9.82-rc.48 → 0.9.82-rc.481

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