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
@@ -1,4 +1,4 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { existsSync, statSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import {
@@ -12,6 +12,8 @@ import {
12
12
  HARNERY_INBOX_STATUS_SCHEMA,
13
13
  HarneryInboxCapacityError,
14
14
  type HarneryInboxLimits,
15
+ HarneryInboxMessageConflictError,
16
+ HarneryInboxMessageHistoryMissingError,
15
17
  type HarneryInboxMessageRecordV1,
16
18
  type HarneryInboxRecordV1,
17
19
  type HarneryInboxStatusV1,
@@ -27,6 +29,8 @@ export interface HarneryInboxServiceOptions {
27
29
  }
28
30
 
29
31
  export interface HarneryInboxSendInput {
32
+ message_id?: string;
33
+ replay_only?: boolean;
30
34
  sender_instance_id: string | null;
31
35
  sender_display_name: string;
32
36
  recipient_instance_id: string;
@@ -35,12 +39,22 @@ export interface HarneryInboxSendInput {
35
39
  }
36
40
 
37
41
  export interface HarneryInboxSendReceipt {
38
- state: "queued";
42
+ state: "queued" | "duplicate";
39
43
  message_id: string;
40
44
  body_bytes: number;
41
45
  synced: true;
42
46
  }
43
47
 
48
+ /** A caller freezes the operation and recipient before its first attempt. */
49
+ export function stableInboxMessageId(operationId: string, recipientInstanceId: string): string {
50
+ const identity = JSON.stringify([
51
+ "harnery.inbox-operation/v1",
52
+ opaqueId(operationId, "operation"),
53
+ opaqueId(recipientInstanceId, "recipient instance"),
54
+ ]);
55
+ return `msg_op_${createHash("sha256").update(identity).digest("hex")}`;
56
+ }
57
+
44
58
  export class HarneryInboxService {
45
59
  readonly #options: HarneryInboxServiceOptions;
46
60
 
@@ -50,6 +64,12 @@ export class HarneryInboxService {
50
64
  }
51
65
 
52
66
  send(input: HarneryInboxSendInput): HarneryInboxSendReceipt {
67
+ if (input.replay_only !== undefined && typeof input.replay_only !== "boolean") {
68
+ throw new Error("invalid replay_only flag");
69
+ }
70
+ if (input.replay_only && input.message_id === undefined) {
71
+ throw new Error("replay requires explicit message id");
72
+ }
53
73
  const recipient = opaqueId(input.recipient_instance_id, "recipient instance");
54
74
  const bodyBytes = Buffer.byteLength(input.body);
55
75
  const limits = this.#options.limits;
@@ -63,7 +83,10 @@ export class HarneryInboxService {
63
83
  const record: HarneryInboxMessageRecordV1 = {
64
84
  schema: HARNERY_INBOX_RECORD_SCHEMA,
65
85
  kind: "message",
66
- message_id: `msg_${this.#options.id?.() ?? randomUUID()}`,
86
+ message_id:
87
+ input.message_id === undefined
88
+ ? `msg_${this.#options.id?.() ?? randomUUID()}`
89
+ : opaqueId(input.message_id, "message"),
67
90
  sender_instance_id: senderId(input.sender_instance_id),
68
91
  sender_display_name: boundedDisplay(input.sender_display_name),
69
92
  recipient_instance_id: recipient,
@@ -78,6 +101,24 @@ export class HarneryInboxService {
78
101
  readOptions(limits),
79
102
  (records) => {
80
103
  const projection = project(records);
104
+ const existing = records.find(
105
+ (item): item is HarneryInboxMessageRecordV1 =>
106
+ item.kind === "message" && item.message_id === record.message_id,
107
+ );
108
+ if (existing) {
109
+ assertSameMessage(existing, record);
110
+ return {
111
+ result: {
112
+ state: "duplicate",
113
+ message_id: existing.message_id,
114
+ body_bytes: existing.body_bytes,
115
+ synced: true,
116
+ },
117
+ };
118
+ }
119
+ if (input.replay_only) {
120
+ throw new HarneryInboxMessageHistoryMissingError(record.message_id);
121
+ }
81
122
  capacityCheck(projection, record, path, limits);
82
123
  return {
83
124
  append: [record],
@@ -92,6 +133,52 @@ export class HarneryInboxService {
92
133
  );
93
134
  }
94
135
 
136
+ /** Append a message created on another machine, keeping its id. Importers
137
+ * on one machine can race, so the duplicate check runs under the lease. */
138
+ receive(record: HarneryInboxMessageRecordV1): "received" | "duplicate" {
139
+ const recipient = opaqueId(record.recipient_instance_id, "recipient instance");
140
+ const limits = this.#options.limits;
141
+ if (record.body_bytes === 0 || record.body_bytes > limits.max_message_body_bytes) {
142
+ throw new HarneryInboxCapacityError(
143
+ "message_body_limit",
144
+ record.body_bytes,
145
+ limits.max_message_body_bytes,
146
+ );
147
+ }
148
+ const incoming: HarneryInboxMessageRecordV1 = {
149
+ schema: HARNERY_INBOX_RECORD_SCHEMA,
150
+ kind: "message",
151
+ message_id: opaqueId(record.message_id, "message"),
152
+ sender_instance_id: senderId(record.sender_instance_id),
153
+ sender_display_name: boundedDisplay(record.sender_display_name),
154
+ ...(record.sender_machine ? { sender_machine: boundedDisplay(record.sender_machine) } : {}),
155
+ recipient_instance_id: recipient,
156
+ recipient_display_name: boundedDisplay(record.recipient_display_name),
157
+ created_at: record.created_at,
158
+ body: record.body,
159
+ body_bytes: record.body_bytes,
160
+ };
161
+ validateRecord(incoming);
162
+ const path = inboxPath(this.#options.coord_root, recipient);
163
+ return updateInboxHistory<HarneryInboxRecordV1, "received" | "duplicate">(
164
+ path,
165
+ readOptions(limits),
166
+ (records) => {
167
+ const projection = project(records);
168
+ const existing = records.find(
169
+ (item): item is HarneryInboxMessageRecordV1 =>
170
+ item.kind === "message" && item.message_id === incoming.message_id,
171
+ );
172
+ if (existing) {
173
+ assertSameMessage(existing, incoming);
174
+ return { result: "duplicate" };
175
+ }
176
+ capacityCheck(projection, incoming, path, limits);
177
+ return { append: [incoming], result: "received" };
178
+ },
179
+ );
180
+ }
181
+
95
182
  records(recipientInstanceId: string): HarneryInboxRecordV1[] {
96
183
  const records = readCrashSafeJsonlFile<HarneryInboxRecordV1>(
97
184
  inboxPath(this.#options.coord_root, opaqueId(recipientInstanceId, "recipient instance")),
@@ -202,6 +289,24 @@ export class HarneryInboxService {
202
289
  }
203
290
  }
204
291
 
292
+ function assertSameMessage(
293
+ existing: HarneryInboxMessageRecordV1,
294
+ incoming: HarneryInboxMessageRecordV1,
295
+ ): void {
296
+ const fields = [
297
+ "sender_instance_id",
298
+ "sender_display_name",
299
+ "sender_machine",
300
+ "recipient_instance_id",
301
+ "recipient_display_name",
302
+ "body",
303
+ "body_bytes",
304
+ ] as const;
305
+ if (fields.some((field) => existing[field] !== incoming[field])) {
306
+ throw new HarneryInboxMessageConflictError(incoming.message_id);
307
+ }
308
+ }
309
+
205
310
  /** The shared writer refuses an occupied lease before invoking the update.
206
311
  * Briefly retry only that exact refusal so concurrent peer senders both get
207
312
  * a chance to append. All validation, authority and I/O failures still fail
@@ -11,10 +11,14 @@ import {
11
11
  nativeInstanceIdV3,
12
12
  observeLiveEventLedgerRouteV3,
13
13
  } from "../events/v3/live-route-observer.ts";
14
+ import { readRemoteMachines } from "../presence/index.ts";
15
+ import { knownRemoteSenders } from "../presence/mail.ts";
14
16
 
15
17
  export interface MessageTargetInput {
16
18
  name?: string;
17
19
  instanceId?: string;
20
+ /** Refuse a later generation of the same immutable instance. */
21
+ generationId?: string;
18
22
  requireLive?: boolean;
19
23
  now?: Date;
20
24
  }
@@ -22,11 +26,17 @@ export interface MessageTargetInput {
22
26
  export type MessageTargetResult =
23
27
  | {
24
28
  ok: true;
25
- target: { instance_id: string; name: string; delivery: "written-live" | "queued" };
29
+ target: {
30
+ instance_id: string;
31
+ name: string;
32
+ delivery: "written-live" | "queued" | "remote";
33
+ /** Set when the recipient lives on another machine (ADR 0222). */
34
+ machine?: string;
35
+ };
26
36
  }
27
37
  | {
28
38
  ok: false;
29
- code: "no_peer" | "ambiguous_peer" | "not_live" | "authority_unsafe";
39
+ code: "no_peer" | "ambiguous_peer" | "not_live" | "authority_unsafe" | "generation_mismatch";
30
40
  message: string;
31
41
  candidates?: string[];
32
42
  };
@@ -35,6 +45,7 @@ export type MessageTargetResult =
35
45
  export function resolveMessageTarget(
36
46
  coordRoot: string,
37
47
  input: MessageTargetInput,
48
+ options: { view?: CoordinationViewV3 } = {},
38
49
  ): MessageTargetResult {
39
50
  const requestedName = normalizeName(input.name);
40
51
  const requestedInstance = input.instanceId?.trim();
@@ -49,7 +60,9 @@ export function resolveMessageTarget(
49
60
  if (observeLiveEventLedgerRouteV3(coordRoot).state === "blocked") {
50
61
  return unsafe("The event ledger cannot establish message recipient authority.");
51
62
  }
52
- view = requireAuthoritySafeCoordinationViewV3(readCoordinationViewV3(coordRoot));
63
+ view = requireAuthoritySafeCoordinationViewV3(
64
+ options.view ?? readCoordinationViewV3(coordRoot),
65
+ );
53
66
  if (!view.source_complete) return unsafe("The event ledger read is incomplete.");
54
67
  } catch {
55
68
  return unsafe("The event ledger cannot establish message recipient authority.");
@@ -79,6 +92,7 @@ export function resolveMessageTarget(
79
92
  {
80
93
  instanceId,
81
94
  name,
95
+ generationId: generation.generation_id,
82
96
  safe: !diagnostics.some((diagnostic) => diagnostic.authority_blocking),
83
97
  fresh:
84
98
  generation.phase === "live" && Number.isFinite(age) && age >= 0 && age <= freshnessMs,
@@ -107,6 +121,13 @@ export function resolveMessageTarget(
107
121
  };
108
122
  }
109
123
  const target = selected[0]!;
124
+ if (input.generationId && input.generationId !== target.generationId) {
125
+ return {
126
+ ok: false,
127
+ code: "generation_mismatch",
128
+ message: "The recipient generation differs from the exact requested generation.",
129
+ };
130
+ }
110
131
  if (input.requireLive && !target.fresh) {
111
132
  return {
112
133
  ok: false,
@@ -125,6 +146,76 @@ export function resolveMessageTarget(
125
146
  };
126
147
  }
127
148
 
149
+ /**
150
+ * Resolve a recipient on another machine, for a name or instance this
151
+ * machine's ledger has never seen. Candidates come from peer presence (live
152
+ * or stale) and from senders this machine has received mail from. Returns
153
+ * null when nothing matches, so the caller can report the local miss.
154
+ */
155
+ export function resolveRemoteMessageTarget(
156
+ coordRoot: string,
157
+ input: MessageTargetInput,
158
+ ): MessageTargetResult | null {
159
+ const requestedName = normalizeName(input.name);
160
+ const requestedInstance = input.instanceId?.trim();
161
+ if (!requestedName && !requestedInstance) return null;
162
+ // Presence/mail names do not prove an exact V3 generation.
163
+ if (input.generationId) return null;
164
+ const live = new Set(
165
+ readRemoteMachines(coordRoot).flatMap(({ agents }) => agents.map((a) => a.instance_id)),
166
+ );
167
+ const known = new Map<string, { instanceId: string; name: string; machine: string }>();
168
+ for (const { machine, agents } of readRemoteMachines(coordRoot, { includeStale: true })) {
169
+ for (const agent of agents) {
170
+ if (agent.name)
171
+ known.set(agent.instance_id, { instanceId: agent.instance_id, name: agent.name, machine });
172
+ }
173
+ }
174
+ for (const sender of knownRemoteSenders(coordRoot)) {
175
+ if (!known.has(sender.instance_id)) {
176
+ known.set(sender.instance_id, {
177
+ instanceId: sender.instance_id,
178
+ name: sender.name,
179
+ machine: sender.machine,
180
+ });
181
+ }
182
+ }
183
+ const candidates = [...known.values()]
184
+ .filter(({ instanceId }) => !requestedInstance || instanceId === requestedInstance)
185
+ .filter(({ name }) => !requestedName || normalizeName(name) === requestedName)
186
+ .map((candidate) => ({ ...candidate, fresh: live.has(candidate.instanceId) }));
187
+ if (candidates.length === 0) return null;
188
+ const fresh = candidates.filter(({ fresh }) => fresh);
189
+ const selected = requestedInstance ? candidates : fresh.length > 0 ? fresh : candidates;
190
+ if (selected.length !== 1) {
191
+ return {
192
+ ok: false,
193
+ code: "ambiguous_peer",
194
+ message:
195
+ "Several agent instances on other machines hold that exact name; specify --instance-id.",
196
+ candidates: selected.map(({ instanceId, machine }) => `${instanceId} (${machine})`).sort(),
197
+ };
198
+ }
199
+ const target = selected[0]!;
200
+ if (input.requireLive && !target.fresh) {
201
+ return {
202
+ ok: false,
203
+ code: "not_live",
204
+ message:
205
+ "That agent is on another machine and outside live presence; omit --require-live to queue its message.",
206
+ };
207
+ }
208
+ return {
209
+ ok: true,
210
+ target: {
211
+ instance_id: target.instanceId,
212
+ name: target.name,
213
+ delivery: "remote",
214
+ machine: target.machine,
215
+ },
216
+ };
217
+ }
218
+
128
219
  function laterPosition(
129
220
  left: CoordinationGenerationViewV3,
130
221
  right: CoordinationGenerationViewV3,
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Git plumbing for the cross-machine presence transport (ADR 0016).
2
+ * Git plumbing for the cross-machine presence transport (ADR 0016) and the
3
+ * mail refs that ride on it (ADR 0222).
3
4
  *
4
5
  * Presence blobs travel as parentless commits (empty tree, JSON message)
5
6
  * force-pushed to `refs/harnery/presence/<machine>` on the host repo's own
@@ -15,9 +16,12 @@
15
16
 
16
17
  import { spawn, spawnSync } from "node:child_process";
17
18
 
18
- /** The ref namespace. Hardcoded — never interpolated from config — so the
19
- * publisher is structurally unable to touch refs/heads/* (ADR 0016). */
19
+ /** The ref namespaces. Hardcoded — never interpolated from config — so the
20
+ * publisher is structurally unable to touch refs/heads/* (ADR 0016). Mail
21
+ * refs carry cross-machine peer messages on the same transport (ADR 0222). */
20
22
  export const PRESENCE_REF_PREFIX = "refs/harnery/presence/";
23
+ export const MAIL_REF_PREFIX = "refs/harnery/mail/";
24
+ type HarneryRefPrefix = typeof PRESENCE_REF_PREFIX | typeof MAIL_REF_PREFIX;
21
25
 
22
26
  /** Well-known SHA of git's empty tree; `git mktree` below guarantees the
23
27
  * object exists in the local odb before we build a commit on it. */
@@ -112,44 +116,46 @@ export function writePresenceCommit(root: string, message: string): string | nul
112
116
  }
113
117
 
114
118
  /**
115
- * Force-push a presence commit to `refs/harnery/presence/<machine>`.
116
- * `sync: true` (CLI/tests) waits and reports; default is detached fire-and-
117
- * forget (hook path — a slow or credential-less push must not block a turn).
119
+ * Force-push a commit to `<prefix><machine>` (presence or mail). `sync: true`
120
+ * (CLI/tests) waits and reports; default is detached fire-and-forget (hook
121
+ * path — a slow or credential-less push must not block a turn).
118
122
  */
119
- export function pushPresenceRef(
123
+ export function pushMachineRef(
120
124
  root: string,
125
+ prefix: HarneryRefPrefix,
121
126
  machine: string,
122
127
  sha: string,
123
128
  opts: { sync?: boolean } = {},
124
129
  ): { ok: boolean; error?: string } {
125
- const refspec = `+${sha}:${PRESENCE_REF_PREFIX}${sanitizeRefComponent(machine)}`;
126
- const args = ["-C", root, "push", "--quiet", "--no-verify", "origin", refspec];
127
- if (opts.sync) {
128
- const r = runGit(root, args.slice(2));
129
- return r.ok ? { ok: true } : { ok: false, error: r.stderr.trim().slice(0, 300) };
130
- }
131
- try {
132
- const child = spawn("git", args, { detached: true, stdio: "ignore", env: gitEnv() });
133
- child.unref();
134
- return { ok: true };
135
- } catch {
136
- return { ok: false, error: "spawn failed" };
137
- }
130
+ const refspec = `+${sha}:${prefix}${sanitizeRefComponent(machine)}`;
131
+ return runNetworkGit(root, ["push", "--quiet", "--no-verify", "origin", refspec], opts);
138
132
  }
139
133
 
140
- /** Fetch every peer presence ref from origin into the local ref namespace. */
141
- export function fetchPresenceRefs(
134
+ /** Fetch every peer presence and mail ref from origin into the local ref
135
+ * namespace, in one round trip. A namespace with no refs is not an error. */
136
+ export function fetchMachineRefs(
142
137
  root: string,
143
138
  opts: { sync?: boolean } = {},
144
139
  ): { ok: boolean; error?: string } {
145
- const refspec = `+${PRESENCE_REF_PREFIX}*:${PRESENCE_REF_PREFIX}*`;
146
- const args = ["-C", root, "fetch", "--quiet", "--no-tags", "origin", refspec];
140
+ const refspecs = [PRESENCE_REF_PREFIX, MAIL_REF_PREFIX].map((p) => `+${p}*:${p}*`);
141
+ return runNetworkGit(root, ["fetch", "--quiet", "--no-tags", "origin", ...refspecs], opts);
142
+ }
143
+
144
+ function runNetworkGit(
145
+ root: string,
146
+ args: string[],
147
+ opts: { sync?: boolean },
148
+ ): { ok: boolean; error?: string } {
147
149
  if (opts.sync) {
148
- const r = runGit(root, args.slice(2));
150
+ const r = runGit(root, args);
149
151
  return r.ok ? { ok: true } : { ok: false, error: r.stderr.trim().slice(0, 300) };
150
152
  }
151
153
  try {
152
- const child = spawn("git", args, { detached: true, stdio: "ignore", env: gitEnv() });
154
+ const child = spawn("git", ["-C", root, ...args], {
155
+ detached: true,
156
+ stdio: "ignore",
157
+ env: gitEnv(),
158
+ });
153
159
  child.unref();
154
160
  return { ok: true };
155
161
  } catch {
@@ -157,15 +163,14 @@ export function fetchPresenceRefs(
157
163
  }
158
164
  }
159
165
 
160
- /** Read every locally-known presence ref → [{ref, machine, message}]. */
161
- export function readPresenceRefs(root: string): Array<{ machine: string; message: string }> {
162
- const r = runGit(root, [
163
- "for-each-ref",
164
- "--format=%(refname)%00%(contents)%01",
165
- `${PRESENCE_REF_PREFIX}*`,
166
- ]);
166
+ /** Read every locally-known ref under `prefix` → [{machine, message}]. */
167
+ export function readMachineRefs(
168
+ root: string,
169
+ prefix: HarneryRefPrefix = PRESENCE_REF_PREFIX,
170
+ ): Array<{ machine: string; message: string }> {
171
+ const r = runGit(root, ["for-each-ref", "--format=%(refname)%00%(contents)%01", `${prefix}*`]);
167
172
  if (!r.ok) return [];
168
- return parseForEachRefOutput(r.stdout);
173
+ return parseForEachRefOutput(r.stdout, prefix);
169
174
  }
170
175
 
171
176
  /**
@@ -173,7 +178,10 @@ export function readPresenceRefs(root: string): Array<{ machine: string; message
173
178
  * Records are %01-terminated (contents may contain newlines); refname and
174
179
  * contents are %00-separated. Exported for unit tests.
175
180
  */
176
- export function parseForEachRefOutput(out: string): Array<{ machine: string; message: string }> {
181
+ export function parseForEachRefOutput(
182
+ out: string,
183
+ prefix: HarneryRefPrefix = PRESENCE_REF_PREFIX,
184
+ ): Array<{ machine: string; message: string }> {
177
185
  const rows: Array<{ machine: string; message: string }> = [];
178
186
  for (const rec of out.split("\u0001")) {
179
187
  const trimmed = rec.replace(/^\n+/, "");
@@ -181,9 +189,9 @@ export function parseForEachRefOutput(out: string): Array<{ machine: string; mes
181
189
  const sep = trimmed.indexOf("\u0000");
182
190
  if (sep === -1) continue;
183
191
  const refname = trimmed.slice(0, sep).trim();
184
- if (!refname.startsWith(PRESENCE_REF_PREFIX)) continue;
192
+ if (!refname.startsWith(prefix)) continue;
185
193
  rows.push({
186
- machine: refname.slice(PRESENCE_REF_PREFIX.length),
194
+ machine: refname.slice(prefix.length),
187
195
  message: trimmed.slice(sep + 1).trim(),
188
196
  });
189
197
  }
@@ -31,17 +31,23 @@ import { resolveMachineLabel } from "../../lib/machine.ts";
31
31
  import { presenceEnabled } from "../config.ts";
32
32
  import { buildPresenceBlob, type PresenceAgent, type PresenceBlob } from "./blob.ts";
33
33
  import {
34
- fetchPresenceRefs,
34
+ fetchMachineRefs,
35
35
  hasOrigin,
36
- pushPresenceRef,
37
- readPresenceRefs,
36
+ PRESENCE_REF_PREFIX,
37
+ pushMachineRef,
38
+ readMachineRefs,
38
39
  sanitizeRefComponent,
39
40
  writePresenceCommit,
40
41
  } from "./git.ts";
41
42
 
42
43
  export type { PresenceAgent, PresenceBlob } from "./blob.ts";
43
44
  export { buildPresenceBlob } from "./blob.ts";
44
- export { PRESENCE_REF_PREFIX, parseForEachRefOutput, sanitizeRefComponent } from "./git.ts";
45
+ export {
46
+ MAIL_REF_PREFIX,
47
+ PRESENCE_REF_PREFIX,
48
+ parseForEachRefOutput,
49
+ sanitizeRefComponent,
50
+ } from "./git.ts";
45
51
  export { ensureRelayDaemon, relayDaemonStatus, runRelayDaemon } from "./relay-client.ts";
46
52
 
47
53
  /** Re-push at least this often while sessions are live, so peers can treat a
@@ -139,7 +145,9 @@ export function publishPresence(
139
145
  const sha = writePresenceCommit(coordRoot, json);
140
146
  if (!sha) return { status: "error", error: "commit-tree failed" };
141
147
 
142
- const push = pushPresenceRef(coordRoot, blob.machine, sha, { sync: opts.sync });
148
+ const push = pushMachineRef(coordRoot, PRESENCE_REF_PREFIX, blob.machine, sha, {
149
+ sync: opts.sync,
150
+ });
143
151
  if (!push.ok) return { status: "error", error: push.error ?? "push failed" };
144
152
 
145
153
  // Optimistic on the detached path: a failed background push self-heals at
@@ -155,7 +163,7 @@ export function publishPresence(
155
163
  }
156
164
  }
157
165
 
158
- /** Throttled fetch of peer presence refs from origin. */
166
+ /** Throttled fetch of peer presence and mail refs from origin. */
159
167
  export function fetchPresence(
160
168
  coordRoot: string,
161
169
  opts: { force?: boolean; sync?: boolean } = {},
@@ -191,7 +199,7 @@ export function fetchPresence(
191
199
  /* best-effort */
192
200
  }
193
201
 
194
- const r = fetchPresenceRefs(coordRoot, { sync: opts.sync });
202
+ const r = fetchMachineRefs(coordRoot, { sync: opts.sync });
195
203
  if (!r.ok) return { status: "error", error: r.error ?? "fetch failed" };
196
204
  return { status: "fetched" };
197
205
  } catch (e) {
@@ -231,7 +239,7 @@ export function readRemoteMachines(
231
239
  if (prev && Date.parse(prev.published_at) >= publishedMs) return;
232
240
  byMachine.set(machine, blob);
233
241
  };
234
- for (const { machine, message } of readPresenceRefs(coordRoot)) {
242
+ for (const { machine, message } of readMachineRefs(coordRoot, PRESENCE_REF_PREFIX)) {
235
243
  try {
236
244
  consider(machine, JSON.parse(message) as PresenceBlob);
237
245
  } catch {