harnery 0.36.0 → 0.37.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 (311) hide show
  1. package/dist/commander.js +4 -0
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +87 -46
  4. package/dist/commands/artifacts.d.ts.map +1 -1
  5. package/dist/commands/artifacts.js +26 -1
  6. package/dist/commands/browse.d.ts.map +1 -1
  7. package/dist/commands/browse.js +11 -0
  8. package/dist/commands/diagnostics.d.ts +4 -0
  9. package/dist/commands/diagnostics.d.ts.map +1 -0
  10. package/dist/commands/diagnostics.js +229 -0
  11. package/dist/commands/files.d.ts +12 -0
  12. package/dist/commands/files.d.ts.map +1 -1
  13. package/dist/commands/files.js +54 -1
  14. package/dist/commands/governor.d.ts.map +1 -1
  15. package/dist/commands/governor.js +4 -0
  16. package/dist/commands/ledger-v3.d.ts.map +1 -1
  17. package/dist/commands/ledger-v3.js +71 -2
  18. package/dist/commands/qa-run.d.ts +4 -0
  19. package/dist/commands/qa-run.d.ts.map +1 -0
  20. package/dist/commands/qa-run.js +141 -0
  21. package/dist/commands/resources.d.ts +4 -0
  22. package/dist/commands/resources.d.ts.map +1 -0
  23. package/dist/commands/resources.js +29 -0
  24. package/dist/commands/supervisor.d.ts +4 -0
  25. package/dist/commands/supervisor.d.ts.map +1 -0
  26. package/dist/commands/supervisor.js +101 -0
  27. package/dist/commands/web.d.ts.map +1 -1
  28. package/dist/commands/web.js +17 -0
  29. package/dist/commands/workflow.d.ts.map +1 -1
  30. package/dist/commands/workflow.js +4 -0
  31. package/dist/core/agents/cli.js +7 -3
  32. package/dist/core/agents/health.d.ts +17 -0
  33. package/dist/core/agents/health.d.ts.map +1 -0
  34. package/dist/core/agents/health.js +109 -0
  35. package/dist/core/agents/mailbox.d.ts +92 -0
  36. package/dist/core/agents/mailbox.d.ts.map +1 -0
  37. package/dist/core/agents/mailbox.js +325 -0
  38. package/dist/core/agents/reconcile-coordination-v3.d.ts +44 -0
  39. package/dist/core/agents/reconcile-coordination-v3.d.ts.map +1 -0
  40. package/dist/core/agents/reconcile-coordination-v3.js +58 -0
  41. package/dist/core/agents/render/prompt-context.d.ts +5 -0
  42. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  43. package/dist/core/agents/render/prompt-context.js +34 -13
  44. package/dist/core/agents/render/session-context.d.ts +8 -0
  45. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  46. package/dist/core/agents/render/session-context.js +34 -3
  47. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  48. package/dist/core/agents/rules/commit-conflict.js +50 -1
  49. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  50. package/dist/core/agents/rules/stop-hook.js +6 -10
  51. package/dist/core/agents/session-finalizer-v3.js +4 -1
  52. package/dist/core/artifacts/index.d.ts +26 -1
  53. package/dist/core/artifacts/index.d.ts.map +1 -1
  54. package/dist/core/artifacts/index.js +146 -6
  55. package/dist/core/config.d.ts +12 -0
  56. package/dist/core/config.d.ts.map +1 -1
  57. package/dist/core/config.js +43 -0
  58. package/dist/core/diagnostics/advice.d.ts +9 -0
  59. package/dist/core/diagnostics/advice.d.ts.map +1 -0
  60. package/dist/core/diagnostics/advice.js +112 -0
  61. package/dist/core/diagnostics/bundle.d.ts +35 -0
  62. package/dist/core/diagnostics/bundle.d.ts.map +1 -0
  63. package/dist/core/diagnostics/bundle.js +874 -0
  64. package/dist/core/diagnostics/comparison.d.ts +5 -0
  65. package/dist/core/diagnostics/comparison.d.ts.map +1 -0
  66. package/dist/core/diagnostics/comparison.js +444 -0
  67. package/dist/core/diagnostics/contract.d.ts +245 -0
  68. package/dist/core/diagnostics/contract.d.ts.map +1 -0
  69. package/dist/core/diagnostics/contract.js +18 -0
  70. package/dist/core/diagnostics/identity.d.ts +3 -0
  71. package/dist/core/diagnostics/identity.d.ts.map +1 -0
  72. package/dist/core/diagnostics/identity.js +10 -0
  73. package/dist/core/diagnostics/index.d.ts +8 -0
  74. package/dist/core/diagnostics/index.d.ts.map +1 -0
  75. package/dist/core/diagnostics/index.js +7 -0
  76. package/dist/core/diagnostics/replay.d.ts +8 -0
  77. package/dist/core/diagnostics/replay.d.ts.map +1 -0
  78. package/dist/core/diagnostics/replay.js +213 -0
  79. package/dist/core/diagnostics/sanitize.d.ts +9 -0
  80. package/dist/core/diagnostics/sanitize.d.ts.map +1 -0
  81. package/dist/core/diagnostics/sanitize.js +84 -0
  82. package/dist/core/events/legacy-storage/compression.d.ts +13 -0
  83. package/dist/core/events/legacy-storage/compression.d.ts.map +1 -0
  84. package/dist/core/events/legacy-storage/compression.js +107 -0
  85. package/dist/core/events/legacy-storage/index.d.ts +1 -0
  86. package/dist/core/events/legacy-storage/index.d.ts.map +1 -1
  87. package/dist/core/events/legacy-storage/index.js +1 -0
  88. package/dist/core/events/v3/archive-retention.d.ts +33 -0
  89. package/dist/core/events/v3/archive-retention.d.ts.map +1 -0
  90. package/dist/core/events/v3/archive-retention.js +195 -0
  91. package/dist/core/events/v3/capabilities.d.ts +1 -1
  92. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  93. package/dist/core/events/v3/capabilities.js +1 -0
  94. package/dist/core/events/v3/index.d.ts +1 -0
  95. package/dist/core/events/v3/index.d.ts.map +1 -1
  96. package/dist/core/events/v3/index.js +1 -0
  97. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  98. package/dist/core/events/v3/live-routing.js +1 -0
  99. package/dist/core/events/v3/producers/hook-base.d.ts +1 -1
  100. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  101. package/dist/core/events/v3/producers/hook-base.js +13 -2
  102. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  103. package/dist/core/events/v3/producers/intake.js +1 -0
  104. package/dist/core/events/v3/producers/recorder.d.ts +16 -0
  105. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  106. package/dist/core/events/v3/producers/recorder.js +88 -1
  107. package/dist/core/hooks/adapter/output.d.ts +8 -0
  108. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  109. package/dist/core/hooks/adapter/output.js +11 -1
  110. package/dist/core/hooks/cli.js +183 -192
  111. package/dist/core/hooks/health.d.ts +59 -0
  112. package/dist/core/hooks/health.d.ts.map +1 -0
  113. package/dist/core/hooks/health.js +84 -0
  114. package/dist/core/resources/contract.d.ts +90 -0
  115. package/dist/core/resources/contract.d.ts.map +1 -0
  116. package/dist/core/resources/contract.js +2 -0
  117. package/dist/core/resources/index.d.ts +6 -0
  118. package/dist/core/resources/index.d.ts.map +1 -0
  119. package/dist/core/resources/index.js +5 -0
  120. package/dist/core/resources/sampler.d.ts +27 -0
  121. package/dist/core/resources/sampler.d.ts.map +1 -0
  122. package/dist/core/resources/sampler.js +357 -0
  123. package/dist/core/resources/service-status.d.ts +3 -0
  124. package/dist/core/resources/service-status.d.ts.map +1 -0
  125. package/dist/core/resources/service-status.js +48 -0
  126. package/dist/core/resources/service.d.ts +26 -0
  127. package/dist/core/resources/service.d.ts.map +1 -0
  128. package/dist/core/resources/service.js +292 -0
  129. package/dist/core/resources/storage.d.ts +13 -0
  130. package/dist/core/resources/storage.d.ts.map +1 -0
  131. package/dist/core/resources/storage.js +30 -0
  132. package/dist/core/storage/atomic-json.d.ts +3 -0
  133. package/dist/core/storage/atomic-json.d.ts.map +1 -0
  134. package/dist/core/storage/atomic-json.js +15 -0
  135. package/dist/core/storage/builtins.d.ts.map +1 -1
  136. package/dist/core/storage/builtins.js +45 -6
  137. package/dist/core/storage/logger.d.ts +2 -0
  138. package/dist/core/storage/logger.d.ts.map +1 -1
  139. package/dist/core/storage/logger.js +2 -0
  140. package/dist/core/storage/query.d.ts +14 -0
  141. package/dist/core/storage/query.d.ts.map +1 -1
  142. package/dist/core/storage/query.js +51 -0
  143. package/dist/core/supervisor/activity.d.ts +4 -0
  144. package/dist/core/supervisor/activity.d.ts.map +1 -0
  145. package/dist/core/supervisor/activity.js +141 -0
  146. package/dist/core/supervisor/contract.d.ts +268 -0
  147. package/dist/core/supervisor/contract.d.ts.map +1 -0
  148. package/dist/core/supervisor/contract.js +35 -0
  149. package/dist/core/supervisor/explanations.d.ts +4 -0
  150. package/dist/core/supervisor/explanations.d.ts.map +1 -0
  151. package/dist/core/supervisor/explanations.js +71 -0
  152. package/dist/core/supervisor/findings.d.ts +20 -0
  153. package/dist/core/supervisor/findings.d.ts.map +1 -0
  154. package/dist/core/supervisor/findings.js +399 -0
  155. package/dist/core/supervisor/history.d.ts +9 -0
  156. package/dist/core/supervisor/history.d.ts.map +1 -0
  157. package/dist/core/supervisor/history.js +46 -0
  158. package/dist/core/supervisor/hook-health-alerts.d.ts +14 -0
  159. package/dist/core/supervisor/hook-health-alerts.d.ts.map +1 -0
  160. package/dist/core/supervisor/hook-health-alerts.js +70 -0
  161. package/dist/core/supervisor/hook-health-storage.d.ts +5 -0
  162. package/dist/core/supervisor/hook-health-storage.d.ts.map +1 -0
  163. package/dist/core/supervisor/hook-health-storage.js +24 -0
  164. package/dist/core/supervisor/hook-health.d.ts +72 -0
  165. package/dist/core/supervisor/hook-health.d.ts.map +1 -0
  166. package/dist/core/supervisor/hook-health.js +203 -0
  167. package/dist/core/supervisor/hooks.d.ts +10 -0
  168. package/dist/core/supervisor/hooks.d.ts.map +1 -0
  169. package/dist/core/supervisor/hooks.js +28 -0
  170. package/dist/core/supervisor/index.d.ts +15 -0
  171. package/dist/core/supervisor/index.d.ts.map +1 -0
  172. package/dist/core/supervisor/index.js +14 -0
  173. package/dist/core/supervisor/log-feed.d.ts +9 -0
  174. package/dist/core/supervisor/log-feed.d.ts.map +1 -0
  175. package/dist/core/supervisor/log-feed.js +103 -0
  176. package/dist/core/supervisor/service.d.ts +29 -0
  177. package/dist/core/supervisor/service.d.ts.map +1 -0
  178. package/dist/core/supervisor/service.js +450 -0
  179. package/dist/core/supervisor/services.d.ts +11 -0
  180. package/dist/core/supervisor/services.d.ts.map +1 -0
  181. package/dist/core/supervisor/services.js +155 -0
  182. package/dist/core/supervisor/status.d.ts +3 -0
  183. package/dist/core/supervisor/status.d.ts.map +1 -0
  184. package/dist/core/supervisor/status.js +48 -0
  185. package/dist/core/supervisor/storage.d.ts +30 -0
  186. package/dist/core/supervisor/storage.d.ts.map +1 -0
  187. package/dist/core/supervisor/storage.js +73 -0
  188. package/dist/core/supervisor/timeline.d.ts +3 -0
  189. package/dist/core/supervisor/timeline.d.ts.map +1 -0
  190. package/dist/core/supervisor/timeline.js +87 -0
  191. package/dist/core/workflow/admission.d.ts +23 -0
  192. package/dist/core/workflow/admission.d.ts.map +1 -0
  193. package/dist/core/workflow/admission.js +136 -0
  194. package/dist/core/workflow/engine.d.ts.map +1 -1
  195. package/dist/core/workflow/engine.js +77 -2
  196. package/dist/core/workflow/index.d.ts +3 -2
  197. package/dist/core/workflow/index.d.ts.map +1 -1
  198. package/dist/core/workflow/index.js +2 -1
  199. package/dist/core/workflow/proof.d.ts +2 -1
  200. package/dist/core/workflow/proof.d.ts.map +1 -1
  201. package/dist/core/workflow/proof.js +39 -1
  202. package/dist/core/workflow/run-state.d.ts +3 -2
  203. package/dist/core/workflow/run-state.d.ts.map +1 -1
  204. package/dist/core/workflow/run-state.js +8 -1
  205. package/dist/core/workflow/types.d.ts +33 -1
  206. package/dist/core/workflow/types.d.ts.map +1 -1
  207. package/dist/core/workflow/types.js +2 -1
  208. package/dist/lib/browser/client.d.ts +5 -0
  209. package/dist/lib/browser/client.d.ts.map +1 -1
  210. package/dist/lib/browser/client.js +1 -0
  211. package/dist/lib/browser/index.d.ts +2 -0
  212. package/dist/lib/browser/index.d.ts.map +1 -1
  213. package/dist/lib/browser/index.js +2 -0
  214. package/dist/lib/browser/qa-run-contracts.d.ts +173 -0
  215. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -0
  216. package/dist/lib/browser/qa-run-contracts.js +226 -0
  217. package/dist/lib/browser/qa-run.d.ts +51 -0
  218. package/dist/lib/browser/qa-run.d.ts.map +1 -0
  219. package/dist/lib/browser/qa-run.js +684 -0
  220. package/dist/lib/coord-root-id.d.ts +9 -0
  221. package/dist/lib/coord-root-id.d.ts.map +1 -0
  222. package/dist/lib/coord-root-id.js +17 -0
  223. package/dist/lib/instructions/apply.d.ts +3 -1
  224. package/dist/lib/instructions/apply.d.ts.map +1 -1
  225. package/dist/lib/instructions/apply.js +74 -3
  226. package/dist/lib/instructions/templates.d.ts.map +1 -1
  227. package/dist/lib/instructions/templates.js +6 -0
  228. package/package.json +12 -1
  229. package/src/commander.ts +26 -0
  230. package/src/commands/agents.ts +102 -49
  231. package/src/commands/artifacts.ts +40 -2
  232. package/src/commands/browse.ts +15 -0
  233. package/src/commands/diagnostics.ts +271 -0
  234. package/src/commands/files.ts +102 -2
  235. package/src/commands/governor.ts +8 -0
  236. package/src/commands/ledger-v3.ts +82 -0
  237. package/src/commands/qa-run.ts +184 -0
  238. package/src/commands/resources.ts +38 -0
  239. package/src/commands/supervisor.ts +141 -0
  240. package/src/commands/web.ts +19 -0
  241. package/src/commands/workflow.ts +8 -0
  242. package/src/core/agents/cli.ts +7 -3
  243. package/src/core/agents/health.ts +142 -0
  244. package/src/core/agents/mailbox.ts +373 -0
  245. package/src/core/agents/reconcile-coordination-v3.ts +77 -0
  246. package/src/core/agents/render/prompt-context.ts +42 -12
  247. package/src/core/agents/render/session-context.ts +37 -3
  248. package/src/core/agents/rules/commit-conflict.ts +58 -1
  249. package/src/core/agents/rules/stop-hook.ts +6 -12
  250. package/src/core/agents/session-finalizer-v3.ts +4 -1
  251. package/src/core/artifacts/index.ts +196 -10
  252. package/src/core/config.ts +111 -2
  253. package/src/core/diagnostics/advice.ts +147 -0
  254. package/src/core/diagnostics/bundle.ts +1142 -0
  255. package/src/core/diagnostics/comparison.ts +565 -0
  256. package/src/core/diagnostics/contract.ts +311 -0
  257. package/src/core/diagnostics/identity.ts +11 -0
  258. package/src/core/diagnostics/index.ts +7 -0
  259. package/src/core/diagnostics/replay.ts +314 -0
  260. package/src/core/diagnostics/sanitize.ts +97 -0
  261. package/src/core/events/legacy-storage/compression.ts +130 -0
  262. package/src/core/events/legacy-storage/index.ts +1 -0
  263. package/src/core/events/v3/archive-retention.ts +251 -0
  264. package/src/core/events/v3/capabilities.ts +2 -0
  265. package/src/core/events/v3/index.ts +9 -0
  266. package/src/core/events/v3/live-routing.ts +1 -0
  267. package/src/core/events/v3/producers/hook-base.ts +14 -2
  268. package/src/core/events/v3/producers/intake.ts +1 -0
  269. package/src/core/events/v3/producers/recorder.ts +110 -1
  270. package/src/core/hooks/adapter/output.ts +12 -1
  271. package/src/core/hooks/cli.ts +192 -215
  272. package/src/core/hooks/health.ts +146 -0
  273. package/src/core/resources/contract.ts +98 -0
  274. package/src/core/resources/index.ts +5 -0
  275. package/src/core/resources/sampler.ts +458 -0
  276. package/src/core/resources/service-status.ts +62 -0
  277. package/src/core/resources/service.ts +337 -0
  278. package/src/core/resources/storage.ts +44 -0
  279. package/src/core/storage/atomic-json.ts +15 -0
  280. package/src/core/storage/builtins.ts +62 -6
  281. package/src/core/storage/logger.ts +2 -0
  282. package/src/core/storage/query.ts +58 -0
  283. package/src/core/supervisor/activity.ts +193 -0
  284. package/src/core/supervisor/contract.ts +312 -0
  285. package/src/core/supervisor/explanations.ts +104 -0
  286. package/src/core/supervisor/findings.ts +788 -0
  287. package/src/core/supervisor/history.ts +61 -0
  288. package/src/core/supervisor/hook-health-alerts.ts +108 -0
  289. package/src/core/supervisor/hook-health-storage.ts +25 -0
  290. package/src/core/supervisor/hook-health.ts +301 -0
  291. package/src/core/supervisor/hooks.ts +35 -0
  292. package/src/core/supervisor/index.ts +14 -0
  293. package/src/core/supervisor/log-feed.ts +131 -0
  294. package/src/core/supervisor/service.ts +511 -0
  295. package/src/core/supervisor/services.ts +203 -0
  296. package/src/core/supervisor/status.ts +59 -0
  297. package/src/core/supervisor/storage.ts +126 -0
  298. package/src/core/supervisor/timeline.ts +133 -0
  299. package/src/core/workflow/admission.ts +215 -0
  300. package/src/core/workflow/engine.ts +93 -1
  301. package/src/core/workflow/index.ts +10 -0
  302. package/src/core/workflow/proof.ts +54 -1
  303. package/src/core/workflow/run-state.ts +13 -1
  304. package/src/core/workflow/types.ts +39 -1
  305. package/src/lib/browser/client.ts +6 -0
  306. package/src/lib/browser/index.ts +29 -0
  307. package/src/lib/browser/qa-run-contracts.ts +380 -0
  308. package/src/lib/browser/qa-run.ts +784 -0
  309. package/src/lib/coord-root-id.ts +23 -0
  310. package/src/lib/instructions/apply.ts +81 -2
  311. package/src/lib/instructions/templates.ts +6 -0
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Name-addressed durable mailbox.
3
+ *
4
+ * Peer messaging used to require a live recipient: the sender resolved a name
5
+ * to a running instance and wrote into that instance's journal. Two failures
6
+ * followed from that. A message to a name whose session had ended was refused
7
+ * outright, so the sender spent a long sequence of discovery calls looking for
8
+ * somewhere else to put it. A message to a live peer was accepted but never
9
+ * shown to them, because nothing surfaces another session's journal.
10
+ *
11
+ * This module addresses the durable persona name instead of the instance, so a
12
+ * message can be queued before its recipient exists. Delivery happens on the
13
+ * recipient's side: their next SessionStart or prompt drains the queue, shows
14
+ * the messages, and records them in their journal.
15
+ *
16
+ * Storage: `.harnery/mailbox/<slug>.jsonl`, one JSON record per line, appended
17
+ * by senders and removed by an atomic claim on the recipient's side. Delivered
18
+ * records are appended to `.harnery/mailbox/delivered/<slug>.jsonl` for audit.
19
+ *
20
+ * Related but distinct: `core/inbox` is an instance-keyed transport with a
21
+ * storage-framework service behind it. This mailbox is name-keyed, file-only,
22
+ * and follows the same shape as council manifests and journals.
23
+ */
24
+ import { randomUUID } from "node:crypto";
25
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
26
+ import { join, resolve } from "node:path";
27
+ import { lookupByName } from "../../lib/identities/index.js";
28
+ import { resolveBinName } from "../config.js";
29
+ import { readLiveCoordinationRows } from "./state/live-coordination-view.js";
30
+ import { COORD_NAMES } from "./state/names.js";
31
+ export const MAILBOX_MESSAGE_SCHEMA = "harnery.mailbox-message/v1";
32
+ /** Per-message body ceiling. Larger payloads belong in a managed artifact, with
33
+ * the path sent as the message. */
34
+ export const MAX_BODY_BYTES = 4000;
35
+ /** Pending messages held for one name before senders are refused. */
36
+ export const MAX_PENDING = 25;
37
+ /** Delivered-archive ceiling; the file is truncated from the head past this. */
38
+ const MAX_DELIVERED_BYTES = 256 * 1024;
39
+ export class MailboxCapacityError extends Error {
40
+ reason_code;
41
+ current;
42
+ limit;
43
+ constructor(reason_code, current, limit) {
44
+ super(`mailbox capacity ${reason_code}: ${current}/${limit}; write the payload to a managed artifact and send its path instead`);
45
+ this.reason_code = reason_code;
46
+ this.current = current;
47
+ this.limit = limit;
48
+ this.name = "MailboxCapacityError";
49
+ }
50
+ }
51
+ /** Strip an `agent-` prefix and normalize to the registry's bare form. */
52
+ export function bareAgentName(raw) {
53
+ const trimmed = raw.trim();
54
+ return trimmed.toLowerCase().startsWith("agent-") ? trimmed.slice("agent-".length) : trimmed;
55
+ }
56
+ /** Filesystem-safe slug for a name. Rejects anything that could escape the
57
+ * mailbox directory rather than sanitizing it into a different agent's file. */
58
+ function slugFor(name) {
59
+ const bare = bareAgentName(name).toLowerCase();
60
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(bare)) {
61
+ throw new Error(`invalid agent name for mailbox addressing: ${JSON.stringify(name)}`);
62
+ }
63
+ return bare;
64
+ }
65
+ export function mailboxDir(coordRoot) {
66
+ return resolve(coordRoot, ".harnery", "mailbox");
67
+ }
68
+ export function mailboxPath(coordRoot, name) {
69
+ return join(mailboxDir(coordRoot), `${slugFor(name)}.jsonl`);
70
+ }
71
+ function deliveredPath(coordRoot, name) {
72
+ return join(mailboxDir(coordRoot), "delivered", `${slugFor(name)}.jsonl`);
73
+ }
74
+ function parseLines(content) {
75
+ const out = [];
76
+ for (const line of content.split("\n")) {
77
+ const trimmed = line.trim();
78
+ if (!trimmed)
79
+ continue;
80
+ try {
81
+ const parsed = JSON.parse(trimmed);
82
+ if (parsed?.schema === MAILBOX_MESSAGE_SCHEMA)
83
+ out.push(parsed);
84
+ }
85
+ catch {
86
+ /* skip a torn line rather than losing the whole mailbox */
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+ /** Every name a message may be addressed to: live sessions, the durable
92
+ * identity registry, and the built-in name pool. A name outside all three is a
93
+ * typo, and queueing it would silently swallow the message. */
94
+ export function isAddressableName(coordRoot, name) {
95
+ const bare = bareAgentName(name).toLowerCase();
96
+ if (!bare)
97
+ return false;
98
+ if (COORD_NAMES.some((n) => n.toLowerCase() === bare))
99
+ return true;
100
+ if (lookupByName(bare, coordRoot))
101
+ return true;
102
+ try {
103
+ return readLiveCoordinationRows(coordRoot).some((r) => (r.name ?? "").toLowerCase() === bare);
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
109
+ /** Names close enough to a miss to be worth suggesting (prefix or one edit). */
110
+ export function suggestNames(coordRoot, name, limit = 5) {
111
+ const bare = bareAgentName(name).toLowerCase();
112
+ const known = new Set(COORD_NAMES);
113
+ try {
114
+ for (const row of readLiveCoordinationRows(coordRoot))
115
+ if (row.name)
116
+ known.add(row.name);
117
+ }
118
+ catch {
119
+ /* live rows are advisory here */
120
+ }
121
+ const scored = [];
122
+ for (const candidate of known) {
123
+ const lower = candidate.toLowerCase();
124
+ if (lower.startsWith(bare.slice(0, 2)) || editDistance(lower, bare) <= 2) {
125
+ scored.push({ name: candidate, distance: editDistance(lower, bare) });
126
+ }
127
+ }
128
+ scored.sort((a, b) => a.distance - b.distance || a.name.localeCompare(b.name));
129
+ return scored.slice(0, limit).map((s) => s.name);
130
+ }
131
+ function editDistance(a, b) {
132
+ const rows = a.length + 1;
133
+ const cols = b.length + 1;
134
+ let prev = Array.from({ length: cols }, (_, i) => i);
135
+ for (let i = 1; i < rows; i++) {
136
+ const curr = [i, ...Array.from({ length: cols - 1 }, () => 0)];
137
+ for (let j = 1; j < cols; j++) {
138
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
139
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
140
+ }
141
+ prev = curr;
142
+ }
143
+ return prev[cols - 1];
144
+ }
145
+ /**
146
+ * Append one message to a name's mailbox. Throws `MailboxCapacityError` when
147
+ * the body or the pending queue is over its ceiling; the caller turns that into
148
+ * an actionable CLI error.
149
+ */
150
+ export function queueMailboxMessage(input) {
151
+ const body = input.body.trim();
152
+ const bodyBytes = Buffer.byteLength(body, "utf8");
153
+ if (bodyBytes > MAX_BODY_BYTES) {
154
+ throw new MailboxCapacityError("body_limit", bodyBytes, MAX_BODY_BYTES);
155
+ }
156
+ const path = mailboxPath(input.coordRoot, input.toName);
157
+ mkdirSync(mailboxDir(input.coordRoot), { recursive: true });
158
+ const existing = existsSync(path) ? parseLines(readFileSync(path, "utf8")) : [];
159
+ if (existing.length >= MAX_PENDING) {
160
+ throw new MailboxCapacityError("pending_limit", existing.length, MAX_PENDING);
161
+ }
162
+ // Preserve registry casing when the persona is known, so the recipient sees
163
+ // their own name spelled the way it is everywhere else.
164
+ const identity = lookupByName(input.toName, input.coordRoot);
165
+ const message = {
166
+ schema: MAILBOX_MESSAGE_SCHEMA,
167
+ message_id: randomUUID(),
168
+ to_name: identity?.name ?? bareAgentName(input.toName),
169
+ from_name: input.fromName,
170
+ from_instance_id: input.fromInstanceId,
171
+ created_at: new Date().toISOString(),
172
+ body,
173
+ journaled: input.journaled ?? false,
174
+ };
175
+ appendFileSync(path, `${JSON.stringify(message)}\n`, "utf8");
176
+ return { message, path, pending: existing.length + 1 };
177
+ }
178
+ /** Pending messages for a name, without claiming them. */
179
+ export function peekMailbox(coordRoot, name) {
180
+ let path;
181
+ try {
182
+ path = mailboxPath(coordRoot, name);
183
+ }
184
+ catch {
185
+ return [];
186
+ }
187
+ if (!existsSync(path))
188
+ return [];
189
+ try {
190
+ return parseLines(readFileSync(path, "utf8"));
191
+ }
192
+ catch {
193
+ return [];
194
+ }
195
+ }
196
+ /**
197
+ * Claim and return every pending message for a name.
198
+ *
199
+ * The claim is an atomic rename, so two sessions sharing a name cannot deliver
200
+ * the same message twice: whichever rename wins owns the batch. Claimed records
201
+ * are appended to the delivered archive before the claim file is removed, so a
202
+ * crash between the two leaves an auditable copy rather than a silent loss.
203
+ */
204
+ export function drainMailbox(coordRoot, name) {
205
+ let path;
206
+ try {
207
+ path = mailboxPath(coordRoot, name);
208
+ }
209
+ catch {
210
+ return [];
211
+ }
212
+ // Heal first: a claim file left by a crashed reader may hold the only copy of
213
+ // a message for this very name, and sweeping after the existence check would
214
+ // skip it whenever the live queue is empty.
215
+ sweepStaleClaims(coordRoot);
216
+ if (!existsSync(path))
217
+ return [];
218
+ const claim = `${path}.claim-${process.pid}-${Date.now()}`;
219
+ try {
220
+ renameSync(path, claim);
221
+ }
222
+ catch {
223
+ // Another reader claimed it first, or the file vanished. Either way there
224
+ // is nothing for this caller to deliver.
225
+ return [];
226
+ }
227
+ let messages = [];
228
+ try {
229
+ messages = parseLines(readFileSync(claim, "utf8"));
230
+ }
231
+ catch {
232
+ messages = [];
233
+ }
234
+ if (messages.length > 0) {
235
+ try {
236
+ const archive = deliveredPath(coordRoot, name);
237
+ mkdirSync(join(mailboxDir(coordRoot), "delivered"), { recursive: true });
238
+ const deliveredAt = new Date().toISOString();
239
+ appendFileSync(archive, `${messages.map((m) => JSON.stringify({ ...m, delivered_at: deliveredAt })).join("\n")}\n`, "utf8");
240
+ trimArchive(archive);
241
+ }
242
+ catch {
243
+ /* audit copy is best-effort; never block delivery on it */
244
+ }
245
+ }
246
+ try {
247
+ unlinkSync(claim);
248
+ }
249
+ catch {
250
+ /* a leftover claim is re-queued by the next drain's sweep */
251
+ }
252
+ return messages;
253
+ }
254
+ /** Re-queue any claim file left behind by a crashed reader (older than 5 min),
255
+ * so a killed session cannot strand a message. Runs at the head of every drain. */
256
+ function sweepStaleClaims(coordRoot) {
257
+ const dir = mailboxDir(coordRoot);
258
+ if (!existsSync(dir))
259
+ return;
260
+ const cutoff = Date.now() - 5 * 60_000;
261
+ try {
262
+ for (const file of readdirSync(dir)) {
263
+ const marker = file.indexOf(".jsonl.claim-");
264
+ if (marker === -1)
265
+ continue;
266
+ const full = join(dir, file);
267
+ let mtime;
268
+ try {
269
+ mtime = statSync(full).mtimeMs;
270
+ }
271
+ catch {
272
+ continue;
273
+ }
274
+ if (mtime > cutoff)
275
+ continue;
276
+ const owner = join(dir, `${file.slice(0, marker)}.jsonl`);
277
+ try {
278
+ const content = readFileSync(full, "utf8");
279
+ if (content.trim())
280
+ appendFileSync(owner, content.endsWith("\n") ? content : `${content}\n`);
281
+ unlinkSync(full);
282
+ }
283
+ catch {
284
+ /* skip; next sweep retries */
285
+ }
286
+ }
287
+ }
288
+ catch {
289
+ /* directory read is best-effort */
290
+ }
291
+ }
292
+ function trimArchive(path) {
293
+ try {
294
+ if (statSync(path).size <= MAX_DELIVERED_BYTES)
295
+ return;
296
+ const lines = readFileSync(path, "utf8").split("\n").filter(Boolean);
297
+ const keep = lines.slice(Math.floor(lines.length / 2));
298
+ writeFileSync(path, `${keep.join("\n")}\n`, "utf8");
299
+ }
300
+ catch {
301
+ /* trimming is housekeeping, never load-bearing */
302
+ }
303
+ }
304
+ /** Render a drained batch as the context block a recipient reads. */
305
+ export function formatMailboxDelivery(coordRoot, messages) {
306
+ if (messages.length === 0)
307
+ return "";
308
+ const bin = resolveBinName(coordRoot);
309
+ const header = messages.length === 1
310
+ ? `Message from a peer agent (delivered now, recorded in your journal):`
311
+ : `${messages.length} messages from peer agents (delivered now, recorded in your journal):`;
312
+ const body = messages
313
+ .map((m) => {
314
+ const when = m.created_at.replace("T", " ").replace(/\.\d+Z$/, "Z");
315
+ return ` from agent-${m.from_name} (${when}):\n${indent(m.body)}`;
316
+ })
317
+ .join("\n\n");
318
+ return `${header}\n\n${body}\n\nReply with \`${bin} agents ping ${messages[0].from_name} "<your reply>"\`; it reaches them whether or not their session is still running.`;
319
+ }
320
+ function indent(text) {
321
+ return text
322
+ .split("\n")
323
+ .map((line) => ` ${line}`)
324
+ .join("\n");
325
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The canonical supervisor reconcile pass.
3
+ *
4
+ * Two things have to happen together, in this order:
5
+ *
6
+ * 1. `staleSweep` prunes the disposable coordination cache under
7
+ * `.harnery/active` (plus orphaned pid-map and `.last-peer-hash` files)
8
+ * and leaves a durable lifecycle observation for every deletion.
9
+ * 2. `reconcileSessionFinalizationV3` turns those observations, along with
10
+ * archive/idle/run/host signals, into session finalization requests.
11
+ *
12
+ * Running the sweep first is what makes a single pass self-contained: the
13
+ * `lifecycle.sweep_observed` rows it writes are visible to the finalizer in
14
+ * the same pass, so a stale session is both reaped and finalized without
15
+ * waiting for a later invocation.
16
+ *
17
+ * ADR 0077 already specified that `agents reconcile` and session start
18
+ * discover stale-sweep in the same pass. Both entry points had drifted to
19
+ * calling only the finalizer, so the sweep never ran outside its own
20
+ * subcommand. This module is the single composition both now share; adding a
21
+ * caller means calling this, not re-composing the two halves.
22
+ */
23
+ import { type ReconcileSessionFinalizationOptionsV3, type ReconcileSessionFinalizationResultV3 } from "./session-finalizer-v3.js";
24
+ export interface ReconcileCoordinationResultV3 extends ReconcileSessionFinalizationResultV3 {
25
+ /** Heartbeat cache files deleted from `.harnery/active` by the sweep. */
26
+ swept_heartbeats: number;
27
+ /** Orphaned `.harnery/pid-map` entries deleted by the sweep. */
28
+ swept_pidmaps: number;
29
+ /** Dead-owner `.last-peer-hash.*` files deleted by the sweep. */
30
+ swept_peer_hashes: number;
31
+ }
32
+ /**
33
+ * Sweep the disposable coordination cache, then reconcile session
34
+ * finalization, and report both sets of counts.
35
+ *
36
+ * The sweep owns its own safety contract (configured coordination freshness
37
+ * for valid rows, an additional stale-mtime gate for malformed or missing
38
+ * timestamps, and a durable audit record before any deletion, failing closed
39
+ * when neither can be written). Nothing here loosens it, and a sweep failure
40
+ * never blocks finalization: the cache is a projection that heals on the
41
+ * owning agent's next tool call, while the V3 ledger is authoritative.
42
+ */
43
+ export declare function reconcileCoordinationV3(coordRoot: string, options?: ReconcileSessionFinalizationOptionsV3): ReconcileCoordinationResultV3;
44
+ //# sourceMappingURL=reconcile-coordination-v3.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile-coordination-v3.d.ts","sourceRoot":"","sources":["../../../src/core/agents/reconcile-coordination-v3.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EACL,KAAK,qCAAqC,EAC1C,KAAK,oCAAoC,EAE1C,MAAM,2BAA2B,CAAC;AAGnC,MAAM,WAAW,6BAA8B,SAAQ,oCAAoC;IACzF,yEAAyE;IACzE,gBAAgB,EAAE,MAAM,CAAC;IACzB,gEAAgE;IAChE,aAAa,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,qCAA0C,GAClD,6BAA6B,CAuB/B"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The canonical supervisor reconcile pass.
3
+ *
4
+ * Two things have to happen together, in this order:
5
+ *
6
+ * 1. `staleSweep` prunes the disposable coordination cache under
7
+ * `.harnery/active` (plus orphaned pid-map and `.last-peer-hash` files)
8
+ * and leaves a durable lifecycle observation for every deletion.
9
+ * 2. `reconcileSessionFinalizationV3` turns those observations, along with
10
+ * archive/idle/run/host signals, into session finalization requests.
11
+ *
12
+ * Running the sweep first is what makes a single pass self-contained: the
13
+ * `lifecycle.sweep_observed` rows it writes are visible to the finalizer in
14
+ * the same pass, so a stale session is both reaped and finalized without
15
+ * waiting for a later invocation.
16
+ *
17
+ * ADR 0077 already specified that `agents reconcile` and session start
18
+ * discover stale-sweep in the same pass. Both entry points had drifted to
19
+ * calling only the finalizer, so the sweep never ran outside its own
20
+ * subcommand. This module is the single composition both now share; adding a
21
+ * caller means calling this, not re-composing the two halves.
22
+ */
23
+ import { reconcileSessionFinalizationV3, } from "./session-finalizer-v3.js";
24
+ import { staleSweep } from "./state/stale-sweep.js";
25
+ /**
26
+ * Sweep the disposable coordination cache, then reconcile session
27
+ * finalization, and report both sets of counts.
28
+ *
29
+ * The sweep owns its own safety contract (configured coordination freshness
30
+ * for valid rows, an additional stale-mtime gate for malformed or missing
31
+ * timestamps, and a durable audit record before any deletion, failing closed
32
+ * when neither can be written). Nothing here loosens it, and a sweep failure
33
+ * never blocks finalization: the cache is a projection that heals on the
34
+ * owning agent's next tool call, while the V3 ledger is authoritative.
35
+ */
36
+ export function reconcileCoordinationV3(coordRoot, options = {}) {
37
+ let sweptHeartbeats = 0;
38
+ let sweptPidmaps = 0;
39
+ let sweptPeerHashes = 0;
40
+ const sweepDiagnostics = [];
41
+ try {
42
+ const sweep = staleSweep(coordRoot);
43
+ sweptHeartbeats = sweep.heartbeatsRemoved.length;
44
+ sweptPidmaps = sweep.pidmapsRemoved;
45
+ sweptPeerHashes = sweep.peerHashesRemoved;
46
+ }
47
+ catch (error) {
48
+ sweepDiagnostics.push(`stale_sweep_failed:${error instanceof Error ? error.message : String(error)}`);
49
+ }
50
+ const result = reconcileSessionFinalizationV3(coordRoot, options);
51
+ return {
52
+ ...result,
53
+ diagnostics: [...sweepDiagnostics, ...result.diagnostics],
54
+ swept_heartbeats: sweptHeartbeats,
55
+ swept_pidmaps: sweptPidmaps,
56
+ swept_peer_hashes: sweptPeerHashes,
57
+ };
58
+ }
@@ -49,6 +49,11 @@ export interface PromptContextOpts {
49
49
  /** When true, append the non-deduplicated Codex status-footer reminder.
50
50
  * The caller enables this only for human-facing Codex sessions. */
51
51
  statusFooterNudge?: boolean;
52
+ /** Adapter id this prompt is being rendered for. Gates the mailbox drain:
53
+ * an adapter that cannot receive UserPromptSubmit context must not consume
54
+ * messages here, or they are removed from the queue and never shown. When
55
+ * omitted, delivery is attempted (the default route can receive). */
56
+ adapter?: string;
52
57
  /** Adapter id whose Stop hook ENFORCES the end-of-turn ritual (task_set +
53
58
  * status check). When set, append a fresh per-prompt reminder of that
54
59
  * contract so the model satisfies it before ending the turn instead of
@@ -1 +1 @@
1
- {"version":3,"file":"prompt-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/prompt-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AA4CH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;8EAC0E;IAC1E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;kFAC8E;IAC9E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;kFAC8E;IAC9E,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;uEACmE;IACnE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;wBAIoB;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CA8DnE"}
1
+ {"version":3,"file":"prompt-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/prompt-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AA+CH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;8EAC0E;IAC1E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;kFAC8E;IAC9E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;kFAC8E;IAC9E,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;uEACmE;IACnE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;yEAGqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;wBAIoB;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAqFnE"}
@@ -37,10 +37,12 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, w
37
37
  import { join } from "node:path";
38
38
  import { coordEnv } from "../../../lib/env.js";
39
39
  import { endOfTurnStatusCommand, hostPromptReminder, resolveBinName } from "../../config.js";
40
+ import { canReceiveContext } from "../../hooks/adapter/output.js";
40
41
  import { readRemoteMachines } from "../../presence/index.js";
42
+ import { drainMailbox, formatMailboxDelivery } from "../mailbox.js";
41
43
  import { sessionNameDisplayBlock, sessionNameDisplayPending } from "../session-name-display.js";
42
44
  import { readLiveCoordinationRow, readLiveCoordinationRows, } from "../state/live-coordination-view.js";
43
- import { formatPendingCouncils } from "./session-context.js";
45
+ import { formatPendingCouncils, recordDeliveredMessages } from "./session-context.js";
44
46
  /**
45
47
  * Build the combined UserPromptSubmit additionalContext string. Returns "" when
46
48
  * nothing has changed since the last call (so the caller can skip the JSON emit).
@@ -50,19 +52,39 @@ import { formatPendingCouncils } from "./session-context.js";
50
52
  * councils are pending, matching the bash behavior).
51
53
  */
52
54
  export function renderPromptContext(opts) {
53
- const { coordRoot, instanceId, sessionId, agentName, sessionNameNudge, taskNudge, hostPromptReminder: promptReminderEnabled, statusFooterNudge, turnRitualNudge, } = opts;
55
+ const { coordRoot, instanceId, sessionId, agentName, sessionNameNudge, taskNudge, hostPromptReminder: promptReminderEnabled, statusFooterNudge, turnRitualNudge, adapter, } = opts;
54
56
  const sections = [];
55
- // 1. Peer table with hash dedup.
57
+ // The hook route does not pass --name, so resolve it from the heartbeat.
58
+ // Every name-addressed section below (messages, councils) is silently
59
+ // skipped without it.
60
+ const resolvedName = agentName ?? readLiveCoordinationRow(coordRoot, instanceId)?.name;
61
+ // 1. Peer messages. Drained first and never deduped: a message is delivered
62
+ // exactly once, and it is the one section here that carries new information
63
+ // from another agent rather than a reminder.
64
+ //
65
+ // Draining is gated on the adapter being able to receive this event's
66
+ // context. Cursor cannot, so draining there would empty the queue into output
67
+ // that is discarded; those sessions get their messages at SessionStart, which
68
+ // Cursor does deliver.
69
+ const canDeliverHere = !adapter || canReceiveContext(adapter, "UserPromptSubmit");
70
+ if (resolvedName && canDeliverHere) {
71
+ const delivered = drainMailbox(coordRoot, resolvedName);
72
+ if (delivered.length > 0) {
73
+ recordDeliveredMessages(instanceId, delivered);
74
+ sections.push(formatMailboxDelivery(coordRoot, delivered));
75
+ }
76
+ }
77
+ // 2. Peer table with hash dedup.
56
78
  const peerTable = computePeerTableIfChanged(coordRoot, instanceId, sessionId);
57
79
  if (peerTable)
58
80
  sections.push(peerTable);
59
- // 2. Council pending with hash dedup.
60
- if (agentName) {
61
- const councilMsg = computeCouncilPendingIfChanged(coordRoot, instanceId, agentName);
81
+ // 3. Council pending with hash dedup.
82
+ if (resolvedName) {
83
+ const councilMsg = computeCouncilPendingIfChanged(coordRoot, instanceId, resolvedName);
62
84
  if (councilMsg)
63
85
  sections.push(councilMsg);
64
86
  }
65
- // 3. First-session naming for every adapter, plus unset/stale-task reminders
87
+ // 4. First-session naming for every adapter, plus unset/stale-task reminders
66
88
  // for Cursor/Codex. One state machine avoids a second generic "task unset"
67
89
  // reminder immediately after the first-session reminder has been deduped.
68
90
  if (sessionNameNudge || taskNudge) {
@@ -73,7 +95,7 @@ export function renderPromptContext(opts) {
73
95
  if (nudgeMsg)
74
96
  sections.push(nudgeMsg);
75
97
  }
76
- // 4. A supported adapter gets the host reminder on every prompt. The text is
98
+ // 5. A supported adapter gets the host reminder on every prompt. The text is
77
99
  // intentionally not hash-deduped or persisted into runtime state: freshness
78
100
  // is the feature, and the project config remains its only durable copy.
79
101
  if (promptReminderEnabled) {
@@ -81,7 +103,7 @@ export function renderPromptContext(opts) {
81
103
  if (reminder)
82
104
  sections.push(reminder);
83
105
  }
84
- // 5. Codex gets this on every prompt. It is intentionally not hash-deduped:
106
+ // 6. Codex gets this on every prompt. It is intentionally not hash-deduped:
85
107
  // the reminder must be fresh in the model's context when it writes the reply.
86
108
  // Missing it is harmless because Codex Stop enforcement remains observe-only.
87
109
  if (statusFooterNudge) {
@@ -89,7 +111,7 @@ export function renderPromptContext(opts) {
89
111
  if (statusMsg)
90
112
  sections.push(statusMsg);
91
113
  }
92
- // 6. Adapters whose Stop hook ENFORCES the turn ritual get a fresh reminder
114
+ // 7. Adapters whose Stop hook ENFORCES the turn ritual get a fresh reminder
93
115
  // on every prompt. Reminding before the reply is drafted is far cheaper than
94
116
  // the bounce-and-retry the Stop hook otherwise forces: enforcement-only
95
117
  // compliance was measured missing on a double-digit share of turns. Not
@@ -113,9 +135,8 @@ function renderTurnRitualReminder(coordRoot, selfInstanceId, adapter) {
113
135
  return "";
114
136
  const bin = resolveBinName(coordRoot);
115
137
  const statusCommand = endOfTurnStatusCommand(coordRoot);
116
- // Claude Code renders the status box only if the reply carries it; Cursor
117
- // shows the box inline on its own, so it is never asked for a second copy.
118
- const paste = adapter === "claude-code"
138
+ // Claude Code and Cursor can both enforce completed assistant reply text.
139
+ const paste = adapter === "claude-code" || adapter === "cursor"
119
140
  ? " and paste its output verbatim in a fenced code block at the end of your reply"
120
141
  : "";
121
142
  return (`Turn ritual (Stop-enforced): on any turn that uses tools, run \`${bin} agents set-task "<short focus>"\` at some point during the turn (required every turn, even when the focus is unchanged; pass an empty string when there is no meaningful focus), then make \`${statusCommand}\` your final tool call${paste}. ` +
@@ -7,6 +7,7 @@
7
7
  * string.
8
8
  */
9
9
  import { statSync } from "node:fs";
10
+ import { type MailboxMessageV1 } from "../mailbox.js";
10
11
  export interface RenderOpts {
11
12
  coordRoot: string;
12
13
  instanceId: string;
@@ -21,6 +22,13 @@ export interface RenderOpts {
21
22
  * string (or "" if there's nothing to say).
22
23
  */
23
24
  export declare function renderSessionContext(opts: RenderOpts): string;
25
+ /**
26
+ * Write delivered peer messages into the recipient's own journal, so they
27
+ * survive context compaction the way any other coordination breadcrumb does.
28
+ * A message the sender already journaled (the recipient was live when it was
29
+ * sent) is skipped to avoid a duplicate entry.
30
+ */
31
+ export declare function recordDeliveredMessages(instanceId: string, messages: readonly MailboxMessageV1[]): void;
24
32
  /**
25
33
  * Returns a list of commit-guard wiring issues (empty when wired). Portable
26
34
  * across host projects: it asserts the FUNCTIONAL property ("does this repo's
@@ -1 +1 @@
1
- {"version":3,"file":"session-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/session-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAyC,QAAQ,EAAE,MAAM,SAAS,CAAC;AAwB1E,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+BAC2B;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CA6E7D;AAkID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAwBvD;AA0DD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAiDlF;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,iBAAW,CAAC"}
1
+ {"version":3,"file":"session-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/session-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAyC,QAAQ,EAAE,MAAM,SAAS,CAAC;AAM1E,OAAO,EAAuC,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAoB3F,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+BAC2B;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAwF7D;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,SAAS,gBAAgB,EAAE,GACpC,IAAI,CAUN;AAkID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAwBvD;AA0DD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAiDlF;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,iBAAW,CAAC"}
@@ -11,7 +11,9 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
  import { resolveBinName, resolveHooksSetupHint } from "../../config.js";
13
13
  import { harneryVersion, loadAdapterWiring } from "../../hooks/adapter/wiring.js";
14
+ import { appendEntry } from "../../journal/index.js";
14
15
  import { readRemoteMachines } from "../../presence/index.js";
16
+ import { drainMailbox, formatMailboxDelivery } from "../mailbox.js";
15
17
  import { readLiveCoordinationRows } from "../state/live-coordination-view.js";
16
18
  import { readForkParent } from "../state/names.js";
17
19
  /**
@@ -50,13 +52,23 @@ export function renderSessionContext(opts) {
50
52
  if (isLinkedWorktree(coordRoot)) {
51
53
  messages.push(`Running inside worktree ${process.cwd()}. The coord layer is scoped to this worktree only; use \`${resolveBinName(coordRoot)} worktree diff\` to check for conflicts against sibling worktrees.`);
52
54
  }
53
- // 3. Council invites
55
+ // 3. Peer messages queued for this name while no session held it. Draining
56
+ // here is what makes `agents ping` safe to use on a dormant agent: the sender
57
+ // never has to know whether the recipient is running.
58
+ if (agentName) {
59
+ const delivered = drainMailbox(coordRoot, agentName);
60
+ if (delivered.length > 0) {
61
+ recordDeliveredMessages(instanceId, delivered);
62
+ messages.push(formatMailboxDelivery(coordRoot, delivered));
63
+ }
64
+ }
65
+ // 4. Council invites
54
66
  if (agentName) {
55
67
  const councilMsg = formatPendingCouncils(coordRoot, agentName);
56
68
  if (councilMsg)
57
69
  messages.push(councilMsg);
58
70
  }
59
- // 4. Commit-guard wiring check
71
+ // 5. Commit-guard wiring check
60
72
  const wiringIssues = checkWiring(coordRoot);
61
73
  if (wiringIssues.length > 0) {
62
74
  const hint = resolveHooksSetupHint(coordRoot);
@@ -66,7 +78,7 @@ export function renderSessionContext(opts) {
66
78
  const wiringSummary = `Coordination hooks are NOT wired: the E-guard will not block conflicting commits, and post-commit claim pruning will not run. ${fix} Detected:\n${wiringIssues.map((i) => ` - ${i}`).join("\n")}`;
67
79
  messages.push(wiringSummary);
68
80
  }
69
- // 5. Adapter-hook drift: a harnery upgrade changed the hook set, but this
81
+ // 6. Adapter-hook drift: a harnery upgrade changed the hook set, but this
70
82
  // project's settings file hasn't been re-wired. Only fires for a adapter the
71
83
  // project already opted into (≥1 hook wired), so it never nags a project that
72
84
  // simply has a settings file. Remedy is always `<bin> init` (idempotent).
@@ -89,6 +101,25 @@ export function renderSessionContext(opts) {
89
101
  }
90
102
  return messages.join("\n\n");
91
103
  }
104
+ /**
105
+ * Write delivered peer messages into the recipient's own journal, so they
106
+ * survive context compaction the way any other coordination breadcrumb does.
107
+ * A message the sender already journaled (the recipient was live when it was
108
+ * sent) is skipped to avoid a duplicate entry.
109
+ */
110
+ export function recordDeliveredMessages(instanceId, messages) {
111
+ for (const m of messages) {
112
+ if (m.journaled)
113
+ continue;
114
+ try {
115
+ appendEntry(instanceId, "handoff", `from agent-${m.from_name}: ${m.body}`);
116
+ }
117
+ catch {
118
+ // A journal write failure must never cost the delivery itself; the
119
+ // message is already in the rendered context the model reads.
120
+ }
121
+ }
122
+ }
92
123
  /** One-line-per-agent view of sessions on other machines (presence refs). */
93
124
  function formatRemoteMachines(coordRoot) {
94
125
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"commit-conflict.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/commit-conflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAkBH,UAAU,QAAQ;IAChB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,0BAA0B;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB;wEACoE;IACpE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,qFAAqF;IACrF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,8DAA8D;IAC9D,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,oBAAoB,GAAG,mBAAmB,CAuHhG"}
1
+ {"version":3,"file":"commit-conflict.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/commit-conflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAoBH,UAAU,QAAQ;IAChB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,0BAA0B;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB;wEACoE;IACpE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,qFAAqF;IACrF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,8DAA8D;IAC9D,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,oBAAoB,GAAG,mBAAmB,CAgIhG"}