harnery 0.43.0 → 0.44.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 (278) hide show
  1. package/README.md +3 -3
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +41 -48
  4. package/dist/commands/browse.d.ts.map +1 -1
  5. package/dist/commands/browse.js +38 -11
  6. package/dist/commands/diagnostics.d.ts.map +1 -1
  7. package/dist/commands/diagnostics.js +10 -0
  8. package/dist/commands/doctor.d.ts +12 -0
  9. package/dist/commands/doctor.d.ts.map +1 -1
  10. package/dist/commands/doctor.js +22 -7
  11. package/dist/commands/governor.js +22 -10
  12. package/dist/commands/inbox.d.ts +1 -1
  13. package/dist/commands/inbox.d.ts.map +1 -1
  14. package/dist/commands/inbox.js +55 -2
  15. package/dist/commands/rm.d.ts.map +1 -1
  16. package/dist/commands/rm.js +28 -4
  17. package/dist/commands/servers.d.ts.map +1 -1
  18. package/dist/commands/servers.js +16 -1
  19. package/dist/commands/supervisor.d.ts.map +1 -1
  20. package/dist/commands/supervisor.js +7 -2
  21. package/dist/commands/toc.d.ts +14 -0
  22. package/dist/commands/toc.d.ts.map +1 -1
  23. package/dist/commands/toc.js +7 -4
  24. package/dist/commands/workflow.js +24 -30
  25. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  26. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  27. package/dist/core/agents/live-authority-v3.js +11 -0
  28. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  29. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  30. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  31. package/dist/core/agents/mailbox.d.ts +9 -8
  32. package/dist/core/agents/mailbox.d.ts.map +1 -1
  33. package/dist/core/agents/mailbox.js +24 -17
  34. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  35. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  36. package/dist/core/agents/render/prompt-context.js +8 -12
  37. package/dist/core/agents/render/session-context.d.ts +2 -8
  38. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  39. package/dist/core/agents/render/session-context.js +7 -30
  40. package/dist/core/agents/rules/stop-hook.js +3 -3
  41. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  42. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  43. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  44. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  45. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  46. package/dist/core/agents/state/pidmap.js +5 -0
  47. package/dist/core/agents/state/proc-start.d.ts +19 -0
  48. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  49. package/dist/core/agents/state/proc-start.js +41 -0
  50. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  51. package/dist/core/agents/state/stale-sweep.js +180 -70
  52. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  53. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  54. package/dist/core/agents/state/unchanged-file.js +390 -0
  55. package/dist/core/context/index.d.ts +22 -10
  56. package/dist/core/context/index.d.ts.map +1 -1
  57. package/dist/core/context/index.js +381 -119
  58. package/dist/core/context/observation.d.ts +26 -0
  59. package/dist/core/context/observation.d.ts.map +1 -0
  60. package/dist/core/context/observation.js +11 -0
  61. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  62. package/dist/core/diagnostics/bundle.js +25 -14
  63. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  64. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  65. package/dist/core/diagnostics/path-identity.js +12 -0
  66. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  67. package/dist/core/diagnostics/sanitize.js +4 -3
  68. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  69. package/dist/core/events/v3/authority-outbox.js +29 -0
  70. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  71. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  72. package/dist/core/events/v3/generation-custody.js +50 -0
  73. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  74. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  75. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  76. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  77. package/dist/core/events/v3/producers/recorder.js +77 -64
  78. package/dist/core/governor/planning.d.ts.map +1 -1
  79. package/dist/core/governor/planning.js +14 -1
  80. package/dist/core/governor/service-read.d.ts +10 -2
  81. package/dist/core/governor/service-read.d.ts.map +1 -1
  82. package/dist/core/governor/service-read.js +105 -22
  83. package/dist/core/governor/service.d.ts +20 -2
  84. package/dist/core/governor/service.d.ts.map +1 -1
  85. package/dist/core/governor/service.js +72 -47
  86. package/dist/core/hooks/cli.js +33 -5
  87. package/dist/core/inbox/contract.d.ts +2 -1
  88. package/dist/core/inbox/contract.d.ts.map +1 -1
  89. package/dist/core/inbox/index.d.ts +2 -0
  90. package/dist/core/inbox/index.d.ts.map +1 -1
  91. package/dist/core/inbox/index.js +2 -0
  92. package/dist/core/inbox/peer-messages.d.ts +37 -0
  93. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  94. package/dist/core/inbox/peer-messages.js +72 -0
  95. package/dist/core/inbox/service.d.ts +8 -1
  96. package/dist/core/inbox/service.d.ts.map +1 -1
  97. package/dist/core/inbox/service.js +67 -21
  98. package/dist/core/inbox/targets.d.ts +22 -0
  99. package/dist/core/inbox/targets.d.ts.map +1 -0
  100. package/dist/core/inbox/targets.js +108 -0
  101. package/dist/core/inbox/watcher.js +7 -3
  102. package/dist/core/journal/index.d.ts.map +1 -1
  103. package/dist/core/journal/index.js +17 -2
  104. package/dist/core/resources/contract.d.ts +3 -0
  105. package/dist/core/resources/contract.d.ts.map +1 -1
  106. package/dist/core/resources/service-status.d.ts +14 -1
  107. package/dist/core/resources/service-status.d.ts.map +1 -1
  108. package/dist/core/resources/service-status.js +23 -13
  109. package/dist/core/resources/service.d.ts +9 -1
  110. package/dist/core/resources/service.d.ts.map +1 -1
  111. package/dist/core/resources/service.js +47 -27
  112. package/dist/core/resources/status.d.ts +2 -5
  113. package/dist/core/resources/status.d.ts.map +1 -1
  114. package/dist/core/resources/status.js +36 -7
  115. package/dist/core/servers/index.d.ts +24 -13
  116. package/dist/core/servers/index.d.ts.map +1 -1
  117. package/dist/core/servers/index.js +132 -92
  118. package/dist/core/servers/tunnels.d.ts +3 -5
  119. package/dist/core/servers/tunnels.d.ts.map +1 -1
  120. package/dist/core/servers/tunnels.js +3 -9
  121. package/dist/core/supervisor/contract.d.ts +12 -0
  122. package/dist/core/supervisor/contract.d.ts.map +1 -1
  123. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  124. package/dist/core/supervisor/explanations.js +2 -1
  125. package/dist/core/supervisor/findings.d.ts.map +1 -1
  126. package/dist/core/supervisor/findings.js +54 -9
  127. package/dist/core/supervisor/service.d.ts +9 -1
  128. package/dist/core/supervisor/service.d.ts.map +1 -1
  129. package/dist/core/supervisor/service.js +59 -29
  130. package/dist/core/supervisor/services.d.ts.map +1 -1
  131. package/dist/core/supervisor/services.js +22 -1
  132. package/dist/core/supervisor/status.d.ts +17 -2
  133. package/dist/core/supervisor/status.d.ts.map +1 -1
  134. package/dist/core/supervisor/status.js +70 -12
  135. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  136. package/dist/core/supervisor/timeline.js +2 -1
  137. package/dist/core/work/state.js +7 -7
  138. package/dist/core/workflow/engine.d.ts.map +1 -1
  139. package/dist/core/workflow/engine.js +105 -32
  140. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  141. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  142. package/dist/core/workflow/failure-evidence.js +114 -0
  143. package/dist/core/workflow/index.d.ts +1 -1
  144. package/dist/core/workflow/index.d.ts.map +1 -1
  145. package/dist/core/workflow/index.js +1 -1
  146. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  147. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  148. package/dist/core/workflow/live-session-v3.js +126 -5
  149. package/dist/core/workflow/proof.d.ts +5 -5
  150. package/dist/core/workflow/proof.d.ts.map +1 -1
  151. package/dist/core/workflow/proof.js +115 -20
  152. package/dist/core/workflow/run-state.d.ts +3 -1
  153. package/dist/core/workflow/run-state.d.ts.map +1 -1
  154. package/dist/core/workflow/run-state.js +31 -27
  155. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  156. package/dist/core/workflow/spawn-claude.js +19 -0
  157. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  158. package/dist/core/workflow/spawn-codex.js +13 -0
  159. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  160. package/dist/core/workflow/spawn-cursor.js +19 -0
  161. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  162. package/dist/core/workflow/spawn-opencode.js +14 -0
  163. package/dist/core/workflow/types.d.ts +64 -17
  164. package/dist/core/workflow/types.d.ts.map +1 -1
  165. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  166. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  167. package/dist/core/workflow/workspaces/index.js +0 -1
  168. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  169. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  170. package/dist/core/workflow/workspaces/leases.js +30 -16
  171. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  172. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  173. package/dist/core/workflow/workspaces/local-git.js +270 -6
  174. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  175. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  176. package/dist/core/workflow/workspaces/ownership.js +194 -0
  177. package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
  178. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  179. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  180. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  181. package/dist/core/workflow/workspaces/recovery.js +21 -13
  182. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  183. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  184. package/dist/core/workflow/workspaces/state.js +21 -1
  185. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  186. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  187. package/dist/lib/browser/windows-chrome.js +262 -73
  188. package/dist/lib/docs-links.d.ts.map +1 -1
  189. package/dist/lib/docs-links.js +45 -2
  190. package/dist/lib/guarded-remove.d.ts +7 -0
  191. package/dist/lib/guarded-remove.d.ts.map +1 -1
  192. package/dist/lib/guarded-remove.js +106 -12
  193. package/dist/lib/identities/assume.d.ts.map +1 -1
  194. package/dist/lib/identities/assume.js +4 -0
  195. package/dist/lib/zip/index.d.ts +41 -0
  196. package/dist/lib/zip/index.d.ts.map +1 -0
  197. package/dist/lib/zip/index.js +398 -0
  198. package/package.json +7 -1
  199. package/src/commands/agents.ts +54 -63
  200. package/src/commands/browse.ts +50 -14
  201. package/src/commands/diagnostics.ts +10 -0
  202. package/src/commands/doctor.ts +26 -7
  203. package/src/commands/governor.ts +23 -10
  204. package/src/commands/inbox.ts +52 -2
  205. package/src/commands/rm.ts +43 -4
  206. package/src/commands/servers.ts +16 -4
  207. package/src/commands/supervisor.ts +7 -2
  208. package/src/commands/toc.ts +14 -6
  209. package/src/commands/workflow.ts +30 -30
  210. package/src/core/agents/live-authority-v3.ts +17 -0
  211. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  212. package/src/core/agents/mailbox.ts +26 -17
  213. package/src/core/agents/render/prompt-context.ts +10 -12
  214. package/src/core/agents/render/session-context.ts +9 -32
  215. package/src/core/agents/rules/stop-hook.ts +3 -3
  216. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  217. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  218. package/src/core/agents/state/pidmap.ts +13 -0
  219. package/src/core/agents/state/proc-start.ts +57 -0
  220. package/src/core/agents/state/stale-sweep.ts +247 -82
  221. package/src/core/agents/state/unchanged-file.ts +456 -0
  222. package/src/core/context/index.ts +446 -136
  223. package/src/core/context/observation.ts +29 -0
  224. package/src/core/diagnostics/bundle.ts +23 -18
  225. package/src/core/diagnostics/path-identity.ts +15 -0
  226. package/src/core/diagnostics/sanitize.ts +4 -3
  227. package/src/core/events/v3/authority-outbox.ts +56 -0
  228. package/src/core/events/v3/generation-custody.ts +59 -0
  229. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  230. package/src/core/events/v3/producers/recorder.ts +94 -68
  231. package/src/core/governor/planning.ts +20 -8
  232. package/src/core/governor/service-read.ts +128 -20
  233. package/src/core/governor/service.ts +119 -42
  234. package/src/core/hooks/cli.ts +33 -5
  235. package/src/core/inbox/contract.ts +2 -1
  236. package/src/core/inbox/index.ts +2 -0
  237. package/src/core/inbox/peer-messages.ts +94 -0
  238. package/src/core/inbox/service.ts +88 -22
  239. package/src/core/inbox/targets.ts +148 -0
  240. package/src/core/inbox/watcher.ts +8 -9
  241. package/src/core/journal/index.ts +19 -2
  242. package/src/core/resources/contract.ts +4 -0
  243. package/src/core/resources/service-status.ts +35 -12
  244. package/src/core/resources/service.ts +71 -24
  245. package/src/core/resources/status.ts +57 -8
  246. package/src/core/servers/index.ts +177 -91
  247. package/src/core/servers/tunnels.ts +3 -11
  248. package/src/core/supervisor/contract.ts +13 -0
  249. package/src/core/supervisor/explanations.ts +4 -1
  250. package/src/core/supervisor/findings.ts +68 -10
  251. package/src/core/supervisor/service.ts +88 -26
  252. package/src/core/supervisor/services.ts +26 -8
  253. package/src/core/supervisor/status.ts +89 -12
  254. package/src/core/supervisor/timeline.ts +2 -1
  255. package/src/core/work/state.ts +7 -7
  256. package/src/core/workflow/engine.ts +131 -49
  257. package/src/core/workflow/failure-evidence.ts +141 -0
  258. package/src/core/workflow/index.ts +0 -1
  259. package/src/core/workflow/live-session-v3.ts +146 -5
  260. package/src/core/workflow/proof.ts +134 -26
  261. package/src/core/workflow/run-state.ts +31 -29
  262. package/src/core/workflow/spawn-claude.ts +18 -0
  263. package/src/core/workflow/spawn-codex.ts +13 -0
  264. package/src/core/workflow/spawn-cursor.ts +18 -0
  265. package/src/core/workflow/spawn-opencode.ts +14 -0
  266. package/src/core/workflow/types.ts +51 -17
  267. package/src/core/workflow/workspaces/index.ts +0 -1
  268. package/src/core/workflow/workspaces/leases.ts +48 -17
  269. package/src/core/workflow/workspaces/local-git.ts +315 -5
  270. package/src/core/workflow/workspaces/ownership.ts +232 -0
  271. package/src/core/workflow/workspaces/reclaim.ts +58 -0
  272. package/src/core/workflow/workspaces/recovery.ts +25 -13
  273. package/src/core/workflow/workspaces/state.ts +20 -0
  274. package/src/lib/browser/windows-chrome.ts +346 -86
  275. package/src/lib/docs-links.ts +49 -2
  276. package/src/lib/guarded-remove.ts +132 -14
  277. package/src/lib/identities/assume.ts +4 -0
  278. package/src/lib/zip/index.ts +483 -0
@@ -0,0 +1,94 @@
1
+ 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";
5
+
6
+ const DAY = 24 * 60 * 60_000;
7
+
8
+ /** Preserve the existing ping body/count bounds. Pending messages have no TTL;
9
+ * the lifecycle planner protects them through the idle finalization window. */
10
+ export const PEER_INBOX_LIMITS: HarneryInboxLimits = {
11
+ max_message_body_bytes: 4_000,
12
+ max_pending_count: 25,
13
+ max_pending_bytes: 100_000,
14
+ max_history_bytes: 1024 * 1024,
15
+ max_history_records: 10_000,
16
+ warning_pressure_ratio: 0.8,
17
+ max_surface_count: 10,
18
+ max_surface_bytes: 16_000,
19
+ max_surface_tokens: 4_000,
20
+ surfaced_grace_ms: 7 * DAY,
21
+ terminal_grace_ms: 7 * DAY,
22
+ };
23
+
24
+ export function peerInbox(coordRoot: string): HarneryInboxService {
25
+ return new HarneryInboxService({ coord_root: coordRoot, limits: PEER_INBOX_LIMITS });
26
+ }
27
+
28
+ /** CLI and web use this one path. A receipt proves a synced append, never that
29
+ * a process is awake or that the recipient has noticed the message. */
30
+ export function sendPeerMessage(input: {
31
+ coordRoot: string;
32
+ target: MessageTargetInput;
33
+ senderInstanceId: string | null;
34
+ senderName: string;
35
+ 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(),
45
+ });
46
+ return { ok: true as const, target: target.target, ...receipt, state: target.target.delivery };
47
+ }
48
+
49
+ export function formatPeerInbox(
50
+ coordRoot: string,
51
+ messages: readonly HarneryInboxMessageRecordV1[],
52
+ ): string {
53
+ if (messages.length === 0) return "";
54
+ const onlyAgents = messages.every(({ sender_instance_id }) => sender_instance_id !== null);
55
+ const header = onlyAgents
56
+ ? messages.length === 1
57
+ ? "Message from a peer agent:"
58
+ : `${messages.length} messages from peer agents:`
59
+ : messages.length === 1
60
+ ? "Message for this task:"
61
+ : `${messages.length} messages for this task:`;
62
+ const body = messages
63
+ .map(
64
+ (message) =>
65
+ ` ${message.message_id} from ${message.sender_instance_id === null ? "Operator" : `agent-${message.sender_display_name}`} (${message.created_at}):\n` +
66
+ message.body
67
+ .split("\n")
68
+ .map((line) => ` ${line}`)
69
+ .join("\n"),
70
+ )
71
+ .join("\n\n");
72
+ const replyTarget = messages.find(({ sender_instance_id }) => sender_instance_id !== null);
73
+ return (
74
+ `${header}\n\n${body}\n\nMessages are bound to this task instance.` +
75
+ (replyTarget
76
+ ? ` Reply with \`${resolveBinName(coordRoot)} agents ping ${replyTarget.sender_display_name} ` +
77
+ `--instance-id ${replyTarget.sender_instance_id} "<your reply>"\`.`
78
+ : "")
79
+ );
80
+ }
81
+
82
+ /** Wait for every earlier stdout write to finish before acknowledging context.
83
+ * If output fails, the caller throws and leaves the batch pending. */
84
+ export async function acknowledgeEmittedPeerInbox(
85
+ coordRoot: string,
86
+ instanceId: string,
87
+ messageIds: readonly string[],
88
+ ): Promise<void> {
89
+ if (messageIds.length === 0) return;
90
+ await new Promise<void>((resolve, reject) => {
91
+ process.stdout.write("", (error) => (error ? reject(error) : resolve()));
92
+ });
93
+ peerInbox(coordRoot).acknowledgeSurface(instanceId, messageIds);
94
+ }
@@ -1,7 +1,12 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { existsSync, statSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
- import { readCrashSafeJsonlFile, updateCrashSafeJsonlFile } from "../storage/durable-history.ts";
4
+ import {
5
+ type CrashSafeJsonlUpdate,
6
+ type DurableHistoryReadOptions,
7
+ readCrashSafeJsonlFile,
8
+ updateCrashSafeJsonlFile,
9
+ } from "../storage/durable-history.ts";
5
10
  import {
6
11
  HARNERY_INBOX_RECORD_SCHEMA,
7
12
  HARNERY_INBOX_STATUS_SCHEMA,
@@ -22,7 +27,7 @@ export interface HarneryInboxServiceOptions {
22
27
  }
23
28
 
24
29
  export interface HarneryInboxSendInput {
25
- sender_instance_id: string;
30
+ sender_instance_id: string | null;
26
31
  sender_display_name: string;
27
32
  recipient_instance_id: string;
28
33
  recipient_display_name: string;
@@ -59,7 +64,7 @@ export class HarneryInboxService {
59
64
  schema: HARNERY_INBOX_RECORD_SCHEMA,
60
65
  kind: "message",
61
66
  message_id: `msg_${this.#options.id?.() ?? randomUUID()}`,
62
- sender_instance_id: opaqueId(input.sender_instance_id, "sender instance"),
67
+ sender_instance_id: senderId(input.sender_instance_id),
63
68
  sender_display_name: boundedDisplay(input.sender_display_name),
64
69
  recipient_instance_id: recipient,
65
70
  recipient_display_name: boundedDisplay(input.recipient_display_name),
@@ -68,7 +73,7 @@ export class HarneryInboxService {
68
73
  body_bytes: bodyBytes,
69
74
  };
70
75
  const path = inboxPath(this.#options.coord_root, recipient);
71
- return updateCrashSafeJsonlFile<HarneryInboxRecordV1, HarneryInboxSendReceipt>(
76
+ return updateInboxHistory<HarneryInboxRecordV1, HarneryInboxSendReceipt>(
72
77
  path,
73
78
  readOptions(limits),
74
79
  (records) => {
@@ -88,10 +93,12 @@ export class HarneryInboxService {
88
93
  }
89
94
 
90
95
  records(recipientInstanceId: string): HarneryInboxRecordV1[] {
91
- return readCrashSafeJsonlFile<HarneryInboxRecordV1>(
96
+ const records = readCrashSafeJsonlFile<HarneryInboxRecordV1>(
92
97
  inboxPath(this.#options.coord_root, opaqueId(recipientInstanceId, "recipient instance")),
93
98
  readOptions(this.#options.limits),
94
99
  );
100
+ for (const record of records) validateRecord(record);
101
+ return records;
95
102
  }
96
103
 
97
104
  pending(recipientInstanceId: string): HarneryInboxMessageRecordV1[] {
@@ -133,13 +140,45 @@ export class HarneryInboxService {
133
140
  max_tokens: this.#options.limits.max_surface_tokens,
134
141
  },
135
142
  ): Promise<HarneryInboxSurfaceResult> {
136
- validateSurfaceBudget(budget);
137
143
  const recipient = opaqueId(recipientInstanceId, "recipient instance");
138
- const selected = selectOldestFitting(this.pending(recipient), budget);
144
+ const selected = this.prepareSurface(recipient, budget);
139
145
  await emit(selected);
140
- if (selected.length > 0) {
146
+ this.acknowledgeSurface(
147
+ recipient,
148
+ selected.map(({ message_id }) => message_id),
149
+ );
150
+ const remaining = this.pending(recipient);
151
+ return {
152
+ emitted: selected,
153
+ remaining_pending_count: remaining.length,
154
+ remaining_pending_bytes: remaining.reduce((sum, record) => sum + record.body_bytes, 0),
155
+ repeated_after_crash_possible: true,
156
+ };
157
+ }
158
+
159
+ /** Preparing prompt text is read-only. A failed or abandoned emission must
160
+ * leave the same messages pending for the recipient's next prompt. */
161
+ prepareSurface(
162
+ recipientInstanceId: string,
163
+ budget: HarneryInboxSurfaceBudget = {
164
+ max_count: this.#options.limits.max_surface_count,
165
+ max_bytes: this.#options.limits.max_surface_bytes,
166
+ max_tokens: this.#options.limits.max_surface_tokens,
167
+ },
168
+ ): HarneryInboxMessageRecordV1[] {
169
+ validateSurfaceBudget(budget);
170
+ return selectOldestFitting(this.pending(recipientInstanceId), budget);
171
+ }
172
+
173
+ /** Call only after the selected IDs have actually been emitted. A message
174
+ * appended while output is in flight remains pending rather than being
175
+ * included in an acknowledgement of an older batch. */
176
+ acknowledgeSurface(recipientInstanceId: string, messageIds: readonly string[]): void {
177
+ const recipient = opaqueId(recipientInstanceId, "recipient instance");
178
+ const selected = new Set(messageIds);
179
+ if (selected.size > 0) {
141
180
  const surfacedAt = now(this.#options).toISOString();
142
- updateCrashSafeJsonlFile<HarneryInboxRecordV1, void>(
181
+ updateInboxHistory<HarneryInboxRecordV1, void>(
143
182
  inboxPath(this.#options.coord_root, recipient),
144
183
  readOptions(this.#options.limits),
145
184
  (records) => {
@@ -147,9 +186,9 @@ export class HarneryInboxService {
147
186
  project(records).pending.map(({ message_id }) => message_id),
148
187
  );
149
188
  return {
150
- append: selected
151
- .filter(({ message_id }) => stillPending.has(message_id))
152
- .map(({ message_id }) => ({
189
+ append: [...selected]
190
+ .filter((message_id) => stillPending.has(message_id))
191
+ .map((message_id) => ({
153
192
  schema: HARNERY_INBOX_RECORD_SCHEMA,
154
193
  kind: "surfaced" as const,
155
194
  message_id,
@@ -160,13 +199,32 @@ export class HarneryInboxService {
160
199
  },
161
200
  );
162
201
  }
163
- const remaining = this.pending(recipient);
164
- return {
165
- emitted: selected,
166
- remaining_pending_count: remaining.length,
167
- remaining_pending_bytes: remaining.reduce((sum, record) => sum + record.body_bytes, 0),
168
- repeated_after_crash_possible: true,
169
- };
202
+ }
203
+ }
204
+
205
+ /** The shared writer refuses an occupied lease before invoking the update.
206
+ * Briefly retry only that exact refusal so concurrent peer senders both get
207
+ * a chance to append. All validation, authority and I/O failures still fail
208
+ * immediately, and an occupied live lease is never removed or overridden. */
209
+ function updateInboxHistory<T, R>(
210
+ path: string,
211
+ options: DurableHistoryReadOptions,
212
+ update: (records: readonly T[]) => CrashSafeJsonlUpdate<R>,
213
+ ): R {
214
+ const deadline = performance.now() + 2_000;
215
+ const pause = new Int32Array(new SharedArrayBuffer(4));
216
+ while (true) {
217
+ try {
218
+ return updateCrashSafeJsonlFile(path, options, update);
219
+ } catch (error) {
220
+ if (
221
+ !(error instanceof Error) ||
222
+ error.message !== `durable history lease busy: ${path}.lease` ||
223
+ performance.now() >= deadline
224
+ )
225
+ throw error;
226
+ Atomics.wait(pause, 0, 0, Math.min(10, deadline - performance.now()));
227
+ }
170
228
  }
171
229
  }
172
230
 
@@ -270,10 +328,15 @@ function now(options: HarneryInboxServiceOptions): Date {
270
328
  }
271
329
 
272
330
  function opaqueId(value: string, field: string): string {
273
- if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(value)) throw new Error(`invalid ${field} id`);
331
+ if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(value))
332
+ throw new Error(`invalid ${field} id`);
274
333
  return value;
275
334
  }
276
335
 
336
+ function senderId(value: string | null): string | null {
337
+ return value === null ? null : opaqueId(value, "sender instance");
338
+ }
339
+
277
340
  function boundedDisplay(value: string): string {
278
341
  const result = value.trim();
279
342
  if (result.length === 0 || Buffer.byteLength(result) > 256)
@@ -283,8 +346,11 @@ function boundedDisplay(value: string): string {
283
346
 
284
347
  function validateRecord(record: HarneryInboxRecordV1): void {
285
348
  if (record.schema !== HARNERY_INBOX_RECORD_SCHEMA) throw new Error("unknown inbox record schema");
286
- if (record.kind === "message" && Buffer.byteLength(record.body) !== record.body_bytes) {
287
- throw new Error(`inbox message ${record.message_id} has invalid body_bytes`);
349
+ if (record.kind === "message") {
350
+ senderId(record.sender_instance_id);
351
+ if (Buffer.byteLength(record.body) !== record.body_bytes) {
352
+ throw new Error(`inbox message ${record.message_id} has invalid body_bytes`);
353
+ }
288
354
  }
289
355
  }
290
356
 
@@ -0,0 +1,148 @@
1
+ import { resolveName } from "../agents/state/names.ts";
2
+ import { coordFreshnessSeconds } from "../config.ts";
3
+ import {
4
+ type CoordinationGenerationViewV3,
5
+ type CoordinationViewV3,
6
+ readCoordinationViewV3,
7
+ requireAuthoritySafeCoordinationViewV3,
8
+ } from "../events/v3/coordination-view.ts";
9
+ import {
10
+ liveInstanceIdV3,
11
+ nativeInstanceIdV3,
12
+ observeLiveEventLedgerRouteV3,
13
+ } from "../events/v3/live-route-observer.ts";
14
+
15
+ export interface MessageTargetInput {
16
+ name?: string;
17
+ instanceId?: string;
18
+ requireLive?: boolean;
19
+ now?: Date;
20
+ }
21
+
22
+ export type MessageTargetResult =
23
+ | {
24
+ ok: true;
25
+ target: { instance_id: string; name: string; delivery: "written-live" | "queued" };
26
+ }
27
+ | {
28
+ ok: false;
29
+ code: "no_peer" | "ambiguous_peer" | "not_live" | "authority_unsafe";
30
+ message: string;
31
+ candidates?: string[];
32
+ };
33
+
34
+ /** Resolve one immutable recipient without extending presence or claiming attention. */
35
+ export function resolveMessageTarget(
36
+ coordRoot: string,
37
+ input: MessageTargetInput,
38
+ ): MessageTargetResult {
39
+ const requestedName = normalizeName(input.name);
40
+ const requestedInstance = input.instanceId?.trim();
41
+ if (!requestedName && !requestedInstance) {
42
+ return { ok: false, code: "no_peer", message: "Supply an exact agent name or instance id." };
43
+ }
44
+ const now = input.now?.getTime() ?? Date.now();
45
+ if (!Number.isFinite(now)) return unsafe("The message addressing clock is invalid.");
46
+
47
+ let view: CoordinationViewV3;
48
+ try {
49
+ if (observeLiveEventLedgerRouteV3(coordRoot).state === "blocked") {
50
+ return unsafe("The event ledger cannot establish message recipient authority.");
51
+ }
52
+ view = requireAuthoritySafeCoordinationViewV3(readCoordinationViewV3(coordRoot));
53
+ if (!view.source_complete) return unsafe("The event ledger read is incomplete.");
54
+ } catch {
55
+ return unsafe("The event ledger cannot establish message recipient authority.");
56
+ }
57
+
58
+ // Address historical instances without reopening them. A current generation
59
+ // wins over its own terminal history; otherwise use its last ledger position.
60
+ const byInstance = new Map<string, CoordinationGenerationViewV3>();
61
+ for (const generation of Object.values(view.terminal_generations)) {
62
+ const prior = byInstance.get(generation.instance_id);
63
+ if (!prior || laterPosition(generation, prior))
64
+ byInstance.set(generation.instance_id, generation);
65
+ }
66
+ for (const generation of Object.values(view.instances))
67
+ byInstance.set(generation.instance_id, generation);
68
+ const freshnessMs = coordFreshnessSeconds(coordRoot) * 1_000;
69
+ const candidates = [...byInstance.values()].flatMap((generation) => {
70
+ const instanceId = nativeInstanceIdV3(generation.instance_id);
71
+ const name = resolveName(coordRoot, instanceId)?.name;
72
+ if (!name) return [];
73
+ if (requestedInstance && liveInstanceIdV3(requestedInstance) !== generation.instance_id)
74
+ return [];
75
+ if (requestedName && normalizeName(name) !== requestedName) return [];
76
+ const age = now - Date.parse(generation.last_observed_at);
77
+ const diagnostics = view.diagnostics_by_generation[generation.generation_id] ?? [];
78
+ return [
79
+ {
80
+ instanceId,
81
+ name,
82
+ safe: !diagnostics.some((diagnostic) => diagnostic.authority_blocking),
83
+ fresh:
84
+ generation.phase === "live" && Number.isFinite(age) && age >= 0 && age <= freshnessMs,
85
+ },
86
+ ];
87
+ });
88
+
89
+ if (candidates.length === 0) {
90
+ return {
91
+ ok: false,
92
+ code: "no_peer",
93
+ message: "No known agent matches that exact name and instance.",
94
+ };
95
+ }
96
+ if (candidates.some((candidate) => !candidate.safe)) {
97
+ return unsafe("A matching agent generation does not have safe coordination authority.");
98
+ }
99
+ const fresh = candidates.filter((candidate) => candidate.fresh);
100
+ const selected = requestedInstance ? candidates : fresh.length > 0 ? fresh : candidates;
101
+ if (selected.length !== 1) {
102
+ return {
103
+ ok: false,
104
+ code: "ambiguous_peer",
105
+ message: "Several agent instances hold that exact name; specify --instance-id.",
106
+ candidates: selected.map(({ instanceId }) => instanceId).sort(),
107
+ };
108
+ }
109
+ const target = selected[0]!;
110
+ if (input.requireLive && !target.fresh) {
111
+ return {
112
+ ok: false,
113
+ code: "not_live",
114
+ message:
115
+ "That agent is known but outside live presence; omit --require-live to queue its message.",
116
+ };
117
+ }
118
+ return {
119
+ ok: true,
120
+ target: {
121
+ instance_id: target.instanceId,
122
+ name: target.name,
123
+ delivery: target.fresh ? "written-live" : "queued",
124
+ },
125
+ };
126
+ }
127
+
128
+ function laterPosition(
129
+ left: CoordinationGenerationViewV3,
130
+ right: CoordinationGenerationViewV3,
131
+ ): boolean {
132
+ return (
133
+ left.source_position.segment_ordinal > right.source_position.segment_ordinal ||
134
+ (left.source_position.segment_ordinal === right.source_position.segment_ordinal &&
135
+ left.source_position.byte_offset > right.source_position.byte_offset)
136
+ );
137
+ }
138
+
139
+ function normalizeName(name: string | undefined): string {
140
+ return (name ?? "")
141
+ .trim()
142
+ .replace(/^agent-/i, "")
143
+ .toLowerCase();
144
+ }
145
+
146
+ function unsafe(message: string): MessageTargetResult {
147
+ return { ok: false, code: "authority_unsafe", message };
148
+ }
@@ -23,14 +23,13 @@ export async function* watchInbox(
23
23
 
24
24
  function delay(ms: number, signal: AbortSignal): Promise<void> {
25
25
  return new Promise((resolve) => {
26
- const timer = setTimeout(resolve, ms);
27
- signal.addEventListener(
28
- "abort",
29
- () => {
30
- clearTimeout(timer);
31
- resolve();
32
- },
33
- { once: true },
34
- );
26
+ const finish = () => {
27
+ clearTimeout(timer);
28
+ signal.removeEventListener("abort", finish);
29
+ resolve();
30
+ };
31
+ const timer = setTimeout(finish, ms);
32
+ signal.addEventListener("abort", finish, { once: true });
33
+ if (signal.aborted) finish();
35
34
  });
36
35
  }
@@ -20,6 +20,14 @@ import {
20
20
  readLiveCoordinationRow,
21
21
  readLiveCoordinationRows,
22
22
  } from "../agents/state/live-coordination-view.ts";
23
+ import {
24
+ readCoordinationViewV3,
25
+ requireAuthoritySafeCoordinationViewV3,
26
+ } from "../events/v3/coordination-view.ts";
27
+ import {
28
+ liveInstanceIdV3,
29
+ observeLiveEventLedgerRouteV3,
30
+ } from "../events/v3/live-route-observer.ts";
23
31
 
24
32
  /**
25
33
  * Agent journal: per-agent markdown journal at `.harnery/journal/<instance_id>.md`.
@@ -421,11 +429,20 @@ export function sweepOrphanJournals(): string[] {
421
429
  const dir = journalDir();
422
430
  const root = monorepoRoot();
423
431
  const archived: string[] = [];
432
+ if (!root) return archived;
433
+ let view: ReturnType<typeof readCoordinationViewV3>;
434
+ try {
435
+ if (observeLiveEventLedgerRouteV3(root).state === "blocked") return archived;
436
+ view = requireAuthoritySafeCoordinationViewV3(readCoordinationViewV3(root));
437
+ if (!view.source_complete) return archived;
438
+ } catch {
439
+ // A missing or unsafe read does not prove that an owner has ended.
440
+ return archived;
441
+ }
424
442
  for (const f of readdirSync(dir)) {
425
443
  if (!f.endsWith(".md")) continue;
426
444
  const instanceId = f.replace(/\.md$/, "");
427
- const hb = root ? readLiveCoordinationRow(root, instanceId) : null;
428
- if (!hb) {
445
+ if (!view.instances[liveInstanceIdV3(instanceId)]) {
429
446
  const dest = archiveJournal(instanceId);
430
447
  if (dest) archived.push(dest);
431
448
  }
@@ -1,3 +1,5 @@
1
+ import type { ProcessGenerationVerdict } from "../agents/state/proc-start.ts";
2
+
1
3
  export const RESOURCE_SNAPSHOT_SCHEMA_VERSION = 2;
2
4
  export const RESOURCE_SERVICE_STATUS_SCHEMA_VERSION = 1;
3
5
 
@@ -154,6 +156,8 @@ export interface ResourceServiceStatusRecord {
154
156
  export interface ResourceServiceStatus {
155
157
  running: boolean;
156
158
  stale: boolean;
159
+ generation: ProcessGenerationVerdict;
160
+ liveness: "running" | "stopped" | "unknown";
157
161
  record?: ResourceServiceStatusRecord;
158
162
  status_path: string;
159
163
  snapshot_path: string;
@@ -1,5 +1,9 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { hostname } from "node:os";
2
- import { checkPidToken } from "../agents/state/proc-start.ts";
3
+ import {
4
+ type ProcessGenerationVerdict,
5
+ verifyProcessGeneration,
6
+ } from "../agents/state/proc-start.ts";
3
7
  import {
4
8
  RESOURCE_SERVICE_STATUS_SCHEMA_VERSION,
5
9
  type ResourceServiceStatus,
@@ -9,9 +13,15 @@ import { readResourceServiceRecord, resourcePaths } from "./storage.ts";
9
13
 
10
14
  const STALE_HEARTBEAT_MS = 15_000;
11
15
 
16
+ export interface ResourceProcessDependencies {
17
+ readToken?: (pid: number) => string | null;
18
+ probePid?: (pid: number) => unknown;
19
+ }
20
+
12
21
  export function readResourceServiceStatus(
13
22
  coordRoot: string,
14
23
  nowMs = Date.now(),
24
+ dependencies: ResourceProcessDependencies = {},
15
25
  ): ResourceServiceStatus {
16
26
  const paths = resourcePaths(coordRoot);
17
27
  const record = readResourceServiceRecord(coordRoot);
@@ -19,21 +29,22 @@ export function readResourceServiceStatus(
19
29
  return {
20
30
  running: false,
21
31
  stale: false,
32
+ generation: "unknown",
33
+ liveness: existsSync(paths.service) ? "unknown" : "stopped",
22
34
  status_path: paths.service,
23
35
  snapshot_path: paths.snapshot,
24
36
  };
25
37
  }
26
38
  const heartbeatAt = Date.parse(record.heartbeat_at);
27
39
  const stale = !Number.isFinite(heartbeatAt) || nowMs - heartbeatAt > STALE_HEARTBEAT_MS;
28
- const local = record.host === hostname();
29
- const running =
30
- local &&
31
- !stale &&
32
- (record.state === "starting" || record.state === "running" || record.state === "stopping") &&
33
- pidStillMatches(record.pid, record.start_token);
40
+ const owner = resourceProcessLiveness(record, dependencies);
41
+ const liveness =
42
+ owner.generation === "same" && record.state === "stopped" ? "stopped" : owner.liveness;
34
43
  return {
35
- running,
44
+ running: liveness === "running" && !stale,
36
45
  stale,
46
+ generation: owner.generation,
47
+ liveness,
37
48
  record,
38
49
  status_path: paths.service,
39
50
  snapshot_path: paths.snapshot,
@@ -52,11 +63,23 @@ function validRecord(
52
63
  );
53
64
  }
54
65
 
55
- function pidStillMatches(pid: number, startToken: string | undefined): boolean {
66
+ export function resourceProcessLiveness(
67
+ owner: { pid: number; host: string; start_token?: string },
68
+ dependencies: ResourceProcessDependencies = {},
69
+ ): { generation: ProcessGenerationVerdict; liveness: ResourceServiceStatus["liveness"] } {
70
+ if (owner.host !== hostname()) return { generation: "unknown", liveness: "unknown" };
56
71
  try {
57
- process.kill(pid, 0);
72
+ (dependencies.probePid ?? ((pid) => process.kill(pid, 0)))(owner.pid);
58
73
  } catch (error) {
59
- if ((error as NodeJS.ErrnoException).code !== "EPERM") return false;
74
+ return {
75
+ generation: "unknown",
76
+ liveness: (error as NodeJS.ErrnoException).code === "ESRCH" ? "stopped" : "unknown",
77
+ };
60
78
  }
61
- return checkPidToken(pid, startToken) !== "mismatch";
79
+ const generation = verifyProcessGeneration(owner.pid, owner.start_token, dependencies.readToken);
80
+ return {
81
+ generation,
82
+ liveness:
83
+ generation === "same" ? "running" : generation === "different" ? "stopped" : "unknown",
84
+ };
62
85
  }