harnery 0.45.2 → 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 (216) 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/core/agents/index.d.ts +1 -0
  21. package/dist/core/agents/index.d.ts.map +1 -1
  22. package/dist/core/agents/index.js +1 -0
  23. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  24. package/dist/core/agents/live-authority-v3.js +11 -4
  25. package/dist/core/agents/render/session-context.d.ts +0 -6
  26. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  27. package/dist/core/agents/render/session-context.js +1 -6
  28. package/dist/core/agents/session-events.d.ts.map +1 -1
  29. package/dist/core/agents/session-events.js +12 -4
  30. package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
  31. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  32. package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
  33. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  34. package/dist/core/agents/state/live-coordination-view.js +6 -2
  35. package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
  36. package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
  37. package/dist/core/agents/state/parent-reference-contract.js +1 -0
  38. package/dist/core/agents/state/parent-reference.d.ts +18 -0
  39. package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
  40. package/dist/core/agents/state/parent-reference.js +119 -0
  41. package/dist/core/config.d.ts +39 -0
  42. package/dist/core/config.d.ts.map +1 -1
  43. package/dist/core/config.js +63 -0
  44. package/dist/core/diagnostics/path-identity.d.ts +1 -1
  45. package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
  46. package/dist/core/diagnostics/path-identity.js +51 -6
  47. package/dist/core/events/v3/canonical.js +43 -14
  48. package/dist/core/events/v3/index.d.ts +1 -1
  49. package/dist/core/events/v3/index.d.ts.map +1 -1
  50. package/dist/core/events/v3/index.js +1 -1
  51. package/dist/core/events/v3/producers/recorder.d.ts +34 -0
  52. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  53. package/dist/core/events/v3/producers/recorder.js +117 -13
  54. package/dist/core/events/v3/reader.d.ts.map +1 -1
  55. package/dist/core/events/v3/reader.js +53 -13
  56. package/dist/core/events/v3/validate.d.ts.map +1 -1
  57. package/dist/core/events/v3/validate.js +36 -1
  58. package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
  59. package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
  60. package/dist/core/hooks/adapter/native-parent.js +91 -0
  61. package/dist/core/hooks/cli.js +79 -4
  62. package/dist/core/hooks/resolve/owner.d.ts +0 -11
  63. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  64. package/dist/core/hooks/resolve/owner.js +0 -23
  65. package/dist/core/hooks/shell-admission.d.ts +84 -0
  66. package/dist/core/hooks/shell-admission.d.ts.map +1 -0
  67. package/dist/core/hooks/shell-admission.js +622 -0
  68. package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
  69. package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
  70. package/dist/core/hooks/shell-commit-tree.js +104 -0
  71. package/dist/core/inbox/contract.d.ts +12 -0
  72. package/dist/core/inbox/contract.d.ts.map +1 -1
  73. package/dist/core/inbox/contract.js +16 -0
  74. package/dist/core/inbox/peer-messages.d.ts +41 -16
  75. package/dist/core/inbox/peer-messages.d.ts.map +1 -1
  76. package/dist/core/inbox/peer-messages.js +372 -15
  77. package/dist/core/inbox/service.d.ts +8 -1
  78. package/dist/core/inbox/service.d.ts.map +1 -1
  79. package/dist/core/inbox/service.js +83 -3
  80. package/dist/core/inbox/targets.d.ts +17 -3
  81. package/dist/core/inbox/targets.d.ts.map +1 -1
  82. package/dist/core/inbox/targets.js +77 -2
  83. package/dist/core/presence/git.d.ts +18 -12
  84. package/dist/core/presence/git.d.ts.map +1 -1
  85. package/dist/core/presence/git.js +32 -38
  86. package/dist/core/presence/index.d.ts +2 -2
  87. package/dist/core/presence/index.d.ts.map +1 -1
  88. package/dist/core/presence/index.js +8 -6
  89. package/dist/core/presence/mail.d.ts +133 -0
  90. package/dist/core/presence/mail.d.ts.map +1 -0
  91. package/dist/core/presence/mail.js +464 -0
  92. package/dist/core/semantic/contract.d.ts +5 -5
  93. package/dist/core/storage/modes.d.ts +4 -0
  94. package/dist/core/storage/modes.d.ts.map +1 -1
  95. package/dist/core/storage/modes.js +4 -0
  96. package/dist/core/storage/segments.d.ts.map +1 -1
  97. package/dist/core/storage/segments.js +5 -4
  98. package/dist/lib/admission-history.d.ts +96 -0
  99. package/dist/lib/admission-history.d.ts.map +1 -0
  100. package/dist/lib/admission-history.js +196 -0
  101. package/dist/lib/admission.d.ts +56 -7
  102. package/dist/lib/admission.d.ts.map +1 -1
  103. package/dist/lib/admission.js +161 -19
  104. package/dist/lib/browser/client.d.ts +18 -0
  105. package/dist/lib/browser/client.d.ts.map +1 -1
  106. package/dist/lib/browser/client.js +100 -12
  107. package/dist/lib/browser/content-checks.d.ts +3 -1
  108. package/dist/lib/browser/content-checks.d.ts.map +1 -1
  109. package/dist/lib/browser/content-checks.js +10 -2
  110. package/dist/lib/browser/critique.d.ts +32 -0
  111. package/dist/lib/browser/critique.d.ts.map +1 -1
  112. package/dist/lib/browser/critique.js +72 -3
  113. package/dist/lib/browser/geometry.d.ts +2 -0
  114. package/dist/lib/browser/geometry.d.ts.map +1 -1
  115. package/dist/lib/browser/geometry.js +61 -15
  116. package/dist/lib/browser/index.d.ts +1 -1
  117. package/dist/lib/browser/index.d.ts.map +1 -1
  118. package/dist/lib/browser/index.js +1 -1
  119. package/dist/lib/browser/page-review-contracts.d.ts +6 -1
  120. package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
  121. package/dist/lib/browser/page-review-contracts.js +6 -1
  122. package/dist/lib/browser/page-review-judge.d.ts +3 -1
  123. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  124. package/dist/lib/browser/page-review-judge.js +41 -11
  125. package/dist/lib/browser/page-review-pack.d.ts +3 -0
  126. package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
  127. package/dist/lib/browser/page-review-pack.js +5 -2
  128. package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
  129. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  130. package/dist/lib/browser/qa-run-contracts.js +11 -4
  131. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  132. package/dist/lib/browser/qa-run.js +73 -16
  133. package/dist/lib/browser/runts.d.ts +4 -2
  134. package/dist/lib/browser/runts.d.ts.map +1 -1
  135. package/dist/lib/browser/runts.js +84 -33
  136. package/dist/lib/browser/windows-chrome.d.ts +14 -0
  137. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  138. package/dist/lib/browser/windows-chrome.js +42 -0
  139. package/dist/lib/browser/windows-login-sync.d.ts +110 -0
  140. package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
  141. package/dist/lib/browser/windows-login-sync.js +171 -0
  142. package/dist/lib/cookies/client.d.ts +30 -5
  143. package/dist/lib/cookies/client.d.ts.map +1 -1
  144. package/dist/lib/cookies/client.js +55 -10
  145. package/dist/lib/cookies/index.d.ts +1 -1
  146. package/dist/lib/cookies/index.d.ts.map +1 -1
  147. package/dist/lib/cookies/index.js +1 -1
  148. package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
  149. package/dist/lib/docs-metadata-sync.js +41 -2
  150. package/dist/lib/durable-job.d.ts +8 -0
  151. package/dist/lib/durable-job.d.ts.map +1 -1
  152. package/dist/lib/durable-job.js +10 -0
  153. package/dist/lib/http/client.d.ts +9 -8
  154. package/dist/lib/http/client.d.ts.map +1 -1
  155. package/dist/lib/http/client.js +44 -2
  156. package/dist/lib/instructions/templates.js +4 -4
  157. package/package.json +2 -2
  158. package/src/commander.ts +13 -0
  159. package/src/commands/admission.ts +272 -29
  160. package/src/commands/agents.ts +132 -27
  161. package/src/commands/browse.ts +208 -6
  162. package/src/commands/fetch.ts +1 -0
  163. package/src/commands/ledger-v3.ts +8 -2
  164. package/src/commands/presence.ts +49 -0
  165. package/src/commands/qa-run.ts +4 -3
  166. package/src/commands/review-pack.ts +41 -5
  167. package/src/core/agents/index.ts +6 -0
  168. package/src/core/agents/live-authority-v3.ts +11 -4
  169. package/src/core/agents/render/session-context.ts +1 -7
  170. package/src/core/agents/session-events.ts +18 -3
  171. package/src/core/agents/state/heartbeat-reader.ts +3 -0
  172. package/src/core/agents/state/live-coordination-view.ts +13 -6
  173. package/src/core/agents/state/parent-reference-contract.ts +14 -0
  174. package/src/core/agents/state/parent-reference.ts +179 -0
  175. package/src/core/config.ts +85 -0
  176. package/src/core/diagnostics/path-identity.ts +52 -6
  177. package/src/core/events/v3/canonical.ts +46 -13
  178. package/src/core/events/v3/index.ts +3 -0
  179. package/src/core/events/v3/producers/recorder.ts +151 -13
  180. package/src/core/events/v3/reader.ts +55 -11
  181. package/src/core/events/v3/validate.ts +42 -1
  182. package/src/core/hooks/adapter/native-parent.ts +109 -0
  183. package/src/core/hooks/cli.ts +83 -4
  184. package/src/core/hooks/resolve/owner.ts +0 -23
  185. package/src/core/hooks/shell-admission.ts +634 -0
  186. package/src/core/hooks/shell-commit-tree.ts +102 -0
  187. package/src/core/inbox/contract.ts +18 -0
  188. package/src/core/inbox/peer-messages.ts +501 -15
  189. package/src/core/inbox/service.ts +108 -3
  190. package/src/core/inbox/targets.ts +94 -3
  191. package/src/core/presence/git.ts +45 -37
  192. package/src/core/presence/index.ts +16 -8
  193. package/src/core/presence/mail.ts +620 -0
  194. package/src/core/storage/modes.ts +6 -0
  195. package/src/core/storage/segments.ts +5 -4
  196. package/src/lib/admission-history.ts +267 -0
  197. package/src/lib/admission.ts +217 -19
  198. package/src/lib/browser/client.ts +147 -35
  199. package/src/lib/browser/content-checks.ts +14 -3
  200. package/src/lib/browser/critique.ts +113 -3
  201. package/src/lib/browser/geometry.ts +63 -14
  202. package/src/lib/browser/index.ts +5 -0
  203. package/src/lib/browser/page-review-contracts.ts +6 -1
  204. package/src/lib/browser/page-review-judge.ts +58 -13
  205. package/src/lib/browser/page-review-pack.ts +13 -2
  206. package/src/lib/browser/qa-run-contracts.ts +30 -4
  207. package/src/lib/browser/qa-run.ts +78 -17
  208. package/src/lib/browser/runts.ts +85 -37
  209. package/src/lib/browser/windows-chrome.ts +50 -0
  210. package/src/lib/browser/windows-login-sync.ts +275 -0
  211. package/src/lib/cookies/client.ts +62 -10
  212. package/src/lib/cookies/index.ts +3 -0
  213. package/src/lib/docs-metadata-sync.ts +44 -2
  214. package/src/lib/durable-job.ts +15 -0
  215. package/src/lib/http/client.ts +63 -1
  216. package/src/lib/instructions/templates.ts +4 -4
@@ -6,6 +6,8 @@ export interface HarneryInboxServiceOptions {
6
6
  id?: () => string;
7
7
  }
8
8
  export interface HarneryInboxSendInput {
9
+ message_id?: string;
10
+ replay_only?: boolean;
9
11
  sender_instance_id: string | null;
10
12
  sender_display_name: string;
11
13
  recipient_instance_id: string;
@@ -13,15 +15,20 @@ export interface HarneryInboxSendInput {
13
15
  body: string;
14
16
  }
15
17
  export interface HarneryInboxSendReceipt {
16
- state: "queued";
18
+ state: "queued" | "duplicate";
17
19
  message_id: string;
18
20
  body_bytes: number;
19
21
  synced: true;
20
22
  }
23
+ /** A caller freezes the operation and recipient before its first attempt. */
24
+ export declare function stableInboxMessageId(operationId: string, recipientInstanceId: string): string;
21
25
  export declare class HarneryInboxService {
22
26
  #private;
23
27
  constructor(options: HarneryInboxServiceOptions);
24
28
  send(input: HarneryInboxSendInput): HarneryInboxSendReceipt;
29
+ /** Append a message created on another machine, keeping its id. Importers
30
+ * on one machine can race, so the duplicate check runs under the lease. */
31
+ receive(record: HarneryInboxMessageRecordV1): "received" | "duplicate";
25
32
  records(recipientInstanceId: string): HarneryInboxRecordV1[];
26
33
  pending(recipientInstanceId: string): HarneryInboxMessageRecordV1[];
27
34
  status(recipientInstanceId: string): HarneryInboxStatusV1;
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/core/inbox/service.ts"],"names":[],"mappings":"AASA,OAAO,EAIL,KAAK,kBAAkB,EACvB,KAAK,2BAA2B,EAChC,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,yBAAyB,EAC/B,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,QAAQ,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,IAAI,CAAC;CACd;AAED,qBAAa,mBAAmB;;gBAGlB,OAAO,EAAE,0BAA0B;IAK/C,IAAI,CAAC,KAAK,EAAE,qBAAqB,GAAG,uBAAuB;IA2C3D,OAAO,CAAC,mBAAmB,EAAE,MAAM,GAAG,oBAAoB,EAAE;IAS5D,OAAO,CAAC,mBAAmB,EAAE,MAAM,GAAG,2BAA2B,EAAE;IAInE,MAAM,CAAC,mBAAmB,EAAE,MAAM,GAAG,oBAAoB;IA0BnD,OAAO,CACX,mBAAmB,EAAE,MAAM,EAC3B,IAAI,EAAE,CAAC,QAAQ,EAAE,SAAS,2BAA2B,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,EAChF,MAAM,GAAE,yBAIP,GACA,OAAO,CAAC,yBAAyB,CAAC;IAiBrC;0EACsE;IACtE,cAAc,CACZ,mBAAmB,EAAE,MAAM,EAC3B,MAAM,GAAE,yBAIP,GACA,2BAA2B,EAAE;IAKhC;;2DAEuD;IACvD,kBAAkB,CAAC,mBAAmB,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI;CA2BrF;AA4BD,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,GAAG,MAAM,CAGhF;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,SAAS,oBAAoB,EAAE,GAAG;IACjE,OAAO,EAAE,2BAA2B,EAAE,CAAC;IACvC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAsBA"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/core/inbox/service.ts"],"names":[],"mappings":"AASA,OAAO,EAIL,KAAK,kBAAkB,EAGvB,KAAK,2BAA2B,EAChC,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,yBAAyB,EAC/B,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,QAAQ,GAAG,WAAW,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,IAAI,CAAC;CACd;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,GAAG,MAAM,CAO7F;AAED,qBAAa,mBAAmB;;gBAGlB,OAAO,EAAE,0BAA0B;IAK/C,IAAI,CAAC,KAAK,EAAE,qBAAqB,GAAG,uBAAuB;IAsE3D;+EAC2E;IAC3E,OAAO,CAAC,MAAM,EAAE,2BAA2B,GAAG,UAAU,GAAG,WAAW;IA4CtE,OAAO,CAAC,mBAAmB,EAAE,MAAM,GAAG,oBAAoB,EAAE;IAS5D,OAAO,CAAC,mBAAmB,EAAE,MAAM,GAAG,2BAA2B,EAAE;IAInE,MAAM,CAAC,mBAAmB,EAAE,MAAM,GAAG,oBAAoB;IA0BnD,OAAO,CACX,mBAAmB,EAAE,MAAM,EAC3B,IAAI,EAAE,CAAC,QAAQ,EAAE,SAAS,2BAA2B,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,EAChF,MAAM,GAAE,yBAIP,GACA,OAAO,CAAC,yBAAyB,CAAC;IAiBrC;0EACsE;IACtE,cAAc,CACZ,mBAAmB,EAAE,MAAM,EAC3B,MAAM,GAAE,yBAIP,GACA,2BAA2B,EAAE;IAKhC;;2DAEuD;IACvD,kBAAkB,CAAC,mBAAmB,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI;CA2BrF;AA8CD,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,GAAG,MAAM,CAGhF;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,SAAS,oBAAoB,EAAE,GAAG;IACjE,OAAO,EAAE,2BAA2B,EAAE,CAAC;IACvC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAsBA"}
@@ -1,8 +1,17 @@
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 { readCrashSafeJsonlFile, updateCrashSafeJsonlFile, } from "../storage/durable-history.js";
5
- import { HARNERY_INBOX_RECORD_SCHEMA, HARNERY_INBOX_STATUS_SCHEMA, HarneryInboxCapacityError, } from "./contract.js";
5
+ import { HARNERY_INBOX_RECORD_SCHEMA, HARNERY_INBOX_STATUS_SCHEMA, HarneryInboxCapacityError, HarneryInboxMessageConflictError, HarneryInboxMessageHistoryMissingError, } from "./contract.js";
6
+ /** A caller freezes the operation and recipient before its first attempt. */
7
+ export function stableInboxMessageId(operationId, recipientInstanceId) {
8
+ const identity = JSON.stringify([
9
+ "harnery.inbox-operation/v1",
10
+ opaqueId(operationId, "operation"),
11
+ opaqueId(recipientInstanceId, "recipient instance"),
12
+ ]);
13
+ return `msg_op_${createHash("sha256").update(identity).digest("hex")}`;
14
+ }
6
15
  export class HarneryInboxService {
7
16
  #options;
8
17
  constructor(options) {
@@ -10,6 +19,12 @@ export class HarneryInboxService {
10
19
  this.#options = options;
11
20
  }
12
21
  send(input) {
22
+ if (input.replay_only !== undefined && typeof input.replay_only !== "boolean") {
23
+ throw new Error("invalid replay_only flag");
24
+ }
25
+ if (input.replay_only && input.message_id === undefined) {
26
+ throw new Error("replay requires explicit message id");
27
+ }
13
28
  const recipient = opaqueId(input.recipient_instance_id, "recipient instance");
14
29
  const bodyBytes = Buffer.byteLength(input.body);
15
30
  const limits = this.#options.limits;
@@ -19,7 +34,9 @@ export class HarneryInboxService {
19
34
  const record = {
20
35
  schema: HARNERY_INBOX_RECORD_SCHEMA,
21
36
  kind: "message",
22
- message_id: `msg_${this.#options.id?.() ?? randomUUID()}`,
37
+ message_id: input.message_id === undefined
38
+ ? `msg_${this.#options.id?.() ?? randomUUID()}`
39
+ : opaqueId(input.message_id, "message"),
23
40
  sender_instance_id: senderId(input.sender_instance_id),
24
41
  sender_display_name: boundedDisplay(input.sender_display_name),
25
42
  recipient_instance_id: recipient,
@@ -31,6 +48,21 @@ export class HarneryInboxService {
31
48
  const path = inboxPath(this.#options.coord_root, recipient);
32
49
  return updateInboxHistory(path, readOptions(limits), (records) => {
33
50
  const projection = project(records);
51
+ const existing = records.find((item) => item.kind === "message" && item.message_id === record.message_id);
52
+ if (existing) {
53
+ assertSameMessage(existing, record);
54
+ return {
55
+ result: {
56
+ state: "duplicate",
57
+ message_id: existing.message_id,
58
+ body_bytes: existing.body_bytes,
59
+ synced: true,
60
+ },
61
+ };
62
+ }
63
+ if (input.replay_only) {
64
+ throw new HarneryInboxMessageHistoryMissingError(record.message_id);
65
+ }
34
66
  capacityCheck(projection, record, path, limits);
35
67
  return {
36
68
  append: [record],
@@ -43,6 +75,40 @@ export class HarneryInboxService {
43
75
  };
44
76
  });
45
77
  }
78
+ /** Append a message created on another machine, keeping its id. Importers
79
+ * on one machine can race, so the duplicate check runs under the lease. */
80
+ receive(record) {
81
+ const recipient = opaqueId(record.recipient_instance_id, "recipient instance");
82
+ const limits = this.#options.limits;
83
+ if (record.body_bytes === 0 || record.body_bytes > limits.max_message_body_bytes) {
84
+ throw new HarneryInboxCapacityError("message_body_limit", record.body_bytes, limits.max_message_body_bytes);
85
+ }
86
+ const incoming = {
87
+ schema: HARNERY_INBOX_RECORD_SCHEMA,
88
+ kind: "message",
89
+ message_id: opaqueId(record.message_id, "message"),
90
+ sender_instance_id: senderId(record.sender_instance_id),
91
+ sender_display_name: boundedDisplay(record.sender_display_name),
92
+ ...(record.sender_machine ? { sender_machine: boundedDisplay(record.sender_machine) } : {}),
93
+ recipient_instance_id: recipient,
94
+ recipient_display_name: boundedDisplay(record.recipient_display_name),
95
+ created_at: record.created_at,
96
+ body: record.body,
97
+ body_bytes: record.body_bytes,
98
+ };
99
+ validateRecord(incoming);
100
+ const path = inboxPath(this.#options.coord_root, recipient);
101
+ return updateInboxHistory(path, readOptions(limits), (records) => {
102
+ const projection = project(records);
103
+ const existing = records.find((item) => item.kind === "message" && item.message_id === incoming.message_id);
104
+ if (existing) {
105
+ assertSameMessage(existing, incoming);
106
+ return { result: "duplicate" };
107
+ }
108
+ capacityCheck(projection, incoming, path, limits);
109
+ return { append: [incoming], result: "received" };
110
+ });
111
+ }
46
112
  records(recipientInstanceId) {
47
113
  const records = readCrashSafeJsonlFile(inboxPath(this.#options.coord_root, opaqueId(recipientInstanceId, "recipient instance")), readOptions(this.#options.limits));
48
114
  for (const record of records)
@@ -125,6 +191,20 @@ export class HarneryInboxService {
125
191
  }
126
192
  }
127
193
  }
194
+ function assertSameMessage(existing, incoming) {
195
+ const fields = [
196
+ "sender_instance_id",
197
+ "sender_display_name",
198
+ "sender_machine",
199
+ "recipient_instance_id",
200
+ "recipient_display_name",
201
+ "body",
202
+ "body_bytes",
203
+ ];
204
+ if (fields.some((field) => existing[field] !== incoming[field])) {
205
+ throw new HarneryInboxMessageConflictError(incoming.message_id);
206
+ }
207
+ }
128
208
  /** The shared writer refuses an occupied lease before invoking the update.
129
209
  * Briefly retry only that exact refusal so concurrent peer senders both get
130
210
  * a chance to append. All validation, authority and I/O failures still fail
@@ -1,6 +1,9 @@
1
+ import { type CoordinationViewV3 } from "../events/v3/coordination-view.js";
1
2
  export interface MessageTargetInput {
2
3
  name?: string;
3
4
  instanceId?: string;
5
+ /** Refuse a later generation of the same immutable instance. */
6
+ generationId?: string;
4
7
  requireLive?: boolean;
5
8
  now?: Date;
6
9
  }
@@ -9,14 +12,25 @@ export type MessageTargetResult = {
9
12
  target: {
10
13
  instance_id: string;
11
14
  name: string;
12
- delivery: "written-live" | "queued";
15
+ delivery: "written-live" | "queued" | "remote";
16
+ /** Set when the recipient lives on another machine (ADR 0222). */
17
+ machine?: string;
13
18
  };
14
19
  } | {
15
20
  ok: false;
16
- code: "no_peer" | "ambiguous_peer" | "not_live" | "authority_unsafe";
21
+ code: "no_peer" | "ambiguous_peer" | "not_live" | "authority_unsafe" | "generation_mismatch";
17
22
  message: string;
18
23
  candidates?: string[];
19
24
  };
20
25
  /** Resolve one immutable recipient without extending presence or claiming attention. */
21
- export declare function resolveMessageTarget(coordRoot: string, input: MessageTargetInput): MessageTargetResult;
26
+ export declare function resolveMessageTarget(coordRoot: string, input: MessageTargetInput, options?: {
27
+ view?: CoordinationViewV3;
28
+ }): MessageTargetResult;
29
+ /**
30
+ * Resolve a recipient on another machine, for a name or instance this
31
+ * machine's ledger has never seen. Candidates come from peer presence (live
32
+ * or stale) and from senders this machine has received mail from. Returns
33
+ * null when nothing matches, so the caller can report the local miss.
34
+ */
35
+ export declare function resolveRemoteMessageTarget(coordRoot: string, input: MessageTargetInput): MessageTargetResult | null;
22
36
  //# sourceMappingURL=targets.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"targets.d.ts","sourceRoot":"","sources":["../../../src/core/inbox/targets.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED,MAAM,MAAM,mBAAmB,GAC3B;IACE,EAAE,EAAE,IAAI,CAAC;IACT,MAAM,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,cAAc,GAAG,QAAQ,CAAA;KAAE,CAAC;CACpF,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,IAAI,EAAE,SAAS,GAAG,gBAAgB,GAAG,UAAU,GAAG,kBAAkB,CAAC;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB,CAAC;AAEN,wFAAwF;AACxF,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,kBAAkB,GACxB,mBAAmB,CAwFrB"}
1
+ {"version":3,"file":"targets.d.ts","sourceRoot":"","sources":["../../../src/core/inbox/targets.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,kBAAkB,EAGxB,MAAM,mCAAmC,CAAC;AAS3C,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED,MAAM,MAAM,mBAAmB,GAC3B;IACE,EAAE,EAAE,IAAI,CAAC;IACT,MAAM,EAAE;QACN,WAAW,EAAE,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,cAAc,GAAG,QAAQ,GAAG,QAAQ,CAAC;QAC/C,kEAAkE;QAClE,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,IAAI,EAAE,SAAS,GAAG,gBAAgB,GAAG,UAAU,GAAG,kBAAkB,GAAG,qBAAqB,CAAC;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB,CAAC;AAEN,wFAAwF;AACxF,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,kBAAkB,EACzB,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,kBAAkB,CAAA;CAAO,GAC1C,mBAAmB,CAkGrB;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,kBAAkB,GACxB,mBAAmB,GAAG,IAAI,CA2D5B"}
@@ -2,8 +2,10 @@ import { resolveName } from "../agents/state/names.js";
2
2
  import { coordFreshnessSeconds } from "../config.js";
3
3
  import { readCoordinationViewV3, requireAuthoritySafeCoordinationViewV3, } from "../events/v3/coordination-view.js";
4
4
  import { liveInstanceIdV3, nativeInstanceIdV3, observeLiveEventLedgerRouteV3, } from "../events/v3/live-route-observer.js";
5
+ import { readRemoteMachines } from "../presence/index.js";
6
+ import { knownRemoteSenders } from "../presence/mail.js";
5
7
  /** Resolve one immutable recipient without extending presence or claiming attention. */
6
- export function resolveMessageTarget(coordRoot, input) {
8
+ export function resolveMessageTarget(coordRoot, input, options = {}) {
7
9
  const requestedName = normalizeName(input.name);
8
10
  const requestedInstance = input.instanceId?.trim();
9
11
  if (!requestedName && !requestedInstance) {
@@ -17,7 +19,7 @@ export function resolveMessageTarget(coordRoot, input) {
17
19
  if (observeLiveEventLedgerRouteV3(coordRoot).state === "blocked") {
18
20
  return unsafe("The event ledger cannot establish message recipient authority.");
19
21
  }
20
- view = requireAuthoritySafeCoordinationViewV3(readCoordinationViewV3(coordRoot));
22
+ view = requireAuthoritySafeCoordinationViewV3(options.view ?? readCoordinationViewV3(coordRoot));
21
23
  if (!view.source_complete)
22
24
  return unsafe("The event ledger read is incomplete.");
23
25
  }
@@ -50,6 +52,7 @@ export function resolveMessageTarget(coordRoot, input) {
50
52
  {
51
53
  instanceId,
52
54
  name,
55
+ generationId: generation.generation_id,
53
56
  safe: !diagnostics.some((diagnostic) => diagnostic.authority_blocking),
54
57
  fresh: generation.phase === "live" && Number.isFinite(age) && age >= 0 && age <= freshnessMs,
55
58
  },
@@ -76,6 +79,13 @@ export function resolveMessageTarget(coordRoot, input) {
76
79
  };
77
80
  }
78
81
  const target = selected[0];
82
+ if (input.generationId && input.generationId !== target.generationId) {
83
+ return {
84
+ ok: false,
85
+ code: "generation_mismatch",
86
+ message: "The recipient generation differs from the exact requested generation.",
87
+ };
88
+ }
79
89
  if (input.requireLive && !target.fresh) {
80
90
  return {
81
91
  ok: false,
@@ -92,6 +102,71 @@ export function resolveMessageTarget(coordRoot, input) {
92
102
  },
93
103
  };
94
104
  }
105
+ /**
106
+ * Resolve a recipient on another machine, for a name or instance this
107
+ * machine's ledger has never seen. Candidates come from peer presence (live
108
+ * or stale) and from senders this machine has received mail from. Returns
109
+ * null when nothing matches, so the caller can report the local miss.
110
+ */
111
+ export function resolveRemoteMessageTarget(coordRoot, input) {
112
+ const requestedName = normalizeName(input.name);
113
+ const requestedInstance = input.instanceId?.trim();
114
+ if (!requestedName && !requestedInstance)
115
+ return null;
116
+ // Presence/mail names do not prove an exact V3 generation.
117
+ if (input.generationId)
118
+ return null;
119
+ const live = new Set(readRemoteMachines(coordRoot).flatMap(({ agents }) => agents.map((a) => a.instance_id)));
120
+ const known = new Map();
121
+ for (const { machine, agents } of readRemoteMachines(coordRoot, { includeStale: true })) {
122
+ for (const agent of agents) {
123
+ if (agent.name)
124
+ known.set(agent.instance_id, { instanceId: agent.instance_id, name: agent.name, machine });
125
+ }
126
+ }
127
+ for (const sender of knownRemoteSenders(coordRoot)) {
128
+ if (!known.has(sender.instance_id)) {
129
+ known.set(sender.instance_id, {
130
+ instanceId: sender.instance_id,
131
+ name: sender.name,
132
+ machine: sender.machine,
133
+ });
134
+ }
135
+ }
136
+ const candidates = [...known.values()]
137
+ .filter(({ instanceId }) => !requestedInstance || instanceId === requestedInstance)
138
+ .filter(({ name }) => !requestedName || normalizeName(name) === requestedName)
139
+ .map((candidate) => ({ ...candidate, fresh: live.has(candidate.instanceId) }));
140
+ if (candidates.length === 0)
141
+ return null;
142
+ const fresh = candidates.filter(({ fresh }) => fresh);
143
+ const selected = requestedInstance ? candidates : fresh.length > 0 ? fresh : candidates;
144
+ if (selected.length !== 1) {
145
+ return {
146
+ ok: false,
147
+ code: "ambiguous_peer",
148
+ message: "Several agent instances on other machines hold that exact name; specify --instance-id.",
149
+ candidates: selected.map(({ instanceId, machine }) => `${instanceId} (${machine})`).sort(),
150
+ };
151
+ }
152
+ const target = selected[0];
153
+ if (input.requireLive && !target.fresh) {
154
+ return {
155
+ ok: false,
156
+ code: "not_live",
157
+ message: "That agent is on another machine and outside live presence; omit --require-live to queue its message.",
158
+ };
159
+ }
160
+ return {
161
+ ok: true,
162
+ target: {
163
+ instance_id: target.instanceId,
164
+ name: target.name,
165
+ delivery: "remote",
166
+ machine: target.machine,
167
+ },
168
+ };
169
+ }
95
170
  function laterPosition(left, right) {
96
171
  return (left.source_position.segment_ordinal > right.source_position.segment_ordinal ||
97
172
  (left.source_position.segment_ordinal === right.source_position.segment_ordinal &&
@@ -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
@@ -12,9 +13,12 @@
12
13
  * detached (hook path: never block a turn) or synchronously (CLI path: the
13
14
  * user wants to see errors).
14
15
  */
15
- /** The ref namespace. Hardcoded — never interpolated from config — so the
16
- * publisher is structurally unable to touch refs/heads/* (ADR 0016). */
16
+ /** The ref namespaces. Hardcoded — never interpolated from config — so the
17
+ * publisher is structurally unable to touch refs/heads/* (ADR 0016). Mail
18
+ * refs carry cross-machine peer messages on the same transport (ADR 0222). */
17
19
  export declare const PRESENCE_REF_PREFIX = "refs/harnery/presence/";
20
+ export declare const MAIL_REF_PREFIX = "refs/harnery/mail/";
21
+ type HarneryRefPrefix = typeof PRESENCE_REF_PREFIX | typeof MAIL_REF_PREFIX;
18
22
  /** Does the repo have an `origin` remote to rendezvous on? */
19
23
  export declare function hasOrigin(root: string): boolean;
20
24
  /** The origin remote URL, or null. Input to relay room derivation. */
@@ -34,25 +38,26 @@ export declare function sanitizeRefComponent(label: string): string;
34
38
  * Returns the commit sha, or null on failure. Local-only, fast (~10ms). */
35
39
  export declare function writePresenceCommit(root: string, message: string): string | null;
36
40
  /**
37
- * Force-push a presence commit to `refs/harnery/presence/<machine>`.
38
- * `sync: true` (CLI/tests) waits and reports; default is detached fire-and-
39
- * forget (hook path — a slow or credential-less push must not block a turn).
41
+ * Force-push a commit to `<prefix><machine>` (presence or mail). `sync: true`
42
+ * (CLI/tests) waits and reports; default is detached fire-and-forget (hook
43
+ * path — a slow or credential-less push must not block a turn).
40
44
  */
41
- export declare function pushPresenceRef(root: string, machine: string, sha: string, opts?: {
45
+ export declare function pushMachineRef(root: string, prefix: HarneryRefPrefix, machine: string, sha: string, opts?: {
42
46
  sync?: boolean;
43
47
  }): {
44
48
  ok: boolean;
45
49
  error?: string;
46
50
  };
47
- /** Fetch every peer presence ref from origin into the local ref namespace. */
48
- export declare function fetchPresenceRefs(root: string, opts?: {
51
+ /** Fetch every peer presence and mail ref from origin into the local ref
52
+ * namespace, in one round trip. A namespace with no refs is not an error. */
53
+ export declare function fetchMachineRefs(root: string, opts?: {
49
54
  sync?: boolean;
50
55
  }): {
51
56
  ok: boolean;
52
57
  error?: string;
53
58
  };
54
- /** Read every locally-known presence ref → [{ref, machine, message}]. */
55
- export declare function readPresenceRefs(root: string): Array<{
59
+ /** Read every locally-known ref under `prefix` → [{machine, message}]. */
60
+ export declare function readMachineRefs(root: string, prefix?: HarneryRefPrefix): Array<{
56
61
  machine: string;
57
62
  message: string;
58
63
  }>;
@@ -61,8 +66,9 @@ export declare function readPresenceRefs(root: string): Array<{
61
66
  * Records are %01-terminated (contents may contain newlines); refname and
62
67
  * contents are %00-separated. Exported for unit tests.
63
68
  */
64
- export declare function parseForEachRefOutput(out: string): Array<{
69
+ export declare function parseForEachRefOutput(out: string, prefix?: HarneryRefPrefix): Array<{
65
70
  machine: string;
66
71
  message: string;
67
72
  }>;
73
+ export {};
68
74
  //# sourceMappingURL=git.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../../src/core/presence/git.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;wEACwE;AACxE,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAwC5D,8DAA8D;AAC9D,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIrD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASzD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS1D;AAED;2EAC2E;AAC3E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOhF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC5B;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAcjC;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC5B;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAcjC;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAQ1F;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAe9F"}
1
+ {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../../src/core/presence/git.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH;;8EAE8E;AAC9E,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAC5D,eAAO,MAAM,eAAe,uBAAuB,CAAC;AACpD,KAAK,gBAAgB,GAAG,OAAO,mBAAmB,GAAG,OAAO,eAAe,CAAC;AAwC5E,8DAA8D;AAC9D,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIrD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASzD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS1D;AAED;2EAC2E;AAC3E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOhF;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC5B;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAGjC;AAED;6EAC6E;AAC7E,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC5B;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAGjC;AAwBD,0EAA0E;AAC1E,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,gBAAsC,GAC7C,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAI7C;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,gBAAsC,GAC7C,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAe7C"}
@@ -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
@@ -13,9 +14,11 @@
13
14
  * user wants to see errors).
14
15
  */
15
16
  import { spawn, spawnSync } from "node:child_process";
16
- /** The ref namespace. Hardcoded — never interpolated from config — so the
17
- * publisher is structurally unable to touch refs/heads/* (ADR 0016). */
17
+ /** The ref namespaces. Hardcoded — never interpolated from config — so the
18
+ * publisher is structurally unable to touch refs/heads/* (ADR 0016). Mail
19
+ * refs carry cross-machine peer messages on the same transport (ADR 0222). */
18
20
  export const PRESENCE_REF_PREFIX = "refs/harnery/presence/";
21
+ export const MAIL_REF_PREFIX = "refs/harnery/mail/";
19
22
  /** Well-known SHA of git's empty tree; `git mktree` below guarantees the
20
23
  * object exists in the local odb before we build a commit on it. */
21
24
  const EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
@@ -99,36 +102,31 @@ export function writePresenceCommit(root, message) {
99
102
  return r.ok && r.stdout.trim() ? r.stdout.trim() : null;
100
103
  }
101
104
  /**
102
- * Force-push a presence commit to `refs/harnery/presence/<machine>`.
103
- * `sync: true` (CLI/tests) waits and reports; default is detached fire-and-
104
- * forget (hook path — a slow or credential-less push must not block a turn).
105
+ * Force-push a commit to `<prefix><machine>` (presence or mail). `sync: true`
106
+ * (CLI/tests) waits and reports; default is detached fire-and-forget (hook
107
+ * path — a slow or credential-less push must not block a turn).
105
108
  */
106
- export function pushPresenceRef(root, machine, sha, opts = {}) {
107
- const refspec = `+${sha}:${PRESENCE_REF_PREFIX}${sanitizeRefComponent(machine)}`;
108
- const args = ["-C", root, "push", "--quiet", "--no-verify", "origin", refspec];
109
- if (opts.sync) {
110
- const r = runGit(root, args.slice(2));
111
- return r.ok ? { ok: true } : { ok: false, error: r.stderr.trim().slice(0, 300) };
112
- }
113
- try {
114
- const child = spawn("git", args, { detached: true, stdio: "ignore", env: gitEnv() });
115
- child.unref();
116
- return { ok: true };
117
- }
118
- catch {
119
- return { ok: false, error: "spawn failed" };
120
- }
109
+ export function pushMachineRef(root, prefix, machine, sha, opts = {}) {
110
+ const refspec = `+${sha}:${prefix}${sanitizeRefComponent(machine)}`;
111
+ return runNetworkGit(root, ["push", "--quiet", "--no-verify", "origin", refspec], opts);
121
112
  }
122
- /** Fetch every peer presence ref from origin into the local ref namespace. */
123
- export function fetchPresenceRefs(root, opts = {}) {
124
- const refspec = `+${PRESENCE_REF_PREFIX}*:${PRESENCE_REF_PREFIX}*`;
125
- const args = ["-C", root, "fetch", "--quiet", "--no-tags", "origin", refspec];
113
+ /** Fetch every peer presence and mail ref from origin into the local ref
114
+ * namespace, in one round trip. A namespace with no refs is not an error. */
115
+ export function fetchMachineRefs(root, opts = {}) {
116
+ const refspecs = [PRESENCE_REF_PREFIX, MAIL_REF_PREFIX].map((p) => `+${p}*:${p}*`);
117
+ return runNetworkGit(root, ["fetch", "--quiet", "--no-tags", "origin", ...refspecs], opts);
118
+ }
119
+ function runNetworkGit(root, args, opts) {
126
120
  if (opts.sync) {
127
- const r = runGit(root, args.slice(2));
121
+ const r = runGit(root, args);
128
122
  return r.ok ? { ok: true } : { ok: false, error: r.stderr.trim().slice(0, 300) };
129
123
  }
130
124
  try {
131
- const child = spawn("git", args, { detached: true, stdio: "ignore", env: gitEnv() });
125
+ const child = spawn("git", ["-C", root, ...args], {
126
+ detached: true,
127
+ stdio: "ignore",
128
+ env: gitEnv(),
129
+ });
132
130
  child.unref();
133
131
  return { ok: true };
134
132
  }
@@ -136,23 +134,19 @@ export function fetchPresenceRefs(root, opts = {}) {
136
134
  return { ok: false, error: "spawn failed" };
137
135
  }
138
136
  }
139
- /** Read every locally-known presence ref → [{ref, machine, message}]. */
140
- export function readPresenceRefs(root) {
141
- const r = runGit(root, [
142
- "for-each-ref",
143
- "--format=%(refname)%00%(contents)%01",
144
- `${PRESENCE_REF_PREFIX}*`,
145
- ]);
137
+ /** Read every locally-known ref under `prefix` → [{machine, message}]. */
138
+ export function readMachineRefs(root, prefix = PRESENCE_REF_PREFIX) {
139
+ const r = runGit(root, ["for-each-ref", "--format=%(refname)%00%(contents)%01", `${prefix}*`]);
146
140
  if (!r.ok)
147
141
  return [];
148
- return parseForEachRefOutput(r.stdout);
142
+ return parseForEachRefOutput(r.stdout, prefix);
149
143
  }
150
144
  /**
151
145
  * Parse `for-each-ref --format='%(refname)%00%(contents)%01'` output.
152
146
  * Records are %01-terminated (contents may contain newlines); refname and
153
147
  * contents are %00-separated. Exported for unit tests.
154
148
  */
155
- export function parseForEachRefOutput(out) {
149
+ export function parseForEachRefOutput(out, prefix = PRESENCE_REF_PREFIX) {
156
150
  const rows = [];
157
151
  for (const rec of out.split("\u0001")) {
158
152
  const trimmed = rec.replace(/^\n+/, "");
@@ -162,10 +156,10 @@ export function parseForEachRefOutput(out) {
162
156
  if (sep === -1)
163
157
  continue;
164
158
  const refname = trimmed.slice(0, sep).trim();
165
- if (!refname.startsWith(PRESENCE_REF_PREFIX))
159
+ if (!refname.startsWith(prefix))
166
160
  continue;
167
161
  rows.push({
168
- machine: refname.slice(PRESENCE_REF_PREFIX.length),
162
+ machine: refname.slice(prefix.length),
169
163
  message: trimmed.slice(sep + 1).trim(),
170
164
  });
171
165
  }
@@ -17,7 +17,7 @@
17
17
  import { type PresenceAgent } from "./blob.js";
18
18
  export type { PresenceAgent, PresenceBlob } from "./blob.js";
19
19
  export { buildPresenceBlob } from "./blob.js";
20
- export { PRESENCE_REF_PREFIX, parseForEachRefOutput, sanitizeRefComponent } from "./git.js";
20
+ export { MAIL_REF_PREFIX, PRESENCE_REF_PREFIX, parseForEachRefOutput, sanitizeRefComponent, } from "./git.js";
21
21
  export { ensureRelayDaemon, relayDaemonStatus, runRelayDaemon } from "./relay-client.js";
22
22
  export type PublishResult = {
23
23
  status: "published";
@@ -49,7 +49,7 @@ export declare function publishPresence(coordRoot: string, opts?: {
49
49
  force?: boolean;
50
50
  sync?: boolean;
51
51
  }): PublishResult;
52
- /** Throttled fetch of peer presence refs from origin. */
52
+ /** Throttled fetch of peer presence and mail refs from origin. */
53
53
  export declare function fetchPresence(coordRoot: string, opts?: {
54
54
  force?: boolean;
55
55
  sync?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/presence/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAgBH,OAAO,EAAqB,KAAK,aAAa,EAAqB,MAAM,WAAW,CAAC;AAUrF,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAC5F,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAYzF,MAAM,MAAM,aAAa,GACrB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACrE;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC,MAAM,MAAM,WAAW,GACnB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAwCvC;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7C,aAAa,CA2Cf;AAED,yDAAyD;AACzD,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7C,WAAW,CAsCb;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,GAC3D,aAAa,EAAE,CAuDjB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/presence/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAgBH,OAAO,EAAqB,KAAK,aAAa,EAAqB,MAAM,WAAW,CAAC;AAWrF,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAYzF,MAAM,MAAM,aAAa,GACrB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACrE;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC,MAAM,MAAM,WAAW,GACnB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAwCvC;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7C,aAAa,CA6Cf;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7C,WAAW,CAsCb;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,GAC3D,aAAa,EAAE,CAuDjB"}
@@ -20,9 +20,9 @@ import { coordEnv } from "../../lib/env.js";
20
20
  import { resolveMachineLabel } from "../../lib/machine.js";
21
21
  import { presenceEnabled } from "../config.js";
22
22
  import { buildPresenceBlob } from "./blob.js";
23
- import { fetchPresenceRefs, hasOrigin, pushPresenceRef, readPresenceRefs, sanitizeRefComponent, writePresenceCommit, } from "./git.js";
23
+ import { fetchMachineRefs, hasOrigin, PRESENCE_REF_PREFIX, pushMachineRef, readMachineRefs, sanitizeRefComponent, writePresenceCommit, } from "./git.js";
24
24
  export { buildPresenceBlob } from "./blob.js";
25
- export { PRESENCE_REF_PREFIX, parseForEachRefOutput, sanitizeRefComponent } from "./git.js";
25
+ export { MAIL_REF_PREFIX, PRESENCE_REF_PREFIX, parseForEachRefOutput, sanitizeRefComponent, } from "./git.js";
26
26
  export { ensureRelayDaemon, relayDaemonStatus, runRelayDaemon } from "./relay-client.js";
27
27
  /** Re-push at least this often while sessions are live, so peers can treat a
28
28
  * blob older than ~3x this as "machine offline" rather than "no change". */
@@ -95,7 +95,9 @@ export function publishPresence(coordRoot, opts = {}) {
95
95
  const sha = writePresenceCommit(coordRoot, json);
96
96
  if (!sha)
97
97
  return { status: "error", error: "commit-tree failed" };
98
- const push = pushPresenceRef(coordRoot, blob.machine, sha, { sync: opts.sync });
98
+ const push = pushMachineRef(coordRoot, PRESENCE_REF_PREFIX, blob.machine, sha, {
99
+ sync: opts.sync,
100
+ });
99
101
  if (!push.ok)
100
102
  return { status: "error", error: push.error ?? "push failed" };
101
103
  // Optimistic on the detached path: a failed background push self-heals at
@@ -111,7 +113,7 @@ export function publishPresence(coordRoot, opts = {}) {
111
113
  return { status: "error", error: e instanceof Error ? e.message : String(e) };
112
114
  }
113
115
  }
114
- /** Throttled fetch of peer presence refs from origin. */
116
+ /** Throttled fetch of peer presence and mail refs from origin. */
115
117
  export function fetchPresence(coordRoot, opts = {}) {
116
118
  try {
117
119
  if (!presenceEnabled(coordRoot))
@@ -144,7 +146,7 @@ export function fetchPresence(coordRoot, opts = {}) {
144
146
  catch {
145
147
  /* best-effort */
146
148
  }
147
- const r = fetchPresenceRefs(coordRoot, { sync: opts.sync });
149
+ const r = fetchMachineRefs(coordRoot, { sync: opts.sync });
148
150
  if (!r.ok)
149
151
  return { status: "error", error: r.error ?? "fetch failed" };
150
152
  return { status: "fetched" };
@@ -178,7 +180,7 @@ export function readRemoteMachines(coordRoot, opts = {}) {
178
180
  return;
179
181
  byMachine.set(machine, blob);
180
182
  };
181
- for (const { machine, message } of readPresenceRefs(coordRoot)) {
183
+ for (const { machine, message } of readMachineRefs(coordRoot, PRESENCE_REF_PREFIX)) {
182
184
  try {
183
185
  consider(machine, JSON.parse(message));
184
186
  }