harnery 0.45.1 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/dist/commander.d.ts +13 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commands/admission.d.ts.map +1 -1
  4. package/dist/commands/admission.js +214 -27
  5. package/dist/commands/agents.d.ts +4 -0
  6. package/dist/commands/agents.d.ts.map +1 -1
  7. package/dist/commands/agents.js +102 -25
  8. package/dist/commands/browse.d.ts +3 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +172 -8
  11. package/dist/commands/fetch.js +1 -0
  12. package/dist/commands/ledger-v3.d.ts.map +1 -1
  13. package/dist/commands/ledger-v3.js +8 -2
  14. package/dist/commands/presence.d.ts.map +1 -1
  15. package/dist/commands/presence.js +40 -0
  16. package/dist/commands/qa-run.d.ts.map +1 -1
  17. package/dist/commands/qa-run.js +4 -3
  18. package/dist/commands/review-pack.d.ts.map +1 -1
  19. package/dist/commands/review-pack.js +33 -6
  20. package/dist/commands/workflow.d.ts.map +1 -1
  21. package/dist/commands/workflow.js +6 -45
  22. package/dist/core/agents/coord-client.d.ts.map +1 -1
  23. package/dist/core/agents/coord-client.js +0 -18
  24. package/dist/core/agents/index.d.ts +1 -0
  25. package/dist/core/agents/index.d.ts.map +1 -1
  26. package/dist/core/agents/index.js +1 -0
  27. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  28. package/dist/core/agents/live-authority-v3.js +11 -4
  29. package/dist/core/agents/render/session-context.d.ts +0 -6
  30. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  31. package/dist/core/agents/render/session-context.js +1 -6
  32. package/dist/core/agents/session-events.d.ts.map +1 -1
  33. package/dist/core/agents/session-events.js +12 -4
  34. package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
  35. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  36. package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
  37. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  38. package/dist/core/agents/state/live-coordination-view.js +6 -2
  39. package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
  40. package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
  41. package/dist/core/agents/state/parent-reference-contract.js +1 -0
  42. package/dist/core/agents/state/parent-reference.d.ts +18 -0
  43. package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
  44. package/dist/core/agents/state/parent-reference.js +119 -0
  45. package/dist/core/agents/state/pidmap.js +2 -2
  46. package/dist/core/agents/state/proc-start.d.ts +20 -31
  47. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  48. package/dist/core/agents/state/proc-start.js +37 -70
  49. package/dist/core/agents/state/service-process.d.ts +61 -0
  50. package/dist/core/agents/state/service-process.d.ts.map +1 -0
  51. package/dist/core/agents/state/service-process.js +143 -0
  52. package/dist/core/config.d.ts +39 -0
  53. package/dist/core/config.d.ts.map +1 -1
  54. package/dist/core/config.js +63 -0
  55. package/dist/core/context/index.d.ts +6 -4
  56. package/dist/core/context/index.d.ts.map +1 -1
  57. package/dist/core/context/index.js +194 -256
  58. package/dist/core/context/observation.d.ts +9 -9
  59. package/dist/core/context/observation.d.ts.map +1 -1
  60. package/dist/core/context/observation.js +37 -6
  61. package/dist/core/diagnostics/path-identity.d.ts +1 -1
  62. package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
  63. package/dist/core/diagnostics/path-identity.js +51 -6
  64. package/dist/core/events/v3/canonical.js +43 -14
  65. package/dist/core/events/v3/index.d.ts +1 -1
  66. package/dist/core/events/v3/index.d.ts.map +1 -1
  67. package/dist/core/events/v3/index.js +1 -1
  68. package/dist/core/events/v3/producers/recorder.d.ts +34 -0
  69. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  70. package/dist/core/events/v3/producers/recorder.js +117 -13
  71. package/dist/core/events/v3/reader.d.ts.map +1 -1
  72. package/dist/core/events/v3/reader.js +53 -13
  73. package/dist/core/events/v3/runtime-build.d.ts +14 -0
  74. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  75. package/dist/core/events/v3/runtime-build.js +44 -0
  76. package/dist/core/events/v3/validate.d.ts.map +1 -1
  77. package/dist/core/events/v3/validate.js +36 -1
  78. package/dist/core/governor/index.d.ts +2 -1
  79. package/dist/core/governor/index.d.ts.map +1 -1
  80. package/dist/core/governor/index.js +2 -1
  81. package/dist/core/governor/service-read.d.ts +3 -10
  82. package/dist/core/governor/service-read.d.ts.map +1 -1
  83. package/dist/core/governor/service-read.js +10 -94
  84. package/dist/core/governor/service.d.ts +3 -7
  85. package/dist/core/governor/service.d.ts.map +1 -1
  86. package/dist/core/governor/service.js +17 -182
  87. package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
  88. package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
  89. package/dist/core/hooks/adapter/native-parent.js +91 -0
  90. package/dist/core/hooks/cli.js +79 -4
  91. package/dist/core/hooks/resolve/owner.d.ts +0 -11
  92. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  93. package/dist/core/hooks/resolve/owner.js +2 -25
  94. package/dist/core/hooks/shell-admission.d.ts +84 -0
  95. package/dist/core/hooks/shell-admission.d.ts.map +1 -0
  96. package/dist/core/hooks/shell-admission.js +622 -0
  97. package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
  98. package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
  99. package/dist/core/hooks/shell-commit-tree.js +104 -0
  100. package/dist/core/inbox/contract.d.ts +12 -0
  101. package/dist/core/inbox/contract.d.ts.map +1 -1
  102. package/dist/core/inbox/contract.js +16 -0
  103. package/dist/core/inbox/peer-messages.d.ts +41 -16
  104. package/dist/core/inbox/peer-messages.d.ts.map +1 -1
  105. package/dist/core/inbox/peer-messages.js +372 -15
  106. package/dist/core/inbox/service.d.ts +8 -1
  107. package/dist/core/inbox/service.d.ts.map +1 -1
  108. package/dist/core/inbox/service.js +83 -3
  109. package/dist/core/inbox/targets.d.ts +17 -3
  110. package/dist/core/inbox/targets.d.ts.map +1 -1
  111. package/dist/core/inbox/targets.js +77 -2
  112. package/dist/core/presence/git.d.ts +18 -12
  113. package/dist/core/presence/git.d.ts.map +1 -1
  114. package/dist/core/presence/git.js +32 -38
  115. package/dist/core/presence/index.d.ts +2 -2
  116. package/dist/core/presence/index.d.ts.map +1 -1
  117. package/dist/core/presence/index.js +8 -6
  118. package/dist/core/presence/mail.d.ts +133 -0
  119. package/dist/core/presence/mail.d.ts.map +1 -0
  120. package/dist/core/presence/mail.js +464 -0
  121. package/dist/core/resources/sampler.js +2 -2
  122. package/dist/core/resources/service-status.d.ts +2 -14
  123. package/dist/core/resources/service-status.d.ts.map +1 -1
  124. package/dist/core/resources/service-status.js +3 -23
  125. package/dist/core/resources/service.d.ts +3 -7
  126. package/dist/core/resources/service.d.ts.map +1 -1
  127. package/dist/core/resources/service.js +12 -101
  128. package/dist/core/resources/status.d.ts.map +1 -1
  129. package/dist/core/resources/status.js +3 -2
  130. package/dist/core/semantic/contract.d.ts +5 -5
  131. package/dist/core/semantic/service.d.ts.map +1 -1
  132. package/dist/core/semantic/service.js +9 -2
  133. package/dist/core/servers/index.d.ts +0 -2
  134. package/dist/core/servers/index.d.ts.map +1 -1
  135. package/dist/core/servers/index.js +3 -18
  136. package/dist/core/storage/modes.d.ts +4 -0
  137. package/dist/core/storage/modes.d.ts.map +1 -1
  138. package/dist/core/storage/modes.js +4 -0
  139. package/dist/core/storage/segments.d.ts.map +1 -1
  140. package/dist/core/storage/segments.js +5 -4
  141. package/dist/core/supervisor/service.d.ts +4 -7
  142. package/dist/core/supervisor/service.d.ts.map +1 -1
  143. package/dist/core/supervisor/service.js +23 -122
  144. package/dist/core/supervisor/services.js +2 -2
  145. package/dist/core/supervisor/status.d.ts +3 -17
  146. package/dist/core/supervisor/status.d.ts.map +1 -1
  147. package/dist/core/supervisor/status.js +7 -70
  148. package/dist/core/workflow/engine.js +2 -1
  149. package/dist/core/workflow/index.d.ts +3 -4
  150. package/dist/core/workflow/index.d.ts.map +1 -1
  151. package/dist/core/workflow/index.js +2 -2
  152. package/dist/core/workflow/proof.d.ts.map +1 -1
  153. package/dist/core/workflow/proof.js +30 -74
  154. package/dist/core/workflow/resume-lock.d.ts +17 -0
  155. package/dist/core/workflow/resume-lock.d.ts.map +1 -0
  156. package/dist/core/workflow/resume-lock.js +158 -0
  157. package/dist/core/workflow/run-state.d.ts +0 -21
  158. package/dist/core/workflow/run-state.d.ts.map +1 -1
  159. package/dist/core/workflow/run-state.js +3 -174
  160. package/dist/core/workflow/workspaces/leases.d.ts +4 -0
  161. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  162. package/dist/core/workflow/workspaces/leases.js +1 -1
  163. package/dist/core/workflow/workspaces/local-git.js +91 -210
  164. package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
  165. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
  166. package/dist/core/workflow/workspaces/ownership.js +3 -130
  167. package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
  168. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  169. package/dist/core/workflow/workspaces/reclaim.js +0 -4
  170. package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
  171. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  172. package/dist/core/workflow/workspaces/recovery.js +1 -1
  173. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  174. package/dist/core/workflow/workspaces/state.js +1 -4
  175. package/dist/lib/admission-history.d.ts +96 -0
  176. package/dist/lib/admission-history.d.ts.map +1 -0
  177. package/dist/lib/admission-history.js +196 -0
  178. package/dist/lib/admission.d.ts +56 -7
  179. package/dist/lib/admission.d.ts.map +1 -1
  180. package/dist/lib/admission.js +161 -19
  181. package/dist/lib/browser/client.d.ts +18 -0
  182. package/dist/lib/browser/client.d.ts.map +1 -1
  183. package/dist/lib/browser/client.js +100 -12
  184. package/dist/lib/browser/content-checks.d.ts +3 -1
  185. package/dist/lib/browser/content-checks.d.ts.map +1 -1
  186. package/dist/lib/browser/content-checks.js +10 -2
  187. package/dist/lib/browser/critique.d.ts +32 -0
  188. package/dist/lib/browser/critique.d.ts.map +1 -1
  189. package/dist/lib/browser/critique.js +72 -3
  190. package/dist/lib/browser/geometry.d.ts +2 -0
  191. package/dist/lib/browser/geometry.d.ts.map +1 -1
  192. package/dist/lib/browser/geometry.js +61 -15
  193. package/dist/lib/browser/index.d.ts +1 -1
  194. package/dist/lib/browser/index.d.ts.map +1 -1
  195. package/dist/lib/browser/index.js +1 -1
  196. package/dist/lib/browser/page-review-contracts.d.ts +6 -1
  197. package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
  198. package/dist/lib/browser/page-review-contracts.js +6 -1
  199. package/dist/lib/browser/page-review-judge.d.ts +3 -1
  200. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  201. package/dist/lib/browser/page-review-judge.js +41 -11
  202. package/dist/lib/browser/page-review-pack.d.ts +3 -0
  203. package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
  204. package/dist/lib/browser/page-review-pack.js +5 -2
  205. package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
  206. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  207. package/dist/lib/browser/qa-run-contracts.js +11 -4
  208. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  209. package/dist/lib/browser/qa-run.js +73 -16
  210. package/dist/lib/browser/runts.d.ts +4 -2
  211. package/dist/lib/browser/runts.d.ts.map +1 -1
  212. package/dist/lib/browser/runts.js +84 -33
  213. package/dist/lib/browser/windows-chrome.d.ts +14 -0
  214. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  215. package/dist/lib/browser/windows-chrome.js +42 -0
  216. package/dist/lib/browser/windows-login-sync.d.ts +110 -0
  217. package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
  218. package/dist/lib/browser/windows-login-sync.js +171 -0
  219. package/dist/lib/cookies/client.d.ts +30 -5
  220. package/dist/lib/cookies/client.d.ts.map +1 -1
  221. package/dist/lib/cookies/client.js +55 -10
  222. package/dist/lib/cookies/index.d.ts +1 -1
  223. package/dist/lib/cookies/index.d.ts.map +1 -1
  224. package/dist/lib/cookies/index.js +1 -1
  225. package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
  226. package/dist/lib/docs-metadata-sync.js +41 -2
  227. package/dist/lib/durable-job.d.ts +8 -0
  228. package/dist/lib/durable-job.d.ts.map +1 -1
  229. package/dist/lib/durable-job.js +10 -0
  230. package/dist/lib/http/client.d.ts +9 -8
  231. package/dist/lib/http/client.d.ts.map +1 -1
  232. package/dist/lib/http/client.js +44 -2
  233. package/dist/lib/instructions/templates.js +4 -4
  234. package/package.json +2 -2
  235. package/src/commander.ts +13 -0
  236. package/src/commands/admission.ts +272 -29
  237. package/src/commands/agents.ts +132 -27
  238. package/src/commands/browse.ts +208 -6
  239. package/src/commands/fetch.ts +1 -0
  240. package/src/commands/ledger-v3.ts +8 -2
  241. package/src/commands/presence.ts +49 -0
  242. package/src/commands/qa-run.ts +4 -3
  243. package/src/commands/review-pack.ts +41 -5
  244. package/src/commands/workflow.ts +8 -58
  245. package/src/core/agents/coord-client.ts +0 -20
  246. package/src/core/agents/index.ts +6 -0
  247. package/src/core/agents/live-authority-v3.ts +11 -4
  248. package/src/core/agents/render/session-context.ts +1 -7
  249. package/src/core/agents/session-events.ts +18 -3
  250. package/src/core/agents/state/heartbeat-reader.ts +3 -0
  251. package/src/core/agents/state/live-coordination-view.ts +13 -6
  252. package/src/core/agents/state/parent-reference-contract.ts +14 -0
  253. package/src/core/agents/state/parent-reference.ts +179 -0
  254. package/src/core/agents/state/pidmap.ts +2 -2
  255. package/src/core/agents/state/proc-start.ts +41 -69
  256. package/src/core/agents/state/service-process.ts +217 -0
  257. package/src/core/config.ts +85 -0
  258. package/src/core/context/index.ts +207 -272
  259. package/src/core/context/observation.ts +40 -9
  260. package/src/core/diagnostics/path-identity.ts +52 -6
  261. package/src/core/events/v3/canonical.ts +46 -13
  262. package/src/core/events/v3/index.ts +3 -0
  263. package/src/core/events/v3/producers/recorder.ts +151 -13
  264. package/src/core/events/v3/reader.ts +55 -11
  265. package/src/core/events/v3/runtime-build.ts +62 -0
  266. package/src/core/events/v3/validate.ts +42 -1
  267. package/src/core/governor/index.ts +1 -2
  268. package/src/core/governor/service-read.ts +15 -114
  269. package/src/core/governor/service.ts +36 -220
  270. package/src/core/hooks/adapter/native-parent.ts +109 -0
  271. package/src/core/hooks/cli.ts +83 -4
  272. package/src/core/hooks/resolve/owner.ts +2 -25
  273. package/src/core/hooks/shell-admission.ts +634 -0
  274. package/src/core/hooks/shell-commit-tree.ts +102 -0
  275. package/src/core/inbox/contract.ts +18 -0
  276. package/src/core/inbox/peer-messages.ts +501 -15
  277. package/src/core/inbox/service.ts +108 -3
  278. package/src/core/inbox/targets.ts +94 -3
  279. package/src/core/presence/git.ts +45 -37
  280. package/src/core/presence/index.ts +16 -8
  281. package/src/core/presence/mail.ts +620 -0
  282. package/src/core/resources/sampler.ts +3 -2
  283. package/src/core/resources/service-status.ts +6 -35
  284. package/src/core/resources/service.ts +23 -133
  285. package/src/core/resources/status.ts +3 -2
  286. package/src/core/semantic/service.ts +8 -2
  287. package/src/core/servers/index.ts +3 -23
  288. package/src/core/storage/modes.ts +6 -0
  289. package/src/core/storage/segments.ts +5 -4
  290. package/src/core/supervisor/service.ts +33 -155
  291. package/src/core/supervisor/services.ts +2 -2
  292. package/src/core/supervisor/status.ts +10 -85
  293. package/src/core/workflow/engine.ts +1 -1
  294. package/src/core/workflow/index.ts +2 -13
  295. package/src/core/workflow/proof.ts +35 -83
  296. package/src/core/workflow/resume-lock.ts +179 -0
  297. package/src/core/workflow/run-state.ts +3 -207
  298. package/src/core/workflow/workspaces/leases.ts +1 -1
  299. package/src/core/workflow/workspaces/local-git.ts +120 -239
  300. package/src/core/workflow/workspaces/ownership.ts +3 -158
  301. package/src/core/workflow/workspaces/reclaim.ts +3 -9
  302. package/src/core/workflow/workspaces/recovery.ts +1 -1
  303. package/src/core/workflow/workspaces/state.ts +1 -5
  304. package/src/lib/admission-history.ts +267 -0
  305. package/src/lib/admission.ts +217 -19
  306. package/src/lib/browser/client.ts +147 -35
  307. package/src/lib/browser/content-checks.ts +14 -3
  308. package/src/lib/browser/critique.ts +113 -3
  309. package/src/lib/browser/geometry.ts +63 -14
  310. package/src/lib/browser/index.ts +5 -0
  311. package/src/lib/browser/page-review-contracts.ts +6 -1
  312. package/src/lib/browser/page-review-judge.ts +58 -13
  313. package/src/lib/browser/page-review-pack.ts +13 -2
  314. package/src/lib/browser/qa-run-contracts.ts +30 -4
  315. package/src/lib/browser/qa-run.ts +78 -17
  316. package/src/lib/browser/runts.ts +85 -37
  317. package/src/lib/browser/windows-chrome.ts +50 -0
  318. package/src/lib/browser/windows-login-sync.ts +275 -0
  319. package/src/lib/cookies/client.ts +62 -10
  320. package/src/lib/cookies/index.ts +3 -0
  321. package/src/lib/docs-metadata-sync.ts +44 -2
  322. package/src/lib/durable-job.ts +15 -0
  323. package/src/lib/http/client.ts +63 -1
  324. package/src/lib/instructions/templates.ts +4 -4
  325. package/dist/core/agents/state/process-handle.d.ts +0 -26
  326. package/dist/core/agents/state/process-handle.d.ts.map +0 -1
  327. package/dist/core/agents/state/process-handle.js +0 -121
  328. package/dist/core/workflow/recovery-audit.d.ts +0 -30
  329. package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
  330. package/dist/core/workflow/recovery-audit.js +0 -116
  331. package/src/core/agents/state/process-handle.ts +0 -165
  332. package/src/core/workflow/recovery-audit.ts +0 -181
@@ -0,0 +1,102 @@
1
+ import { basename, shellCommandFromTool, tokenize } from "./shell-tokens.ts";
2
+
3
+ /**
4
+ * Deny `git commit-tree` in an agent's shell command. In a checkout shared by
5
+ * several agents, a peer's commit can hold the Git index lock for minutes
6
+ * while its pre-commit checks run. An agent that builds its own commit from a
7
+ * separate index with `git commit-tree` and pushes it gets around that lock,
8
+ * and with it every pre-commit check and the commit-time claim guard. The
9
+ * right move is to wait for the peer's commit to finish.
10
+ *
11
+ * A guardrail against the reflex, not a security boundary: it reads command
12
+ * positions only (so quoted examples and other tools pass), skips Git's
13
+ * global options, and looks inside `bash -c '...'`.
14
+ * HARNERY_ALLOW_COMMIT_TREE=1 on the command is the deliberate override.
15
+ */
16
+
17
+ const OVERRIDE = "HARNERY_ALLOW_COMMIT_TREE=1";
18
+ const SEPARATORS = new Set([";", "|", "||", "&", "&&", "(", ")", "{", "}"]);
19
+ const KEYWORDS = new Set(["do", "if", "then", "elif", "else", "!", "while", "until", "time"]);
20
+ const WRAPPERS = new Set(["command", "exec", "builtin", "nohup", "sudo", "doas", "env", "nice"]);
21
+ // Git global options that consume the following token as their value.
22
+ const GIT_VALUE_OPTIONS = new Set([
23
+ "-C",
24
+ "-c",
25
+ "--git-dir",
26
+ "--work-tree",
27
+ "--namespace",
28
+ "--exec-path",
29
+ ]);
30
+
31
+ function gitSubcommand(args: { value: string; operator?: boolean }[]): string | null {
32
+ for (let j = 0; j < args.length; j++) {
33
+ const arg = args[j]!;
34
+ if (arg.operator) return null;
35
+ if (arg.value.startsWith("-")) {
36
+ if (GIT_VALUE_OPTIONS.has(arg.value)) j++;
37
+ continue;
38
+ }
39
+ return arg.value;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function invokesCommitTree(source: string, depth = 0): boolean {
45
+ if (depth > 8) return false;
46
+ const tokens = tokenize(source);
47
+ if (tokens.some((t) => !t.quoted && t.value === OVERRIDE)) 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) continue;
60
+ if (!token.quoted) {
61
+ if (KEYWORDS.has(token.value)) continue;
62
+ if (/^[A-Za-z_][A-Za-z_0-9]*=/.test(token.value)) continue;
63
+ if (afterWrapper && token.value.startsWith("-")) continue;
64
+ }
65
+ const name = basename(token.value);
66
+ if (WRAPPERS.has(name)) {
67
+ afterWrapper = true;
68
+ continue;
69
+ }
70
+ start = false;
71
+ afterWrapper = false;
72
+ if (token.quoted) continue;
73
+ if (name === "git" && gitSubcommand(tokens.slice(i + 1)) === "commit-tree") return true;
74
+ if (name === "git-commit-tree") return true;
75
+ if (["bash", "sh", "dash", "zsh", "ksh"].includes(name)) {
76
+ for (let j = i + 1; j < tokens.length && !tokens[j]!.operator; j++) {
77
+ if (/^-[a-z]*c[a-z]*$/.test(tokens[j]!.value)) {
78
+ const script = tokens[j + 1];
79
+ if (script && !script.operator && invokesCommitTree(script.value, depth + 1)) return true;
80
+ break;
81
+ }
82
+ }
83
+ }
84
+ }
85
+ return false;
86
+ }
87
+
88
+ export function shellCommitTreeReason(
89
+ toolName: unknown,
90
+ toolInput: unknown,
91
+ binName: string,
92
+ ): string | null {
93
+ const command = shellCommandFromTool(toolName, toolInput);
94
+ if (command === null || !invokesCommitTree(command)) return null;
95
+ return (
96
+ `git commit-tree blocked: building a commit by hand gets around the Git index lock, and with ` +
97
+ `it every pre-commit check and the claim guard. When a peer's commit holds the lock, wait ` +
98
+ `for it to finish (\`${binName} admission status\` shows whether its checks are queued for a ` +
99
+ `slot), then commit normally. If you genuinely need plumbing, prefix the command with ` +
100
+ `${OVERRIDE}.`
101
+ );
102
+ }
@@ -22,6 +22,8 @@ export interface HarneryInboxMessageRecordV1 {
22
22
  /** A required null identifies an operator; strings identify real agent instances. */
23
23
  sender_instance_id: string | null;
24
24
  sender_display_name: string;
25
+ /** Set only on a message imported from another machine (ADR 0222). */
26
+ sender_machine?: string;
25
27
  recipient_instance_id: string;
26
28
  recipient_display_name: string;
27
29
  created_at: string;
@@ -68,6 +70,22 @@ export class HarneryInboxCapacityError extends Error {
68
70
  }
69
71
  }
70
72
 
73
+ export class HarneryInboxMessageConflictError extends Error {
74
+ readonly reason_code = "message_identity_conflict";
75
+
76
+ constructor(readonly message_id: string) {
77
+ super(`inbox message identity conflict: ${message_id}`);
78
+ }
79
+ }
80
+
81
+ export class HarneryInboxMessageHistoryMissingError extends Error {
82
+ readonly reason_code = "message_history_unknown";
83
+
84
+ constructor(readonly message_id: string) {
85
+ super(`inbox message history unknown: ${message_id}`);
86
+ }
87
+ }
88
+
71
89
  export interface HarneryInboxSurfaceBudget {
72
90
  max_count: number;
73
91
  max_bytes: number;
@@ -1,7 +1,28 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { join } from "node:path";
3
+ import {
4
+ type AgentParentReference,
5
+ resolveAgentParentReference,
6
+ } from "../agents/state/parent-reference.ts";
1
7
  import { resolveBinName } from "../config.ts";
2
- import type { HarneryInboxLimits, HarneryInboxMessageRecordV1 } from "./contract.ts";
3
- import { HarneryInboxService } from "./service.ts";
4
- import { type MessageTargetInput, resolveMessageTarget } from "./targets.ts";
8
+ import { type CoordinationViewV3, readCoordinationViewV3 } from "../events/v3/coordination-view.ts";
9
+ import { liveInstanceIdV3 } from "../events/v3/live-route-observer.ts";
10
+ import { publishMail, queueMail } from "../presence/mail.ts";
11
+ import { updateCrashSafeJsonlFile } from "../storage/durable-history.ts";
12
+ import {
13
+ HarneryInboxCapacityError,
14
+ type HarneryInboxLimits,
15
+ HarneryInboxMessageConflictError,
16
+ HarneryInboxMessageHistoryMissingError,
17
+ type HarneryInboxMessageRecordV1,
18
+ } from "./contract.ts";
19
+ import { HarneryInboxService, stableInboxMessageId } from "./service.ts";
20
+ import {
21
+ type MessageTargetInput,
22
+ type MessageTargetResult,
23
+ resolveMessageTarget,
24
+ resolveRemoteMessageTarget,
25
+ } from "./targets.ts";
5
26
 
6
27
  const DAY = 24 * 60 * 60_000;
7
28
 
@@ -27,23 +48,487 @@ export function peerInbox(coordRoot: string): HarneryInboxService {
27
48
 
28
49
  /** CLI and web use this one path. A receipt proves a synced append, never that
29
50
  * a process is awake or that the recipient has noticed the message. */
30
- export function sendPeerMessage(input: {
51
+ export interface PeerMessageInput {
31
52
  coordRoot: string;
32
53
  target: MessageTargetInput;
33
54
  senderInstanceId: string | null;
34
55
  senderName: string;
35
56
  body: string;
36
- }) {
37
- const target = resolveMessageTarget(input.coordRoot, input.target);
38
- if (!target.ok) return target;
39
- const receipt = peerInbox(input.coordRoot).send({
40
- sender_instance_id: input.senderInstanceId,
41
- sender_display_name: input.senderName,
42
- recipient_instance_id: target.target.instance_id,
43
- recipient_display_name: target.target.name,
44
- body: input.body.trim(),
57
+ /** Reuse the same caller-owned ID after a crash, with identical intent. */
58
+ operationId?: string;
59
+ /** A caller with an ambiguous prior dispatch must not create a new plan. */
60
+ operationReplayOnly?: boolean;
61
+ }
62
+
63
+ type PeerTarget = Extract<MessageTargetResult, { ok: true }>["target"];
64
+ export type PeerRecipientResult =
65
+ | {
66
+ ok: true;
67
+ target: PeerTarget;
68
+ state: "written-live" | "queued" | "remote" | "duplicate";
69
+ message_id: string;
70
+ body_bytes: number;
71
+ synced: true;
72
+ published?: boolean;
73
+ publish_error?: string;
74
+ delivery_state?: "pending" | "delivered" | "expired";
75
+ }
76
+ | {
77
+ ok: false;
78
+ code: string;
79
+ message: string;
80
+ target?: PeerTarget;
81
+ candidates?: string[];
82
+ failure_stage?: "addressing" | "transport" | "operation";
83
+ };
84
+ type ParentCopyResult = PeerRecipientResult | { state: "skipped"; reason: string };
85
+ export type PeerMessageResult = PeerRecipientResult & {
86
+ operation_id: string;
87
+ child: PeerRecipientResult;
88
+ parent_reference: AgentParentReference;
89
+ parent_copy: ParentCopyResult;
90
+ complete: boolean;
91
+ };
92
+ interface RecipientPlan {
93
+ target: PeerTarget;
94
+ generation_id?: string;
95
+ body: string;
96
+ }
97
+ interface RoutingPlan {
98
+ kind: "plan";
99
+ schema: "harnery.peer-routing/v1";
100
+ request_sha256: string;
101
+ sender_instance_id: string | null;
102
+ sender_name: string;
103
+ require_live: boolean;
104
+ child: RecipientPlan;
105
+ parent_reference: AgentParentReference;
106
+ parent: RecipientPlan | ParentCopyResult;
107
+ }
108
+ type OperationRecord =
109
+ | RoutingPlan
110
+ | { kind: "started"; recipient: "child" | "parent" }
111
+ | { kind: "result"; recipient: "child" | "parent"; result: PeerRecipientResult };
112
+ const OPERATION_LIMITS = { max_record_bytes: 24_000, max_records: 5 };
113
+
114
+ /** One frozen routing intent, child first, then at most its direct parent.
115
+ * Receipts establish durable writes, never attention or a process wake. */
116
+ export function sendPeerMessage(input: PeerMessageInput): PeerMessageResult {
117
+ const operationId = input.operationId ?? randomUUID();
118
+ const unknown: AgentParentReference = { state: "unknown", reason: "original_target_unresolved" };
119
+ const refused = (code: string, message: string): PeerMessageResult => {
120
+ const child = { ok: false as const, code, message };
121
+ return {
122
+ ...child,
123
+ child,
124
+ operation_id: operationId,
125
+ parent_reference: unknown,
126
+ parent_copy: { state: "skipped", reason: "original_target_unresolved" },
127
+ complete: false,
128
+ };
129
+ };
130
+ if (!/^[A-Za-z0-9._:-]{1,160}$/.test(operationId))
131
+ return refused("invalid_operation_id", "Supply a bounded opaque operation ID.");
132
+ if (input.operationReplayOnly && !input.operationId)
133
+ return refused("invalid_operation_id", "Replay-only requires the original operation ID.");
134
+ if (!input.body.trim()) return refused("empty_message", "message is required (and non-empty)");
135
+ const request = hash(
136
+ JSON.stringify({
137
+ target: {
138
+ name: input.target.name ?? null,
139
+ instance: input.target.instanceId ?? null,
140
+ generation: input.target.generationId ?? null,
141
+ require_live: input.target.requireLive ?? false,
142
+ },
143
+ sender: input.senderInstanceId,
144
+ sender_name: input.senderName,
145
+ body: input.body,
146
+ }),
147
+ );
148
+ const path = join(input.coordRoot, ".harnery", "peer-operations", `${hash(operationId)}.jsonl`);
149
+ let plan: RoutingPlan;
150
+ try {
151
+ const prepared = updateCrashSafeJsonlFile<OperationRecord, RoutingPlan | PeerRecipientResult>(
152
+ path,
153
+ OPERATION_LIMITS,
154
+ (records) => {
155
+ if (records.length) {
156
+ validateOperationRecords(records, operationId);
157
+ const existing = records[0];
158
+ if (existing?.kind !== "plan" || existing.schema !== "harnery.peer-routing/v1")
159
+ throw new Error("routing operation has no valid frozen plan");
160
+ if (existing.request_sha256 !== request)
161
+ return {
162
+ result: {
163
+ ok: false,
164
+ code: "operation_conflict",
165
+ message: "Operation ID already binds different sender, body or target intent.",
166
+ },
167
+ };
168
+ return { result: existing };
169
+ }
170
+ if (input.operationReplayOnly)
171
+ return {
172
+ result: {
173
+ ok: false,
174
+ code: "message_history_unknown",
175
+ message: "The original routing plan is unavailable; no recipient was resubmitted.",
176
+ },
177
+ };
178
+ const view = readCoordinationViewV3(input.coordRoot);
179
+ let target = resolveMessageTarget(input.coordRoot, input.target, { view });
180
+ if (!target.ok && target.code === "no_peer") {
181
+ target = resolveRemoteMessageTarget(input.coordRoot, input.target) ?? target;
182
+ }
183
+ if (!target.ok) return { result: target };
184
+ const generation = selectedGeneration(view, target.target.instance_id);
185
+ const reference = target.target.machine
186
+ ? { state: "unknown" as const, reason: "remote_parent_evidence_unavailable" }
187
+ : resolveAgentParentReference(
188
+ input.coordRoot,
189
+ { instance_id: target.target.instance_id, generation_id: generation?.generation_id },
190
+ { view },
191
+ );
192
+ const child = {
193
+ target: target.target,
194
+ generation_id: generation?.generation_id,
195
+ body: input.body,
196
+ };
197
+ let parent: RoutingPlan["parent"] = {
198
+ state: "skipped",
199
+ reason: reference.state === "verified" ? "parent_not_addressable" : reference.reason,
200
+ };
201
+ if (reference.state === "verified" && reference.addressable) {
202
+ const parentTarget = resolveMessageTarget(
203
+ input.coordRoot,
204
+ {
205
+ instanceId: reference.parent_v3_instance_id,
206
+ generationId: reference.parent_generation_id ?? undefined,
207
+ requireLive: input.target.requireLive,
208
+ now: input.target.now,
209
+ },
210
+ { view },
211
+ );
212
+ const body = `[Parent copy for agent-${target.target.name} (${target.target.instance_id})]\n${input.body}`;
213
+ parent = !parentTarget.ok
214
+ ? parentTarget
215
+ : Buffer.byteLength(body) > PEER_INBOX_LIMITS.max_message_body_bytes
216
+ ? {
217
+ ok: false,
218
+ code: "parent_copy_overflow",
219
+ message:
220
+ "The parent marker and unchanged message exceed the 4000-byte message limit.",
221
+ target: parentTarget.target,
222
+ }
223
+ : {
224
+ target: parentTarget.target,
225
+ generation_id: reference.parent_generation_id ?? undefined,
226
+ body,
227
+ };
228
+ }
229
+ const created: RoutingPlan = {
230
+ kind: "plan",
231
+ schema: "harnery.peer-routing/v1",
232
+ request_sha256: request,
233
+ sender_instance_id: input.senderInstanceId,
234
+ sender_name: input.senderName,
235
+ require_live: input.target.requireLive ?? false,
236
+ child,
237
+ parent_reference: reference,
238
+ parent,
239
+ };
240
+ return { append: [created], result: created };
241
+ },
242
+ );
243
+ if (!("kind" in prepared))
244
+ return {
245
+ ...prepared,
246
+ child: prepared,
247
+ operation_id: operationId,
248
+ parent_reference: unknown,
249
+ parent_copy: { state: "skipped", reason: "original_target_unresolved" },
250
+ complete: false,
251
+ };
252
+ plan = prepared;
253
+ } catch (error) {
254
+ return refused(
255
+ "operation_unavailable",
256
+ error instanceof Error ? error.message : "Routing plan unavailable.",
257
+ );
258
+ }
259
+ const resolvePlanned = (recipient: RecipientPlan) =>
260
+ recipient.target.machine
261
+ ? resolveRemoteMessageTarget(input.coordRoot, {
262
+ instanceId: recipient.target.instance_id,
263
+ requireLive: plan.require_live,
264
+ now: input.target.now,
265
+ })
266
+ : resolveMessageTarget(input.coordRoot, {
267
+ instanceId: recipient.target.instance_id,
268
+ generationId: recipient.generation_id,
269
+ requireLive: plan.require_live,
270
+ now: input.target.now,
271
+ });
272
+ const deliver = (
273
+ role: "child" | "parent",
274
+ recipient: RecipientPlan,
275
+ childOutcome?: PeerRecipientResult,
276
+ ): PeerRecipientResult => {
277
+ try {
278
+ // Persist intent before transport. An interrupted prior attempt is only
279
+ // reconcilable from retained transport history, never from its absence.
280
+ const started = updateCrashSafeJsonlFile<
281
+ OperationRecord,
282
+ { replay: boolean } | PeerRecipientResult
283
+ >(path, OPERATION_LIMITS, (records) => {
284
+ validateOperationRecords(records, operationId);
285
+ const prior = records.find((r) => r.kind === "result" && r.recipient === role);
286
+ if (prior?.kind === "result") return { result: prior.result };
287
+ // Historical receipts win; a new parent append requires the original
288
+ // child identity to remain safely addressable now.
289
+ if (role === "parent") {
290
+ const currentChild = resolvePlanned(plan.child);
291
+ if (
292
+ (childOutcome && !childOutcome.ok && childOutcome.failure_stage !== "transport") ||
293
+ !currentChild?.ok ||
294
+ currentChild.target.machine !== plan.child.target.machine
295
+ ) {
296
+ return {
297
+ result: {
298
+ ok: false,
299
+ target: recipient.target,
300
+ failure_stage: "addressing",
301
+ code: "parent_copy_suppressed",
302
+ message: "Parent copy suppressed because the original task lost safe addressing.",
303
+ },
304
+ };
305
+ }
306
+ }
307
+ if (records.some((r) => r.kind === "started" && r.recipient === role))
308
+ return { result: { replay: true } };
309
+ return { append: [{ kind: "started", recipient: role }], result: { replay: false } };
310
+ });
311
+ if ("ok" in started) return started;
312
+ return updateCrashSafeJsonlFile<OperationRecord, PeerRecipientResult>(
313
+ path,
314
+ OPERATION_LIMITS,
315
+ (records) => {
316
+ validateOperationRecords(records, operationId);
317
+ const frozen = records[0];
318
+ if (frozen?.kind !== "plan" || frozen.request_sha256 !== request)
319
+ throw new Error("routing operation plan changed");
320
+ const prior = records.find((r) => r.kind === "result" && r.recipient === role);
321
+ if (prior?.kind === "result") return { result: prior.result };
322
+ const current = resolvePlanned(recipient);
323
+ let result: PeerRecipientResult;
324
+ if (!current?.ok || current.target.machine !== recipient.target.machine) {
325
+ result =
326
+ current && !current.ok
327
+ ? { ...current, target: recipient.target, failure_stage: "addressing" }
328
+ : {
329
+ ok: false,
330
+ target: recipient.target,
331
+ code: "recipient_changed",
332
+ failure_stage: "addressing",
333
+ message:
334
+ "The frozen recipient is no longer safely addressable on the same machine.",
335
+ };
336
+ } else {
337
+ try {
338
+ result = deliverPeerRecipient(
339
+ input.coordRoot,
340
+ plan,
341
+ recipient,
342
+ operationId,
343
+ current.target.delivery,
344
+ started.replay,
345
+ );
346
+ } catch (error) {
347
+ result = recipientError(error, recipient.target);
348
+ }
349
+ }
350
+ // A concurrent first attempt may still finish. Retain the start and
351
+ // report uncertainty, without turning absence into a permanent result.
352
+ if (!result.ok && result.code === "message_history_unknown") return { result };
353
+ return { append: [{ kind: "result", recipient: role, result }], result };
354
+ },
355
+ );
356
+ } catch (error) {
357
+ return recipientError(error, recipient.target, "operation");
358
+ }
359
+ };
360
+ const child = deliver("child", plan.child);
361
+ const parent = "body" in plan.parent ? deliver("parent", plan.parent, child) : plan.parent;
362
+ return {
363
+ ...child,
364
+ child,
365
+ operation_id: operationId,
366
+ parent_reference: plan.parent_reference,
367
+ parent_copy: parent,
368
+ complete: child.ok && (!("ok" in parent) || parent.ok),
369
+ };
370
+ }
371
+
372
+ function selectedGeneration(view: CoordinationViewV3, instance: string) {
373
+ const id = liveInstanceIdV3(instance);
374
+ return (
375
+ view.instances[id] ??
376
+ Object.values(view.terminal_generations)
377
+ .filter((row) => row.instance_id === id)
378
+ .sort(
379
+ (a, b) =>
380
+ b.source_position.segment_ordinal - a.source_position.segment_ordinal ||
381
+ b.source_position.byte_offset - a.source_position.byte_offset,
382
+ )[0]
383
+ );
384
+ }
385
+
386
+ function hash(value: string): string {
387
+ return createHash("sha256").update(value).digest("hex");
388
+ }
389
+
390
+ function validateOperationRecords(records: readonly OperationRecord[], operationId: string): void {
391
+ const plan = records[0];
392
+ if (
393
+ plan?.kind !== "plan" ||
394
+ plan.schema !== "harnery.peer-routing/v1" ||
395
+ typeof plan.request_sha256 !== "string" ||
396
+ typeof plan.sender_name !== "string" ||
397
+ (plan.sender_instance_id !== null && typeof plan.sender_instance_id !== "string") ||
398
+ typeof plan.require_live !== "boolean"
399
+ )
400
+ throw new Error("Invalid routing operation plan.");
401
+ const validRecipient = (r: RecipientPlan) =>
402
+ typeof r.body === "string" &&
403
+ r.target &&
404
+ typeof r.target.instance_id === "string" &&
405
+ typeof r.target.name === "string" &&
406
+ (r.generation_id === undefined || typeof r.generation_id === "string");
407
+ const validParent =
408
+ "body" in plan.parent
409
+ ? validRecipient(plan.parent)
410
+ : "ok" in plan.parent
411
+ ? plan.parent.ok === false &&
412
+ typeof plan.parent.code === "string" &&
413
+ typeof plan.parent.message === "string"
414
+ : plan.parent.state === "skipped" && typeof plan.parent.reason === "string";
415
+ if (!validRecipient(plan.child) || !validParent)
416
+ throw new Error("Invalid frozen recipient plan.");
417
+ const started = new Set<string>();
418
+ const completed = new Set<string>();
419
+ for (const row of records.slice(1)) {
420
+ if (row.kind === "plan" || !["child", "parent"].includes(row.recipient))
421
+ throw new Error("Invalid routing operation record.");
422
+ if (row.kind === "started") {
423
+ if (started.has(row.recipient)) throw new Error("Duplicate recipient start.");
424
+ started.add(row.recipient);
425
+ continue;
426
+ }
427
+ if (row.kind !== "result" || !started.has(row.recipient) || completed.has(row.recipient))
428
+ throw new Error("Invalid recipient result sequence.");
429
+ completed.add(row.recipient);
430
+ const recipient = row.recipient === "child" ? plan.child : plan.parent;
431
+ const result = row.result;
432
+ if (!("body" in recipient) || !result || typeof result.ok !== "boolean")
433
+ throw new Error("Invalid recipient result.");
434
+ if (result.ok) {
435
+ if (
436
+ result.target?.instance_id !== recipient.target.instance_id ||
437
+ result.message_id !== stableInboxMessageId(operationId, recipient.target.instance_id) ||
438
+ result.body_bytes !== Buffer.byteLength(recipient.body) ||
439
+ result.synced !== true ||
440
+ !["written-live", "queued", "remote", "duplicate"].includes(result.state)
441
+ )
442
+ throw new Error("Recipient receipt does not match frozen intent.");
443
+ } else if (typeof result.code !== "string" || typeof result.message !== "string") {
444
+ throw new Error("Invalid recipient failure.");
445
+ }
446
+ }
447
+ }
448
+
449
+ function recipientError(
450
+ error: unknown,
451
+ target: PeerTarget,
452
+ failureStage: "transport" | "operation" = "transport",
453
+ ): PeerRecipientResult {
454
+ return {
455
+ ok: false,
456
+ target,
457
+ failure_stage: failureStage,
458
+ code:
459
+ error instanceof HarneryInboxCapacityError
460
+ ? `inbox_${error.reason_code}`
461
+ : error instanceof HarneryInboxMessageHistoryMissingError ||
462
+ error instanceof HarneryInboxMessageConflictError
463
+ ? error.reason_code
464
+ : "recipient_delivery_failed",
465
+ message: error instanceof Error ? error.message : "Recipient delivery failed.",
466
+ };
467
+ }
468
+
469
+ function deliverPeerRecipient(
470
+ coordRoot: string,
471
+ plan: RoutingPlan,
472
+ recipient: RecipientPlan,
473
+ operationId: string,
474
+ delivery: PeerTarget["delivery"],
475
+ replayOnly: boolean,
476
+ ): PeerRecipientResult {
477
+ const target = recipient.target;
478
+ const messageId = stableInboxMessageId(operationId, target.instance_id);
479
+ if (target.machine) {
480
+ // Cross-machine (ADR 0222): queue in this machine's mail ref and push it
481
+ // now. A failed push keeps the message queued for the next hook publish.
482
+ const queued = queueMail(coordRoot, {
483
+ messageId,
484
+ replayOnly,
485
+ toMachine: target.machine,
486
+ senderInstanceId: plan.sender_instance_id,
487
+ senderName: plan.sender_name,
488
+ recipientInstanceId: target.instance_id,
489
+ recipientName: target.name,
490
+ body: recipient.body,
491
+ maxBodyBytes: PEER_INBOX_LIMITS.max_message_body_bytes,
492
+ });
493
+ const envelope = queued.envelope;
494
+ if (queued.delivery_state === "expired")
495
+ return {
496
+ ok: false,
497
+ target,
498
+ code: "remote_message_expired",
499
+ message: "The original remote message expired; it was not republished.",
500
+ };
501
+ const published =
502
+ queued.delivery_state === "delivered"
503
+ ? null
504
+ : publishMail(coordRoot, { sync: true, force: true });
505
+ return {
506
+ ok: true as const,
507
+ target,
508
+ state: queued.state === "duplicate" ? "duplicate" : "remote",
509
+ message_id: envelope.message_id,
510
+ body_bytes: envelope.body_bytes,
511
+ synced: true as const,
512
+ delivery_state: queued.delivery_state,
513
+ published: published?.status === "published",
514
+ ...(published?.status === "error" ? { publish_error: published.error } : {}),
515
+ };
516
+ }
517
+ const receipt = peerInbox(coordRoot).send({
518
+ message_id: messageId,
519
+ replay_only: replayOnly,
520
+ sender_instance_id: plan.sender_instance_id,
521
+ sender_display_name: plan.sender_name,
522
+ recipient_instance_id: target.instance_id,
523
+ recipient_display_name: target.name,
524
+ body: recipient.body,
45
525
  });
46
- return { ok: true as const, target: target.target, ...receipt, state: target.target.delivery };
526
+ return {
527
+ ok: true,
528
+ target,
529
+ ...receipt,
530
+ state: receipt.state === "duplicate" ? "duplicate" : delivery,
531
+ };
47
532
  }
48
533
 
49
534
  export function formatPeerInbox(
@@ -62,7 +547,8 @@ export function formatPeerInbox(
62
547
  const body = messages
63
548
  .map(
64
549
  (message) =>
65
- ` ${message.message_id} from ${message.sender_instance_id === null ? "Operator" : `agent-${message.sender_display_name}`} (${message.created_at}):\n` +
550
+ ` ${message.message_id} from ${message.sender_instance_id === null ? "Operator" : `agent-${message.sender_display_name}`}` +
551
+ `${message.sender_machine ? ` on ${message.sender_machine}` : ""} (${message.created_at}):\n` +
66
552
  message.body
67
553
  .split("\n")
68
554
  .map((line) => ` ${line}`)