harnery 0.42.0 → 0.44.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 (343) hide show
  1. package/README.md +3 -3
  2. package/dist/commander.d.ts +16 -0
  3. package/dist/commander.d.ts.map +1 -1
  4. package/dist/commander.js +18 -0
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +95 -52
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +38 -11
  9. package/dist/commands/diagnostics.d.ts.map +1 -1
  10. package/dist/commands/diagnostics.js +10 -0
  11. package/dist/commands/doctor.d.ts +12 -0
  12. package/dist/commands/doctor.d.ts.map +1 -1
  13. package/dist/commands/doctor.js +22 -7
  14. package/dist/commands/governor.js +22 -10
  15. package/dist/commands/inbox.d.ts +1 -1
  16. package/dist/commands/inbox.d.ts.map +1 -1
  17. package/dist/commands/inbox.js +55 -2
  18. package/dist/commands/rm.d.ts.map +1 -1
  19. package/dist/commands/rm.js +28 -4
  20. package/dist/commands/servers.d.ts.map +1 -1
  21. package/dist/commands/servers.js +16 -1
  22. package/dist/commands/supervisor.d.ts.map +1 -1
  23. package/dist/commands/supervisor.js +7 -2
  24. package/dist/commands/toc.d.ts +14 -0
  25. package/dist/commands/toc.d.ts.map +1 -1
  26. package/dist/commands/toc.js +7 -4
  27. package/dist/commands/tunnel.d.ts +65 -0
  28. package/dist/commands/tunnel.d.ts.map +1 -1
  29. package/dist/commands/tunnel.js +290 -101
  30. package/dist/commands/web-fetch.d.ts +3 -2
  31. package/dist/commands/web-fetch.d.ts.map +1 -1
  32. package/dist/commands/web-fetch.js +25 -19
  33. package/dist/commands/web.d.ts +7 -1
  34. package/dist/commands/web.d.ts.map +1 -1
  35. package/dist/commands/web.js +9 -3
  36. package/dist/commands/workflow.js +24 -30
  37. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  38. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  39. package/dist/core/agents/live-authority-v3.js +11 -0
  40. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  41. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  42. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  43. package/dist/core/agents/mailbox.d.ts +9 -8
  44. package/dist/core/agents/mailbox.d.ts.map +1 -1
  45. package/dist/core/agents/mailbox.js +24 -17
  46. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  47. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  48. package/dist/core/agents/render/prompt-context.js +19 -17
  49. package/dist/core/agents/render/session-context.d.ts +2 -8
  50. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  51. package/dist/core/agents/render/session-context.js +64 -43
  52. package/dist/core/agents/rules/stop-hook.js +3 -3
  53. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  54. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  55. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  56. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  57. package/dist/core/agents/state/names.d.ts +43 -3
  58. package/dist/core/agents/state/names.d.ts.map +1 -1
  59. package/dist/core/agents/state/names.js +87 -3
  60. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  61. package/dist/core/agents/state/pidmap.js +5 -0
  62. package/dist/core/agents/state/proc-start.d.ts +19 -0
  63. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  64. package/dist/core/agents/state/proc-start.js +41 -0
  65. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  66. package/dist/core/agents/state/stale-sweep.js +180 -70
  67. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  68. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  69. package/dist/core/agents/state/unchanged-file.js +390 -0
  70. package/dist/core/context/index.d.ts +22 -10
  71. package/dist/core/context/index.d.ts.map +1 -1
  72. package/dist/core/context/index.js +381 -119
  73. package/dist/core/context/observation.d.ts +26 -0
  74. package/dist/core/context/observation.d.ts.map +1 -0
  75. package/dist/core/context/observation.js +11 -0
  76. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  77. package/dist/core/diagnostics/bundle.js +25 -14
  78. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  79. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  80. package/dist/core/diagnostics/path-identity.js +12 -0
  81. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  82. package/dist/core/diagnostics/sanitize.js +4 -3
  83. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  84. package/dist/core/events/v3/authority-outbox.js +29 -0
  85. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  86. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  87. package/dist/core/events/v3/generation-custody.js +50 -0
  88. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  89. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  90. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  91. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  92. package/dist/core/events/v3/producers/recorder.js +77 -64
  93. package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
  94. package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
  95. package/dist/core/governor/planning.d.ts.map +1 -1
  96. package/dist/core/governor/planning.js +14 -1
  97. package/dist/core/governor/service-read.d.ts +10 -2
  98. package/dist/core/governor/service-read.d.ts.map +1 -1
  99. package/dist/core/governor/service-read.js +105 -22
  100. package/dist/core/governor/service.d.ts +20 -2
  101. package/dist/core/governor/service.d.ts.map +1 -1
  102. package/dist/core/governor/service.js +72 -47
  103. package/dist/core/hooks/cli.js +120 -23
  104. package/dist/core/hooks/fork-lineage.d.ts +40 -0
  105. package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
  106. package/dist/core/hooks/fork-lineage.js +62 -0
  107. package/dist/core/hooks/resolve/transcript.d.ts +16 -0
  108. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  109. package/dist/core/hooks/resolve/transcript.js +84 -0
  110. package/dist/core/hooks/shell-rm.d.ts +2 -0
  111. package/dist/core/hooks/shell-rm.d.ts.map +1 -0
  112. package/dist/core/hooks/shell-rm.js +105 -0
  113. package/dist/core/hooks/shell-tokens.d.ts +12 -0
  114. package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
  115. package/dist/core/hooks/shell-tokens.js +97 -0
  116. package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
  117. package/dist/core/hooks/shell-waiter.js +3 -93
  118. package/dist/core/inbox/contract.d.ts +2 -1
  119. package/dist/core/inbox/contract.d.ts.map +1 -1
  120. package/dist/core/inbox/index.d.ts +2 -0
  121. package/dist/core/inbox/index.d.ts.map +1 -1
  122. package/dist/core/inbox/index.js +2 -0
  123. package/dist/core/inbox/peer-messages.d.ts +37 -0
  124. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  125. package/dist/core/inbox/peer-messages.js +72 -0
  126. package/dist/core/inbox/service.d.ts +8 -1
  127. package/dist/core/inbox/service.d.ts.map +1 -1
  128. package/dist/core/inbox/service.js +67 -21
  129. package/dist/core/inbox/targets.d.ts +22 -0
  130. package/dist/core/inbox/targets.d.ts.map +1 -0
  131. package/dist/core/inbox/targets.js +108 -0
  132. package/dist/core/inbox/watcher.js +7 -3
  133. package/dist/core/journal/index.d.ts.map +1 -1
  134. package/dist/core/journal/index.js +17 -2
  135. package/dist/core/resources/contract.d.ts +3 -0
  136. package/dist/core/resources/contract.d.ts.map +1 -1
  137. package/dist/core/resources/service-status.d.ts +14 -1
  138. package/dist/core/resources/service-status.d.ts.map +1 -1
  139. package/dist/core/resources/service-status.js +23 -13
  140. package/dist/core/resources/service.d.ts +9 -1
  141. package/dist/core/resources/service.d.ts.map +1 -1
  142. package/dist/core/resources/service.js +47 -27
  143. package/dist/core/resources/status.d.ts +2 -5
  144. package/dist/core/resources/status.d.ts.map +1 -1
  145. package/dist/core/resources/status.js +36 -7
  146. package/dist/core/semantic/contract.d.ts +5 -5
  147. package/dist/core/servers/index.d.ts +24 -13
  148. package/dist/core/servers/index.d.ts.map +1 -1
  149. package/dist/core/servers/index.js +132 -92
  150. package/dist/core/servers/tunnels.d.ts +3 -5
  151. package/dist/core/servers/tunnels.d.ts.map +1 -1
  152. package/dist/core/servers/tunnels.js +3 -9
  153. package/dist/core/storage/process-log.d.ts +12 -0
  154. package/dist/core/storage/process-log.d.ts.map +1 -1
  155. package/dist/core/storage/process-log.js +10 -2
  156. package/dist/core/supervisor/contract.d.ts +12 -0
  157. package/dist/core/supervisor/contract.d.ts.map +1 -1
  158. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  159. package/dist/core/supervisor/explanations.js +2 -1
  160. package/dist/core/supervisor/findings.d.ts.map +1 -1
  161. package/dist/core/supervisor/findings.js +54 -9
  162. package/dist/core/supervisor/service.d.ts +9 -1
  163. package/dist/core/supervisor/service.d.ts.map +1 -1
  164. package/dist/core/supervisor/service.js +59 -29
  165. package/dist/core/supervisor/services.d.ts.map +1 -1
  166. package/dist/core/supervisor/services.js +22 -1
  167. package/dist/core/supervisor/status.d.ts +17 -2
  168. package/dist/core/supervisor/status.d.ts.map +1 -1
  169. package/dist/core/supervisor/status.js +70 -12
  170. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  171. package/dist/core/supervisor/timeline.js +2 -1
  172. package/dist/core/work/state.js +7 -7
  173. package/dist/core/workflow/engine.d.ts.map +1 -1
  174. package/dist/core/workflow/engine.js +105 -32
  175. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  176. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  177. package/dist/core/workflow/failure-evidence.js +114 -0
  178. package/dist/core/workflow/index.d.ts +1 -1
  179. package/dist/core/workflow/index.d.ts.map +1 -1
  180. package/dist/core/workflow/index.js +1 -1
  181. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  182. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  183. package/dist/core/workflow/live-session-v3.js +126 -5
  184. package/dist/core/workflow/proof.d.ts +5 -5
  185. package/dist/core/workflow/proof.d.ts.map +1 -1
  186. package/dist/core/workflow/proof.js +115 -20
  187. package/dist/core/workflow/run-state.d.ts +3 -1
  188. package/dist/core/workflow/run-state.d.ts.map +1 -1
  189. package/dist/core/workflow/run-state.js +31 -27
  190. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  191. package/dist/core/workflow/spawn-claude.js +19 -0
  192. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  193. package/dist/core/workflow/spawn-codex.js +13 -0
  194. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  195. package/dist/core/workflow/spawn-cursor.js +19 -0
  196. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  197. package/dist/core/workflow/spawn-opencode.js +14 -0
  198. package/dist/core/workflow/types.d.ts +64 -17
  199. package/dist/core/workflow/types.d.ts.map +1 -1
  200. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  201. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  202. package/dist/core/workflow/workspaces/index.js +0 -1
  203. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  204. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  205. package/dist/core/workflow/workspaces/leases.js +30 -16
  206. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  207. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  208. package/dist/core/workflow/workspaces/local-git.js +270 -6
  209. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  210. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  211. package/dist/core/workflow/workspaces/ownership.js +194 -0
  212. package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
  213. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  214. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  215. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  216. package/dist/core/workflow/workspaces/recovery.js +21 -13
  217. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  218. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  219. package/dist/core/workflow/workspaces/state.js +21 -1
  220. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  221. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  222. package/dist/lib/browser/windows-chrome.js +262 -73
  223. package/dist/lib/docs-links.d.ts.map +1 -1
  224. package/dist/lib/docs-links.js +45 -2
  225. package/dist/lib/guarded-remove.d.ts +7 -0
  226. package/dist/lib/guarded-remove.d.ts.map +1 -1
  227. package/dist/lib/guarded-remove.js +106 -12
  228. package/dist/lib/identities/assume.d.ts.map +1 -1
  229. package/dist/lib/identities/assume.js +4 -0
  230. package/dist/lib/tunnel/allowlist.d.ts +44 -0
  231. package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
  232. package/dist/lib/tunnel/allowlist.js +204 -0
  233. package/dist/lib/tunnel/current-address.d.ts +41 -0
  234. package/dist/lib/tunnel/current-address.d.ts.map +1 -0
  235. package/dist/lib/tunnel/current-address.js +123 -0
  236. package/dist/lib/tunnel/gate.js +13 -6
  237. package/dist/lib/tunnel/processes.d.ts +39 -0
  238. package/dist/lib/tunnel/processes.d.ts.map +1 -0
  239. package/dist/lib/tunnel/processes.js +121 -0
  240. package/dist/lib/tunnel/state.d.ts +28 -3
  241. package/dist/lib/tunnel/state.d.ts.map +1 -1
  242. package/dist/lib/tunnel/state.js +98 -27
  243. package/dist/lib/zip/index.d.ts +41 -0
  244. package/dist/lib/zip/index.d.ts.map +1 -0
  245. package/dist/lib/zip/index.js +398 -0
  246. package/package.json +8 -1
  247. package/src/commander.ts +32 -0
  248. package/src/commands/agents.ts +151 -74
  249. package/src/commands/browse.ts +50 -14
  250. package/src/commands/diagnostics.ts +10 -0
  251. package/src/commands/doctor.ts +26 -7
  252. package/src/commands/governor.ts +23 -10
  253. package/src/commands/inbox.ts +52 -2
  254. package/src/commands/rm.ts +43 -4
  255. package/src/commands/servers.ts +16 -4
  256. package/src/commands/supervisor.ts +7 -2
  257. package/src/commands/toc.ts +14 -6
  258. package/src/commands/tunnel.ts +394 -116
  259. package/src/commands/web-fetch.ts +30 -19
  260. package/src/commands/web.ts +9 -3
  261. package/src/commands/workflow.ts +30 -30
  262. package/src/core/agents/live-authority-v3.ts +17 -0
  263. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  264. package/src/core/agents/mailbox.ts +26 -17
  265. package/src/core/agents/render/prompt-context.ts +35 -20
  266. package/src/core/agents/render/session-context.ts +92 -48
  267. package/src/core/agents/rules/stop-hook.ts +3 -3
  268. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  269. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  270. package/src/core/agents/state/names.ts +117 -6
  271. package/src/core/agents/state/pidmap.ts +13 -0
  272. package/src/core/agents/state/proc-start.ts +57 -0
  273. package/src/core/agents/state/stale-sweep.ts +247 -82
  274. package/src/core/agents/state/unchanged-file.ts +456 -0
  275. package/src/core/context/index.ts +446 -136
  276. package/src/core/context/observation.ts +29 -0
  277. package/src/core/diagnostics/bundle.ts +23 -18
  278. package/src/core/diagnostics/path-identity.ts +15 -0
  279. package/src/core/diagnostics/sanitize.ts +4 -3
  280. package/src/core/events/v3/authority-outbox.ts +56 -0
  281. package/src/core/events/v3/generation-custody.ts +59 -0
  282. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  283. package/src/core/events/v3/producers/recorder.ts +94 -68
  284. package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
  285. package/src/core/governor/planning.ts +20 -8
  286. package/src/core/governor/service-read.ts +128 -20
  287. package/src/core/governor/service.ts +119 -42
  288. package/src/core/hooks/cli.ts +139 -22
  289. package/src/core/hooks/fork-lineage.ts +82 -0
  290. package/src/core/hooks/resolve/transcript.ts +88 -0
  291. package/src/core/hooks/shell-rm.ts +103 -0
  292. package/src/core/hooks/shell-tokens.ts +96 -0
  293. package/src/core/hooks/shell-waiter.ts +4 -91
  294. package/src/core/inbox/contract.ts +2 -1
  295. package/src/core/inbox/index.ts +2 -0
  296. package/src/core/inbox/peer-messages.ts +94 -0
  297. package/src/core/inbox/service.ts +88 -22
  298. package/src/core/inbox/targets.ts +148 -0
  299. package/src/core/inbox/watcher.ts +8 -9
  300. package/src/core/journal/index.ts +19 -2
  301. package/src/core/resources/contract.ts +4 -0
  302. package/src/core/resources/service-status.ts +35 -12
  303. package/src/core/resources/service.ts +71 -24
  304. package/src/core/resources/status.ts +57 -8
  305. package/src/core/servers/index.ts +177 -91
  306. package/src/core/servers/tunnels.ts +3 -11
  307. package/src/core/storage/process-log.ts +17 -2
  308. package/src/core/supervisor/contract.ts +13 -0
  309. package/src/core/supervisor/explanations.ts +4 -1
  310. package/src/core/supervisor/findings.ts +68 -10
  311. package/src/core/supervisor/service.ts +88 -26
  312. package/src/core/supervisor/services.ts +26 -8
  313. package/src/core/supervisor/status.ts +89 -12
  314. package/src/core/supervisor/timeline.ts +2 -1
  315. package/src/core/work/state.ts +7 -7
  316. package/src/core/workflow/engine.ts +131 -49
  317. package/src/core/workflow/failure-evidence.ts +141 -0
  318. package/src/core/workflow/index.ts +0 -1
  319. package/src/core/workflow/live-session-v3.ts +146 -5
  320. package/src/core/workflow/proof.ts +134 -26
  321. package/src/core/workflow/run-state.ts +31 -29
  322. package/src/core/workflow/spawn-claude.ts +18 -0
  323. package/src/core/workflow/spawn-codex.ts +13 -0
  324. package/src/core/workflow/spawn-cursor.ts +18 -0
  325. package/src/core/workflow/spawn-opencode.ts +14 -0
  326. package/src/core/workflow/types.ts +51 -17
  327. package/src/core/workflow/workspaces/index.ts +0 -1
  328. package/src/core/workflow/workspaces/leases.ts +48 -17
  329. package/src/core/workflow/workspaces/local-git.ts +315 -5
  330. package/src/core/workflow/workspaces/ownership.ts +232 -0
  331. package/src/core/workflow/workspaces/reclaim.ts +58 -0
  332. package/src/core/workflow/workspaces/recovery.ts +25 -13
  333. package/src/core/workflow/workspaces/state.ts +20 -0
  334. package/src/lib/browser/windows-chrome.ts +346 -86
  335. package/src/lib/docs-links.ts +49 -2
  336. package/src/lib/guarded-remove.ts +132 -14
  337. package/src/lib/identities/assume.ts +4 -0
  338. package/src/lib/tunnel/allowlist.ts +208 -0
  339. package/src/lib/tunnel/current-address.ts +158 -0
  340. package/src/lib/tunnel/gate.ts +16 -8
  341. package/src/lib/tunnel/processes.ts +149 -0
  342. package/src/lib/tunnel/state.ts +123 -27
  343. package/src/lib/zip/index.ts +483 -0
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Repository observation contract shared by context recovery and workflow proof.
3
+ *
4
+ * This module has no runtime imports so read-only consumers (the Codec
5
+ * dashboard, proof readers) can use it without reaching the Git-running
6
+ * snapshot code in ./index.ts.
7
+ */
8
+
9
+ export type RepoObservation = "ok" | "unavailable" | "partial";
10
+
11
+ export interface RepoSnapshot {
12
+ cwd: string;
13
+ observation: RepoObservation;
14
+ /** Bounded explanation of an unavailable or incomplete observation. */
15
+ observation_detail?: string;
16
+ root?: string;
17
+ branch?: string;
18
+ head?: string;
19
+ dirty_paths: string[];
20
+ dirty_paths_truncated?: boolean;
21
+ }
22
+
23
+ /** Only explicitly successful observations can establish repository drift. */
24
+ export function observationsComparable(
25
+ a: { observation?: string },
26
+ b: { observation?: string },
27
+ ): boolean {
28
+ return a.observation === "ok" && b.observation === "ok";
29
+ }
@@ -134,12 +134,12 @@ export function captureDiagnosticBundle(
134
134
  const aggregateStats: SanitizationStats = { sanitized_value_count: 0, omitted_value_count: 0 };
135
135
  const sources = [
136
136
  ...SOURCE_SPECS.map(([sourceKind, relativePath]) =>
137
- readCapturedSource(repoRoot, sourceKind, relativePath, aggregateStats),
137
+ readCapturedSource(repoRoot, sourceKind, relativePath),
138
138
  ),
139
- readShadowAdmissionSource(repoRoot, capturedAt, aggregateStats),
139
+ readShadowAdmissionSource(repoRoot, capturedAt),
140
140
  ];
141
141
  const selection = diagnosticSelection(input, sources, now);
142
- const observations: DiagnosticObservations = {
142
+ const rawObservations: DiagnosticObservations = {
143
143
  schema_version: DIAGNOSTIC_INPUT_SCHEMA_VERSION,
144
144
  captured_at: capturedAt,
145
145
  selection,
@@ -156,7 +156,23 @@ export function captureDiagnosticBundle(
156
156
  pressure_policy: PRESSURE_POLICY,
157
157
  },
158
158
  };
159
- const expected = deriveCapturedExpected(observations, thresholds);
159
+ // Freeze the producer's actual output before sanitization. The replay must
160
+ // independently preserve that output, rather than share a transformed oracle.
161
+ const rawExpected = deriveCapturedExpected(rawObservations, thresholds);
162
+ const sanitizedExpected = sanitizeDiagnosticValue(JSON.parse(JSON.stringify(rawExpected)));
163
+ aggregateStats.sanitized_value_count += sanitizedExpected.stats.sanitized_value_count;
164
+ aggregateStats.omitted_value_count += sanitizedExpected.stats.omitted_value_count;
165
+ const observations: DiagnosticObservations = {
166
+ ...rawObservations,
167
+ sources: rawObservations.sources.map((source) => {
168
+ if (source.value === undefined) return source;
169
+ const sanitized = sanitizeDiagnosticValue(source.value);
170
+ aggregateStats.sanitized_value_count += sanitized.stats.sanitized_value_count;
171
+ aggregateStats.omitted_value_count += sanitized.stats.omitted_value_count;
172
+ return { ...source, value: sanitized.value };
173
+ }),
174
+ };
175
+ const expected = sanitizedExpected.value as typeof rawExpected;
160
176
  if (input.findingId && !expected.findings.some((finding) => finding.id === input.findingId)) {
161
177
  throw new Error(
162
178
  `finding "${input.findingId}" is not present in the captured findings projection`,
@@ -446,7 +462,6 @@ function readCapturedSource(
446
462
  repoRoot: string,
447
463
  sourceKind: string,
448
464
  relativePath: string,
449
- aggregateStats: SanitizationStats,
450
465
  ): DiagnosticCapturedSource {
451
466
  const path = join(repoRoot, relativePath);
452
467
  if (!existsSync(path)) {
@@ -462,10 +477,7 @@ function readCapturedSource(
462
477
  : (() => {
463
478
  throw new Error("source_is_not_regular_file");
464
479
  })();
465
- const sanitized = sanitizeDiagnosticValue(parsed);
466
- aggregateStats.sanitized_value_count += sanitized.stats.sanitized_value_count;
467
- aggregateStats.omitted_value_count += sanitized.stats.omitted_value_count;
468
- const value = sanitized.value;
480
+ const value = parsed;
469
481
  const record = object(value);
470
482
  return {
471
483
  source_kind: sourceKind,
@@ -483,11 +495,7 @@ function readCapturedSource(
483
495
  }
484
496
  }
485
497
 
486
- function readShadowAdmissionSource(
487
- repoRoot: string,
488
- capturedAt: string,
489
- aggregateStats: SanitizationStats,
490
- ): DiagnosticCapturedSource {
498
+ function readShadowAdmissionSource(repoRoot: string, capturedAt: string): DiagnosticCapturedSource {
491
499
  const sourceKind = "workflow.diagnostic-admission";
492
500
  const workflowsRoot = join(repoRoot, ".harnery", "workflows");
493
501
  if (!existsSync(workflowsRoot)) {
@@ -545,15 +553,12 @@ function readShadowAdmissionSource(
545
553
  invalid_run_count: invalidCount,
546
554
  records,
547
555
  };
548
- const sanitized = sanitizeDiagnosticValue(projection);
549
- aggregateStats.sanitized_value_count += sanitized.stats.sanitized_value_count;
550
- aggregateStats.omitted_value_count += sanitized.stats.omitted_value_count;
551
556
  return {
552
557
  source_kind: sourceKind,
553
558
  capability: omittedCount > 0 || invalidCount > 0 ? "partial" : "supported",
554
559
  schema_version: 1,
555
560
  observed_at: records[0]?.ended_at as string | undefined,
556
- value: sanitized.value,
561
+ value: projection,
557
562
  ...(omittedCount > 0
558
563
  ? { reason_code: "bounded_workflow_window_truncated" }
559
564
  : invalidCount > 0
@@ -0,0 +1,15 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ const HOME_PATH =
4
+ /(?:[A-Za-z]:[\\/]Users[\\/](?:[^\\/\r\n\0:"<>|]+(?=[\\/])|[^\\/\s\0:]+)|\/home\/[^/\s\0:]+|\/Users\/[^/\s\0:]+)/g;
5
+
6
+ /** One stable representation for private home paths in values and identity inputs. */
7
+ export function canonicalizeDiagnosticIdentity(value: string): string {
8
+ return value.replace(HOME_PATH, (home) => {
9
+ const digest = createHash("sha256")
10
+ .update("harnery-diagnostic-home-v1\0")
11
+ .update(home)
12
+ .digest("hex");
13
+ return `[REDACTED:home:${digest}]`;
14
+ });
15
+ }
@@ -2,7 +2,6 @@ const SENSITIVE_KEY =
2
2
  /(?:authorization|cookie|credential|password|passwd|private[_-]?key|secret|session[_-]?token|access[_-]?token|refresh[_-]?token|api[_-]?key)/i;
3
3
  const OMITTED_CONTENT_KEY =
4
4
  /(?:^|[_-])(?:environment|env|prompt|prompts|tool[_-]?payload|tool[_-]?result|transcript|transcripts)(?:$|[_-])/i;
5
- const HOME_PATH = /(?:[A-Za-z]:\\Users\\[^\\\s]+|\/home\/[^/\s]+|\/Users\/[^/\s]+)/g;
6
5
  const BEARER_VALUE = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
7
6
  const URL_CREDENTIALS = /([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/gi;
8
7
  const PRIVATE_KEY = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g;
@@ -76,7 +75,8 @@ function sanitize(value: unknown, stats: SanitizationStats, depth: number): unkn
76
75
  }
77
76
 
78
77
  function sanitizeString(value: string, stats: SanitizationStats): string {
79
- let next = value;
78
+ let next = canonicalizeDiagnosticIdentity(value);
79
+ if (next !== value) stats.sanitized_value_count += 1;
80
80
  const replacements: Array<[RegExp, string | ((...args: string[]) => string)]> = [
81
81
  [PRIVATE_KEY, "[REDACTED:private-key]"],
82
82
  [BEARER_VALUE, "Bearer [REDACTED:token]"],
@@ -86,7 +86,6 @@ function sanitizeString(value: string, stats: SanitizationStats): string {
86
86
  [SENSITIVE_ENV, "$1[REDACTED]"],
87
87
  [SENSITIVE_ARGUMENT, "$1[REDACTED]"],
88
88
  [URL_CREDENTIALS, (_match, scheme: string) => `${scheme}[REDACTED]@`],
89
- [HOME_PATH, "[REDACTED:home]"],
90
89
  ];
91
90
  for (const [pattern, replacement] of replacements) {
92
91
  const replaced = next.replace(pattern, replacement as never);
@@ -95,3 +94,5 @@ function sanitizeString(value: string, stats: SanitizationStats): string {
95
94
  }
96
95
  return next;
97
96
  }
97
+
98
+ import { canonicalizeDiagnosticIdentity } from "./path-identity.ts";
@@ -16,6 +16,7 @@ import { fsyncParentDirectory } from "../../workflow/durable-record.ts";
16
16
  import { canonicalJsonV3, sha256V3 } from "./canonical.ts";
17
17
  import type { EventV3 } from "./contract.ts";
18
18
  import { liveGenesisIdV3 } from "./control.ts";
19
+ import { assertCurrentGenerationV3, withGenerationCustodyV3 } from "./generation-custody.ts";
19
20
  import { assertEventV3 } from "./validate.ts";
20
21
  import { ensureEventV3Layout, eventV3Paths, writeEventV3 } from "./writer.ts";
21
22
 
@@ -154,9 +155,32 @@ export function publishAuthorityTransactionV3(
154
155
  coordRoot: string,
155
156
  transaction: AuthorityTransactionV3,
156
157
  fence: AuthorityEpochFenceV3 = {},
158
+ ): AuthorityTransactionV3 {
159
+ return withGenerationCustodyV3(
160
+ coordRoot,
161
+ [transaction.actor_instance_id, transaction.subject_instance_id],
162
+ () => publishAuthorityTransactionWithCustodyV3(coordRoot, transaction, fence),
163
+ );
164
+ }
165
+
166
+ function publishAuthorityTransactionWithCustodyV3(
167
+ coordRoot: string,
168
+ transaction: AuthorityTransactionV3,
169
+ fence: AuthorityEpochFenceV3,
157
170
  ): AuthorityTransactionV3 {
158
171
  validateTransaction(transaction);
159
172
  const paths = ensureEventV3Layout(coordRoot);
173
+ const committedPath = authorityCommittedPath(paths.authorityOutbox, transaction.transaction_id);
174
+ if (existsSync(committedPath)) {
175
+ if (
176
+ readAuthorityReceiptV3(committedPath).transaction_digest !==
177
+ sha256V3(`${canonicalJsonV3(transaction)}\n`)
178
+ ) {
179
+ throw new Error("authority committed transaction conflicts with this transaction");
180
+ }
181
+ return transaction;
182
+ }
183
+ assertTransactionGeneration(coordRoot, transaction);
160
184
  if (fence.expectedGenesisId && liveGenesisIdV3(coordRoot) !== fence.expectedGenesisId) {
161
185
  throw new Error("authority transaction epoch was replaced before publication");
162
186
  }
@@ -185,6 +209,26 @@ export function reconcileAuthorityTransactionV3(
185
209
  transactionId: string,
186
210
  reconciler: AuthorityReconcilerV3,
187
211
  fence: AuthorityEpochFenceV3 = {},
212
+ ): AuthorityReceiptV3 {
213
+ assertTransactionId(transactionId);
214
+ const paths = ensureEventV3Layout(coordRoot);
215
+ const committedPath = authorityCommittedPath(paths.authorityOutbox, transactionId);
216
+ if (existsSync(committedPath)) return readAuthorityReceiptV3(committedPath);
217
+ const transaction = readAuthorityTransactionV3(
218
+ authorityReadyPath(paths.authorityOutbox, transactionId),
219
+ );
220
+ return withGenerationCustodyV3(
221
+ coordRoot,
222
+ [transaction.actor_instance_id, transaction.subject_instance_id],
223
+ () => reconcileAuthorityTransactionWithCustodyV3(coordRoot, transactionId, reconciler, fence),
224
+ );
225
+ }
226
+
227
+ function reconcileAuthorityTransactionWithCustodyV3(
228
+ coordRoot: string,
229
+ transactionId: string,
230
+ reconciler: AuthorityReconcilerV3,
231
+ fence: AuthorityEpochFenceV3,
188
232
  ): AuthorityReceiptV3 {
189
233
  assertTransactionId(transactionId);
190
234
  const paths = ensureEventV3Layout(coordRoot);
@@ -192,8 +236,10 @@ export function reconcileAuthorityTransactionV3(
192
236
  if (existsSync(committedPath)) return readAuthorityReceiptV3(committedPath);
193
237
  const readyPath = authorityReadyPath(paths.authorityOutbox, transactionId);
194
238
  const transaction = readAuthorityTransactionV3(readyPath);
239
+ assertTransactionGeneration(coordRoot, transaction);
195
240
  const current = reconciler.readStateDigest();
196
241
  if (current === transaction.expected_prior_state_digest) {
242
+ assertTransactionGeneration(coordRoot, transaction);
197
243
  reconciler.apply(transaction.mutation, transaction.transaction_id);
198
244
  } else if (current !== transaction.desired_state_digest) {
199
245
  throw new Error("authority transaction prior state conflicts with current state");
@@ -201,6 +247,7 @@ export function reconcileAuthorityTransactionV3(
201
247
  if (reconciler.readStateDigest() !== transaction.desired_state_digest) {
202
248
  throw new Error("authority transaction did not reach its declared desired state");
203
249
  }
250
+ assertTransactionGeneration(coordRoot, transaction);
204
251
  const event = JSON.parse(transaction.event_row) as unknown;
205
252
  assertEventV3(event);
206
253
  const durability = writeEventV3(coordRoot, event, {
@@ -230,6 +277,15 @@ export function reconcileAuthorityTransactionV3(
230
277
  return receipt;
231
278
  }
232
279
 
280
+ function assertTransactionGeneration(coordRoot: string, transaction: AuthorityTransactionV3): void {
281
+ const event = JSON.parse(transaction.event_row) as EventV3;
282
+ assertCurrentGenerationV3(
283
+ coordRoot,
284
+ transaction.actor_instance_id,
285
+ "generation_id" in event.scope ? event.scope.generation_id : undefined,
286
+ );
287
+ }
288
+
233
289
  export function listPendingAuthorityTransactionsV3(coordRoot: string): AuthorityTransactionV3[] {
234
290
  const outbox = eventV3Paths(coordRoot).authorityOutbox;
235
291
  if (!existsSync(outbox)) return [];
@@ -0,0 +1,59 @@
1
+ import { createHash } from "node:crypto";
2
+ import { hostname } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { acquireNoClobberLease } from "../../workflow/workspaces/leases.ts";
5
+ import { readHookProducerStateByInstanceV3 } from "./producers/recorder.ts";
6
+
7
+ const held = new Set<string>();
8
+
9
+ /** Serialize synchronous authority work with generation changes for these instances only. */
10
+ export function withGenerationCustodyV3<T>(
11
+ coordRoot: string,
12
+ instanceIds: readonly string[],
13
+ operation: () => T,
14
+ ): T {
15
+ const ids = [...new Set(instanceIds)].sort();
16
+ for (const id of ids) {
17
+ if (!/^inst_[a-zA-Z0-9._-]{1,128}$/.test(id)) {
18
+ throw new Error("generation custody instance is invalid");
19
+ }
20
+ }
21
+ const enter = (index: number): T => {
22
+ const id = ids[index];
23
+ if (id === undefined) return operation();
24
+ const key = `${resolve(coordRoot)}\0${id}`;
25
+ if (held.has(key)) return enter(index + 1);
26
+ const authority = createHash("sha256").update(key).digest("hex");
27
+ const lease = acquireNoClobberLease({
28
+ // Custody spans epoch rotation; the ledger directory itself is archived.
29
+ path: join(coordRoot, ".harnery", "private", "event-v3-generation-custody", authority),
30
+ scope: "event-v3-generation-custody",
31
+ authoritySha256: authority,
32
+ // Expiry never permits stealing a live/unknown owner. A dead local process
33
+ // can be recovered immediately, including after a cache-handoff crash.
34
+ staleAfterMs: 1,
35
+ validateStaleOwner: (owner) =>
36
+ owner.host === hostname() && owner.authority_sha256 === authority,
37
+ });
38
+ held.add(key);
39
+ try {
40
+ return enter(index + 1);
41
+ } finally {
42
+ held.delete(key);
43
+ lease.release();
44
+ }
45
+ };
46
+ return enter(0);
47
+ }
48
+
49
+ /** The event already durably binds the generation; replay must not rebind its actor. */
50
+ export function assertCurrentGenerationV3(
51
+ coordRoot: string,
52
+ instanceId: string,
53
+ generationId: string | undefined,
54
+ ): void {
55
+ const current = readHookProducerStateByInstanceV3(coordRoot, instanceId as `inst_${string}`);
56
+ if (!generationId || !current || current.generation_id !== generationId) {
57
+ throw new Error("authority_generation_changed");
58
+ }
59
+ }
@@ -33,6 +33,7 @@ import { markObservedClockRegressionV3 } from "../clock-order.ts";
33
33
  import type { EventV3 } from "../contract.ts";
34
34
  import { type EventV3WriteMode, readEventV3ControlState } from "../control.ts";
35
35
  import { fingerprintContextV3 } from "../fingerprint-keys.ts";
36
+ import { assertCurrentGenerationV3, withGenerationCustodyV3 } from "../generation-custody.ts";
36
37
  import { clockIdV3, spanIdV3 } from "../ids.ts";
37
38
  import { closeSpanStateV3, type OpenSpanStateV3, openSpanStateV3 } from "../span-state.ts";
38
39
  import { EVENT_V3_LEDGER_RELATIVE_ROOT } from "../writer.ts";
@@ -103,6 +104,7 @@ export interface RecordCoordinationAuthorityV3Input<
103
104
  bridge?: "codex-wsl";
104
105
  expected_prior_state_digest: `sha256:${string}`;
105
106
  desired_state_digest: `sha256:${string}`;
107
+ expected_generation_id?: `gen_${string}`;
106
108
  monotonic_ns?: string;
107
109
  observed_at?: string;
108
110
  reconciler: AuthorityReconcilerV3;
@@ -129,6 +131,16 @@ export type RecordCoordinationAuthorityV3Result =
129
131
  */
130
132
  export function recordCoordinationAuthorityV3<S extends CoordinationAuthoritySignalV3>(
131
133
  input: RecordCoordinationAuthorityV3Input<S>,
134
+ ): RecordCoordinationAuthorityV3Result {
135
+ return withGenerationCustodyV3(
136
+ input.coordRoot,
137
+ [input.actor_instance_id, input.subject_instance_id],
138
+ () => recordCoordinationAuthorityWithCustodyV3(input),
139
+ );
140
+ }
141
+
142
+ function recordCoordinationAuthorityWithCustodyV3<S extends CoordinationAuthoritySignalV3>(
143
+ input: RecordCoordinationAuthorityV3Input<S>,
132
144
  ): RecordCoordinationAuthorityV3Result {
133
145
  const control = readEventV3ControlState(input.coordRoot);
134
146
  if (control.state !== input.mode) return { state: "gate_closed", reason: control.state };
@@ -141,6 +153,8 @@ export function recordCoordinationAuthorityV3<S extends CoordinationAuthoritySig
141
153
  if (
142
154
  !hook ||
143
155
  hook.terminal ||
156
+ (input.expected_generation_id !== undefined &&
157
+ hook.generation_id !== input.expected_generation_id) ||
144
158
  hook.instance_id !== input.actor_instance_id ||
145
159
  !hook.last_event_id
146
160
  ) {
@@ -328,6 +342,7 @@ export function recordCoordinationAuthorityV3<S extends CoordinationAuthoritySig
328
342
  mutation: normalized.mutation,
329
343
  event: normalized.event,
330
344
  });
345
+ assertCurrentGenerationV3(input.coordRoot, state.actor_instance_id, state.generation_id);
331
346
  if (input.signal === "wait-ended" && waitId) delete state.open_waits[waitId];
332
347
  state.pending = { source_id: sourceId, transaction };
333
348
  publishCoordinationState(path, state);
@@ -43,6 +43,7 @@ import { markObservedClockRegressionV3 } from "../clock-order.ts";
43
43
  import type { EventV3 } from "../contract.ts";
44
44
  import { type EventV3WriteMode, readEventV3ControlState } from "../control.ts";
45
45
  import { fingerprintContextV3 } from "../fingerprint-keys.ts";
46
+ import { withGenerationCustodyV3 } from "../generation-custody.ts";
46
47
  import {
47
48
  attestationIdV3,
48
49
  clockIdV3,
@@ -658,6 +659,17 @@ function processHookSignalLocked(
658
659
  input: RecordHookSignalV3Input,
659
660
  sessionHash: `hid_${string}`,
660
661
  path: string,
662
+ ): RecordHookSignalV3Result {
663
+ return withGenerationCustodyV3(input.coordRoot, [input.instance_id], () =>
664
+ processHookSignalWithCustodyLocked(control, input, sessionHash, path),
665
+ );
666
+ }
667
+
668
+ function processHookSignalWithCustodyLocked(
669
+ control: OpenHookControlStateV3,
670
+ input: RecordHookSignalV3Input,
671
+ sessionHash: `hid_${string}`,
672
+ path: string,
661
673
  ): RecordHookSignalV3Result {
662
674
  const behavior = adapterBehavior(input.adapter);
663
675
  const rootId = control.genesis.event.scope.root_id as `root_${string}`;
@@ -3209,6 +3221,14 @@ function suppressClosedSpanSignal(
3209
3221
  */
3210
3222
  export function recordApprovedSessionEndV3(
3211
3223
  input: RecordApprovedSessionEndV3Input,
3224
+ ): RecordApprovedSessionEndV3Result {
3225
+ return withGenerationCustodyV3(input.coordRoot, [input.instance_id], () =>
3226
+ recordApprovedSessionEndWithCustodyV3(input),
3227
+ );
3228
+ }
3229
+
3230
+ function recordApprovedSessionEndWithCustodyV3(
3231
+ input: RecordApprovedSessionEndV3Input,
3212
3232
  ): RecordApprovedSessionEndV3Result {
3213
3233
  const control = readEventV3ControlState(input.coordRoot);
3214
3234
  if (control.state !== input.mode) {
@@ -3556,77 +3576,81 @@ export function reanchorArchivedHookProducersV3(
3556
3576
  for (const archived of listArchivedHookProducerStatesV3(input.archivedEpoch)) {
3557
3577
  const previous = archived.state;
3558
3578
  if (previous.terminal || !previous.started_event_id) continue;
3559
- const path = producerStatePath(input.coordRoot, previous.adapter, archived.sessionHash);
3560
- const lease = acquireStateLeaseWithRetry(input.coordRoot, path, 40);
3561
- if (!lease) {
3562
- throw new Error(`event_v3_rotation_reanchor_busy:${previous.instance_id}`);
3563
- }
3564
- try {
3565
- if (existsSync(path)) {
3566
- const current = readProducerState(path);
3567
- if (current.epoch_genesis_id === genesisId) continue;
3579
+ withGenerationCustodyV3(input.coordRoot, [previous.instance_id], () => {
3580
+ const path = producerStatePath(input.coordRoot, previous.adapter, archived.sessionHash);
3581
+ const lease = acquireStateLeaseWithRetry(input.coordRoot, path, 40);
3582
+ if (!lease) {
3583
+ throw new Error(`event_v3_rotation_reanchor_busy:${previous.instance_id}`);
3568
3584
  }
3569
- const sourceEvent = `${previous.adapter}.epoch-rotation-reanchor`;
3570
- const rotationInput: RecordHookSignalV3Input = {
3571
- coordRoot: input.coordRoot,
3572
- mode: input.mode,
3573
- signal: "session-start",
3574
- payload: {
3575
- raw: {},
3576
- ...(previous.current_native_turn_id ? { turn_id: previous.current_native_turn_id } : {}),
3577
- ...(adapterBehavior(previous.adapter).tracksExecutionMode && previous.cursor_mode
3578
- ? { cursor_mode: previous.cursor_mode }
3579
- : {}),
3580
- },
3581
- adapter: previous.adapter,
3582
- instance_id: previous.instance_id,
3583
- producer_id: "prd_agent-hook",
3584
- build_id: input.build_id,
3585
- platform: input.platform,
3586
- observed_at: observedAt,
3587
- writerOptions: { expectedGenesisId: genesisId },
3588
- };
3589
- const state = newProducerState(
3590
- rotationInput,
3591
- previous.session_id,
3592
- epochId,
3593
- boundaryEventId as `evt_${string}`,
3594
- genesisId,
3595
- );
3596
- const sessionStart = buildMidFlightSessionStart(rotationInput, state, rootId);
3597
- if (sessionStart.event_type !== "session.started") {
3598
- throw new Error("rotation re-anchor session start could not be normalized");
3599
- }
3600
- sessionStart.provenance = {
3601
- ...sessionStart.provenance,
3602
- source_event: sourceEvent,
3603
- attestation: "derived",
3604
- confidence: "high",
3605
- attribution: {
3606
- method: "session_env",
3607
- state: "verified",
3608
- subject_instance_id: state.instance_id,
3609
- },
3610
- };
3611
- sessionStart.payload.resume = {
3612
- state: "unknown",
3613
- reason: "epoch_rotation_reanchor",
3614
- };
3615
- assertEventV3(sessionStart);
3616
- commitEventLocked(rotationInput, state, path, sessionStart);
3617
- sessions += 1;
3618
-
3619
- if (previous.current_turn_id && previous.current_turn_span) {
3620
- commitMidFlightTurnStart(rotationInput, state, path, rootId, {
3585
+ try {
3586
+ if (existsSync(path)) {
3587
+ const current = readProducerState(path);
3588
+ if (current.epoch_genesis_id === genesisId) return;
3589
+ }
3590
+ const sourceEvent = `${previous.adapter}.epoch-rotation-reanchor`;
3591
+ const rotationInput: RecordHookSignalV3Input = {
3592
+ coordRoot: input.coordRoot,
3593
+ mode: input.mode,
3594
+ signal: "session-start",
3595
+ payload: {
3596
+ raw: {},
3597
+ ...(previous.current_native_turn_id
3598
+ ? { turn_id: previous.current_native_turn_id }
3599
+ : {}),
3600
+ ...(adapterBehavior(previous.adapter).tracksExecutionMode && previous.cursor_mode
3601
+ ? { cursor_mode: previous.cursor_mode }
3602
+ : {}),
3603
+ },
3604
+ adapter: previous.adapter,
3605
+ instance_id: previous.instance_id,
3606
+ producer_id: "prd_agent-hook",
3607
+ build_id: input.build_id,
3608
+ platform: input.platform,
3609
+ observed_at: observedAt,
3610
+ writerOptions: { expectedGenesisId: genesisId },
3611
+ };
3612
+ const state = newProducerState(
3613
+ rotationInput,
3614
+ previous.session_id,
3615
+ epochId,
3616
+ boundaryEventId as `evt_${string}`,
3617
+ genesisId,
3618
+ );
3619
+ const sessionStart = buildMidFlightSessionStart(rotationInput, state, rootId);
3620
+ if (sessionStart.event_type !== "session.started") {
3621
+ throw new Error("rotation re-anchor session start could not be normalized");
3622
+ }
3623
+ sessionStart.provenance = {
3624
+ ...sessionStart.provenance,
3621
3625
  source_event: sourceEvent,
3626
+ attestation: "derived",
3622
3627
  confidence: "high",
3623
- attribution_method: "session_env",
3624
- });
3625
- turns += 1;
3628
+ attribution: {
3629
+ method: "session_env",
3630
+ state: "verified",
3631
+ subject_instance_id: state.instance_id,
3632
+ },
3633
+ };
3634
+ sessionStart.payload.resume = {
3635
+ state: "unknown",
3636
+ reason: "epoch_rotation_reanchor",
3637
+ };
3638
+ assertEventV3(sessionStart);
3639
+ commitEventLocked(rotationInput, state, path, sessionStart);
3640
+ sessions += 1;
3641
+
3642
+ if (previous.current_turn_id && previous.current_turn_span) {
3643
+ commitMidFlightTurnStart(rotationInput, state, path, rootId, {
3644
+ source_event: sourceEvent,
3645
+ confidence: "high",
3646
+ attribution_method: "session_env",
3647
+ });
3648
+ turns += 1;
3649
+ }
3650
+ } finally {
3651
+ lease.release();
3626
3652
  }
3627
- } finally {
3628
- lease.release();
3629
- }
3653
+ });
3630
3654
  }
3631
3655
  return { sessions, turns };
3632
3656
  }
@@ -3864,7 +3888,9 @@ export function readJoinableHookProducerStateV3(
3864
3888
  instanceId: `inst_${string}`,
3865
3889
  ): HookProducerStateV3 | undefined {
3866
3890
  const direct = readHookProducerStateV3(coordRoot, adapter, nativeSessionId);
3867
- if (direct?.instance_id === instanceId && !direct.terminal) return direct;
3891
+ // A terminal native session belonging to this actor is stale authority,
3892
+ // not permission to adopt a replacement published under the same instance.
3893
+ if (direct?.instance_id === instanceId) return direct.terminal ? undefined : direct;
3868
3894
  const byInstance = readHookProducerStateByInstanceV3(coordRoot, instanceId);
3869
3895
  return byInstance?.adapter === adapter && !byInstance.terminal ? byInstance : undefined;
3870
3896
  }