harnery 0.45.1 → 0.46.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 (332) hide show
  1. package/dist/commander.d.ts +13 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commands/admission.d.ts.map +1 -1
  4. package/dist/commands/admission.js +214 -27
  5. package/dist/commands/agents.d.ts +4 -0
  6. package/dist/commands/agents.d.ts.map +1 -1
  7. package/dist/commands/agents.js +102 -25
  8. package/dist/commands/browse.d.ts +3 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +172 -8
  11. package/dist/commands/fetch.js +1 -0
  12. package/dist/commands/ledger-v3.d.ts.map +1 -1
  13. package/dist/commands/ledger-v3.js +8 -2
  14. package/dist/commands/presence.d.ts.map +1 -1
  15. package/dist/commands/presence.js +40 -0
  16. package/dist/commands/qa-run.d.ts.map +1 -1
  17. package/dist/commands/qa-run.js +4 -3
  18. package/dist/commands/review-pack.d.ts.map +1 -1
  19. package/dist/commands/review-pack.js +33 -6
  20. package/dist/commands/workflow.d.ts.map +1 -1
  21. package/dist/commands/workflow.js +6 -45
  22. package/dist/core/agents/coord-client.d.ts.map +1 -1
  23. package/dist/core/agents/coord-client.js +0 -18
  24. package/dist/core/agents/index.d.ts +1 -0
  25. package/dist/core/agents/index.d.ts.map +1 -1
  26. package/dist/core/agents/index.js +1 -0
  27. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  28. package/dist/core/agents/live-authority-v3.js +11 -4
  29. package/dist/core/agents/render/session-context.d.ts +0 -6
  30. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  31. package/dist/core/agents/render/session-context.js +1 -6
  32. package/dist/core/agents/session-events.d.ts.map +1 -1
  33. package/dist/core/agents/session-events.js +12 -4
  34. package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
  35. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  36. package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
  37. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  38. package/dist/core/agents/state/live-coordination-view.js +6 -2
  39. package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
  40. package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
  41. package/dist/core/agents/state/parent-reference-contract.js +1 -0
  42. package/dist/core/agents/state/parent-reference.d.ts +18 -0
  43. package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
  44. package/dist/core/agents/state/parent-reference.js +119 -0
  45. package/dist/core/agents/state/pidmap.js +2 -2
  46. package/dist/core/agents/state/proc-start.d.ts +20 -31
  47. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  48. package/dist/core/agents/state/proc-start.js +37 -70
  49. package/dist/core/agents/state/service-process.d.ts +61 -0
  50. package/dist/core/agents/state/service-process.d.ts.map +1 -0
  51. package/dist/core/agents/state/service-process.js +143 -0
  52. package/dist/core/config.d.ts +39 -0
  53. package/dist/core/config.d.ts.map +1 -1
  54. package/dist/core/config.js +63 -0
  55. package/dist/core/context/index.d.ts +6 -4
  56. package/dist/core/context/index.d.ts.map +1 -1
  57. package/dist/core/context/index.js +194 -256
  58. package/dist/core/context/observation.d.ts +9 -9
  59. package/dist/core/context/observation.d.ts.map +1 -1
  60. package/dist/core/context/observation.js +37 -6
  61. package/dist/core/diagnostics/path-identity.d.ts +1 -1
  62. package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
  63. package/dist/core/diagnostics/path-identity.js +51 -6
  64. package/dist/core/events/v3/canonical.js +43 -14
  65. package/dist/core/events/v3/index.d.ts +1 -1
  66. package/dist/core/events/v3/index.d.ts.map +1 -1
  67. package/dist/core/events/v3/index.js +1 -1
  68. package/dist/core/events/v3/producers/recorder.d.ts +34 -0
  69. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  70. package/dist/core/events/v3/producers/recorder.js +117 -13
  71. package/dist/core/events/v3/reader.d.ts.map +1 -1
  72. package/dist/core/events/v3/reader.js +53 -13
  73. package/dist/core/events/v3/runtime-build.d.ts +14 -0
  74. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  75. package/dist/core/events/v3/runtime-build.js +44 -0
  76. package/dist/core/events/v3/validate.d.ts.map +1 -1
  77. package/dist/core/events/v3/validate.js +36 -1
  78. package/dist/core/governor/index.d.ts +2 -1
  79. package/dist/core/governor/index.d.ts.map +1 -1
  80. package/dist/core/governor/index.js +2 -1
  81. package/dist/core/governor/service-read.d.ts +3 -10
  82. package/dist/core/governor/service-read.d.ts.map +1 -1
  83. package/dist/core/governor/service-read.js +10 -94
  84. package/dist/core/governor/service.d.ts +3 -7
  85. package/dist/core/governor/service.d.ts.map +1 -1
  86. package/dist/core/governor/service.js +17 -182
  87. package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
  88. package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
  89. package/dist/core/hooks/adapter/native-parent.js +91 -0
  90. package/dist/core/hooks/cli.js +79 -4
  91. package/dist/core/hooks/resolve/owner.d.ts +0 -11
  92. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  93. package/dist/core/hooks/resolve/owner.js +2 -25
  94. package/dist/core/hooks/shell-admission.d.ts +84 -0
  95. package/dist/core/hooks/shell-admission.d.ts.map +1 -0
  96. package/dist/core/hooks/shell-admission.js +622 -0
  97. package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
  98. package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
  99. package/dist/core/hooks/shell-commit-tree.js +104 -0
  100. package/dist/core/inbox/contract.d.ts +12 -0
  101. package/dist/core/inbox/contract.d.ts.map +1 -1
  102. package/dist/core/inbox/contract.js +16 -0
  103. package/dist/core/inbox/peer-messages.d.ts +41 -16
  104. package/dist/core/inbox/peer-messages.d.ts.map +1 -1
  105. package/dist/core/inbox/peer-messages.js +372 -15
  106. package/dist/core/inbox/service.d.ts +8 -1
  107. package/dist/core/inbox/service.d.ts.map +1 -1
  108. package/dist/core/inbox/service.js +83 -3
  109. package/dist/core/inbox/targets.d.ts +17 -3
  110. package/dist/core/inbox/targets.d.ts.map +1 -1
  111. package/dist/core/inbox/targets.js +77 -2
  112. package/dist/core/presence/git.d.ts +18 -12
  113. package/dist/core/presence/git.d.ts.map +1 -1
  114. package/dist/core/presence/git.js +32 -38
  115. package/dist/core/presence/index.d.ts +2 -2
  116. package/dist/core/presence/index.d.ts.map +1 -1
  117. package/dist/core/presence/index.js +8 -6
  118. package/dist/core/presence/mail.d.ts +133 -0
  119. package/dist/core/presence/mail.d.ts.map +1 -0
  120. package/dist/core/presence/mail.js +464 -0
  121. package/dist/core/resources/sampler.js +2 -2
  122. package/dist/core/resources/service-status.d.ts +2 -14
  123. package/dist/core/resources/service-status.d.ts.map +1 -1
  124. package/dist/core/resources/service-status.js +3 -23
  125. package/dist/core/resources/service.d.ts +3 -7
  126. package/dist/core/resources/service.d.ts.map +1 -1
  127. package/dist/core/resources/service.js +12 -101
  128. package/dist/core/resources/status.d.ts.map +1 -1
  129. package/dist/core/resources/status.js +3 -2
  130. package/dist/core/semantic/contract.d.ts +5 -5
  131. package/dist/core/semantic/service.d.ts.map +1 -1
  132. package/dist/core/semantic/service.js +9 -2
  133. package/dist/core/servers/index.d.ts +0 -2
  134. package/dist/core/servers/index.d.ts.map +1 -1
  135. package/dist/core/servers/index.js +3 -18
  136. package/dist/core/storage/modes.d.ts +4 -0
  137. package/dist/core/storage/modes.d.ts.map +1 -1
  138. package/dist/core/storage/modes.js +4 -0
  139. package/dist/core/storage/segments.d.ts.map +1 -1
  140. package/dist/core/storage/segments.js +5 -4
  141. package/dist/core/supervisor/service.d.ts +4 -7
  142. package/dist/core/supervisor/service.d.ts.map +1 -1
  143. package/dist/core/supervisor/service.js +23 -122
  144. package/dist/core/supervisor/services.js +2 -2
  145. package/dist/core/supervisor/status.d.ts +3 -17
  146. package/dist/core/supervisor/status.d.ts.map +1 -1
  147. package/dist/core/supervisor/status.js +7 -70
  148. package/dist/core/workflow/engine.js +2 -1
  149. package/dist/core/workflow/index.d.ts +3 -4
  150. package/dist/core/workflow/index.d.ts.map +1 -1
  151. package/dist/core/workflow/index.js +2 -2
  152. package/dist/core/workflow/proof.d.ts.map +1 -1
  153. package/dist/core/workflow/proof.js +30 -74
  154. package/dist/core/workflow/resume-lock.d.ts +17 -0
  155. package/dist/core/workflow/resume-lock.d.ts.map +1 -0
  156. package/dist/core/workflow/resume-lock.js +158 -0
  157. package/dist/core/workflow/run-state.d.ts +0 -21
  158. package/dist/core/workflow/run-state.d.ts.map +1 -1
  159. package/dist/core/workflow/run-state.js +3 -174
  160. package/dist/core/workflow/workspaces/leases.d.ts +4 -0
  161. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  162. package/dist/core/workflow/workspaces/leases.js +1 -1
  163. package/dist/core/workflow/workspaces/local-git.js +91 -210
  164. package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
  165. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
  166. package/dist/core/workflow/workspaces/ownership.js +3 -130
  167. package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
  168. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  169. package/dist/core/workflow/workspaces/reclaim.js +0 -4
  170. package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
  171. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  172. package/dist/core/workflow/workspaces/recovery.js +1 -1
  173. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  174. package/dist/core/workflow/workspaces/state.js +1 -4
  175. package/dist/lib/admission-history.d.ts +96 -0
  176. package/dist/lib/admission-history.d.ts.map +1 -0
  177. package/dist/lib/admission-history.js +196 -0
  178. package/dist/lib/admission.d.ts +56 -7
  179. package/dist/lib/admission.d.ts.map +1 -1
  180. package/dist/lib/admission.js +161 -19
  181. package/dist/lib/browser/client.d.ts +18 -0
  182. package/dist/lib/browser/client.d.ts.map +1 -1
  183. package/dist/lib/browser/client.js +100 -12
  184. package/dist/lib/browser/content-checks.d.ts +3 -1
  185. package/dist/lib/browser/content-checks.d.ts.map +1 -1
  186. package/dist/lib/browser/content-checks.js +10 -2
  187. package/dist/lib/browser/critique.d.ts +32 -0
  188. package/dist/lib/browser/critique.d.ts.map +1 -1
  189. package/dist/lib/browser/critique.js +72 -3
  190. package/dist/lib/browser/geometry.d.ts +2 -0
  191. package/dist/lib/browser/geometry.d.ts.map +1 -1
  192. package/dist/lib/browser/geometry.js +61 -15
  193. package/dist/lib/browser/index.d.ts +1 -1
  194. package/dist/lib/browser/index.d.ts.map +1 -1
  195. package/dist/lib/browser/index.js +1 -1
  196. package/dist/lib/browser/page-review-contracts.d.ts +6 -1
  197. package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
  198. package/dist/lib/browser/page-review-contracts.js +6 -1
  199. package/dist/lib/browser/page-review-judge.d.ts +3 -1
  200. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  201. package/dist/lib/browser/page-review-judge.js +41 -11
  202. package/dist/lib/browser/page-review-pack.d.ts +3 -0
  203. package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
  204. package/dist/lib/browser/page-review-pack.js +5 -2
  205. package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
  206. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  207. package/dist/lib/browser/qa-run-contracts.js +11 -4
  208. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  209. package/dist/lib/browser/qa-run.js +73 -16
  210. package/dist/lib/browser/runts.d.ts +4 -2
  211. package/dist/lib/browser/runts.d.ts.map +1 -1
  212. package/dist/lib/browser/runts.js +84 -33
  213. package/dist/lib/browser/windows-chrome.d.ts +14 -0
  214. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  215. package/dist/lib/browser/windows-chrome.js +42 -0
  216. package/dist/lib/browser/windows-login-sync.d.ts +110 -0
  217. package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
  218. package/dist/lib/browser/windows-login-sync.js +171 -0
  219. package/dist/lib/cookies/client.d.ts +30 -5
  220. package/dist/lib/cookies/client.d.ts.map +1 -1
  221. package/dist/lib/cookies/client.js +55 -10
  222. package/dist/lib/cookies/index.d.ts +1 -1
  223. package/dist/lib/cookies/index.d.ts.map +1 -1
  224. package/dist/lib/cookies/index.js +1 -1
  225. package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
  226. package/dist/lib/docs-metadata-sync.js +41 -2
  227. package/dist/lib/durable-job.d.ts +8 -0
  228. package/dist/lib/durable-job.d.ts.map +1 -1
  229. package/dist/lib/durable-job.js +10 -0
  230. package/dist/lib/http/client.d.ts +9 -8
  231. package/dist/lib/http/client.d.ts.map +1 -1
  232. package/dist/lib/http/client.js +44 -2
  233. package/dist/lib/instructions/templates.js +4 -4
  234. package/package.json +2 -2
  235. package/src/commander.ts +13 -0
  236. package/src/commands/admission.ts +272 -29
  237. package/src/commands/agents.ts +132 -27
  238. package/src/commands/browse.ts +208 -6
  239. package/src/commands/fetch.ts +1 -0
  240. package/src/commands/ledger-v3.ts +8 -2
  241. package/src/commands/presence.ts +49 -0
  242. package/src/commands/qa-run.ts +4 -3
  243. package/src/commands/review-pack.ts +41 -5
  244. package/src/commands/workflow.ts +8 -58
  245. package/src/core/agents/coord-client.ts +0 -20
  246. package/src/core/agents/index.ts +6 -0
  247. package/src/core/agents/live-authority-v3.ts +11 -4
  248. package/src/core/agents/render/session-context.ts +1 -7
  249. package/src/core/agents/session-events.ts +18 -3
  250. package/src/core/agents/state/heartbeat-reader.ts +3 -0
  251. package/src/core/agents/state/live-coordination-view.ts +13 -6
  252. package/src/core/agents/state/parent-reference-contract.ts +14 -0
  253. package/src/core/agents/state/parent-reference.ts +179 -0
  254. package/src/core/agents/state/pidmap.ts +2 -2
  255. package/src/core/agents/state/proc-start.ts +41 -69
  256. package/src/core/agents/state/service-process.ts +217 -0
  257. package/src/core/config.ts +85 -0
  258. package/src/core/context/index.ts +207 -272
  259. package/src/core/context/observation.ts +40 -9
  260. package/src/core/diagnostics/path-identity.ts +52 -6
  261. package/src/core/events/v3/canonical.ts +46 -13
  262. package/src/core/events/v3/index.ts +3 -0
  263. package/src/core/events/v3/producers/recorder.ts +151 -13
  264. package/src/core/events/v3/reader.ts +55 -11
  265. package/src/core/events/v3/runtime-build.ts +62 -0
  266. package/src/core/events/v3/validate.ts +42 -1
  267. package/src/core/governor/index.ts +1 -2
  268. package/src/core/governor/service-read.ts +15 -114
  269. package/src/core/governor/service.ts +36 -220
  270. package/src/core/hooks/adapter/native-parent.ts +109 -0
  271. package/src/core/hooks/cli.ts +83 -4
  272. package/src/core/hooks/resolve/owner.ts +2 -25
  273. package/src/core/hooks/shell-admission.ts +634 -0
  274. package/src/core/hooks/shell-commit-tree.ts +102 -0
  275. package/src/core/inbox/contract.ts +18 -0
  276. package/src/core/inbox/peer-messages.ts +501 -15
  277. package/src/core/inbox/service.ts +108 -3
  278. package/src/core/inbox/targets.ts +94 -3
  279. package/src/core/presence/git.ts +45 -37
  280. package/src/core/presence/index.ts +16 -8
  281. package/src/core/presence/mail.ts +620 -0
  282. package/src/core/resources/sampler.ts +3 -2
  283. package/src/core/resources/service-status.ts +6 -35
  284. package/src/core/resources/service.ts +23 -133
  285. package/src/core/resources/status.ts +3 -2
  286. package/src/core/semantic/service.ts +8 -2
  287. package/src/core/servers/index.ts +3 -23
  288. package/src/core/storage/modes.ts +6 -0
  289. package/src/core/storage/segments.ts +5 -4
  290. package/src/core/supervisor/service.ts +33 -155
  291. package/src/core/supervisor/services.ts +2 -2
  292. package/src/core/supervisor/status.ts +10 -85
  293. package/src/core/workflow/engine.ts +1 -1
  294. package/src/core/workflow/index.ts +2 -13
  295. package/src/core/workflow/proof.ts +35 -83
  296. package/src/core/workflow/resume-lock.ts +179 -0
  297. package/src/core/workflow/run-state.ts +3 -207
  298. package/src/core/workflow/workspaces/leases.ts +1 -1
  299. package/src/core/workflow/workspaces/local-git.ts +120 -239
  300. package/src/core/workflow/workspaces/ownership.ts +3 -158
  301. package/src/core/workflow/workspaces/reclaim.ts +3 -9
  302. package/src/core/workflow/workspaces/recovery.ts +1 -1
  303. package/src/core/workflow/workspaces/state.ts +1 -5
  304. package/src/lib/admission-history.ts +267 -0
  305. package/src/lib/admission.ts +217 -19
  306. package/src/lib/browser/client.ts +147 -35
  307. package/src/lib/browser/content-checks.ts +14 -3
  308. package/src/lib/browser/critique.ts +113 -3
  309. package/src/lib/browser/geometry.ts +63 -14
  310. package/src/lib/browser/index.ts +5 -0
  311. package/src/lib/browser/page-review-contracts.ts +6 -1
  312. package/src/lib/browser/page-review-judge.ts +58 -13
  313. package/src/lib/browser/page-review-pack.ts +13 -2
  314. package/src/lib/browser/qa-run-contracts.ts +30 -4
  315. package/src/lib/browser/qa-run.ts +78 -17
  316. package/src/lib/browser/runts.ts +85 -37
  317. package/src/lib/browser/windows-chrome.ts +50 -0
  318. package/src/lib/browser/windows-login-sync.ts +275 -0
  319. package/src/lib/cookies/client.ts +62 -10
  320. package/src/lib/cookies/index.ts +3 -0
  321. package/src/lib/docs-metadata-sync.ts +44 -2
  322. package/src/lib/durable-job.ts +15 -0
  323. package/src/lib/http/client.ts +63 -1
  324. package/src/lib/instructions/templates.ts +4 -4
  325. package/dist/core/agents/state/process-handle.d.ts +0 -26
  326. package/dist/core/agents/state/process-handle.d.ts.map +0 -1
  327. package/dist/core/agents/state/process-handle.js +0 -121
  328. package/dist/core/workflow/recovery-audit.d.ts +0 -30
  329. package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
  330. package/dist/core/workflow/recovery-audit.js +0 -116
  331. package/src/core/agents/state/process-handle.ts +0 -165
  332. package/src/core/workflow/recovery-audit.ts +0 -181
@@ -0,0 +1,620 @@
1
+ /**
2
+ * Cross-machine peer messages (ADR 0222). A ping whose recipient lives on
3
+ * another machine is queued here and published as part of this machine's
4
+ * mail ref, `refs/harnery/mail/<machine>`, on the same origin the presence
5
+ * refs use. Each machine writes only its own ref, so force-pushing it never
6
+ * races another writer.
7
+ *
8
+ * The ref is a parentless commit whose message is one JSON blob:
9
+ * outbox — messages this machine sent that the recipient machine has not
10
+ * acknowledged yet;
11
+ * acks — ids of messages this machine imported from other machines.
12
+ *
13
+ * The recipient machine imports envelopes addressed to it into the ordinary
14
+ * per-instance inbox (keeping the sender's message id, so a re-import is a
15
+ * no-op) and acknowledges them in its own ref. The sender drops an
16
+ * acknowledged message from its outbox; the recipient stops acknowledging
17
+ * once the sender's ref no longer lists it. Delivery is therefore
18
+ * at-least-once into a deduplicating inbox.
19
+ *
20
+ * Local state is one append-only log, `.harnery/mail/log.jsonl`, updated
21
+ * under the durable-history lease so concurrent hooks on one machine agree.
22
+ * Repo access is the trust boundary, exactly as for presence: anyone who can
23
+ * push to origin can publish a mail ref.
24
+ */
25
+
26
+ import { createHash } from "node:crypto";
27
+ import {
28
+ existsSync,
29
+ mkdirSync,
30
+ readFileSync,
31
+ renameSync,
32
+ statSync,
33
+ utimesSync,
34
+ writeFileSync,
35
+ } from "node:fs";
36
+ import { join } from "node:path";
37
+ import { resolveMachineLabel } from "../../lib/machine.ts";
38
+ import { presenceEnabled } from "../config.ts";
39
+ import type { HarneryInboxMessageRecordV1 } from "../inbox/contract.ts";
40
+ import {
41
+ HARNERY_INBOX_RECORD_SCHEMA,
42
+ HarneryInboxCapacityError,
43
+ HarneryInboxMessageConflictError,
44
+ HarneryInboxMessageHistoryMissingError,
45
+ } from "../inbox/contract.ts";
46
+ import type { HarneryInboxService } from "../inbox/service.ts";
47
+ import { readCrashSafeJsonlFile, updateCrashSafeJsonlFile } from "../storage/durable-history.ts";
48
+ import {
49
+ hasOrigin,
50
+ MAIL_REF_PREFIX,
51
+ pushMachineRef,
52
+ readMachineRefs,
53
+ sanitizeRefComponent,
54
+ writePresenceCommit,
55
+ } from "./git.ts";
56
+
57
+ export const HARNERY_MAIL_ENVELOPE_SCHEMA = "harnery.mail-envelope/v1" as const;
58
+ const MAIL_LOG_SCHEMA = "harnery.mail-log/v1" as const;
59
+
60
+ /** An unacknowledged message stops being offered after this long. */
61
+ export const MAIL_TTL_MS = 14 * 24 * 60 * 60_000;
62
+ /** While a message is still unacknowledged, re-push at least this often, so a
63
+ * detached push that failed heals without anyone noticing. */
64
+ const MAIL_KEEPALIVE_SECS = 600;
65
+ const MAX_OUTBOX = 100;
66
+ /** Hook-driven imports run at most this often (the presence fetch is 60s). */
67
+ const IMPORT_INTERVAL_SECS = 30;
68
+ const MAX_LOG_RECORDS = 20_000;
69
+ const MAX_LOG_RECORD_BYTES = 16_384;
70
+
71
+ export interface HarneryMailEnvelopeV1 {
72
+ schema: typeof HARNERY_MAIL_ENVELOPE_SCHEMA;
73
+ message_id: string;
74
+ from_machine: string;
75
+ to_machine: string;
76
+ sender_instance_id: string | null;
77
+ sender_display_name: string;
78
+ recipient_instance_id: string;
79
+ recipient_display_name: string;
80
+ created_at: string;
81
+ body: string;
82
+ body_bytes: number;
83
+ }
84
+
85
+ export interface HarneryMailBlobV1 {
86
+ v: 1;
87
+ machine: string;
88
+ published_at: string;
89
+ outbox: HarneryMailEnvelopeV1[];
90
+ acks: string[];
91
+ }
92
+
93
+ type MailLogRecord =
94
+ | {
95
+ schema: typeof MAIL_LOG_SCHEMA;
96
+ kind: "outbound";
97
+ at: string;
98
+ envelope: HarneryMailEnvelopeV1;
99
+ }
100
+ | { schema: typeof MAIL_LOG_SCHEMA; kind: "delivered"; at: string; message_id: string }
101
+ | { schema: typeof MAIL_LOG_SCHEMA; kind: "expired"; at: string; message_id: string }
102
+ | {
103
+ schema: typeof MAIL_LOG_SCHEMA;
104
+ kind: "imported";
105
+ at: string;
106
+ message_id: string;
107
+ from_machine: string;
108
+ sender_instance_id: string | null;
109
+ sender_display_name: string;
110
+ }
111
+ | { schema: typeof MAIL_LOG_SCHEMA; kind: "released"; at: string; message_id: string };
112
+
113
+ interface MailProjection {
114
+ /** Retained outbound identities, including acknowledged and expired mail. */
115
+ sent: Map<
116
+ string,
117
+ { envelope: HarneryMailEnvelopeV1; delivery_state: HarneryMailQueueReceipt["delivery_state"] }
118
+ >;
119
+ /** Sent, neither acknowledged nor expired, oldest first. */
120
+ outbox: Array<{ at: string; envelope: HarneryMailEnvelopeV1 }>;
121
+ /** Imported and still acknowledged, keyed by message id. */
122
+ acks: Map<string, { at: string; from_machine: string }>;
123
+ /** Every message id this machine has imported, released or not. */
124
+ imported: Set<string>;
125
+ /** Senders seen in imported mail, so a reply resolves after they go idle. */
126
+ senders: Map<string, { name: string; machine: string; at: string }>;
127
+ delivered: number;
128
+ expired: number;
129
+ }
130
+
131
+ /** This machine's mail identity: the same sanitized label as its ref. */
132
+ export function mailMachine(): string {
133
+ return sanitizeRefComponent(resolveMachineLabel());
134
+ }
135
+
136
+ function mailDir(coordRoot: string): string {
137
+ return join(coordRoot, ".harnery", "mail");
138
+ }
139
+
140
+ function logPath(coordRoot: string): string {
141
+ return join(mailDir(coordRoot), "log.jsonl");
142
+ }
143
+
144
+ const LOG_OPTIONS = { max_record_bytes: MAX_LOG_RECORD_BYTES, max_records: MAX_LOG_RECORDS };
145
+
146
+ function stamp(now: Date): string {
147
+ return now.toISOString();
148
+ }
149
+
150
+ function projectMail(records: readonly MailLogRecord[]): MailProjection {
151
+ const sent: MailProjection["sent"] = new Map();
152
+ const outbound = new Map<string, { at: string; envelope: HarneryMailEnvelopeV1 }>();
153
+ const acks = new Map<string, { at: string; from_machine: string }>();
154
+ const imported = new Set<string>();
155
+ const senders = new Map<string, { name: string; machine: string; at: string }>();
156
+ let delivered = 0;
157
+ let expired = 0;
158
+ for (const record of records) {
159
+ if (record?.schema !== MAIL_LOG_SCHEMA) continue;
160
+ switch (record.kind) {
161
+ case "outbound":
162
+ outbound.set(record.envelope.message_id, { at: record.at, envelope: record.envelope });
163
+ sent.set(record.envelope.message_id, {
164
+ envelope: record.envelope,
165
+ delivery_state: "pending",
166
+ });
167
+ break;
168
+ case "delivered":
169
+ if (outbound.delete(record.message_id)) delivered++;
170
+ if (sent.has(record.message_id)) sent.get(record.message_id)!.delivery_state = "delivered";
171
+ break;
172
+ case "expired":
173
+ if (outbound.delete(record.message_id)) expired++;
174
+ if (sent.has(record.message_id)) sent.get(record.message_id)!.delivery_state = "expired";
175
+ break;
176
+ case "imported":
177
+ imported.add(record.message_id);
178
+ acks.set(record.message_id, { at: record.at, from_machine: record.from_machine });
179
+ if (record.sender_instance_id) {
180
+ senders.set(record.sender_instance_id, {
181
+ name: record.sender_display_name,
182
+ machine: record.from_machine,
183
+ at: record.at,
184
+ });
185
+ }
186
+ break;
187
+ case "released":
188
+ acks.delete(record.message_id);
189
+ break;
190
+ }
191
+ }
192
+ return { sent, outbox: [...outbound.values()], acks, imported, senders, delivered, expired };
193
+ }
194
+
195
+ function readMailLog(coordRoot: string): MailLogRecord[] {
196
+ try {
197
+ return readCrashSafeJsonlFile<MailLogRecord>(logPath(coordRoot), LOG_OPTIONS);
198
+ } catch {
199
+ return [];
200
+ }
201
+ }
202
+
203
+ function updateMailLog<R>(
204
+ coordRoot: string,
205
+ update: (projection: MailProjection) => { append?: MailLogRecord[]; result: R },
206
+ ): R {
207
+ mkdirSync(mailDir(coordRoot), { recursive: true });
208
+ return updateCrashSafeJsonlFile<MailLogRecord, R>(logPath(coordRoot), LOG_OPTIONS, (records) =>
209
+ update(projectMail(records)),
210
+ );
211
+ }
212
+
213
+ export interface QueueMailInput {
214
+ messageId: string;
215
+ replayOnly?: boolean;
216
+ toMachine: string;
217
+ senderInstanceId: string | null;
218
+ senderName: string;
219
+ recipientInstanceId: string;
220
+ recipientName: string;
221
+ body: string;
222
+ maxBodyBytes: number;
223
+ now?: Date;
224
+ }
225
+
226
+ export interface HarneryMailQueueReceipt {
227
+ envelope: HarneryMailEnvelopeV1;
228
+ state: "queued" | "duplicate";
229
+ delivery_state: "pending" | "delivered" | "expired";
230
+ synced: true;
231
+ }
232
+
233
+ /** Queue one message for another machine. The caller publishes afterwards. */
234
+ export function queueMail(coordRoot: string, input: QueueMailInput): HarneryMailQueueReceipt {
235
+ if (input.replayOnly !== undefined && typeof input.replayOnly !== "boolean") {
236
+ throw new Error("invalid replayOnly flag");
237
+ }
238
+ if (
239
+ typeof input.messageId !== "string" ||
240
+ !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(input.messageId)
241
+ ) {
242
+ throw new Error("invalid message id");
243
+ }
244
+ const bodyBytes = Buffer.byteLength(input.body);
245
+ if (bodyBytes === 0 || bodyBytes > input.maxBodyBytes) {
246
+ throw new HarneryInboxCapacityError("message_body_limit", bodyBytes, input.maxBodyBytes);
247
+ }
248
+ const now = input.now ?? new Date();
249
+ const envelope: HarneryMailEnvelopeV1 = {
250
+ schema: HARNERY_MAIL_ENVELOPE_SCHEMA,
251
+ message_id: input.messageId,
252
+ from_machine: mailMachine(),
253
+ to_machine: sanitizeRefComponent(input.toMachine),
254
+ sender_instance_id: input.senderInstanceId,
255
+ sender_display_name: input.senderName,
256
+ recipient_instance_id: input.recipientInstanceId,
257
+ recipient_display_name: input.recipientName,
258
+ created_at: stamp(now),
259
+ body: input.body,
260
+ body_bytes: bodyBytes,
261
+ };
262
+ return updateMailLog<HarneryMailQueueReceipt>(coordRoot, (projection) => {
263
+ const existing = projection.sent.get(envelope.message_id);
264
+ if (existing) {
265
+ const fields = [
266
+ "from_machine",
267
+ "to_machine",
268
+ "sender_instance_id",
269
+ "sender_display_name",
270
+ "recipient_instance_id",
271
+ "recipient_display_name",
272
+ "body",
273
+ "body_bytes",
274
+ ] as const;
275
+ if (fields.some((field) => existing.envelope[field] !== envelope[field])) {
276
+ throw new HarneryInboxMessageConflictError(envelope.message_id);
277
+ }
278
+ return {
279
+ result: {
280
+ envelope: existing.envelope,
281
+ state: "duplicate",
282
+ delivery_state: existing.delivery_state,
283
+ synced: true,
284
+ },
285
+ };
286
+ }
287
+ if (input.replayOnly) {
288
+ throw new HarneryInboxMessageHistoryMissingError(envelope.message_id);
289
+ }
290
+ if (projection.outbox.length >= MAX_OUTBOX) {
291
+ throw new HarneryInboxCapacityError(
292
+ "pending_count_limit",
293
+ projection.outbox.length,
294
+ MAX_OUTBOX,
295
+ );
296
+ }
297
+ return {
298
+ append: [{ schema: MAIL_LOG_SCHEMA, kind: "outbound", at: stamp(now), envelope }],
299
+ result: { envelope, state: "queued", delivery_state: "pending", synced: true },
300
+ };
301
+ });
302
+ }
303
+
304
+ export type MailPublishResult =
305
+ | { status: "published"; machine: string; outbox: number; acks: number; sha: string }
306
+ | { status: "skipped"; reason: string }
307
+ | { status: "error"; error: string };
308
+
309
+ interface MailPublishState {
310
+ hash?: string;
311
+ pushed_at?: string;
312
+ empty?: boolean;
313
+ }
314
+
315
+ function statePath(coordRoot: string): string {
316
+ return join(mailDir(coordRoot), "publish-state.json");
317
+ }
318
+
319
+ function readState(coordRoot: string): MailPublishState {
320
+ try {
321
+ return JSON.parse(readFileSync(statePath(coordRoot), "utf8")) as MailPublishState;
322
+ } catch {
323
+ return {};
324
+ }
325
+ }
326
+
327
+ function writeState(coordRoot: string, state: MailPublishState): void {
328
+ try {
329
+ mkdirSync(mailDir(coordRoot), { recursive: true });
330
+ const p = statePath(coordRoot);
331
+ const tmp = `${p}.tmp.${process.pid}`;
332
+ writeFileSync(tmp, JSON.stringify(state), "utf8");
333
+ renameSync(tmp, p);
334
+ } catch {
335
+ /* best-effort: the keepalive re-push covers a lost state write */
336
+ }
337
+ }
338
+
339
+ /** Expire stale outbound mail, then return the blob this machine should publish. */
340
+ function currentBlob(coordRoot: string, now: Date): HarneryMailBlobV1 {
341
+ const projection = updateMailLog(coordRoot, (p) => {
342
+ const cutoff = now.getTime() - MAIL_TTL_MS;
343
+ const stale = p.outbox.filter(({ at }) => Date.parse(at) < cutoff);
344
+ return {
345
+ append: stale.map(({ envelope }) => ({
346
+ schema: MAIL_LOG_SCHEMA,
347
+ kind: "expired" as const,
348
+ at: stamp(now),
349
+ message_id: envelope.message_id,
350
+ })),
351
+ result: { ...p, outbox: p.outbox.filter(({ at }) => Date.parse(at) >= cutoff) },
352
+ };
353
+ });
354
+ return {
355
+ v: 1,
356
+ machine: mailMachine(),
357
+ published_at: stamp(now),
358
+ outbox: projection.outbox.map(({ envelope }) => envelope),
359
+ acks: [...projection.acks.keys()].sort(),
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Publish this machine's mail ref when its content changed, or as a keepalive
365
+ * while messages are still unacknowledged. A machine that never sent or
366
+ * received cross-machine mail never creates the ref.
367
+ */
368
+ export function publishMail(
369
+ coordRoot: string,
370
+ opts: { force?: boolean; sync?: boolean; now?: Date } = {},
371
+ ): MailPublishResult {
372
+ try {
373
+ if (!presenceEnabled(coordRoot)) return { status: "skipped", reason: "disabled" };
374
+ // A machine that never sent or received cross-machine mail has no log;
375
+ // the hook path stops here without spawning git.
376
+ if (!existsSync(logPath(coordRoot))) return { status: "skipped", reason: "no mail" };
377
+ if (!hasOrigin(coordRoot)) return { status: "skipped", reason: "no origin remote" };
378
+ const now = opts.now ?? new Date();
379
+ const blob = currentBlob(coordRoot, now);
380
+ const empty = blob.outbox.length === 0 && blob.acks.length === 0;
381
+ const hash = createHash("sha256")
382
+ .update(JSON.stringify([blob.outbox.map((e) => e.message_id), blob.acks]))
383
+ .digest("hex");
384
+ const state = readState(coordRoot);
385
+ if (!opts.force) {
386
+ if (empty && (state.empty ?? true)) return { status: "skipped", reason: "nothing to carry" };
387
+ const pushedAt = Date.parse(state.pushed_at ?? "");
388
+ const sincePush = Number.isFinite(pushedAt)
389
+ ? (now.getTime() - pushedAt) / 1000
390
+ : Number.POSITIVE_INFINITY;
391
+ if (state.hash === hash && (blob.outbox.length === 0 || sincePush < MAIL_KEEPALIVE_SECS)) {
392
+ return { status: "skipped", reason: "unchanged" };
393
+ }
394
+ }
395
+ const sha = writePresenceCommit(coordRoot, JSON.stringify(blob));
396
+ if (!sha) return { status: "error", error: "commit-tree failed" };
397
+ const push = pushMachineRef(coordRoot, MAIL_REF_PREFIX, blob.machine, sha, {
398
+ sync: opts.sync,
399
+ });
400
+ if (!push.ok) return { status: "error", error: push.error ?? "push failed" };
401
+ writeState(coordRoot, { hash, pushed_at: stamp(now), empty });
402
+ return {
403
+ status: "published",
404
+ machine: blob.machine,
405
+ outbox: blob.outbox.length,
406
+ acks: blob.acks.length,
407
+ sha,
408
+ };
409
+ } catch (e) {
410
+ return { status: "error", error: e instanceof Error ? e.message : String(e) };
411
+ }
412
+ }
413
+
414
+ function parseBlob(machine: string, message: string): HarneryMailBlobV1 | null {
415
+ try {
416
+ const blob = JSON.parse(message) as HarneryMailBlobV1;
417
+ if (blob?.v !== 1 || sanitizeRefComponent(String(blob.machine)) !== machine) return null;
418
+ if (!Number.isFinite(Date.parse(blob.published_at))) return null;
419
+ if (!Array.isArray(blob.outbox) || !Array.isArray(blob.acks)) return null;
420
+ return blob;
421
+ } catch {
422
+ return null;
423
+ }
424
+ }
425
+
426
+ function validEnvelope(envelope: HarneryMailEnvelopeV1, from: string, self: string): boolean {
427
+ return (
428
+ envelope?.schema === HARNERY_MAIL_ENVELOPE_SCHEMA &&
429
+ envelope.from_machine === from &&
430
+ envelope.to_machine === self &&
431
+ typeof envelope.message_id === "string" &&
432
+ typeof envelope.recipient_instance_id === "string" &&
433
+ typeof envelope.body === "string" &&
434
+ Buffer.byteLength(envelope.body) === envelope.body_bytes
435
+ );
436
+ }
437
+
438
+ export interface MailImportResult {
439
+ received: number;
440
+ delivered: number;
441
+ released: number;
442
+ rejected: number;
443
+ published?: MailPublishResult["status"];
444
+ }
445
+
446
+ /**
447
+ * Apply every locally-fetched peer mail ref: import envelopes addressed to
448
+ * this machine, record acknowledgements of this machine's outbox, and stop
449
+ * acknowledging messages their sender has dropped. Reads only local refs;
450
+ * `fetchPresence` brings them in. Never throws.
451
+ */
452
+ export function importMail(
453
+ coordRoot: string,
454
+ inbox: HarneryInboxService,
455
+ opts: { now?: Date; publish?: boolean; throttle?: boolean } = {},
456
+ ): MailImportResult {
457
+ const result: MailImportResult = { received: 0, delivered: 0, released: 0, rejected: 0 };
458
+ try {
459
+ if (!presenceEnabled(coordRoot)) return result;
460
+ const now = opts.now ?? new Date();
461
+ if (opts.throttle && !importDue(coordRoot, now)) return result;
462
+ const self = mailMachine();
463
+ const peers = readMachineRefs(coordRoot, MAIL_REF_PREFIX)
464
+ .filter(({ machine }) => machine !== self)
465
+ .map(({ machine, message }) => ({ machine, blob: parseBlob(machine, message) }))
466
+ .filter((peer): peer is { machine: string; blob: HarneryMailBlobV1 } => peer.blob !== null);
467
+ if (peers.length === 0) return result;
468
+
469
+ updateMailLog(coordRoot, (projection) => {
470
+ const append: MailLogRecord[] = [];
471
+ const pending = new Map(projection.outbox.map((o) => [o.envelope.message_id, o.envelope]));
472
+ for (const { machine, blob } of peers) {
473
+ const listed = new Set<string>();
474
+ for (const envelope of blob.outbox) {
475
+ if (!validEnvelope(envelope, machine, self)) continue;
476
+ listed.add(envelope.message_id);
477
+ if (projection.imported.has(envelope.message_id)) continue;
478
+ if (!receive(inbox, envelope, result)) continue;
479
+ append.push({
480
+ schema: MAIL_LOG_SCHEMA,
481
+ kind: "imported",
482
+ at: stamp(now),
483
+ message_id: envelope.message_id,
484
+ from_machine: machine,
485
+ sender_instance_id: envelope.sender_instance_id,
486
+ sender_display_name: envelope.sender_display_name,
487
+ });
488
+ }
489
+ for (const id of blob.acks) {
490
+ if (pending.get(id)?.to_machine !== machine) continue;
491
+ pending.delete(id);
492
+ append.push({
493
+ schema: MAIL_LOG_SCHEMA,
494
+ kind: "delivered",
495
+ at: stamp(now),
496
+ message_id: id,
497
+ });
498
+ result.delivered++;
499
+ }
500
+ const publishedAt = Date.parse(blob.published_at);
501
+ for (const [id, ack] of projection.acks) {
502
+ if (ack.from_machine !== machine || listed.has(id)) continue;
503
+ if (publishedAt <= Date.parse(ack.at)) continue;
504
+ append.push({
505
+ schema: MAIL_LOG_SCHEMA,
506
+ kind: "released",
507
+ at: stamp(now),
508
+ message_id: id,
509
+ });
510
+ result.released++;
511
+ }
512
+ }
513
+ return { append, result: undefined };
514
+ });
515
+
516
+ if (opts.publish !== false && result.received + result.delivered + result.released > 0) {
517
+ result.published = publishMail(coordRoot, { now }).status;
518
+ }
519
+ } catch {
520
+ /* mail must never break a hook */
521
+ }
522
+ return result;
523
+ }
524
+
525
+ /** Hooks import at most once per interval: refs only change when the
526
+ * throttled presence fetch lands, so checking every prompt spawns git for
527
+ * nothing. The stamp is touched first so concurrent hooks do not stampede. */
528
+ function importDue(coordRoot: string, now: Date): boolean {
529
+ const path = join(mailDir(coordRoot), "import-stamp");
530
+ try {
531
+ if ((now.getTime() - statSync(path).mtimeMs) / 1000 < IMPORT_INTERVAL_SECS) return false;
532
+ } catch {
533
+ /* no stamp yet → import */
534
+ }
535
+ try {
536
+ mkdirSync(mailDir(coordRoot), { recursive: true });
537
+ writeFileSync(path, "", "utf8");
538
+ utimesSync(path, now, now);
539
+ } catch {
540
+ /* best-effort */
541
+ }
542
+ return true;
543
+ }
544
+
545
+ /** Inbox append for one envelope. A full inbox leaves the message unacknowledged
546
+ * so the sender keeps offering it; a malformed one is acknowledged and dropped. */
547
+ function receive(
548
+ inbox: HarneryInboxService,
549
+ envelope: HarneryMailEnvelopeV1,
550
+ result: MailImportResult,
551
+ ): boolean {
552
+ const record: HarneryInboxMessageRecordV1 = {
553
+ schema: HARNERY_INBOX_RECORD_SCHEMA,
554
+ kind: "message",
555
+ message_id: envelope.message_id,
556
+ sender_instance_id: envelope.sender_instance_id,
557
+ sender_display_name: envelope.sender_display_name,
558
+ sender_machine: envelope.from_machine,
559
+ recipient_instance_id: envelope.recipient_instance_id,
560
+ recipient_display_name: envelope.recipient_display_name,
561
+ created_at: envelope.created_at,
562
+ body: envelope.body,
563
+ body_bytes: envelope.body_bytes,
564
+ };
565
+ try {
566
+ if (inbox.receive(record) === "received") result.received++;
567
+ return true;
568
+ } catch (error) {
569
+ if (error instanceof HarneryInboxCapacityError && error.reason_code !== "message_body_limit") {
570
+ return false;
571
+ }
572
+ result.rejected++;
573
+ return true;
574
+ }
575
+ }
576
+
577
+ export interface RemoteSender {
578
+ instance_id: string;
579
+ name: string;
580
+ machine: string;
581
+ }
582
+
583
+ /** Senders this machine has received mail from, so a reply can find them even
584
+ * after their machine's presence goes quiet. */
585
+ export function knownRemoteSenders(coordRoot: string): RemoteSender[] {
586
+ return [...projectMail(readMailLog(coordRoot)).senders].map(([instance_id, s]) => ({
587
+ instance_id,
588
+ name: s.name,
589
+ machine: s.machine,
590
+ }));
591
+ }
592
+
593
+ export interface MailStatus {
594
+ machine: string;
595
+ outbox: Array<{
596
+ message_id: string;
597
+ to_machine: string;
598
+ recipient: string;
599
+ queued_at: string;
600
+ }>;
601
+ acks: number;
602
+ delivered: number;
603
+ expired: number;
604
+ }
605
+
606
+ export function mailStatus(coordRoot: string): MailStatus {
607
+ const projection = projectMail(readMailLog(coordRoot));
608
+ return {
609
+ machine: mailMachine(),
610
+ outbox: projection.outbox.map(({ at, envelope }) => ({
611
+ message_id: envelope.message_id,
612
+ to_machine: envelope.to_machine,
613
+ recipient: envelope.recipient_display_name,
614
+ queued_at: at,
615
+ })),
616
+ acks: projection.acks.size,
617
+ delivered: projection.delivered,
618
+ expired: projection.expired,
619
+ };
620
+ }
@@ -5,7 +5,7 @@ import { basename, join } from "node:path";
5
5
  import { performance } from "node:perf_hooks";
6
6
  import { readLiveCoordinationRows } from "../agents/state/live-coordination-view.ts";
7
7
  import { parsePidmapRow } from "../agents/state/pidmap.ts";
8
- import { checkPidToken } from "../agents/state/proc-start.ts";
8
+ import { verifyProcessGeneration } from "../agents/state/proc-start.ts";
9
9
  import {
10
10
  RESOURCE_SNAPSHOT_SCHEMA_VERSION,
11
11
  type ResourceProcessGroup,
@@ -386,7 +386,8 @@ function readOwnershipAnchors(
386
386
  if (!byPid.has(pid)) continue;
387
387
  try {
388
388
  const row = parsePidmapRow(readFileSync(join(directory, entry), "utf8"));
389
- if (!row.instanceId || checkPidToken(pid, row.startToken) === "mismatch") continue;
389
+ if (!row.instanceId || verifyProcessGeneration(pid, row.startToken) === "different")
390
+ continue;
390
391
  anchors.set(pid, { kind: "agent", id: row.instanceId, source: "pid-map" });
391
392
  } catch {
392
393
  // An unreadable or racing row cannot prove ownership.