@adhdev/daemon-core 0.9.82-rc.31 → 0.9.82-rc.310

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 (293) 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 +23 -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 +106 -63
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  12. package/dist/cli-adapters/provider-cli-shared.d.ts +38 -0
  13. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  14. package/dist/cli-adapters/resolve-executable.d.ts +14 -0
  15. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  16. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  17. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  18. package/dist/commands/cli-manager.d.ts +2 -1
  19. package/dist/commands/handler.d.ts +110 -0
  20. package/dist/commands/mesh-coordinator.d.ts +85 -1
  21. package/dist/commands/router.d.ts +210 -1
  22. package/dist/config/chat-history.d.ts +9 -0
  23. package/dist/config/config.d.ts +5 -0
  24. package/dist/config/mesh-config.d.ts +72 -1
  25. package/dist/git/git-commands.d.ts +9 -1
  26. package/dist/git/git-diff.d.ts +6 -0
  27. package/dist/git/git-executor.d.ts +11 -0
  28. package/dist/git/git-status.d.ts +9 -0
  29. package/dist/git/git-types.d.ts +2 -50
  30. package/dist/index.d.ts +51 -12
  31. package/dist/index.js +38320 -16024
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.mjs +38009 -15817
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/installer.d.ts +1 -4
  36. package/dist/ipc/local-ipc-server.d.ts +91 -0
  37. package/dist/launch.d.ts +1 -1
  38. package/dist/logging/async-batch-writer.d.ts +10 -0
  39. package/dist/mesh/contracts.d.ts +164 -0
  40. package/dist/mesh/coordinator-prompt.d.ts +36 -0
  41. package/dist/mesh/coordinator-registry.d.ts +59 -0
  42. package/dist/mesh/mesh-active-work.d.ts +108 -0
  43. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  44. package/dist/mesh/mesh-events-coordinator.d.ts +138 -0
  45. package/dist/mesh/mesh-events-pending.d.ts +45 -0
  46. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  47. package/dist/mesh/mesh-events-utils.d.ts +47 -0
  48. package/dist/mesh/mesh-events.d.ts +6 -49
  49. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  50. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  51. package/dist/mesh/mesh-init.d.ts +86 -0
  52. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  53. package/dist/mesh/mesh-ledger.d.ts +77 -1
  54. package/dist/mesh/mesh-missions.d.ts +116 -0
  55. package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
  56. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  57. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  58. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  59. package/dist/mesh/mesh-routing.d.ts +70 -0
  60. package/dist/mesh/mesh-runtime-store.d.ts +377 -0
  61. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  62. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
  63. package/dist/mesh/mesh-work-queue.d.ts +181 -5
  64. package/dist/mesh/preview-freshness.d.ts +18 -0
  65. package/dist/mesh/refine-config.d.ts +215 -0
  66. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  67. package/dist/providers/acp-provider-instance.d.ts +2 -0
  68. package/dist/providers/approval-utils.d.ts +13 -0
  69. package/dist/providers/cli-provider-instance.d.ts +61 -3
  70. package/dist/providers/contracts.d.ts +130 -6
  71. package/dist/providers/external-sources.d.ts +71 -0
  72. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  73. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  74. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  75. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  76. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  77. package/dist/providers/native-history/index.d.ts +13 -0
  78. package/dist/providers/provider-instance-manager.d.ts +13 -0
  79. package/dist/providers/provider-instance.d.ts +13 -1
  80. package/dist/providers/provider-loader.d.ts +26 -4
  81. package/dist/providers/provider-trust.d.ts +31 -0
  82. package/dist/providers/read-chat-contract.d.ts +29 -0
  83. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  84. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  85. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  86. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  87. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  88. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  89. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  90. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  91. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  92. package/dist/providers/sdk/v1/index.d.ts +30 -0
  93. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  94. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  95. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  96. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  97. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  98. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  99. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  100. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  101. package/dist/providers/spec/adapter.d.ts +59 -0
  102. package/dist/providers/spec/cli-adapter.d.ts +186 -0
  103. package/dist/providers/spec/evaluator.d.ts +23 -0
  104. package/dist/providers/spec/fsm-driver.d.ts +278 -0
  105. package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
  106. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  107. package/dist/providers/spec/fsm-types.d.ts +128 -0
  108. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  109. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  110. package/dist/providers/spec/route.d.ts +4 -0
  111. package/dist/providers/spec/types.d.ts +144 -0
  112. package/dist/providers/transcript-v2.d.ts +176 -0
  113. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  114. package/dist/repo-mesh-types.d.ts +203 -13
  115. package/dist/sessions/registry.d.ts +6 -0
  116. package/dist/shared-types-extra.d.ts +2 -4
  117. package/dist/shared-types.d.ts +42 -1
  118. package/dist/status/normalize.d.ts +1 -1
  119. package/dist/status/normalize.js +1 -0
  120. package/dist/status/normalize.js.map +1 -1
  121. package/dist/status/normalize.mjs +1 -0
  122. package/dist/status/normalize.mjs.map +1 -1
  123. package/dist/status/reporter.d.ts +2 -0
  124. package/dist/status/snapshot.d.ts +1 -0
  125. package/dist/types.d.ts +5 -0
  126. package/package.json +7 -3
  127. package/src/agent-stream/poller.ts +2 -3
  128. package/src/boot/daemon-lifecycle.ts +55 -12
  129. package/src/boot/process-hardening.ts +89 -0
  130. package/src/build-info.ts +73 -0
  131. package/src/chat/source-machine.ts +534 -0
  132. package/src/chat/source-resolver.ts +0 -0
  133. package/src/chat/subscription-updates.ts +20 -1
  134. package/src/cli-adapter-types.d.ts +1 -0
  135. package/src/cli-adapter-types.ts +22 -2
  136. package/src/cli-adapters/cli-script-runner.ts +421 -0
  137. package/src/cli-adapters/cli-state-engine.ts +1215 -0
  138. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  139. package/src/cli-adapters/provider-cli-adapter.ts +674 -1139
  140. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  141. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  142. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  143. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  144. package/src/cli-adapters/provider-cli-shared.ts +68 -11
  145. package/src/cli-adapters/pty-transport.ts +2 -1
  146. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  147. package/src/cli-adapters/resolve-executable.ts +45 -0
  148. package/src/cli-adapters/session-host-transport.ts +2 -1
  149. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
  150. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  151. package/src/cli-adapters/terminal-screen.ts +16 -81
  152. package/src/commands/chat-commands.ts +1813 -60
  153. package/src/commands/cli-manager.ts +286 -14
  154. package/src/commands/handler.ts +809 -2
  155. package/src/commands/mesh-coordinator.ts +332 -122
  156. package/src/commands/router.ts +5911 -595
  157. package/src/commands/upgrade-helper.ts +29 -0
  158. package/src/config/chat-history.ts +474 -24
  159. package/src/config/config.ts +12 -0
  160. package/src/config/mesh-config.ts +303 -3
  161. package/src/config/recent-activity.ts +8 -2
  162. package/src/daemon/dev-auto-implement.ts +3 -2
  163. package/src/daemon/dev-cli-debug.ts +10 -1
  164. package/src/detection/ide-detector.ts +26 -16
  165. package/src/git/git-commands.ts +42 -12
  166. package/src/git/git-diff.ts +53 -0
  167. package/src/git/git-executor.ts +12 -0
  168. package/src/git/git-status.ts +302 -18
  169. package/src/git/git-types.ts +14 -62
  170. package/src/git/git-worktree.ts +8 -1
  171. package/src/index.ts +131 -11
  172. package/src/installer.d.ts +1 -1
  173. package/src/installer.ts +8 -6
  174. package/src/ipc/local-ipc-server.ts +278 -0
  175. package/src/launch.d.ts +1 -1
  176. package/src/launch.ts +37 -28
  177. package/src/logging/async-batch-writer.ts +55 -0
  178. package/src/logging/logger.ts +2 -1
  179. package/src/mesh/contracts.ts +329 -0
  180. package/src/mesh/coordinator-prompt.ts +234 -31
  181. package/src/mesh/coordinator-registry.ts +121 -0
  182. package/src/mesh/mesh-active-work.ts +469 -0
  183. package/src/mesh/mesh-delivery-policy.ts +315 -0
  184. package/src/mesh/mesh-events-coordinator.ts +1786 -0
  185. package/src/mesh/mesh-events-pending.ts +508 -0
  186. package/src/mesh/mesh-events-stale.ts +290 -0
  187. package/src/mesh/mesh-events-utils.ts +254 -0
  188. package/src/mesh/mesh-events.ts +30 -1035
  189. package/src/mesh/mesh-fast-forward.ts +839 -0
  190. package/src/mesh/mesh-host-ownership.ts +73 -0
  191. package/src/mesh/mesh-init.ts +260 -0
  192. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  193. package/src/mesh/mesh-ledger.ts +589 -102
  194. package/src/mesh/mesh-missions.ts +248 -0
  195. package/src/mesh/mesh-reconcile-loop.ts +466 -0
  196. package/src/mesh/mesh-refine-batch.ts +197 -0
  197. package/src/mesh/mesh-refine-status.ts +231 -0
  198. package/src/mesh/mesh-review-inbox.ts +307 -0
  199. package/src/mesh/mesh-routing.ts +291 -0
  200. package/src/mesh/mesh-runtime-store.ts +1513 -0
  201. package/src/mesh/mesh-task-stats.ts +154 -0
  202. package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
  203. package/src/mesh/mesh-work-queue.ts +604 -145
  204. package/src/mesh/preview-freshness.ts +118 -0
  205. package/src/mesh/refine-config.ts +403 -0
  206. package/src/mesh/worktree-bootstrap-config.ts +326 -0
  207. package/src/providers/acp-provider-instance.ts +15 -1
  208. package/src/providers/approval-utils.d.ts +4 -0
  209. package/src/providers/approval-utils.ts +47 -5
  210. package/src/providers/chat-message-normalization.ts +4 -11
  211. package/src/providers/cli-provider-instance.ts +953 -87
  212. package/src/providers/contracts.d.ts +55 -0
  213. package/src/providers/contracts.ts +141 -6
  214. package/src/providers/extension-provider-instance.ts +5 -1
  215. package/src/providers/external-sources.ts +218 -0
  216. package/src/providers/ide-provider-instance.ts +25 -6
  217. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  218. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  219. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  220. package/src/providers/native-history/dispatcher.ts +340 -0
  221. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  222. package/src/providers/native-history/index.ts +30 -0
  223. package/src/providers/provider-instance-manager.ts +30 -0
  224. package/src/providers/provider-instance.ts +10 -1
  225. package/src/providers/provider-loader.ts +584 -50
  226. package/src/providers/provider-schema.ts +87 -14
  227. package/src/providers/provider-trust.ts +114 -0
  228. package/src/providers/read-chat-contract.ts +76 -16
  229. package/src/providers/sdk/README.md +49 -0
  230. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  231. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  232. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  233. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  234. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  235. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  236. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  237. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  238. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  239. package/src/providers/sdk/v1/index.ts +152 -0
  240. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  241. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  242. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  243. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  244. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  245. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  246. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  247. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  248. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  249. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  250. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  251. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  252. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  253. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  254. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  255. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  256. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  257. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  258. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  259. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  260. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  261. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  262. package/src/providers/sdk/v1/validators/index.ts +19 -0
  263. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  264. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  265. package/src/providers/spec/adapter.ts +155 -0
  266. package/src/providers/spec/cli-adapter.ts +838 -0
  267. package/src/providers/spec/evaluator.ts +345 -0
  268. package/src/providers/spec/fsm-driver.ts +875 -0
  269. package/src/providers/spec/fsm-evaluator.ts +255 -0
  270. package/src/providers/spec/fsm-loader.ts +112 -0
  271. package/src/providers/spec/fsm-types.ts +222 -0
  272. package/src/providers/spec/native-history-executor.ts +943 -0
  273. package/src/providers/spec/pre-launch-trust.ts +104 -0
  274. package/src/providers/spec/route.ts +51 -0
  275. package/src/providers/spec/types.ts +189 -0
  276. package/src/providers/transcript-v2.ts +567 -0
  277. package/src/providers/types/interactive-prompt.ts +536 -0
  278. package/src/providers/version-archive.ts +38 -20
  279. package/src/repo-mesh-types.ts +257 -13
  280. package/src/sessions/registry.ts +6 -0
  281. package/src/shared-types-extra.ts +2 -4
  282. package/src/shared-types.ts +45 -1
  283. package/src/status/builders.ts +26 -6
  284. package/src/status/normalize.ts +2 -0
  285. package/src/status/reporter.ts +15 -0
  286. package/src/status/snapshot.ts +84 -25
  287. package/src/system/host-memory.ts +29 -12
  288. package/src/types.ts +5 -0
  289. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  290. package/dist/mesh/mesh-sync.d.ts +0 -53
  291. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  292. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  293. package/src/mesh/mesh-sync.ts +0 -111
@@ -0,0 +1,943 @@
1
+ /**
2
+ * Declarative native-history executor.
3
+ *
4
+ * Reads a spec.json's `native_history.source` block and turns it into a
5
+ * NativeHistoryResult by talking directly to the on-disk store (jsonl
6
+ * file or sqlite db). No per-provider TypeScript reader required —
7
+ * adding a new provider is just authoring spec.json.
8
+ *
9
+ * Two source kinds:
10
+ * - jsonl — newest matching file inside a path (with variable expansion)
11
+ * + jsonpath-lite map for each record
12
+ * - sqlite — read-only better-sqlite3 handle, two queries (session pick
13
+ * + message fetch) + jsonpath-lite map for each row
14
+ *
15
+ * Exotic formats can still ship a provider-local reader via
16
+ * `native_history.override_path` — the dispatcher in provider-loader picks
17
+ * that path instead of calling this executor.
18
+ */
19
+ 'use strict';
20
+
21
+ import * as fs from 'node:fs';
22
+ import * as os from 'node:os';
23
+ import * as path from 'node:path';
24
+ import type {
25
+ NativeHistoryConfig,
26
+ NativeHistoryJsonlSource,
27
+ NativeHistoryMessageMap,
28
+ NativeHistorySqliteSource,
29
+ } from './types.js';
30
+
31
+ export interface NativeHistoryInput {
32
+ agentType?: string;
33
+ sessionId?: string;
34
+ providerSessionId?: string;
35
+ historySessionId?: string;
36
+ workspace?: string;
37
+ /** Daemon-side wall clock at the moment the session was registered.
38
+ * Native-history file lookups use this as the lower bound: any file
39
+ * whose mtime is before the current session started can't be from
40
+ * this session, so it's excluded from newest-recent matching. The
41
+ * caller (chat-history pipeline) populates this from the session
42
+ * registry; specs/executor never need to know how it's sourced. */
43
+ sessionStartedAtMs?: number;
44
+ /** Env overrides the daemon set on the spawned CLI. The mesh
45
+ * coordinator points hermes at a per-coordinator HERMES_HOME so
46
+ * the hermes process writes its state.db into a tmp directory
47
+ * instead of ~/.hermes. expandPath consults this map before
48
+ * process.env so the native-history reader follows the spawned
49
+ * child's view of HERMES_HOME / similar overrides; without it
50
+ * the reader would always look at ~/.hermes and miss every
51
+ * coordinator-session transcript. */
52
+ envOverrides?: Record<string, string>;
53
+ args?: Record<string, unknown>;
54
+ }
55
+
56
+ export interface NativeHistoryMessage {
57
+ role: 'user' | 'assistant' | 'system';
58
+ content: string;
59
+ receivedAt: number;
60
+ kind?: string;
61
+ workspace?: string;
62
+ }
63
+
64
+ export interface NativeHistoryResult {
65
+ messages: NativeHistoryMessage[];
66
+ providerSessionId?: string;
67
+ sourcePath: string;
68
+ sourceMtimeMs: number;
69
+ nativeHistoryCoverage?: 'full' | 'partial' | 'best-effort';
70
+ workspace?: string;
71
+ }
72
+
73
+ const UUID_RE = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i;
74
+
75
+ export function executeNativeHistory(cfg: NativeHistoryConfig, input: NativeHistoryInput): NativeHistoryResult | null {
76
+ if (!cfg?.source) return null;
77
+ if (cfg.source.kind === 'jsonl') return executeJsonl(cfg.source, input);
78
+ if (cfg.source.kind === 'sqlite') return executeSqlite(cfg.source, input);
79
+ return null;
80
+ }
81
+
82
+ // ────────────────────────────────────────────────────────────────────────────
83
+ // JSONL
84
+ // ────────────────────────────────────────────────────────────────────────────
85
+
86
+ function executeJsonl(src: NativeHistoryJsonlSource, input: NativeHistoryInput): NativeHistoryResult | null {
87
+ const resolved = expandPath(src.path, input);
88
+ if (!resolved) return null;
89
+
90
+ const windowMs = typeof src.recent_window_ms === 'number' ? src.recent_window_ms : 5 * 60_000;
91
+ const filePat = src.file_pattern ? globToRegex(src.file_pattern) : /.*\.jsonl$/;
92
+ const requestedSessionId = readRequestedSessionId(input);
93
+
94
+ // path can be:
95
+ // - a concrete file → used as-is
96
+ // - a concrete dir → newest matching file inside recent_window_ms
97
+ // - a path with `*` or `**` segments → walk all dirs that match the
98
+ // glob, pick the newest matching file across all matches. Lets
99
+ // specs like ~/.gemini/antigravity-cli/brain/*/.system_generated/logs
100
+ // resolve transparently without a per-provider override.
101
+ // The session-start cutoff guarantees a fresh dashboard view can't
102
+ // pick up a transcript file from a session that ended before this
103
+ // one started. recent_window_ms only controls how far back we'd
104
+ // otherwise look for a matching file; the session-start floor wins
105
+ // when it's later.
106
+ const sessionFloor = typeof input.sessionStartedAtMs === 'number' ? input.sessionStartedAtMs : 0;
107
+ // When multiple concurrent CLI sessions in the same workspace each create
108
+ // their own rollout (e.g. two codex-cli sessions both writing into
109
+ // ~/.codex/sessions/{date}), `newestRecentFile` picks the same file for
110
+ // every reader and the daemon sessions cross-alias each other. Prefer
111
+ // session-meta-aware matching: the candidate whose meta.cwd matches the
112
+ // workspace AND whose meta.timestamp is closest to (or within
113
+ // spawnGraceMs of) the daemon's spawn time wins. Falls back to mtime
114
+ // ordering when no candidate exposes a usable session_meta.
115
+ const workspaceHint = typeof input.workspace === 'string' && input.workspace.trim() ? input.workspace.trim() : '';
116
+ let sourcePath: string | null = null;
117
+ if (resolved.includes('*')) {
118
+ sourcePath = pickExactSessionFileAcrossGlob(resolved, filePat, requestedSessionId)
119
+ || pickSessionBoundFileAcrossGlob(resolved, filePat, windowMs, sessionFloor, workspaceHint)
120
+ || newestRecentFileAcrossGlob(resolved, filePat, windowMs, sessionFloor);
121
+ } else {
122
+ let stat: fs.Stats | null = null;
123
+ try { stat = fs.statSync(resolved); } catch { /* fall through to date-walk fallback */ }
124
+ if (stat && stat.isFile()) {
125
+ sourcePath = resolved;
126
+ } else if (stat && stat.isDirectory()) {
127
+ sourcePath = pickExactSessionFile(resolved, filePat, requestedSessionId)
128
+ || (requestedSessionId ? null : pickSessionBoundFile(resolved, filePat, windowMs, sessionFloor, workspaceHint))
129
+ || (requestedSessionId ? null : newestRecentFile(resolved, filePat, windowMs, sessionFloor));
130
+ }
131
+ // Date-templated directories (e.g. ~/.codex/sessions/{yyyy}/{mm}/{dd})
132
+ // can drift from the provider's chosen calendar day because CLIs
133
+ // disagree on local-vs-UTC date buckets. Search nearby date dirs
134
+ // before falling back to non-exact matching.
135
+ if (!sourcePath && hasDateTemplateSegment(src.path)) {
136
+ sourcePath = pickExactSessionFileAcrossDateWindow(src.path, input, filePat, requestedSessionId)
137
+ || (requestedSessionId ? null : pickSessionBoundFileAcrossDateWindow(src.path, input, filePat, windowMs, sessionFloor, workspaceHint))
138
+ || (requestedSessionId ? null : newestRecentFileAcrossDateWindow(src.path, input, filePat, windowMs, sessionFloor));
139
+ }
140
+ }
141
+ if (!sourcePath) return null;
142
+
143
+ const mtime = safeMtimeMs(sourcePath);
144
+ const lines = readJsonlLines(sourcePath);
145
+ if (lines.length === 0) return null;
146
+ const transcriptWorkspace = readSessionMetaWorkspace(lines);
147
+
148
+ // session id: filename uuid or extracted from first record
149
+ let providerSessionId: string | undefined;
150
+ if (src.session_id_from === 'first_record' && src.session_id_path) {
151
+ const v = jsonPathGet(lines[0], src.session_id_path);
152
+ if (typeof v === 'string' && v) providerSessionId = v;
153
+ } else if (src.session_id_from === 'filename_uuid' || !src.session_id_from) {
154
+ const m = path.basename(sourcePath).match(UUID_RE);
155
+ if (m) providerSessionId = m[1];
156
+ }
157
+
158
+ const requested = requestedSessionId || '';
159
+ if (requested && providerSessionId && providerSessionId !== requested) return null;
160
+
161
+ const filter = src.message_filter ? compileWhere(src.message_filter.where) : null;
162
+ const messages: NativeHistoryMessage[] = [];
163
+ for (let i = 0; i < lines.length; i += 1) {
164
+ const rec = lines[i];
165
+ if (filter && !filter(rec)) continue;
166
+ const msg = projectMessage(rec, src.message_map, i, lines.length, mtime);
167
+ if (msg) {
168
+ if (transcriptWorkspace) msg.workspace = transcriptWorkspace;
169
+ messages.push(msg);
170
+ }
171
+ }
172
+ if (messages.length === 0) return null;
173
+
174
+ return {
175
+ messages,
176
+ providerSessionId,
177
+ sourcePath,
178
+ sourceMtimeMs: mtime,
179
+ nativeHistoryCoverage: 'full',
180
+ workspace: transcriptWorkspace,
181
+ };
182
+ }
183
+
184
+ function readSessionMetaWorkspace(lines: any[]): string | undefined {
185
+ for (const record of lines.slice(0, 5)) {
186
+ if (String(record?.type ?? '') !== 'session_meta') continue;
187
+ const cwd = typeof record?.payload?.cwd === 'string' ? record.payload.cwd.trim() : '';
188
+ if (cwd) return cwd;
189
+ }
190
+ return undefined;
191
+ }
192
+
193
+ function readJsonlLines(p: string): any[] {
194
+ let text: string;
195
+ try { text = fs.readFileSync(p, 'utf8'); } catch { return []; }
196
+ const out: any[] = [];
197
+ for (const line of text.split('\n')) {
198
+ const trimmed = line.trim();
199
+ if (!trimmed) continue;
200
+ try { out.push(JSON.parse(trimmed)); } catch { /* skip malformed line */ }
201
+ }
202
+ return out;
203
+ }
204
+
205
+ // ────────────────────────────────────────────────────────────────────────────
206
+ // SQLite
207
+ // ────────────────────────────────────────────────────────────────────────────
208
+
209
+ function executeSqlite(src: NativeHistorySqliteSource, input: NativeHistoryInput): NativeHistoryResult | null {
210
+ const resolved = expandPath(src.path, input);
211
+ if (!resolved || !fs.existsSync(resolved)) return null;
212
+
213
+ let Database: any;
214
+ try {
215
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
216
+ Database = require('better-sqlite3');
217
+ } catch { return null; }
218
+
219
+ let db: any;
220
+ try { db = new Database(resolved, { readonly: true, fileMustExist: true }); }
221
+ catch { return null; }
222
+
223
+ try {
224
+ let sessionRow: any;
225
+ try {
226
+ // session_query may reference `?` to receive the session's
227
+ // start-time floor in seconds (e.g. WHERE started_at >= ?).
228
+ // That gives spec authors a robust way to keep prior-session
229
+ // rows out of a fresh dashboard view without inventing their
230
+ // own time arithmetic in SQL. When the caller didn't pass a
231
+ // session floor (i.e. no live session is associated with the
232
+ // call), we use 0 so spec queries that bind `?` still produce
233
+ // a sane result rather than choking the whole executor.
234
+ const sessionFloorSeconds = typeof input.sessionStartedAtMs === 'number'
235
+ ? Math.floor(input.sessionStartedAtMs / 1000)
236
+ : 0;
237
+ const stmt = db.prepare(src.session_query);
238
+ try {
239
+ sessionRow = stmt.get(sessionFloorSeconds);
240
+ } catch {
241
+ sessionRow = stmt.get();
242
+ }
243
+ } catch { return null; }
244
+ if (!sessionRow) return null;
245
+ // First column of the first row is the session id.
246
+ const sessionIdRaw = Object.values(sessionRow)[0];
247
+ const sessionId = sessionIdRaw == null ? '' : String(sessionIdRaw);
248
+ if (!sessionId) return null;
249
+
250
+ const requested = input.providerSessionId || '';
251
+ if (requested && sessionId !== requested) return null;
252
+
253
+ const messageRows: any[] = db.prepare(src.message_query).all(sessionId);
254
+ if (!messageRows || messageRows.length === 0) return null;
255
+
256
+ const mtime = safeMtimeMs(resolved);
257
+ const messages: NativeHistoryMessage[] = [];
258
+ for (let i = 0; i < messageRows.length; i += 1) {
259
+ const msg = projectMessage(messageRows[i], src.message_map, i, messageRows.length, mtime);
260
+ if (msg) messages.push(msg);
261
+ }
262
+ if (messages.length === 0) return null;
263
+
264
+ return {
265
+ messages,
266
+ providerSessionId: sessionId,
267
+ sourcePath: resolved,
268
+ sourceMtimeMs: mtime,
269
+ nativeHistoryCoverage: 'full',
270
+ };
271
+ } finally {
272
+ try { db.close(); } catch { /* ignore */ }
273
+ }
274
+ }
275
+
276
+ // ────────────────────────────────────────────────────────────────────────────
277
+ // Path expansion + globbing
278
+ // ────────────────────────────────────────────────────────────────────────────
279
+
280
+ function expandPath(template: string, input: NativeHistoryInput): string | null {
281
+ if (!template) return null;
282
+ let out = template;
283
+ if (out.startsWith('~/') || out === '~') {
284
+ out = path.join(os.homedir(), out.slice(2));
285
+ }
286
+ // ${VAR} expands from envOverrides (the spawned child's view) first,
287
+ // then process.env. ${VAR:-fallback} keeps the bash-style default
288
+ // so spec authors can say e.g. ${HERMES_HOME:-~/.hermes}/state.db
289
+ // and have it work both for coordinator-launched sessions (where
290
+ // HERMES_HOME is set to a tmpdir) and normal sessions.
291
+ out = out.replace(/\$\{([A-Z_][A-Z0-9_]*)(?::-(.*?))?\}/g, (_m, name, fallback) => {
292
+ const v = input.envOverrides?.[name] ?? process.env[name];
293
+ return v != null && v !== '' ? v : (fallback ?? '');
294
+ });
295
+ // Re-expand ~ in case the fallback used it.
296
+ if (out.startsWith('~/')) out = path.join(os.homedir(), out.slice(2));
297
+ const now = new Date();
298
+ // Claude writes per-cwd transcripts under the resolved path and replaces
299
+ // every non-alphanumeric project-path character except `_` and `-` with
300
+ // `-`. Realpath also handles aliases such as /tmp -> /private/tmp.
301
+ const workspaceRaw = input.workspace ?? '';
302
+ let workspaceResolved = workspaceRaw;
303
+ if (workspaceRaw) {
304
+ try { workspaceResolved = fs.realpathSync(workspaceRaw); }
305
+ catch { /* path may not exist yet — keep the raw value */ }
306
+ }
307
+ const vars: Record<string, string> = {
308
+ cwd: workspaceResolved,
309
+ cwd_dashed: workspaceResolved.replace(/\//g, '-'),
310
+ cwd_claude_project: claudeProjectDirName(workspaceResolved),
311
+ session_id: input.providerSessionId || input.sessionId || input.historySessionId || '',
312
+ yyyy: String(now.getFullYear()),
313
+ mm: String(now.getMonth() + 1).padStart(2, '0'),
314
+ dd: String(now.getDate()).padStart(2, '0'),
315
+ };
316
+ // Replace {var}. If a referenced variable is empty (e.g. session_id
317
+ // before the agent has allocated one), return null so the caller
318
+ // doesn't accidentally fall through to an unrelated newest-file
319
+ // match. Wildcards (`*`) are explicitly allowed to pass — the dir
320
+ // glob walker handles them separately.
321
+ let missing = false;
322
+ out = out.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_m, name) => {
323
+ const v = vars[name] ?? '';
324
+ if (!v) missing = true;
325
+ return v;
326
+ });
327
+ if (missing) return null;
328
+ return out;
329
+ }
330
+
331
+ function claudeProjectDirName(workspace: string): string {
332
+ return workspace.replace(/[^A-Za-z0-9_-]/g, '-');
333
+ }
334
+
335
+ function globToRegex(pattern: string): RegExp {
336
+ // Minimal glob: `*` → `[^/]*`, `?` → `[^/]`, `.` → `\.`. Anchored.
337
+ const re = pattern
338
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
339
+ .replace(/\*/g, '[^/]*')
340
+ .replace(/\?/g, '[^/]');
341
+ return new RegExp(`^${re}$`);
342
+ }
343
+
344
+ /**
345
+ * Resolve a path with `*` segments to all concrete directories that match,
346
+ * then pick the newest file inside any of them. `*` matches one path
347
+ * component (no slashes); `**` matches zero or more components. Filenames
348
+ * are matched against `pattern`, not the glob — file_pattern is the right
349
+ * place for the leaf match.
350
+ */
351
+ function expandDirGlob(template: string): string[] {
352
+ const parts = template.split('/');
353
+ let dirs: string[] = parts[0] === '' ? ['/'] : [parts[0]];
354
+ for (let i = 1; i < parts.length; i += 1) {
355
+ const seg = parts[i];
356
+ if (!seg) continue;
357
+ const next: string[] = [];
358
+ if (seg === '**') {
359
+ for (const d of dirs) walkAllDirs(d, next);
360
+ dirs = next;
361
+ continue;
362
+ }
363
+ if (seg.includes('*') || seg.includes('?')) {
364
+ const re = globToRegex(seg);
365
+ for (const d of dirs) {
366
+ let entries: fs.Dirent[];
367
+ try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { continue; }
368
+ for (const e of entries) {
369
+ if (e.isDirectory() && re.test(e.name)) next.push(path.join(d, e.name));
370
+ }
371
+ }
372
+ } else {
373
+ for (const d of dirs) {
374
+ const candidate = path.join(d, seg);
375
+ let stat: fs.Stats | null = null;
376
+ try { stat = fs.statSync(candidate); } catch { continue; }
377
+ if (stat.isDirectory()) next.push(candidate);
378
+ }
379
+ }
380
+ dirs = next;
381
+ }
382
+ return dirs;
383
+ }
384
+
385
+ function walkAllDirs(root: string, out: string[]): void {
386
+ let entries: fs.Dirent[];
387
+ try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return; }
388
+ out.push(root);
389
+ for (const e of entries) {
390
+ if (e.isDirectory()) walkAllDirs(path.join(root, e.name), out);
391
+ }
392
+ }
393
+
394
+ function newestRecentFileAcrossGlob(template: string, pattern: RegExp, windowMs: number, sessionFloorMs = 0): string | null {
395
+ const dirs = expandDirGlob(template);
396
+ const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs);
397
+ let best: { p: string; mtime: number } | null = null;
398
+ for (const d of dirs) {
399
+ let entries: fs.Dirent[];
400
+ try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { continue; }
401
+ for (const e of entries) {
402
+ if (!e.isFile() || !pattern.test(e.name)) continue;
403
+ const p = path.join(d, e.name);
404
+ const mtime = safeMtimeMs(p);
405
+ if (mtime < cutoff) continue;
406
+ if (!best || mtime > best.mtime) best = { p, mtime };
407
+ }
408
+ }
409
+ return best ? best.p : null;
410
+ }
411
+
412
+ function hasDateTemplateSegment(template: string): boolean {
413
+ return /\{yyyy\}|\{mm\}|\{dd\}/.test(template);
414
+ }
415
+
416
+ /**
417
+ * Walk nearby local calendar days of a date-templated path (e.g.
418
+ * `~/.codex/sessions/{yyyy}/{mm}/{dd}`) and return the newest matching
419
+ * file across all of them. Providers differ on local-vs-UTC date buckets,
420
+ * so today's expanded dir alone can miss a live transcript.
421
+ */
422
+ function newestRecentFileAcrossDateWindow(
423
+ template: string,
424
+ input: NativeHistoryInput,
425
+ pattern: RegExp,
426
+ windowMs: number,
427
+ sessionFloorMs: number,
428
+ ): string | null {
429
+ const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs);
430
+ let best: { p: string; mtime: number } | null = null;
431
+ for (const dayOffset of [0, -1, 1, -2, 2]) {
432
+ const dayMs = Date.now() + dayOffset * 24 * 60 * 60 * 1000;
433
+ const dayInput: NativeHistoryInput = { ...input, sessionStartedAtMs: sessionFloorMs };
434
+ const resolved = expandPathForDate(template, dayInput, new Date(dayMs));
435
+ if (!resolved) continue;
436
+ let entries: fs.Dirent[];
437
+ try { entries = fs.readdirSync(resolved, { withFileTypes: true }); } catch { continue; }
438
+ for (const e of entries) {
439
+ if (!e.isFile() || !pattern.test(e.name)) continue;
440
+ const p = path.join(resolved, e.name);
441
+ const mtime = safeMtimeMs(p);
442
+ if (mtime < cutoff) continue;
443
+ if (!best || mtime > best.mtime) best = { p, mtime };
444
+ }
445
+ }
446
+ return best ? best.p : null;
447
+ }
448
+
449
+ function expandPathForDate(template: string, input: NativeHistoryInput, day: Date): string | null {
450
+ // Reuse expandPath logic but stamp {yyyy}/{mm}/{dd} from the given day.
451
+ if (!template) return null;
452
+ let out = template;
453
+ if (out.startsWith('~/') || out === '~') {
454
+ out = path.join(os.homedir(), out.slice(2));
455
+ }
456
+ out = out.replace(/\$\{([A-Z_][A-Z0-9_]*)(?::-(.*?))?\}/g, (_m, name, fallback) => {
457
+ const v = input.envOverrides?.[name] ?? process.env[name];
458
+ return v != null && v !== '' ? v : (fallback ?? '');
459
+ });
460
+ if (out.startsWith('~/')) out = path.join(os.homedir(), out.slice(2));
461
+ const workspaceRaw = input.workspace ?? '';
462
+ let workspaceResolved = workspaceRaw;
463
+ if (workspaceRaw) {
464
+ try { workspaceResolved = fs.realpathSync(workspaceRaw); } catch { /* keep raw */ }
465
+ }
466
+ const vars: Record<string, string> = {
467
+ cwd: workspaceResolved,
468
+ cwd_dashed: workspaceResolved.replace(/\//g, '-'),
469
+ cwd_claude_project: claudeProjectDirName(workspaceResolved),
470
+ session_id: input.providerSessionId || input.sessionId || input.historySessionId || '',
471
+ yyyy: String(day.getFullYear()),
472
+ mm: String(day.getMonth() + 1).padStart(2, '0'),
473
+ dd: String(day.getDate()).padStart(2, '0'),
474
+ };
475
+ let missing = false;
476
+ out = out.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_m, name) => {
477
+ const v = vars[name] ?? '';
478
+ if (!v) missing = true;
479
+ return v;
480
+ });
481
+ if (missing) return null;
482
+ return out;
483
+ }
484
+
485
+ function newestRecentFile(dir: string, pattern: RegExp, windowMs: number, sessionFloorMs = 0): string | null {
486
+ let entries: fs.Dirent[];
487
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return null; }
488
+ const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs);
489
+ let best: { p: string; mtime: number } | null = null;
490
+ for (const e of entries) {
491
+ if (!e.isFile() || !pattern.test(e.name)) continue;
492
+ const p = path.join(dir, e.name);
493
+ const mtime = safeMtimeMs(p);
494
+ if (mtime < cutoff) continue;
495
+ if (!best || mtime > best.mtime) best = { p, mtime };
496
+ }
497
+ return best ? best.p : null;
498
+ }
499
+
500
+ function safeMtimeMs(p: string): number {
501
+ try { return Math.floor(fs.statSync(p).mtimeMs); } catch { return 0; }
502
+ }
503
+
504
+ function readRequestedSessionId(input: NativeHistoryInput): string {
505
+ const raw = input.providerSessionId || input.sessionId || input.historySessionId || '';
506
+ const value = typeof raw === 'string' ? raw.trim() : '';
507
+ return UUID_RE.test(value) ? value : '';
508
+ }
509
+
510
+ function filenameUuid(filePath: string): string {
511
+ const match = path.basename(filePath).match(UUID_RE);
512
+ return match?.[1] || '';
513
+ }
514
+
515
+ function pickExactSessionFile(dir: string, pattern: RegExp, requestedSessionId: string): string | null {
516
+ if (!requestedSessionId) return null;
517
+ const files = listMatchingFiles(dir, pattern)
518
+ .filter(p => filenameUuid(p).toLowerCase() === requestedSessionId.toLowerCase())
519
+ .sort((a, b) => safeMtimeMs(b) - safeMtimeMs(a));
520
+ return files[0] || null;
521
+ }
522
+
523
+ function pickExactSessionFileAcrossGlob(template: string, pattern: RegExp, requestedSessionId: string): string | null {
524
+ if (!requestedSessionId) return null;
525
+ const dirs = expandDirGlob(template);
526
+ const matches: string[] = [];
527
+ for (const d of dirs) {
528
+ const found = pickExactSessionFile(d, pattern, requestedSessionId);
529
+ if (found) matches.push(found);
530
+ }
531
+ matches.sort((a, b) => safeMtimeMs(b) - safeMtimeMs(a));
532
+ return matches[0] || null;
533
+ }
534
+
535
+ function pickExactSessionFileAcrossDateWindow(
536
+ template: string,
537
+ input: NativeHistoryInput,
538
+ pattern: RegExp,
539
+ requestedSessionId: string,
540
+ ): string | null {
541
+ if (!requestedSessionId) return null;
542
+ const matches: string[] = [];
543
+ for (const dayOffset of [0, -1, 1, -2, 2]) {
544
+ const dayMs = Date.now() + dayOffset * 24 * 60 * 60 * 1000;
545
+ const resolved = expandPathForDate(template, input, new Date(dayMs));
546
+ if (!resolved) continue;
547
+ const found = pickExactSessionFile(resolved, pattern, requestedSessionId);
548
+ if (found) matches.push(found);
549
+ }
550
+ matches.sort((a, b) => safeMtimeMs(b) - safeMtimeMs(a));
551
+ return matches[0] || null;
552
+ }
553
+
554
+ // ────────────────────────────────────────────────────────────────────────────
555
+ // Per-session rollout binding
556
+ //
557
+ // Reads the first JSONL line of a candidate file and returns a `session_meta`
558
+ // payload if present. Codex-cli writes
559
+ // {"timestamp":"...","type":"session_meta","payload":{"id":...,"cwd":...,
560
+ // "timestamp":"..."}}
561
+ // as the first record. Other providers that don't follow this convention
562
+ // return null and fall back to the mtime-based picker.
563
+ // ────────────────────────────────────────────────────────────────────────────
564
+
565
+ interface CandidateMeta {
566
+ cwd?: string;
567
+ sessionTimestampMs?: number;
568
+ }
569
+
570
+ function readCandidateSessionMeta(filePath: string): CandidateMeta | null {
571
+ try {
572
+ // Read only the first line — meta is always the first JSONL record
573
+ // and full file reads here would scale O(files × file_size).
574
+ const fd = fs.openSync(filePath, 'r');
575
+ try {
576
+ const buf = Buffer.alloc(8192);
577
+ const bytes = fs.readSync(fd, buf, 0, buf.length, 0);
578
+ if (bytes <= 0) return null;
579
+ const text = buf.subarray(0, bytes).toString('utf8');
580
+ const nl = text.indexOf('\n');
581
+ const firstLine = (nl >= 0 ? text.slice(0, nl) : text).trim();
582
+ if (!firstLine) return null;
583
+ const parsed = JSON.parse(firstLine) as Record<string, unknown>;
584
+ if (String(parsed.type ?? '') !== 'session_meta') return null;
585
+ const payload = parsed.payload && typeof parsed.payload === 'object'
586
+ ? (parsed.payload as Record<string, unknown>)
587
+ : null;
588
+ if (!payload) return null;
589
+ const cwd = typeof payload.cwd === 'string' ? payload.cwd : undefined;
590
+ const tsRaw = payload.timestamp;
591
+ const tsMs = typeof tsRaw === 'string'
592
+ ? Date.parse(tsRaw)
593
+ : typeof tsRaw === 'number'
594
+ ? (tsRaw < 1e12 ? Math.floor(tsRaw * 1000) : Math.floor(tsRaw))
595
+ : NaN;
596
+ return {
597
+ cwd,
598
+ sessionTimestampMs: Number.isFinite(tsMs) ? tsMs : undefined,
599
+ };
600
+ } finally {
601
+ fs.closeSync(fd);
602
+ }
603
+ } catch {
604
+ return null;
605
+ }
606
+ }
607
+
608
+ /**
609
+ * Spawn-bind grace window: an on-disk rollout whose session_meta.timestamp
610
+ * lands within ±SPAWN_BIND_GRACE_MS of the daemon's spawnedAtMs is treated
611
+ * as belonging to that daemon session. 10s is long enough to absorb codex
612
+ * binary startup latency on cold caches and short enough that two
613
+ * back-to-back launches don't both fall inside the same window.
614
+ */
615
+ const SPAWN_BIND_GRACE_MS = 10_000;
616
+
617
+ function pickBoundFromEntries(
618
+ candidatePaths: string[],
619
+ sessionFloorMs: number,
620
+ workspaceHint: string,
621
+ ): string | null {
622
+ if (!sessionFloorMs || !workspaceHint || candidatePaths.length === 0) return null;
623
+ // Resolve workspaceHint to handle macOS /tmp → /private/tmp aliasing, the
624
+ // same way expandPath does for the template substitution. Without this
625
+ // the daemon's `/Users/foo/repo` and codex's `/private/Users/foo/repo`
626
+ // never compare equal and disambiguation silently fails.
627
+ let workspaceResolved = workspaceHint;
628
+ try { workspaceResolved = fs.realpathSync(workspaceHint); } catch { /* keep raw */ }
629
+ let best: { p: string; diff: number } | null = null;
630
+ for (const p of candidatePaths) {
631
+ const meta = readCandidateSessionMeta(p);
632
+ if (!meta || !meta.cwd || meta.sessionTimestampMs == null) continue;
633
+ let candidateCwd = meta.cwd;
634
+ try { candidateCwd = fs.realpathSync(meta.cwd); } catch { /* keep raw */ }
635
+ if (candidateCwd !== workspaceResolved && meta.cwd !== workspaceHint) continue;
636
+ const diff = Math.abs(meta.sessionTimestampMs - sessionFloorMs);
637
+ if (diff > SPAWN_BIND_GRACE_MS) continue;
638
+ if (!best || diff < best.diff) best = { p, diff };
639
+ }
640
+ return best ? best.p : null;
641
+ }
642
+
643
+ function listMatchingFiles(dir: string, pattern: RegExp): string[] {
644
+ let entries: fs.Dirent[];
645
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
646
+ const out: string[] = [];
647
+ for (const e of entries) {
648
+ if (!e.isFile() || !pattern.test(e.name)) continue;
649
+ out.push(path.join(dir, e.name));
650
+ }
651
+ return out;
652
+ }
653
+
654
+ function pickSessionBoundFile(
655
+ dir: string,
656
+ pattern: RegExp,
657
+ windowMs: number,
658
+ sessionFloorMs: number,
659
+ workspaceHint: string,
660
+ ): string | null {
661
+ if (!sessionFloorMs || !workspaceHint) return null;
662
+ const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs - SPAWN_BIND_GRACE_MS);
663
+ const files = listMatchingFiles(dir, pattern).filter(p => safeMtimeMs(p) >= cutoff);
664
+ return pickBoundFromEntries(files, sessionFloorMs, workspaceHint);
665
+ }
666
+
667
+ function pickSessionBoundFileAcrossGlob(
668
+ template: string,
669
+ pattern: RegExp,
670
+ windowMs: number,
671
+ sessionFloorMs: number,
672
+ workspaceHint: string,
673
+ ): string | null {
674
+ if (!sessionFloorMs || !workspaceHint) return null;
675
+ const dirs = expandDirGlob(template);
676
+ const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs - SPAWN_BIND_GRACE_MS);
677
+ const files: string[] = [];
678
+ for (const d of dirs) {
679
+ for (const p of listMatchingFiles(d, pattern)) {
680
+ if (safeMtimeMs(p) >= cutoff) files.push(p);
681
+ }
682
+ }
683
+ return pickBoundFromEntries(files, sessionFloorMs, workspaceHint);
684
+ }
685
+
686
+ function pickSessionBoundFileAcrossDateWindow(
687
+ template: string,
688
+ input: NativeHistoryInput,
689
+ pattern: RegExp,
690
+ windowMs: number,
691
+ sessionFloorMs: number,
692
+ workspaceHint: string,
693
+ ): string | null {
694
+ if (!sessionFloorMs || !workspaceHint) return null;
695
+ const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs - SPAWN_BIND_GRACE_MS);
696
+ const files: string[] = [];
697
+ for (const dayOffset of [0, -1, 1, -2, 2]) {
698
+ const dayMs = sessionFloorMs + dayOffset * 24 * 60 * 60 * 1000;
699
+ const dayInput: NativeHistoryInput = { ...input, sessionStartedAtMs: sessionFloorMs };
700
+ const resolved = expandPathForDate(template, dayInput, new Date(dayMs));
701
+ if (!resolved) continue;
702
+ for (const p of listMatchingFiles(resolved, pattern)) {
703
+ if (safeMtimeMs(p) >= cutoff) files.push(p);
704
+ }
705
+ }
706
+ return pickBoundFromEntries(files, sessionFloorMs, workspaceHint);
707
+ }
708
+
709
+ // ────────────────────────────────────────────────────────────────────────────
710
+ // jsonpath-lite + projection
711
+ // ────────────────────────────────────────────────────────────────────────────
712
+
713
+ /**
714
+ * Resolve `$.a.b[0].c` against a record. Strings without leading `$` are
715
+ * literals. Supports `||` fallback between paths so a single message_map
716
+ * entry can pick the first non-empty value across alternative locations
717
+ * (e.g. agy's content vs. thinking).
718
+ */
719
+ function jsonPathGet(record: any, expr: string): unknown {
720
+ if (typeof expr !== 'string') return undefined;
721
+ if (expr.includes('||')) {
722
+ for (const alt of expr.split('||')) {
723
+ const v = jsonPathGet(record, alt.trim());
724
+ if (v != null && v !== '') return v;
725
+ }
726
+ return undefined;
727
+ }
728
+ if (!expr.startsWith('$')) return expr;
729
+ let cur: any = record;
730
+ let i = 1;
731
+ while (i < expr.length && cur != null) {
732
+ const ch = expr[i];
733
+ if (ch === '.') { i += 1; continue; }
734
+ if (ch === '[') {
735
+ const close = expr.indexOf(']', i);
736
+ if (close < 0) return undefined;
737
+ const idx = Number(expr.slice(i + 1, close));
738
+ if (!Number.isInteger(idx)) return undefined;
739
+ cur = cur[idx];
740
+ i = close + 1;
741
+ continue;
742
+ }
743
+ let end = i;
744
+ while (end < expr.length && expr[end] !== '.' && expr[end] !== '[') end += 1;
745
+ const key = expr.slice(i, end);
746
+ cur = cur[key];
747
+ i = end;
748
+ }
749
+ return cur;
750
+ }
751
+
752
+ function projectMessage(record: any, map: NativeHistoryMessageMap, index: number, total: number, sourceMtimeMs: number): NativeHistoryMessage | null {
753
+ const roleRaw = jsonPathGet(record, map.role);
754
+ const contentRaw = jsonPathGet(record, map.content);
755
+ const role = normalizeRole(roleRaw);
756
+ let content = stringifyContent(contentRaw);
757
+ if (content && map.content_strip) {
758
+ for (const tag of map.content_strip) {
759
+ const safeTag = tag.replace(/[.+^${}()|[\]\\]/g, '\\$&');
760
+ const re = new RegExp(`<${safeTag}\\b[^>]*>[\\s\\S]*?<\\/${safeTag}\\s*>`, 'gi');
761
+ content = content.replace(re, '');
762
+ }
763
+ }
764
+ if (content && map.content_unwrap) {
765
+ for (const tag of map.content_unwrap) {
766
+ const safeTag = tag.replace(/[.+^${}()|[\]\\]/g, '\\$&');
767
+ const open = new RegExp(`<${safeTag}\\b[^>]*>`, 'gi');
768
+ const close = new RegExp(`<\\/${safeTag}\\s*>`, 'gi');
769
+ content = content.replace(open, '').replace(close, '');
770
+ }
771
+ }
772
+ if (content) content = content.trim();
773
+ if (!content) return null;
774
+
775
+ // Records are passed in chronological order (oldest → newest), so the
776
+ // last record's receivedAt should be ~sourceMtimeMs (when the file was
777
+ // last touched) and earlier records should walk backwards. Earlier
778
+ // version had this inverted, which made the dashboard render bubbles
779
+ // in reverse order and produce the "chat jumping" effect.
780
+ let receivedAt = sourceMtimeMs - ((total - 1 - index) * 1000);
781
+ if (map.timestamp_ms) {
782
+ const tsRaw = jsonPathGet(record, map.timestamp_ms);
783
+ const parsed = parseTimestamp(tsRaw);
784
+ if (parsed != null) receivedAt = parsed;
785
+ }
786
+ const kindRaw = map.kind ? jsonPathGet(record, map.kind) : undefined;
787
+ const kind = typeof kindRaw === 'string' && kindRaw ? kindRaw : 'standard';
788
+ return { role, content, receivedAt, kind };
789
+ }
790
+
791
+ /**
792
+ * Coerce a timestamp value to epoch milliseconds. Accepts:
793
+ * - number (ms or seconds — heuristic: < 1e12 means seconds)
794
+ * - ISO 8601 string ("2026-06-05T01:25:28Z")
795
+ * - numeric string
796
+ *
797
+ * Returns null when the value can't be parsed; caller falls back to the
798
+ * monotonic-by-index estimate.
799
+ */
800
+ function parseTimestamp(v: unknown): number | null {
801
+ if (v == null) return null;
802
+ if (typeof v === 'number' && Number.isFinite(v) && v > 0) {
803
+ return v < 1e12 ? Math.floor(v * 1000) : Math.floor(v);
804
+ }
805
+ if (typeof v === 'string' && v) {
806
+ const trimmed = v.trim();
807
+ const asNum = Number(trimmed);
808
+ if (Number.isFinite(asNum) && asNum > 0) {
809
+ return asNum < 1e12 ? Math.floor(asNum * 1000) : Math.floor(asNum);
810
+ }
811
+ const parsed = Date.parse(trimmed);
812
+ if (Number.isFinite(parsed)) return parsed;
813
+ }
814
+ return null;
815
+ }
816
+
817
+ function normalizeRole(r: unknown): 'user' | 'assistant' | 'system' {
818
+ const s = String(r ?? '').toLowerCase();
819
+ if (s === 'user' || s === 'human' || s === 'user_explicit') return 'user';
820
+ if (s === 'assistant' || s === 'ai' || s === 'model') return 'assistant';
821
+ if (s === 'tool' || s === 'tool_result' || s === 'function') return 'assistant';
822
+ return 'system';
823
+ }
824
+
825
+ /**
826
+ * Coerce a content value to a plain string the dashboard can render.
827
+ *
828
+ * Many providers ship structured content (claude messages are arrays of
829
+ * typed blocks: text / tool_use / tool_result). We collapse those to
830
+ * their text-bearing parts so the dashboard doesn't show raw JSON
831
+ * fragments in the transcript. Tool calls/results are intentionally
832
+ * dropped — the daemon's chat schema is for user-visible turns.
833
+ *
834
+ * Order of attempts:
835
+ * 1. string → as-is
836
+ * 2. array of blocks → join the `text` field of each
837
+ * block that has one; if none have
838
+ * a text field, fall through
839
+ * 3. object with a top-level `text` → that string
840
+ * 4. last resort → JSON.stringify
841
+ */
842
+ function stringifyContent(v: unknown): string {
843
+ if (v == null) return '';
844
+ if (typeof v === 'string') return v;
845
+ if (Array.isArray(v)) {
846
+ const parts: string[] = [];
847
+ for (const block of v) {
848
+ if (block == null) continue;
849
+ if (typeof block === 'string') { parts.push(block); continue; }
850
+ if (typeof block === 'object') {
851
+ const t = (block as any).text;
852
+ if (typeof t === 'string' && t) { parts.push(t); continue; }
853
+ // tool_use / tool_result / image / etc — skip from the
854
+ // user-facing transcript. They re-surface via the
855
+ // adapter's tool-event channel if/when that's wired.
856
+ }
857
+ }
858
+ if (parts.length > 0) return parts.join('\n');
859
+ return '';
860
+ }
861
+ if (typeof v === 'object') {
862
+ const t = (v as any).text;
863
+ if (typeof t === 'string') return t;
864
+ }
865
+ try { return JSON.stringify(v); } catch { return String(v); }
866
+ }
867
+
868
+ // ────────────────────────────────────────────────────────────────────────────
869
+ // where-clause mini-language
870
+ //
871
+ // Grammar (intentionally tiny — keep spec readable):
872
+ // expr := term (('&&' | '||') term)*
873
+ // term := path op literal
874
+ // op := '==' | '!=' | '>' | '<' | '>=' | '<='
875
+ // path := jsonpath like '$.foo.bar[0].baz'
876
+ // literal := string ('"…"' or "'…'") | number | true | false | null
877
+ //
878
+ // No grouping, no negation. If you need more, write a real reader file
879
+ // behind `native_history.override_path`.
880
+ // ────────────────────────────────────────────────────────────────────────────
881
+
882
+ interface WhereTerm { path: string; op: string; lit: unknown; negate?: boolean }
883
+
884
+ function compileWhere(src: string): (record: any) => boolean {
885
+ const ors: WhereTerm[][] = [];
886
+ for (const orChunk of src.split('||')) {
887
+ const ands: WhereTerm[] = [];
888
+ for (const andChunk of orChunk.split('&&')) {
889
+ const term = parseTerm(andChunk.trim());
890
+ if (term) ands.push(term);
891
+ }
892
+ if (ands.length > 0) ors.push(ands);
893
+ }
894
+ return (record: any) => ors.some(ands => ands.every(t => evalTerm(t, record)));
895
+ }
896
+
897
+ function parseTerm(src: string): WhereTerm | null {
898
+ let s = src.trim();
899
+ let negate = false;
900
+ if (s.startsWith('!')) { negate = true; s = s.slice(1).trim(); }
901
+ // Function-call form: startsWith($.x, "y") / endsWith($.x, "y") / contains($.x, "y")
902
+ const fnMatch = s.match(/^(startsWith|endsWith|contains)\s*\(\s*(.+?)\s*,\s*(.+?)\s*\)$/);
903
+ if (fnMatch) {
904
+ const [, op, pathExpr, litExpr] = fnMatch;
905
+ return { path: pathExpr, op, lit: parseLiteral(litExpr), negate };
906
+ }
907
+ // Binary comparison: <path> <op> <literal>
908
+ const opMatch = s.match(/^(.+?)\s*(==|!=|>=|<=|>|<)\s*(.+)$/);
909
+ if (!opMatch) return null;
910
+ const [, lhs, op, rhsRaw] = opMatch;
911
+ return { path: lhs.trim(), op, lit: parseLiteral(rhsRaw.trim()), negate };
912
+ }
913
+
914
+ function parseLiteral(src: string): unknown {
915
+ if (src === 'true') return true;
916
+ if (src === 'false') return false;
917
+ if (src === 'null') return null;
918
+ if ((src.startsWith('"') && src.endsWith('"')) || (src.startsWith("'") && src.endsWith("'"))) {
919
+ return src.slice(1, -1);
920
+ }
921
+ const n = Number(src);
922
+ if (!Number.isNaN(n)) return n;
923
+ return src; // bareword — treated as string
924
+ }
925
+
926
+ function evalTerm(t: WhereTerm, record: any): boolean {
927
+ const lhs = jsonPathGet(record, t.path);
928
+ const lit = t.lit;
929
+ let result: boolean;
930
+ switch (t.op) {
931
+ case '==': result = lhs === lit; break;
932
+ case '!=': result = lhs !== lit; break;
933
+ case '>': result = Number(lhs) > Number(lit); break;
934
+ case '<': result = Number(lhs) < Number(lit); break;
935
+ case '>=': result = Number(lhs) >= Number(lit); break;
936
+ case '<=': result = Number(lhs) <= Number(lit); break;
937
+ case 'startsWith': result = typeof lhs === 'string' && typeof lit === 'string' && lhs.startsWith(lit); break;
938
+ case 'endsWith': result = typeof lhs === 'string' && typeof lit === 'string' && lhs.endsWith(lit); break;
939
+ case 'contains': result = typeof lhs === 'string' && typeof lit === 'string' && lhs.includes(lit); break;
940
+ default: result = false;
941
+ }
942
+ return t.negate ? !result : result;
943
+ }