harnery 0.43.0 → 0.45.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 (323) hide show
  1. package/README.md +3 -3
  2. package/dist/commander.js +1 -0
  3. package/dist/commands/agents.d.ts.map +1 -1
  4. package/dist/commands/agents.js +41 -48
  5. package/dist/commands/browse.d.ts.map +1 -1
  6. package/dist/commands/browse.js +38 -11
  7. package/dist/commands/diagnostics.d.ts.map +1 -1
  8. package/dist/commands/diagnostics.js +10 -0
  9. package/dist/commands/doctor.d.ts +12 -0
  10. package/dist/commands/doctor.d.ts.map +1 -1
  11. package/dist/commands/doctor.js +22 -7
  12. package/dist/commands/files.d.ts +2 -2
  13. package/dist/commands/files.d.ts.map +1 -1
  14. package/dist/commands/files.js +7 -4
  15. package/dist/commands/governor.js +22 -10
  16. package/dist/commands/inbox.d.ts +1 -1
  17. package/dist/commands/inbox.d.ts.map +1 -1
  18. package/dist/commands/inbox.js +55 -2
  19. package/dist/commands/rm.d.ts.map +1 -1
  20. package/dist/commands/rm.js +28 -4
  21. package/dist/commands/servers.d.ts.map +1 -1
  22. package/dist/commands/servers.js +26 -17
  23. package/dist/commands/supervisor.d.ts.map +1 -1
  24. package/dist/commands/supervisor.js +7 -2
  25. package/dist/commands/toc.d.ts +14 -0
  26. package/dist/commands/toc.d.ts.map +1 -1
  27. package/dist/commands/toc.js +7 -4
  28. package/dist/commands/tunnel.d.ts +15 -0
  29. package/dist/commands/tunnel.d.ts.map +1 -1
  30. package/dist/commands/tunnel.js +453 -153
  31. package/dist/commands/turn-rollback.d.ts +5 -0
  32. package/dist/commands/turn-rollback.d.ts.map +1 -0
  33. package/dist/commands/turn-rollback.js +76 -0
  34. package/dist/commands/workflow.d.ts.map +1 -1
  35. package/dist/commands/workflow.js +68 -30
  36. package/dist/core/agents/coord-client.d.ts.map +1 -1
  37. package/dist/core/agents/coord-client.js +18 -0
  38. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  39. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  40. package/dist/core/agents/live-authority-v3.js +11 -0
  41. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  42. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  43. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  44. package/dist/core/agents/mailbox.d.ts +9 -8
  45. package/dist/core/agents/mailbox.d.ts.map +1 -1
  46. package/dist/core/agents/mailbox.js +24 -17
  47. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  48. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  49. package/dist/core/agents/render/prompt-context.js +8 -12
  50. package/dist/core/agents/render/session-context.d.ts +2 -8
  51. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  52. package/dist/core/agents/render/session-context.js +7 -30
  53. package/dist/core/agents/rules/stop-hook.js +3 -3
  54. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  55. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  56. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  57. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  58. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  59. package/dist/core/agents/state/pidmap.js +5 -0
  60. package/dist/core/agents/state/proc-start.d.ts +19 -1
  61. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  62. package/dist/core/agents/state/proc-start.js +46 -1
  63. package/dist/core/agents/state/process-handle.d.ts +26 -0
  64. package/dist/core/agents/state/process-handle.d.ts.map +1 -0
  65. package/dist/core/agents/state/process-handle.js +121 -0
  66. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  67. package/dist/core/agents/state/stale-sweep.js +180 -70
  68. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  69. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  70. package/dist/core/agents/state/unchanged-file.js +390 -0
  71. package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
  72. package/dist/core/artifacts/delivery-card.js +2 -1
  73. package/dist/core/context/index.d.ts +22 -10
  74. package/dist/core/context/index.d.ts.map +1 -1
  75. package/dist/core/context/index.js +381 -119
  76. package/dist/core/context/observation.d.ts +26 -0
  77. package/dist/core/context/observation.d.ts.map +1 -0
  78. package/dist/core/context/observation.js +11 -0
  79. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  80. package/dist/core/diagnostics/bundle.js +25 -14
  81. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  82. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  83. package/dist/core/diagnostics/path-identity.js +12 -0
  84. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  85. package/dist/core/diagnostics/sanitize.js +4 -3
  86. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  87. package/dist/core/events/v3/authority-outbox.js +29 -0
  88. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  89. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  90. package/dist/core/events/v3/generation-custody.js +50 -0
  91. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  92. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  93. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  94. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  95. package/dist/core/events/v3/producers/recorder.js +77 -64
  96. package/dist/core/governor/planning.d.ts.map +1 -1
  97. package/dist/core/governor/planning.js +14 -1
  98. package/dist/core/governor/service-read.d.ts +10 -2
  99. package/dist/core/governor/service-read.d.ts.map +1 -1
  100. package/dist/core/governor/service-read.js +105 -22
  101. package/dist/core/governor/service.d.ts +20 -2
  102. package/dist/core/governor/service.d.ts.map +1 -1
  103. package/dist/core/governor/service.js +72 -47
  104. package/dist/core/hooks/cli.js +33 -5
  105. package/dist/core/inbox/contract.d.ts +2 -1
  106. package/dist/core/inbox/contract.d.ts.map +1 -1
  107. package/dist/core/inbox/index.d.ts +2 -0
  108. package/dist/core/inbox/index.d.ts.map +1 -1
  109. package/dist/core/inbox/index.js +2 -0
  110. package/dist/core/inbox/peer-messages.d.ts +37 -0
  111. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  112. package/dist/core/inbox/peer-messages.js +72 -0
  113. package/dist/core/inbox/service.d.ts +8 -1
  114. package/dist/core/inbox/service.d.ts.map +1 -1
  115. package/dist/core/inbox/service.js +67 -21
  116. package/dist/core/inbox/targets.d.ts +22 -0
  117. package/dist/core/inbox/targets.d.ts.map +1 -0
  118. package/dist/core/inbox/targets.js +108 -0
  119. package/dist/core/inbox/watcher.js +7 -3
  120. package/dist/core/journal/index.d.ts.map +1 -1
  121. package/dist/core/journal/index.js +17 -2
  122. package/dist/core/resources/contract.d.ts +3 -0
  123. package/dist/core/resources/contract.d.ts.map +1 -1
  124. package/dist/core/resources/service-status.d.ts +14 -1
  125. package/dist/core/resources/service-status.d.ts.map +1 -1
  126. package/dist/core/resources/service-status.js +23 -13
  127. package/dist/core/resources/service.d.ts +7 -13
  128. package/dist/core/resources/service.d.ts.map +1 -1
  129. package/dist/core/resources/service.js +34 -91
  130. package/dist/core/resources/status.d.ts +2 -5
  131. package/dist/core/resources/status.d.ts.map +1 -1
  132. package/dist/core/resources/status.js +36 -7
  133. package/dist/core/semantic/service.d.ts.map +1 -1
  134. package/dist/core/semantic/service.js +2 -9
  135. package/dist/core/servers/index.d.ts +34 -22
  136. package/dist/core/servers/index.d.ts.map +1 -1
  137. package/dist/core/servers/index.js +297 -144
  138. package/dist/core/servers/tunnels.d.ts +7 -7
  139. package/dist/core/servers/tunnels.d.ts.map +1 -1
  140. package/dist/core/servers/tunnels.js +47 -10
  141. package/dist/core/supervisor/contract.d.ts +12 -0
  142. package/dist/core/supervisor/contract.d.ts.map +1 -1
  143. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  144. package/dist/core/supervisor/explanations.js +2 -1
  145. package/dist/core/supervisor/findings.d.ts.map +1 -1
  146. package/dist/core/supervisor/findings.js +54 -9
  147. package/dist/core/supervisor/service.d.ts +7 -1
  148. package/dist/core/supervisor/service.d.ts.map +1 -1
  149. package/dist/core/supervisor/service.js +59 -29
  150. package/dist/core/supervisor/services.d.ts.map +1 -1
  151. package/dist/core/supervisor/services.js +22 -1
  152. package/dist/core/supervisor/status.d.ts +17 -2
  153. package/dist/core/supervisor/status.d.ts.map +1 -1
  154. package/dist/core/supervisor/status.js +70 -12
  155. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  156. package/dist/core/supervisor/timeline.js +2 -1
  157. package/dist/core/turn-rollback/content.d.ts +53 -0
  158. package/dist/core/turn-rollback/content.d.ts.map +1 -0
  159. package/dist/core/turn-rollback/content.js +396 -0
  160. package/dist/core/turn-rollback/index.d.ts +72 -0
  161. package/dist/core/turn-rollback/index.d.ts.map +1 -0
  162. package/dist/core/turn-rollback/index.js +443 -0
  163. package/dist/core/work/state.js +7 -7
  164. package/dist/core/workflow/engine.d.ts.map +1 -1
  165. package/dist/core/workflow/engine.js +105 -32
  166. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  167. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  168. package/dist/core/workflow/failure-evidence.js +114 -0
  169. package/dist/core/workflow/index.d.ts +5 -3
  170. package/dist/core/workflow/index.d.ts.map +1 -1
  171. package/dist/core/workflow/index.js +3 -2
  172. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  173. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  174. package/dist/core/workflow/live-session-v3.js +126 -5
  175. package/dist/core/workflow/proof.d.ts +5 -5
  176. package/dist/core/workflow/proof.d.ts.map +1 -1
  177. package/dist/core/workflow/proof.js +115 -20
  178. package/dist/core/workflow/recovery-audit.d.ts +30 -0
  179. package/dist/core/workflow/recovery-audit.d.ts.map +1 -0
  180. package/dist/core/workflow/recovery-audit.js +116 -0
  181. package/dist/core/workflow/run-state.d.ts +20 -1
  182. package/dist/core/workflow/run-state.d.ts.map +1 -1
  183. package/dist/core/workflow/run-state.js +141 -32
  184. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  185. package/dist/core/workflow/spawn-claude.js +19 -0
  186. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  187. package/dist/core/workflow/spawn-codex.js +13 -0
  188. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  189. package/dist/core/workflow/spawn-cursor.js +19 -0
  190. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  191. package/dist/core/workflow/spawn-opencode.js +14 -0
  192. package/dist/core/workflow/types.d.ts +64 -17
  193. package/dist/core/workflow/types.d.ts.map +1 -1
  194. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  195. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  196. package/dist/core/workflow/workspaces/index.js +0 -1
  197. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  198. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  199. package/dist/core/workflow/workspaces/leases.js +30 -16
  200. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  201. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  202. package/dist/core/workflow/workspaces/local-git.js +328 -8
  203. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  204. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  205. package/dist/core/workflow/workspaces/ownership.js +194 -0
  206. package/dist/core/workflow/workspaces/reclaim.d.ts +40 -0
  207. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  208. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  209. package/dist/core/workflow/workspaces/recovery.d.ts +2 -0
  210. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  211. package/dist/core/workflow/workspaces/recovery.js +42 -10
  212. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  213. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  214. package/dist/core/workflow/workspaces/state.js +21 -1
  215. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  216. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  217. package/dist/lib/browser/windows-chrome.js +262 -73
  218. package/dist/lib/docs-links.d.ts.map +1 -1
  219. package/dist/lib/docs-links.js +45 -2
  220. package/dist/lib/guarded-remove.d.ts +7 -0
  221. package/dist/lib/guarded-remove.d.ts.map +1 -1
  222. package/dist/lib/guarded-remove.js +106 -12
  223. package/dist/lib/identities/assume.d.ts.map +1 -1
  224. package/dist/lib/identities/assume.js +4 -0
  225. package/dist/lib/tunnel/state.d.ts +13 -6
  226. package/dist/lib/tunnel/state.d.ts.map +1 -1
  227. package/dist/lib/tunnel/state.js +58 -12
  228. package/dist/lib/zip/index.d.ts +41 -0
  229. package/dist/lib/zip/index.d.ts.map +1 -0
  230. package/dist/lib/zip/index.js +398 -0
  231. package/package.json +12 -1
  232. package/src/commander.ts +10 -0
  233. package/src/commands/agents.ts +54 -63
  234. package/src/commands/browse.ts +50 -14
  235. package/src/commands/diagnostics.ts +10 -0
  236. package/src/commands/doctor.ts +26 -7
  237. package/src/commands/files.ts +13 -5
  238. package/src/commands/governor.ts +23 -10
  239. package/src/commands/inbox.ts +52 -2
  240. package/src/commands/rm.ts +43 -4
  241. package/src/commands/servers.ts +27 -26
  242. package/src/commands/supervisor.ts +7 -2
  243. package/src/commands/toc.ts +14 -6
  244. package/src/commands/tunnel.ts +533 -166
  245. package/src/commands/turn-rollback.ts +111 -0
  246. package/src/commands/workflow.ts +89 -30
  247. package/src/core/agents/coord-client.ts +20 -0
  248. package/src/core/agents/live-authority-v3.ts +17 -0
  249. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  250. package/src/core/agents/mailbox.ts +26 -17
  251. package/src/core/agents/render/prompt-context.ts +10 -12
  252. package/src/core/agents/render/session-context.ts +9 -32
  253. package/src/core/agents/rules/stop-hook.ts +3 -3
  254. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  255. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  256. package/src/core/agents/state/pidmap.ts +13 -0
  257. package/src/core/agents/state/proc-start.ts +62 -2
  258. package/src/core/agents/state/process-handle.ts +165 -0
  259. package/src/core/agents/state/stale-sweep.ts +247 -82
  260. package/src/core/agents/state/unchanged-file.ts +456 -0
  261. package/src/core/artifacts/delivery-card.ts +7 -1
  262. package/src/core/context/index.ts +446 -136
  263. package/src/core/context/observation.ts +29 -0
  264. package/src/core/diagnostics/bundle.ts +23 -18
  265. package/src/core/diagnostics/path-identity.ts +15 -0
  266. package/src/core/diagnostics/sanitize.ts +4 -3
  267. package/src/core/events/v3/authority-outbox.ts +56 -0
  268. package/src/core/events/v3/generation-custody.ts +59 -0
  269. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  270. package/src/core/events/v3/producers/recorder.ts +94 -68
  271. package/src/core/governor/planning.ts +20 -8
  272. package/src/core/governor/service-read.ts +128 -20
  273. package/src/core/governor/service.ts +119 -42
  274. package/src/core/hooks/cli.ts +33 -5
  275. package/src/core/inbox/contract.ts +2 -1
  276. package/src/core/inbox/index.ts +2 -0
  277. package/src/core/inbox/peer-messages.ts +94 -0
  278. package/src/core/inbox/service.ts +88 -22
  279. package/src/core/inbox/targets.ts +148 -0
  280. package/src/core/inbox/watcher.ts +8 -9
  281. package/src/core/journal/index.ts +19 -2
  282. package/src/core/resources/contract.ts +4 -0
  283. package/src/core/resources/service-status.ts +35 -12
  284. package/src/core/resources/service.ts +57 -95
  285. package/src/core/resources/status.ts +57 -8
  286. package/src/core/semantic/service.ts +2 -8
  287. package/src/core/servers/index.ts +376 -141
  288. package/src/core/servers/tunnels.ts +57 -13
  289. package/src/core/supervisor/contract.ts +13 -0
  290. package/src/core/supervisor/explanations.ts +4 -1
  291. package/src/core/supervisor/findings.ts +68 -10
  292. package/src/core/supervisor/service.ts +87 -26
  293. package/src/core/supervisor/services.ts +26 -8
  294. package/src/core/supervisor/status.ts +89 -12
  295. package/src/core/supervisor/timeline.ts +2 -1
  296. package/src/core/turn-rollback/content.ts +499 -0
  297. package/src/core/turn-rollback/index.ts +580 -0
  298. package/src/core/work/state.ts +7 -7
  299. package/src/core/workflow/engine.ts +131 -49
  300. package/src/core/workflow/failure-evidence.ts +141 -0
  301. package/src/core/workflow/index.ts +12 -2
  302. package/src/core/workflow/live-session-v3.ts +146 -5
  303. package/src/core/workflow/proof.ts +134 -26
  304. package/src/core/workflow/recovery-audit.ts +181 -0
  305. package/src/core/workflow/run-state.ts +165 -34
  306. package/src/core/workflow/spawn-claude.ts +18 -0
  307. package/src/core/workflow/spawn-codex.ts +13 -0
  308. package/src/core/workflow/spawn-cursor.ts +18 -0
  309. package/src/core/workflow/spawn-opencode.ts +14 -0
  310. package/src/core/workflow/types.ts +51 -17
  311. package/src/core/workflow/workspaces/index.ts +0 -1
  312. package/src/core/workflow/workspaces/leases.ts +48 -17
  313. package/src/core/workflow/workspaces/local-git.ts +388 -7
  314. package/src/core/workflow/workspaces/ownership.ts +232 -0
  315. package/src/core/workflow/workspaces/reclaim.ts +62 -0
  316. package/src/core/workflow/workspaces/recovery.ts +45 -10
  317. package/src/core/workflow/workspaces/state.ts +20 -0
  318. package/src/lib/browser/windows-chrome.ts +346 -86
  319. package/src/lib/docs-links.ts +49 -2
  320. package/src/lib/guarded-remove.ts +132 -14
  321. package/src/lib/identities/assume.ts +4 -0
  322. package/src/lib/tunnel/state.ts +70 -15
  323. package/src/lib/zip/index.ts +483 -0
@@ -12,9 +12,8 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
  import { resolveBinName, resolveHooksSetupHint } from "../../config.ts";
14
14
  import { harneryVersion, loadAdapterWiring } from "../../hooks/adapter/wiring.ts";
15
- import { appendEntry } from "../../journal/index.ts";
15
+ import { formatPeerInbox, peerInbox } from "../../inbox/peer-messages.ts";
16
16
  import { readRemoteMachines } from "../../presence/index.ts";
17
- import { drainMailbox, formatMailboxDelivery, type MailboxMessageV1 } from "../mailbox.ts";
18
17
  import { readLiveCoordinationRows } from "../state/live-coordination-view.ts";
19
18
  import { forkRelationLabels, readForkState } from "../state/names.ts";
20
19
  import type { AgentActivity, TaskState } from "../state/session-state.ts";
@@ -42,6 +41,8 @@ export interface RenderOpts {
42
41
  /** Adapter label rendered in parens after the self-name (e.g. "Cursor", "Codex").
43
42
  * Claude Code omits it. */
44
43
  platformLabel?: string;
44
+ /** The emitter acknowledges exactly these IDs only after output succeeds. */
45
+ onInboxPrepared?: (messageIds: readonly string[]) => void;
45
46
  }
46
47
 
47
48
  /**
@@ -92,15 +93,12 @@ export function renderSessionContext(opts: RenderOpts): string {
92
93
  );
93
94
  }
94
95
 
95
- // 3. Peer messages queued for this name while no session held it. Draining
96
- // here is what makes `agents ping` safe to use on a dormant agent: the sender
97
- // never has to know whether the recipient is running.
98
- if (agentName) {
99
- const delivered = drainMailbox(coordRoot, agentName);
100
- if (delivered.length > 0) {
101
- recordDeliveredMessages(instanceId, delivered);
102
- messages.push(formatMailboxDelivery(coordRoot, delivered));
103
- }
96
+ // 3. Rendering never consumes a message. The native emitter acknowledges the
97
+ // prepared instance-bound batch only after it has flushed the output.
98
+ const delivered = peerInbox(coordRoot).prepareSurface(instanceId);
99
+ if (delivered.length > 0) {
100
+ opts.onInboxPrepared?.(delivered.map(({ message_id }) => message_id));
101
+ messages.push(formatPeerInbox(coordRoot, delivered));
104
102
  }
105
103
 
106
104
  // 4. Council invites
@@ -147,27 +145,6 @@ export function renderSessionContext(opts: RenderOpts): string {
147
145
  return messages.join("\n\n");
148
146
  }
149
147
 
150
- /**
151
- * Write delivered peer messages into the recipient's own journal, so they
152
- * survive context compaction the way any other coordination breadcrumb does.
153
- * A message the sender already journaled (the recipient was live when it was
154
- * sent) is skipped to avoid a duplicate entry.
155
- */
156
- export function recordDeliveredMessages(
157
- instanceId: string,
158
- messages: readonly MailboxMessageV1[],
159
- ): void {
160
- for (const m of messages) {
161
- if (m.journaled) continue;
162
- try {
163
- appendEntry(instanceId, "handoff", `from agent-${m.from_name}: ${m.body}`);
164
- } catch {
165
- // A journal write failure must never cost the delivery itself; the
166
- // message is already in the rendered context the model reads.
167
- }
168
- }
169
- }
170
-
171
148
  /** One-line-per-agent view of sessions on other machines (presence refs). */
172
149
  function formatRemoteMachines(coordRoot: string): string {
173
150
  try {
@@ -424,9 +424,9 @@ function sessionNameBlock(coordRoot: string, instanceId: string): VerdictResult
424
424
  } catch {
425
425
  // The ledger evidence still authorizes the block; the cache is UX only.
426
426
  }
427
- const instruction = name
428
- ? `Reproduce it by itself in a fenced code block: ${name}`
429
- : `Run \`${resolveBinName(coordRoot)} agents suggest-name\`, then reproduce its output by itself in a fenced code block.`;
427
+ // The first post-mint reply was already rejected. Repeating its title later
428
+ // cannot change that evidence; the JSON retry result opens a new window.
429
+ const instruction = `Run \`${resolveBinName(coordRoot)} agents suggest-name --json\` as your only tool call to create a fresh verification boundary, then display its suggested_session_name verbatim by itself in a fenced code block${name ? `: ${name}` : "."}`;
430
430
  return {
431
431
  allow: false,
432
432
  exit_code: 2,
@@ -24,6 +24,11 @@ import {
24
24
  writeFileSync,
25
25
  } from "node:fs";
26
26
  import { dirname, join } from "node:path";
27
+ import {
28
+ assertCurrentGenerationV3,
29
+ withGenerationCustodyV3,
30
+ } from "../../events/v3/generation-custody.ts";
31
+ import { liveInstanceIdV3 } from "../../events/v3/live-routing.ts";
27
32
  import { canonicalClaimPath } from "../claim-path.ts";
28
33
  import { type Heartbeat, heartbeatPath, readHeartbeat } from "./heartbeat-reader.ts";
29
34
  import type { TaskState } from "./session-state.ts";
@@ -63,12 +68,15 @@ function mutate(
63
68
  instanceId: string,
64
69
  fn: (hb: Heartbeat) => Heartbeat,
65
70
  ): Heartbeat | null {
66
- const hb = readHeartbeat(coordRoot, instanceId);
67
- if (!hb) return null;
68
- const next = fn(hb);
69
- next.last_heartbeat = nowIsoSeconds();
70
- atomicWrite(heartbeatPath(coordRoot, instanceId), JSON.stringify(next, null, 2));
71
- return next;
71
+ return withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(instanceId)], () => {
72
+ const hb = readHeartbeat(coordRoot, instanceId);
73
+ if (!hb) return null;
74
+ const next = fn(hb);
75
+ next.last_heartbeat = nowIsoSeconds();
76
+ assertCurrentGenerationV3(coordRoot, liveInstanceIdV3(instanceId), hb.v3_generation_id);
77
+ atomicWrite(heartbeatPath(coordRoot, instanceId), JSON.stringify(next, null, 2));
78
+ return next;
79
+ });
72
80
  }
73
81
 
74
82
  export function setTask(coordRoot: string, instanceId: string, task: string): Heartbeat | null {
@@ -100,12 +108,15 @@ export function setIdentityCache(
100
108
  name: string,
101
109
  agentId: string,
102
110
  ): Heartbeat | null {
103
- const heartbeat = readHeartbeat(coordRoot, instanceId);
104
- if (!heartbeat) return null;
105
- heartbeat.name = name;
106
- heartbeat.agent_id = agentId;
107
- atomicWrite(heartbeatPath(coordRoot, instanceId), JSON.stringify(heartbeat, null, 2));
108
- return heartbeat;
111
+ return withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(instanceId)], () => {
112
+ const heartbeat = readHeartbeat(coordRoot, instanceId);
113
+ if (!heartbeat) return null;
114
+ heartbeat.name = name;
115
+ heartbeat.agent_id = agentId;
116
+ assertCurrentGenerationV3(coordRoot, liveInstanceIdV3(instanceId), heartbeat.v3_generation_id);
117
+ atomicWrite(heartbeatPath(coordRoot, instanceId), JSON.stringify(heartbeat, null, 2));
118
+ return heartbeat;
119
+ });
109
120
  }
110
121
 
111
122
  /** Restore the pool-assigned display name on a generation that was
@@ -328,6 +339,12 @@ export function clearCoordinationCache(coordRoot: string, instanceId: string): b
328
339
  }
329
340
 
330
341
  export function healPidmap(coordRoot: string, instanceId: string, pid: number): void {
342
+ withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(instanceId)], () =>
343
+ healPidmapUnderCustody(coordRoot, instanceId, pid),
344
+ );
345
+ }
346
+
347
+ function healPidmapUnderCustody(coordRoot: string, instanceId: string, pid: number): void {
331
348
  const dir = join(coordRoot, ".harnery", "pid-map");
332
349
  mkdirSync(dir, { recursive: true });
333
350
  const hb = readHeartbeat(coordRoot, instanceId);
@@ -5,6 +5,10 @@ import {
5
5
  readCoordinationViewV3,
6
6
  requireAuthoritySafeCoordinationViewV3,
7
7
  } from "../../events/v3/coordination-view.ts";
8
+ import {
9
+ assertCurrentGenerationV3,
10
+ withGenerationCustodyV3,
11
+ } from "../../events/v3/generation-custody.ts";
8
12
  import { liveInstanceIdV3, resolveLiveEventLedgerRouteV3 } from "../../events/v3/live-routing.ts";
9
13
  import { type Heartbeat, readHeartbeat } from "./heartbeat-reader.ts";
10
14
  import {
@@ -64,6 +68,26 @@ function materializeLiveCoordinationHeartbeat(
64
68
  _adapter: Adapter,
65
69
  model: string | undefined,
66
70
  force: boolean,
71
+ ): Heartbeat | null {
72
+ return withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(nativeInstanceId)], () =>
73
+ materializeHeartbeatUnderCustody(
74
+ coordRoot,
75
+ nativeInstanceId,
76
+ nativeSessionId,
77
+ _adapter,
78
+ model,
79
+ force,
80
+ ),
81
+ );
82
+ }
83
+
84
+ function materializeHeartbeatUnderCustody(
85
+ coordRoot: string,
86
+ nativeInstanceId: string,
87
+ nativeSessionId: string,
88
+ _adapter: Adapter,
89
+ model: string | undefined,
90
+ force: boolean,
67
91
  ): Heartbeat | null {
68
92
  const route = resolveLiveEventLedgerRouteV3(coordRoot);
69
93
  if (route.state === "blocked") throw new Error(`event_v3_coordination_view:${route.reason}`);
@@ -133,6 +157,11 @@ function materializeLiveCoordinationHeartbeat(
133
157
  v3_projection_event_id: generation.last_event_id,
134
158
  v3_task_state: generation.task_state === "set" ? "set" : "cleared",
135
159
  };
160
+ assertCurrentGenerationV3(
161
+ coordRoot,
162
+ liveInstanceIdV3(nativeInstanceId),
163
+ generation.generation_id,
164
+ );
136
165
  writeHeartbeatCache(coordRoot, nativeInstanceId, materialized);
137
166
  return materialized;
138
167
  }
@@ -24,6 +24,8 @@ import {
24
24
  writeFileSync,
25
25
  } from "node:fs";
26
26
  import { dirname, join } from "node:path";
27
+ import { withGenerationCustodyV3 } from "../../events/v3/generation-custody.ts";
28
+ import { liveInstanceIdV3 } from "../../events/v3/live-routing.ts";
27
29
  import { checkPidToken, processStartToken } from "./proc-start.ts";
28
30
 
29
31
  /**
@@ -126,6 +128,17 @@ export function writePidmapRow(
126
128
  pid: number,
127
129
  instanceId: string,
128
130
  platform: string,
131
+ ): void {
132
+ withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(instanceId)], () =>
133
+ writePidmapRowUnderCustody(coordRoot, pid, instanceId, platform),
134
+ );
135
+ }
136
+
137
+ function writePidmapRowUnderCustody(
138
+ coordRoot: string,
139
+ pid: number,
140
+ instanceId: string,
141
+ platform: string,
129
142
  ): void {
130
143
  const dir = join(coordRoot, ".harnery", "pid-map");
131
144
  const path = join(dir, String(pid));
@@ -34,9 +34,10 @@
34
34
  import { spawnSync } from "node:child_process";
35
35
  import { existsSync, readFileSync } from "node:fs";
36
36
  import { coordEnv } from "../../../lib/env.ts";
37
+ import { signalProcessWithHandle, windowsProcessStartToken } from "./process-handle.ts";
37
38
 
38
39
  /** Which probe answers "when did this process start?" on this machine. */
39
- export type StartTokenProbe = "procfs" | "ps";
40
+ export type StartTokenProbe = "procfs" | "ps" | "windows";
40
41
 
41
42
  /**
42
43
  * The `ps` probe is text formatted by `strftime("%c", localtime(...))`, so its
@@ -60,6 +61,7 @@ let cachedBootId: string | null | undefined;
60
61
  export function startTokenProbe(): StartTokenProbe {
61
62
  const override = coordEnv("PID_PROBE");
62
63
  if (override === "procfs" || override === "ps") return override;
64
+ if (process.platform === "win32") return "windows";
63
65
  if (cachedProbe === undefined) {
64
66
  cachedProbe = existsSync("/proc/self/stat") ? "procfs" : "ps";
65
67
  }
@@ -157,7 +159,65 @@ function defaultPsRun(pid: number): { status: number | null; stdout: string } {
157
159
  */
158
160
  export function processStartToken(pid: number): string | null {
159
161
  if (!Number.isFinite(pid) || pid <= 0) return null;
160
- return startTokenProbe() === "procfs" ? procfsStartToken(pid) : psStartToken(pid);
162
+ const probe = startTokenProbe();
163
+ return probe === "windows"
164
+ ? windowsProcessStartToken(pid)
165
+ : probe === "procfs"
166
+ ? procfsStartToken(pid)
167
+ : psStartToken(pid);
168
+ }
169
+
170
+ export type ProcessGenerationVerdict = "same" | "different" | "unknown";
171
+
172
+ type ReadProcessStartToken = (pid: number) => string | null;
173
+
174
+ /** A missing or unreadable token never establishes process identity. */
175
+ export function verifyProcessGeneration(
176
+ pid: number,
177
+ recordedToken: string | null | undefined,
178
+ readToken: ReadProcessStartToken = processStartToken,
179
+ ): ProcessGenerationVerdict {
180
+ if (
181
+ !Number.isInteger(pid) ||
182
+ pid <= 0 ||
183
+ typeof recordedToken !== "string" ||
184
+ !recordedToken.trim()
185
+ )
186
+ return "unknown";
187
+ // A bootless Linux token cannot identify a process across machine boots.
188
+ if (/^l\d+$/.test(recordedToken)) return "unknown";
189
+ try {
190
+ const currentToken = readToken(pid);
191
+ if (typeof currentToken !== "string" || !currentToken.trim()) return "unknown";
192
+ if (/^l\d+$/.test(currentToken)) return "unknown";
193
+ return currentToken === recordedToken ? "same" : "different";
194
+ } catch {
195
+ return "unknown";
196
+ }
197
+ }
198
+
199
+ export interface ProcessGenerationSignalDependencies {
200
+ readToken?: ReadProcessStartToken;
201
+ signalProcess?: typeof signalProcessWithHandle;
202
+ }
203
+
204
+ /**
205
+ * Observations may refuse a signal, but only the retained native process handle
206
+ * can authorize it. No numeric-PID effect follows the preliminary token read.
207
+ */
208
+ export function signalIfSameGeneration(
209
+ pid: number,
210
+ recordedToken: string | null | undefined,
211
+ signal: NodeJS.Signals | number,
212
+ dependencies: ProcessGenerationSignalDependencies = {},
213
+ ): { sent: boolean; verdict: ProcessGenerationVerdict } {
214
+ const verdict = verifyProcessGeneration(pid, recordedToken, dependencies.readToken);
215
+ if (verdict !== "same") return { sent: false, verdict };
216
+ try {
217
+ return (dependencies.signalProcess ?? signalProcessWithHandle)(pid, recordedToken!, signal);
218
+ } catch {
219
+ return { sent: false, verdict: "unknown" };
220
+ }
161
221
  }
162
222
 
163
223
  /**
@@ -0,0 +1,165 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { constants } from "node:os";
3
+ import type { ProcessGenerationVerdict } from "./proc-start.ts";
4
+
5
+ export interface ProcessHandleSignalResult {
6
+ sent: boolean;
7
+ verdict: ProcessGenerationVerdict;
8
+ }
9
+
10
+ interface NativeHelperResult extends ProcessHandleSignalResult {
11
+ token?: string;
12
+ }
13
+
14
+ export type NativeProcessHelperRunner = (
15
+ command: string,
16
+ args: string[],
17
+ options: {
18
+ input: string;
19
+ encoding: "utf8";
20
+ timeout: number;
21
+ maxBuffer: number;
22
+ windowsHide: boolean;
23
+ },
24
+ ) => { status: number | null; stdout: string | Buffer | null; error?: Error };
25
+
26
+ // The helper owns the descriptor throughout observation and effect. The second
27
+ // input line authorizes the effect; EOF releases the handle without signaling.
28
+ export const LINUX_PROCESS_HANDLE_SCRIPT = `
29
+ import json, os, pathlib, signal, sys
30
+ result = {"sent": False, "verdict": "unknown"}
31
+ fd = None
32
+ try:
33
+ request = json.loads(sys.stdin.readline())
34
+ if not hasattr(os, "pidfd_open") or not hasattr(signal, "pidfd_send_signal"):
35
+ raise RuntimeError("pidfd support unavailable")
36
+ fd = os.pidfd_open(request["pid"])
37
+ stat = pathlib.Path("/proc/%d/stat" % request["pid"]).read_text()
38
+ ticks = stat[stat.rfind(") ") + 2:].split()[19]
39
+ boot = pathlib.Path("/proc/sys/kernel/random/boot_id").read_text().strip().replace("-", "")[:8]
40
+ token = "l%s.%s" % (boot, ticks)
41
+ result["token"] = token
42
+ recorded = request.get("token")
43
+ result["verdict"] = "same" if token == recorded else "different"
44
+ if request["mode"] == "signal" and result["verdict"] == "same":
45
+ print(json.dumps({"phase": "verified"}), flush=True)
46
+ if sys.stdin.readline().strip() == "signal":
47
+ signal.pidfd_send_signal(fd, request["signal"])
48
+ result["sent"] = True
49
+ except Exception:
50
+ pass
51
+ finally:
52
+ if fd is not None:
53
+ os.close(fd)
54
+ print(json.dumps(result), flush=True)
55
+ `;
56
+
57
+ export const WINDOWS_PROCESS_HANDLE_SCRIPT = `
58
+ $ErrorActionPreference = 'Stop'
59
+ $result = @{ sent = $false; verdict = 'unknown' }
60
+ $handle = [IntPtr]::Zero
61
+ try {
62
+ Add-Type -TypeDefinition @'
63
+ using System;
64
+ using System.Runtime.InteropServices;
65
+ public static class HarneryProcessHandle {
66
+ [DllImport("kernel32.dll", SetLastError=true)] public static extern IntPtr OpenProcess(uint access, bool inherit, uint processId);
67
+ [DllImport("kernel32.dll", SetLastError=true)] public static extern bool GetProcessTimes(IntPtr handle, out long creation, out long exit, out long kernel, out long user);
68
+ [DllImport("kernel32.dll", SetLastError=true)] public static extern bool TerminateProcess(IntPtr handle, uint exitCode);
69
+ [DllImport("kernel32.dll")] public static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
70
+ [DllImport("kernel32.dll")] public static extern bool CloseHandle(IntPtr handle);
71
+ }
72
+ '@
73
+ $request = [Console]::In.ReadLine() | ConvertFrom-Json
74
+ $access = [uint32]0x101000
75
+ if ($request.mode -eq 'signal') { $access = $access -bor 1 }
76
+ $handle = [HarneryProcessHandle]::OpenProcess($access, $false, [uint32]$request.pid)
77
+ if ($handle -eq [IntPtr]::Zero) { throw 'process handle unavailable' }
78
+ [long]$creation = 0; [long]$exitTime = 0; [long]$kernel = 0; [long]$userTime = 0
79
+ if (-not [HarneryProcessHandle]::GetProcessTimes($handle, [ref]$creation, [ref]$exitTime, [ref]$kernel, [ref]$userTime)) { throw 'process time unavailable' }
80
+ $token = 'w' + $creation.ToString([Globalization.CultureInfo]::InvariantCulture)
81
+ $result.token = $token
82
+ $result.verdict = if ($token -ceq $request.token) { 'same' } else { 'different' }
83
+ if ($request.mode -eq 'signal' -and $result.verdict -eq 'same') {
84
+ [Console]::Out.WriteLine('{"phase":"verified"}')
85
+ [Console]::Out.Flush()
86
+ if ([Console]::In.ReadLine() -eq 'signal' -and [HarneryProcessHandle]::WaitForSingleObject($handle, 0) -eq 258) {
87
+ $result.sent = [HarneryProcessHandle]::TerminateProcess($handle, 1)
88
+ }
89
+ }
90
+ } catch {} finally {
91
+ if ($handle -ne [IntPtr]::Zero) { [void][HarneryProcessHandle]::CloseHandle($handle) }
92
+ }
93
+ $result | ConvertTo-Json -Compress
94
+ `;
95
+
96
+ function runHelper(
97
+ request: { mode: "probe" | "signal"; pid: number; token?: string; signal?: number },
98
+ platform: NodeJS.Platform,
99
+ run: NativeProcessHelperRunner,
100
+ ): NativeHelperResult {
101
+ const unknown = { sent: false, verdict: "unknown" } as const;
102
+ if (!Number.isInteger(request.pid) || request.pid <= 0) return unknown;
103
+ if (platform !== "linux" && platform !== "win32") return unknown;
104
+ try {
105
+ const command = platform === "linux" ? "python3" : "powershell.exe";
106
+ const args =
107
+ platform === "linux"
108
+ ? ["-I", "-c", LINUX_PROCESS_HANDLE_SCRIPT]
109
+ : ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", WINDOWS_PROCESS_HANDLE_SCRIPT];
110
+ const output = run(command, args, {
111
+ input: `${JSON.stringify(request)}\nsignal\n`,
112
+ encoding: "utf8",
113
+ timeout: 10_000,
114
+ maxBuffer: 64 * 1024,
115
+ windowsHide: true,
116
+ });
117
+ if (output.error || output.status !== 0) return unknown;
118
+ const lines = String(output.stdout ?? "")
119
+ .trim()
120
+ .split(/\r?\n/);
121
+ const parsed = JSON.parse(lines.at(-1) ?? "") as NativeHelperResult;
122
+ if (
123
+ typeof parsed.sent !== "boolean" ||
124
+ !["same", "different", "unknown"].includes(parsed.verdict) ||
125
+ (parsed.sent && parsed.verdict !== "same")
126
+ )
127
+ return unknown;
128
+ return parsed;
129
+ } catch {
130
+ return unknown;
131
+ }
132
+ }
133
+
134
+ /** Windows creation FILETIME, read from an OpenProcess handle. */
135
+ export function windowsProcessStartToken(
136
+ pid: number,
137
+ run: NativeProcessHelperRunner = spawnSync,
138
+ ): string | null {
139
+ const result = runHelper({ mode: "probe", pid }, "win32", run);
140
+ return typeof result.token === "string" && /^w\d+$/.test(result.token) ? result.token : null;
141
+ }
142
+
143
+ /** No numeric-PID signaling fallback is permitted when an exact handle fails. */
144
+ export function signalProcessWithHandle(
145
+ pid: number,
146
+ recordedToken: string,
147
+ signal: NodeJS.Signals | number,
148
+ options: { platform?: NodeJS.Platform; run?: NativeProcessHelperRunner } = {},
149
+ ): ProcessHandleSignalResult {
150
+ const platform = options.platform ?? process.platform;
151
+ const number = typeof signal === "number" ? signal : constants.signals[signal];
152
+ if (
153
+ !Number.isInteger(number) ||
154
+ number <= 0 ||
155
+ (platform === "linux" && !/^l[0-9a-f]{8}\.\d+$/.test(recordedToken)) ||
156
+ (platform === "win32" && (!/^w\d+$/.test(recordedToken) || ![9, 15].includes(number)))
157
+ )
158
+ return { sent: false, verdict: "unknown" };
159
+ const result = runHelper(
160
+ { mode: "signal", pid, token: recordedToken, signal: number },
161
+ platform,
162
+ options.run ?? spawnSync,
163
+ );
164
+ return { sent: result.sent, verdict: result.verdict };
165
+ }