@adhdev/daemon-core 0.9.82-rc.34 → 0.9.82-rc.340

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 (318) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +8 -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 +46 -2
  8. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  9. package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
  10. package/dist/cli-adapters/provider-cli-adapter.d.ts +116 -64
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  12. package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
  13. package/dist/cli-adapters/provider-cli-shared.d.ts +53 -0
  14. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  15. package/dist/cli-adapters/resolve-executable.d.ts +14 -0
  16. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  17. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  18. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  19. package/dist/commands/cli-manager.d.ts +2 -1
  20. package/dist/commands/handler.d.ts +110 -0
  21. package/dist/commands/mesh-coordinator.d.ts +85 -1
  22. package/dist/commands/router.d.ts +308 -1
  23. package/dist/commands/upgrade-helper.d.ts +12 -0
  24. package/dist/config/chat-history.d.ts +9 -0
  25. package/dist/config/config.d.ts +5 -0
  26. package/dist/config/mesh-config.d.ts +77 -1
  27. package/dist/detection/ide-detector.d.ts +13 -0
  28. package/dist/detection/win32-ide-version.d.ts +37 -0
  29. package/dist/git/change-impact-config.d.ts +159 -0
  30. package/dist/git/git-commands.d.ts +11 -1
  31. package/dist/git/git-diff.d.ts +6 -0
  32. package/dist/git/git-executor.d.ts +11 -0
  33. package/dist/git/git-status.d.ts +23 -0
  34. package/dist/git/git-types.d.ts +2 -50
  35. package/dist/git/index.d.ts +2 -0
  36. package/dist/index.d.ts +52 -13
  37. package/dist/index.js +39439 -14533
  38. package/dist/index.js.map +1 -1
  39. package/dist/index.mjs +39000 -14215
  40. package/dist/index.mjs.map +1 -1
  41. package/dist/installer.d.ts +1 -4
  42. package/dist/ipc/local-ipc-server.d.ts +91 -0
  43. package/dist/launch.d.ts +1 -1
  44. package/dist/logging/async-batch-writer.d.ts +10 -0
  45. package/dist/logging/log-redactor.d.ts +24 -0
  46. package/dist/logging/log-tail-reader.d.ts +46 -0
  47. package/dist/mesh/contracts.d.ts +164 -0
  48. package/dist/mesh/coordinator-prompt.d.ts +36 -0
  49. package/dist/mesh/coordinator-registry.d.ts +59 -0
  50. package/dist/mesh/mesh-active-work.d.ts +174 -0
  51. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  52. package/dist/mesh/mesh-events-coordinator.d.ts +169 -0
  53. package/dist/mesh/mesh-events-pending.d.ts +45 -0
  54. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  55. package/dist/mesh/mesh-events-utils.d.ts +66 -0
  56. package/dist/mesh/mesh-events.d.ts +6 -49
  57. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  58. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  59. package/dist/mesh/mesh-init.d.ts +86 -0
  60. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  61. package/dist/mesh/mesh-ledger.d.ts +77 -1
  62. package/dist/mesh/mesh-missions.d.ts +116 -0
  63. package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
  64. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  65. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  66. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  67. package/dist/mesh/mesh-routing.d.ts +70 -0
  68. package/dist/mesh/mesh-runtime-store.d.ts +418 -0
  69. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  70. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
  71. package/dist/mesh/mesh-work-queue.d.ts +208 -5
  72. package/dist/mesh/preview-freshness.d.ts +18 -0
  73. package/dist/mesh/refine-config.d.ts +216 -0
  74. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  75. package/dist/providers/acp-provider-instance.d.ts +2 -0
  76. package/dist/providers/approval-utils.d.ts +13 -0
  77. package/dist/providers/chat-message-normalization.d.ts +12 -0
  78. package/dist/providers/cli-provider-instance.d.ts +76 -3
  79. package/dist/providers/contracts.d.ts +139 -6
  80. package/dist/providers/external-sources.d.ts +71 -0
  81. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  82. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  83. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  84. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  85. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  86. package/dist/providers/native-history/index.d.ts +13 -0
  87. package/dist/providers/provider-instance-manager.d.ts +13 -0
  88. package/dist/providers/provider-instance.d.ts +13 -1
  89. package/dist/providers/provider-loader.d.ts +26 -4
  90. package/dist/providers/provider-trust.d.ts +31 -0
  91. package/dist/providers/read-chat-contract.d.ts +29 -0
  92. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  93. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  94. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  95. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  96. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  97. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  98. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  99. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  100. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  101. package/dist/providers/sdk/v1/index.d.ts +30 -0
  102. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  103. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  104. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  105. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  106. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  107. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  108. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  109. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  110. package/dist/providers/spec/adapter.d.ts +69 -0
  111. package/dist/providers/spec/cli-adapter.d.ts +217 -0
  112. package/dist/providers/spec/evaluator.d.ts +23 -0
  113. package/dist/providers/spec/fsm-driver.d.ts +306 -0
  114. package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
  115. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  116. package/dist/providers/spec/fsm-types.d.ts +144 -0
  117. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  118. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  119. package/dist/providers/spec/route.d.ts +4 -0
  120. package/dist/providers/spec/types.d.ts +180 -0
  121. package/dist/providers/status-monitor.d.ts +7 -7
  122. package/dist/providers/transcript-v2.d.ts +176 -0
  123. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  124. package/dist/providers/working-dir.d.ts +17 -0
  125. package/dist/repo-mesh-types.d.ts +295 -13
  126. package/dist/sessions/registry.d.ts +6 -0
  127. package/dist/shared-types-extra.d.ts +2 -4
  128. package/dist/shared-types.d.ts +59 -2
  129. package/dist/status/normalize.d.ts +1 -1
  130. package/dist/status/normalize.js +1 -0
  131. package/dist/status/normalize.js.map +1 -1
  132. package/dist/status/normalize.mjs +1 -0
  133. package/dist/status/normalize.mjs.map +1 -1
  134. package/dist/status/reporter.d.ts +2 -0
  135. package/dist/status/snapshot.d.ts +26 -0
  136. package/dist/system/load-better-sqlite3.d.ts +21 -0
  137. package/dist/types.d.ts +5 -0
  138. package/package.json +7 -3
  139. package/src/agent-stream/poller.ts +2 -3
  140. package/src/agent-stream/provider-adapter.ts +1 -1
  141. package/src/boot/daemon-lifecycle.ts +55 -12
  142. package/src/boot/process-hardening.ts +89 -0
  143. package/src/build-info.ts +73 -0
  144. package/src/chat/source-machine.ts +534 -0
  145. package/src/chat/source-resolver.ts +0 -0
  146. package/src/chat/subscription-updates.ts +20 -1
  147. package/src/cli-adapter-types.d.ts +1 -0
  148. package/src/cli-adapter-types.ts +47 -2
  149. package/src/cli-adapters/cli-script-runner.ts +421 -0
  150. package/src/cli-adapters/cli-state-engine.ts +1215 -0
  151. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  152. package/src/cli-adapters/provider-cli-adapter.ts +774 -1141
  153. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  154. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  155. package/src/cli-adapters/provider-cli-runtime.ts +56 -7
  156. package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
  157. package/src/cli-adapters/provider-cli-shared.ts +100 -11
  158. package/src/cli-adapters/pty-transport.ts +2 -1
  159. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  160. package/src/cli-adapters/resolve-executable.ts +90 -0
  161. package/src/cli-adapters/session-host-transport.ts +2 -1
  162. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
  163. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  164. package/src/cli-adapters/terminal-screen.ts +16 -81
  165. package/src/commands/chat-commands.ts +1823 -62
  166. package/src/commands/cli-manager.ts +286 -14
  167. package/src/commands/handler.ts +809 -2
  168. package/src/commands/mesh-coordinator.ts +332 -122
  169. package/src/commands/router.ts +7433 -1317
  170. package/src/commands/stream-commands.ts +8 -0
  171. package/src/commands/upgrade-helper.ts +81 -30
  172. package/src/config/chat-history.ts +483 -24
  173. package/src/config/config.ts +12 -0
  174. package/src/config/mesh-config.ts +388 -6
  175. package/src/config/recent-activity.ts +8 -2
  176. package/src/daemon/dev-auto-implement.ts +3 -2
  177. package/src/daemon/dev-cli-debug.ts +10 -1
  178. package/src/detection/cli-detector.ts +28 -9
  179. package/src/detection/ide-detector.ts +55 -16
  180. package/src/detection/win32-ide-version.ts +106 -0
  181. package/src/git/change-impact-config.ts +354 -0
  182. package/src/git/git-commands.ts +55 -14
  183. package/src/git/git-diff.ts +53 -0
  184. package/src/git/git-executor.ts +12 -0
  185. package/src/git/git-status.ts +574 -43
  186. package/src/git/git-types.ts +14 -62
  187. package/src/git/git-worktree.ts +8 -1
  188. package/src/git/index.ts +16 -0
  189. package/src/index.ts +146 -12
  190. package/src/installer.d.ts +1 -1
  191. package/src/installer.ts +8 -6
  192. package/src/ipc/local-ipc-server.ts +278 -0
  193. package/src/launch.d.ts +1 -1
  194. package/src/launch.ts +37 -28
  195. package/src/logging/async-batch-writer.ts +55 -0
  196. package/src/logging/log-redactor.ts +100 -0
  197. package/src/logging/log-tail-reader.ts +220 -0
  198. package/src/logging/logger.ts +2 -1
  199. package/src/mesh/contracts.ts +329 -0
  200. package/src/mesh/coordinator-prompt.ts +234 -31
  201. package/src/mesh/coordinator-registry.ts +121 -0
  202. package/src/mesh/mesh-active-work.ts +623 -0
  203. package/src/mesh/mesh-delivery-policy.ts +315 -0
  204. package/src/mesh/mesh-events-coordinator.ts +2144 -0
  205. package/src/mesh/mesh-events-pending.ts +508 -0
  206. package/src/mesh/mesh-events-stale.ts +290 -0
  207. package/src/mesh/mesh-events-utils.ts +300 -0
  208. package/src/mesh/mesh-events.ts +30 -1035
  209. package/src/mesh/mesh-fast-forward.ts +839 -0
  210. package/src/mesh/mesh-host-ownership.ts +73 -0
  211. package/src/mesh/mesh-init.ts +260 -0
  212. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  213. package/src/mesh/mesh-ledger.ts +589 -102
  214. package/src/mesh/mesh-missions.ts +248 -0
  215. package/src/mesh/mesh-reconcile-loop.ts +822 -0
  216. package/src/mesh/mesh-refine-batch.ts +197 -0
  217. package/src/mesh/mesh-refine-status.ts +231 -0
  218. package/src/mesh/mesh-review-inbox.ts +307 -0
  219. package/src/mesh/mesh-routing.ts +291 -0
  220. package/src/mesh/mesh-runtime-store.ts +1626 -0
  221. package/src/mesh/mesh-task-stats.ts +154 -0
  222. package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
  223. package/src/mesh/mesh-work-queue.ts +888 -141
  224. package/src/mesh/preview-freshness.ts +118 -0
  225. package/src/mesh/refine-config.ts +423 -0
  226. package/src/mesh/worktree-bootstrap-config.ts +331 -0
  227. package/src/providers/acp-provider-instance.ts +25 -9
  228. package/src/providers/approval-utils.d.ts +4 -0
  229. package/src/providers/approval-utils.ts +47 -5
  230. package/src/providers/chat-message-normalization.ts +45 -5
  231. package/src/providers/cli-provider-instance.ts +1038 -99
  232. package/src/providers/contracts.d.ts +55 -0
  233. package/src/providers/contracts.ts +150 -6
  234. package/src/providers/extension-provider-instance.ts +12 -7
  235. package/src/providers/external-sources.ts +218 -0
  236. package/src/providers/ide-provider-instance.ts +35 -12
  237. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  238. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  239. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  240. package/src/providers/native-history/dispatcher.ts +340 -0
  241. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  242. package/src/providers/native-history/index.ts +30 -0
  243. package/src/providers/provider-instance-manager.ts +30 -0
  244. package/src/providers/provider-instance.ts +10 -1
  245. package/src/providers/provider-loader.ts +670 -50
  246. package/src/providers/provider-schema.ts +87 -14
  247. package/src/providers/provider-trust.ts +114 -0
  248. package/src/providers/read-chat-contract.ts +76 -16
  249. package/src/providers/sdk/README.md +49 -0
  250. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  251. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  252. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  253. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  254. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  255. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  256. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  257. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  258. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  259. package/src/providers/sdk/v1/index.ts +152 -0
  260. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  261. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  262. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  263. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  264. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  265. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  266. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  267. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  268. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  269. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  270. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  271. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  272. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  273. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  274. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  275. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  276. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  277. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  278. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  279. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  280. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  281. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  282. package/src/providers/sdk/v1/validators/index.ts +19 -0
  283. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  284. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  285. package/src/providers/spec/adapter.ts +168 -0
  286. package/src/providers/spec/cli-adapter.ts +986 -0
  287. package/src/providers/spec/evaluator.ts +345 -0
  288. package/src/providers/spec/fsm-driver.ts +993 -0
  289. package/src/providers/spec/fsm-evaluator.ts +255 -0
  290. package/src/providers/spec/fsm-loader.ts +120 -0
  291. package/src/providers/spec/fsm-types.ts +238 -0
  292. package/src/providers/spec/native-history-executor.ts +1136 -0
  293. package/src/providers/spec/pre-launch-trust.ts +104 -0
  294. package/src/providers/spec/route.ts +51 -0
  295. package/src/providers/spec/types.ts +226 -0
  296. package/src/providers/status-monitor.d.ts +7 -7
  297. package/src/providers/status-monitor.ts +37 -22
  298. package/src/providers/transcript-v2.ts +567 -0
  299. package/src/providers/types/interactive-prompt.ts +536 -0
  300. package/src/providers/version-archive.ts +64 -24
  301. package/src/providers/working-dir.ts +23 -0
  302. package/src/repo-mesh-types.ts +378 -14
  303. package/src/sessions/registry.ts +6 -0
  304. package/src/shared-types-extra.ts +2 -4
  305. package/src/shared-types.d.ts +8 -0
  306. package/src/shared-types.ts +64 -1
  307. package/src/status/builders.ts +26 -6
  308. package/src/status/normalize.ts +2 -0
  309. package/src/status/reporter.ts +19 -1
  310. package/src/status/snapshot.ts +95 -26
  311. package/src/system/host-memory.ts +29 -12
  312. package/src/system/load-better-sqlite3.ts +68 -0
  313. package/src/types.ts +5 -0
  314. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  315. package/dist/mesh/mesh-sync.d.ts +0 -53
  316. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  317. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  318. package/src/mesh/mesh-sync.ts +0 -111
@@ -12,18 +12,54 @@ import type { CliAdapter } from '../cli-adapter-types.js';
12
12
  import { flattenContent, normalizeInputEnvelope, type InputEnvelope, type ProviderModule, type ProviderScripts } from '../providers/contracts.js';
13
13
  import { assertProviderSupportsDeclaredInput, assertTextOnlyInput } from '../providers/provider-input-support.js';
14
14
  import { validateReadChatResultPayload } from '../providers/read-chat-contract.js';
15
+ import { pickApprovalButton } from '../providers/approval-utils.js';
15
16
  import type { ProviderInstance } from '../providers/provider-instance.js';
16
- import { readProviderChatHistory } from '../config/chat-history.js';
17
+ import { isNativeSourceCanonicalHistory, readChatHistory, readProviderChatHistory } from '../config/chat-history.js';
18
+ import { getCoordinatorForSession } from '../mesh/coordinator-registry.js';
17
19
  import { LOG, getRecentLogs } from '../logging/logger.js';
18
20
  import { getRecentDebugTrace, recordDebugTrace } from '../logging/debug-trace.js';
19
21
  import { buildChatMessageSignature, hashSignatureParts } from '../chat/chat-signatures.js';
22
+ import {
23
+ CHAT_SOURCE_REGISTRY,
24
+ buildV1NativePresentObservation,
25
+ chatSourceSessionKey,
26
+ type ChatSourceDecision,
27
+ type ChatSourceObservation,
28
+ type ChatSourceTransitionCause,
29
+ } from '../chat/source-resolver.js';
20
30
  import type { ChatMessage } from '../types.js';
21
31
  import type { SessionTransport } from '../shared-types.js';
22
- import { filterUserFacingChatMessages, normalizeChatMessages } from '../providers/chat-message-normalization.js';
32
+ import { filterUserFacingChatMessages, isActivityChatMessage, isUserFacingChatMessage, normalizeChatMessages } from '../providers/chat-message-normalization.js';
23
33
 
24
34
  const RECENT_SEND_WINDOW_MS = 1200;
25
35
  export const READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS = 25_000;
36
+ // Minimum tail floor for hot-path history/mirror reads. The dashboard requests a
37
+ // bounded tail (~60); we keep a small floor so a tiny requested tailLimit still
38
+ // has enough surrounding context for seed/mirror dedup correctness, but it must
39
+ // NOT dominate the hot subscribe/poll path the way the previous 200 floor did.
40
+ // readChatHistory now serves this as an O(tail) bounded read, so the cost scales
41
+ // with this floor, not with total accumulated history.
42
+ const HOT_TAIL_MIN_LIMIT = 60;
26
43
  const HERMES_CLI_STARTING_SEND_SETTLE_MS = 2_000;
44
+ // (A2.2) CLI_NATIVE_HISTORY_FRESH_MS removed with isNativeHistoryFreshEnough.
45
+ // Hardcoded native-transcript provider allow-list. Deprecated. Kept only as a
46
+ // last-resort fallback when ProviderModule is not yet loaded; on every hit we
47
+ // warn so the dependency on this set is visible. A2 deletes the set entirely
48
+ // and routes solely through canonicalHistory.contractVersion +
49
+ // isNativeSourceCanonicalHistory().
50
+ const CLI_NATIVE_TRANSCRIPT_PROVIDERS = new Set(['codex-cli', 'claude-cli', 'hermes-cli', 'antigravity-cli']);
51
+ const warnedLegacyNativeAllowlistHits = new Set<string>();
52
+ function warnLegacyNativeAllowlistHit(providerType: string): void {
53
+ if (warnedLegacyNativeAllowlistHits.has(providerType)) return;
54
+ warnedLegacyNativeAllowlistHits.add(providerType);
55
+ // eslint-disable-next-line no-console
56
+ console.warn(
57
+ `[chat-commands] supportsCliNativeTranscript fell back to the hardcoded `
58
+ + `CLI_NATIVE_TRANSCRIPT_PROVIDERS set for "${providerType}". `
59
+ + `The provider module was unavailable or did not declare canonicalHistory. `
60
+ + `Set canonicalHistory.contractVersion in the provider.json to remove this dependency.`,
61
+ );
62
+ }
27
63
  const recentSendByTarget = new Map<string, number>();
28
64
 
29
65
  interface ApprovalSelectableInstance extends ProviderInstance {
@@ -32,6 +68,7 @@ interface ApprovalSelectableInstance extends ProviderInstance {
32
68
 
33
69
  interface RuntimeChatMessageMerger extends ProviderInstance {
34
70
  mergeRuntimeChatMessages?(messages: ChatMessage[]): ChatMessage[];
71
+ recordAcknowledgedUserInput?(input: InputEnvelope | string): void;
35
72
  }
36
73
 
37
74
  type LegacyStringScript = (params?: Record<string, unknown> | string) => string;
@@ -48,6 +85,16 @@ function getTargetedCliAdapter(h: CommandHelpers, args: any, providerType?: stri
48
85
  return h.getCliAdapter(args?.targetSessionId || providerType || h.currentSession?.providerType || h.currentManagerKey);
49
86
  }
50
87
 
88
+ function getExplicitHistorySessionId(args: any): string | undefined {
89
+ const explicit = typeof args?.historySessionId === 'string' ? args.historySessionId.trim() : '';
90
+ if (explicit) return explicit;
91
+
92
+ const explicitProviderSessionId = typeof args?.providerSessionId === 'string' ? args.providerSessionId.trim() : '';
93
+ if (explicitProviderSessionId) return explicitProviderSessionId;
94
+
95
+ return undefined;
96
+ }
97
+
51
98
  function getTargetInstance(h: CommandHelpers, args: any): ApprovalSelectableInstance | null {
52
99
  const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
53
100
  const sessionId = targetSessionId || h.currentSession?.sessionId || '';
@@ -139,19 +186,82 @@ async function waitOnceForFreshHermesCliStart(adapter: CliAdapter, log: (msg: st
139
186
  }
140
187
 
141
188
  function getHistorySessionId(h: CommandHelpers, args: any): string | undefined {
142
- const explicit = typeof args?.historySessionId === 'string' ? args.historySessionId.trim() : '';
189
+ const explicit = getExplicitHistorySessionId(args);
143
190
  if (explicit) return explicit;
144
191
 
145
- const explicitProviderSessionId = typeof args?.providerSessionId === 'string' ? args.providerSessionId.trim() : '';
146
- if (explicitProviderSessionId) return explicitProviderSessionId;
147
-
148
192
  const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
149
193
  if (!targetSessionId) return undefined;
150
194
 
151
- const instance = h.ctx.instanceManager?.getInstance(targetSessionId);
195
+ const session = h.ctx.sessionRegistry?.get(targetSessionId) as any;
196
+ const registeredProviderSessionId = typeof session?.providerSessionId === 'string' ? session.providerSessionId.trim() : '';
197
+ if (registeredProviderSessionId) return registeredProviderSessionId;
198
+
199
+ const instance = getTargetInstance(h, args);
152
200
  const state = instance?.getState?.();
153
201
  const providerSessionId = typeof state?.providerSessionId === 'string' ? state.providerSessionId.trim() : '';
154
- return providerSessionId || targetSessionId;
202
+ if (providerSessionId) return providerSessionId;
203
+
204
+ const currentSession = h.currentSession as any;
205
+ if (currentSession?.sessionId === targetSessionId) {
206
+ const currentProviderSessionId = typeof currentSession.providerSessionId === 'string'
207
+ ? currentSession.providerSessionId.trim()
208
+ : '';
209
+ if (currentProviderSessionId) return currentProviderSessionId;
210
+ }
211
+
212
+ return targetSessionId;
213
+ }
214
+
215
+ function resolveCliNativeHistorySessionId(args: any, currentHistorySessionId: string | undefined, parsedProviderSessionId: string | undefined): string | undefined {
216
+ const explicit = getExplicitHistorySessionId(args);
217
+ if (explicit) return explicit;
218
+
219
+ const parsed = typeof parsedProviderSessionId === 'string' ? parsedProviderSessionId.trim() : '';
220
+ const current = typeof currentHistorySessionId === 'string' ? currentHistorySessionId.trim() : '';
221
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
222
+
223
+ // getHistorySessionId falls back to the runtime session id when no native
224
+ // handle has been registered yet. For live CLI adapters the parser may
225
+ // already know the provider-native handle; prefer it over the runtime id so
226
+ // exact native reads do not miss the worker transcript and fall back to PTY
227
+ // or same-workspace history.
228
+ if (parsed && (!current || current === targetSessionId)) return parsed;
229
+ return current || parsed || undefined;
230
+ }
231
+
232
+ function shouldSkipLiveCliNativeHistoryWithoutProviderSession(args: {
233
+ adapter?: CliAdapter | null;
234
+ providerType?: string;
235
+ readChatArgs: any;
236
+ nativeHistorySessionId?: string;
237
+ parsedProviderSessionId?: string;
238
+ }): boolean {
239
+ const explicit = getExplicitHistorySessionId(args.readChatArgs);
240
+ if (explicit) return false;
241
+
242
+ const targetSessionId = typeof args.readChatArgs?.targetSessionId === 'string'
243
+ ? args.readChatArgs.targetSessionId.trim()
244
+ : '';
245
+ if (!targetSessionId) return false;
246
+
247
+ const resolved = typeof args.nativeHistorySessionId === 'string'
248
+ ? args.nativeHistorySessionId.trim()
249
+ : '';
250
+ if (!resolved || resolved !== targetSessionId) return false;
251
+
252
+ const parsed = typeof args.parsedProviderSessionId === 'string'
253
+ ? args.parsedProviderSessionId.trim()
254
+ : '';
255
+ if (parsed) return false;
256
+
257
+ const cliType = args.adapter?.cliType || args.providerType || '';
258
+ if (cliType !== 'codex-cli') return false;
259
+
260
+ // A live Codex session starts with only the daemon runtime UUID. That UUID
261
+ // is not the provider-native rollout id, so using it for native history
262
+ // lets the file picker fall back to the newest same-workspace transcript
263
+ // and makes concurrent fresh sessions all show the same old conversation.
264
+ return !!args.adapter;
155
265
  }
156
266
 
157
267
  function getInteractionId(args: any): string | undefined {
@@ -221,6 +331,967 @@ function normalizeReadChatMessages(payload: Record<string, any>): ChatMessage[]
221
331
  return normalizeChatMessages(messages);
222
332
  }
223
333
 
334
+ function getMessageNewestReceivedAt(messages: Array<{ receivedAt?: unknown; timestamp?: unknown }>): number {
335
+ let newest = 0;
336
+ for (const message of messages) {
337
+ const receivedAt = Number(message?.receivedAt ?? message?.timestamp ?? 0);
338
+ if (Number.isFinite(receivedAt) && receivedAt > newest) newest = receivedAt;
339
+ }
340
+ return newest;
341
+ }
342
+
343
+ function readHistorySessionIdFromMessages(messages: ChatMessage[]): string | undefined {
344
+ for (const message of messages as Array<ChatMessage & { historySessionId?: unknown }>) {
345
+ const historySessionId = typeof message?.historySessionId === 'string' ? message.historySessionId.trim() : '';
346
+ if (historySessionId) return historySessionId;
347
+ }
348
+ return undefined;
349
+ }
350
+
351
+ function shouldPreserveNativeIdentity(providerType: string, sessionId: string, message: ChatMessage): boolean {
352
+ const providerUnitKey = typeof message.providerUnitKey === 'string' ? message.providerUnitKey.trim() : '';
353
+ const turnKey = typeof message._turnKey === 'string' ? message._turnKey.trim() : '';
354
+ if (!providerUnitKey) return false;
355
+ // (A2.3) v2 stamped identity is producer-owned and globally stable; trust it
356
+ // unconditionally. Producers may omit _turnKey (the daemon recomputes it
357
+ // from the current ordering), so do not require turnKey for v2 messages.
358
+ if (providerUnitKey.startsWith('v2:') || providerUnitKey.startsWith('v2-pty:')) {
359
+ return true;
360
+ }
361
+ // v1 identity always required both keys to be present.
362
+ if (!turnKey) return false;
363
+ if (providerType === 'hermes-cli' && sessionId) {
364
+ return providerUnitKey.startsWith(`${providerType}:native:${sessionId}:`)
365
+ && turnKey.startsWith(`${providerType}:native-turn:${sessionId}:`);
366
+ }
367
+ return true;
368
+ }
369
+
370
+ /**
371
+ * Drop the synthetic "user" message some CLIs surface in their native
372
+ * transcript when the daemon injects a coordinator system prompt
373
+ * (codex puts the AGENTS.md / developer_instructions block in as
374
+ * role=user; agy/claude/hermes have similar artifacts). The user can
375
+ * opt back into seeing it via the provider setting
376
+ * `showCoordinatorSystemPrompt`. Default is off — the prompt is still
377
+ * fully visible from the chat-header ⓘ "Session info" dialog.
378
+ *
379
+ * Matching rules:
380
+ * 1. Setting must be off (default).
381
+ * 2. There must be a registered coordinator entry for the session.
382
+ * 3. The candidate message is filtered when its role is user OR
383
+ * system and its content either contains the prompt body verbatim,
384
+ * OR contains the well-known coordinator marker
385
+ * `adhdev-mesh-coordinator-prompt`. The marker covers context-file
386
+ * cases (agy AGENTS.md / gemini GEMINI.md) where the CLI may wrap
387
+ * its own preamble around our block. Verbatim-content covers
388
+ * codex's developer_instructions echo.
389
+ *
390
+ * Returns the messages array unchanged when none of the rules match,
391
+ * so this is safe to apply unconditionally to every read_chat result.
392
+ */
393
+ function maybeHideCoordinatorPromptMessage(
394
+ h: CommandHelpers,
395
+ providerType: string,
396
+ sessionId: string | undefined,
397
+ messages: ChatMessage[],
398
+ ): ChatMessage[] {
399
+ if (!Array.isArray(messages) || messages.length === 0) return messages;
400
+ if (!sessionId) return messages;
401
+ const loader = h.ctx?.providerLoader;
402
+ if (!loader) return messages;
403
+ let showSetting: unknown = undefined;
404
+ try {
405
+ showSetting = (loader as any).getSettingValue?.(providerType, 'showCoordinatorSystemPrompt');
406
+ } catch { /* unknown setting key for this provider — fall through */ }
407
+ if (showSetting === true) return messages;
408
+ const coord = getCoordinatorForSession(sessionId);
409
+ if (!coord) return messages;
410
+ const promptBody = typeof coord.systemPrompt === 'string' ? coord.systemPrompt : '';
411
+ const MARKER = 'adhdev-mesh-coordinator-prompt';
412
+ const filtered = messages.filter(m => {
413
+ const role = String((m as any)?.role || '').toLowerCase();
414
+ if (role !== 'user' && role !== 'system') return true;
415
+ const content = flattenContent((m as any)?.content);
416
+ if (!content) return true;
417
+ if (content.includes(MARKER)) return false;
418
+ if (promptBody && content.includes(promptBody.slice(0, Math.min(400, promptBody.length)))) return false;
419
+ return true;
420
+ });
421
+ if (filtered.length !== messages.length) {
422
+ LOG.debug('ChatFilter', `[${providerType}] hid ${messages.length - filtered.length} coordinator-prompt message(s) from ${sessionId}`);
423
+ }
424
+ return filtered;
425
+ }
426
+
427
+ /**
428
+ * Convenience wrapper used at every native-history call site: normalize +
429
+ * conditionally drop the coordinator system-prompt message. Avoids
430
+ * duplicating the filter at four read_chat code paths.
431
+ */
432
+ function normalizeAndFilterNativeHistory(
433
+ h: CommandHelpers,
434
+ providerType: string,
435
+ args: any,
436
+ messages: ChatMessage[],
437
+ nativeSessionId?: string,
438
+ ): ChatMessage[] {
439
+ const normalized = normalizeNativeHistoryMessages(providerType, messages, nativeSessionId);
440
+ const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId
441
+ : typeof args?.sessionId === 'string' ? args.sessionId
442
+ : undefined;
443
+ return maybeHideCoordinatorPromptMessage(h, providerType, sessionId, normalized);
444
+ }
445
+
446
+ function normalizeNativeHistoryMessages(providerType: string, messages: ChatMessage[], nativeSessionId?: string): ChatMessage[] {
447
+ let turnIndex = 0;
448
+ return normalizeChatMessages(messages).map((message, index) => {
449
+ const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
450
+ const kind = typeof message.kind === 'string' && message.kind.trim() ? message.kind.trim() : (role === 'system' ? 'system' : 'standard');
451
+ if ((role === 'user' || role === 'human') && index > 0) turnIndex += 1;
452
+ const historySessionId = typeof (message as any).historySessionId === 'string'
453
+ ? (message as any).historySessionId.trim()
454
+ : '';
455
+ const contentHash = hashSignatureParts([
456
+ providerType,
457
+ historySessionId,
458
+ String(message.receivedAt || message.timestamp || index),
459
+ role,
460
+ kind,
461
+ flattenContent(message.content),
462
+ ]).slice(0, 12);
463
+ const nativeIdentitySessionId = historySessionId || (typeof nativeSessionId === 'string' ? nativeSessionId.trim() : '');
464
+ const preserveNativeIdentity = shouldPreserveNativeIdentity(providerType, nativeIdentitySessionId, message);
465
+ const existingProviderUnitKey = typeof message.providerUnitKey === 'string' ? message.providerUnitKey.trim() : '';
466
+ const existingTurnKey = typeof message._turnKey === 'string' ? message._turnKey.trim() : '';
467
+ const providerUnitKey = preserveNativeIdentity
468
+ ? existingProviderUnitKey
469
+ : `${providerType}:native:${nativeIdentitySessionId || 'workspace'}:${index}:${role || 'message'}:${kind}:${contentHash}`;
470
+ const meta = message.meta && typeof message.meta === 'object' ? message.meta as Record<string, unknown> : undefined;
471
+ const isSystemSessionStart = role === 'system' || kind === 'system' || kind === 'session_start';
472
+ const isActivity = role === 'assistant' && (kind === 'tool' || kind === 'terminal' || kind === 'thought');
473
+ // (A2.3) sequence emit. Producer-supplied wins (v2-stamped messages
474
+ // bring their own monotonic sequence); otherwise derive from
475
+ // receivedAt/timestamp; otherwise positional. Always present on the
476
+ // output so consumers (ChatSourceMachine) have a stable ordering key.
477
+ const existingSequence = typeof (message as any).sequence === 'number'
478
+ && Number.isFinite((message as any).sequence)
479
+ ? (message as any).sequence
480
+ : null;
481
+ const tsCandidate = Number(message.receivedAt || message.timestamp || 0);
482
+ const sequence = existingSequence !== null
483
+ ? existingSequence
484
+ : (tsCandidate > 0 ? tsCandidate : index);
485
+ return {
486
+ ...message,
487
+ role: role === 'human' ? 'user' : (role || 'assistant'),
488
+ kind: isSystemSessionStart ? 'system' : kind,
489
+ ...(nativeIdentitySessionId ? { historySessionId: nativeIdentitySessionId } : {}),
490
+ providerUnitKey,
491
+ bubbleId: typeof message.bubbleId === 'string' && message.bubbleId.trim()
492
+ && preserveNativeIdentity
493
+ ? message.bubbleId.trim()
494
+ : `bubble:${providerUnitKey}`,
495
+ sequence,
496
+ _turnKey: preserveNativeIdentity
497
+ ? existingTurnKey
498
+ : `${providerType}:native-turn:${nativeIdentitySessionId || 'workspace'}:${turnIndex}`,
499
+ bubbleState: message.bubbleState || 'final',
500
+ ...(isSystemSessionStart ? {
501
+ visibility: message.visibility || 'hidden',
502
+ transcriptVisibility: message.transcriptVisibility || 'hidden',
503
+ audience: message.audience || 'internal',
504
+ source: message.source || 'runtime_status',
505
+ } : isActivity ? {
506
+ source: message.source || (kind === 'terminal' ? 'terminal_command' : 'tool_call'),
507
+ meta: { ...meta, label: message.senderName || meta?.label || (kind === 'terminal' ? 'Terminal' : 'Tool') },
508
+ } : {
509
+ source: message.source || (role === 'assistant' ? 'assistant_text' : undefined),
510
+ }),
511
+ } as ChatMessage;
512
+ });
513
+ }
514
+
515
+ function buildCliMessageSourceProvenance(args: {
516
+ selected: 'native-history' | 'pty-parser';
517
+ provider: string;
518
+ nativeHandle?: string;
519
+ sessionWorkspace?: string;
520
+ intendedWorkspace?: string;
521
+ transcriptWorkspace?: string;
522
+ fallbackReason?: string;
523
+ nativeSource?: string;
524
+ sourcePath?: string;
525
+ sourceMtimeMs?: number;
526
+ nativeHistoryCoverage?: string;
527
+ partialReason?: string;
528
+ unavailableReason?: string;
529
+ nativeMessages?: ChatMessage[];
530
+ ptyMessages?: ChatMessage[];
531
+ returnedMessages?: ChatMessage[];
532
+ safeMapping?: boolean;
533
+ freshEnough?: boolean;
534
+ ptyStatusApprovalOnly?: boolean;
535
+ }): Record<string, unknown> {
536
+ const sourceMtimeMs = Number(args.sourceMtimeMs || 0);
537
+ const sourceMtimeAgeMs = sourceMtimeMs > 0 ? Math.max(0, Date.now() - sourceMtimeMs) : undefined;
538
+ const nativeMessages = args.nativeMessages || [];
539
+ const ptyMessages = args.ptyMessages || [];
540
+ const returnedMessages = args.returnedMessages || [];
541
+ const identityStatus = args.selected === 'native-history'
542
+ ? 'safe'
543
+ : args.fallbackReason === 'native_history_not_safely_mapped'
544
+ ? 'ambiguous_session_identity'
545
+ : args.fallbackReason?.startsWith('native_history_unavailable')
546
+ ? 'transcript_unmapped'
547
+ : undefined;
548
+ return {
549
+ selected: args.selected,
550
+ provider: args.provider,
551
+ providerType: args.provider,
552
+ ...(identityStatus ? { identityStatus } : {}),
553
+ ...(args.nativeHandle ? { nativeHandle: args.nativeHandle } : {}),
554
+ ...(args.nativeHandle ? { nativeSessionId: args.nativeHandle } : {}),
555
+ ...(args.sessionWorkspace ? { sessionWorkspace: args.sessionWorkspace } : {}),
556
+ ...(args.intendedWorkspace ? { intendedWorkspace: args.intendedWorkspace } : {}),
557
+ ...(args.transcriptWorkspace ? { transcriptWorkspace: args.transcriptWorkspace } : {}),
558
+ ...(args.fallbackReason ? { fallbackReason: args.fallbackReason } : {}),
559
+ ...(args.nativeSource ? { nativeSource: args.nativeSource } : {}),
560
+ ...(args.sourcePath ? { sourcePath: args.sourcePath } : {}),
561
+ ...(args.nativeHistoryCoverage ? { nativeHistoryCoverage: args.nativeHistoryCoverage } : {}),
562
+ ...(args.partialReason ? { partialReason: args.partialReason } : {}),
563
+ ...(args.unavailableReason ? { unavailableReason: args.unavailableReason } : {}),
564
+ ptyStatusApprovalOnly: args.ptyStatusApprovalOnly === true,
565
+ staleness: {
566
+ sourceMtimeMs: sourceMtimeMs || undefined,
567
+ sourceMtimeAgeMs,
568
+ nativeNewestMessageAt: getMessageNewestReceivedAt(nativeMessages),
569
+ ptyNewestMessageAt: getMessageNewestReceivedAt(ptyMessages),
570
+ freshEnough: args.freshEnough === true,
571
+ },
572
+ coverage: {
573
+ nativeMessageCount: nativeMessages.length,
574
+ ptyMessageCount: ptyMessages.length,
575
+ returnedMessageCount: returnedMessages.length,
576
+ safeMapping: args.safeMapping === true,
577
+ // true when PTY message bodies are suppressed and must not be treated as
578
+ // chat content. PTY may still contribute status/approval/screen evidence.
579
+ ptyMessagesSuppressed: args.selected === 'native-history' || args.ptyStatusApprovalOnly === true,
580
+ },
581
+ };
582
+ }
583
+
584
+ /**
585
+ * Map a ChatSourceMachine transition cause back to the v1 messageSource
586
+ * `fallbackReason` vocabulary so legacy consumers (web-cloud, tests, mesh
587
+ * debug bundles) keep parsing strings they already know. A3 replaces the
588
+ * caller surface with stateTransition/lockState, after which this map can be
589
+ * deleted.
590
+ *
591
+ * Returns undefined when the cause does not correspond to a fallback (i.e.
592
+ * the source is native-history and there is nothing to explain).
593
+ */
594
+ function causeToLegacyFallbackReason(
595
+ cause: ChatSourceTransitionCause,
596
+ selected: 'native-history' | 'pty-parser',
597
+ extraDetail?: { unavailableReason?: string; nativeSource?: string },
598
+ ): string | undefined {
599
+ if (selected === 'native-history') return undefined;
600
+ switch (cause) {
601
+ case 'initial':
602
+ return 'native_history_not_checked';
603
+ case 'native_progressed':
604
+ // Selected pty-parser despite a progressed observation — that
605
+ // means we held PtyOnly stickily (peak unmet or non-superset).
606
+ return 'native_history_not_selected';
607
+ case 'native_regressed_shrunk':
608
+ return 'native_history_empty';
609
+ case 'native_regressed_unsafe_mapping':
610
+ return 'native_history_not_safely_mapped';
611
+ case 'native_regressed_coverage_partial':
612
+ return 'native_history_partial';
613
+ case 'native_regressed_coverage_unavailable':
614
+ return 'native_history_unavailable';
615
+ case 'native_unavailable_read_error':
616
+ return extraDetail?.unavailableReason
617
+ ? `native_history_unavailable:${extraDetail.unavailableReason}`
618
+ : 'native_history_unavailable';
619
+ case 'native_unavailable_provider_unsupported':
620
+ return 'provider_native_transcript_not_supported';
621
+ case 'native_unavailable_empty':
622
+ return 'native_history_empty';
623
+ case 'native_unavailable_not_native_source':
624
+ return extraDetail?.nativeSource
625
+ ? `native_history_source_${extraDetail.nativeSource}`
626
+ : 'native_history_unavailable';
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Translate a native-history fetch result + provider/adapter context into a
632
+ * ChatSourceObservation and drive ChatSourceRegistry. Returns the decision
633
+ * together with the legacy messageSource payload so call sites can produce
634
+ * a v1-compatible response without duplicating the registry plumbing.
635
+ *
636
+ * This is the replacement for the 300-line if-ladder that previously lived
637
+ * inline in handleReadChat. It is intentionally split out for two reasons:
638
+ * (1) we will call it from two places (CLI adapter branch + history-only
639
+ * branch) instead of duplicating the ladder, (2) tests can drive it with
640
+ * synthetic native-history results to verify the cause→fallbackReason
641
+ * mapping without booting the whole readChat pipeline.
642
+ */
643
+ function decideCliReadChatSource(args: {
644
+ providerType: string;
645
+ provider?: ProviderModule;
646
+ sessionId: string;
647
+ nativeHistoryResult: any | null;
648
+ nativeHistoryError?: unknown;
649
+ safeMapping: boolean;
650
+ trustedExactNativeIdentity?: boolean;
651
+ sessionWorkspace?: string;
652
+ intendedWorkspace?: string;
653
+ ptyMessages: ChatMessage[];
654
+ ptyStatusApprovalOnly: boolean;
655
+ }): {
656
+ decision: ChatSourceDecision;
657
+ messageSource: Record<string, unknown>;
658
+ nativeMessages: ChatMessage[];
659
+ nativeSelected: boolean;
660
+ } {
661
+ const supportsNative = supportsCliNativeTranscript(args.providerType, args.provider);
662
+ const observation = buildObservationForCli(args, supportsNative);
663
+ const sessionKey = chatSourceSessionKey(args.providerType, args.sessionId);
664
+ let decision = CHAT_SOURCE_REGISTRY.observe(sessionKey, observation);
665
+
666
+ // A restored runtime can briefly expose different native slices while the
667
+ // provider transcript settles (for example, startup/system rows may be
668
+ // filtered after the first read). The source machine correctly treats a
669
+ // shrinking slice as regression, but an exact provider-session lookup with
670
+ // no PTY transcript has no safer fallback. Re-bootstrap only this proven
671
+ // identity so the chat does not disappear after daemon restart.
672
+ if (
673
+ decision.selected === 'pty-parser'
674
+ && args.trustedExactNativeIdentity === true
675
+ && args.safeMapping
676
+ && args.ptyMessages.length === 0
677
+ && observation.kind === 'native_present'
678
+ && observation.coverage !== 'partial'
679
+ && observation.messages.length > 0
680
+ ) {
681
+ CHAT_SOURCE_REGISTRY.clear(sessionKey);
682
+ decision = CHAT_SOURCE_REGISTRY.observe(sessionKey, observation);
683
+ }
684
+
685
+ const nativeMessages: ChatMessage[] = observation.kind === 'native_present'
686
+ ? extractNativeMessagesFromResult(args.providerType, args.nativeHistoryResult)
687
+ : [];
688
+
689
+ const nativeSource = typeof args.nativeHistoryResult?.source === 'string'
690
+ ? args.nativeHistoryResult.source
691
+ : undefined;
692
+ const sourcePath = typeof args.nativeHistoryResult?.sourcePath === 'string'
693
+ ? args.nativeHistoryResult.sourcePath
694
+ : undefined;
695
+ const sourceMtimeMs = typeof args.nativeHistoryResult?.sourceMtimeMs === 'number'
696
+ ? args.nativeHistoryResult.sourceMtimeMs
697
+ : undefined;
698
+ const coverageHint = typeof args.nativeHistoryResult?.nativeHistoryCoverage === 'string'
699
+ ? args.nativeHistoryResult.nativeHistoryCoverage
700
+ : undefined;
701
+ const partialReason = typeof args.nativeHistoryResult?.partialReason === 'string'
702
+ ? args.nativeHistoryResult.partialReason
703
+ : undefined;
704
+ const unavailableReason = typeof args.nativeHistoryResult?.unavailableReason === 'string'
705
+ ? args.nativeHistoryResult.unavailableReason
706
+ : args.nativeHistoryError
707
+ ? `error:${(args.nativeHistoryError as any)?.message || String(args.nativeHistoryError)}`
708
+ : undefined;
709
+ const nativeHandle = typeof args.nativeHistoryResult?.providerSessionId === 'string'
710
+ ? args.nativeHistoryResult.providerSessionId
711
+ : undefined;
712
+ const transcriptWorkspace = typeof args.nativeHistoryResult?.workspace === 'string'
713
+ ? args.nativeHistoryResult.workspace
714
+ : nativeMessages.map((m: any) => typeof m?.workspace === 'string' ? m.workspace.trim() : '').find(Boolean);
715
+
716
+ const fallbackReason = causeToLegacyFallbackReason(decision.transition.cause, decision.selected, {
717
+ unavailableReason,
718
+ nativeSource: nativeSource && nativeSource !== 'provider-native' ? nativeSource : undefined,
719
+ });
720
+
721
+ // ptyStatusApprovalOnly: when the machine selected native-history we
722
+ // suppress PTY content so the dashboard does not double-show messages
723
+ // already in the native transcript. When the machine selected
724
+ // pty-parser, PTY is the authoritative source — do NOT suppress it.
725
+ // Callers used to hard-code this to `nativeSelected first = true` which
726
+ // suppressed PTY content even when native was empty/unavailable, leaving
727
+ // the dashboard with zero visible messages (the codex generating/waiting
728
+ // approval stuck state). Trust the machine here, not the caller hint.
729
+ const ptyStatusApprovalOnly = decision.selected === 'native-history'
730
+ ? true
731
+ : args.ptyStatusApprovalOnly;
732
+
733
+ const messageSource = buildCliMessageSourceProvenance({
734
+ selected: decision.selected,
735
+ provider: args.providerType,
736
+ nativeHandle,
737
+ sessionWorkspace: args.sessionWorkspace,
738
+ intendedWorkspace: args.intendedWorkspace,
739
+ transcriptWorkspace,
740
+ fallbackReason,
741
+ nativeSource,
742
+ sourcePath,
743
+ sourceMtimeMs,
744
+ nativeHistoryCoverage: coverageHint,
745
+ partialReason,
746
+ unavailableReason,
747
+ nativeMessages,
748
+ ptyMessages: args.ptyMessages,
749
+ returnedMessages: decision.selected === 'native-history' ? nativeMessages : args.ptyMessages,
750
+ safeMapping: args.safeMapping,
751
+ // freshEnough is a v1 concept the machine does not model directly.
752
+ // We surface lockState.locked here so v1 consumers reading
753
+ // staleness.freshEnough still get a meaningful boolean.
754
+ freshEnough: decision.lockState.locked,
755
+ ptyStatusApprovalOnly,
756
+ });
757
+
758
+ return {
759
+ decision,
760
+ messageSource,
761
+ nativeMessages,
762
+ nativeSelected: decision.selected === 'native-history',
763
+ };
764
+ }
765
+
766
+ function buildObservationForCli(
767
+ args: {
768
+ providerType: string;
769
+ sessionId: string;
770
+ nativeHistoryResult: any | null;
771
+ nativeHistoryError?: unknown;
772
+ safeMapping: boolean;
773
+ },
774
+ supportsNative: boolean,
775
+ ): ChatSourceObservation {
776
+ if (!supportsNative) {
777
+ return { kind: 'native_unavailable', reason: 'provider_not_supported' };
778
+ }
779
+ if (args.nativeHistoryError) {
780
+ return { kind: 'native_unavailable', reason: 'read_error' };
781
+ }
782
+ const result = args.nativeHistoryResult;
783
+ if (!result || typeof result !== 'object') {
784
+ return { kind: 'native_unavailable', reason: 'read_error' };
785
+ }
786
+ const source = typeof result.source === 'string' ? result.source : '';
787
+ if (source && source !== 'provider-native') {
788
+ // 'native-unavailable' or other producer-side declined source.
789
+ return { kind: 'native_unavailable', reason: source === 'native-unavailable' ? 'empty' : 'not_native_source' };
790
+ }
791
+ const messages = Array.isArray(result.messages) ? result.messages : [];
792
+ if (messages.length === 0) {
793
+ return { kind: 'native_unavailable', reason: 'empty' };
794
+ }
795
+ const coverage = typeof result.nativeHistoryCoverage === 'string'
796
+ ? result.nativeHistoryCoverage
797
+ : 'tail';
798
+ if (coverage === 'unavailable') {
799
+ return { kind: 'native_unavailable', reason: 'coverage_unavailable' };
800
+ }
801
+ return buildV1NativePresentObservation({
802
+ providerType: args.providerType,
803
+ sessionId: args.sessionId,
804
+ messages,
805
+ coverage: coverage === 'full' || coverage === 'tail' || coverage === 'current-turn' || coverage === 'partial'
806
+ ? coverage
807
+ : 'tail',
808
+ safeMapping: args.safeMapping,
809
+ });
810
+ }
811
+
812
+ function extractNativeMessagesFromResult(providerType: string, result: any): ChatMessage[] {
813
+ if (!result || !Array.isArray(result.messages)) return [];
814
+ return normalizeNativeHistoryMessages(
815
+ providerType,
816
+ result.messages as ChatMessage[],
817
+ typeof result.providerSessionId === 'string' ? result.providerSessionId : undefined,
818
+ );
819
+ }
820
+
821
+ /**
822
+ * ptyStatusApprovalOnly is true when the daemon should treat PTY content as
823
+ * status/approval signal only (not as chat messages). v1 set this to `true`
824
+ * whenever native-history was selected as the source, and `false` otherwise.
825
+ * The machine equivalent: when native is the source we want PTY suppressed.
826
+ */
827
+ function primaryPtyApprovalOnlyFor(_cliType: string, nativeSelected: boolean): boolean {
828
+ return nativeSelected;
829
+ }
830
+
831
+ /**
832
+ * Codex-only unsafe-native fallback: when the primary native fetch produced
833
+ * unsafe-mapping data, v1 attempted to recover by reading exact runtime
834
+ * mirror messages, runtime input ACK messages, or by trusting the current-
835
+ * runtime PTY when safely attributed. None of this is the machine's
836
+ * responsibility — the machine already decided pty-parser. This helper
837
+ * preserves the daemon-side message selection and annotates messageSource.
838
+ */
839
+ function applyUnsafeNativeDaemonFallback(args: {
840
+ providerType: string;
841
+ adapter: CliAdapter;
842
+ helpers: CommandHelpers;
843
+ readChatArgs: any;
844
+ sessionWorkspace?: string;
845
+ intendedWorkspace?: string;
846
+ ptyMessages: ChatMessage[];
847
+ nativeHistoryLimit: number;
848
+ provider?: ProviderModule;
849
+ messageSourceRef: { set(value: Record<string, unknown>): void; get(): Record<string, unknown> };
850
+ apply(selection: {
851
+ messages: ChatMessage[];
852
+ transcriptAuthority?: 'provider' | 'daemon';
853
+ coverage?: 'full' | 'tail' | 'current-turn';
854
+ status?: string;
855
+ }): void;
856
+ activeModal: unknown;
857
+ returnedStatus: string;
858
+ coverage?: 'full' | 'tail' | 'current-turn';
859
+ }): void {
860
+ if (args.adapter.cliType !== 'codex-cli') {
861
+ // Only codex-cli had v1 daemon mirror recovery. Other providers skip.
862
+ return;
863
+ }
864
+ const ms = args.messageSourceRef.get();
865
+ const fallbackReason = typeof ms.fallbackReason === 'string' ? ms.fallbackReason : '';
866
+ if (!isUnsafeNativeTranscriptFallback(fallbackReason)) {
867
+ return;
868
+ }
869
+ const safeCurrentRuntimePtyMessages = isCurrentRuntimePtySafelyAttributed({
870
+ adapter: args.adapter,
871
+ helpers: args.helpers,
872
+ readChatArgs: args.readChatArgs,
873
+ sessionWorkspace: args.sessionWorkspace,
874
+ intendedWorkspace: args.intendedWorkspace,
875
+ ptyMessages: args.ptyMessages,
876
+ });
877
+ if (safeCurrentRuntimePtyMessages) {
878
+ args.apply({
879
+ messages: args.ptyMessages,
880
+ transcriptAuthority: 'daemon',
881
+ coverage: args.coverage || 'current-turn',
882
+ status: args.returnedStatus,
883
+ });
884
+ const next = { ...ms, selectedDaemonSource: 'current-runtime-pty', transcriptAuthority: 'daemon', runtimeMappingSafe: true };
885
+ args.messageSourceRef.set(next);
886
+ return;
887
+ }
888
+ const safeRuntimeAckMessages = selectRuntimeInputAckMessages(args.ptyMessages);
889
+ if (safeRuntimeAckMessages.length > 0) {
890
+ args.apply({
891
+ messages: safeRuntimeAckMessages,
892
+ transcriptAuthority: 'daemon',
893
+ coverage: 'tail',
894
+ status: coerceUnsafeNativeFallbackStatus(args.returnedStatus, args.activeModal),
895
+ });
896
+ const next = { ...ms, ptyStatusApprovalOnly: true };
897
+ args.messageSourceRef.set(next);
898
+ return;
899
+ }
900
+ const exactRuntimeMirrorMessages = readExactRuntimeMirrorMessages({
901
+ providerType: args.providerType,
902
+ targetSessionId: typeof args.readChatArgs?.targetSessionId === 'string' ? args.readChatArgs.targetSessionId : undefined,
903
+ currentSessionId: typeof (args.helpers.currentSession as any)?.sessionId === 'string' ? (args.helpers.currentSession as any).sessionId : undefined,
904
+ tailLimit: args.nativeHistoryLimit,
905
+ historyBehavior: args.provider?.historyBehavior,
906
+ });
907
+ if (exactRuntimeMirrorMessages.length > 0) {
908
+ args.apply({
909
+ messages: exactRuntimeMirrorMessages,
910
+ transcriptAuthority: 'daemon',
911
+ coverage: 'tail',
912
+ status: coerceUnsafeNativeFallbackStatus(args.returnedStatus, args.activeModal),
913
+ });
914
+ const next = { ...ms, selectedDaemonSource: 'exact-runtime-mirror', transcriptAuthority: 'daemon', ptyStatusApprovalOnly: true };
915
+ args.messageSourceRef.set(next);
916
+ return;
917
+ }
918
+ // No daemon mirror available — keep PTY messages as-is (still pty-parser
919
+ // selection); just coerce status for waiting_approval consistency.
920
+ args.apply({
921
+ messages: args.ptyMessages,
922
+ coverage: args.coverage,
923
+ status: coerceUnsafeNativeFallbackStatus(args.returnedStatus, args.activeModal),
924
+ });
925
+ const next = { ...ms, ptyStatusApprovalOnly: true };
926
+ args.messageSourceRef.set(next);
927
+ }
928
+
929
+ // (A2.2) buildNativeHistoryFallbackReason removed. ChatSourceMachine emits a
930
+ // ChatSourceTransitionCause; causeToLegacyFallbackReason maps it back to the
931
+ // v1 vocabulary for response compatibility. A3 deletes the v1 vocabulary
932
+ // entirely and surfaces stateTransition/lockState directly.
933
+
934
+ function isUnsafeNativeTranscriptFallback(reason?: string): boolean {
935
+ const value = String(reason || '').trim();
936
+ return value.startsWith('native_history_unavailable')
937
+ || value === 'native_history_not_safely_mapped'
938
+ || value === 'native_history_stale'
939
+ || value === 'native_history_partial';
940
+ }
941
+
942
+ function coerceUnsafeNativeFallbackStatus(status: string, activeModal: unknown): string {
943
+ if (status === 'waiting_approval' && activeModal) return status;
944
+ return 'idle';
945
+ }
946
+
947
+ function isRuntimeInputAckMessage(message: ChatMessage | undefined): boolean {
948
+ if (!message || typeof message !== 'object') return false;
949
+ const role = String((message as any).role || '').trim().toLowerCase();
950
+ if (role !== 'user' && role !== 'human') return false;
951
+ const meta = (message as any).meta;
952
+ return !!meta && typeof meta === 'object' && !Array.isArray(meta) && meta.runtimeInputAck === true;
953
+ }
954
+
955
+ function selectRuntimeInputAckMessages(messages: ChatMessage[]): ChatMessage[] {
956
+ return messages.filter((message) => isRuntimeInputAckMessage(message));
957
+ }
958
+
959
+ function readExactRuntimeMirrorMessages(args: {
960
+ providerType: string;
961
+ targetSessionId?: string;
962
+ currentSessionId?: string;
963
+ tailLimit: number;
964
+ historyBehavior?: ProviderModule['historyBehavior'];
965
+ }): ChatMessage[] {
966
+ const targetSessionId = String(args.targetSessionId || '').trim();
967
+ const currentSessionId = String(args.currentSessionId || '').trim();
968
+ if (!targetSessionId || targetSessionId !== currentSessionId) return [];
969
+
970
+ const history = readChatHistory(
971
+ args.providerType,
972
+ 0,
973
+ Math.max(args.tailLimit || 0, HOT_TAIL_MIN_LIMIT),
974
+ targetSessionId,
975
+ 0,
976
+ args.historyBehavior,
977
+ );
978
+ return normalizeChatMessages((history.messages || []) as ChatMessage[])
979
+ .filter((message) => {
980
+ const historySessionId = String((message as any).historySessionId || '').trim();
981
+ const instanceId = String((message as any).instanceId || '').trim();
982
+ return historySessionId === targetSessionId || instanceId === targetSessionId;
983
+ });
984
+ }
985
+
986
+ function normalizeComparableWorkspace(value: unknown): string {
987
+ const text = typeof value === 'string' ? value.trim() : '';
988
+ if (!text) return '';
989
+ return path.resolve(text);
990
+ }
991
+
992
+ function isCurrentRuntimePtySafelyAttributed(args: {
993
+ adapter: CliAdapter;
994
+ helpers: CommandHelpers;
995
+ readChatArgs: any;
996
+ sessionWorkspace?: string;
997
+ intendedWorkspace?: string;
998
+ ptyMessages: ChatMessage[];
999
+ }): boolean {
1000
+ if (args.adapter.cliType !== 'codex-cli') return false;
1001
+ if (!Array.isArray(args.ptyMessages) || args.ptyMessages.length === 0) return false;
1002
+ const targetSessionId = typeof args.readChatArgs?.targetSessionId === 'string'
1003
+ ? args.readChatArgs.targetSessionId.trim()
1004
+ : '';
1005
+ const currentSession = args.helpers.currentSession as any;
1006
+ const currentSessionId = typeof currentSession?.sessionId === 'string'
1007
+ ? currentSession.sessionId.trim()
1008
+ : '';
1009
+ if (!targetSessionId || !currentSessionId || targetSessionId !== currentSessionId) return false;
1010
+
1011
+ const runtimeMeta = typeof (args.adapter as any).getRuntimeMetadata === 'function'
1012
+ ? (args.adapter as any).getRuntimeMetadata()
1013
+ : null;
1014
+ const runtimeId = typeof runtimeMeta?.runtimeId === 'string' ? runtimeMeta.runtimeId.trim() : '';
1015
+ if (!runtimeId || runtimeId !== targetSessionId) return false;
1016
+ const surfaceKind = typeof runtimeMeta?.surfaceKind === 'string' ? runtimeMeta.surfaceKind : '';
1017
+ if (surfaceKind === 'inactive_record' || surfaceKind === 'recovery_snapshot') return false;
1018
+
1019
+ const sessionWorkspace = normalizeComparableWorkspace(args.sessionWorkspace);
1020
+ const adapterWorkspace = normalizeComparableWorkspace(args.adapter.workingDir);
1021
+ if (!sessionWorkspace || !adapterWorkspace || sessionWorkspace !== adapterWorkspace) return false;
1022
+ const intendedWorkspace = normalizeComparableWorkspace(args.intendedWorkspace);
1023
+ if (intendedWorkspace && intendedWorkspace !== sessionWorkspace) return false;
1024
+
1025
+ const registryEntry = args.helpers.ctx?.sessionRegistry?.get?.(targetSessionId) as any;
1026
+ const registryInstanceKey = typeof registryEntry?.adapterKey === 'string' && registryEntry.adapterKey.trim()
1027
+ ? registryEntry.adapterKey.trim()
1028
+ : typeof registryEntry?.instanceKey === 'string' && registryEntry.instanceKey.trim()
1029
+ ? registryEntry.instanceKey.trim()
1030
+ : '';
1031
+ if (registryInstanceKey) {
1032
+ const targetInstance = args.helpers.ctx?.instanceManager?.getInstance?.(registryInstanceKey);
1033
+ if (targetInstance) {
1034
+ const instanceType = typeof (targetInstance as any).type === 'string' ? (targetInstance as any).type : '';
1035
+ if (instanceType && instanceType !== args.adapter.cliType) return false;
1036
+ }
1037
+ }
1038
+
1039
+ return true;
1040
+ }
1041
+
1042
+ function supportsCliNativeTranscript(providerType: string, provider?: ProviderModule): boolean {
1043
+ // Preferred path: the provider module declares canonicalHistory in its
1044
+ // provider.json. We trust that declaration regardless of the legacy
1045
+ // allow-list. A2 will additionally require canonicalHistory.contractVersion
1046
+ // to be a supported value (transcript-v2.ts).
1047
+ if (provider?.category === 'cli' && isNativeSourceCanonicalHistory(provider?.nativeHistory)) {
1048
+ return true;
1049
+ }
1050
+ // Last-resort fallback for early call sites where the provider module is
1051
+ // not yet loaded. Warn once per provider type so this dependency is visible
1052
+ // and can be removed in A2.
1053
+ if (CLI_NATIVE_TRANSCRIPT_PROVIDERS.has(providerType)) {
1054
+ warnLegacyNativeAllowlistHit(providerType);
1055
+ return true;
1056
+ }
1057
+ return false;
1058
+ }
1059
+
1060
+ function getComparableVisibleText(message: ChatMessage | undefined): string {
1061
+ if (!message) return '';
1062
+ const role = String((message as any).role || '').trim().toLowerCase();
1063
+ if (role !== 'user' && role !== 'assistant') return '';
1064
+ const kind = String((message as any).kind || 'standard').trim().toLowerCase();
1065
+ if (kind && kind !== 'standard') return '';
1066
+ const content = flattenContent((message as any).content).replace(/\s+/g, ' ').trim();
1067
+ return content;
1068
+ }
1069
+
1070
+ function hasOverlappingVisibleConversationText(nativeMessages: ChatMessage[], ptyMessages: ChatMessage[]): boolean {
1071
+ const nativeTexts = nativeMessages.map(getComparableVisibleText).filter(Boolean);
1072
+ const ptyTexts = ptyMessages.map(getComparableVisibleText).filter(Boolean);
1073
+ if (nativeTexts.length === 0 || ptyTexts.length === 0) return false;
1074
+ for (const nativeText of nativeTexts) {
1075
+ for (const ptyText of ptyTexts) {
1076
+ if (nativeText === ptyText) return true;
1077
+ const shorter = nativeText.length <= ptyText.length ? nativeText : ptyText;
1078
+ const longer = nativeText.length <= ptyText.length ? ptyText : nativeText;
1079
+ if (shorter.length >= 32 && longer.includes(shorter)) return true;
1080
+ }
1081
+ }
1082
+ return false;
1083
+ }
1084
+
1085
+ function hasSafeNativeHistoryMapping(args: {
1086
+ historySessionId?: string;
1087
+ providerSessionId?: string;
1088
+ workspace?: string;
1089
+ nativeMessages: ChatMessage[];
1090
+ ptyMessages?: ChatMessage[];
1091
+ requireWorkspaceContentOverlap?: boolean;
1092
+ }): boolean {
1093
+ const isCoordinatorTranscript = args.nativeMessages.some((m: any) => {
1094
+ const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '');
1095
+ return text.includes('mesh_send_task') || text.includes('mesh_status') || text.includes('mesh_read_chat') || text.includes('mesh_launch_session');
1096
+ });
1097
+
1098
+ const explicitSessionId = String(args.historySessionId || args.providerSessionId || '').trim();
1099
+ if (explicitSessionId) {
1100
+ const expectedWorkspace = normalizeComparableWorkspace(args.workspace);
1101
+ const declaredWorkspaces = args.nativeMessages
1102
+ .map((message: any) => normalizeComparableWorkspace(message?.workspace))
1103
+ .filter(Boolean);
1104
+ if (
1105
+ expectedWorkspace
1106
+ && declaredWorkspaces.length > 0
1107
+ && !declaredWorkspaces.some((workspace) => workspace === expectedWorkspace)
1108
+ ) {
1109
+ return false;
1110
+ }
1111
+ const messageSessionIds = args.nativeMessages
1112
+ .map((message: any) => typeof message?.historySessionId === 'string' ? message.historySessionId.trim() : '')
1113
+ .filter(Boolean);
1114
+ if (messageSessionIds.length > 0) {
1115
+ return messageSessionIds.some((id) => id === explicitSessionId);
1116
+ }
1117
+
1118
+ // Messages carry no historySessionId — cannot confirm they belong to the requested session.
1119
+ // Only allow a coordinator transcript that is also confirmed by the PTY side; otherwise
1120
+ // fail closed so a same-workspace session's history is never silently accepted.
1121
+ if (isCoordinatorTranscript && args.ptyMessages && args.ptyMessages.length > 0) {
1122
+ const ptyHasCoordinator = args.ptyMessages.some((m: any) => {
1123
+ const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '');
1124
+ return text.includes('mesh_send_task') || text.includes('mesh_status') || text.includes('mesh_read_chat');
1125
+ });
1126
+ return ptyHasCoordinator;
1127
+ }
1128
+
1129
+ // No historySessionId in messages and no coordinator cross-check: fail closed.
1130
+ // Workspace-only matching must not override an explicit session identity.
1131
+ return false;
1132
+ }
1133
+ const workspace = String(args.workspace || '').trim();
1134
+ if (!workspace) return false;
1135
+ const workspaceMatches = args.nativeMessages.some((message: any) => String(message?.workspace || '').trim() === workspace);
1136
+ if (!workspaceMatches) return false;
1137
+
1138
+ if (isCoordinatorTranscript && args.ptyMessages && args.ptyMessages.length > 0) {
1139
+ const ptyHasCoordinator = args.ptyMessages.some((m: any) => {
1140
+ const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '');
1141
+ return text.includes('mesh_send_task') || text.includes('mesh_status') || text.includes('mesh_read_chat');
1142
+ });
1143
+ if (!ptyHasCoordinator) {
1144
+ return false;
1145
+ }
1146
+ }
1147
+
1148
+ if (!args.requireWorkspaceContentOverlap) return true;
1149
+ return hasOverlappingVisibleConversationText(args.nativeMessages, args.ptyMessages || []);
1150
+ }
1151
+
1152
+ // Provenance boundary: workspace-only native history lookup is never safe
1153
+ // because multiple concurrent sessions sharing the same cwd would alias each
1154
+ // other. historySessionId (the provider-native session key) is required to
1155
+ // establish ownership. hasSafeNativeHistoryMapping() enforces the same
1156
+ // invariant after the read; both guards must hold for native history to be used.
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".
1165
+ */
1166
+ function sessionStartedAtMsFromRegistry(h: CommandHelpers, targetSessionId: string | undefined): number | undefined {
1167
+ const sid = typeof targetSessionId === 'string' ? targetSessionId.trim() : '';
1168
+ if (!sid) return undefined;
1169
+ const target = h.ctx?.sessionRegistry?.get?.(sid);
1170
+ return typeof target?.spawnedAtMs === 'number' ? target.spawnedAtMs : undefined;
1171
+ }
1172
+
1173
+ /**
1174
+ * Pull the env vars the daemon set when it spawned this session's CLI.
1175
+ * Mesh coordinator points hermes at a per-coordinator HERMES_HOME so
1176
+ * the native-history reader needs that override to find the right
1177
+ * state.db; without it the reader sees ~/.hermes/state.db and misses
1178
+ * every coordinator-session transcript.
1179
+ *
1180
+ * Returns undefined when no SpecCliAdapter is in play (legacy
1181
+ * providers / CDP) or when the adapter exposes no spawn env.
1182
+ */
1183
+ function sessionSpawnEnvFromAdapter(h: CommandHelpers, targetSessionId: string | undefined): Record<string, string> | undefined {
1184
+ const adapter = getTargetedCliAdapter(h, { targetSessionId }, undefined);
1185
+ if (!adapter || typeof adapter.getRuntimeMetadata !== 'function') return undefined;
1186
+ const meta = adapter.getRuntimeMetadata() as Record<string, unknown> | undefined;
1187
+ const env = meta && typeof meta === 'object' ? (meta as Record<string, unknown>).spawnedEnv : undefined;
1188
+ return env && typeof env === 'object' ? env as Record<string, string> : undefined;
1189
+ }
1190
+
1191
+
1192
+ function readCliProviderNativeHistory(agentStr: string, args: {
1193
+ canonicalHistory?: ProviderModule['canonicalHistory'];
1194
+ historySessionId?: string;
1195
+ workspace?: string;
1196
+ offset: number;
1197
+ limit: number;
1198
+ excludeRecentCount: number;
1199
+ historyBehavior?: ProviderModule['historyBehavior'];
1200
+ scripts?: ProviderScripts;
1201
+ excludeInProgressTurn?: boolean;
1202
+ sessionStartedAtMs?: number;
1203
+ envOverrides?: Record<string, string>;
1204
+ }): ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' } {
1205
+ const canBindFromLiveSession = !args.historySessionId
1206
+ && typeof args.sessionStartedAtMs === 'number'
1207
+ && args.sessionStartedAtMs > 0
1208
+ && typeof args.workspace === 'string'
1209
+ && args.workspace.trim().length > 0;
1210
+ if (!args.historySessionId && !canBindFromLiveSession) {
1211
+ return {
1212
+ messages: [],
1213
+ hasMore: false,
1214
+ source: 'native-unavailable',
1215
+ unavailableReason: 'native_history_workspace_only_lookup_unsafe',
1216
+ lookup: 'session',
1217
+ } as ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' };
1218
+ }
1219
+ const sessionHistory = readProviderChatHistory(agentStr, {
1220
+ canonicalHistory: args.canonicalHistory,
1221
+ historySessionId: args.historySessionId,
1222
+ workspace: args.workspace,
1223
+ offset: args.offset,
1224
+ limit: args.limit,
1225
+ excludeRecentCount: args.excludeRecentCount,
1226
+ historyBehavior: args.historyBehavior,
1227
+ scripts: args.scripts as any,
1228
+ excludeInProgressTurn: args.excludeInProgressTurn,
1229
+ sessionStartedAtMs: args.sessionStartedAtMs,
1230
+ envOverrides: args.envOverrides,
1231
+ });
1232
+ const boundProviderSessionId = typeof (sessionHistory as any)?.providerSessionId === 'string'
1233
+ ? (sessionHistory as any).providerSessionId.trim()
1234
+ : '';
1235
+ // A fresh live session can be bound without a provider id when the native
1236
+ // reader matched both cwd and session_meta.timestamp to spawnedAtMs.
1237
+ return {
1238
+ ...(sessionHistory as any),
1239
+ lookup: args.historySessionId || (canBindFromLiveSession && boundProviderSessionId)
1240
+ ? 'session'
1241
+ : 'workspace',
1242
+ };
1243
+ }
1244
+
1245
+ function readLiveCodexWorkspaceNativeHistory(agentStr: string, args: {
1246
+ canonicalHistory?: ProviderModule['canonicalHistory'];
1247
+ workspace?: string;
1248
+ offset: number;
1249
+ limit: number;
1250
+ excludeRecentCount: number;
1251
+ historyBehavior?: ProviderModule['historyBehavior'];
1252
+ scripts?: ProviderScripts;
1253
+ }): (ReturnType<typeof readProviderChatHistory> & { lookup: 'workspace' }) | null {
1254
+ if (agentStr !== 'codex-cli') return null;
1255
+ const workspace = typeof args.workspace === 'string' ? args.workspace.trim() : '';
1256
+ if (!workspace) return null;
1257
+ const history = readProviderChatHistory(agentStr, {
1258
+ canonicalHistory: args.canonicalHistory,
1259
+ workspace,
1260
+ offset: args.offset,
1261
+ limit: args.limit,
1262
+ excludeRecentCount: args.excludeRecentCount,
1263
+ historyBehavior: args.historyBehavior,
1264
+ scripts: args.scripts as any,
1265
+ });
1266
+ return { ...(history as any), lookup: 'workspace' };
1267
+ }
1268
+
1269
+ // (A2.2) isNativeHistoryFreshEnough removed. The v1 freshness comparison
1270
+ // (native_newest vs pty_newest with a 5-minute mtime grace window) was the
1271
+ // direct cause of the plipping behaviour: PTY arrived every turn so native
1272
+ // looked stale by default. ChatSourceMachine never compares native vs PTY
1273
+ // freshness — the lock holds across arbitrary PTY arrival. See
1274
+ // chat/source-machine.ts for the new semantics.
1275
+
1276
+ function shouldPreserveReadChatPayloadField(key: string): boolean {
1277
+ return key === 'messageSource' || key === 'transcriptProvenance';
1278
+ }
1279
+
1280
+ function updateMessageSourceReturnedCount(value: unknown, returnedMessageCount: number): unknown {
1281
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return value;
1282
+ const record = value as Record<string, unknown>;
1283
+ const coverage = record.coverage && typeof record.coverage === 'object' && !Array.isArray(record.coverage)
1284
+ ? record.coverage as Record<string, unknown>
1285
+ : undefined;
1286
+ if (!coverage) return value;
1287
+ return {
1288
+ ...record,
1289
+ coverage: {
1290
+ ...coverage,
1291
+ returnedMessageCount,
1292
+ },
1293
+ };
1294
+ }
224
1295
 
225
1296
  function deriveHistoryDedupKey(message: ChatMessage & { _unitKey?: string; _turnKey?: string }): string | undefined {
226
1297
  const unitKey = typeof message._unitKey === 'string' ? message._unitKey.trim() : '';
@@ -281,18 +1352,46 @@ function normalizeReadChatCommandStatus(status: unknown, activeModal: unknown):
281
1352
  }
282
1353
  switch (raw) {
283
1354
  case 'starting':
284
- return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'generating';
1355
+ return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'starting';
285
1356
  case 'stopped':
286
1357
  case 'disconnected':
287
1358
  case 'not_monitored':
288
1359
  return 'error';
1360
+ case 'waiting_approval':
1361
+ // The contract validator requires activeModal+buttons whenever
1362
+ // status is waiting_approval. If a producer/coercer set this
1363
+ // status without staging the modal yet (a race we hit with
1364
+ // codex-cli during tool approval setup), downgrade to a
1365
+ // generating-like status so readChat still returns successfully.
1366
+ // The next poll will pick up the modal once the provider has
1367
+ // emitted it.
1368
+ return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'generating';
289
1369
  default:
290
1370
  return raw;
291
1371
  }
292
1372
  }
293
1373
 
294
1374
  function isGeneratingLikeStatus(status: unknown): boolean {
295
- return status === 'generating' || status === 'streaming' || status === 'long_generating' || status === 'starting';
1375
+ return status === 'generating' || status === 'streaming' || status === 'no_progress' || status === 'long_generating' || status === 'starting';
1376
+ }
1377
+
1378
+ function hasVisibleAssistantMessage(messages: unknown[] | undefined): boolean {
1379
+ if (!Array.isArray(messages)) return false;
1380
+ return messages.some((message: any) => {
1381
+ if (!message || message.role !== 'assistant') return false;
1382
+ const kind = typeof message.kind === 'string' ? message.kind : 'standard';
1383
+ if (kind !== 'standard') return false;
1384
+ return String(message.content || '').trim().length > 0;
1385
+ });
1386
+ }
1387
+
1388
+ function hasFinalVisibleAssistantMessage(messages: unknown[] | undefined): boolean {
1389
+ if (!Array.isArray(messages)) return false;
1390
+ const visible = filterUserFacingChatMessages(messages as ChatMessage[]);
1391
+ const last = visible[visible.length - 1] as ChatMessage | undefined;
1392
+ const role = typeof last?.role === 'string' ? last.role.trim().toLowerCase() : '';
1393
+ const content = last ? flattenContent(last.content).trim() : '';
1394
+ return (role === 'assistant' || role === 'model') && content.length > 0;
296
1395
  }
297
1396
 
298
1397
  function shouldTrustCliAdapterTerminalStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): boolean {
@@ -304,7 +1403,26 @@ function shouldTrustCliAdapterTerminalStatus(parsedStatus: unknown, activeModal:
304
1403
  return true;
305
1404
  }
306
1405
 
307
- function normalizeCliReadChatStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): string {
1406
+ function normalizeCliReadChatStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any, parsedMessages?: unknown[]): string {
1407
+ const adapterRawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
1408
+ if (adapterRawStatus === 'starting'
1409
+ && isGeneratingLikeStatus(parsedStatus)
1410
+ && !hasNonEmptyModalButtons(activeModal)
1411
+ && Array.isArray(parsedMessages)
1412
+ && parsedMessages.length === 0
1413
+ && Array.isArray(adapterStatus?.messages)
1414
+ && adapterStatus.messages.length === 0
1415
+ && !(typeof adapter.isProcessing === 'function' && adapter.isProcessing())) {
1416
+ return 'starting';
1417
+ }
1418
+ if (
1419
+ isGeneratingLikeStatus(adapterRawStatus)
1420
+ && parsedStatus === 'idle'
1421
+ && !hasNonEmptyModalButtons(activeModal)
1422
+ && !hasVisibleAssistantMessage(parsedMessages)
1423
+ ) {
1424
+ return adapterRawStatus;
1425
+ }
308
1426
  if (shouldTrustCliAdapterTerminalStatus(parsedStatus, activeModal, adapter, adapterStatus)) return 'idle';
309
1427
  return typeof parsedStatus === 'string' && parsedStatus.trim() ? parsedStatus : 'idle';
310
1428
  }
@@ -325,7 +1443,65 @@ function finalizeStreamingMessagesWhenIdle(messages: ChatMessage[], status: stri
325
1443
  });
326
1444
  }
327
1445
 
328
- function buildReadChatCommandResult(payload: Record<string, any>, args: any): CommandResult {
1446
+ /**
1447
+ * Collapse adjacent PTY messages whose canonical (whitespace-stripped)
1448
+ * content is identical, OR whose turn key + role/kind match.
1449
+ *
1450
+ * The PTY parser of some providers (hermes-cli observed in the wild)
1451
+ * emits the same logical assistant turn twice when the terminal re-wraps
1452
+ * the text at a different column. The two emissions differ in newline
1453
+ * position — and sometimes in a single inserted space next to punctuation
1454
+ * (e.g. `(수정 2개), upstream` vs `(수정 2개 ), upstream`), so a simple
1455
+ * `\s+ -> ' '` normalize cannot collapse them.
1456
+ *
1457
+ * Strategy:
1458
+ * 1. If both messages carry the same _turnKey + role + kind, they are
1459
+ * the same logical turn by construction. Collapse.
1460
+ * 2. Otherwise compare with all whitespace stripped — wrap variants
1461
+ * collapse to identical strings.
1462
+ *
1463
+ * Native-history paths run through pageHistoryRecords and already
1464
+ * collapse on a normalized signature; this helper is the PTY equivalent
1465
+ * the readChat sync path was missing.
1466
+ */
1467
+ function collapseAdjacentDuplicateChatMessages(messages: ChatMessage[]): ChatMessage[] {
1468
+ if (!Array.isArray(messages) || messages.length <= 1) return messages;
1469
+ const result: ChatMessage[] = [];
1470
+ let prevRoleKind = '';
1471
+ let prevStripped = '';
1472
+ for (const message of messages) {
1473
+ const role = typeof message.role === 'string' ? message.role : '';
1474
+ const kind = typeof message.kind === 'string' ? message.kind : 'standard';
1475
+ const content = typeof message.content === 'string'
1476
+ ? message.content
1477
+ : (Array.isArray(message.content) ? message.content.map((p: any) => typeof p?.text === 'string' ? p.text : '').join('') : '');
1478
+ const strippedContent = content.replace(/\s+/g, '');
1479
+ // Empty content or system messages are passed through untouched.
1480
+ if (!strippedContent || role === 'system') {
1481
+ result.push(message);
1482
+ prevRoleKind = '';
1483
+ prevStripped = '';
1484
+ continue;
1485
+ }
1486
+ const roleKind = `${role}:${kind}`;
1487
+ const sameStripped = strippedContent === prevStripped && roleKind === prevRoleKind;
1488
+ if (result.length > 0 && sameStripped) {
1489
+ // Adjacent duplicate after stripping all whitespace. Keep the
1490
+ // *later* copy because PTY's last emission usually has the most
1491
+ // complete formatting.
1492
+ result[result.length - 1] = message;
1493
+ prevRoleKind = roleKind;
1494
+ prevStripped = strippedContent;
1495
+ continue;
1496
+ }
1497
+ result.push(message);
1498
+ prevRoleKind = roleKind;
1499
+ prevStripped = strippedContent;
1500
+ }
1501
+ return result;
1502
+ }
1503
+
1504
+ function buildReadChatCommandResult(payload: Record<string, any>, args: any, h?: CommandHelpers): CommandResult {
329
1505
  let validatedPayload: Record<string, any>;
330
1506
  const debugReadChat = payload?.debugReadChat && typeof payload.debugReadChat === 'object'
331
1507
  ? payload.debugReadChat
@@ -339,9 +1515,40 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
339
1515
  return { success: false, error: error?.message || String(error) };
340
1516
  }
341
1517
  const messages = normalizeReadChatMessages(validatedPayload);
342
- const visibleMessages = filterUserFacingChatMessages(messages);
1518
+ // Last-mile coordinator-prompt filter. Different read_chat code paths
1519
+ // produce the final messages array (native-history main path, codex
1520
+ // exact-runtime-mirror fallback, daemon-side pty-parser, etc), so
1521
+ // applying it here means we don't have to thread the filter through
1522
+ // every one. Driven by the provider setting `showCoordinatorSystemPrompt`
1523
+ // + the coordinator-registry entry for the target session.
1524
+ const sessionIdHint = typeof args?.targetSessionId === 'string' ? args.targetSessionId
1525
+ : typeof args?.sessionId === 'string' ? args.sessionId
1526
+ : '';
1527
+ const providerHint = typeof args?.cliType === 'string' ? args.cliType
1528
+ : typeof args?.providerType === 'string' ? args.providerType
1529
+ : typeof args?.agentType === 'string' ? args.agentType
1530
+ : '';
1531
+ const filteredMessages = h
1532
+ ? maybeHideCoordinatorPromptMessage(h, providerHint, sessionIdHint, messages)
1533
+ : messages;
1534
+ // By default read_chat returns only user-facing prose turns. When the
1535
+ // caller opts in with `includeActivity`, tool/terminal/thought activity
1536
+ // bubbles (e.g. the native transcript's tool calls and results) are kept
1537
+ // inline too, in chronological order, so a restored conversation can show
1538
+ // what the agent actually did — not just the prose around it.
1539
+ const includeActivity = args?.includeActivity === true || args?.includeActivity === 'true';
1540
+ const visibleMessages = includeActivity
1541
+ ? filteredMessages.filter((m) => isUserFacingChatMessage(m) || isActivityChatMessage(m))
1542
+ : filterUserFacingChatMessages(filteredMessages);
343
1543
  const sync = buildFullTail(visibleMessages, normalizeReadChatTailLimit(args));
344
1544
  const hiddenMsgCount = Math.max(0, messages.length - visibleMessages.length);
1545
+ const preservedPayloadFields = Object.fromEntries(Object.entries(payload).filter(([key]) => shouldPreserveReadChatPayloadField(key)));
1546
+ if (preservedPayloadFields.messageSource) {
1547
+ preservedPayloadFields.messageSource = updateMessageSourceReturnedCount(preservedPayloadFields.messageSource, sync.messages.length);
1548
+ }
1549
+ if (preservedPayloadFields.transcriptProvenance) {
1550
+ preservedPayloadFields.transcriptProvenance = updateMessageSourceReturnedCount(preservedPayloadFields.transcriptProvenance, sync.messages.length);
1551
+ }
345
1552
  const returnedDebugReadChat = debugReadChat
346
1553
  ? {
347
1554
  ...debugReadChat,
@@ -356,6 +1563,7 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
356
1563
  return {
357
1564
  success: true,
358
1565
  ...validatedPayload,
1566
+ ...preservedPayloadFields,
359
1567
  messages: sync.messages,
360
1568
  totalMessages: sync.totalMessages,
361
1569
  ...(returnedDebugReadChat ? { debugReadChat: returnedDebugReadChat } : {}),
@@ -456,7 +1664,7 @@ function summarizeProviderForDebug(provider: ProviderModule | undefined): Record
456
1664
  name: provider.name,
457
1665
  category: provider.category,
458
1666
  version: (provider as any).version,
459
- canonicalHistory: provider.canonicalHistory,
1667
+ canonicalHistory: provider.nativeHistory,
460
1668
  historyBehavior: provider.historyBehavior,
461
1669
  webviewMatchText: provider.webviewMatchText,
462
1670
  scriptNames: scripts,
@@ -508,8 +1716,10 @@ function summarizeStateForDebug(state: any): Record<string, unknown> | null {
508
1716
  title: activeChat.title,
509
1717
  messageCount: Array.isArray(activeChat.messages) ? activeChat.messages.length : undefined,
510
1718
  activeModal: activeChat.activeModal,
1719
+ activeInteractivePrompt: activeChat.activeInteractivePrompt ?? null,
511
1720
  messagesTail: Array.isArray(activeChat.messages) ? activeChat.messages.slice(-10) : undefined,
512
1721
  } : null,
1722
+ activeInteractivePrompt: (state as { activeInteractivePrompt?: unknown }).activeInteractivePrompt ?? null,
513
1723
  controlValues: state.controlValues,
514
1724
  summaryMetadata: state.summaryMetadata,
515
1725
  };
@@ -584,6 +1794,8 @@ function buildChatDebugBundleSummary(bundle: Record<string, unknown>): Record<st
584
1794
  adapterStatus: debugReadChat.adapterStatus,
585
1795
  parsedStatus: debugReadChat.parsedStatus,
586
1796
  returnedStatus: debugReadChat.returnedStatus,
1797
+ selectedMessageSource: debugReadChat.selectedMessageSource,
1798
+ messageSource: debugReadChat.messageSource,
587
1799
  parsedMsgCount: debugReadChat.parsedMsgCount,
588
1800
  returnedMsgCount: debugReadChat.returnedMsgCount,
589
1801
  shouldPreferAdapterMessages: debugReadChat.shouldPreferAdapterMessages,
@@ -646,11 +1858,20 @@ export async function handleGetChatDebugBundle(h: CommandHelpers, args: any): Pr
646
1858
  providerSessionId: readResult.providerSessionId,
647
1859
  transcriptAuthority: readResult.transcriptAuthority,
648
1860
  coverage: readResult.coverage,
1861
+ messageSource: readResult.messageSource,
1862
+ transcriptProvenance: readResult.transcriptProvenance,
649
1863
  activeModal: readResult.activeModal,
650
1864
  messagesTail: Array.isArray(readResult.messages) ? readResult.messages.slice(-20) : [],
651
1865
  debugReadChat: readResult.debugReadChat,
652
1866
  }
653
- : { success: false, error: readResult.error };
1867
+ : {
1868
+ success: false,
1869
+ error: readResult.error,
1870
+ code: readResult.code,
1871
+ messageSource: readResult.messageSource,
1872
+ transcriptProvenance: readResult.transcriptProvenance,
1873
+ debugReadChat: readResult.debugReadChat,
1874
+ };
654
1875
  } catch (error: any) {
655
1876
  readChat = { success: false, error: error?.message || String(error) };
656
1877
  }
@@ -693,6 +1914,9 @@ export async function handleGetChatDebugBundle(h: CommandHelpers, args: any): Pr
693
1914
  ready: typeof adapter.isReady === 'function' ? adapter.isReady() : undefined,
694
1915
  processing: typeof adapter.isProcessing === 'function' ? adapter.isProcessing() : undefined,
695
1916
  debugSnapshot: adapterDebugSnapshot,
1917
+ scriptInvocationTrace: typeof (adapter as any).getScriptInvocationTrace === 'function'
1918
+ ? (adapter as any).getScriptInvocationTrace()
1919
+ : undefined,
696
1920
  } : null,
697
1921
  readChat,
698
1922
  frontend: args?.frontendSnapshot && typeof args.frontendSnapshot === 'object' ? args.frontendSnapshot : null,
@@ -817,16 +2041,58 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
817
2041
  : typeof (h.currentSession as any)?.workspace === 'string'
818
2042
  ? (h.currentSession as any).workspace
819
2043
  : undefined;
820
- const result = readProviderChatHistory(agentStr, {
821
- canonicalHistory: provider?.canonicalHistory,
822
- historySessionId,
823
- workspace,
824
- offset: offset || 0,
825
- limit: limit || 30,
826
- excludeRecentCount,
827
- historyBehavior: provider?.historyBehavior,
828
- scripts: provider?.scripts as any,
829
- });
2044
+ const exactNativeHistoryScope = Boolean(
2045
+ (typeof args?.targetSessionId === 'string' && args.targetSessionId.trim())
2046
+ || (typeof args?.historySessionId === 'string' && args.historySessionId.trim())
2047
+ || (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
2048
+ );
2049
+ const result = supportsCliNativeTranscript(agentStr, provider) && isNativeSourceCanonicalHistory(provider?.nativeHistory)
2050
+ ? readCliProviderNativeHistory(agentStr, {
2051
+ canonicalHistory: provider?.nativeHistory,
2052
+ historySessionId,
2053
+ workspace,
2054
+ offset: offset || 0,
2055
+ limit: limit || 30,
2056
+ excludeRecentCount,
2057
+ historyBehavior: provider?.historyBehavior,
2058
+ scripts: provider?.scripts as any,
2059
+ sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
2060
+ envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
2061
+ })
2062
+ : readProviderChatHistory(agentStr, {
2063
+ canonicalHistory: provider?.nativeHistory,
2064
+ historySessionId,
2065
+ workspace,
2066
+ offset: offset || 0,
2067
+ limit: limit || 30,
2068
+ excludeRecentCount,
2069
+ historyBehavior: provider?.historyBehavior,
2070
+ scripts: provider?.scripts as any,
2071
+ });
2072
+ if (supportsCliNativeTranscript(agentStr, provider) && isNativeSourceCanonicalHistory(provider?.nativeHistory)) {
2073
+ const lookup = (result as any).lookup === 'workspace' ? 'workspace' : 'session';
2074
+ const messages = Array.isArray((result as any).messages)
2075
+ ? normalizeAndFilterNativeHistory(h, agentStr, args, (result as any).messages as ChatMessage[], (result as any)?.providerSessionId)
2076
+ : [];
2077
+ const historyProviderSessionId = typeof (result as any)?.providerSessionId === 'string'
2078
+ ? (result as any).providerSessionId
2079
+ : readHistorySessionIdFromMessages(messages) || historySessionId;
2080
+ const safeMapping = hasSafeNativeHistoryMapping({
2081
+ historySessionId: lookup === 'workspace' ? undefined : historySessionId,
2082
+ providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
2083
+ workspace,
2084
+ nativeMessages: messages,
2085
+ });
2086
+ if ((result as any).source === 'provider-native' && messages.length > 0 && !safeMapping) {
2087
+ return {
2088
+ success: true,
2089
+ messages: [],
2090
+ hasMore: false,
2091
+ source: 'native-unavailable',
2092
+ agent: agentStr,
2093
+ };
2094
+ }
2095
+ }
830
2096
  return { success: true, ...result, agent: agentStr };
831
2097
  } catch (e: any) {
832
2098
  return { success: false, error: e.message };
@@ -834,7 +2100,25 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
834
2100
  }
835
2101
 
836
2102
  export async function handleReadChat(h: CommandHelpers, args: any): Promise<CommandResult> {
837
- const provider = h.getProvider(args?.agentType || args?.providerType);
2103
+ // Resolve provider in order: explicit agentType/providerType > registered session.
2104
+ // Without this fallback, callers that only have a sessionId (e.g. a chat tail
2105
+ // controller that just got handed a session ID over WS) get an empty result
2106
+ // because getProvider(undefined) returns undefined and the rest of the pipeline
2107
+ // bails. This makes the UI look like the session "disappeared".
2108
+ let providerHint: string | undefined = args?.agentType || args?.providerType;
2109
+ if (!providerHint) {
2110
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
2111
+ if (targetSessionId) {
2112
+ const session = (h.ctx as any)?.sessionRegistry?.get?.(targetSessionId);
2113
+ if (session && typeof session.providerType === 'string') {
2114
+ providerHint = session.providerType;
2115
+ }
2116
+ }
2117
+ if (!providerHint && h.currentSession?.providerType) {
2118
+ providerHint = h.currentSession.providerType;
2119
+ }
2120
+ }
2121
+ const provider = h.getProvider(providerHint);
838
2122
  const transport = getTargetTransport(h, provider);
839
2123
  const historySessionId = getHistorySessionId(h, args);
840
2124
 
@@ -874,66 +2158,518 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
874
2158
  ? parsedRecord.coverage
875
2159
  : undefined;
876
2160
  const activeModal = parsedRecord.activeModal ?? parsedRecord.modal ?? null;
877
- const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus);
2161
+ const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus, parsedRecord.messages);
878
2162
  const runtimeMessageMerger = getTargetInstance(h, args) as RuntimeChatMessageMerger | null;
879
- const parsedMessages = finalizeStreamingMessagesWhenIdle(parsedRecord.messages as ChatMessage[], returnedStatus);
2163
+ const parsedMessages = collapseAdjacentDuplicateChatMessages(
2164
+ finalizeStreamingMessagesWhenIdle(parsedRecord.messages as ChatMessage[], returnedStatus),
2165
+ );
880
2166
  const returnedMessages = runtimeMessageMerger?.category === 'cli'
881
2167
  && runtimeMessageMerger.type === adapter.cliType
882
2168
  && typeof runtimeMessageMerger.mergeRuntimeChatMessages === 'function'
883
2169
  ? runtimeMessageMerger.mergeRuntimeChatMessages(parsedMessages)
884
2170
  : parsedMessages;
2171
+ const providerType = provider?.type || adapter.cliType;
2172
+ let selectedMessages = returnedMessages;
2173
+ let selectedTitle = title;
2174
+ let selectedProviderSessionId = providerSessionId;
2175
+ let selectedTranscriptAuthority = transcriptAuthority;
2176
+ let selectedCoverage = coverage;
2177
+ let selectedStatus = returnedStatus;
2178
+ const _targetSidForWs = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
2179
+ const _registryWs = _targetSidForWs
2180
+ ? (h.ctx?.sessionRegistry?.get?.(_targetSidForWs) as any)?.workspace
2181
+ : undefined;
2182
+ const _currentSessionWs = typeof (h.currentSession as any)?.workspace === 'string'
2183
+ ? (h.currentSession as any).workspace
2184
+ : typeof adapter.workingDir === 'string'
2185
+ ? adapter.workingDir
2186
+ : undefined;
2187
+ const sessionWorkspace = _targetSidForWs
2188
+ ? (typeof _registryWs === 'string' ? _registryWs : (typeof args?.workspace === 'string' ? args.workspace : undefined) ?? _currentSessionWs)
2189
+ : _currentSessionWs;
2190
+ const intendedWorkspace = typeof args?.workspace === 'string' ? args.workspace : undefined;
2191
+ // ───────────────────────────────────────────────────────────
2192
+ // Chat source decision via ChatSourceMachine (A2 big-bang).
2193
+ // Replaces the ~300-line if-ladder that mixed source decision
2194
+ // with native fetch, anchor mutation, and runtime mirror
2195
+ // selection. The machine decides only between native-history
2196
+ // and pty-parser; downstream selection of which message array
2197
+ // to surface stays here.
2198
+ //
2199
+ // Behavioural changes vs v1:
2200
+ // - No more nativeHistoryAnchoredAt mutation on the adapter.
2201
+ // Lock state lives in CHAT_SOURCE_REGISTRY keyed by
2202
+ // (providerType, sessionId).
2203
+ // - No PTY-vs-native freshness comparison. The lock holds
2204
+ // across arbitrary PTY arrival; only native regression /
2205
+ // unavailability unlocks. This is the plipping fix.
2206
+ // - 6 trigger strings (native_history_partial / _stale /
2207
+ // _not_safely_mapped / _empty / _error / _unavailable)
2208
+ // collapse to 3 events with diagnostic causes preserved
2209
+ // and mapped back to legacy fallbackReason strings for
2210
+ // response compatibility.
2211
+ // - Codex live-workspace native probe and unsafe-native
2212
+ // daemon mirror fallbacks are preserved as additional
2213
+ // input rounds to the machine; they were never the source
2214
+ // decision itself, they were retries.
2215
+ // ───────────────────────────────────────────────────────────
2216
+
2217
+ const supportsNative = supportsCliNativeTranscript(providerType, provider)
2218
+ && isNativeSourceCanonicalHistory(provider?.nativeHistory);
2219
+ const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h, adapter.cliType);
2220
+ const workspace = sessionWorkspace;
2221
+ const nativeHistoryLimit = Math.max(
2222
+ normalizeReadChatTailLimit(args) || 0,
2223
+ returnedMessages.length,
2224
+ HOT_TAIL_MIN_LIMIT,
2225
+ );
2226
+ const nativeHistorySessionId = supportsNative
2227
+ ? resolveCliNativeHistorySessionId(args, historySessionId, providerSessionId)
2228
+ : undefined;
2229
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
2230
+ const skipLiveNativeHistoryWithoutProviderSession = shouldSkipLiveCliNativeHistoryWithoutProviderSession({
2231
+ adapter,
2232
+ providerType,
2233
+ readChatArgs: args,
2234
+ nativeHistorySessionId,
2235
+ parsedProviderSessionId: providerSessionId,
2236
+ });
2237
+ const nativeHistoryReadSessionId = skipLiveNativeHistoryWithoutProviderSession
2238
+ ? undefined
2239
+ : nativeHistorySessionId;
2240
+ const exactNativeHistoryScope = Boolean(
2241
+ (typeof args?.historySessionId === 'string' && args.historySessionId.trim())
2242
+ || (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
2243
+ || providerSessionId
2244
+ || (nativeHistoryReadSessionId && nativeHistoryReadSessionId !== targetSessionId)
2245
+ || ((h.currentSession as any)?.sessionId === args?.targetSessionId && typeof (h.currentSession as any)?.providerSessionId === 'string' && (h.currentSession as any).providerSessionId.trim())
2246
+ );
2247
+
2248
+ // 1. Fetch native history (or skip if provider does not support it).
2249
+ let nativeHistory: any | null = null;
2250
+ let nativeHistoryError: unknown | undefined;
2251
+ if (supportsNative) {
2252
+ try {
2253
+ nativeHistory = readCliProviderNativeHistory(agentStr, {
2254
+ canonicalHistory: provider?.nativeHistory,
2255
+ historySessionId: nativeHistoryReadSessionId,
2256
+ workspace,
2257
+ offset: 0,
2258
+ limit: nativeHistoryLimit,
2259
+ excludeRecentCount: 0,
2260
+ historyBehavior: provider?.historyBehavior,
2261
+ scripts: provider?.scripts as any,
2262
+ excludeInProgressTurn: returnedStatus === 'waiting_approval',
2263
+ sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
2264
+ envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
2265
+ });
2266
+ } catch (error: any) {
2267
+ nativeHistoryError = error;
2268
+ nativeHistory = null;
2269
+ }
2270
+ }
2271
+
2272
+ // 2. Compute safeMapping with the same rules the v1 code used so the
2273
+ // machine sees the same observation it always would have.
2274
+ let nativeMessages: ChatMessage[] = nativeHistory && Array.isArray(nativeHistory.messages)
2275
+ ? normalizeAndFilterNativeHistory(h, agentStr, args, nativeHistory.messages as ChatMessage[], nativeHistory.providerSessionId)
2276
+ : [];
2277
+ const sessionStartedAtMs = sessionStartedAtMsFromRegistry(h, args?.targetSessionId);
2278
+ let historyProviderSessionId = typeof nativeHistory?.providerSessionId === 'string'
2279
+ ? nativeHistory.providerSessionId
2280
+ : readHistorySessionIdFromMessages(nativeMessages) || nativeHistoryReadSessionId || historySessionId;
2281
+ let lookup = nativeHistory?.lookup === 'workspace' ? 'workspace' : 'session';
2282
+ let nativeHistorySessionForMapping = adapter.cliType === 'antigravity-cli'
2283
+ && historyProviderSessionId
2284
+ && nativeHistoryReadSessionId
2285
+ && historyProviderSessionId !== nativeHistoryReadSessionId
2286
+ ? undefined
2287
+ : nativeHistoryReadSessionId;
2288
+ let safeMapping = supportsNative && nativeHistory
2289
+ ? hasSafeNativeHistoryMapping({
2290
+ historySessionId: lookup === 'workspace' ? undefined : nativeHistorySessionForMapping,
2291
+ providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId || providerSessionId,
2292
+ workspace,
2293
+ nativeMessages,
2294
+ ptyMessages: returnedMessages,
2295
+ requireWorkspaceContentOverlap: lookup === 'workspace' && !exactNativeHistoryScope,
2296
+ })
2297
+ : false;
2298
+ if (skipLiveNativeHistoryWithoutProviderSession && (!safeMapping || returnedMessages.length === 0)) {
2299
+ nativeHistory = null;
2300
+ nativeMessages = [];
2301
+ historyProviderSessionId = undefined;
2302
+ lookup = 'session';
2303
+ safeMapping = false;
2304
+ }
2305
+ const mayRetryUnsafeAutoDetectedCodexSession = adapter.cliType === 'codex-cli'
2306
+ && !getExplicitHistorySessionId(args)
2307
+ && Boolean(sessionStartedAtMs && sessionStartedAtMs > 0)
2308
+ && !skipLiveNativeHistoryWithoutProviderSession
2309
+ && !safeMapping;
2310
+ if (mayRetryUnsafeAutoDetectedCodexSession) {
2311
+ try {
2312
+ nativeHistory = readCliProviderNativeHistory(agentStr, {
2313
+ canonicalHistory: provider?.nativeHistory,
2314
+ historySessionId: undefined,
2315
+ workspace,
2316
+ offset: 0,
2317
+ limit: nativeHistoryLimit,
2318
+ excludeRecentCount: 0,
2319
+ historyBehavior: provider?.historyBehavior,
2320
+ scripts: provider?.scripts as any,
2321
+ excludeInProgressTurn: returnedStatus === 'waiting_approval',
2322
+ sessionStartedAtMs,
2323
+ envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
2324
+ });
2325
+ nativeHistoryError = undefined;
2326
+ nativeMessages = nativeHistory && Array.isArray(nativeHistory.messages)
2327
+ ? normalizeAndFilterNativeHistory(h, agentStr, args, nativeHistory.messages as ChatMessage[], nativeHistory.providerSessionId)
2328
+ : [];
2329
+ historyProviderSessionId = typeof nativeHistory?.providerSessionId === 'string'
2330
+ ? nativeHistory.providerSessionId
2331
+ : readHistorySessionIdFromMessages(nativeMessages);
2332
+ lookup = nativeHistory?.lookup === 'workspace' ? 'workspace' : 'session';
2333
+ nativeHistorySessionForMapping = undefined;
2334
+ safeMapping = supportsNative && nativeHistory
2335
+ ? hasSafeNativeHistoryMapping({
2336
+ historySessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
2337
+ providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
2338
+ workspace,
2339
+ nativeMessages,
2340
+ ptyMessages: returnedMessages,
2341
+ requireWorkspaceContentOverlap: lookup === 'workspace' && !exactNativeHistoryScope,
2342
+ })
2343
+ : false;
2344
+ } catch (error: any) {
2345
+ nativeHistoryError = error;
2346
+ nativeHistory = null;
2347
+ nativeMessages = [];
2348
+ historyProviderSessionId = undefined;
2349
+ safeMapping = false;
2350
+ }
2351
+ }
2352
+ const trustedExactNativeIdentity = lookup !== 'workspace'
2353
+ && Boolean(nativeHistoryReadSessionId)
2354
+ && Boolean(historyProviderSessionId)
2355
+ && nativeHistoryReadSessionId === historyProviderSessionId;
2356
+
2357
+ // 3. Drive ChatSourceMachine — one observation per readChat call,
2358
+ // keyed by (providerType, sessionKey-for-this-call). targetSessionId
2359
+ // is the most specific session anchor we have; fall back to
2360
+ // historySessionId so we never leak state across distinct sessions.
2361
+ const machineSessionKey = String(
2362
+ args?.targetSessionId
2363
+ || providerSessionId
2364
+ || historySessionId
2365
+ || (h.currentSession as any)?.sessionId
2366
+ || ''
2367
+ );
2368
+ const primary = decideCliReadChatSource({
2369
+ providerType,
2370
+ provider,
2371
+ sessionId: machineSessionKey,
2372
+ nativeHistoryResult: nativeHistory,
2373
+ nativeHistoryError,
2374
+ safeMapping,
2375
+ trustedExactNativeIdentity,
2376
+ sessionWorkspace,
2377
+ intendedWorkspace,
2378
+ ptyMessages: returnedMessages,
2379
+ // Start with PTY visible; decideCliReadChatSource flips this
2380
+ // to true when the machine actually selects native-history.
2381
+ ptyStatusApprovalOnly: false,
2382
+ });
2383
+ let messageSource: Record<string, unknown> = primary.messageSource;
2384
+
2385
+ if (primary.nativeSelected) {
2386
+ selectedMessages = finalizeStreamingMessagesWhenIdle(primary.nativeMessages, returnedStatus);
2387
+ selectedProviderSessionId = historyProviderSessionId || providerSessionId;
2388
+ selectedTranscriptAuthority = 'provider';
2389
+ selectedCoverage = nativeHistory?.hasMore ? 'tail' : 'full';
2390
+ if (selectedProviderSessionId && selectedProviderSessionId !== providerSessionId) {
2391
+ adapter.updateRuntimeMeta?.({ providerSessionId: selectedProviderSessionId });
2392
+ }
2393
+ } else if (supportsNative) {
2394
+ // Native not selected. Two preserved v1 fallbacks before settling
2395
+ // on PTY: (a) Codex-only live workspace native probe; (b) unsafe-
2396
+ // native daemon mirror selection. The machine sees each retry as
2397
+ // an additional observation.
2398
+ const liveCurrentRuntimePtySafe = isCurrentRuntimePtySafelyAttributed({
2399
+ adapter,
2400
+ helpers: h,
2401
+ readChatArgs: args,
2402
+ sessionWorkspace,
2403
+ intendedWorkspace,
2404
+ ptyMessages: returnedMessages,
2405
+ });
2406
+ const mayProbeLiveCodexWorkspaceNative = adapter.cliType === 'codex-cli'
2407
+ && liveCurrentRuntimePtySafe
2408
+ && !(typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
2409
+ && !(providerSessionId && providerSessionId.trim())
2410
+ && !nativeHistoryReadSessionId
2411
+ && (!historyProviderSessionId || historyProviderSessionId === nativeHistoryReadSessionId || historyProviderSessionId === historySessionId)
2412
+ && !skipLiveNativeHistoryWithoutProviderSession;
2413
+ const liveWorkspaceNativeHistory = mayProbeLiveCodexWorkspaceNative
2414
+ ? readLiveCodexWorkspaceNativeHistory(agentStr, {
2415
+ canonicalHistory: provider?.nativeHistory,
2416
+ workspace,
2417
+ offset: 0,
2418
+ limit: nativeHistoryLimit,
2419
+ excludeRecentCount: 0,
2420
+ historyBehavior: provider?.historyBehavior,
2421
+ scripts: provider?.scripts as any,
2422
+ })
2423
+ : null;
2424
+ const liveWorkspaceNativeMessages = Array.isArray((liveWorkspaceNativeHistory as any)?.messages)
2425
+ ? normalizeAndFilterNativeHistory(h, agentStr, args, (liveWorkspaceNativeHistory as any).messages as ChatMessage[], (liveWorkspaceNativeHistory as any)?.providerSessionId)
2426
+ : [];
2427
+ const liveWorkspaceNativeProviderSessionId = typeof (liveWorkspaceNativeHistory as any)?.providerSessionId === 'string'
2428
+ ? (liveWorkspaceNativeHistory as any).providerSessionId
2429
+ : readHistorySessionIdFromMessages(liveWorkspaceNativeMessages);
2430
+ const liveWorkspaceNativeSafeMapping = liveWorkspaceNativeMessages.length > 0
2431
+ && hasSafeNativeHistoryMapping({
2432
+ workspace,
2433
+ nativeMessages: liveWorkspaceNativeMessages,
2434
+ ptyMessages: returnedMessages,
2435
+ requireWorkspaceContentOverlap: true,
2436
+ });
2437
+ if (liveWorkspaceNativeHistory) {
2438
+ const liveDecision = decideCliReadChatSource({
2439
+ providerType,
2440
+ provider,
2441
+ // Distinct session key so a transient codex live-probe does not
2442
+ // clobber the primary session's lock. The machine treats this
2443
+ // as its own session; the primary session's state is untouched.
2444
+ sessionId: `${machineSessionKey}::live-workspace`,
2445
+ nativeHistoryResult: liveWorkspaceNativeHistory,
2446
+ safeMapping: liveWorkspaceNativeSafeMapping,
2447
+ sessionWorkspace,
2448
+ intendedWorkspace,
2449
+ ptyMessages: returnedMessages,
2450
+ ptyStatusApprovalOnly: true,
2451
+ });
2452
+ if (liveDecision.nativeSelected) {
2453
+ selectedMessages = finalizeStreamingMessagesWhenIdle(liveDecision.nativeMessages, returnedStatus);
2454
+ selectedProviderSessionId = liveWorkspaceNativeProviderSessionId || providerSessionId;
2455
+ selectedTranscriptAuthority = 'provider';
2456
+ selectedCoverage = (liveWorkspaceNativeHistory as any).hasMore ? 'tail' : 'full';
2457
+ if (selectedProviderSessionId && selectedProviderSessionId !== providerSessionId) {
2458
+ adapter.updateRuntimeMeta?.({ providerSessionId: selectedProviderSessionId });
2459
+ }
2460
+ messageSource = liveDecision.messageSource;
2461
+ (messageSource as any).selectedDaemonSource = 'live-workspace-native-history';
2462
+ (messageSource as any).runtimeMappingSafe = true;
2463
+ } else {
2464
+ // Live probe also rejected: apply unsafe-native daemon mirror
2465
+ // selection (codex-only) using the primary decision's
2466
+ // fallbackReason.
2467
+ applyUnsafeNativeDaemonFallback({
2468
+ providerType,
2469
+ adapter,
2470
+ helpers: h,
2471
+ readChatArgs: args,
2472
+ sessionWorkspace,
2473
+ intendedWorkspace,
2474
+ ptyMessages: returnedMessages,
2475
+ nativeHistoryLimit,
2476
+ provider,
2477
+ messageSourceRef: { set(value) { messageSource = value; }, get() { return messageSource; } },
2478
+ apply(selection) {
2479
+ selectedMessages = selection.messages;
2480
+ selectedTranscriptAuthority = selection.transcriptAuthority;
2481
+ selectedCoverage = selection.coverage ?? coverage;
2482
+ selectedStatus = selection.status ?? returnedStatus;
2483
+ },
2484
+ activeModal,
2485
+ returnedStatus,
2486
+ coverage,
2487
+ });
2488
+ }
2489
+ } else {
2490
+ applyUnsafeNativeDaemonFallback({
2491
+ providerType,
2492
+ adapter,
2493
+ helpers: h,
2494
+ readChatArgs: args,
2495
+ sessionWorkspace,
2496
+ intendedWorkspace,
2497
+ ptyMessages: returnedMessages,
2498
+ nativeHistoryLimit,
2499
+ provider,
2500
+ messageSourceRef: { set(value) { messageSource = value; }, get() { return messageSource; } },
2501
+ apply(selection) {
2502
+ selectedMessages = selection.messages;
2503
+ selectedTranscriptAuthority = selection.transcriptAuthority;
2504
+ selectedCoverage = selection.coverage ?? coverage;
2505
+ selectedStatus = selection.status ?? returnedStatus;
2506
+ },
2507
+ activeModal,
2508
+ returnedStatus,
2509
+ coverage,
2510
+ });
2511
+ }
2512
+ }
2513
+ if (
2514
+ isGeneratingLikeStatus(selectedStatus)
2515
+ && selectedTranscriptAuthority === 'provider'
2516
+ && !hasNonEmptyModalButtons(activeModal)
2517
+ && hasFinalVisibleAssistantMessage(selectedMessages)
2518
+ ) {
2519
+ selectedStatus = 'idle';
2520
+ selectedMessages = finalizeStreamingMessagesWhenIdle(selectedMessages, selectedStatus);
2521
+ messageSource = {
2522
+ ...messageSource,
2523
+ statusReconciled: {
2524
+ from: returnedStatus,
2525
+ to: 'idle',
2526
+ reason: 'provider_native_final_assistant',
2527
+ },
2528
+ };
2529
+ }
885
2530
  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}`);
886
2531
  return buildReadChatCommandResult({
887
- messages: returnedMessages,
888
- status: returnedStatus,
2532
+ messages: selectedMessages,
2533
+ status: selectedStatus,
889
2534
  activeModal,
2535
+ messageSource,
2536
+ transcriptProvenance: messageSource,
890
2537
  debugReadChat: {
891
2538
  provider: adapter.cliType,
892
2539
  targetSessionId: String(args?.targetSessionId || ''),
893
2540
  adapterStatus: String(adapterStatus.status || ''),
894
2541
  parsedStatus: String(parsedRecord.status || ''),
895
- returnedStatus: String(returnedStatus || ''),
896
- shouldPreferAdapterMessages: false,
2542
+ returnedStatus: String(selectedStatus || ''),
2543
+ selectedMessageSource: (messageSource as any).selected,
2544
+ messageSource,
2545
+ shouldPreferAdapterMessages: supportsCliNativeTranscript(providerType, provider)
2546
+ && isNativeSourceCanonicalHistory(provider?.nativeHistory)
2547
+ && (messageSource as any).selected !== 'native-history'
2548
+ && typeof (messageSource as any).fallbackReason === 'string'
2549
+ && (messageSource as any).fallbackReason.startsWith('native_history_')
2550
+ && (messageSource as any).fallbackReason !== 'native_history_not_checked'
2551
+ && !isUnsafeNativeTranscriptFallback((messageSource as any).fallbackReason)
2552
+ && !(selectedTranscriptAuthority === 'provider' && selectedCoverage === 'full'),
897
2553
  parsedMsgCount: parsedRecord.messages.length,
898
- returnedMsgCount: returnedMessages.length,
2554
+ returnedMsgCount: selectedMessages.length,
899
2555
  },
900
- ...(title ? { title } : {}),
901
- ...(providerSessionId ? { providerSessionId } : {}),
902
- ...(transcriptAuthority ? { transcriptAuthority } : {}),
903
- ...(coverage ? { coverage } : {}),
904
- }, args);
2556
+ ...(selectedTitle ? { title: selectedTitle } : {}),
2557
+ ...(selectedProviderSessionId ? { providerSessionId: selectedProviderSessionId } : {}),
2558
+ ...(selectedTranscriptAuthority ? { transcriptAuthority: selectedTranscriptAuthority } : {}),
2559
+ ...(selectedCoverage ? { coverage: selectedCoverage } : {}),
2560
+ }, args, h);
905
2561
  }
2562
+ // History-only path (no adapter). Same source-decision contract as
2563
+ // the adapter path above, but with no PTY messages — the machine
2564
+ // simply decides whether native is usable; if not we return the
2565
+ // history we have plus a `native_history_not_safely_available`
2566
+ // error response when the provider requires native source.
906
2567
  const historyLimit = normalizeReadChatTailLimit(args);
907
2568
  try {
908
2569
  const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h);
909
- const workspace = typeof args?.workspace === 'string'
910
- ? args.workspace
911
- : typeof (h.currentSession as any)?.workspace === 'string'
912
- ? (h.currentSession as any).workspace
913
- : undefined;
914
- const history = readProviderChatHistory(agentStr, {
915
- canonicalHistory: provider?.canonicalHistory,
916
- historySessionId,
917
- workspace,
918
- offset: 0,
919
- limit: historyLimit,
920
- excludeRecentCount: 0,
921
- historyBehavior: provider?.historyBehavior,
922
- scripts: provider?.scripts as any,
923
- });
2570
+ const targetSid = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
2571
+ const registrySessionWorkspace = targetSid
2572
+ ? (h.ctx?.sessionRegistry?.get?.(targetSid) as any)?.workspace
2573
+ : undefined;
2574
+ const currentSessionWorkspace = typeof (h.currentSession as any)?.workspace === 'string'
2575
+ ? (h.currentSession as any).workspace
2576
+ : undefined;
2577
+ const argsWorkspace = typeof args?.workspace === 'string' ? args.workspace : undefined;
2578
+ // When reading a different session (targetSid), prefer that session's registered
2579
+ // workspace (or the caller-supplied args.workspace) over the current (coordinator)
2580
+ // session's workspace — otherwise the coordinator's cwd shadows the worker's cwd
2581
+ // and history lookups find the wrong files.
2582
+ const workspace = targetSid
2583
+ ? (typeof registrySessionWorkspace === 'string' ? registrySessionWorkspace : argsWorkspace ?? currentSessionWorkspace)
2584
+ : (typeof currentSessionWorkspace === 'string' ? currentSessionWorkspace : undefined);
2585
+ const intendedWorkspace = argsWorkspace;
2586
+ const supportsNative = supportsCliNativeTranscript(agentStr, provider)
2587
+ && isNativeSourceCanonicalHistory(provider?.nativeHistory);
2588
+ const history = supportsNative
2589
+ ? readCliProviderNativeHistory(agentStr, {
2590
+ canonicalHistory: provider?.nativeHistory,
2591
+ historySessionId,
2592
+ workspace,
2593
+ offset: 0,
2594
+ limit: historyLimit,
2595
+ excludeRecentCount: 0,
2596
+ historyBehavior: provider?.historyBehavior,
2597
+ scripts: provider?.scripts as any,
2598
+ sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
2599
+ envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
2600
+ })
2601
+ : readProviderChatHistory(agentStr, {
2602
+ canonicalHistory: provider?.nativeHistory,
2603
+ historySessionId,
2604
+ workspace,
2605
+ offset: 0,
2606
+ limit: historyLimit,
2607
+ excludeRecentCount: 0,
2608
+ historyBehavior: provider?.historyBehavior,
2609
+ scripts: provider?.scripts as any,
2610
+ });
2611
+ const lookup = (history as any)?.lookup === 'workspace' ? 'workspace' : 'session';
2612
+ const historyMessages = Array.isArray((history as any)?.messages)
2613
+ ? normalizeAndFilterNativeHistory(h, agentStr, args, (history as any).messages as ChatMessage[], (history as any)?.providerSessionId)
2614
+ : [];
924
2615
  const historyProviderSessionId = typeof (history as any)?.providerSessionId === 'string'
925
2616
  ? (history as any).providerSessionId
926
- : historySessionId;
2617
+ : readHistorySessionIdFromMessages(historyMessages) || historySessionId;
2618
+ const safeMapping = supportsNative
2619
+ ? hasSafeNativeHistoryMapping({
2620
+ historySessionId: lookup === 'workspace' ? undefined : historySessionId,
2621
+ providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
2622
+ workspace,
2623
+ nativeMessages: historyMessages,
2624
+ })
2625
+ : false;
2626
+ const trustedExactNativeIdentity = lookup !== 'workspace'
2627
+ && Boolean(historySessionId)
2628
+ && Boolean(historyProviderSessionId)
2629
+ && historySessionId === historyProviderSessionId;
2630
+
2631
+ const machineSessionKey = String(
2632
+ args?.targetSessionId
2633
+ || historyProviderSessionId
2634
+ || historySessionId
2635
+ || (h.currentSession as any)?.sessionId
2636
+ || ''
2637
+ );
2638
+ const decision = decideCliReadChatSource({
2639
+ providerType: agentStr,
2640
+ provider,
2641
+ sessionId: machineSessionKey,
2642
+ nativeHistoryResult: history,
2643
+ safeMapping,
2644
+ trustedExactNativeIdentity,
2645
+ sessionWorkspace: workspace,
2646
+ intendedWorkspace,
2647
+ ptyMessages: [],
2648
+ ptyStatusApprovalOnly: false,
2649
+ });
2650
+
2651
+ if (supportsNative && !decision.nativeSelected) {
2652
+ return {
2653
+ success: false,
2654
+ code: 'native_history_not_safely_available',
2655
+ error: 'Provider-native history was not safely available for the requested CLI session.',
2656
+ providerSessionId: historyProviderSessionId,
2657
+ messageSource: decision.messageSource,
2658
+ transcriptProvenance: decision.messageSource,
2659
+ };
2660
+ }
927
2661
  return buildReadChatCommandResult({
928
- messages: Array.isArray((history as any)?.messages) ? (history as any).messages : [],
2662
+ messages: historyMessages,
929
2663
  status: 'idle',
2664
+ messageSource: decision.messageSource,
2665
+ transcriptProvenance: decision.messageSource,
930
2666
  ...(typeof (history as any)?.title === 'string' ? { title: (history as any).title } : {}),
931
2667
  ...(historyProviderSessionId ? { providerSessionId: historyProviderSessionId } : {}),
932
2668
  ...(((provider?.historyBehavior as any)?.transcriptAuthority === 'provider' || (provider?.historyBehavior as any)?.transcriptAuthority === 'daemon')
933
2669
  ? { transcriptAuthority: (provider?.historyBehavior as any).transcriptAuthority }
934
2670
  : {}),
935
2671
  coverage: 'tail',
936
- }, args);
2672
+ }, args, h);
937
2673
  } catch (error: any) {
938
2674
  return { success: false, error: error?.message || `${transport} adapter not found` };
939
2675
  }
@@ -973,7 +2709,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
973
2709
  args?.targetSessionId,
974
2710
  historySessionId,
975
2711
  );
976
- return buildReadChatCommandResult(validated as Record<string, any>, args);
2712
+ return buildReadChatCommandResult(validated as Record<string, any>, args, h);
977
2713
  }
978
2714
  if (!extensionReadChatError) {
979
2715
  extensionReadChatError = 'extension read_chat returned a non-object payload';
@@ -1012,7 +2748,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1012
2748
  messages: stream.messages || [],
1013
2749
  status: stream.status,
1014
2750
  agentType: stream.agentType,
1015
- }, args);
2751
+ }, args, h);
1016
2752
  }
1017
2753
  }
1018
2754
  }
@@ -1052,7 +2788,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1052
2788
  args?.targetSessionId,
1053
2789
  historySessionId,
1054
2790
  );
1055
- return buildReadChatCommandResult(validated as Record<string, any>, args);
2791
+ return buildReadChatCommandResult(validated as Record<string, any>, args, h);
1056
2792
  }
1057
2793
  if (!webviewReadChatError) {
1058
2794
  webviewReadChatError = 'webview read_chat returned a non-object payload';
@@ -1102,7 +2838,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1102
2838
  args?.targetSessionId,
1103
2839
  historySessionId,
1104
2840
  );
1105
- return buildReadChatCommandResult(validated as Record<string, any>, args);
2841
+ return buildReadChatCommandResult(validated as Record<string, any>, args, h);
1106
2842
  }
1107
2843
  if (!ideReadChatError) {
1108
2844
  ideReadChatError = 'ide read_chat returned a non-object payload';
@@ -1184,8 +2920,24 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
1184
2920
  assertTextOnlyInput(provider, input);
1185
2921
  if (!text) return { success: false, error: 'text required for PTY send' };
1186
2922
  await waitOnceForFreshHermesCliStart(adapter, _log);
1187
- await adapter.sendMessage(text);
1188
- return _logSendSuccess(`${transport}-adapter`, adapter.cliType);
2923
+ const forceSend = args?.force === true || args?.forceSend === true;
2924
+ if (forceSend && typeof adapter.forceSendMessage === 'function') {
2925
+ await adapter.forceSendMessage(text);
2926
+ } else if (forceSend) {
2927
+ await adapter.sendMessage(text, { force: true });
2928
+ } else {
2929
+ await adapter.sendMessage(text);
2930
+ }
2931
+ const target = getTargetInstance(h, args) as RuntimeChatMessageMerger | null;
2932
+ if (target?.category === 'cli'
2933
+ && target.type === adapter.cliType
2934
+ && typeof target.recordAcknowledgedUserInput === 'function') {
2935
+ target.recordAcknowledgedUserInput(input);
2936
+ }
2937
+ return {
2938
+ ..._logSendSuccess(`${transport}-adapter`, adapter.cliType),
2939
+ ...(forceSend ? { forceSent: true } : {}),
2940
+ };
1189
2941
  } catch (e: any) {
1190
2942
  return { success: false, error: `${transport} send failed: ${e.message}` };
1191
2943
  }
@@ -1742,9 +3494,18 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
1742
3494
  if (buttonIndex < 0 && (action === 'always' || /always/i.test(button))) {
1743
3495
  buttonIndex = buttons.findIndex(b => /always/i.test(b));
1744
3496
  }
3497
+ if (buttonIndex < 0 && (action === 'approve' || action === 'accept')) {
3498
+ buttonIndex = pickApprovalButton(buttons, provider).index;
3499
+ }
1745
3500
  if (buttonIndex < 0) {
1746
3501
  return { success: false, error: 'Approval action did not match any visible button' };
1747
3502
  }
3503
+ // Idempotency: if the adapter already resolved this approval within cooldown, report
3504
+ // stale_prompt rather than writing a second key to the PTY.
3505
+ if (typeof adapter.isApprovalRecentlyResolved === 'function' && adapter.isApprovalRecentlyResolved()) {
3506
+ LOG.info('Command', `[resolveAction] CLI PTY → stale_prompt (already resolved within cooldown)`);
3507
+ return { success: true, stalePrompt: true, buttonIndex, button: buttons[buttonIndex] ?? button };
3508
+ }
1748
3509
  if (typeof adapter.resolveModal === 'function') {
1749
3510
  adapter.resolveModal(buttonIndex);
1750
3511
  } else {