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
@@ -0,0 +1,104 @@
1
+ import { basename, shellCommandFromTool, tokenize } from "./shell-tokens.js";
2
+ /**
3
+ * Deny `git commit-tree` in an agent's shell command. In a checkout shared by
4
+ * several agents, a peer's commit can hold the Git index lock for minutes
5
+ * while its pre-commit checks run. An agent that builds its own commit from a
6
+ * separate index with `git commit-tree` and pushes it gets around that lock,
7
+ * and with it every pre-commit check and the commit-time claim guard. The
8
+ * right move is to wait for the peer's commit to finish.
9
+ *
10
+ * A guardrail against the reflex, not a security boundary: it reads command
11
+ * positions only (so quoted examples and other tools pass), skips Git's
12
+ * global options, and looks inside `bash -c '...'`.
13
+ * HARNERY_ALLOW_COMMIT_TREE=1 on the command is the deliberate override.
14
+ */
15
+ const OVERRIDE = "HARNERY_ALLOW_COMMIT_TREE=1";
16
+ const SEPARATORS = new Set([";", "|", "||", "&", "&&", "(", ")", "{", "}"]);
17
+ const KEYWORDS = new Set(["do", "if", "then", "elif", "else", "!", "while", "until", "time"]);
18
+ const WRAPPERS = new Set(["command", "exec", "builtin", "nohup", "sudo", "doas", "env", "nice"]);
19
+ // Git global options that consume the following token as their value.
20
+ const GIT_VALUE_OPTIONS = new Set([
21
+ "-C",
22
+ "-c",
23
+ "--git-dir",
24
+ "--work-tree",
25
+ "--namespace",
26
+ "--exec-path",
27
+ ]);
28
+ function gitSubcommand(args) {
29
+ for (let j = 0; j < args.length; j++) {
30
+ const arg = args[j];
31
+ if (arg.operator)
32
+ return null;
33
+ if (arg.value.startsWith("-")) {
34
+ if (GIT_VALUE_OPTIONS.has(arg.value))
35
+ j++;
36
+ continue;
37
+ }
38
+ return arg.value;
39
+ }
40
+ return null;
41
+ }
42
+ function invokesCommitTree(source, depth = 0) {
43
+ if (depth > 8)
44
+ return false;
45
+ const tokens = tokenize(source);
46
+ if (tokens.some((t) => !t.quoted && t.value === OVERRIDE))
47
+ return false;
48
+ let start = true;
49
+ let afterWrapper = false;
50
+ for (let i = 0; i < tokens.length; i++) {
51
+ const token = tokens[i];
52
+ if (token.operator) {
53
+ if (SEPARATORS.has(token.value)) {
54
+ start = true;
55
+ afterWrapper = false;
56
+ }
57
+ continue;
58
+ }
59
+ if (!start)
60
+ continue;
61
+ if (!token.quoted) {
62
+ if (KEYWORDS.has(token.value))
63
+ continue;
64
+ if (/^[A-Za-z_][A-Za-z_0-9]*=/.test(token.value))
65
+ continue;
66
+ if (afterWrapper && token.value.startsWith("-"))
67
+ continue;
68
+ }
69
+ const name = basename(token.value);
70
+ if (WRAPPERS.has(name)) {
71
+ afterWrapper = true;
72
+ continue;
73
+ }
74
+ start = false;
75
+ afterWrapper = false;
76
+ if (token.quoted)
77
+ continue;
78
+ if (name === "git" && gitSubcommand(tokens.slice(i + 1)) === "commit-tree")
79
+ return true;
80
+ if (name === "git-commit-tree")
81
+ return true;
82
+ if (["bash", "sh", "dash", "zsh", "ksh"].includes(name)) {
83
+ for (let j = i + 1; j < tokens.length && !tokens[j].operator; j++) {
84
+ if (/^-[a-z]*c[a-z]*$/.test(tokens[j].value)) {
85
+ const script = tokens[j + 1];
86
+ if (script && !script.operator && invokesCommitTree(script.value, depth + 1))
87
+ return true;
88
+ break;
89
+ }
90
+ }
91
+ }
92
+ }
93
+ return false;
94
+ }
95
+ export function shellCommitTreeReason(toolName, toolInput, binName) {
96
+ const command = shellCommandFromTool(toolName, toolInput);
97
+ if (command === null || !invokesCommitTree(command))
98
+ return null;
99
+ return (`git commit-tree blocked: building a commit by hand gets around the Git index lock, and with ` +
100
+ `it every pre-commit check and the claim guard. When a peer's commit holds the lock, wait ` +
101
+ `for it to finish (\`${binName} admission status\` shows whether its checks are queued for a ` +
102
+ `slot), then commit normally. If you genuinely need plumbing, prefix the command with ` +
103
+ `${OVERRIDE}.`);
104
+ }
@@ -20,6 +20,8 @@ export interface HarneryInboxMessageRecordV1 {
20
20
  /** A required null identifies an operator; strings identify real agent instances. */
21
21
  sender_instance_id: string | null;
22
22
  sender_display_name: string;
23
+ /** Set only on a message imported from another machine (ADR 0222). */
24
+ sender_machine?: string;
23
25
  recipient_instance_id: string;
24
26
  recipient_display_name: string;
25
27
  created_at: string;
@@ -51,6 +53,16 @@ export declare class HarneryInboxCapacityError extends Error {
51
53
  readonly limit: number;
52
54
  constructor(reason_code: HarneryInboxCapacityReason, current: number, limit: number);
53
55
  }
56
+ export declare class HarneryInboxMessageConflictError extends Error {
57
+ readonly message_id: string;
58
+ readonly reason_code = "message_identity_conflict";
59
+ constructor(message_id: string);
60
+ }
61
+ export declare class HarneryInboxMessageHistoryMissingError extends Error {
62
+ readonly message_id: string;
63
+ readonly reason_code = "message_history_unknown";
64
+ constructor(message_id: string);
65
+ }
54
66
  export interface HarneryInboxSurfaceBudget {
55
67
  max_count: number;
56
68
  max_bytes: number;
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../src/core/inbox/contract.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,2BAA2B,EAAG,yBAAkC,CAAC;AAC9E,eAAO,MAAM,2BAA2B,EAAG,yBAAkC,CAAC;AAE9E,MAAM,WAAW,kBAAkB;IACjC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,OAAO,2BAA2B,CAAC;IAC3C,IAAI,EAAE,SAAS,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,qFAAqF;IACrF,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,OAAO,2BAA2B,CAAC;IAC3C,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,oBAAoB,GAAG,2BAA2B,GAAG,4BAA4B,CAAC;AAE9F,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,OAAO,2BAA2B,CAAC;IAC3C,qBAAqB,EAAE,MAAM,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,CAAC;IAC7C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,MAAM,0BAA0B,GAClC,oBAAoB,GACpB,qBAAqB,GACrB,qBAAqB,GACrB,qBAAqB,CAAC;AAE1B,qBAAa,yBAA0B,SAAQ,KAAK;IAEhD,QAAQ,CAAC,WAAW,EAAE,0BAA0B;IAChD,QAAQ,CAAC,OAAO,EAAE,MAAM;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM;gBAFb,WAAW,EAAE,0BAA0B,EACvC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM;CAMzB;AAED,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,SAAS,2BAA2B,EAAE,CAAC;IAChD,uBAAuB,EAAE,MAAM,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;IAChC,6BAA6B,EAAE,IAAI,CAAC;CACrC"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../src/core/inbox/contract.ts"],"names":[],"mappings":"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"}
@@ -11,3 +11,19 @@ export class HarneryInboxCapacityError extends Error {
11
11
  this.limit = limit;
12
12
  }
13
13
  }
14
+ export class HarneryInboxMessageConflictError extends Error {
15
+ message_id;
16
+ reason_code = "message_identity_conflict";
17
+ constructor(message_id) {
18
+ super(`inbox message identity conflict: ${message_id}`);
19
+ this.message_id = message_id;
20
+ }
21
+ }
22
+ export class HarneryInboxMessageHistoryMissingError extends Error {
23
+ message_id;
24
+ reason_code = "message_history_unknown";
25
+ constructor(message_id) {
26
+ super(`inbox message history unknown: ${message_id}`);
27
+ this.message_id = message_id;
28
+ }
29
+ }
@@ -1,37 +1,62 @@
1
- import type { HarneryInboxLimits, HarneryInboxMessageRecordV1 } from "./contract.js";
1
+ import { type AgentParentReference } from "../agents/state/parent-reference.js";
2
+ import { type HarneryInboxLimits, type HarneryInboxMessageRecordV1 } from "./contract.js";
2
3
  import { HarneryInboxService } from "./service.js";
3
- import { type MessageTargetInput } from "./targets.js";
4
+ import { type MessageTargetInput, type MessageTargetResult } from "./targets.js";
4
5
  /** Preserve the existing ping body/count bounds. Pending messages have no TTL;
5
6
  * the lifecycle planner protects them through the idle finalization window. */
6
7
  export declare const PEER_INBOX_LIMITS: HarneryInboxLimits;
7
8
  export declare function peerInbox(coordRoot: string): HarneryInboxService;
8
9
  /** CLI and web use this one path. A receipt proves a synced append, never that
9
10
  * a process is awake or that the recipient has noticed the message. */
10
- export declare function sendPeerMessage(input: {
11
+ export interface PeerMessageInput {
11
12
  coordRoot: string;
12
13
  target: MessageTargetInput;
13
14
  senderInstanceId: string | null;
14
15
  senderName: string;
15
16
  body: string;
16
- }): {
17
- ok: false;
18
- code: "no_peer" | "ambiguous_peer" | "not_live" | "authority_unsafe";
19
- message: string;
20
- candidates?: string[];
21
- } | {
22
- state: "queued" | "written-live";
17
+ /** Reuse the same caller-owned ID after a crash, with identical intent. */
18
+ operationId?: string;
19
+ /** A caller with an ambiguous prior dispatch must not create a new plan. */
20
+ operationReplayOnly?: boolean;
21
+ }
22
+ type PeerTarget = Extract<MessageTargetResult, {
23
+ ok: true;
24
+ }>["target"];
25
+ export type PeerRecipientResult = {
26
+ ok: true;
27
+ target: PeerTarget;
28
+ state: "written-live" | "queued" | "remote" | "duplicate";
23
29
  message_id: string;
24
30
  body_bytes: number;
25
31
  synced: true;
26
- ok: true;
27
- target: {
28
- instance_id: string;
29
- name: string;
30
- delivery: "written-live" | "queued";
31
- };
32
+ published?: boolean;
33
+ publish_error?: string;
34
+ delivery_state?: "pending" | "delivered" | "expired";
35
+ } | {
36
+ ok: false;
37
+ code: string;
38
+ message: string;
39
+ target?: PeerTarget;
40
+ candidates?: string[];
41
+ failure_stage?: "addressing" | "transport" | "operation";
42
+ };
43
+ type ParentCopyResult = PeerRecipientResult | {
44
+ state: "skipped";
45
+ reason: string;
46
+ };
47
+ export type PeerMessageResult = PeerRecipientResult & {
48
+ operation_id: string;
49
+ child: PeerRecipientResult;
50
+ parent_reference: AgentParentReference;
51
+ parent_copy: ParentCopyResult;
52
+ complete: boolean;
32
53
  };
54
+ /** One frozen routing intent, child first, then at most its direct parent.
55
+ * Receipts establish durable writes, never attention or a process wake. */
56
+ export declare function sendPeerMessage(input: PeerMessageInput): PeerMessageResult;
33
57
  export declare function formatPeerInbox(coordRoot: string, messages: readonly HarneryInboxMessageRecordV1[]): string;
34
58
  /** Wait for every earlier stdout write to finish before acknowledging context.
35
59
  * If output fails, the caller throws and leaves the batch pending. */
36
60
  export declare function acknowledgeEmittedPeerInbox(coordRoot: string, instanceId: string, messageIds: readonly string[]): Promise<void>;
61
+ export {};
37
62
  //# sourceMappingURL=peer-messages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"peer-messages.d.ts","sourceRoot":"","sources":["../../../src/core/inbox/peer-messages.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,MAAM,eAAe,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,KAAK,kBAAkB,EAAwB,MAAM,cAAc,CAAC;AAI7E;+EAC+E;AAC/E,eAAO,MAAM,iBAAiB,EAAE,kBAY/B,CAAC;AAEF,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB,CAEhE;AAED;uEACuE;AACvE,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;;;;;;;;;;;;;;;;EAWA;AAED,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,SAAS,2BAA2B,EAAE,GAC/C,MAAM,CA4BR;AAED;sEACsE;AACtE,wBAAsB,2BAA2B,CAC/C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,OAAO,CAAC,IAAI,CAAC,CAMf"}
1
+ {"version":3,"file":"peer-messages.d.ts","sourceRoot":"","sources":["../../../src/core/inbox/peer-messages.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,oBAAoB,EAE1B,MAAM,qCAAqC,CAAC;AAM7C,OAAO,EAEL,KAAK,kBAAkB,EAGvB,KAAK,2BAA2B,EACjC,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,mBAAmB,EAAwB,MAAM,cAAc,CAAC;AACzE,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EAGzB,MAAM,cAAc,CAAC;AAItB;+EAC+E;AAC/E,eAAO,MAAM,iBAAiB,EAAE,kBAY/B,CAAC;AAEF,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB,CAEhE;AAED;uEACuE;AACvE,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,KAAK,UAAU,GAAG,OAAO,CAAC,mBAAmB,EAAE;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;AACvE,MAAM,MAAM,mBAAmB,GAC3B;IACE,EAAE,EAAE,IAAI,CAAC;IACT,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAC;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,IAAI,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;CACtD,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,aAAa,CAAC,EAAE,YAAY,GAAG,WAAW,GAAG,WAAW,CAAC;CAC1D,CAAC;AACN,KAAK,gBAAgB,GAAG,mBAAmB,GAAG;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AACnF,MAAM,MAAM,iBAAiB,GAAG,mBAAmB,GAAG;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,mBAAmB,CAAC;IAC3B,gBAAgB,EAAE,oBAAoB,CAAC;IACvC,WAAW,EAAE,gBAAgB,CAAC;IAC9B,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAuBF;2EAC2E;AAC3E,wBAAgB,eAAe,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CA8P1E;AAoKD,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,SAAS,2BAA2B,EAAE,GAC/C,MAAM,CA6BR;AAED;sEACsE;AACtE,wBAAsB,2BAA2B,CAC/C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,OAAO,CAAC,IAAI,CAAC,CAMf"}
@@ -1,6 +1,14 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { join } from "node:path";
3
+ import { resolveAgentParentReference, } from "../agents/state/parent-reference.js";
1
4
  import { resolveBinName } from "../config.js";
2
- import { HarneryInboxService } from "./service.js";
3
- import { resolveMessageTarget } from "./targets.js";
5
+ import { readCoordinationViewV3 } from "../events/v3/coordination-view.js";
6
+ import { liveInstanceIdV3 } from "../events/v3/live-route-observer.js";
7
+ import { publishMail, queueMail } from "../presence/mail.js";
8
+ import { updateCrashSafeJsonlFile } from "../storage/durable-history.js";
9
+ import { HarneryInboxCapacityError, HarneryInboxMessageConflictError, HarneryInboxMessageHistoryMissingError, } from "./contract.js";
10
+ import { HarneryInboxService, stableInboxMessageId } from "./service.js";
11
+ import { resolveMessageTarget, resolveRemoteMessageTarget, } from "./targets.js";
4
12
  const DAY = 24 * 60 * 60_000;
5
13
  /** Preserve the existing ping body/count bounds. Pending messages have no TTL;
6
14
  * the lifecycle planner protects them through the idle finalization window. */
@@ -20,20 +28,368 @@ export const PEER_INBOX_LIMITS = {
20
28
  export function peerInbox(coordRoot) {
21
29
  return new HarneryInboxService({ coord_root: coordRoot, limits: PEER_INBOX_LIMITS });
22
30
  }
23
- /** CLI and web use this one path. A receipt proves a synced append, never that
24
- * a process is awake or that the recipient has noticed the message. */
31
+ const OPERATION_LIMITS = { max_record_bytes: 24_000, max_records: 5 };
32
+ /** One frozen routing intent, child first, then at most its direct parent.
33
+ * Receipts establish durable writes, never attention or a process wake. */
25
34
  export function sendPeerMessage(input) {
26
- const target = resolveMessageTarget(input.coordRoot, input.target);
27
- if (!target.ok)
28
- return target;
29
- const receipt = peerInbox(input.coordRoot).send({
30
- sender_instance_id: input.senderInstanceId,
31
- sender_display_name: input.senderName,
32
- recipient_instance_id: target.target.instance_id,
33
- recipient_display_name: target.target.name,
34
- body: input.body.trim(),
35
+ const operationId = input.operationId ?? randomUUID();
36
+ const unknown = { state: "unknown", reason: "original_target_unresolved" };
37
+ const refused = (code, message) => {
38
+ const child = { ok: false, code, message };
39
+ return {
40
+ ...child,
41
+ child,
42
+ operation_id: operationId,
43
+ parent_reference: unknown,
44
+ parent_copy: { state: "skipped", reason: "original_target_unresolved" },
45
+ complete: false,
46
+ };
47
+ };
48
+ if (!/^[A-Za-z0-9._:-]{1,160}$/.test(operationId))
49
+ return refused("invalid_operation_id", "Supply a bounded opaque operation ID.");
50
+ if (input.operationReplayOnly && !input.operationId)
51
+ return refused("invalid_operation_id", "Replay-only requires the original operation ID.");
52
+ if (!input.body.trim())
53
+ return refused("empty_message", "message is required (and non-empty)");
54
+ const request = hash(JSON.stringify({
55
+ target: {
56
+ name: input.target.name ?? null,
57
+ instance: input.target.instanceId ?? null,
58
+ generation: input.target.generationId ?? null,
59
+ require_live: input.target.requireLive ?? false,
60
+ },
61
+ sender: input.senderInstanceId,
62
+ sender_name: input.senderName,
63
+ body: input.body,
64
+ }));
65
+ const path = join(input.coordRoot, ".harnery", "peer-operations", `${hash(operationId)}.jsonl`);
66
+ let plan;
67
+ try {
68
+ const prepared = updateCrashSafeJsonlFile(path, OPERATION_LIMITS, (records) => {
69
+ if (records.length) {
70
+ validateOperationRecords(records, operationId);
71
+ const existing = records[0];
72
+ if (existing?.kind !== "plan" || existing.schema !== "harnery.peer-routing/v1")
73
+ throw new Error("routing operation has no valid frozen plan");
74
+ if (existing.request_sha256 !== request)
75
+ return {
76
+ result: {
77
+ ok: false,
78
+ code: "operation_conflict",
79
+ message: "Operation ID already binds different sender, body or target intent.",
80
+ },
81
+ };
82
+ return { result: existing };
83
+ }
84
+ if (input.operationReplayOnly)
85
+ return {
86
+ result: {
87
+ ok: false,
88
+ code: "message_history_unknown",
89
+ message: "The original routing plan is unavailable; no recipient was resubmitted.",
90
+ },
91
+ };
92
+ const view = readCoordinationViewV3(input.coordRoot);
93
+ let target = resolveMessageTarget(input.coordRoot, input.target, { view });
94
+ if (!target.ok && target.code === "no_peer") {
95
+ target = resolveRemoteMessageTarget(input.coordRoot, input.target) ?? target;
96
+ }
97
+ if (!target.ok)
98
+ return { result: target };
99
+ const generation = selectedGeneration(view, target.target.instance_id);
100
+ const reference = target.target.machine
101
+ ? { state: "unknown", reason: "remote_parent_evidence_unavailable" }
102
+ : resolveAgentParentReference(input.coordRoot, { instance_id: target.target.instance_id, generation_id: generation?.generation_id }, { view });
103
+ const child = {
104
+ target: target.target,
105
+ generation_id: generation?.generation_id,
106
+ body: input.body,
107
+ };
108
+ let parent = {
109
+ state: "skipped",
110
+ reason: reference.state === "verified" ? "parent_not_addressable" : reference.reason,
111
+ };
112
+ if (reference.state === "verified" && reference.addressable) {
113
+ const parentTarget = resolveMessageTarget(input.coordRoot, {
114
+ instanceId: reference.parent_v3_instance_id,
115
+ generationId: reference.parent_generation_id ?? undefined,
116
+ requireLive: input.target.requireLive,
117
+ now: input.target.now,
118
+ }, { view });
119
+ const body = `[Parent copy for agent-${target.target.name} (${target.target.instance_id})]\n${input.body}`;
120
+ parent = !parentTarget.ok
121
+ ? parentTarget
122
+ : Buffer.byteLength(body) > PEER_INBOX_LIMITS.max_message_body_bytes
123
+ ? {
124
+ ok: false,
125
+ code: "parent_copy_overflow",
126
+ message: "The parent marker and unchanged message exceed the 4000-byte message limit.",
127
+ target: parentTarget.target,
128
+ }
129
+ : {
130
+ target: parentTarget.target,
131
+ generation_id: reference.parent_generation_id ?? undefined,
132
+ body,
133
+ };
134
+ }
135
+ const created = {
136
+ kind: "plan",
137
+ schema: "harnery.peer-routing/v1",
138
+ request_sha256: request,
139
+ sender_instance_id: input.senderInstanceId,
140
+ sender_name: input.senderName,
141
+ require_live: input.target.requireLive ?? false,
142
+ child,
143
+ parent_reference: reference,
144
+ parent,
145
+ };
146
+ return { append: [created], result: created };
147
+ });
148
+ if (!("kind" in prepared))
149
+ return {
150
+ ...prepared,
151
+ child: prepared,
152
+ operation_id: operationId,
153
+ parent_reference: unknown,
154
+ parent_copy: { state: "skipped", reason: "original_target_unresolved" },
155
+ complete: false,
156
+ };
157
+ plan = prepared;
158
+ }
159
+ catch (error) {
160
+ return refused("operation_unavailable", error instanceof Error ? error.message : "Routing plan unavailable.");
161
+ }
162
+ const resolvePlanned = (recipient) => recipient.target.machine
163
+ ? resolveRemoteMessageTarget(input.coordRoot, {
164
+ instanceId: recipient.target.instance_id,
165
+ requireLive: plan.require_live,
166
+ now: input.target.now,
167
+ })
168
+ : resolveMessageTarget(input.coordRoot, {
169
+ instanceId: recipient.target.instance_id,
170
+ generationId: recipient.generation_id,
171
+ requireLive: plan.require_live,
172
+ now: input.target.now,
173
+ });
174
+ const deliver = (role, recipient, childOutcome) => {
175
+ try {
176
+ // Persist intent before transport. An interrupted prior attempt is only
177
+ // reconcilable from retained transport history, never from its absence.
178
+ const started = updateCrashSafeJsonlFile(path, OPERATION_LIMITS, (records) => {
179
+ validateOperationRecords(records, operationId);
180
+ const prior = records.find((r) => r.kind === "result" && r.recipient === role);
181
+ if (prior?.kind === "result")
182
+ return { result: prior.result };
183
+ // Historical receipts win; a new parent append requires the original
184
+ // child identity to remain safely addressable now.
185
+ if (role === "parent") {
186
+ const currentChild = resolvePlanned(plan.child);
187
+ if ((childOutcome && !childOutcome.ok && childOutcome.failure_stage !== "transport") ||
188
+ !currentChild?.ok ||
189
+ currentChild.target.machine !== plan.child.target.machine) {
190
+ return {
191
+ result: {
192
+ ok: false,
193
+ target: recipient.target,
194
+ failure_stage: "addressing",
195
+ code: "parent_copy_suppressed",
196
+ message: "Parent copy suppressed because the original task lost safe addressing.",
197
+ },
198
+ };
199
+ }
200
+ }
201
+ if (records.some((r) => r.kind === "started" && r.recipient === role))
202
+ return { result: { replay: true } };
203
+ return { append: [{ kind: "started", recipient: role }], result: { replay: false } };
204
+ });
205
+ if ("ok" in started)
206
+ return started;
207
+ return updateCrashSafeJsonlFile(path, OPERATION_LIMITS, (records) => {
208
+ validateOperationRecords(records, operationId);
209
+ const frozen = records[0];
210
+ if (frozen?.kind !== "plan" || frozen.request_sha256 !== request)
211
+ throw new Error("routing operation plan changed");
212
+ const prior = records.find((r) => r.kind === "result" && r.recipient === role);
213
+ if (prior?.kind === "result")
214
+ return { result: prior.result };
215
+ const current = resolvePlanned(recipient);
216
+ let result;
217
+ if (!current?.ok || current.target.machine !== recipient.target.machine) {
218
+ result =
219
+ current && !current.ok
220
+ ? { ...current, target: recipient.target, failure_stage: "addressing" }
221
+ : {
222
+ ok: false,
223
+ target: recipient.target,
224
+ code: "recipient_changed",
225
+ failure_stage: "addressing",
226
+ message: "The frozen recipient is no longer safely addressable on the same machine.",
227
+ };
228
+ }
229
+ else {
230
+ try {
231
+ result = deliverPeerRecipient(input.coordRoot, plan, recipient, operationId, current.target.delivery, started.replay);
232
+ }
233
+ catch (error) {
234
+ result = recipientError(error, recipient.target);
235
+ }
236
+ }
237
+ // A concurrent first attempt may still finish. Retain the start and
238
+ // report uncertainty, without turning absence into a permanent result.
239
+ if (!result.ok && result.code === "message_history_unknown")
240
+ return { result };
241
+ return { append: [{ kind: "result", recipient: role, result }], result };
242
+ });
243
+ }
244
+ catch (error) {
245
+ return recipientError(error, recipient.target, "operation");
246
+ }
247
+ };
248
+ const child = deliver("child", plan.child);
249
+ const parent = "body" in plan.parent ? deliver("parent", plan.parent, child) : plan.parent;
250
+ return {
251
+ ...child,
252
+ child,
253
+ operation_id: operationId,
254
+ parent_reference: plan.parent_reference,
255
+ parent_copy: parent,
256
+ complete: child.ok && (!("ok" in parent) || parent.ok),
257
+ };
258
+ }
259
+ function selectedGeneration(view, instance) {
260
+ const id = liveInstanceIdV3(instance);
261
+ return (view.instances[id] ??
262
+ Object.values(view.terminal_generations)
263
+ .filter((row) => row.instance_id === id)
264
+ .sort((a, b) => b.source_position.segment_ordinal - a.source_position.segment_ordinal ||
265
+ b.source_position.byte_offset - a.source_position.byte_offset)[0]);
266
+ }
267
+ function hash(value) {
268
+ return createHash("sha256").update(value).digest("hex");
269
+ }
270
+ function validateOperationRecords(records, operationId) {
271
+ const plan = records[0];
272
+ if (plan?.kind !== "plan" ||
273
+ plan.schema !== "harnery.peer-routing/v1" ||
274
+ typeof plan.request_sha256 !== "string" ||
275
+ typeof plan.sender_name !== "string" ||
276
+ (plan.sender_instance_id !== null && typeof plan.sender_instance_id !== "string") ||
277
+ typeof plan.require_live !== "boolean")
278
+ throw new Error("Invalid routing operation plan.");
279
+ const validRecipient = (r) => typeof r.body === "string" &&
280
+ r.target &&
281
+ typeof r.target.instance_id === "string" &&
282
+ typeof r.target.name === "string" &&
283
+ (r.generation_id === undefined || typeof r.generation_id === "string");
284
+ const validParent = "body" in plan.parent
285
+ ? validRecipient(plan.parent)
286
+ : "ok" in plan.parent
287
+ ? plan.parent.ok === false &&
288
+ typeof plan.parent.code === "string" &&
289
+ typeof plan.parent.message === "string"
290
+ : plan.parent.state === "skipped" && typeof plan.parent.reason === "string";
291
+ if (!validRecipient(plan.child) || !validParent)
292
+ throw new Error("Invalid frozen recipient plan.");
293
+ const started = new Set();
294
+ const completed = new Set();
295
+ for (const row of records.slice(1)) {
296
+ if (row.kind === "plan" || !["child", "parent"].includes(row.recipient))
297
+ throw new Error("Invalid routing operation record.");
298
+ if (row.kind === "started") {
299
+ if (started.has(row.recipient))
300
+ throw new Error("Duplicate recipient start.");
301
+ started.add(row.recipient);
302
+ continue;
303
+ }
304
+ if (row.kind !== "result" || !started.has(row.recipient) || completed.has(row.recipient))
305
+ throw new Error("Invalid recipient result sequence.");
306
+ completed.add(row.recipient);
307
+ const recipient = row.recipient === "child" ? plan.child : plan.parent;
308
+ const result = row.result;
309
+ if (!("body" in recipient) || !result || typeof result.ok !== "boolean")
310
+ throw new Error("Invalid recipient result.");
311
+ if (result.ok) {
312
+ if (result.target?.instance_id !== recipient.target.instance_id ||
313
+ result.message_id !== stableInboxMessageId(operationId, recipient.target.instance_id) ||
314
+ result.body_bytes !== Buffer.byteLength(recipient.body) ||
315
+ result.synced !== true ||
316
+ !["written-live", "queued", "remote", "duplicate"].includes(result.state))
317
+ throw new Error("Recipient receipt does not match frozen intent.");
318
+ }
319
+ else if (typeof result.code !== "string" || typeof result.message !== "string") {
320
+ throw new Error("Invalid recipient failure.");
321
+ }
322
+ }
323
+ }
324
+ function recipientError(error, target, failureStage = "transport") {
325
+ return {
326
+ ok: false,
327
+ target,
328
+ failure_stage: failureStage,
329
+ code: error instanceof HarneryInboxCapacityError
330
+ ? `inbox_${error.reason_code}`
331
+ : error instanceof HarneryInboxMessageHistoryMissingError ||
332
+ error instanceof HarneryInboxMessageConflictError
333
+ ? error.reason_code
334
+ : "recipient_delivery_failed",
335
+ message: error instanceof Error ? error.message : "Recipient delivery failed.",
336
+ };
337
+ }
338
+ function deliverPeerRecipient(coordRoot, plan, recipient, operationId, delivery, replayOnly) {
339
+ const target = recipient.target;
340
+ const messageId = stableInboxMessageId(operationId, target.instance_id);
341
+ if (target.machine) {
342
+ // Cross-machine (ADR 0222): queue in this machine's mail ref and push it
343
+ // now. A failed push keeps the message queued for the next hook publish.
344
+ const queued = queueMail(coordRoot, {
345
+ messageId,
346
+ replayOnly,
347
+ toMachine: target.machine,
348
+ senderInstanceId: plan.sender_instance_id,
349
+ senderName: plan.sender_name,
350
+ recipientInstanceId: target.instance_id,
351
+ recipientName: target.name,
352
+ body: recipient.body,
353
+ maxBodyBytes: PEER_INBOX_LIMITS.max_message_body_bytes,
354
+ });
355
+ const envelope = queued.envelope;
356
+ if (queued.delivery_state === "expired")
357
+ return {
358
+ ok: false,
359
+ target,
360
+ code: "remote_message_expired",
361
+ message: "The original remote message expired; it was not republished.",
362
+ };
363
+ const published = queued.delivery_state === "delivered"
364
+ ? null
365
+ : publishMail(coordRoot, { sync: true, force: true });
366
+ return {
367
+ ok: true,
368
+ target,
369
+ state: queued.state === "duplicate" ? "duplicate" : "remote",
370
+ message_id: envelope.message_id,
371
+ body_bytes: envelope.body_bytes,
372
+ synced: true,
373
+ delivery_state: queued.delivery_state,
374
+ published: published?.status === "published",
375
+ ...(published?.status === "error" ? { publish_error: published.error } : {}),
376
+ };
377
+ }
378
+ const receipt = peerInbox(coordRoot).send({
379
+ message_id: messageId,
380
+ replay_only: replayOnly,
381
+ sender_instance_id: plan.sender_instance_id,
382
+ sender_display_name: plan.sender_name,
383
+ recipient_instance_id: target.instance_id,
384
+ recipient_display_name: target.name,
385
+ body: recipient.body,
35
386
  });
36
- return { ok: true, target: target.target, ...receipt, state: target.target.delivery };
387
+ return {
388
+ ok: true,
389
+ target,
390
+ ...receipt,
391
+ state: receipt.state === "duplicate" ? "duplicate" : delivery,
392
+ };
37
393
  }
38
394
  export function formatPeerInbox(coordRoot, messages) {
39
395
  if (messages.length === 0)
@@ -47,7 +403,8 @@ export function formatPeerInbox(coordRoot, messages) {
47
403
  ? "Message for this task:"
48
404
  : `${messages.length} messages for this task:`;
49
405
  const body = messages
50
- .map((message) => ` ${message.message_id} from ${message.sender_instance_id === null ? "Operator" : `agent-${message.sender_display_name}`} (${message.created_at}):\n` +
406
+ .map((message) => ` ${message.message_id} from ${message.sender_instance_id === null ? "Operator" : `agent-${message.sender_display_name}`}` +
407
+ `${message.sender_machine ? ` on ${message.sender_machine}` : ""} (${message.created_at}):\n` +
51
408
  message.body
52
409
  .split("\n")
53
410
  .map((line) => ` ${line}`)