harnery 0.33.0 → 0.34.0

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 (657) hide show
  1. package/dist/commander.d.ts.map +1 -1
  2. package/dist/commander.js +4 -0
  3. package/dist/commands/adapter.js +2 -2
  4. package/dist/commands/agents.d.ts +144 -1
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +1220 -588
  7. package/dist/commands/artifacts.d.ts.map +1 -1
  8. package/dist/commands/artifacts.js +7 -6
  9. package/dist/commands/backup.d.ts +1 -1
  10. package/dist/commands/backup.d.ts.map +1 -1
  11. package/dist/commands/backup.js +29 -16
  12. package/dist/commands/browse.d.ts.map +1 -1
  13. package/dist/commands/browse.js +234 -12
  14. package/dist/commands/checkpoint.d.ts.map +1 -1
  15. package/dist/commands/checkpoint.js +3 -2
  16. package/dist/commands/claude-desktop.d.ts +1 -1
  17. package/dist/commands/claude-desktop.d.ts.map +1 -1
  18. package/dist/commands/claude-desktop.js +3 -74
  19. package/dist/commands/context.js +1 -6
  20. package/dist/commands/decision.d.ts.map +1 -1
  21. package/dist/commands/decision.js +22 -38
  22. package/dist/commands/docs.d.ts.map +1 -1
  23. package/dist/commands/docs.js +91 -28
  24. package/dist/commands/doctor.js +10 -1
  25. package/dist/commands/events.d.ts +5 -0
  26. package/dist/commands/events.d.ts.map +1 -0
  27. package/dist/commands/events.js +189 -0
  28. package/dist/commands/governor.d.ts.map +1 -1
  29. package/dist/commands/governor.js +13 -4
  30. package/dist/commands/init.d.ts +9 -2
  31. package/dist/commands/init.d.ts.map +1 -1
  32. package/dist/commands/init.js +223 -51
  33. package/dist/commands/journal.d.ts.map +1 -1
  34. package/dist/commands/journal.js +4 -13
  35. package/dist/commands/ledger-v3.d.ts +5 -0
  36. package/dist/commands/ledger-v3.d.ts.map +1 -0
  37. package/dist/commands/ledger-v3.js +91 -0
  38. package/dist/commands/presence.d.ts.map +1 -1
  39. package/dist/commands/presence.js +11 -5
  40. package/dist/commands/sync.d.ts +1 -1
  41. package/dist/commands/sync.js +1 -1
  42. package/dist/commands/web.d.ts +6 -0
  43. package/dist/commands/web.d.ts.map +1 -1
  44. package/dist/commands/web.js +67 -7
  45. package/dist/commands/work.d.ts.map +1 -1
  46. package/dist/commands/work.js +22 -5
  47. package/dist/core/adapter.d.ts +5 -0
  48. package/dist/core/adapter.d.ts.map +1 -0
  49. package/dist/core/adapter.js +1 -0
  50. package/dist/core/adapters/attest.d.ts +5 -2
  51. package/dist/core/adapters/attest.d.ts.map +1 -1
  52. package/dist/core/adapters/attest.js +8 -3
  53. package/dist/core/adapters/profiles.d.ts +6 -6
  54. package/dist/core/adapters/profiles.d.ts.map +1 -1
  55. package/dist/core/adapters/profiles.js +4 -4
  56. package/dist/core/agents/canonical-emit.d.ts +17 -15
  57. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  58. package/dist/core/agents/canonical-emit.js +22 -22
  59. package/dist/core/agents/cli.js +207 -234
  60. package/dist/core/agents/codex-archive-v3.d.ts +14 -0
  61. package/dist/core/agents/codex-archive-v3.d.ts.map +1 -0
  62. package/dist/core/agents/codex-archive-v3.js +110 -0
  63. package/dist/core/agents/coord-client.d.ts +19 -81
  64. package/dist/core/agents/coord-client.d.ts.map +1 -1
  65. package/dist/core/agents/coord-client.js +65 -155
  66. package/dist/core/agents/finalization.d.ts +4 -1
  67. package/dist/core/agents/finalization.d.ts.map +1 -1
  68. package/dist/core/agents/finalization.js +89 -32
  69. package/dist/core/agents/index.d.ts +4 -5
  70. package/dist/core/agents/index.d.ts.map +1 -1
  71. package/dist/core/agents/index.js +4 -5
  72. package/dist/core/agents/live-authority-v3.d.ts +59 -0
  73. package/dist/core/agents/live-authority-v3.d.ts.map +1 -0
  74. package/dist/core/agents/live-authority-v3.js +233 -0
  75. package/dist/core/agents/live-lifecycle-v3.d.ts +20 -0
  76. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -0
  77. package/dist/core/agents/live-lifecycle-v3.js +91 -0
  78. package/dist/core/agents/live-observation-v3.d.ts +25 -0
  79. package/dist/core/agents/live-observation-v3.d.ts.map +1 -0
  80. package/dist/core/agents/live-observation-v3.js +157 -0
  81. package/dist/core/agents/render/prompt-context.d.ts +4 -4
  82. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  83. package/dist/core/agents/render/prompt-context.js +23 -58
  84. package/dist/core/agents/render/session-context.d.ts +1 -1
  85. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  86. package/dist/core/agents/render/session-context.js +8 -32
  87. package/dist/core/agents/rules/claim-conflict.d.ts +3 -3
  88. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  89. package/dist/core/agents/rules/claim-conflict.js +79 -127
  90. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  91. package/dist/core/agents/rules/commit-conflict.js +3 -18
  92. package/dist/core/agents/rules/stop-hook.d.ts +9 -50
  93. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  94. package/dist/core/agents/rules/stop-hook.js +193 -236
  95. package/dist/core/agents/session-events.d.ts +5 -7
  96. package/dist/core/agents/session-events.d.ts.map +1 -1
  97. package/dist/core/agents/session-events.js +127 -95
  98. package/dist/core/agents/session-finalization-state-v3.d.ts +30 -0
  99. package/dist/core/agents/session-finalization-state-v3.d.ts.map +1 -0
  100. package/dist/core/agents/session-finalization-state-v3.js +40 -0
  101. package/dist/core/agents/session-finalizer-v3.d.ts +107 -0
  102. package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -0
  103. package/dist/core/agents/session-finalizer-v3.js +734 -0
  104. package/dist/core/agents/session-name-display.d.ts +40 -0
  105. package/dist/core/agents/session-name-display.d.ts.map +1 -0
  106. package/dist/core/agents/session-name-display.js +119 -0
  107. package/dist/core/agents/state/activity-log.d.ts +3 -3
  108. package/dist/core/agents/state/activity-log.d.ts.map +1 -1
  109. package/dist/core/agents/state/activity-log.js +7 -16
  110. package/dist/core/agents/state/heartbeat-writer.d.ts +23 -55
  111. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  112. package/dist/core/agents/state/heartbeat-writer.js +56 -228
  113. package/dist/core/agents/state/live-coordination-view.d.ts +31 -0
  114. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -0
  115. package/dist/core/agents/state/live-coordination-view.js +209 -0
  116. package/dist/core/agents/state/session-state.d.ts +4 -3
  117. package/dist/core/agents/state/session-state.d.ts.map +1 -1
  118. package/dist/core/agents/state/session-state.js +18 -9
  119. package/dist/core/agents/state/stale-sweep.d.ts +1 -1
  120. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  121. package/dist/core/agents/state/stale-sweep.js +26 -15
  122. package/dist/core/artifacts/index.d.ts.map +1 -1
  123. package/dist/core/artifacts/index.js +5 -7
  124. package/dist/core/config.d.ts +33 -2
  125. package/dist/core/config.d.ts.map +1 -1
  126. package/dist/core/config.js +98 -2
  127. package/dist/core/context/index.d.ts +1 -4
  128. package/dist/core/context/index.d.ts.map +1 -1
  129. package/dist/core/context/index.js +9 -9
  130. package/dist/core/events/span-parent.d.ts +15 -0
  131. package/dist/core/events/span-parent.d.ts.map +1 -0
  132. package/dist/core/events/span-parent.js +34 -0
  133. package/dist/core/events/v3/advance.d.ts +14 -0
  134. package/dist/core/events/v3/advance.d.ts.map +1 -0
  135. package/dist/core/events/v3/advance.js +134 -0
  136. package/dist/core/events/v3/authority-outbox.d.ts +78 -0
  137. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -0
  138. package/dist/core/events/v3/authority-outbox.js +399 -0
  139. package/dist/core/events/v3/base-builder.d.ts +20 -0
  140. package/dist/core/events/v3/base-builder.d.ts.map +1 -0
  141. package/dist/core/events/v3/base-builder.js +29 -0
  142. package/dist/core/events/v3/base-contract.d.ts +7576 -0
  143. package/dist/core/events/v3/base-contract.d.ts.map +1 -0
  144. package/dist/core/events/v3/base-contract.js +647 -0
  145. package/dist/core/events/v3/bootstrap.d.ts +34 -0
  146. package/dist/core/events/v3/bootstrap.d.ts.map +1 -0
  147. package/dist/core/events/v3/bootstrap.js +175 -0
  148. package/dist/core/events/v3/builder.d.ts +20 -0
  149. package/dist/core/events/v3/builder.d.ts.map +1 -0
  150. package/dist/core/events/v3/builder.js +28 -0
  151. package/dist/core/events/v3/canonical.d.ts +31 -0
  152. package/dist/core/events/v3/canonical.d.ts.map +1 -0
  153. package/dist/core/events/v3/canonical.js +100 -0
  154. package/dist/core/events/v3/capabilities.d.ts +31 -0
  155. package/dist/core/events/v3/capabilities.d.ts.map +1 -0
  156. package/dist/core/events/v3/capabilities.js +107 -0
  157. package/dist/core/events/v3/capability-drift.d.ts +22 -0
  158. package/dist/core/events/v3/capability-drift.d.ts.map +1 -0
  159. package/dist/core/events/v3/capability-drift.js +134 -0
  160. package/dist/core/events/v3/catalog.d.ts +38 -0
  161. package/dist/core/events/v3/catalog.d.ts.map +1 -0
  162. package/dist/core/events/v3/catalog.js +135 -0
  163. package/dist/core/events/v3/clock-order.d.ts +34 -0
  164. package/dist/core/events/v3/clock-order.d.ts.map +1 -0
  165. package/dist/core/events/v3/clock-order.js +35 -0
  166. package/dist/core/events/v3/contract.d.ts +171 -0
  167. package/dist/core/events/v3/contract.d.ts.map +1 -0
  168. package/dist/core/events/v3/contract.js +180 -0
  169. package/dist/core/events/v3/control.d.ts +111 -0
  170. package/dist/core/events/v3/control.d.ts.map +1 -0
  171. package/dist/core/events/v3/control.js +419 -0
  172. package/dist/core/events/v3/coordination-view.d.ts +69 -0
  173. package/dist/core/events/v3/coordination-view.d.ts.map +1 -0
  174. package/dist/core/events/v3/coordination-view.js +119 -0
  175. package/dist/core/events/v3/economics.d.ts +77 -0
  176. package/dist/core/events/v3/economics.d.ts.map +1 -0
  177. package/dist/core/events/v3/economics.js +274 -0
  178. package/dist/core/events/v3/finalization-view.d.ts +26 -0
  179. package/dist/core/events/v3/finalization-view.d.ts.map +1 -0
  180. package/dist/core/events/v3/finalization-view.js +37 -0
  181. package/dist/core/events/v3/fingerprint-keys.d.ts +25 -0
  182. package/dist/core/events/v3/fingerprint-keys.d.ts.map +1 -0
  183. package/dist/core/events/v3/fingerprint-keys.js +217 -0
  184. package/dist/core/events/v3/generated.d.ts +4 -0
  185. package/dist/core/events/v3/generated.d.ts.map +1 -0
  186. package/dist/core/events/v3/generated.js +47 -0
  187. package/dist/core/events/v3/ids.d.ts +9 -0
  188. package/dist/core/events/v3/ids.d.ts.map +1 -0
  189. package/dist/core/events/v3/ids.js +25 -0
  190. package/dist/core/events/v3/index.d.ts +35 -0
  191. package/dist/core/events/v3/index.d.ts.map +1 -0
  192. package/dist/core/events/v3/index.js +33 -0
  193. package/dist/core/events/v3/latency.d.ts +63 -0
  194. package/dist/core/events/v3/latency.d.ts.map +1 -0
  195. package/dist/core/events/v3/latency.js +412 -0
  196. package/dist/core/events/v3/live-feed.d.ts +45 -0
  197. package/dist/core/events/v3/live-feed.d.ts.map +1 -0
  198. package/dist/core/events/v3/live-feed.js +246 -0
  199. package/dist/core/events/v3/live-observation.d.ts +35 -0
  200. package/dist/core/events/v3/live-observation.d.ts.map +1 -0
  201. package/dist/core/events/v3/live-observation.js +1 -0
  202. package/dist/core/events/v3/live-routing.d.ts +57 -0
  203. package/dist/core/events/v3/live-routing.d.ts.map +1 -0
  204. package/dist/core/events/v3/live-routing.js +107 -0
  205. package/dist/core/events/v3/producers/command-recorder.d.ts +67 -0
  206. package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -0
  207. package/dist/core/events/v3/producers/command-recorder.js +477 -0
  208. package/dist/core/events/v3/producers/command.d.ts +50 -0
  209. package/dist/core/events/v3/producers/command.d.ts.map +1 -0
  210. package/dist/core/events/v3/producers/command.js +135 -0
  211. package/dist/core/events/v3/producers/coordination-recorder.d.ts +53 -0
  212. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -0
  213. package/dist/core/events/v3/producers/coordination-recorder.js +376 -0
  214. package/dist/core/events/v3/producers/coordination.d.ts +90 -0
  215. package/dist/core/events/v3/producers/coordination.d.ts.map +1 -0
  216. package/dist/core/events/v3/producers/coordination.js +229 -0
  217. package/dist/core/events/v3/producers/hook-base.d.ts +54 -0
  218. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -0
  219. package/dist/core/events/v3/producers/hook-base.js +424 -0
  220. package/dist/core/events/v3/producers/hook.d.ts +29 -0
  221. package/dist/core/events/v3/producers/hook.d.ts.map +1 -0
  222. package/dist/core/events/v3/producers/hook.js +71 -0
  223. package/dist/core/events/v3/producers/intake.d.ts +65 -0
  224. package/dist/core/events/v3/producers/intake.d.ts.map +1 -0
  225. package/dist/core/events/v3/producers/intake.js +244 -0
  226. package/dist/core/events/v3/producers/recorder.d.ts +240 -0
  227. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -0
  228. package/dist/core/events/v3/producers/recorder.js +2176 -0
  229. package/dist/core/events/v3/projection.d.ts +101 -0
  230. package/dist/core/events/v3/projection.d.ts.map +1 -0
  231. package/dist/core/events/v3/projection.js +479 -0
  232. package/dist/core/events/v3/reader.d.ts +75 -0
  233. package/dist/core/events/v3/reader.d.ts.map +1 -0
  234. package/dist/core/events/v3/reader.js +778 -0
  235. package/dist/core/events/v3/recovery-record.d.ts +47 -0
  236. package/dist/core/events/v3/recovery-record.d.ts.map +1 -0
  237. package/dist/core/events/v3/recovery-record.js +167 -0
  238. package/dist/core/events/v3/recovery.d.ts +22 -0
  239. package/dist/core/events/v3/recovery.d.ts.map +1 -0
  240. package/dist/core/events/v3/recovery.js +227 -0
  241. package/dist/core/events/v3/span-state.d.ts +52 -0
  242. package/dist/core/events/v3/span-state.d.ts.map +1 -0
  243. package/dist/core/events/v3/span-state.js +142 -0
  244. package/dist/core/events/v3/targets.d.ts +23 -0
  245. package/dist/core/events/v3/targets.d.ts.map +1 -0
  246. package/dist/core/events/v3/targets.js +134 -0
  247. package/dist/core/events/v3/turn-telemetry.d.ts +41 -0
  248. package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -0
  249. package/dist/core/events/v3/turn-telemetry.js +168 -0
  250. package/dist/core/events/v3/validate.d.ts +9 -0
  251. package/dist/core/events/v3/validate.d.ts.map +1 -0
  252. package/dist/core/events/v3/validate.js +281 -0
  253. package/dist/core/events/v3/writer.d.ts +56 -0
  254. package/dist/core/events/v3/writer.d.ts.map +1 -0
  255. package/dist/core/events/v3/writer.js +335 -0
  256. package/dist/core/governor/index.d.ts +1 -1
  257. package/dist/core/governor/index.d.ts.map +1 -1
  258. package/dist/core/governor/index.js +1 -1
  259. package/dist/core/governor/plan-api.d.ts +2 -1
  260. package/dist/core/governor/plan-api.d.ts.map +1 -1
  261. package/dist/core/governor/plan-api.js +6 -3
  262. package/dist/core/governor/read.d.ts +1 -1
  263. package/dist/core/governor/read.d.ts.map +1 -1
  264. package/dist/core/governor/read.js +1 -1
  265. package/dist/core/governor/state.d.ts +9 -0
  266. package/dist/core/governor/state.d.ts.map +1 -1
  267. package/dist/core/governor/state.js +40 -3
  268. package/dist/core/guard/config.d.ts +5 -0
  269. package/dist/core/guard/config.d.ts.map +1 -0
  270. package/dist/core/guard/config.js +157 -0
  271. package/dist/core/guard/coordinator.d.ts +14 -0
  272. package/dist/core/guard/coordinator.d.ts.map +1 -0
  273. package/dist/core/guard/coordinator.js +173 -0
  274. package/dist/core/guard/evaluator.d.ts +4 -0
  275. package/dist/core/guard/evaluator.d.ts.map +1 -0
  276. package/dist/core/guard/evaluator.js +242 -0
  277. package/dist/core/guard/evidence-v3.d.ts +15 -0
  278. package/dist/core/guard/evidence-v3.d.ts.map +1 -0
  279. package/dist/core/guard/evidence-v3.js +171 -0
  280. package/dist/core/guard/index.d.ts +10 -0
  281. package/dist/core/guard/index.d.ts.map +1 -0
  282. package/dist/core/guard/index.js +6 -0
  283. package/dist/core/guard/live-source-v3.d.ts +40 -0
  284. package/dist/core/guard/live-source-v3.d.ts.map +1 -0
  285. package/dist/core/guard/live-source-v3.js +128 -0
  286. package/dist/core/guard/storage.d.ts +27 -0
  287. package/dist/core/guard/storage.d.ts.map +1 -0
  288. package/dist/core/guard/storage.js +140 -0
  289. package/dist/core/guard/types.d.ts +119 -0
  290. package/dist/core/guard/types.d.ts.map +1 -0
  291. package/dist/core/guard/types.js +1 -0
  292. package/dist/core/hooks/adapter/detect.d.ts +18 -1
  293. package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
  294. package/dist/core/hooks/adapter/detect.js +29 -0
  295. package/dist/core/hooks/adapter/events.d.ts +7 -4
  296. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  297. package/dist/core/hooks/adapter/events.js +12 -4
  298. package/dist/core/hooks/adapter/output.d.ts +2 -2
  299. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  300. package/dist/core/hooks/adapter/parse.d.ts +7 -4
  301. package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
  302. package/dist/core/hooks/adapter/parse.js +63 -25
  303. package/dist/core/hooks/adapter/wiring.d.ts +30 -4
  304. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  305. package/dist/core/hooks/adapter/wiring.js +89 -11
  306. package/dist/core/hooks/cli.d.ts +1 -1
  307. package/dist/core/hooks/cli.js +305 -444
  308. package/dist/core/hooks/effects/image-capture.d.ts +20 -34
  309. package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
  310. package/dist/core/hooks/effects/image-capture.js +212 -221
  311. package/dist/core/hooks/effects/index.d.ts +1 -9
  312. package/dist/core/hooks/effects/index.d.ts.map +1 -1
  313. package/dist/core/hooks/effects/index.js +1 -24
  314. package/dist/core/hooks/events/input-hash.d.ts +7 -0
  315. package/dist/core/hooks/events/input-hash.d.ts.map +1 -0
  316. package/dist/core/hooks/events/input-hash.js +43 -0
  317. package/dist/core/hooks/index.d.ts +4 -3
  318. package/dist/core/hooks/index.d.ts.map +1 -1
  319. package/dist/core/hooks/index.js +2 -3
  320. package/dist/core/hooks/resolve/coord-root.d.ts +1 -1
  321. package/dist/core/hooks/resolve/coord-root.js +1 -1
  322. package/dist/core/hooks/resolve/owner.d.ts +16 -15
  323. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  324. package/dist/core/hooks/resolve/owner.js +51 -42
  325. package/dist/core/hooks/resolve/transcript.d.ts +33 -3
  326. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  327. package/dist/core/hooks/resolve/transcript.js +155 -14
  328. package/dist/core/hooks/session-name-presence.d.ts +9 -10
  329. package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
  330. package/dist/core/hooks/session-name-presence.js +15 -15
  331. package/dist/core/hooks/unsafe-cross-shell.d.ts +1 -1
  332. package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -1
  333. package/dist/core/journal/index.d.ts +2 -2
  334. package/dist/core/journal/index.d.ts.map +1 -1
  335. package/dist/core/journal/index.js +9 -22
  336. package/dist/core/presence/blob.d.ts +4 -3
  337. package/dist/core/presence/blob.d.ts.map +1 -1
  338. package/dist/core/presence/blob.js +45 -44
  339. package/dist/core/presence/codec-digest.d.ts +29 -0
  340. package/dist/core/presence/codec-digest.d.ts.map +1 -0
  341. package/dist/core/presence/codec-digest.js +170 -0
  342. package/dist/core/presence/relay-client.d.ts +1 -1
  343. package/dist/core/presence/relay-client.d.ts.map +1 -1
  344. package/dist/core/presence/relay-client.js +5 -5
  345. package/dist/core/work/index.d.ts +1 -1
  346. package/dist/core/work/index.d.ts.map +1 -1
  347. package/dist/core/work/index.js +1 -1
  348. package/dist/core/work/read.d.ts +1 -1
  349. package/dist/core/work/read.d.ts.map +1 -1
  350. package/dist/core/work/read.js +1 -1
  351. package/dist/core/work/runner.d.ts.map +1 -1
  352. package/dist/core/work/runner.js +6 -0
  353. package/dist/core/work/state.d.ts +9 -0
  354. package/dist/core/work/state.d.ts.map +1 -1
  355. package/dist/core/work/state.js +17 -3
  356. package/dist/core/workflow/engine.js +24 -25
  357. package/dist/core/workflow/live-session-v3.d.ts +19 -0
  358. package/dist/core/workflow/live-session-v3.d.ts.map +1 -0
  359. package/dist/core/workflow/live-session-v3.js +86 -0
  360. package/dist/core/workflow/scope-id.d.ts +3 -0
  361. package/dist/core/workflow/scope-id.d.ts.map +1 -0
  362. package/dist/core/workflow/scope-id.js +5 -0
  363. package/dist/core/workflow/workspaces/leases.d.ts +4 -0
  364. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  365. package/dist/core/workflow/workspaces/leases.js +53 -19
  366. package/dist/index.d.ts +1 -1
  367. package/dist/index.js +1 -1
  368. package/dist/lib/browser/client.d.ts +41 -0
  369. package/dist/lib/browser/client.d.ts.map +1 -1
  370. package/dist/lib/browser/client.js +203 -0
  371. package/dist/lib/browser/critique.d.ts +28 -2
  372. package/dist/lib/browser/critique.d.ts.map +1 -1
  373. package/dist/lib/browser/critique.js +74 -18
  374. package/dist/lib/browser/geometry.d.ts +1 -1
  375. package/dist/lib/browser/geometry.d.ts.map +1 -1
  376. package/dist/lib/browser/geometry.js +130 -6
  377. package/dist/lib/browser/index.d.ts +3 -0
  378. package/dist/lib/browser/index.d.ts.map +1 -1
  379. package/dist/lib/browser/index.js +3 -0
  380. package/dist/lib/browser/qa-plan.d.ts +174 -0
  381. package/dist/lib/browser/qa-plan.d.ts.map +1 -0
  382. package/dist/lib/browser/qa-plan.js +321 -0
  383. package/dist/lib/browser/qa-reuse.d.ts +99 -0
  384. package/dist/lib/browser/qa-reuse.d.ts.map +1 -0
  385. package/dist/lib/browser/qa-reuse.js +222 -0
  386. package/dist/lib/browser/qa-snapshot.d.ts +64 -0
  387. package/dist/lib/browser/qa-snapshot.d.ts.map +1 -0
  388. package/dist/lib/browser/qa-snapshot.js +171 -0
  389. package/dist/lib/browser/runts.d.ts.map +1 -1
  390. package/dist/lib/browser/runts.js +22 -1
  391. package/dist/lib/browser/tiling.d.ts +71 -0
  392. package/dist/lib/browser/tiling.d.ts.map +1 -0
  393. package/dist/lib/browser/tiling.js +118 -0
  394. package/dist/lib/claude-desktop.d.ts +0 -47
  395. package/dist/lib/claude-desktop.d.ts.map +1 -1
  396. package/dist/lib/claude-desktop.js +1 -169
  397. package/dist/lib/context/index.d.ts +0 -3
  398. package/dist/lib/context/index.d.ts.map +1 -1
  399. package/dist/lib/context/index.js +20 -42
  400. package/dist/lib/council/index.d.ts +2 -2
  401. package/dist/lib/council/index.d.ts.map +1 -1
  402. package/dist/lib/council/index.js +9 -20
  403. package/dist/lib/docs-frontmatter.d.ts +1 -6
  404. package/dist/lib/docs-frontmatter.d.ts.map +1 -1
  405. package/dist/lib/docs-frontmatter.js +12 -78
  406. package/dist/lib/docs-index.d.ts +9 -0
  407. package/dist/lib/docs-index.d.ts.map +1 -1
  408. package/dist/lib/docs-index.js +32 -3
  409. package/dist/lib/docs-lint.d.ts.map +1 -1
  410. package/dist/lib/docs-lint.js +14 -30
  411. package/dist/lib/docs-metadata-audit.d.ts +19 -0
  412. package/dist/lib/docs-metadata-audit.d.ts.map +1 -0
  413. package/dist/lib/docs-metadata-audit.js +132 -0
  414. package/dist/lib/docs-metadata-managed.d.ts +23 -0
  415. package/dist/lib/docs-metadata-managed.d.ts.map +1 -0
  416. package/dist/lib/docs-metadata-managed.js +78 -0
  417. package/dist/lib/docs-metadata-sync.d.ts +20 -0
  418. package/dist/lib/docs-metadata-sync.d.ts.map +1 -0
  419. package/dist/lib/docs-metadata-sync.js +222 -0
  420. package/dist/lib/docs-metadata-v2-schema.d.ts +273 -0
  421. package/dist/lib/docs-metadata-v2-schema.d.ts.map +1 -0
  422. package/dist/lib/docs-metadata-v2-schema.js +134 -0
  423. package/dist/lib/docs-metadata-v2.d.ts +21 -0
  424. package/dist/lib/docs-metadata-v2.d.ts.map +1 -0
  425. package/dist/lib/docs-metadata-v2.js +424 -0
  426. package/dist/lib/docs-new.d.ts +11 -0
  427. package/dist/lib/docs-new.d.ts.map +1 -0
  428. package/dist/lib/docs-new.js +52 -0
  429. package/dist/lib/docs-sweep.d.ts.map +1 -1
  430. package/dist/lib/docs-sweep.js +45 -29
  431. package/dist/lib/headless/index.d.ts +98 -0
  432. package/dist/lib/headless/index.d.ts.map +1 -0
  433. package/dist/lib/headless/index.js +211 -0
  434. package/dist/lib/identities/assume.d.ts +4 -4
  435. package/dist/lib/identities/assume.d.ts.map +1 -1
  436. package/dist/lib/identities/assume.js +52 -68
  437. package/dist/lib/instructions/apply.d.ts +5 -4
  438. package/dist/lib/instructions/apply.d.ts.map +1 -1
  439. package/dist/lib/instructions/apply.js +32 -20
  440. package/dist/lib/instructions/bundle.d.ts +1 -1
  441. package/dist/lib/instructions/bundle.d.ts.map +1 -1
  442. package/dist/lib/instructions/bundle.js +1 -1
  443. package/dist/lib/instructions/splice.d.ts +0 -1
  444. package/dist/lib/instructions/splice.d.ts.map +1 -1
  445. package/dist/lib/instructions/splice.js +22 -6
  446. package/dist/lib/instructions/templates.d.ts +7 -5
  447. package/dist/lib/instructions/templates.d.ts.map +1 -1
  448. package/dist/lib/instructions/templates.js +79 -9
  449. package/package.json +26 -5
  450. package/schemas/config.schema.json +120 -2
  451. package/schemas/docs-metadata-v2.schema.json +441 -0
  452. package/schemas/event-v3.schema.json +24472 -0
  453. package/src/commander.ts +4 -0
  454. package/src/commands/adapter.ts +2 -2
  455. package/src/commands/agents.ts +1512 -592
  456. package/src/commands/artifacts.ts +7 -6
  457. package/src/commands/backup.ts +29 -21
  458. package/src/commands/browse.ts +349 -8
  459. package/src/commands/checkpoint.ts +4 -3
  460. package/src/commands/claude-desktop.ts +2 -101
  461. package/src/commands/context.ts +1 -6
  462. package/src/commands/decision.ts +27 -44
  463. package/src/commands/docs.ts +100 -33
  464. package/src/commands/doctor.ts +14 -1
  465. package/src/commands/events.ts +256 -0
  466. package/src/commands/governor.ts +18 -4
  467. package/src/commands/init.ts +253 -57
  468. package/src/commands/instructions.ts +1 -1
  469. package/src/commands/journal.ts +4 -13
  470. package/src/commands/ledger-v3.ts +111 -0
  471. package/src/commands/presence.ts +11 -10
  472. package/src/commands/sync.ts +1 -1
  473. package/src/commands/web.ts +67 -8
  474. package/src/commands/work.ts +28 -5
  475. package/src/core/adapter.ts +5 -0
  476. package/src/core/adapters/attest.ts +8 -3
  477. package/src/core/adapters/profiles.ts +4 -6
  478. package/src/core/agents/canonical-emit.ts +34 -25
  479. package/src/core/agents/cli.ts +236 -262
  480. package/src/core/agents/codex-archive-v3.ts +138 -0
  481. package/src/core/agents/coord-client.ts +75 -188
  482. package/src/core/agents/finalization.ts +120 -38
  483. package/src/core/agents/index.ts +4 -5
  484. package/src/core/agents/live-authority-v3.ts +375 -0
  485. package/src/core/agents/live-lifecycle-v3.ts +140 -0
  486. package/src/core/agents/live-observation-v3.ts +216 -0
  487. package/src/core/agents/render/prompt-context.ts +29 -60
  488. package/src/core/agents/render/session-context.ts +10 -31
  489. package/src/core/agents/rules/claim-conflict.ts +85 -126
  490. package/src/core/agents/rules/commit-conflict.ts +3 -14
  491. package/src/core/agents/rules/stop-hook.ts +239 -293
  492. package/src/core/agents/session-events.ts +137 -104
  493. package/src/core/agents/session-finalization-state-v3.ts +90 -0
  494. package/src/core/agents/session-finalizer-v3.ts +953 -0
  495. package/src/core/agents/session-name-display.ts +149 -0
  496. package/src/core/agents/state/activity-log.ts +6 -14
  497. package/src/core/agents/state/heartbeat-writer.ts +82 -277
  498. package/src/core/agents/state/live-coordination-view.ts +290 -0
  499. package/src/core/agents/state/session-state.ts +24 -12
  500. package/src/core/agents/state/stale-sweep.ts +40 -17
  501. package/src/core/artifacts/index.ts +4 -5
  502. package/src/core/config.ts +148 -3
  503. package/src/core/context/index.ts +9 -17
  504. package/src/core/events/span-parent.ts +46 -0
  505. package/src/core/events/v3/advance.ts +168 -0
  506. package/src/core/events/v3/authority-outbox.ts +548 -0
  507. package/src/core/events/v3/base-builder.ts +60 -0
  508. package/src/core/events/v3/base-contract.ts +882 -0
  509. package/src/core/events/v3/bootstrap.ts +226 -0
  510. package/src/core/events/v3/builder.ts +59 -0
  511. package/src/core/events/v3/canonical.ts +153 -0
  512. package/src/core/events/v3/capabilities.ts +171 -0
  513. package/src/core/events/v3/capability-drift.ts +177 -0
  514. package/src/core/events/v3/catalog.ts +202 -0
  515. package/src/core/events/v3/clock-order.ts +41 -0
  516. package/src/core/events/v3/contract.ts +365 -0
  517. package/src/core/events/v3/control.ts +576 -0
  518. package/src/core/events/v3/coordination-view.ts +193 -0
  519. package/src/core/events/v3/economics.ts +384 -0
  520. package/src/core/events/v3/finalization-view.ts +67 -0
  521. package/src/core/events/v3/fingerprint-keys.ts +260 -0
  522. package/src/core/events/v3/generated.ts +48 -0
  523. package/src/core/events/v3/ids.ts +33 -0
  524. package/src/core/events/v3/index.ts +322 -0
  525. package/src/core/events/v3/latency.ts +551 -0
  526. package/src/core/events/v3/live-feed.ts +319 -0
  527. package/src/core/events/v3/live-observation.ts +39 -0
  528. package/src/core/events/v3/live-routing.ts +135 -0
  529. package/src/core/events/v3/producers/command-recorder.ts +631 -0
  530. package/src/core/events/v3/producers/command.ts +201 -0
  531. package/src/core/events/v3/producers/coordination-recorder.ts +538 -0
  532. package/src/core/events/v3/producers/coordination.ts +355 -0
  533. package/src/core/events/v3/producers/hook-base.ts +578 -0
  534. package/src/core/events/v3/producers/hook.ts +119 -0
  535. package/src/core/events/v3/producers/intake.ts +319 -0
  536. package/src/core/events/v3/producers/recorder.ts +2805 -0
  537. package/src/core/events/v3/projection.ts +653 -0
  538. package/src/core/events/v3/reader.ts +1127 -0
  539. package/src/core/events/v3/recovery-record.ts +244 -0
  540. package/src/core/events/v3/recovery.ts +271 -0
  541. package/src/core/events/v3/span-state.ts +187 -0
  542. package/src/core/events/v3/targets.ts +182 -0
  543. package/src/core/events/v3/turn-telemetry.ts +259 -0
  544. package/src/core/events/v3/validate.ts +361 -0
  545. package/src/core/events/v3/writer.ts +419 -0
  546. package/src/core/governor/index.ts +3 -0
  547. package/src/core/governor/plan-api.ts +10 -2
  548. package/src/core/governor/read.ts +3 -0
  549. package/src/core/governor/state.ts +52 -5
  550. package/src/core/guard/config.ts +206 -0
  551. package/src/core/guard/coordinator.ts +227 -0
  552. package/src/core/guard/evaluator.ts +343 -0
  553. package/src/core/guard/evidence-v3.ts +227 -0
  554. package/src/core/guard/index.ts +35 -0
  555. package/src/core/guard/live-source-v3.ts +204 -0
  556. package/src/core/guard/storage.ts +188 -0
  557. package/src/core/guard/types.ts +152 -0
  558. package/src/core/hooks/adapter/detect.ts +28 -1
  559. package/src/core/hooks/adapter/events.ts +14 -6
  560. package/src/core/hooks/adapter/output.ts +7 -2
  561. package/src/core/hooks/adapter/parse.ts +80 -38
  562. package/src/core/hooks/adapter/wiring.ts +119 -13
  563. package/src/core/hooks/cli.ts +351 -466
  564. package/src/core/hooks/effects/image-capture.ts +228 -238
  565. package/src/core/hooks/effects/index.ts +1 -36
  566. package/src/core/hooks/events/input-hash.ts +47 -0
  567. package/src/core/hooks/index.ts +4 -11
  568. package/src/core/hooks/resolve/coord-root.ts +1 -1
  569. package/src/core/hooks/resolve/owner.ts +52 -45
  570. package/src/core/hooks/resolve/transcript.ts +165 -14
  571. package/src/core/hooks/session-name-presence.ts +14 -15
  572. package/src/core/hooks/unsafe-cross-shell.ts +1 -1
  573. package/src/core/journal/index.ts +15 -22
  574. package/src/core/presence/blob.ts +48 -41
  575. package/src/core/presence/codec-digest.ts +215 -0
  576. package/src/core/presence/relay-client.ts +5 -5
  577. package/src/core/work/index.ts +3 -0
  578. package/src/core/work/read.ts +3 -0
  579. package/src/core/work/runner.ts +6 -0
  580. package/src/core/work/state.ts +27 -3
  581. package/src/core/workflow/engine.ts +24 -24
  582. package/src/core/workflow/live-session-v3.ts +126 -0
  583. package/src/core/workflow/scope-id.ts +6 -0
  584. package/src/core/workflow/workspaces/leases.ts +56 -19
  585. package/src/index.ts +1 -1
  586. package/src/lib/browser/client.ts +230 -0
  587. package/src/lib/browser/critique.ts +104 -27
  588. package/src/lib/browser/geometry.ts +143 -14
  589. package/src/lib/browser/index.ts +34 -0
  590. package/src/lib/browser/qa-plan.ts +501 -0
  591. package/src/lib/browser/qa-reuse.ts +306 -0
  592. package/src/lib/browser/qa-snapshot.ts +223 -0
  593. package/src/lib/browser/runts.ts +27 -2
  594. package/src/lib/browser/tiling.ts +164 -0
  595. package/src/lib/claude-desktop.ts +1 -232
  596. package/src/lib/context/index.ts +22 -46
  597. package/src/lib/council/index.ts +9 -21
  598. package/src/lib/docs-frontmatter.ts +8 -79
  599. package/src/lib/docs-index.ts +33 -3
  600. package/src/lib/docs-lint.ts +15 -29
  601. package/src/lib/docs-metadata-audit.ts +166 -0
  602. package/src/lib/docs-metadata-managed.ts +83 -0
  603. package/src/lib/docs-metadata-sync.ts +262 -0
  604. package/src/lib/docs-metadata-v2-schema.ts +138 -0
  605. package/src/lib/docs-metadata-v2.ts +646 -0
  606. package/src/lib/docs-new.ts +63 -0
  607. package/src/lib/docs-sweep.ts +42 -28
  608. package/src/lib/headless/index.ts +287 -0
  609. package/src/lib/identities/assume.ts +67 -86
  610. package/src/lib/instructions/apply.ts +33 -20
  611. package/src/lib/instructions/bundle.ts +2 -2
  612. package/src/lib/instructions/splice.ts +21 -7
  613. package/src/lib/instructions/templates.ts +85 -13
  614. package/dist/core/agents/cli-emit.d.ts +0 -27
  615. package/dist/core/agents/cli-emit.d.ts.map +0 -1
  616. package/dist/core/agents/cli-emit.js +0 -57
  617. package/dist/core/agents/codex-replay.d.ts +0 -29
  618. package/dist/core/agents/codex-replay.d.ts.map +0 -1
  619. package/dist/core/agents/codex-replay.js +0 -138
  620. package/dist/core/agents/events/consume.d.ts +0 -82
  621. package/dist/core/agents/events/consume.d.ts.map +0 -1
  622. package/dist/core/agents/events/consume.js +0 -195
  623. package/dist/core/agents/events/emit.d.ts +0 -42
  624. package/dist/core/agents/events/emit.d.ts.map +0 -1
  625. package/dist/core/agents/events/emit.js +0 -75
  626. package/dist/core/agents/events/ulid.d.ts +0 -11
  627. package/dist/core/agents/events/ulid.d.ts.map +0 -1
  628. package/dist/core/agents/events/ulid.js +0 -47
  629. package/dist/core/agents/state/heartbeat-projector.d.ts +0 -76
  630. package/dist/core/agents/state/heartbeat-projector.d.ts.map +0 -1
  631. package/dist/core/agents/state/heartbeat-projector.js +0 -581
  632. package/dist/core/hooks/events/emit.d.ts +0 -31
  633. package/dist/core/hooks/events/emit.d.ts.map +0 -1
  634. package/dist/core/hooks/events/emit.js +0 -93
  635. package/dist/core/hooks/events/rotate.d.ts +0 -43
  636. package/dist/core/hooks/events/rotate.d.ts.map +0 -1
  637. package/dist/core/hooks/events/rotate.js +0 -142
  638. package/dist/core/hooks/events/schema.d.ts +0 -341
  639. package/dist/core/hooks/events/schema.d.ts.map +0 -1
  640. package/dist/core/hooks/events/schema.js +0 -12
  641. package/dist/core/hooks/events/ulid.d.ts +0 -10
  642. package/dist/core/hooks/events/ulid.d.ts.map +0 -1
  643. package/dist/core/hooks/events/ulid.js +0 -47
  644. package/dist/lib/docs-frontmatter-migrate.d.ts +0 -33
  645. package/dist/lib/docs-frontmatter-migrate.d.ts.map +0 -1
  646. package/dist/lib/docs-frontmatter-migrate.js +0 -364
  647. package/src/core/agents/cli-emit.ts +0 -64
  648. package/src/core/agents/codex-replay.ts +0 -163
  649. package/src/core/agents/events/consume.ts +0 -254
  650. package/src/core/agents/events/emit.ts +0 -115
  651. package/src/core/agents/events/ulid.ts +0 -51
  652. package/src/core/agents/state/heartbeat-projector.ts +0 -645
  653. package/src/core/hooks/events/emit.ts +0 -125
  654. package/src/core/hooks/events/rotate.ts +0 -151
  655. package/src/core/hooks/events/schema.ts +0 -582
  656. package/src/core/hooks/events/ulid.ts +0 -51
  657. package/src/lib/docs-frontmatter-migrate.ts +0 -427
@@ -0,0 +1,2805 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ chmodSync,
4
+ closeSync,
5
+ existsSync,
6
+ fsyncSync,
7
+ lstatSync,
8
+ mkdirSync,
9
+ openSync,
10
+ readdirSync,
11
+ readFileSync,
12
+ renameSync,
13
+ unlinkSync,
14
+ writeFileSync,
15
+ } from "node:fs";
16
+ import { hostname } from "node:os";
17
+ import { basename, join, resolve } from "node:path";
18
+ import type { Adapter } from "../../../adapter.ts";
19
+ import { extractBashCommand, type ParsedPayload } from "../../../hooks/adapter/parse.ts";
20
+ import { fsyncParentDirectory } from "../../../workflow/durable-record.ts";
21
+ import { acquireNoClobberLease } from "../../../workflow/workspaces/leases.ts";
22
+ import { buildEventV3 } from "../builder.ts";
23
+ import { fingerprintV3, normalizeNativeIdV3 } from "../canonical.ts";
24
+ import {
25
+ type AdapterSignalV3,
26
+ adapterCapabilityProfileDigestV3,
27
+ adapterSignalSupportV3,
28
+ type CursorExecutionModeV3,
29
+ cursorToolChannelSupportV3,
30
+ } from "../capabilities.ts";
31
+ import { capabilityDriftPayloadsV3 } from "../capability-drift.ts";
32
+ import { markObservedClockRegressionV3 } from "../clock-order.ts";
33
+ import type { EventV3 } from "../contract.ts";
34
+ import { type EventV3WriteMode, readEventV3ControlState } from "../control.ts";
35
+ import { fingerprintContextV3 } from "../fingerprint-keys.ts";
36
+ import { attestationIdV3, clockIdV3, delegationIdV3, generationIdV3, spanIdV3 } from "../ids.ts";
37
+ import { readLedgerV3 } from "../reader.ts";
38
+ import {
39
+ closeSpanStateV3,
40
+ type OpenSpanStateV3,
41
+ openSpanStateV3,
42
+ recoverSpanUpperBoundV3,
43
+ type SpanClockV3,
44
+ } from "../span-state.ts";
45
+ import {
46
+ type ContextMeasurementV3,
47
+ extractTurnTelemetryV3,
48
+ type TelemetryObservationV3,
49
+ } from "../turn-telemetry.ts";
50
+ import { assertEventV3, validateEventV3 } from "../validate.ts";
51
+ import {
52
+ EVENT_V3_LEDGER_RELATIVE_ROOT,
53
+ type WriteEventV3Options,
54
+ type WriteEventV3Result,
55
+ writeEventV3,
56
+ } from "../writer.ts";
57
+ import { type HookSignalV3, normalizeHookEventV3 } from "./hook.ts";
58
+ import type { TurnRitualEvidenceV3 } from "./hook-base.ts";
59
+ import {
60
+ appendHookIntakeRecordV3,
61
+ type HookIntakeRecordV3,
62
+ hookIntakeGroupDirV3,
63
+ listHookIntakeGroupsV3,
64
+ listHookIntakeRecordsV3,
65
+ removeIntakeRecordV3,
66
+ writeProducerDiagnosticV3,
67
+ } from "./intake.ts";
68
+
69
+ const STATE_FORMAT = "harnery-v3-hook-producer" as const;
70
+ const STATE_VERSION = 3 as const;
71
+
72
+ /** ADR 0078 recovery policy constants. */
73
+ const CLOSED_SPAN_TURN_RETENTION = 2;
74
+ const CLOSED_SPAN_MEMORY_CAP = 512;
75
+ const CLOSED_TURN_MEMORY_CAP = 16;
76
+ const SPAN_SOFT_WATERMARK = 128;
77
+ /**
78
+ * Adapters whose turn-boundary recovery and mid-flight onboarding are enabled.
79
+ * Kept in code, outside the digested capability profiles, so tuning recovery
80
+ * never changes an adapter capability digest (ADR 0078).
81
+ */
82
+ const RECOVERY_ENABLED_ADAPTERS: ReadonlySet<Adapter> = new Set(["claude-code", "codex", "cursor"]);
83
+
84
+ interface SpanStateV3 extends OpenSpanStateV3 {
85
+ source_id: `hid_${string}`;
86
+ semantic_key?: `hid_${string}`;
87
+ recovery_reason?: string;
88
+ turn_id?: `tid_${string}`;
89
+ turn_stamp?: "native_payload" | "producer_state";
90
+ requested_event_id?: `evt_${string}`;
91
+ tool_name?: string;
92
+ }
93
+
94
+ interface ClosedSpanV3 {
95
+ source_id: `hid_${string}`;
96
+ semantic_key?: `hid_${string}`;
97
+ span_id: `span_${string}`;
98
+ closed_event_id: `evt_${string}`;
99
+ turn_ordinal: number;
100
+ }
101
+
102
+ interface OpenWaitV3 extends OpenSpanStateV3 {
103
+ wait_id: `hid_${string}`;
104
+ started_event_id: `evt_${string}`;
105
+ turn_id: `tid_${string}`;
106
+ }
107
+
108
+ interface TurnHarnessTimingV3 {
109
+ hook_time_ms: number;
110
+ hook_count: number;
111
+ slowest_hook?: string;
112
+ slowest_hook_ms: number;
113
+ }
114
+
115
+ interface PendingEventV3 {
116
+ source_id?: `hid_${string}`;
117
+ event: EventV3;
118
+ }
119
+
120
+ interface DelegationStateV3 extends OpenSpanStateV3 {
121
+ source_id: `hid_${string}`;
122
+ delegation_id: `del_${string}`;
123
+ child_generation_id: `gen_${string}`;
124
+ role: string;
125
+ }
126
+
127
+ export interface HookProducerStateV3 {
128
+ format: typeof STATE_FORMAT;
129
+ format_version: typeof STATE_VERSION;
130
+ adapter: Adapter;
131
+ instance_id: `inst_${string}`;
132
+ session_id: `sid_${string}`;
133
+ generation_id: `gen_${string}`;
134
+ attestation_id: `att_${string}`;
135
+ capability_profile: `cap_${string}`;
136
+ cursor_mode?: CursorExecutionModeV3;
137
+ privacy_epoch_id: `pep_${string}`;
138
+ boot_id: `boot_${string}`;
139
+ clock_id: `clk_${string}`;
140
+ next_sequence: number;
141
+ current_turn_id?: `tid_${string}`;
142
+ tool_call_count: number;
143
+ tool_call_count_turn_id?: `tid_${string}`;
144
+ last_event_id?: `evt_${string}`;
145
+ last_monotonic_ns?: string;
146
+ last_observed_at?: string;
147
+ started_event_id?: `evt_${string}`;
148
+ session_span: OpenSpanStateV3;
149
+ current_turn_span?: OpenSpanStateV3;
150
+ terminal: boolean;
151
+ spans: SpanStateV3[];
152
+ delegations: DelegationStateV3[];
153
+ closed_spans: ClosedSpanV3[];
154
+ closed_turn_ids: `tid_${string}`[];
155
+ waits: OpenWaitV3[];
156
+ turn_harness: TurnHarnessTimingV3;
157
+ turn_ordinal: number;
158
+ pending?: PendingEventV3;
159
+ }
160
+
161
+ export interface RecordHookSignalV3Input {
162
+ coordRoot: string;
163
+ mode: EventV3WriteMode;
164
+ signal: HookSignalV3;
165
+ payload: ParsedPayload;
166
+ adapter: Adapter;
167
+ instance_id: `inst_${string}`;
168
+ run_id?: `run_${string}`;
169
+ workflow_id?: `wf_${string}`;
170
+ workflow_agent_id?: string;
171
+ producer_id: `prd_${string}`;
172
+ build_id: `build_${string}`;
173
+ platform: "linux" | "windows" | "macos" | "unknown";
174
+ bridge?: "codex-wsl";
175
+ adapterVersion?: string;
176
+ harnessVersion?: string;
177
+ monotonic_ns?: string;
178
+ observed_at?: string;
179
+ hook_name?: string;
180
+ hook_duration_ms?: number;
181
+ stop_remediation?: boolean;
182
+ turn_ritual?: TurnRitualEvidenceV3;
183
+ session_start_derivation?: "approved_lifecycle_reopen";
184
+ writerOptions?: WriteEventV3Options;
185
+ }
186
+
187
+ export type RecordHookSignalV3Result =
188
+ | { state: "gate_closed"; reason: string }
189
+ | { state: "missing_session_start" }
190
+ | { state: "already_started"; event_id: string }
191
+ | { state: "unpairable_tool"; reason: "missing_tool_use_id" | "no_open_span" }
192
+ /** A late signal for a span already closed in memory; preserved in diagnostics, never re-opened. */
193
+ | { state: "suppressed"; reason: "closed_span" }
194
+ | { state: "ignored" }
195
+ /** Durably queued in the intake spool; a lease holder or drain hook records it. */
196
+ | { state: "spooled" }
197
+ | { state: "recorded"; event: EventV3; durability: WriteEventV3Result; recovered: boolean };
198
+
199
+ export type ApprovedSessionEndReasonV3 =
200
+ | "approved_explicit_end"
201
+ | "approved_verified_archive"
202
+ | "policy_idle_timeout"
203
+ | "policy_parent_terminal"
204
+ | "policy_stale_sweep"
205
+ | "policy_agent_completed"
206
+ | "policy_run_completed"
207
+ | "policy_superseded"
208
+ | "policy_host_disappeared";
209
+
210
+ export interface HookProducerStateRecordV3 {
211
+ path: string;
212
+ modified_at_ms: number;
213
+ state: HookProducerStateV3;
214
+ }
215
+
216
+ export interface RecordApprovedSessionEndV3Input {
217
+ coordRoot: string;
218
+ mode: EventV3WriteMode;
219
+ instance_id: `inst_${string}`;
220
+ generation_id: `gen_${string}`;
221
+ build_id: `build_${string}`;
222
+ platform: "linux" | "windows" | "macos" | "unknown";
223
+ reason: ApprovedSessionEndReasonV3;
224
+ outcome:
225
+ | "succeeded"
226
+ | "failed"
227
+ | "cancelled"
228
+ | "timed_out"
229
+ | "denied"
230
+ | "interrupted"
231
+ | "unknown";
232
+ observed_at?: string;
233
+ caused_by_event_id?: `evt_${string}`;
234
+ coordination_finalized?: boolean;
235
+ confidence?: "exact" | "high" | "medium" | "low";
236
+ writerOptions?: WriteEventV3Options;
237
+ }
238
+
239
+ export type RecordApprovedSessionEndV3Result =
240
+ | { state: "gate_closed"; reason: string }
241
+ | { state: "generation_unavailable"; reason: string }
242
+ | { state: "already_ended"; event_id?: `evt_${string}` }
243
+ | { state: "recorded"; event: EventV3; durability: WriteEventV3Result; recovered: boolean };
244
+
245
+ type OpenHookControlStateV3 = Extract<
246
+ ReturnType<typeof readEventV3ControlState>,
247
+ { state: "candidate" } | { state: "active" }
248
+ >;
249
+
250
+ const STATE_LEASE_RETRY_ATTEMPTS = 8;
251
+ const STATE_LEASE_RETRY_DELAY_MS = 25;
252
+
253
+ /**
254
+ * Record one hook signal through a private, crash-recoverable producer state file.
255
+ * The function is inert unless the exact requested candidate or active gate is open.
256
+ *
257
+ * Delivery guarantee: the parsed signal is appended to a durable intake spool
258
+ * BEFORE any producer state is read or validated, so a lost lease, a crash, or
259
+ * a state-format mismatch never destroys a delivered signal. Whichever process
260
+ * holds the session's state lease drains the spool in append order and rescans
261
+ * until an empty pass; reconcile and session-start hooks drain any group whose
262
+ * final appender never got the lease.
263
+ */
264
+ export function recordHookSignalV3(input: RecordHookSignalV3Input): RecordHookSignalV3Result {
265
+ const control = readEventV3ControlState(input.coordRoot);
266
+ if (control.state !== input.mode) {
267
+ return { state: "gate_closed", reason: control.state };
268
+ }
269
+ const gate = hookSignalGate(control, input.adapter, input.signal);
270
+ if (gate) return gate;
271
+ const rootId = control.genesis.event.scope.root_id as `root_${string}`;
272
+ const epochId = control.genesis.profile.privacy_key_epoch;
273
+ const rootFingerprintContext = fingerprintContextV3(input.coordRoot, rootId, undefined, epochId);
274
+ const sessionHash = sessionHashForSignal(input, rootFingerprintContext);
275
+ const path = producerStatePath(input.coordRoot, input.adapter, sessionHash);
276
+ const spoolPath = appendHookIntakeRecordV3(input.coordRoot, sessionHash, intakeRecord(input));
277
+ const lease = acquireStateLeaseWithRetry(input.coordRoot, path, STATE_LEASE_RETRY_ATTEMPTS);
278
+ if (!lease) return { state: "spooled" };
279
+ try {
280
+ return (
281
+ drainSessionIntakeLocked(input.coordRoot, control, input.adapter, sessionHash, path, {
282
+ path: spoolPath,
283
+ input,
284
+ }) ?? { state: "spooled" }
285
+ );
286
+ } finally {
287
+ lease.release();
288
+ }
289
+ }
290
+
291
+ function hookSignalGate(
292
+ control: OpenHookControlStateV3,
293
+ adapter: Adapter,
294
+ signal: HookSignalV3,
295
+ ): RecordHookSignalV3Result | undefined {
296
+ const expectedCapabilityDigest = `sha256:${adapterCapabilityProfileDigestV3(adapter).slice(4)}`;
297
+ if (
298
+ !control.genesis.profile.adapter_capability_profile_digests.includes(expectedCapabilityDigest)
299
+ ) {
300
+ return { state: "gate_closed", reason: "capability_profile_not_approved" };
301
+ }
302
+ const requiredCapability = hookSignalCapability(signal);
303
+ if (adapterSignalSupportV3(adapter, requiredCapability) === "unsupported") {
304
+ return {
305
+ state: "gate_closed",
306
+ reason: `signal_not_approved:${requiredCapability}`,
307
+ };
308
+ }
309
+ return undefined;
310
+ }
311
+
312
+ function sessionHashForSignal(
313
+ input: Pick<RecordHookSignalV3Input, "coordRoot" | "payload" | "adapter" | "instance_id">,
314
+ context: ReturnType<typeof fingerprintContextV3>,
315
+ ): `hid_${string}` {
316
+ const nativeSession =
317
+ input.payload.session_id ?? input.payload.conversation_id ?? input.payload.agent_id;
318
+ if (nativeSession !== undefined) {
319
+ return normalizeNativeIdV3(context, `${input.adapter}.session`, nativeSession);
320
+ }
321
+
322
+ // Cursor carries native session identity only on its start signal. Later
323
+ // hooks may still have an exact coordination instance, so reuse its single
324
+ // live producer authority instead of hashing the instance into a second
325
+ // state path. Ambiguous or absent matches keep the fail-closed fallback.
326
+ if (input.adapter === "cursor") {
327
+ const matches = listHookProducerStateRecordsV3(input.coordRoot).filter(
328
+ ({ state }) => state.adapter === input.adapter && state.instance_id === input.instance_id,
329
+ );
330
+ if (matches.length === 1) {
331
+ const name = basename(matches[0]!.path);
332
+ if (/^hid_[a-f0-9]{64}\.json$/.test(name)) {
333
+ return name.slice(0, -".json".length) as `hid_${string}`;
334
+ }
335
+ }
336
+ }
337
+ return normalizeNativeIdV3(context, `${input.adapter}.session`, input.instance_id);
338
+ }
339
+
340
+ function intakeRecord(input: RecordHookSignalV3Input): HookIntakeRecordV3 {
341
+ return {
342
+ format: "harnery-v3-hook-intake",
343
+ format_version: 1,
344
+ mode: input.mode,
345
+ signal: input.signal,
346
+ payload: input.payload,
347
+ adapter: input.adapter,
348
+ instance_id: input.instance_id,
349
+ producer_id: input.producer_id,
350
+ build_id: input.build_id,
351
+ platform: input.platform,
352
+ ...(input.bridge ? { bridge: input.bridge } : {}),
353
+ ...(input.adapterVersion ? { adapterVersion: input.adapterVersion } : {}),
354
+ ...(input.harnessVersion ? { harnessVersion: input.harnessVersion } : {}),
355
+ ...(input.monotonic_ns ? { monotonic_ns: input.monotonic_ns } : {}),
356
+ ...(input.observed_at ? { observed_at: input.observed_at } : {}),
357
+ ...(input.hook_name ? { hook_name: safeRole(input.hook_name) } : {}),
358
+ ...(input.hook_duration_ms !== undefined
359
+ ? { hook_duration_ms: Math.max(0, Math.floor(input.hook_duration_ms)) }
360
+ : {}),
361
+ ...(input.stop_remediation !== undefined ? { stop_remediation: input.stop_remediation } : {}),
362
+ ...(input.turn_ritual ? { turn_ritual: input.turn_ritual } : {}),
363
+ };
364
+ }
365
+
366
+ function inputForIntakeRecord(
367
+ coordRoot: string,
368
+ record: HookIntakeRecordV3,
369
+ ): RecordHookSignalV3Input {
370
+ return {
371
+ coordRoot,
372
+ mode: record.mode,
373
+ signal: record.signal,
374
+ payload: record.payload,
375
+ adapter: record.adapter,
376
+ instance_id: record.instance_id,
377
+ producer_id: record.producer_id,
378
+ build_id: record.build_id,
379
+ platform: record.platform,
380
+ ...(record.bridge ? { bridge: record.bridge } : {}),
381
+ ...(record.adapterVersion ? { adapterVersion: record.adapterVersion } : {}),
382
+ ...(record.harnessVersion ? { harnessVersion: record.harnessVersion } : {}),
383
+ ...(record.monotonic_ns ? { monotonic_ns: record.monotonic_ns } : {}),
384
+ ...(record.observed_at ? { observed_at: record.observed_at } : {}),
385
+ ...(record.hook_name ? { hook_name: record.hook_name } : {}),
386
+ ...(record.hook_duration_ms !== undefined ? { hook_duration_ms: record.hook_duration_ms } : {}),
387
+ ...(record.stop_remediation !== undefined ? { stop_remediation: record.stop_remediation } : {}),
388
+ ...(record.turn_ritual ? { turn_ritual: record.turn_ritual } : {}),
389
+ };
390
+ }
391
+
392
+ interface OwnIntakeRecordV3 {
393
+ path: string;
394
+ input: RecordHookSignalV3Input;
395
+ }
396
+
397
+ /**
398
+ * Drain one session's intake spool under its held state lease. Records are
399
+ * processed in append order and deleted only after their outcome is durably
400
+ * published; the loop rescans until an empty pass so appends that landed while
401
+ * draining are still picked up. A record that throws is preserved in the
402
+ * diagnostics spool and removed so it cannot poison the drain; the caller's
403
+ * own record rethrows to keep today's error visibility.
404
+ */
405
+ function drainSessionIntakeLocked(
406
+ coordRoot: string,
407
+ control: OpenHookControlStateV3,
408
+ adapter: Adapter,
409
+ sessionHash: `hid_${string}`,
410
+ statePath: string,
411
+ own?: OwnIntakeRecordV3,
412
+ ): RecordHookSignalV3Result | undefined {
413
+ const directory = hookIntakeGroupDirV3(coordRoot, adapter, sessionHash);
414
+ let ownResult: RecordHookSignalV3Result | undefined;
415
+ for (;;) {
416
+ const entries = listHookIntakeRecordsV3(directory);
417
+ if (entries.length === 0) break;
418
+ for (const entry of entries) {
419
+ const isOwn = own !== undefined && entry.path === own.path;
420
+ let result: RecordHookSignalV3Result | undefined;
421
+ if (!entry.record) {
422
+ writeProducerDiagnosticV3(coordRoot, "intake_unreadable", { path: entry.path });
423
+ } else if (entry.record.mode !== control.state) {
424
+ writeProducerDiagnosticV3(coordRoot, "intake_gate_mismatch", {
425
+ observed_gate: control.state,
426
+ ...entry.record,
427
+ });
428
+ } else {
429
+ const recordInput = isOwn ? own.input : inputForIntakeRecord(coordRoot, entry.record);
430
+ try {
431
+ result = processHookSignalLocked(control, recordInput, sessionHash, statePath);
432
+ } catch (error) {
433
+ writeProducerDiagnosticV3(coordRoot, "intake_poison", {
434
+ error: String(error),
435
+ ...entry.record,
436
+ });
437
+ removeIntakeRecordV3(entry.path);
438
+ if (isOwn) throw error;
439
+ continue;
440
+ }
441
+ }
442
+ removeIntakeRecordV3(entry.path);
443
+ if (isOwn) ownResult = result ?? { state: "spooled" };
444
+ }
445
+ }
446
+ return ownResult;
447
+ }
448
+
449
+ export interface DrainHookIntakeSpoolV3Result {
450
+ groups_with_records: number;
451
+ groups_drained: number;
452
+ groups_skipped_busy: number;
453
+ }
454
+
455
+ /**
456
+ * Drain every session's pending intake records. This is the terminal drainer:
457
+ * it does not depend on a "next signal" ever arriving for a session, so it is
458
+ * wired into reconcile and session-start paths to pick up a final signal whose
459
+ * appender lost the lease and exited.
460
+ */
461
+ export function drainHookIntakeSpoolV3(coordRoot: string): DrainHookIntakeSpoolV3Result {
462
+ const result: DrainHookIntakeSpoolV3Result = {
463
+ groups_with_records: 0,
464
+ groups_drained: 0,
465
+ groups_skipped_busy: 0,
466
+ };
467
+ const control = readEventV3ControlState(coordRoot);
468
+ if (control.state !== "candidate" && control.state !== "active") return result;
469
+ for (const group of listHookIntakeGroupsV3(coordRoot)) {
470
+ if (listHookIntakeRecordsV3(group.directory).length === 0) continue;
471
+ result.groups_with_records += 1;
472
+ const statePath = producerStatePath(coordRoot, group.adapter, group.session_hash);
473
+ const lease = acquireStateLeaseWithRetry(coordRoot, statePath, 2);
474
+ if (!lease) {
475
+ result.groups_skipped_busy += 1;
476
+ continue;
477
+ }
478
+ try {
479
+ drainSessionIntakeLocked(coordRoot, control, group.adapter, group.session_hash, statePath);
480
+ result.groups_drained += 1;
481
+ } finally {
482
+ lease.release();
483
+ }
484
+ }
485
+ return result;
486
+ }
487
+
488
+ function acquireStateLeaseWithRetry(
489
+ coordRoot: string,
490
+ statePath: string,
491
+ attempts: number,
492
+ ): ReturnType<typeof acquireStateLease> | undefined {
493
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
494
+ try {
495
+ return acquireStateLease(coordRoot, statePath);
496
+ } catch (error) {
497
+ const message = String(error);
498
+ const contended =
499
+ message.includes("held by a live or unexpired owner") ||
500
+ message.includes("recovery is already in progress");
501
+ if (!contended) throw error;
502
+ if (attempt < attempts) sleepSync(STATE_LEASE_RETRY_DELAY_MS);
503
+ }
504
+ }
505
+ return undefined;
506
+ }
507
+
508
+ function sleepSync(milliseconds: number): void {
509
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
510
+ }
511
+
512
+ /** The pre-spool body of the recorder: requires the session's state lease to be held. */
513
+ function processHookSignalLocked(
514
+ control: OpenHookControlStateV3,
515
+ input: RecordHookSignalV3Input,
516
+ sessionHash: `hid_${string}`,
517
+ path: string,
518
+ ): RecordHookSignalV3Result {
519
+ const rootId = control.genesis.event.scope.root_id as `root_${string}`;
520
+ const epochId = control.genesis.profile.privacy_key_epoch;
521
+ const boundaryEventId =
522
+ control.state === "candidate"
523
+ ? control.genesis.event.event_id
524
+ : control.activation.event.event_id;
525
+ const rootFingerprintContext = fingerprintContextV3(input.coordRoot, rootId, undefined, epochId);
526
+ const sessionId = `sid_${sessionHash.slice(4)}` as `sid_${string}`;
527
+ try {
528
+ let state = existsSync(path) ? readProducerState(path) : undefined;
529
+ if (
530
+ state &&
531
+ (state.adapter !== input.adapter ||
532
+ state.session_id !== sessionId ||
533
+ state.privacy_epoch_id !== epochId ||
534
+ state.capability_profile !== adapterCapabilityProfileDigestV3(input.adapter))
535
+ ) {
536
+ throw new Error("V3 producer state authority does not match the active boundary");
537
+ }
538
+ if (
539
+ state?.adapter === "cursor" &&
540
+ input.payload.cursor_mode &&
541
+ input.payload.cursor_mode !== "unknown"
542
+ ) {
543
+ if (!state.cursor_mode || state.cursor_mode === "unknown") {
544
+ state.cursor_mode = input.payload.cursor_mode;
545
+ } else if (state.cursor_mode !== input.payload.cursor_mode) {
546
+ throw new Error("Cursor execution mode changed within one V3 generation");
547
+ }
548
+ }
549
+ let recovered: RecordHookSignalV3Result | undefined;
550
+ if (state?.pending) {
551
+ const pendingSource = state.pending.source_id;
552
+ const incomingSource = sourceIdForSignal(input, rootFingerprintContext);
553
+ const pendingEvent = state.pending.event;
554
+ const durability = writeEventV3(input.coordRoot, pendingEvent);
555
+ applyCommittedEvent(state, pendingEvent);
556
+ state.pending = undefined;
557
+ publishProducerState(path, state);
558
+ if (pendingSource && incomingSource === pendingSource) {
559
+ recovered = { state: "recorded", event: pendingEvent, durability, recovered: true };
560
+ }
561
+ }
562
+ if (recovered) return recovered;
563
+
564
+ if (input.signal === "session-start") {
565
+ if (state && !state.terminal && state.started_event_id) {
566
+ return { state: "already_started", event_id: state.started_event_id };
567
+ }
568
+ if (input.session_start_derivation && !state?.terminal) {
569
+ return { state: "missing_session_start" };
570
+ }
571
+ state = newProducerState(input, sessionId, epochId, boundaryEventId as `evt_${string}`);
572
+ } else if (!state || state.terminal) {
573
+ // Mid-flight onboarding (ADR 0078): a live session with no producer
574
+ // state in this epoch (fresh epoch, lost session-start hook) opens a
575
+ // new generation with a derived session.started so its activity is
576
+ // recorded instead of refused. A TERMINAL state never re-onboards:
577
+ // resurrection after authoritative termination stays forbidden.
578
+ // Onboarding requires the payload's own native session identity: an
579
+ // env-attributed signal must not mint a generation under a hash derived
580
+ // from the observer instance (that would split the real session).
581
+ const nativeSessionIdentity =
582
+ input.payload.session_id ?? input.payload.conversation_id ?? input.payload.agent_id;
583
+ const onboardable =
584
+ !state &&
585
+ RECOVERY_ENABLED_ADAPTERS.has(input.adapter) &&
586
+ input.signal !== "session-end" &&
587
+ nativeSessionIdentity !== undefined;
588
+ if (!onboardable) {
589
+ writeProducerDiagnosticV3(input.coordRoot, "missing_session_start", {
590
+ adapter: input.adapter,
591
+ instance_id: input.instance_id,
592
+ signal: input.signal,
593
+ session_hash: sessionHash,
594
+ payload: input.payload,
595
+ });
596
+ return { state: "missing_session_start" };
597
+ }
598
+ state = newProducerState(input, sessionId, epochId, boundaryEventId as `evt_${string}`);
599
+ const cursorPromptBootstrap = isCursorPromptBootstrap(input);
600
+ const onboarding = buildMidFlightSessionStart(input, state, rootId, cursorPromptBootstrap);
601
+ commitEventLocked(input, state, path, onboarding);
602
+ if (!cursorPromptBootstrap) {
603
+ writeProducerDiagnosticV3(input.coordRoot, "mid_flight_onboarding", {
604
+ adapter: input.adapter,
605
+ instance_id: input.instance_id,
606
+ signal: input.signal,
607
+ session_hash: sessionHash,
608
+ });
609
+ }
610
+ }
611
+
612
+ const fingerprintContext = fingerprintContextV3(
613
+ input.coordRoot,
614
+ rootId,
615
+ state.generation_id,
616
+ state.privacy_epoch_id,
617
+ );
618
+ const recoveryEnabled = RECOVERY_ENABLED_ADAPTERS.has(input.adapter);
619
+ const nativeTid = input.payload.turn_id
620
+ ? (normalizeNativeIdV3(
621
+ fingerprintContext,
622
+ `${input.adapter}.turn`,
623
+ input.payload.turn_id,
624
+ ).replace(/^hid_/, "tid_") as `tid_${string}`)
625
+ : undefined;
626
+ const cursorPromptWithinOpenTurn =
627
+ input.adapter === "cursor" &&
628
+ input.signal === "user-prompt-submit" &&
629
+ state.current_turn_id !== undefined &&
630
+ state.current_turn_span !== undefined;
631
+ const startsDifferentNativeTurn =
632
+ recoveryEnabled &&
633
+ input.signal === "user-prompt-submit" &&
634
+ !cursorPromptWithinOpenTurn &&
635
+ nativeTid !== undefined &&
636
+ state.current_turn_id !== undefined &&
637
+ state.current_turn_id !== nativeTid &&
638
+ state.current_turn_span !== undefined;
639
+ if (startsDifferentNativeTurn) {
640
+ closeResolvedWaits(input, state, path, rootId, fingerprintContext, nativeTid);
641
+ const endingTid = state.current_turn_id as `tid_${string}`;
642
+ sweepOpenSpans(
643
+ input,
644
+ state,
645
+ path,
646
+ rootId,
647
+ (candidate) => candidate.turn_id === endingTid,
648
+ "completion_not_observed_before_next_turn",
649
+ );
650
+ commitRecoveredTurnCompleted(input, state, path, rootId, fingerprintContext);
651
+ }
652
+
653
+ const staleClosedTurnTool =
654
+ recoveryEnabled &&
655
+ isToolSignal(input.signal) &&
656
+ nativeTid !== undefined &&
657
+ state.closed_turn_ids.includes(nativeTid) &&
658
+ state.current_turn_id !== nativeTid;
659
+ if (staleClosedTurnTool && !state.current_turn_id) {
660
+ commitRecoveredToolTurnStart(input, state, path, rootId, fingerprintContext);
661
+ }
662
+ const eventInput = staleClosedTurnTool
663
+ ? { ...input, payload: withoutNativeTurnId(input.payload) }
664
+ : input;
665
+ const duplicateOpenTurnStart =
666
+ input.signal === "user-prompt-submit" &&
667
+ state.current_turn_span !== undefined &&
668
+ (cursorPromptWithinOpenTurn ||
669
+ (nativeTid !== undefined && state.current_turn_id === nativeTid));
670
+
671
+ recordTurnHarnessTiming(state, input, !duplicateOpenTurnStart);
672
+
673
+ closeResolvedWaits(input, state, path, rootId, fingerprintContext, nativeTid);
674
+ if (input.signal === "permission-request") {
675
+ const waitId = waitIdForInput(input, fingerprintContext);
676
+ if (waitId && state.waits.some((wait) => wait.wait_id === waitId)) {
677
+ return { state: "ignored" };
678
+ }
679
+ }
680
+
681
+ // Turn-boundary recovery (ADR 0078): a turn terminal is authoritative for
682
+ // the spans it owns. Derived terminals land BEFORE the native turn event.
683
+ if (recoveryEnabled && (input.signal === "stop" || input.signal === "stop-failure")) {
684
+ // The open producer turn is authoritative for a terminal. Cursor may
685
+ // report a different generation_id on Stop than beforeSubmitPrompt.
686
+ const endingTid = state.current_turn_id ?? nativeTid;
687
+ if (endingTid) {
688
+ sweepOpenSpans(
689
+ input,
690
+ state,
691
+ path,
692
+ rootId,
693
+ (candidate) => candidate.turn_id !== undefined && candidate.turn_id === endingTid,
694
+ "completion_not_observed_before_turn_end",
695
+ );
696
+ }
697
+ } else if (recoveryEnabled && input.signal === "user-prompt-submit") {
698
+ sweepOpenSpans(
699
+ input,
700
+ state,
701
+ path,
702
+ rootId,
703
+ (candidate) =>
704
+ candidate.turn_id !== undefined &&
705
+ (nativeTid === undefined || candidate.turn_id !== nativeTid),
706
+ "completion_not_observed_before_next_turn",
707
+ );
708
+ }
709
+
710
+ if (duplicateOpenTurnStart) {
711
+ writeProducerDiagnosticV3(input.coordRoot, "duplicate_turn_start_suppressed", {
712
+ adapter: input.adapter,
713
+ signal: input.signal,
714
+ reason: cursorPromptWithinOpenTurn
715
+ ? "cursor_prompt_while_turn_open"
716
+ : "native_turn_already_open",
717
+ instance_id: input.instance_id,
718
+ session_hash: sessionHash,
719
+ generation_id: state.generation_id,
720
+ turn_id: state.current_turn_id ?? nativeTid,
721
+ span_id: state.current_turn_span?.span_id,
722
+ payload: input.payload,
723
+ });
724
+ publishProducerState(path, state);
725
+ return { state: "ignored" };
726
+ }
727
+
728
+ // Every event derived before the native signal has now been committed.
729
+ // Sample once for the native event and every span it opens or closes so
730
+ // their self-contained timing cannot drift by a millisecond.
731
+ const eventClock = signalClock(input);
732
+ if (input.signal === "user-prompt-submit" && !state.current_turn_span) {
733
+ state.current_turn_span = openSpanStateV3({
734
+ span_id: spanIdV3(),
735
+ parent_span_id: state.session_span.span_id,
736
+ boot_id: state.boot_id,
737
+ clock: eventClock,
738
+ });
739
+ }
740
+
741
+ let sourceId = sourceIdForSignal(input, rootFingerprintContext);
742
+ const semanticKey = cursorShellSemanticKey(input, rootFingerprintContext);
743
+ let span: SpanStateV3 | undefined;
744
+ let delegation: DelegationStateV3 | undefined;
745
+ if (input.signal === "pre-tool-use") {
746
+ span = semanticKey
747
+ ? state.spans.find((candidate) => candidate.semantic_key === semanticKey)
748
+ : undefined;
749
+ if (span?.requested_event_id) return { state: "ignored" };
750
+ if (!sourceId && semanticKey) {
751
+ sourceId = cursorShellFallbackSourceId(semanticKey, state, rootFingerprintContext);
752
+ }
753
+ if (!sourceId) return unpairableTool(input, sessionHash, "missing_tool_use_id");
754
+ if (state.closed_spans.some((closed) => closed.source_id === sourceId)) {
755
+ // A late pre for a closed span must never open a fresh span: that is
756
+ // the pre/post-inversion orphan (ADR 0078).
757
+ return suppressClosedSpanSignal(input, sessionHash, "late_pre_suppressed");
758
+ }
759
+ if (recoveryEnabled && state.spans.length >= SPAN_SOFT_WATERMARK) {
760
+ const currentTid = nativeTid ?? state.current_turn_id;
761
+ sweepOpenSpans(
762
+ input,
763
+ state,
764
+ path,
765
+ rootId,
766
+ (candidate) => candidate.turn_id !== undefined && candidate.turn_id !== currentTid,
767
+ "span_cap_pressure",
768
+ );
769
+ }
770
+ span ??= state.spans.find((candidate) => candidate.source_id === sourceId);
771
+ if (span?.requested_event_id) return { state: "ignored" };
772
+ if (!span) {
773
+ const opened = openSpanStateV3({
774
+ span_id: spanIdV3(),
775
+ parent_span_id: state.current_turn_span?.span_id ?? state.session_span.span_id,
776
+ boot_id: state.boot_id,
777
+ clock: eventClock,
778
+ });
779
+ span = {
780
+ source_id: sourceId,
781
+ ...(semanticKey ? { semantic_key: semanticKey } : {}),
782
+ ...opened,
783
+ ...(input.payload.tool_name ? { tool_name: safeRole(input.payload.tool_name) } : {}),
784
+ };
785
+ state.spans.push(span);
786
+ }
787
+ } else if (input.signal === "post-tool-use" || input.signal === "post-tool-use-failure") {
788
+ span = sourceId
789
+ ? state.spans.find((candidate) => candidate.source_id === sourceId)
790
+ : undefined;
791
+ span ??= semanticKey
792
+ ? state.spans.find((candidate) => candidate.semantic_key === semanticKey)
793
+ : undefined;
794
+ if (span) sourceId = span.source_id;
795
+ if (!sourceId && semanticKey) {
796
+ sourceId = cursorShellFallbackSourceId(semanticKey, state, rootFingerprintContext);
797
+ }
798
+ if (!sourceId) return unpairableTool(input, sessionHash, "missing_tool_use_id");
799
+ if (!span) {
800
+ if (
801
+ semanticKey &&
802
+ state.closed_spans.some((closed) => closed.semantic_key === semanticKey)
803
+ ) {
804
+ return { state: "ignored" };
805
+ }
806
+ if (state.closed_spans.some((closed) => closed.source_id === sourceId)) {
807
+ return suppressClosedSpanSignal(input, sessionHash, "late_post_suppressed");
808
+ }
809
+ if (!recoveryEnabled) return unpairableTool(input, sessionHash, "no_open_span");
810
+ // Unmatched post (ADR 0078): record a derived request and pair the
811
+ // native completion on a fresh span instead of discarding the result.
812
+ const opened = openSpanStateV3({
813
+ span_id: spanIdV3(),
814
+ parent_span_id: state.current_turn_span?.span_id ?? state.session_span.span_id,
815
+ boot_id: state.boot_id,
816
+ clock: eventClock,
817
+ });
818
+ span = {
819
+ source_id: sourceId,
820
+ ...(semanticKey ? { semantic_key: semanticKey } : {}),
821
+ ...opened,
822
+ recovery_reason: "request_not_observed",
823
+ ...(input.payload.tool_name ? { tool_name: safeRole(input.payload.tool_name) } : {}),
824
+ };
825
+ const derivedRequest = normalizeHookEventV3("pre-tool-use", eventInput.payload, {
826
+ coordRoot: input.coordRoot,
827
+ adapter: input.adapter,
828
+ adapterVersion: input.adapterVersion,
829
+ harnessVersion: input.harnessVersion,
830
+ root_id: rootId,
831
+ run_id: input.run_id,
832
+ workflow_id: input.workflow_id,
833
+ workflow_agent_id: input.workflow_agent_id,
834
+ instance_id: input.instance_id,
835
+ generation_id: state.generation_id,
836
+ attestation_id: state.attestation_id,
837
+ producer_id: input.producer_id,
838
+ boot_id: state.boot_id,
839
+ sequence: state.next_sequence,
840
+ build_id: input.build_id,
841
+ platform: input.platform,
842
+ bridge: input.bridge,
843
+ capability_profile: state.capability_profile,
844
+ fingerprintContext,
845
+ turn_id: state.current_turn_id,
846
+ span_id: span.span_id,
847
+ caused_by: state.last_event_id ? [state.last_event_id] : [],
848
+ observed_at: eventClock.observed_at,
849
+ monotonic_ns: orderedEventMonotonic(state, input.monotonic_ns),
850
+ clock_id: state.clock_id,
851
+ });
852
+ if (!derivedRequest) return unpairableTool(input, sessionHash, "no_open_span");
853
+ derivedRequest.provenance = {
854
+ ...derivedRequest.provenance,
855
+ attestation: "derived",
856
+ confidence: input.payload.tool_input !== undefined ? "high" : "medium",
857
+ };
858
+ (derivedRequest.payload as { recovery?: { reason: string } }).recovery = {
859
+ reason: "request_not_observed",
860
+ };
861
+ state.spans.push(span);
862
+ commitEventLocked(input, state, path, derivedRequest);
863
+ span.requested_event_id = derivedRequest.event_id as `evt_${string}`;
864
+ span.open_event_id = derivedRequest.event_id as `evt_${string}`;
865
+ stampSpanTurn(span, derivedRequest, eventInput, state);
866
+ }
867
+ }
868
+ if (input.signal === "sub-agent-start") {
869
+ if (!sourceId) return { state: "ignored" };
870
+ delegation = state.delegations.find((candidate) => candidate.source_id === sourceId);
871
+ if (!delegation) {
872
+ const opened = openSpanStateV3({
873
+ span_id: spanIdV3(),
874
+ parent_span_id: state.current_turn_span?.span_id ?? state.session_span.span_id,
875
+ boot_id: state.boot_id,
876
+ clock: eventClock,
877
+ });
878
+ delegation = {
879
+ source_id: sourceId,
880
+ ...opened,
881
+ delegation_id: delegationIdV3(),
882
+ child_generation_id: generationIdV3(),
883
+ role: safeRole(input.payload.raw.agent_type),
884
+ };
885
+ state.delegations.push(delegation);
886
+ }
887
+ } else if (input.signal === "sub-agent-stop") {
888
+ if (!sourceId) return { state: "ignored" };
889
+ delegation = state.delegations.find((candidate) => candidate.source_id === sourceId);
890
+ if (!delegation) return { state: "ignored" };
891
+ }
892
+
893
+ const waitSpan =
894
+ input.signal === "permission-request"
895
+ ? openSpanStateV3({
896
+ span_id: spanIdV3(),
897
+ parent_span_id: state.current_turn_span?.span_id ?? state.session_span.span_id,
898
+ boot_id: state.boot_id,
899
+ clock: eventClock,
900
+ })
901
+ : undefined;
902
+ const openingSpan =
903
+ input.signal === "session-start"
904
+ ? state.session_span
905
+ : input.signal === "user-prompt-submit"
906
+ ? state.current_turn_span
907
+ : input.signal === "pre-tool-use"
908
+ ? span
909
+ : input.signal === "permission-request"
910
+ ? waitSpan
911
+ : input.signal === "sub-agent-start"
912
+ ? delegation
913
+ : undefined;
914
+ const closingSpan =
915
+ input.signal === "session-end"
916
+ ? state.session_span
917
+ : input.signal === "stop" || input.signal === "stop-failure"
918
+ ? state.current_turn_span
919
+ : input.signal === "post-tool-use" || input.signal === "post-tool-use-failure"
920
+ ? span
921
+ : input.signal === "sub-agent-stop"
922
+ ? delegation
923
+ : undefined;
924
+ const terminalSpan = closingSpan
925
+ ? closeSpanStateV3(closingSpan, {
926
+ boot_id: state.boot_id,
927
+ clock: eventClock,
928
+ ...(span && closingSpan === span && span.recovery_reason
929
+ ? { recovery_reason: span.recovery_reason }
930
+ : {}),
931
+ })
932
+ : undefined;
933
+
934
+ const turnTelemetry =
935
+ input.signal === "stop" || input.signal === "stop-failure"
936
+ ? extractTurnTelemetryV3(input.adapter, input.payload.raw, eventClock.observed_at)
937
+ : undefined;
938
+ const cursorToolChannelSupport =
939
+ input.adapter === "cursor"
940
+ ? cursorToolChannelSupportV3(state.cursor_mode ?? "unknown")
941
+ : undefined;
942
+ const cursorToolChannelUnattested =
943
+ (input.signal === "stop" || input.signal === "stop-failure") &&
944
+ input.adapter === "cursor" &&
945
+ cursorToolChannelSupport !== "unsupported" &&
946
+ state.tool_call_count === 0;
947
+ if (input.signal === "sub-agent-start" && delegation && !delegation.open_event_id) {
948
+ commitAgentDelegated(input, state, path, rootId, fingerprintContext, delegation);
949
+ }
950
+ const toolCallCountScopeMismatch =
951
+ (input.signal === "stop" || input.signal === "stop-failure") &&
952
+ (!state.current_turn_id || state.tool_call_count_turn_id !== state.current_turn_id);
953
+ const event = normalizeHookEventV3(input.signal, eventInput.payload, {
954
+ coordRoot: input.coordRoot,
955
+ adapter: input.adapter,
956
+ adapterVersion: input.adapterVersion,
957
+ harnessVersion: input.harnessVersion,
958
+ root_id: rootId,
959
+ run_id: input.run_id,
960
+ workflow_id: input.workflow_id,
961
+ workflow_agent_id: input.workflow_agent_id,
962
+ // Native subagent tool hooks can carry the parent's session id while
963
+ // resolving to a child process instance. The session-keyed producer
964
+ // state remains the generation authority; route those signals through
965
+ // its instance instead of rejecting or forging a child/parent scope.
966
+ instance_id: state.instance_id,
967
+ generation_id: state.generation_id,
968
+ attestation_id: state.attestation_id,
969
+ producer_id: input.producer_id,
970
+ boot_id: state.boot_id,
971
+ sequence: state.next_sequence,
972
+ build_id: input.build_id,
973
+ platform: input.platform,
974
+ bridge: input.bridge,
975
+ capability_profile: state.capability_profile,
976
+ fingerprintContext,
977
+ turn_id: state.current_turn_id,
978
+ span_id: openingSpan?.span_id ?? closingSpan?.span_id,
979
+ parent_span_id: openingSpan?.parent_span_id ?? closingSpan?.parent_span_id,
980
+ terminal_span: terminalSpan,
981
+ harness_timing: state.turn_harness,
982
+ turn_telemetry: turnTelemetry,
983
+ caused_by: [
984
+ ...(state.last_event_id ? [state.last_event_id] : []),
985
+ ...(terminalSpan?.open_event_id ? [terminalSpan.open_event_id] : []),
986
+ ].filter((value, index, values) => values.indexOf(value) === index) as `evt_${string}`[],
987
+ observed_at: eventClock.observed_at,
988
+ monotonic_ns: orderedEventMonotonic(state, input.monotonic_ns),
989
+ clock_id: state.clock_id,
990
+ duration_ms: durationMilliseconds(span?.opened_monotonic_ns, input.monotonic_ns),
991
+ // Cursor's terminal payload has no native tool aggregate. With no
992
+ // delivered tool hook, the recorder has no evidence that zero calls
993
+ // occurred; emitting an exact zero would turn hook loss into false data.
994
+ tool_call_count:
995
+ cursorToolChannelSupport === "unsupported" ||
996
+ cursorToolChannelUnattested ||
997
+ toolCallCountScopeMismatch
998
+ ? undefined
999
+ : state.tool_call_count,
1000
+ tool_call_count_support: cursorToolChannelSupport,
1001
+ tool_call_count_missing_reason: cursorToolChannelUnattested
1002
+ ? "tool_channel_unattested"
1003
+ : toolCallCountScopeMismatch
1004
+ ? "tool_count_turn_scope_unattested"
1005
+ : undefined,
1006
+ delegation_id: delegation?.delegation_id,
1007
+ child_generation_id: delegation?.child_generation_id,
1008
+ agent_role: delegation?.role,
1009
+ stop_remediation: input.stop_remediation,
1010
+ turn_ritual: input.turn_ritual,
1011
+ });
1012
+ if (!event) return { state: "ignored" };
1013
+ if (
1014
+ input.signal === "session-start" &&
1015
+ input.session_start_derivation === "approved_lifecycle_reopen" &&
1016
+ event.event_type === "session.started"
1017
+ ) {
1018
+ event.provenance = {
1019
+ ...event.provenance,
1020
+ source_event: `${input.adapter}.approved-lifecycle-reopen`,
1021
+ attestation: "derived",
1022
+ confidence: "high",
1023
+ attribution: {
1024
+ method: "session_env",
1025
+ state: "verified",
1026
+ subject_instance_id: state.instance_id,
1027
+ },
1028
+ };
1029
+ event.payload.resume = {
1030
+ state: "unknown",
1031
+ reason: "approved_lifecycle_reopen",
1032
+ };
1033
+ assertEventV3(event);
1034
+ }
1035
+ if (input.signal === "pre-tool-use" && span && !span.requested_event_id) {
1036
+ span.requested_event_id = event.event_id as `evt_${string}`;
1037
+ span.open_event_id = event.event_id as `evt_${string}`;
1038
+ stampSpanTurn(span, event, eventInput, state);
1039
+ }
1040
+ const durability = commitEventLocked(input, state, path, event, sourceId);
1041
+ if (event.event_type === "tool.requested" && sourceId) {
1042
+ const explicitWait = explicitNativeWait(input);
1043
+ if (explicitWait) {
1044
+ commitNativeWait(input, state, path, rootId, sourceId, explicitWait);
1045
+ } else if (input.payload.tool_name === "request_user_input") {
1046
+ commitNativeWait(input, state, path, rootId, sourceId, { kind: "needs_input" });
1047
+ }
1048
+ }
1049
+ const progressKind = progressKindForCompletedTool(event, input);
1050
+ if (progressKind) {
1051
+ commitProgressObservation(input, state, path, rootId, event, progressKind);
1052
+ }
1053
+ if (turnTelemetry) {
1054
+ commitTurnContextObservation(input, state, path, rootId, event, turnTelemetry.context);
1055
+ }
1056
+ if (event.event_type === "session.ended") {
1057
+ commitCapabilityDriftEvents(input, state, path, rootId);
1058
+ }
1059
+ return { state: "recorded", event, durability, recovered: false };
1060
+ } finally {
1061
+ // The session's state lease is held by the draining caller.
1062
+ }
1063
+ }
1064
+
1065
+ function commitAgentDelegated(
1066
+ input: RecordHookSignalV3Input,
1067
+ state: HookProducerStateV3,
1068
+ path: string,
1069
+ rootId: `root_${string}`,
1070
+ fingerprintContext: ReturnType<typeof fingerprintContextV3>,
1071
+ delegation: DelegationStateV3,
1072
+ ): void {
1073
+ const event = buildEventV3("agent.delegated", {
1074
+ producer: producerForDerivedEvent(input, state),
1075
+ scope: scopeForDerivedEvent(input, state, rootId),
1076
+ attestation_id: state.attestation_id,
1077
+ links: {
1078
+ span_id: delegation.span_id,
1079
+ ...(delegation.parent_span_id ? { parent_span_id: delegation.parent_span_id } : {}),
1080
+ caused_by: state.last_event_id ? [state.last_event_id] : [],
1081
+ },
1082
+ provenance: derivedProvenance(input, state, "subagent-delegated"),
1083
+ observed_at: signalClock(input).observed_at,
1084
+ monotonic_ns: orderedEventMonotonic(state, input.monotonic_ns),
1085
+ clock_id: state.clock_id,
1086
+ payload: {
1087
+ delegation_id: delegation.delegation_id,
1088
+ child_generation_id: delegation.child_generation_id,
1089
+ role: delegation.role,
1090
+ ownership_fingerprint: fingerprintV3(fingerprintContext, "delegation-ownership", {
1091
+ role: delegation.role,
1092
+ source_id: delegation.source_id,
1093
+ }),
1094
+ },
1095
+ }) as EventV3;
1096
+ commitEventLocked(input, state, path, event);
1097
+ }
1098
+
1099
+ type NativeWaitKind =
1100
+ | "permission"
1101
+ | "needs_input"
1102
+ | "decision"
1103
+ | "approval"
1104
+ | "scheduled"
1105
+ | "rate_limit"
1106
+ | "unknown";
1107
+
1108
+ interface NativeWaitObservation {
1109
+ kind: NativeWaitKind;
1110
+ wake_at?: string;
1111
+ authority_reference?: string;
1112
+ }
1113
+
1114
+ function commitNativeWait(
1115
+ input: RecordHookSignalV3Input,
1116
+ state: HookProducerStateV3,
1117
+ path: string,
1118
+ rootId: `root_${string}`,
1119
+ waitId: `hid_${string}`,
1120
+ observation: NativeWaitObservation,
1121
+ ): void {
1122
+ if (!state.current_turn_id || state.waits.some((wait) => wait.wait_id === waitId)) return;
1123
+ const span = openSpanStateV3({
1124
+ span_id: spanIdV3(),
1125
+ parent_span_id: state.current_turn_span?.span_id ?? state.session_span.span_id,
1126
+ boot_id: state.boot_id,
1127
+ clock: signalClock(input),
1128
+ });
1129
+ const event = buildEventV3("wait.started", {
1130
+ producer: producerForDerivedEvent(input, state),
1131
+ scope: scopeForDerivedEvent(input, state, rootId),
1132
+ attestation_id: state.attestation_id,
1133
+ links: {
1134
+ span_id: span.span_id,
1135
+ ...(span.parent_span_id ? { parent_span_id: span.parent_span_id } : {}),
1136
+ caused_by: state.last_event_id ? [state.last_event_id] : [],
1137
+ },
1138
+ provenance: derivedProvenance(input, state, "native-typed-wait"),
1139
+ observed_at: signalClock(input).observed_at,
1140
+ monotonic_ns: orderedEventMonotonic(state, input.monotonic_ns),
1141
+ clock_id: state.clock_id,
1142
+ payload: {
1143
+ wait_id: waitId,
1144
+ kind: observation.kind,
1145
+ ...(observation.wake_at ? { wake_at: observation.wake_at } : {}),
1146
+ ...(observation.authority_reference
1147
+ ? { authority_reference: observation.authority_reference }
1148
+ : {}),
1149
+ },
1150
+ }) as EventV3;
1151
+ commitEventLocked(input, state, path, event);
1152
+ }
1153
+
1154
+ type SemanticProgressKind =
1155
+ | "write"
1156
+ | "test"
1157
+ | "commit"
1158
+ | "deploy"
1159
+ | "publication"
1160
+ | "review"
1161
+ | "artifact";
1162
+
1163
+ function progressKindForCompletedTool(
1164
+ event: EventV3,
1165
+ input: RecordHookSignalV3Input,
1166
+ ): SemanticProgressKind | undefined {
1167
+ if (event.event_type !== "tool.completed" || event.payload.outcome !== "succeeded") return;
1168
+ const explicit = explicitNativeProgress(input);
1169
+ if (explicit) return explicit;
1170
+ const name = event.payload.tool.name;
1171
+ if (["apply_patch", "Edit", "Write", "MultiEdit", "NotebookEdit"].includes(name)) return "write";
1172
+ if (name === "view_image") return "review";
1173
+ const leaf =
1174
+ name
1175
+ .split(/__|[/:]/)
1176
+ .at(-1)
1177
+ ?.toLowerCase() ?? name.toLowerCase();
1178
+ if (["run_tests", "test", "vitest", "jest", "playwright_test"].includes(leaf)) return "test";
1179
+ if (["git_commit", "commit"].includes(leaf)) return "commit";
1180
+ if (["deploy", "deploy_app", "deploy_service"].includes(leaf)) return "deploy";
1181
+ if (["publish", "wiki_publish", "publish_page"].includes(leaf)) return "publication";
1182
+ if (["build", "compile", "bundle"].includes(leaf)) return "artifact";
1183
+ if (/imagegen|create_document|create_spreadsheet|create_presentation/i.test(name)) {
1184
+ return "artifact";
1185
+ }
1186
+ return undefined;
1187
+ }
1188
+
1189
+ function commitProgressObservation(
1190
+ input: RecordHookSignalV3Input,
1191
+ state: HookProducerStateV3,
1192
+ path: string,
1193
+ rootId: `root_${string}`,
1194
+ terminal: EventV3,
1195
+ kind: SemanticProgressKind,
1196
+ ): void {
1197
+ const event = buildEventV3("progress.observed", {
1198
+ producer: producerForDerivedEvent(input, state),
1199
+ scope: scopeForDerivedEvent(input, state, rootId),
1200
+ attestation_id: state.attestation_id,
1201
+ links: { caused_by: [terminal.event_id] },
1202
+ provenance: derivedProvenance(input, state, "semantic-progress"),
1203
+ observed_at: terminal.time.observed_at,
1204
+ monotonic_ns: orderedEventMonotonic(state, input.monotonic_ns),
1205
+ clock_id: state.clock_id,
1206
+ payload: {
1207
+ kind,
1208
+ evidence_event_ids: [terminal.event_id],
1209
+ reducer_build_id: input.build_id,
1210
+ },
1211
+ }) as EventV3;
1212
+ commitEventLocked(input, state, path, event);
1213
+ }
1214
+
1215
+ function explicitNativeProgress(input: RecordHookSignalV3Input): SemanticProgressKind | undefined {
1216
+ const value = input.payload.raw.harnery_progress_kind ?? input.payload.raw.progress_kind;
1217
+ return value === "write" ||
1218
+ value === "test" ||
1219
+ value === "commit" ||
1220
+ value === "deploy" ||
1221
+ value === "publication" ||
1222
+ value === "review" ||
1223
+ value === "artifact"
1224
+ ? value
1225
+ : undefined;
1226
+ }
1227
+
1228
+ function explicitNativeWait(input: RecordHookSignalV3Input): NativeWaitObservation | undefined {
1229
+ if (input.signal !== "pre-tool-use") return undefined;
1230
+ const raw = input.payload.raw;
1231
+ const value = raw.harnery_wait_kind ?? raw.wait_kind;
1232
+ const kind =
1233
+ value === "permission" ||
1234
+ value === "needs_input" ||
1235
+ value === "decision" ||
1236
+ value === "approval" ||
1237
+ value === "scheduled" ||
1238
+ value === "rate_limit" ||
1239
+ value === "unknown"
1240
+ ? value
1241
+ : undefined;
1242
+ if (!kind) return undefined;
1243
+ const wakeCandidate = raw.harnery_wake_at ?? raw.wake_at;
1244
+ const wake_at =
1245
+ typeof wakeCandidate === "string" &&
1246
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(wakeCandidate) &&
1247
+ Number.isFinite(Date.parse(wakeCandidate))
1248
+ ? wakeCandidate
1249
+ : undefined;
1250
+ const authorityCandidate = raw.harnery_authority_reference ?? raw.authority_reference;
1251
+ const authority_reference = safeTokenOrUndefined(authorityCandidate);
1252
+ return {
1253
+ kind,
1254
+ ...(wake_at ? { wake_at } : {}),
1255
+ ...(authority_reference ? { authority_reference } : {}),
1256
+ };
1257
+ }
1258
+
1259
+ function producerForDerivedEvent(input: RecordHookSignalV3Input, state: HookProducerStateV3) {
1260
+ return {
1261
+ producer_id: input.producer_id,
1262
+ boot_id: state.boot_id,
1263
+ sequence: state.next_sequence,
1264
+ component: "agent-hook" as const,
1265
+ build_id: input.build_id,
1266
+ platform: input.platform,
1267
+ ...(input.bridge ? { bridge: input.bridge } : {}),
1268
+ };
1269
+ }
1270
+
1271
+ function scopeForDerivedEvent(
1272
+ input: RecordHookSignalV3Input,
1273
+ state: HookProducerStateV3,
1274
+ rootId: `root_${string}`,
1275
+ ) {
1276
+ return {
1277
+ root_id: rootId,
1278
+ instance_id: state.instance_id,
1279
+ session_id: state.session_id,
1280
+ generation_id: state.generation_id,
1281
+ ...(state.current_turn_id ? { turn_id: state.current_turn_id } : {}),
1282
+ ...(input.run_id ? { run_id: input.run_id } : {}),
1283
+ ...(input.workflow_id ? { workflow_id: input.workflow_id } : {}),
1284
+ ...(input.workflow_agent_id ? { workflow_agent_id: input.workflow_agent_id } : {}),
1285
+ };
1286
+ }
1287
+
1288
+ function derivedProvenance(
1289
+ input: RecordHookSignalV3Input,
1290
+ state: HookProducerStateV3,
1291
+ signal: string,
1292
+ ) {
1293
+ return {
1294
+ source_event: `${input.adapter}.${signal}`,
1295
+ attestation: "derived" as const,
1296
+ confidence: "high" as const,
1297
+ attribution: {
1298
+ method: "session_env" as const,
1299
+ state: "verified" as const,
1300
+ subject_instance_id: state.instance_id,
1301
+ },
1302
+ };
1303
+ }
1304
+
1305
+ /**
1306
+ * Stamp the span with the turn its request event was attributed to. No stamp
1307
+ * is written without a real turn context: a native payload turn id or an open
1308
+ * producer-state turn. Unstamped spans are excluded from every boundary and
1309
+ * cap sweep (fail closed; explicit-end salvage may still reach them).
1310
+ */
1311
+ function stampSpanTurn(
1312
+ span: SpanStateV3,
1313
+ requestEvent: EventV3,
1314
+ input: RecordHookSignalV3Input,
1315
+ state: HookProducerStateV3,
1316
+ ): void {
1317
+ if (input.payload.turn_id) {
1318
+ span.turn_id = (requestEvent.scope as { turn_id: `tid_${string}` }).turn_id;
1319
+ span.turn_stamp = "native_payload";
1320
+ } else if (state.current_turn_id) {
1321
+ span.turn_id = state.current_turn_id;
1322
+ span.turn_stamp = "producer_state";
1323
+ }
1324
+ }
1325
+
1326
+ function isToolSignal(signal: HookSignalV3): boolean {
1327
+ return (
1328
+ signal === "pre-tool-use" || signal === "post-tool-use" || signal === "post-tool-use-failure"
1329
+ );
1330
+ }
1331
+
1332
+ function withoutNativeTurnId(payload: ParsedPayload): ParsedPayload {
1333
+ const { turn_id: _turnId, ...rest } = payload;
1334
+ return rest;
1335
+ }
1336
+
1337
+ function recoveryBoundaryPayload(payload: ParsedPayload): ParsedPayload {
1338
+ return {
1339
+ raw: {},
1340
+ ...(payload.session_id ? { session_id: payload.session_id } : {}),
1341
+ ...(payload.conversation_id ? { conversation_id: payload.conversation_id } : {}),
1342
+ ...(payload.agent_id ? { agent_id: payload.agent_id } : {}),
1343
+ };
1344
+ }
1345
+
1346
+ /** Close an open turn when a different native prompt proves the next boundary. */
1347
+ function commitRecoveredTurnCompleted(
1348
+ input: RecordHookSignalV3Input,
1349
+ state: HookProducerStateV3,
1350
+ path: string,
1351
+ rootId: `root_${string}`,
1352
+ fingerprintContext: ReturnType<typeof fingerprintContextV3>,
1353
+ ): void {
1354
+ const turnId = state.current_turn_id;
1355
+ const span = state.current_turn_span;
1356
+ if (!turnId || !span) return;
1357
+ const clock = signalClock(input);
1358
+ const terminalSpan = closeSpanStateV3(span, {
1359
+ boot_id: state.boot_id,
1360
+ clock,
1361
+ recovery_reason: "completion_not_observed_before_next_turn",
1362
+ });
1363
+ const toolCountAttested = state.tool_call_count_turn_id === turnId;
1364
+ const event = normalizeHookEventV3("stop", recoveryBoundaryPayload(input.payload), {
1365
+ coordRoot: input.coordRoot,
1366
+ adapter: input.adapter,
1367
+ adapterVersion: input.adapterVersion,
1368
+ harnessVersion: input.harnessVersion,
1369
+ root_id: rootId,
1370
+ run_id: input.run_id,
1371
+ workflow_id: input.workflow_id,
1372
+ workflow_agent_id: input.workflow_agent_id,
1373
+ instance_id: state.instance_id,
1374
+ generation_id: state.generation_id,
1375
+ attestation_id: state.attestation_id,
1376
+ producer_id: input.producer_id,
1377
+ boot_id: state.boot_id,
1378
+ sequence: state.next_sequence,
1379
+ build_id: input.build_id,
1380
+ platform: input.platform,
1381
+ bridge: input.bridge,
1382
+ capability_profile: state.capability_profile,
1383
+ fingerprintContext,
1384
+ turn_id: turnId,
1385
+ span_id: span.span_id,
1386
+ terminal_span: terminalSpan,
1387
+ harness_timing: state.turn_harness,
1388
+ caused_by: [
1389
+ ...(state.last_event_id ? [state.last_event_id] : []),
1390
+ ...(terminalSpan.open_event_id ? [terminalSpan.open_event_id] : []),
1391
+ ].filter((value, index, values) => values.indexOf(value) === index) as `evt_${string}`[],
1392
+ observed_at: clock.observed_at,
1393
+ monotonic_ns: orderedEventMonotonic(state, input.monotonic_ns),
1394
+ clock_id: state.clock_id,
1395
+ tool_call_count: toolCountAttested ? state.tool_call_count : undefined,
1396
+ tool_call_count_missing_reason: toolCountAttested
1397
+ ? undefined
1398
+ : "tool_count_turn_scope_unattested",
1399
+ });
1400
+ if (event?.event_type !== "turn.completed") {
1401
+ throw new Error("recovered turn terminal could not be normalized");
1402
+ }
1403
+ event.provenance = {
1404
+ ...event.provenance,
1405
+ source_event: `${input.adapter}.recovery`,
1406
+ attestation: "derived",
1407
+ confidence: "medium",
1408
+ attribution: {
1409
+ method: "native_payload",
1410
+ state: "verified",
1411
+ subject_instance_id: state.instance_id,
1412
+ },
1413
+ };
1414
+ event.payload.outcome = "unknown";
1415
+ assertEventV3(event);
1416
+ commitEventLocked(input, state, path, event);
1417
+ }
1418
+
1419
+ /** Open a derived turn for tools that arrive after their native turn closed. */
1420
+ function commitRecoveredToolTurnStart(
1421
+ input: RecordHookSignalV3Input,
1422
+ state: HookProducerStateV3,
1423
+ path: string,
1424
+ rootId: `root_${string}`,
1425
+ fingerprintContext: ReturnType<typeof fingerprintContextV3>,
1426
+ ): void {
1427
+ const clock = signalClock(input);
1428
+ const span = openSpanStateV3({
1429
+ span_id: spanIdV3(),
1430
+ parent_span_id: state.session_span.span_id,
1431
+ boot_id: state.boot_id,
1432
+ clock,
1433
+ });
1434
+ state.current_turn_span = span;
1435
+ const event = normalizeHookEventV3("user-prompt-submit", recoveryBoundaryPayload(input.payload), {
1436
+ coordRoot: input.coordRoot,
1437
+ adapter: input.adapter,
1438
+ adapterVersion: input.adapterVersion,
1439
+ harnessVersion: input.harnessVersion,
1440
+ root_id: rootId,
1441
+ run_id: input.run_id,
1442
+ workflow_id: input.workflow_id,
1443
+ workflow_agent_id: input.workflow_agent_id,
1444
+ instance_id: state.instance_id,
1445
+ generation_id: state.generation_id,
1446
+ attestation_id: state.attestation_id,
1447
+ producer_id: input.producer_id,
1448
+ boot_id: state.boot_id,
1449
+ sequence: state.next_sequence,
1450
+ build_id: input.build_id,
1451
+ platform: input.platform,
1452
+ bridge: input.bridge,
1453
+ capability_profile: state.capability_profile,
1454
+ fingerprintContext,
1455
+ turn_native_id: `post-terminal:${state.generation_id}:${state.turn_ordinal + 1}`,
1456
+ span_id: span.span_id,
1457
+ caused_by: state.last_event_id ? [state.last_event_id] : [],
1458
+ observed_at: clock.observed_at,
1459
+ monotonic_ns: orderedEventMonotonic(state, input.monotonic_ns),
1460
+ clock_id: state.clock_id,
1461
+ });
1462
+ if (event?.event_type !== "turn.started") {
1463
+ throw new Error("recovered tool turn start could not be normalized");
1464
+ }
1465
+ event.provenance = {
1466
+ ...event.provenance,
1467
+ source_event: `${input.adapter}.recovery`,
1468
+ attestation: "derived",
1469
+ confidence: "medium",
1470
+ attribution: {
1471
+ method: "session_env",
1472
+ state: "verified",
1473
+ subject_instance_id: state.instance_id,
1474
+ },
1475
+ };
1476
+ assertEventV3(event);
1477
+ commitEventLocked(input, state, path, event);
1478
+ }
1479
+
1480
+ /** The single pending-publish/write/apply/publish cycle every locked event commit uses. */
1481
+ function commitEventLocked(
1482
+ input: RecordHookSignalV3Input,
1483
+ state: HookProducerStateV3,
1484
+ path: string,
1485
+ event: EventV3,
1486
+ sourceId?: `hid_${string}`,
1487
+ ): WriteEventV3Result {
1488
+ markObservedClockRegressionV3(event, state.last_observed_at);
1489
+ assertEventV3(event);
1490
+ state.pending = { ...(sourceId ? { source_id: sourceId } : {}), event };
1491
+ publishProducerState(path, state);
1492
+ const durability = writeEventV3(input.coordRoot, event, input.writerOptions);
1493
+ applyCommittedEvent(state, event);
1494
+ state.pending = undefined;
1495
+ publishProducerState(path, state);
1496
+ return durability;
1497
+ }
1498
+
1499
+ function commitTurnContextObservation(
1500
+ input: RecordHookSignalV3Input,
1501
+ state: HookProducerStateV3,
1502
+ path: string,
1503
+ rootId: `root_${string}`,
1504
+ turnTerminal: EventV3,
1505
+ measurement: TelemetryObservationV3<ContextMeasurementV3>,
1506
+ ): void {
1507
+ if (turnTerminal.event_type !== "turn.completed") return;
1508
+ const event = buildEventV3("context.observed", {
1509
+ producer: {
1510
+ producer_id: input.producer_id,
1511
+ boot_id: state.boot_id,
1512
+ sequence: state.next_sequence,
1513
+ component: "agent-hook",
1514
+ build_id: input.build_id,
1515
+ platform: input.platform,
1516
+ ...(input.bridge ? { bridge: input.bridge } : {}),
1517
+ },
1518
+ scope: {
1519
+ root_id: rootId,
1520
+ instance_id: state.instance_id,
1521
+ session_id: state.session_id,
1522
+ generation_id: state.generation_id,
1523
+ turn_id: (turnTerminal.scope as { turn_id: `tid_${string}` }).turn_id,
1524
+ ...(input.run_id ? { run_id: input.run_id } : {}),
1525
+ ...(input.workflow_id ? { workflow_id: input.workflow_id } : {}),
1526
+ ...(input.workflow_agent_id ? { workflow_agent_id: input.workflow_agent_id } : {}),
1527
+ },
1528
+ attestation_id: state.attestation_id,
1529
+ links: { caused_by: [turnTerminal.event_id] },
1530
+ provenance: {
1531
+ source_event: `${input.adapter}.turn-context`,
1532
+ attestation: "native",
1533
+ confidence: "exact",
1534
+ attribution: {
1535
+ method: "native_payload",
1536
+ state: "verified",
1537
+ subject_instance_id: state.instance_id,
1538
+ },
1539
+ },
1540
+ observed_at:
1541
+ measurement.state === "observed"
1542
+ ? measurement.value.measured_at
1543
+ : turnTerminal.time.observed_at,
1544
+ monotonic_ns: orderedEventMonotonic(state, input.monotonic_ns),
1545
+ clock_id: state.clock_id,
1546
+ payload: {
1547
+ measurement,
1548
+ },
1549
+ }) as EventV3;
1550
+ commitEventLocked(input, state, path, event);
1551
+ }
1552
+
1553
+ function commitCapabilityDriftEvents(
1554
+ input: RecordHookSignalV3Input,
1555
+ state: HookProducerStateV3,
1556
+ path: string,
1557
+ rootId: `root_${string}`,
1558
+ ): void {
1559
+ const read = readLedgerV3(input.coordRoot);
1560
+ if (!read.complete) return;
1561
+ const generationEvents = read.events
1562
+ .map(({ event }) => event)
1563
+ .filter(
1564
+ (event) =>
1565
+ "generation_id" in event.scope && event.scope.generation_id === state.generation_id,
1566
+ );
1567
+ const emittedSignals = new Set(
1568
+ generationEvents
1569
+ .filter((event) => event.event_type === "health.capability_drift")
1570
+ .map((event) => (event.payload as { signal: string }).signal),
1571
+ );
1572
+ for (const payload of capabilityDriftPayloadsV3(input.adapter, generationEvents)) {
1573
+ if (emittedSignals.has(payload.signal)) continue;
1574
+ const event = buildEventV3("health.capability_drift", {
1575
+ producer: {
1576
+ producer_id: input.producer_id,
1577
+ boot_id: state.boot_id,
1578
+ sequence: state.next_sequence,
1579
+ component: "agent-hook",
1580
+ build_id: input.build_id,
1581
+ platform: input.platform,
1582
+ ...(input.bridge ? { bridge: input.bridge } : {}),
1583
+ },
1584
+ scope: {
1585
+ root_id: rootId,
1586
+ instance_id: state.instance_id,
1587
+ session_id: state.session_id,
1588
+ generation_id: state.generation_id,
1589
+ },
1590
+ attestation_id: state.attestation_id,
1591
+ links: { caused_by: state.last_event_id ? [state.last_event_id] : [] },
1592
+ provenance: {
1593
+ source_event: `${input.adapter}.capability-coherence`,
1594
+ attestation: "derived",
1595
+ confidence: "exact",
1596
+ attribution: {
1597
+ method: "session_env",
1598
+ state: "verified",
1599
+ subject_instance_id: state.instance_id,
1600
+ },
1601
+ },
1602
+ observed_at: signalClock(input).observed_at,
1603
+ monotonic_ns: orderedEventMonotonic(state, input.monotonic_ns),
1604
+ clock_id: state.clock_id,
1605
+ payload,
1606
+ }) as EventV3;
1607
+ commitEventLocked(input, state, path, event);
1608
+ }
1609
+ }
1610
+
1611
+ /**
1612
+ * Terminalize every open span matching the predicate with a derived
1613
+ * `tool.completed` (ADR 0078). One event per span, committed sequentially
1614
+ * through the pending cycle so a crash replays exactly one.
1615
+ */
1616
+ function sweepOpenSpans(
1617
+ input: RecordHookSignalV3Input,
1618
+ state: HookProducerStateV3,
1619
+ path: string,
1620
+ rootId: `root_${string}`,
1621
+ matches: (span: SpanStateV3) => boolean,
1622
+ reason:
1623
+ | "completion_not_observed_before_turn_end"
1624
+ | "completion_not_observed_before_next_turn"
1625
+ | "span_cap_pressure",
1626
+ ): void {
1627
+ for (const span of [...state.spans]) {
1628
+ if (!matches(span)) continue;
1629
+ const event = buildDerivedToolCompleted(input, state, rootId, span, reason);
1630
+ commitEventLocked(input, state, path, event);
1631
+ }
1632
+ }
1633
+
1634
+ function buildDerivedToolCompleted(
1635
+ input: RecordHookSignalV3Input,
1636
+ state: HookProducerStateV3,
1637
+ rootId: `root_${string}`,
1638
+ span: SpanStateV3,
1639
+ reason:
1640
+ | "completion_not_observed_before_turn_end"
1641
+ | "completion_not_observed_before_next_turn"
1642
+ | "span_cap_pressure"
1643
+ | "explicit_end_salvage",
1644
+ producerOverride?: { producer_id: `prd_${string}`; boot_id: `boot_${string}`; sequence: number },
1645
+ observedAt?: string,
1646
+ ): EventV3 {
1647
+ const clock = signalClock(input);
1648
+ if (observedAt !== undefined) clock.observed_at = observedAt;
1649
+ const baseConfidence =
1650
+ reason === "completion_not_observed_before_turn_end" || reason === "explicit_end_salvage"
1651
+ ? "medium"
1652
+ : "low";
1653
+ const confidence =
1654
+ span.turn_stamp === "producer_state" && baseConfidence === "medium" ? "low" : baseConfidence;
1655
+ const terminalSpan = closeSpanStateV3(span, {
1656
+ boot_id: state.boot_id,
1657
+ clock,
1658
+ recovery_reason: reason,
1659
+ });
1660
+ const elapsedUpperBound = recoverSpanUpperBoundV3(span, {
1661
+ boot_id: state.boot_id,
1662
+ clock,
1663
+ });
1664
+ const event = buildEventV3("tool.completed", {
1665
+ producer: {
1666
+ producer_id: producerOverride?.producer_id ?? input.producer_id,
1667
+ boot_id: producerOverride?.boot_id ?? state.boot_id,
1668
+ sequence: producerOverride?.sequence ?? state.next_sequence,
1669
+ component: "agent-hook",
1670
+ build_id: input.build_id,
1671
+ platform: input.platform,
1672
+ ...(input.bridge ? { bridge: input.bridge } : {}),
1673
+ },
1674
+ scope: {
1675
+ root_id: rootId,
1676
+ instance_id: state.instance_id,
1677
+ session_id: state.session_id,
1678
+ generation_id: state.generation_id,
1679
+ turn_id: span.turn_id as `tid_${string}`,
1680
+ },
1681
+ attestation_id: state.attestation_id,
1682
+ links: {
1683
+ caused_by: [
1684
+ ...(state.last_event_id ? [state.last_event_id] : []),
1685
+ ...(span.open_event_id ? [span.open_event_id] : []),
1686
+ ].filter((value, index, values) => values.indexOf(value) === index),
1687
+ span_id: span.span_id,
1688
+ },
1689
+ provenance: {
1690
+ source_event: `${input.adapter}.recovery`,
1691
+ attestation: "derived",
1692
+ confidence,
1693
+ attribution: {
1694
+ method: span.turn_stamp === "native_payload" ? "native_payload" : "session_env",
1695
+ state: "verified",
1696
+ subject_instance_id: state.instance_id,
1697
+ },
1698
+ },
1699
+ observed_at: clock.observed_at,
1700
+ monotonic_ns: orderedEventMonotonic(state, input.monotonic_ns),
1701
+ clock_id: state.clock_id,
1702
+ payload: {
1703
+ tool: { namespace: input.adapter, name: span.tool_name ?? "unknown_tool" },
1704
+ outcome: "unknown",
1705
+ duration_ms: structuredClone(terminalSpan.duration_ms),
1706
+ span: terminalSpan,
1707
+ result: { storage: "omitted", media_type: "application/octet-stream", bytes: 0 },
1708
+ recovery: {
1709
+ reason,
1710
+ ...(span.requested_event_id ? { requested_event_id: span.requested_event_id } : {}),
1711
+ elapsed_upper_bound_ms: elapsedUpperBound,
1712
+ },
1713
+ },
1714
+ }) as EventV3;
1715
+ assertEventV3(event);
1716
+ return event;
1717
+ }
1718
+
1719
+ /**
1720
+ * A derived `session.started` for a live session Harnery first observed
1721
+ * mid-flight (fresh epoch or lost session-start hook). Records that the
1722
+ * session exists without claiming the adapter delivered a start signal.
1723
+ */
1724
+ function buildMidFlightSessionStart(
1725
+ input: RecordHookSignalV3Input,
1726
+ state: HookProducerStateV3,
1727
+ rootId: `root_${string}`,
1728
+ cursorPromptBootstrap = false,
1729
+ ): EventV3 {
1730
+ const fingerprintContext = fingerprintContextV3(
1731
+ input.coordRoot,
1732
+ rootId,
1733
+ state.generation_id,
1734
+ state.privacy_epoch_id,
1735
+ );
1736
+ const event = normalizeHookEventV3("session-start", input.payload, {
1737
+ coordRoot: input.coordRoot,
1738
+ adapter: input.adapter,
1739
+ adapterVersion: input.adapterVersion,
1740
+ harnessVersion: input.harnessVersion,
1741
+ root_id: rootId,
1742
+ run_id: input.run_id,
1743
+ workflow_id: input.workflow_id,
1744
+ workflow_agent_id: input.workflow_agent_id,
1745
+ instance_id: input.instance_id,
1746
+ generation_id: state.generation_id,
1747
+ attestation_id: state.attestation_id,
1748
+ producer_id: input.producer_id,
1749
+ boot_id: state.boot_id,
1750
+ sequence: state.next_sequence,
1751
+ build_id: input.build_id,
1752
+ platform: input.platform,
1753
+ bridge: input.bridge,
1754
+ capability_profile: state.capability_profile,
1755
+ fingerprintContext,
1756
+ caused_by: state.last_event_id ? [state.last_event_id] : [],
1757
+ monotonic_ns: orderedEventMonotonic(state, input.monotonic_ns),
1758
+ clock_id: state.clock_id,
1759
+ });
1760
+ if (!event) throw new Error("mid-flight session start could not be normalized");
1761
+ event.provenance = {
1762
+ ...event.provenance,
1763
+ attestation: "derived",
1764
+ confidence: cursorPromptBootstrap ? "high" : "medium",
1765
+ };
1766
+ (event.payload as { resume?: unknown }).resume = {
1767
+ state: "unknown",
1768
+ reason: cursorPromptBootstrap ? "cursor_prompt_bootstrap" : "mid_flight_onboarding",
1769
+ };
1770
+ assertEventV3(event);
1771
+ return event;
1772
+ }
1773
+
1774
+ /**
1775
+ * Cursor can deliver its native `beforeSubmitPrompt` signal before its
1776
+ * `sessionStart` hook. The prompt carries both conversation and turn identity,
1777
+ * so it is a high-confidence session boundary rather than degraded recovery.
1778
+ */
1779
+ function isCursorPromptBootstrap(input: RecordHookSignalV3Input): boolean {
1780
+ return (
1781
+ input.adapter === "cursor" &&
1782
+ input.signal === "user-prompt-submit" &&
1783
+ (input.payload.conversation_id !== undefined || input.payload.session_id !== undefined) &&
1784
+ input.payload.turn_id !== undefined
1785
+ );
1786
+ }
1787
+
1788
+ function suppressClosedSpanSignal(
1789
+ input: RecordHookSignalV3Input,
1790
+ sessionHash: `hid_${string}`,
1791
+ category: "late_pre_suppressed" | "late_post_suppressed",
1792
+ ): RecordHookSignalV3Result {
1793
+ writeProducerDiagnosticV3(input.coordRoot, category, {
1794
+ adapter: input.adapter,
1795
+ instance_id: input.instance_id,
1796
+ signal: input.signal,
1797
+ session_hash: sessionHash,
1798
+ payload: input.payload,
1799
+ });
1800
+ return { state: "suppressed", reason: "closed_span" };
1801
+ }
1802
+
1803
+ /**
1804
+ * End one exact live generation under the same private-state lease used by its
1805
+ * native hook producer. This is the only approved-authority terminal writer:
1806
+ * archive reconciliation, explicit `harn-end`, and policy cascades all converge
1807
+ * here so they cannot race each other or append activity after termination.
1808
+ */
1809
+ export function recordApprovedSessionEndV3(
1810
+ input: RecordApprovedSessionEndV3Input,
1811
+ ): RecordApprovedSessionEndV3Result {
1812
+ const control = readEventV3ControlState(input.coordRoot);
1813
+ if (control.state !== input.mode) {
1814
+ return { state: "gate_closed", reason: control.state };
1815
+ }
1816
+ const matches = listHookProducerStateRecordsV3(input.coordRoot, { includeTerminal: true }).filter(
1817
+ ({ state }) =>
1818
+ state.instance_id === input.instance_id && state.generation_id === input.generation_id,
1819
+ );
1820
+ if (matches.length !== 1) {
1821
+ return {
1822
+ state: "generation_unavailable",
1823
+ reason: matches.length === 0 ? "not_found" : "ambiguous",
1824
+ };
1825
+ }
1826
+ const record = matches[0]!;
1827
+ const lease = acquireStateLease(input.coordRoot, record.path);
1828
+ try {
1829
+ const state = readProducerState(record.path);
1830
+ if (state.instance_id !== input.instance_id || state.generation_id !== input.generation_id) {
1831
+ return { state: "generation_unavailable", reason: "authority_changed" };
1832
+ }
1833
+ let recovered = false;
1834
+ if (state.pending) {
1835
+ const pending = state.pending.event;
1836
+ const durability = writeEventV3(input.coordRoot, pending, input.writerOptions);
1837
+ applyCommittedEvent(state, pending);
1838
+ state.pending = undefined;
1839
+ publishProducerState(record.path, state);
1840
+ recovered = true;
1841
+ if (pending.event_type === "session.ended") {
1842
+ return { state: "recorded", event: pending, durability, recovered };
1843
+ }
1844
+ }
1845
+ if (state.terminal) return { state: "already_ended", event_id: state.last_event_id };
1846
+ if (!state.started_event_id || !state.last_event_id) {
1847
+ return { state: "generation_unavailable", reason: "session_start_missing" };
1848
+ }
1849
+
1850
+ const rootId = control.genesis.event.scope.root_id as `root_${string}`;
1851
+ const context = fingerprintContextV3(
1852
+ input.coordRoot,
1853
+ rootId,
1854
+ state.generation_id,
1855
+ state.privacy_epoch_id,
1856
+ );
1857
+ const expected = [
1858
+ "session_started",
1859
+ "turn_closed",
1860
+ "tool_spans_closed",
1861
+ "delegated_children_closed",
1862
+ "coordination_finalized",
1863
+ ];
1864
+ const observed = [
1865
+ ...(state.started_event_id ? ["session_started"] : []),
1866
+ ...(!state.current_turn_id ? ["turn_closed"] : []),
1867
+ ...(state.spans.length === 0 ? ["tool_spans_closed"] : []),
1868
+ ...(state.delegations.length === 0 ? ["delegated_children_closed"] : []),
1869
+ ...(input.coordination_finalized ? ["coordination_finalized"] : []),
1870
+ ];
1871
+ const observedSet = new Set(observed);
1872
+ const missing = expected.filter((field) => !observedSet.has(field));
1873
+ const causedBy = [
1874
+ ...new Set(
1875
+ [state.last_event_id, input.caused_by_event_id, state.session_span.open_event_id].filter(
1876
+ Boolean,
1877
+ ),
1878
+ ),
1879
+ ] as [`evt_${string}`, ...`evt_${string}`[]] | [];
1880
+ const event = buildEventV3("session.ended", {
1881
+ producer: {
1882
+ producer_id: "prd_agent-finalizer",
1883
+ boot_id: `boot_${randomUUID()}`,
1884
+ sequence: 1,
1885
+ component: "agent-coord",
1886
+ build_id: input.build_id,
1887
+ platform: input.platform,
1888
+ },
1889
+ scope: {
1890
+ root_id: rootId,
1891
+ instance_id: state.instance_id,
1892
+ session_id: state.session_id,
1893
+ generation_id: state.generation_id,
1894
+ },
1895
+ attestation_id: state.attestation_id,
1896
+ links: { caused_by: causedBy },
1897
+ provenance: {
1898
+ source_event: `agent-coord.session-finalizer.${input.reason}`,
1899
+ attestation: "derived",
1900
+ confidence: input.confidence ?? "high",
1901
+ source_record_id: normalizeNativeIdV3(
1902
+ context,
1903
+ "agent-coord.approved-session-end",
1904
+ `${input.reason}\0${state.generation_id}\0${input.observed_at ?? "now"}`,
1905
+ ),
1906
+ attribution: {
1907
+ method: "explicit_argument",
1908
+ state: "verified",
1909
+ observer_instance_id: state.instance_id,
1910
+ subject_instance_id: state.instance_id,
1911
+ },
1912
+ },
1913
+ observed_at: input.observed_at,
1914
+ clock_id: state.clock_id,
1915
+ payload: {
1916
+ outcome: input.outcome,
1917
+ authority: "approved",
1918
+ reason: input.reason,
1919
+ span: closeSpanStateV3(state.session_span, {
1920
+ boot_id: state.boot_id,
1921
+ clock: { observed_at: input.observed_at ?? new Date().toISOString() },
1922
+ }),
1923
+ completeness: {
1924
+ state: "observed",
1925
+ value: { expected, observed, missing },
1926
+ attestation: "derived",
1927
+ confidence: input.confidence ?? "high",
1928
+ },
1929
+ },
1930
+ }) as EventV3;
1931
+ markObservedClockRegressionV3(event, state.last_observed_at);
1932
+ assertEventV3(event);
1933
+
1934
+ state.pending = { event };
1935
+ publishProducerState(record.path, state);
1936
+ const durability = writeEventV3(input.coordRoot, event, input.writerOptions);
1937
+ applyCommittedEvent(state, event);
1938
+ state.pending = undefined;
1939
+ publishProducerState(record.path, state);
1940
+ return { state: "recorded", event, durability, recovered };
1941
+ } finally {
1942
+ lease.release();
1943
+ }
1944
+ }
1945
+
1946
+ export interface SalvageOpenSpansV3Input {
1947
+ coordRoot: string;
1948
+ mode: EventV3WriteMode;
1949
+ instance_id: `inst_${string}`;
1950
+ generation_id: `gen_${string}`;
1951
+ allowed_span_ids: readonly `span_${string}`[];
1952
+ requested_turn_id?: `tid_${string}`;
1953
+ build_id: `build_${string}`;
1954
+ platform: "linux" | "windows" | "macos" | "unknown";
1955
+ observed_at?: string;
1956
+ writerOptions?: WriteEventV3Options;
1957
+ }
1958
+
1959
+ export type SalvageOpenSpansV3Result =
1960
+ | { state: "gate_closed"; reason: string }
1961
+ | { state: "generation_unavailable"; reason: string }
1962
+ | { state: "salvaged"; closed: number };
1963
+
1964
+ /**
1965
+ * Explicit-end salvage (ADR 0078): terminalize exactly the approved open-span
1966
+ * set with derived recovery terminals so an authorized end stops wedging
1967
+ * behind spans nothing else can close. Runs under the same private-state
1968
+ * lease as the native producer; spans outside the approved set are refused by
1969
+ * the caller's eligibility gate and never touched here.
1970
+ */
1971
+ export function salvageOpenSpansV3(input: SalvageOpenSpansV3Input): SalvageOpenSpansV3Result {
1972
+ const control = readEventV3ControlState(input.coordRoot);
1973
+ if (control.state !== input.mode) {
1974
+ return { state: "gate_closed", reason: control.state };
1975
+ }
1976
+ const matches = listHookProducerStateRecordsV3(input.coordRoot).filter(
1977
+ ({ state }) =>
1978
+ state.instance_id === input.instance_id && state.generation_id === input.generation_id,
1979
+ );
1980
+ if (matches.length !== 1) {
1981
+ return {
1982
+ state: "generation_unavailable",
1983
+ reason: matches.length === 0 ? "not_found" : "ambiguous",
1984
+ };
1985
+ }
1986
+ const record = matches[0]!;
1987
+ const lease = acquireStateLeaseWithRetry(input.coordRoot, record.path, 2);
1988
+ if (!lease) return { state: "generation_unavailable", reason: "busy" };
1989
+ try {
1990
+ const state = readProducerState(record.path);
1991
+ if (
1992
+ state.instance_id !== input.instance_id ||
1993
+ state.generation_id !== input.generation_id ||
1994
+ state.terminal
1995
+ ) {
1996
+ return { state: "generation_unavailable", reason: "authority_changed" };
1997
+ }
1998
+ const rootId = control.genesis.event.scope.root_id as `root_${string}`;
1999
+ const salvageInput: RecordHookSignalV3Input = {
2000
+ coordRoot: input.coordRoot,
2001
+ mode: input.mode,
2002
+ signal: "stop",
2003
+ payload: { raw: {} },
2004
+ adapter: state.adapter,
2005
+ instance_id: state.instance_id,
2006
+ producer_id: "prd_agent-finalizer",
2007
+ build_id: input.build_id,
2008
+ platform: input.platform,
2009
+ writerOptions: input.writerOptions,
2010
+ };
2011
+ if (state.pending) {
2012
+ const pendingEvent = state.pending.event;
2013
+ writeEventV3(input.coordRoot, pendingEvent, input.writerOptions);
2014
+ applyCommittedEvent(state, pendingEvent);
2015
+ state.pending = undefined;
2016
+ publishProducerState(record.path, state);
2017
+ }
2018
+ const allowed = new Set(input.allowed_span_ids);
2019
+ const fingerprintContext = fingerprintContextV3(
2020
+ input.coordRoot,
2021
+ rootId,
2022
+ state.generation_id,
2023
+ state.privacy_epoch_id,
2024
+ );
2025
+ // Salvage runs outside the hook producer chain: a fresh boot starting at
2026
+ // sequence 1 keeps the reader's per-(producer, boot) continuity intact.
2027
+ const salvageBoot = `boot_${randomUUID()}` as `boot_${string}`;
2028
+ let salvageSequence = 1;
2029
+ let closed = 0;
2030
+ for (const span of [...state.spans]) {
2031
+ if (!allowed.has(span.span_id)) continue;
2032
+ if (!span.turn_id) {
2033
+ // An unstamped span still needs a turn scope; the requested turn is
2034
+ // the only honest owner the explicit end named, and a salvage-scoped
2035
+ // synthetic id is the last resort.
2036
+ span.turn_id =
2037
+ input.requested_turn_id ??
2038
+ (`tid_${normalizeNativeIdV3(
2039
+ fingerprintContext,
2040
+ `${state.adapter}.turn`,
2041
+ `salvage:${span.span_id}`,
2042
+ ).slice(4)}` as `tid_${string}`);
2043
+ }
2044
+ const event = buildDerivedToolCompleted(
2045
+ salvageInput,
2046
+ state,
2047
+ rootId,
2048
+ span,
2049
+ "explicit_end_salvage",
2050
+ { producer_id: "prd_agent-finalizer", boot_id: salvageBoot, sequence: salvageSequence },
2051
+ input.observed_at,
2052
+ );
2053
+ salvageSequence += 1;
2054
+ commitEventLocked(salvageInput, state, record.path, event);
2055
+ closed += 1;
2056
+ }
2057
+ return { state: "salvaged", closed };
2058
+ } finally {
2059
+ lease.release();
2060
+ }
2061
+ }
2062
+
2063
+ export function listHookProducerStateRecordsV3(
2064
+ coordRoot: string,
2065
+ options: { includeTerminal?: boolean } = {},
2066
+ ): HookProducerStateRecordV3[] {
2067
+ const control = readEventV3ControlState(coordRoot);
2068
+ if (control.state !== "candidate" && control.state !== "active") return [];
2069
+ const producerRoot = join(resolve(coordRoot), EVENT_V3_LEDGER_RELATIVE_ROOT, "private-producers");
2070
+ if (!existsSync(producerRoot)) return [];
2071
+ const records: HookProducerStateRecordV3[] = [];
2072
+ for (const adapter of ["claude-code", "codex", "cursor"] as const) {
2073
+ const directory = join(producerRoot, adapter);
2074
+ if (!existsSync(directory)) continue;
2075
+ const metadata = lstatSync(directory);
2076
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
2077
+ throw new Error("V3 producer state directory is unsafe");
2078
+ }
2079
+ for (const name of readdirSync(directory).filter((entry) =>
2080
+ /^hid_[a-f0-9]{64}\.json$/.test(entry),
2081
+ )) {
2082
+ const path = join(directory, name);
2083
+ const state = readProducerState(path);
2084
+ if (!options.includeTerminal && state.terminal) continue;
2085
+ records.push({ path, modified_at_ms: lstatSync(path).mtimeMs, state });
2086
+ }
2087
+ }
2088
+ return records.sort(
2089
+ (left, right) =>
2090
+ left.state.generation_id.localeCompare(right.state.generation_id) ||
2091
+ left.path.localeCompare(right.path),
2092
+ );
2093
+ }
2094
+
2095
+ function hookSignalCapability(signal: HookSignalV3): AdapterSignalV3 {
2096
+ switch (signal) {
2097
+ case "session-start":
2098
+ return "session_start";
2099
+ case "session-end":
2100
+ return "session_end";
2101
+ case "user-prompt-submit":
2102
+ return "prompt";
2103
+ case "stop":
2104
+ case "stop-failure":
2105
+ return "turn_completion";
2106
+ case "pre-tool-use":
2107
+ return "tool_request";
2108
+ case "post-tool-use":
2109
+ return "tool_result";
2110
+ case "post-tool-use-failure":
2111
+ return "tool_failure";
2112
+ case "permission-request":
2113
+ return "permission";
2114
+ case "sub-agent-start":
2115
+ case "sub-agent-stop":
2116
+ return "subagent";
2117
+ case "pre-compact":
2118
+ return "pre_compaction";
2119
+ case "post-compact":
2120
+ return "post_compaction";
2121
+ }
2122
+ }
2123
+
2124
+ export function readHookProducerStateV3(
2125
+ coordRoot: string,
2126
+ adapter: Adapter,
2127
+ nativeSessionId: string,
2128
+ ): HookProducerStateV3 | undefined {
2129
+ const control = readEventV3ControlState(coordRoot);
2130
+ if (control.state !== "candidate" && control.state !== "active") return undefined;
2131
+ const rootId = control.genesis.event.scope.root_id as `root_${string}`;
2132
+ const context = fingerprintContextV3(
2133
+ coordRoot,
2134
+ rootId,
2135
+ undefined,
2136
+ control.genesis.profile.privacy_key_epoch,
2137
+ );
2138
+ const sessionHash = normalizeNativeIdV3(context, `${adapter}.session`, nativeSessionId);
2139
+ const path = producerStatePath(coordRoot, adapter, sessionHash);
2140
+ return existsSync(path) ? readProducerState(path) : undefined;
2141
+ }
2142
+
2143
+ /** Resolve one terminal producer only when native session and instance identity agree. */
2144
+ export function readTerminalHookProducerStateV3(
2145
+ coordRoot: string,
2146
+ nativeSessionId: string,
2147
+ instanceId: `inst_${string}`,
2148
+ ): HookProducerStateV3 | undefined {
2149
+ const matches = (["claude-code", "codex", "cursor"] as const)
2150
+ .map((adapter) => readHookProducerStateV3(coordRoot, adapter, nativeSessionId))
2151
+ .filter(
2152
+ (state): state is HookProducerStateV3 =>
2153
+ state?.terminal === true && state.instance_id === instanceId,
2154
+ );
2155
+ return matches.length === 1 ? matches[0] : undefined;
2156
+ }
2157
+
2158
+ export function readHookProducerStateByInstanceV3(
2159
+ coordRoot: string,
2160
+ instanceId: `inst_${string}`,
2161
+ ): HookProducerStateV3 | undefined {
2162
+ const control = readEventV3ControlState(coordRoot);
2163
+ if (control.state !== "candidate" && control.state !== "active") return undefined;
2164
+ const producerRoot = join(resolve(coordRoot), EVENT_V3_LEDGER_RELATIVE_ROOT, "private-producers");
2165
+ if (!existsSync(producerRoot)) return undefined;
2166
+ const matches: HookProducerStateV3[] = [];
2167
+ for (const adapter of ["claude-code", "codex", "cursor"] as const) {
2168
+ const directory = join(producerRoot, adapter);
2169
+ if (!existsSync(directory)) continue;
2170
+ const metadata = lstatSync(directory);
2171
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
2172
+ throw new Error("V3 producer state directory is unsafe");
2173
+ }
2174
+ for (const name of readdirSync(directory).filter((entry) =>
2175
+ /^hid_[a-f0-9]{64}\.json$/.test(entry),
2176
+ )) {
2177
+ const state = readProducerState(join(directory, name));
2178
+ if (state.instance_id === instanceId && !state.terminal) matches.push(state);
2179
+ }
2180
+ }
2181
+ return matches.length === 1 ? matches[0] : undefined;
2182
+ }
2183
+
2184
+ function newProducerState(
2185
+ input: RecordHookSignalV3Input,
2186
+ sessionId: `sid_${string}`,
2187
+ epochId: `pep_${string}`,
2188
+ boundaryEventId: `evt_${string}`,
2189
+ ): HookProducerStateV3 {
2190
+ const bootId = `boot_${randomUUID()}` as const;
2191
+ return {
2192
+ format: STATE_FORMAT,
2193
+ format_version: STATE_VERSION,
2194
+ adapter: input.adapter,
2195
+ instance_id: input.instance_id,
2196
+ session_id: sessionId,
2197
+ generation_id: generationIdV3(),
2198
+ attestation_id: attestationIdV3(),
2199
+ capability_profile: adapterCapabilityProfileDigestV3(input.adapter),
2200
+ cursor_mode: input.adapter === "cursor" ? (input.payload.cursor_mode ?? "unknown") : undefined,
2201
+ privacy_epoch_id: epochId,
2202
+ boot_id: bootId,
2203
+ clock_id: clockIdV3(),
2204
+ next_sequence: 1,
2205
+ tool_call_count: 0,
2206
+ last_event_id: boundaryEventId,
2207
+ session_span: openSpanStateV3({
2208
+ span_id: spanIdV3(),
2209
+ boot_id: bootId,
2210
+ clock: signalClock(input),
2211
+ }),
2212
+ terminal: false,
2213
+ spans: [],
2214
+ delegations: [],
2215
+ closed_spans: [],
2216
+ closed_turn_ids: [],
2217
+ waits: [],
2218
+ turn_harness: emptyTurnHarnessTiming(),
2219
+ turn_ordinal: 0,
2220
+ };
2221
+ }
2222
+
2223
+ function applyCommittedEvent(state: HookProducerStateV3, event: EventV3): void {
2224
+ // Sequence continuity is keyed on (producer_id, boot_id): only events that
2225
+ // ride the state's own producer chain advance it. Finalizer-authored events
2226
+ // (fresh boot, sequence 1) must not create gaps in the hook chain.
2227
+ if (event.producer.boot_id === state.boot_id) state.next_sequence += 1;
2228
+ if (event.time.monotonic_ns) state.last_monotonic_ns = event.time.monotonic_ns;
2229
+ // Mirrors the reader, which tracks the last observed value per clock even
2230
+ // when that value regressed. Holding the maximum instead would hide a
2231
+ // second regression behind the first.
2232
+ state.last_observed_at = event.time.observed_at;
2233
+ state.last_event_id = event.event_id as `evt_${string}`;
2234
+ if (event.event_type === "session.started") {
2235
+ state.started_event_id = event.event_id as `evt_${string}`;
2236
+ state.session_span.open_event_id = event.event_id as `evt_${string}`;
2237
+ }
2238
+ if (event.event_type === "turn.started") {
2239
+ const nextTurnId = (event.scope as { turn_id: `tid_${string}` }).turn_id;
2240
+ state.current_turn_id = nextTurnId;
2241
+ if (state.tool_call_count_turn_id !== nextTurnId) {
2242
+ state.tool_call_count = 0;
2243
+ state.tool_call_count_turn_id = nextTurnId;
2244
+ }
2245
+ if (state.current_turn_span) {
2246
+ state.current_turn_span.open_event_id = event.event_id as `evt_${string}`;
2247
+ }
2248
+ state.turn_ordinal += 1;
2249
+ state.closed_spans = state.closed_spans.filter(
2250
+ (closed) => closed.turn_ordinal >= state.turn_ordinal - CLOSED_SPAN_TURN_RETENTION,
2251
+ );
2252
+ }
2253
+ if (event.event_type === "tool.requested") {
2254
+ const requestTurnId = (event.scope as { turn_id: `tid_${string}` }).turn_id;
2255
+ if (state.tool_call_count_turn_id !== requestTurnId) {
2256
+ state.tool_call_count = 0;
2257
+ state.tool_call_count_turn_id = requestTurnId;
2258
+ }
2259
+ state.tool_call_count += 1;
2260
+ }
2261
+ if (event.event_type === "wait.started") {
2262
+ const waitId = (event.payload as { wait_id: `hid_${string}` }).wait_id;
2263
+ const turnId = (event.scope as { turn_id: `tid_${string}` }).turn_id;
2264
+ const waitLinks = event.links as {
2265
+ span_id: `span_${string}`;
2266
+ parent_span_id?: `span_${string}`;
2267
+ };
2268
+ if (!state.waits.some((wait) => wait.wait_id === waitId)) {
2269
+ state.waits.push({
2270
+ wait_id: waitId,
2271
+ started_event_id: event.event_id as `evt_${string}`,
2272
+ turn_id: turnId,
2273
+ span_id: waitLinks.span_id,
2274
+ ...(waitLinks.parent_span_id ? { parent_span_id: waitLinks.parent_span_id } : {}),
2275
+ opened_at: event.time.observed_at,
2276
+ boot_id: state.boot_id,
2277
+ ...(event.time.monotonic_ns ? { opened_monotonic_ns: event.time.monotonic_ns } : {}),
2278
+ open_event_id: event.event_id as `evt_${string}`,
2279
+ });
2280
+ }
2281
+ }
2282
+ if (event.event_type === "wait.ended") {
2283
+ const waitId = (event.payload as { wait_id: string }).wait_id;
2284
+ state.waits = state.waits.filter((wait) => wait.wait_id !== waitId);
2285
+ }
2286
+ if (event.event_type === "tool.completed") {
2287
+ const completedSpan = (event.links as { span_id: `span_${string}` }).span_id;
2288
+ const closing = state.spans.find((span) => span.span_id === completedSpan);
2289
+ if (closing) {
2290
+ state.closed_spans.push({
2291
+ source_id: closing.source_id,
2292
+ ...(closing.semantic_key ? { semantic_key: closing.semantic_key } : {}),
2293
+ span_id: closing.span_id,
2294
+ closed_event_id: event.event_id as `evt_${string}`,
2295
+ turn_ordinal: state.turn_ordinal,
2296
+ });
2297
+ if (state.closed_spans.length > CLOSED_SPAN_MEMORY_CAP) {
2298
+ state.closed_spans = state.closed_spans.slice(-CLOSED_SPAN_MEMORY_CAP);
2299
+ }
2300
+ }
2301
+ state.spans = state.spans.filter((span) => span.span_id !== completedSpan);
2302
+ }
2303
+ if (event.event_type === "agent.completed") {
2304
+ const completedDelegation = event.payload.delegation_id;
2305
+ state.delegations = state.delegations.filter(
2306
+ (delegation) => delegation.delegation_id !== completedDelegation,
2307
+ );
2308
+ }
2309
+ if (event.event_type === "agent.started") {
2310
+ const openedDelegation = state.delegations.find(
2311
+ (candidate) => candidate.delegation_id === event.payload.delegation_id,
2312
+ );
2313
+ if (openedDelegation) openedDelegation.open_event_id = event.event_id as `evt_${string}`;
2314
+ }
2315
+ if (event.event_type === "turn.completed") {
2316
+ const completedTurnId = (event.scope as { turn_id: `tid_${string}` }).turn_id;
2317
+ if (!state.closed_turn_ids.includes(completedTurnId)) {
2318
+ state.closed_turn_ids.push(completedTurnId);
2319
+ if (state.closed_turn_ids.length > CLOSED_TURN_MEMORY_CAP) {
2320
+ state.closed_turn_ids = state.closed_turn_ids.slice(-CLOSED_TURN_MEMORY_CAP);
2321
+ }
2322
+ }
2323
+ state.current_turn_id = undefined;
2324
+ state.current_turn_span = undefined;
2325
+ state.tool_call_count = 0;
2326
+ state.tool_call_count_turn_id = undefined;
2327
+ state.turn_harness = emptyTurnHarnessTiming();
2328
+ }
2329
+ if (event.event_type === "session.ended") state.terminal = true;
2330
+ }
2331
+
2332
+ function unpairableTool(
2333
+ input: RecordHookSignalV3Input,
2334
+ sessionHash: `hid_${string}`,
2335
+ reason: "missing_tool_use_id" | "no_open_span",
2336
+ ): RecordHookSignalV3Result {
2337
+ writeProducerDiagnosticV3(input.coordRoot, "unpairable_tool", {
2338
+ reason,
2339
+ adapter: input.adapter,
2340
+ instance_id: input.instance_id,
2341
+ signal: input.signal,
2342
+ session_hash: sessionHash,
2343
+ payload: input.payload,
2344
+ });
2345
+ return { state: "unpairable_tool", reason };
2346
+ }
2347
+
2348
+ function sourceIdForSignal(
2349
+ input: RecordHookSignalV3Input,
2350
+ context: ReturnType<typeof fingerprintContextV3>,
2351
+ ): `hid_${string}` | undefined {
2352
+ const toolSignal =
2353
+ input.signal === "pre-tool-use" ||
2354
+ input.signal === "post-tool-use" ||
2355
+ input.signal === "post-tool-use-failure";
2356
+ const subagentSignal = input.signal === "sub-agent-start" || input.signal === "sub-agent-stop";
2357
+ const native = toolSignal
2358
+ ? input.payload.tool_use_id
2359
+ : subagentSignal
2360
+ ? (input.payload.subagent_id ?? input.payload.agent_id)
2361
+ : (input.payload.turn_id ??
2362
+ (input.signal === "session-start"
2363
+ ? (input.payload.session_id ?? input.payload.conversation_id ?? input.payload.agent_id)
2364
+ : undefined));
2365
+ return native
2366
+ ? normalizeNativeIdV3(
2367
+ context,
2368
+ `${input.adapter}.hook-source`,
2369
+ `${toolSignal ? "tool" : subagentSignal ? "subagent" : input.signal}:${native}`,
2370
+ )
2371
+ : undefined;
2372
+ }
2373
+
2374
+ function cursorShellSemanticKey(
2375
+ input: RecordHookSignalV3Input,
2376
+ context: ReturnType<typeof fingerprintContextV3>,
2377
+ ): `hid_${string}` | undefined {
2378
+ if (input.adapter !== "cursor") return undefined;
2379
+ const command = extractBashCommand(input.payload.tool_name, input.payload.tool_input);
2380
+ return command
2381
+ ? normalizeNativeIdV3(context, "cursor.shell-operation", command.normalize("NFC"))
2382
+ : undefined;
2383
+ }
2384
+
2385
+ function cursorShellFallbackSourceId(
2386
+ semanticKey: `hid_${string}`,
2387
+ state: HookProducerStateV3,
2388
+ context: ReturnType<typeof fingerprintContextV3>,
2389
+ ): `hid_${string}` {
2390
+ return normalizeNativeIdV3(
2391
+ context,
2392
+ "cursor.hook-source",
2393
+ `shell:${semanticKey}:${state.turn_ordinal}:${state.tool_call_count}`,
2394
+ );
2395
+ }
2396
+
2397
+ function closeResolvedWaits(
2398
+ input: RecordHookSignalV3Input,
2399
+ state: HookProducerStateV3,
2400
+ path: string,
2401
+ rootId: `root_${string}`,
2402
+ fingerprintContext: ReturnType<typeof fingerprintContextV3>,
2403
+ nativeTid: `tid_${string}` | undefined,
2404
+ ): void {
2405
+ const toolResolution =
2406
+ input.signal === "pre-tool-use" ||
2407
+ input.signal === "post-tool-use" ||
2408
+ input.signal === "post-tool-use-failure";
2409
+ const resolvedWaitId = toolResolution ? waitIdForInput(input, fingerprintContext) : undefined;
2410
+ const endingTurnId =
2411
+ input.signal === "stop" || input.signal === "stop-failure"
2412
+ ? (nativeTid ?? state.current_turn_id)
2413
+ : undefined;
2414
+ let waits = state.waits.filter(
2415
+ (wait) => wait.wait_id === resolvedWaitId || wait.turn_id === endingTurnId,
2416
+ );
2417
+ if (endingTurnId && waits.length === 0) waits = [...state.waits];
2418
+ if (toolResolution && waits.length === 0 && state.current_turn_id) {
2419
+ const turnWaits = state.waits.filter((wait) => wait.turn_id === state.current_turn_id);
2420
+ if (turnWaits.length === 1) waits = turnWaits;
2421
+ }
2422
+ for (const wait of waits) {
2423
+ const outcome = endingTurnId
2424
+ ? "interrupted"
2425
+ : input.signal === "post-tool-use-failure"
2426
+ ? "denied"
2427
+ : "succeeded";
2428
+ const event = buildEventV3("wait.ended", {
2429
+ producer: {
2430
+ producer_id: input.producer_id,
2431
+ boot_id: state.boot_id,
2432
+ sequence: state.next_sequence,
2433
+ component: "agent-hook",
2434
+ build_id: input.build_id,
2435
+ platform: input.platform,
2436
+ ...(input.bridge ? { bridge: input.bridge } : {}),
2437
+ },
2438
+ scope: {
2439
+ root_id: rootId,
2440
+ instance_id: state.instance_id,
2441
+ session_id: state.session_id,
2442
+ generation_id: state.generation_id,
2443
+ turn_id: wait.turn_id,
2444
+ ...(input.run_id ? { run_id: input.run_id } : {}),
2445
+ ...(input.workflow_id ? { workflow_id: input.workflow_id } : {}),
2446
+ ...(input.workflow_agent_id ? { workflow_agent_id: input.workflow_agent_id } : {}),
2447
+ },
2448
+ attestation_id: state.attestation_id,
2449
+ links: { caused_by: [wait.started_event_id] },
2450
+ provenance: {
2451
+ source_event: `${input.adapter}.permission-resolution`,
2452
+ attestation: "derived",
2453
+ confidence: "high",
2454
+ attribution: {
2455
+ method: "native_payload",
2456
+ state: "verified",
2457
+ subject_instance_id: state.instance_id,
2458
+ },
2459
+ },
2460
+ monotonic_ns: orderedEventMonotonic(state, input.monotonic_ns),
2461
+ clock_id: state.clock_id,
2462
+ payload: {
2463
+ wait_id: wait.wait_id,
2464
+ outcome,
2465
+ resolution_reference: endingTurnId ? "turn_terminal" : input.signal,
2466
+ span: closeSpanStateV3(wait, {
2467
+ boot_id: state.boot_id,
2468
+ clock: signalClock(input),
2469
+ }),
2470
+ },
2471
+ }) as EventV3;
2472
+ commitEventLocked(input, state, path, event);
2473
+ }
2474
+ }
2475
+
2476
+ function waitIdForInput(
2477
+ input: RecordHookSignalV3Input,
2478
+ fingerprintContext: ReturnType<typeof fingerprintContextV3>,
2479
+ ): `hid_${string}` | undefined {
2480
+ return input.payload.tool_use_id
2481
+ ? normalizeNativeIdV3(fingerprintContext, `${input.adapter}.wait`, input.payload.tool_use_id)
2482
+ : undefined;
2483
+ }
2484
+
2485
+ function signalClock(
2486
+ input: Pick<RecordHookSignalV3Input, "observed_at" | "monotonic_ns">,
2487
+ ): SpanClockV3 {
2488
+ return {
2489
+ observed_at: input.observed_at ?? new Date().toISOString(),
2490
+ ...(input.monotonic_ns ? { monotonic_ns: input.monotonic_ns } : {}),
2491
+ };
2492
+ }
2493
+
2494
+ function durationMilliseconds(
2495
+ start: string | undefined,
2496
+ end: string | undefined,
2497
+ ): number | undefined {
2498
+ if (!start || !end || !/^\d+$/.test(start) || !/^\d+$/.test(end)) return undefined;
2499
+ const delta = BigInt(end) - BigInt(start);
2500
+ if (delta < 0n) return undefined;
2501
+ const milliseconds = Number(delta / 1_000_000n);
2502
+ return Number.isSafeInteger(milliseconds) ? milliseconds : undefined;
2503
+ }
2504
+
2505
+ function safeRole(value: unknown): string {
2506
+ if (typeof value !== "string") return "agent";
2507
+ const normalized = value
2508
+ .normalize("NFC")
2509
+ .replace(/[^a-zA-Z0-9._:/+-]/g, "_")
2510
+ .slice(0, 128);
2511
+ return /^[a-zA-Z0-9]/.test(normalized) ? normalized : "agent";
2512
+ }
2513
+
2514
+ function safeTokenOrUndefined(value: unknown): string | undefined {
2515
+ if (typeof value !== "string") return undefined;
2516
+ const normalized = value.normalize("NFC");
2517
+ return /^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,127}$/.test(normalized) ? normalized : undefined;
2518
+ }
2519
+
2520
+ function producerStatePath(
2521
+ coordRoot: string,
2522
+ adapter: Adapter,
2523
+ sessionHash: `hid_${string}`,
2524
+ ): string {
2525
+ return join(
2526
+ resolve(coordRoot),
2527
+ EVENT_V3_LEDGER_RELATIVE_ROOT,
2528
+ "private-producers",
2529
+ adapter,
2530
+ `${sessionHash}.json`,
2531
+ );
2532
+ }
2533
+
2534
+ function acquireStateLease(coordRoot: string, statePath: string) {
2535
+ const directory = join(statePath, "..");
2536
+ const producerRoot = join(resolve(coordRoot), EVENT_V3_LEDGER_RELATIVE_ROOT, "private-producers");
2537
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
2538
+ chmodSync(producerRoot, 0o700);
2539
+ chmodSync(directory, 0o700);
2540
+ return acquireNoClobberLease({
2541
+ path: `${statePath}.lease`,
2542
+ scope: "event-v3-hook-producer",
2543
+ authoritySha256: createHash("sha256")
2544
+ .update(resolve(coordRoot))
2545
+ .update("\0")
2546
+ .update(statePath)
2547
+ .digest("hex"),
2548
+ staleAfterMs: 5_000,
2549
+ validateStaleOwner: (owner) => owner.host === hostname() && !pidIsAlive(owner.pid),
2550
+ });
2551
+ }
2552
+
2553
+ function publishProducerState(path: string, state: HookProducerStateV3): void {
2554
+ const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
2555
+ let fd: number | undefined;
2556
+ try {
2557
+ fd = openSync(temporary, "wx", 0o600);
2558
+ writeFileSync(fd, `${JSON.stringify(state)}\n`, "utf8");
2559
+ fsyncSync(fd);
2560
+ closeSync(fd);
2561
+ fd = undefined;
2562
+ renameSync(temporary, path);
2563
+ chmodSync(path, 0o600);
2564
+ fsyncParentDirectory(path);
2565
+ } finally {
2566
+ if (fd !== undefined) closeSync(fd);
2567
+ if (existsSync(temporary)) unlinkSync(temporary);
2568
+ }
2569
+ }
2570
+
2571
+ function readProducerState(path: string): HookProducerStateV3 {
2572
+ const metadata = lstatSync(path);
2573
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
2574
+ throw new Error("V3 producer state path is unsafe");
2575
+ }
2576
+ if ((metadata.mode & 0o077) !== 0) throw new Error("V3 producer state is not owner-only");
2577
+ let parsed: unknown;
2578
+ try {
2579
+ parsed = JSON.parse(readFileSync(path, "utf8"));
2580
+ } catch {
2581
+ throw new Error("V3 producer state is unreadable");
2582
+ }
2583
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2584
+ throw new Error("V3 producer state is invalid");
2585
+ }
2586
+ const state = parsed as HookProducerStateV3;
2587
+ // In-place additive upgrade from format 1 (pre-ADR-0078): the new fields
2588
+ // default empty; an old build reading a version-2 file still throws, and its
2589
+ // signal is already durable in the intake spool.
2590
+ if ((state as { format_version: number }).format_version === 1 && state.format === STATE_FORMAT) {
2591
+ state.format_version = STATE_VERSION;
2592
+ state.closed_spans ??= [];
2593
+ state.turn_ordinal ??= 0;
2594
+ }
2595
+ // Additive format-2 state: existing files predate wait tracking but remain
2596
+ // valid and acquire an empty set on their next read.
2597
+ state.waits ??= [];
2598
+ state.closed_turn_ids ??= [];
2599
+ state.turn_harness ??= emptyTurnHarnessTiming();
2600
+ if (state.adapter === "cursor") state.cursor_mode ??= "unknown";
2601
+ const allowedKeys = new Set([
2602
+ "adapter",
2603
+ "attestation_id",
2604
+ "boot_id",
2605
+ "capability_profile",
2606
+ "clock_id",
2607
+ "closed_spans",
2608
+ "closed_turn_ids",
2609
+ "current_turn_id",
2610
+ "current_turn_span",
2611
+ "cursor_mode",
2612
+ "delegations",
2613
+ "format",
2614
+ "format_version",
2615
+ "generation_id",
2616
+ "instance_id",
2617
+ "last_event_id",
2618
+ "last_monotonic_ns",
2619
+ "last_observed_at",
2620
+ "next_sequence",
2621
+ "pending",
2622
+ "privacy_epoch_id",
2623
+ "session_id",
2624
+ "session_span",
2625
+ "spans",
2626
+ "started_event_id",
2627
+ "terminal",
2628
+ "tool_call_count",
2629
+ "tool_call_count_turn_id",
2630
+ "turn_harness",
2631
+ "turn_ordinal",
2632
+ "waits",
2633
+ ]);
2634
+ if (
2635
+ Object.keys(state).some((key) => !allowedKeys.has(key)) ||
2636
+ state.format !== STATE_FORMAT ||
2637
+ state.format_version !== STATE_VERSION ||
2638
+ !["claude-code", "codex", "cursor"].includes(state.adapter) ||
2639
+ (state.cursor_mode !== undefined &&
2640
+ !["local", "cloud", "unknown"].includes(state.cursor_mode)) ||
2641
+ !/^inst_[a-zA-Z0-9._-]{1,128}$/.test(state.instance_id) ||
2642
+ !/^sid_[a-f0-9]{64}$/.test(state.session_id) ||
2643
+ !/^gen_[0-9a-f-]{36}$/.test(state.generation_id) ||
2644
+ !/^att_[0-9a-f-]{36}$/.test(state.attestation_id) ||
2645
+ !/^cap_[a-f0-9]{64}$/.test(state.capability_profile) ||
2646
+ !/^pep_[a-zA-Z0-9._-]+$/.test(state.privacy_epoch_id) ||
2647
+ !/^boot_[a-zA-Z0-9._-]+$/.test(state.boot_id) ||
2648
+ !/^clk_[0-9a-f-]{36}$/.test(state.clock_id) ||
2649
+ !Number.isSafeInteger(state.next_sequence) ||
2650
+ state.next_sequence < 1 ||
2651
+ !Number.isSafeInteger(state.tool_call_count) ||
2652
+ state.tool_call_count < 0 ||
2653
+ (state.tool_call_count_turn_id !== undefined &&
2654
+ !/^tid_[a-f0-9]{64}$/.test(state.tool_call_count_turn_id)) ||
2655
+ !validTurnHarnessTiming(state.turn_harness) ||
2656
+ !validOpenSpanState(state.session_span) ||
2657
+ (state.current_turn_span !== undefined && !validOpenSpanState(state.current_turn_span)) ||
2658
+ typeof state.terminal !== "boolean" ||
2659
+ !Array.isArray(state.spans) ||
2660
+ !Array.isArray(state.delegations) ||
2661
+ state.delegations.length > 256 ||
2662
+ state.delegations.some(
2663
+ (delegation) =>
2664
+ !/^hid_[a-f0-9]{64}$/.test(delegation.source_id) ||
2665
+ !/^del_[0-9a-f-]{36}$/.test(delegation.delegation_id) ||
2666
+ !/^gen_[0-9a-f-]{36}$/.test(delegation.child_generation_id) ||
2667
+ !validOpenSpanState(delegation) ||
2668
+ !/^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,127}$/.test(delegation.role),
2669
+ ) ||
2670
+ state.spans.length > 256 ||
2671
+ state.spans.some(
2672
+ (span) =>
2673
+ !/^hid_[a-f0-9]{64}$/.test(span.source_id) ||
2674
+ !validOpenSpanState(span) ||
2675
+ (span.recovery_reason !== undefined &&
2676
+ !/^[a-z0-9][a-z0-9._-]{0,79}$/.test(span.recovery_reason)) ||
2677
+ (span.turn_id !== undefined && !/^tid_[a-f0-9]{64}$/.test(span.turn_id)) ||
2678
+ (span.turn_stamp !== undefined &&
2679
+ span.turn_stamp !== "native_payload" &&
2680
+ span.turn_stamp !== "producer_state") ||
2681
+ (span.requested_event_id !== undefined &&
2682
+ !/^evt_[0-9a-f-]{36}$/.test(span.requested_event_id)) ||
2683
+ (span.semantic_key !== undefined && !/^hid_[a-f0-9]{64}$/.test(span.semantic_key)) ||
2684
+ (span.tool_name !== undefined &&
2685
+ !/^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,127}$/.test(span.tool_name)),
2686
+ ) ||
2687
+ !Array.isArray(state.closed_spans) ||
2688
+ state.closed_spans.length > CLOSED_SPAN_MEMORY_CAP ||
2689
+ state.closed_spans.some(
2690
+ (closed) =>
2691
+ !/^hid_[a-f0-9]{64}$/.test(closed.source_id) ||
2692
+ (closed.semantic_key !== undefined && !/^hid_[a-f0-9]{64}$/.test(closed.semantic_key)) ||
2693
+ !/^span_[0-9a-f-]{36}$/.test(closed.span_id) ||
2694
+ !/^evt_[0-9a-f-]{36}$/.test(closed.closed_event_id) ||
2695
+ !Number.isSafeInteger(closed.turn_ordinal) ||
2696
+ closed.turn_ordinal < 0,
2697
+ ) ||
2698
+ !Array.isArray(state.closed_turn_ids) ||
2699
+ state.closed_turn_ids.length > CLOSED_TURN_MEMORY_CAP ||
2700
+ state.closed_turn_ids.some((turnId) => !/^tid_[a-f0-9]{64}$/.test(turnId)) ||
2701
+ !Array.isArray(state.waits) ||
2702
+ state.waits.length > 256 ||
2703
+ state.waits.some(
2704
+ (wait) =>
2705
+ !/^hid_[a-f0-9]{64}$/.test(wait.wait_id) ||
2706
+ !/^evt_[0-9a-f-]{36}$/.test(wait.started_event_id) ||
2707
+ !/^tid_[a-f0-9]{64}$/.test(wait.turn_id) ||
2708
+ !validOpenSpanState(wait),
2709
+ ) ||
2710
+ !Number.isSafeInteger(state.turn_ordinal) ||
2711
+ state.turn_ordinal < 0 ||
2712
+ (state.current_turn_id !== undefined && !/^tid_[a-f0-9]{64}$/.test(state.current_turn_id)) ||
2713
+ (state.last_event_id !== undefined && !/^evt_[0-9a-f-]{36}$/.test(state.last_event_id)) ||
2714
+ (state.last_monotonic_ns !== undefined && !/^\d+$/.test(state.last_monotonic_ns)) ||
2715
+ (state.last_observed_at !== undefined &&
2716
+ !Number.isFinite(Date.parse(state.last_observed_at))) ||
2717
+ (state.started_event_id !== undefined && !/^evt_[0-9a-f-]{36}$/.test(state.started_event_id)) ||
2718
+ (state.pending?.source_id !== undefined &&
2719
+ !/^hid_[a-f0-9]{64}$/.test(state.pending.source_id)) ||
2720
+ (state.pending && !validateEventV3(state.pending.event).ok)
2721
+ ) {
2722
+ throw new Error("V3 producer state is invalid");
2723
+ }
2724
+ return state;
2725
+ }
2726
+
2727
+ function recordTurnHarnessTiming(
2728
+ state: HookProducerStateV3,
2729
+ input: RecordHookSignalV3Input,
2730
+ resetForTurnStart = true,
2731
+ ): void {
2732
+ if (input.signal === "user-prompt-submit" && resetForTurnStart) {
2733
+ state.turn_harness = emptyTurnHarnessTiming();
2734
+ }
2735
+ if (
2736
+ input.hook_duration_ms === undefined ||
2737
+ (!state.current_turn_id && input.signal !== "user-prompt-submit")
2738
+ ) {
2739
+ return;
2740
+ }
2741
+ const duration = Math.max(0, Math.floor(input.hook_duration_ms));
2742
+ if (!Number.isSafeInteger(duration)) return;
2743
+ const hook = safeRole(input.hook_name ?? input.signal);
2744
+ state.turn_harness = {
2745
+ hook_time_ms: state.turn_harness.hook_time_ms + duration,
2746
+ hook_count: state.turn_harness.hook_count + 1,
2747
+ slowest_hook:
2748
+ duration >= state.turn_harness.slowest_hook_ms ? hook : state.turn_harness.slowest_hook,
2749
+ slowest_hook_ms: Math.max(duration, state.turn_harness.slowest_hook_ms),
2750
+ };
2751
+ }
2752
+
2753
+ /**
2754
+ * Concurrent hook processes can acquire the session lease in a different
2755
+ * order from their clock capture. Preserve raw readings in span state for
2756
+ * pairing, but omit an out-of-order reading from the producer event chain so
2757
+ * a valid global clock is never asserted falsely.
2758
+ */
2759
+ function orderedEventMonotonic(
2760
+ state: HookProducerStateV3,
2761
+ candidate: string | undefined,
2762
+ ): string | undefined {
2763
+ if (!candidate || !/^\d+$/.test(candidate)) return undefined;
2764
+ if (!state.last_monotonic_ns) return candidate;
2765
+ return BigInt(candidate) < BigInt(state.last_monotonic_ns) ? undefined : candidate;
2766
+ }
2767
+
2768
+ function emptyTurnHarnessTiming(): TurnHarnessTimingV3 {
2769
+ return { hook_time_ms: 0, hook_count: 0, slowest_hook_ms: 0 };
2770
+ }
2771
+
2772
+ function validOpenSpanState(value: OpenSpanStateV3 | undefined): boolean {
2773
+ return Boolean(
2774
+ value &&
2775
+ /^span_[0-9a-f-]{36}$/.test(value.span_id) &&
2776
+ (value.parent_span_id === undefined || /^span_[0-9a-f-]{36}$/.test(value.parent_span_id)) &&
2777
+ Number.isFinite(Date.parse(value.opened_at)) &&
2778
+ /^boot_[a-zA-Z0-9._-]+$/.test(value.boot_id) &&
2779
+ (value.opened_monotonic_ns === undefined || /^\d+$/.test(value.opened_monotonic_ns)) &&
2780
+ (value.open_event_id === undefined || /^evt_[0-9a-f-]{36}$/.test(value.open_event_id)),
2781
+ );
2782
+ }
2783
+
2784
+ function validTurnHarnessTiming(value: TurnHarnessTimingV3): boolean {
2785
+ return (
2786
+ Number.isSafeInteger(value.hook_time_ms) &&
2787
+ value.hook_time_ms >= 0 &&
2788
+ Number.isSafeInteger(value.hook_count) &&
2789
+ value.hook_count >= 0 &&
2790
+ Number.isSafeInteger(value.slowest_hook_ms) &&
2791
+ value.slowest_hook_ms >= 0 &&
2792
+ (value.slowest_hook === undefined ||
2793
+ /^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,127}$/.test(value.slowest_hook))
2794
+ );
2795
+ }
2796
+
2797
+ function pidIsAlive(pid: number): boolean {
2798
+ if (!Number.isSafeInteger(pid) || pid < 1) return false;
2799
+ try {
2800
+ process.kill(pid, 0);
2801
+ return true;
2802
+ } catch (error) {
2803
+ return (error as NodeJS.ErrnoException).code !== "ESRCH";
2804
+ }
2805
+ }