harnery 0.42.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 (343) hide show
  1. package/README.md +3 -3
  2. package/dist/commander.d.ts +16 -0
  3. package/dist/commander.d.ts.map +1 -1
  4. package/dist/commander.js +18 -0
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +95 -52
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +38 -11
  9. package/dist/commands/diagnostics.d.ts.map +1 -1
  10. package/dist/commands/diagnostics.js +10 -0
  11. package/dist/commands/doctor.d.ts +12 -0
  12. package/dist/commands/doctor.d.ts.map +1 -1
  13. package/dist/commands/doctor.js +22 -7
  14. package/dist/commands/governor.js +22 -10
  15. package/dist/commands/inbox.d.ts +1 -1
  16. package/dist/commands/inbox.d.ts.map +1 -1
  17. package/dist/commands/inbox.js +55 -2
  18. package/dist/commands/rm.d.ts.map +1 -1
  19. package/dist/commands/rm.js +28 -4
  20. package/dist/commands/servers.d.ts.map +1 -1
  21. package/dist/commands/servers.js +16 -1
  22. package/dist/commands/supervisor.d.ts.map +1 -1
  23. package/dist/commands/supervisor.js +7 -2
  24. package/dist/commands/toc.d.ts +14 -0
  25. package/dist/commands/toc.d.ts.map +1 -1
  26. package/dist/commands/toc.js +7 -4
  27. package/dist/commands/tunnel.d.ts +65 -0
  28. package/dist/commands/tunnel.d.ts.map +1 -1
  29. package/dist/commands/tunnel.js +290 -101
  30. package/dist/commands/web-fetch.d.ts +3 -2
  31. package/dist/commands/web-fetch.d.ts.map +1 -1
  32. package/dist/commands/web-fetch.js +25 -19
  33. package/dist/commands/web.d.ts +7 -1
  34. package/dist/commands/web.d.ts.map +1 -1
  35. package/dist/commands/web.js +9 -3
  36. package/dist/commands/workflow.js +24 -30
  37. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  38. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  39. package/dist/core/agents/live-authority-v3.js +11 -0
  40. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  41. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  42. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  43. package/dist/core/agents/mailbox.d.ts +9 -8
  44. package/dist/core/agents/mailbox.d.ts.map +1 -1
  45. package/dist/core/agents/mailbox.js +24 -17
  46. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  47. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  48. package/dist/core/agents/render/prompt-context.js +19 -17
  49. package/dist/core/agents/render/session-context.d.ts +2 -8
  50. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  51. package/dist/core/agents/render/session-context.js +64 -43
  52. package/dist/core/agents/rules/stop-hook.js +3 -3
  53. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  54. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  55. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  56. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  57. package/dist/core/agents/state/names.d.ts +43 -3
  58. package/dist/core/agents/state/names.d.ts.map +1 -1
  59. package/dist/core/agents/state/names.js +87 -3
  60. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  61. package/dist/core/agents/state/pidmap.js +5 -0
  62. package/dist/core/agents/state/proc-start.d.ts +19 -0
  63. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  64. package/dist/core/agents/state/proc-start.js +41 -0
  65. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  66. package/dist/core/agents/state/stale-sweep.js +180 -70
  67. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  68. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  69. package/dist/core/agents/state/unchanged-file.js +390 -0
  70. package/dist/core/context/index.d.ts +22 -10
  71. package/dist/core/context/index.d.ts.map +1 -1
  72. package/dist/core/context/index.js +381 -119
  73. package/dist/core/context/observation.d.ts +26 -0
  74. package/dist/core/context/observation.d.ts.map +1 -0
  75. package/dist/core/context/observation.js +11 -0
  76. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  77. package/dist/core/diagnostics/bundle.js +25 -14
  78. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  79. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  80. package/dist/core/diagnostics/path-identity.js +12 -0
  81. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  82. package/dist/core/diagnostics/sanitize.js +4 -3
  83. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  84. package/dist/core/events/v3/authority-outbox.js +29 -0
  85. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  86. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  87. package/dist/core/events/v3/generation-custody.js +50 -0
  88. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  89. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  90. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  91. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  92. package/dist/core/events/v3/producers/recorder.js +77 -64
  93. package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
  94. package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
  95. package/dist/core/governor/planning.d.ts.map +1 -1
  96. package/dist/core/governor/planning.js +14 -1
  97. package/dist/core/governor/service-read.d.ts +10 -2
  98. package/dist/core/governor/service-read.d.ts.map +1 -1
  99. package/dist/core/governor/service-read.js +105 -22
  100. package/dist/core/governor/service.d.ts +20 -2
  101. package/dist/core/governor/service.d.ts.map +1 -1
  102. package/dist/core/governor/service.js +72 -47
  103. package/dist/core/hooks/cli.js +120 -23
  104. package/dist/core/hooks/fork-lineage.d.ts +40 -0
  105. package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
  106. package/dist/core/hooks/fork-lineage.js +62 -0
  107. package/dist/core/hooks/resolve/transcript.d.ts +16 -0
  108. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  109. package/dist/core/hooks/resolve/transcript.js +84 -0
  110. package/dist/core/hooks/shell-rm.d.ts +2 -0
  111. package/dist/core/hooks/shell-rm.d.ts.map +1 -0
  112. package/dist/core/hooks/shell-rm.js +105 -0
  113. package/dist/core/hooks/shell-tokens.d.ts +12 -0
  114. package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
  115. package/dist/core/hooks/shell-tokens.js +97 -0
  116. package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
  117. package/dist/core/hooks/shell-waiter.js +3 -93
  118. package/dist/core/inbox/contract.d.ts +2 -1
  119. package/dist/core/inbox/contract.d.ts.map +1 -1
  120. package/dist/core/inbox/index.d.ts +2 -0
  121. package/dist/core/inbox/index.d.ts.map +1 -1
  122. package/dist/core/inbox/index.js +2 -0
  123. package/dist/core/inbox/peer-messages.d.ts +37 -0
  124. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  125. package/dist/core/inbox/peer-messages.js +72 -0
  126. package/dist/core/inbox/service.d.ts +8 -1
  127. package/dist/core/inbox/service.d.ts.map +1 -1
  128. package/dist/core/inbox/service.js +67 -21
  129. package/dist/core/inbox/targets.d.ts +22 -0
  130. package/dist/core/inbox/targets.d.ts.map +1 -0
  131. package/dist/core/inbox/targets.js +108 -0
  132. package/dist/core/inbox/watcher.js +7 -3
  133. package/dist/core/journal/index.d.ts.map +1 -1
  134. package/dist/core/journal/index.js +17 -2
  135. package/dist/core/resources/contract.d.ts +3 -0
  136. package/dist/core/resources/contract.d.ts.map +1 -1
  137. package/dist/core/resources/service-status.d.ts +14 -1
  138. package/dist/core/resources/service-status.d.ts.map +1 -1
  139. package/dist/core/resources/service-status.js +23 -13
  140. package/dist/core/resources/service.d.ts +9 -1
  141. package/dist/core/resources/service.d.ts.map +1 -1
  142. package/dist/core/resources/service.js +47 -27
  143. package/dist/core/resources/status.d.ts +2 -5
  144. package/dist/core/resources/status.d.ts.map +1 -1
  145. package/dist/core/resources/status.js +36 -7
  146. package/dist/core/semantic/contract.d.ts +5 -5
  147. package/dist/core/servers/index.d.ts +24 -13
  148. package/dist/core/servers/index.d.ts.map +1 -1
  149. package/dist/core/servers/index.js +132 -92
  150. package/dist/core/servers/tunnels.d.ts +3 -5
  151. package/dist/core/servers/tunnels.d.ts.map +1 -1
  152. package/dist/core/servers/tunnels.js +3 -9
  153. package/dist/core/storage/process-log.d.ts +12 -0
  154. package/dist/core/storage/process-log.d.ts.map +1 -1
  155. package/dist/core/storage/process-log.js +10 -2
  156. package/dist/core/supervisor/contract.d.ts +12 -0
  157. package/dist/core/supervisor/contract.d.ts.map +1 -1
  158. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  159. package/dist/core/supervisor/explanations.js +2 -1
  160. package/dist/core/supervisor/findings.d.ts.map +1 -1
  161. package/dist/core/supervisor/findings.js +54 -9
  162. package/dist/core/supervisor/service.d.ts +9 -1
  163. package/dist/core/supervisor/service.d.ts.map +1 -1
  164. package/dist/core/supervisor/service.js +59 -29
  165. package/dist/core/supervisor/services.d.ts.map +1 -1
  166. package/dist/core/supervisor/services.js +22 -1
  167. package/dist/core/supervisor/status.d.ts +17 -2
  168. package/dist/core/supervisor/status.d.ts.map +1 -1
  169. package/dist/core/supervisor/status.js +70 -12
  170. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  171. package/dist/core/supervisor/timeline.js +2 -1
  172. package/dist/core/work/state.js +7 -7
  173. package/dist/core/workflow/engine.d.ts.map +1 -1
  174. package/dist/core/workflow/engine.js +105 -32
  175. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  176. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  177. package/dist/core/workflow/failure-evidence.js +114 -0
  178. package/dist/core/workflow/index.d.ts +1 -1
  179. package/dist/core/workflow/index.d.ts.map +1 -1
  180. package/dist/core/workflow/index.js +1 -1
  181. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  182. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  183. package/dist/core/workflow/live-session-v3.js +126 -5
  184. package/dist/core/workflow/proof.d.ts +5 -5
  185. package/dist/core/workflow/proof.d.ts.map +1 -1
  186. package/dist/core/workflow/proof.js +115 -20
  187. package/dist/core/workflow/run-state.d.ts +3 -1
  188. package/dist/core/workflow/run-state.d.ts.map +1 -1
  189. package/dist/core/workflow/run-state.js +31 -27
  190. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  191. package/dist/core/workflow/spawn-claude.js +19 -0
  192. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  193. package/dist/core/workflow/spawn-codex.js +13 -0
  194. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  195. package/dist/core/workflow/spawn-cursor.js +19 -0
  196. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  197. package/dist/core/workflow/spawn-opencode.js +14 -0
  198. package/dist/core/workflow/types.d.ts +64 -17
  199. package/dist/core/workflow/types.d.ts.map +1 -1
  200. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  201. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  202. package/dist/core/workflow/workspaces/index.js +0 -1
  203. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  204. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  205. package/dist/core/workflow/workspaces/leases.js +30 -16
  206. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  207. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  208. package/dist/core/workflow/workspaces/local-git.js +270 -6
  209. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  210. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  211. package/dist/core/workflow/workspaces/ownership.js +194 -0
  212. package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
  213. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  214. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  215. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  216. package/dist/core/workflow/workspaces/recovery.js +21 -13
  217. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  218. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  219. package/dist/core/workflow/workspaces/state.js +21 -1
  220. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  221. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  222. package/dist/lib/browser/windows-chrome.js +262 -73
  223. package/dist/lib/docs-links.d.ts.map +1 -1
  224. package/dist/lib/docs-links.js +45 -2
  225. package/dist/lib/guarded-remove.d.ts +7 -0
  226. package/dist/lib/guarded-remove.d.ts.map +1 -1
  227. package/dist/lib/guarded-remove.js +106 -12
  228. package/dist/lib/identities/assume.d.ts.map +1 -1
  229. package/dist/lib/identities/assume.js +4 -0
  230. package/dist/lib/tunnel/allowlist.d.ts +44 -0
  231. package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
  232. package/dist/lib/tunnel/allowlist.js +204 -0
  233. package/dist/lib/tunnel/current-address.d.ts +41 -0
  234. package/dist/lib/tunnel/current-address.d.ts.map +1 -0
  235. package/dist/lib/tunnel/current-address.js +123 -0
  236. package/dist/lib/tunnel/gate.js +13 -6
  237. package/dist/lib/tunnel/processes.d.ts +39 -0
  238. package/dist/lib/tunnel/processes.d.ts.map +1 -0
  239. package/dist/lib/tunnel/processes.js +121 -0
  240. package/dist/lib/tunnel/state.d.ts +28 -3
  241. package/dist/lib/tunnel/state.d.ts.map +1 -1
  242. package/dist/lib/tunnel/state.js +98 -27
  243. package/dist/lib/zip/index.d.ts +41 -0
  244. package/dist/lib/zip/index.d.ts.map +1 -0
  245. package/dist/lib/zip/index.js +398 -0
  246. package/package.json +8 -1
  247. package/src/commander.ts +32 -0
  248. package/src/commands/agents.ts +151 -74
  249. package/src/commands/browse.ts +50 -14
  250. package/src/commands/diagnostics.ts +10 -0
  251. package/src/commands/doctor.ts +26 -7
  252. package/src/commands/governor.ts +23 -10
  253. package/src/commands/inbox.ts +52 -2
  254. package/src/commands/rm.ts +43 -4
  255. package/src/commands/servers.ts +16 -4
  256. package/src/commands/supervisor.ts +7 -2
  257. package/src/commands/toc.ts +14 -6
  258. package/src/commands/tunnel.ts +394 -116
  259. package/src/commands/web-fetch.ts +30 -19
  260. package/src/commands/web.ts +9 -3
  261. package/src/commands/workflow.ts +30 -30
  262. package/src/core/agents/live-authority-v3.ts +17 -0
  263. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  264. package/src/core/agents/mailbox.ts +26 -17
  265. package/src/core/agents/render/prompt-context.ts +35 -20
  266. package/src/core/agents/render/session-context.ts +92 -48
  267. package/src/core/agents/rules/stop-hook.ts +3 -3
  268. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  269. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  270. package/src/core/agents/state/names.ts +117 -6
  271. package/src/core/agents/state/pidmap.ts +13 -0
  272. package/src/core/agents/state/proc-start.ts +57 -0
  273. package/src/core/agents/state/stale-sweep.ts +247 -82
  274. package/src/core/agents/state/unchanged-file.ts +456 -0
  275. package/src/core/context/index.ts +446 -136
  276. package/src/core/context/observation.ts +29 -0
  277. package/src/core/diagnostics/bundle.ts +23 -18
  278. package/src/core/diagnostics/path-identity.ts +15 -0
  279. package/src/core/diagnostics/sanitize.ts +4 -3
  280. package/src/core/events/v3/authority-outbox.ts +56 -0
  281. package/src/core/events/v3/generation-custody.ts +59 -0
  282. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  283. package/src/core/events/v3/producers/recorder.ts +94 -68
  284. package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
  285. package/src/core/governor/planning.ts +20 -8
  286. package/src/core/governor/service-read.ts +128 -20
  287. package/src/core/governor/service.ts +119 -42
  288. package/src/core/hooks/cli.ts +139 -22
  289. package/src/core/hooks/fork-lineage.ts +82 -0
  290. package/src/core/hooks/resolve/transcript.ts +88 -0
  291. package/src/core/hooks/shell-rm.ts +103 -0
  292. package/src/core/hooks/shell-tokens.ts +96 -0
  293. package/src/core/hooks/shell-waiter.ts +4 -91
  294. package/src/core/inbox/contract.ts +2 -1
  295. package/src/core/inbox/index.ts +2 -0
  296. package/src/core/inbox/peer-messages.ts +94 -0
  297. package/src/core/inbox/service.ts +88 -22
  298. package/src/core/inbox/targets.ts +148 -0
  299. package/src/core/inbox/watcher.ts +8 -9
  300. package/src/core/journal/index.ts +19 -2
  301. package/src/core/resources/contract.ts +4 -0
  302. package/src/core/resources/service-status.ts +35 -12
  303. package/src/core/resources/service.ts +71 -24
  304. package/src/core/resources/status.ts +57 -8
  305. package/src/core/servers/index.ts +177 -91
  306. package/src/core/servers/tunnels.ts +3 -11
  307. package/src/core/storage/process-log.ts +17 -2
  308. package/src/core/supervisor/contract.ts +13 -0
  309. package/src/core/supervisor/explanations.ts +4 -1
  310. package/src/core/supervisor/findings.ts +68 -10
  311. package/src/core/supervisor/service.ts +88 -26
  312. package/src/core/supervisor/services.ts +26 -8
  313. package/src/core/supervisor/status.ts +89 -12
  314. package/src/core/supervisor/timeline.ts +2 -1
  315. package/src/core/work/state.ts +7 -7
  316. package/src/core/workflow/engine.ts +131 -49
  317. package/src/core/workflow/failure-evidence.ts +141 -0
  318. package/src/core/workflow/index.ts +0 -1
  319. package/src/core/workflow/live-session-v3.ts +146 -5
  320. package/src/core/workflow/proof.ts +134 -26
  321. package/src/core/workflow/run-state.ts +31 -29
  322. package/src/core/workflow/spawn-claude.ts +18 -0
  323. package/src/core/workflow/spawn-codex.ts +13 -0
  324. package/src/core/workflow/spawn-cursor.ts +18 -0
  325. package/src/core/workflow/spawn-opencode.ts +14 -0
  326. package/src/core/workflow/types.ts +51 -17
  327. package/src/core/workflow/workspaces/index.ts +0 -1
  328. package/src/core/workflow/workspaces/leases.ts +48 -17
  329. package/src/core/workflow/workspaces/local-git.ts +315 -5
  330. package/src/core/workflow/workspaces/ownership.ts +232 -0
  331. package/src/core/workflow/workspaces/reclaim.ts +58 -0
  332. package/src/core/workflow/workspaces/recovery.ts +25 -13
  333. package/src/core/workflow/workspaces/state.ts +20 -0
  334. package/src/lib/browser/windows-chrome.ts +346 -86
  335. package/src/lib/docs-links.ts +49 -2
  336. package/src/lib/guarded-remove.ts +132 -14
  337. package/src/lib/identities/assume.ts +4 -0
  338. package/src/lib/tunnel/allowlist.ts +208 -0
  339. package/src/lib/tunnel/current-address.ts +158 -0
  340. package/src/lib/tunnel/gate.ts +16 -8
  341. package/src/lib/tunnel/processes.ts +149 -0
  342. package/src/lib/tunnel/state.ts +123 -27
  343. package/src/lib/zip/index.ts +483 -0
@@ -151,6 +151,47 @@ export function processStartToken(pid) {
151
151
  return null;
152
152
  return startTokenProbe() === "procfs" ? procfsStartToken(pid) : psStartToken(pid);
153
153
  }
154
+ /** A missing or unreadable token never establishes process identity. */
155
+ export function verifyProcessGeneration(pid, recordedToken, readToken = processStartToken) {
156
+ if (!Number.isInteger(pid) ||
157
+ pid <= 0 ||
158
+ typeof recordedToken !== "string" ||
159
+ !recordedToken.trim())
160
+ return "unknown";
161
+ // A bootless Linux token cannot identify a process across machine boots.
162
+ if (/^l\d+$/.test(recordedToken))
163
+ return "unknown";
164
+ try {
165
+ const currentToken = readToken(pid);
166
+ if (typeof currentToken !== "string" || !currentToken.trim())
167
+ return "unknown";
168
+ if (/^l\d+$/.test(currentToken))
169
+ return "unknown";
170
+ return currentToken === recordedToken ? "same" : "different";
171
+ }
172
+ catch {
173
+ return "unknown";
174
+ }
175
+ }
176
+ /**
177
+ * Recheck the process generation immediately before sending the signal.
178
+ *
179
+ * The process can still exit and its PID can be reused between the token read
180
+ * and the signal. Eliminating that race requires a generation-bound OS handle.
181
+ */
182
+ export function signalIfSameGeneration(pid, recordedToken, signal, dependencies = {}) {
183
+ const kill = dependencies.kill ?? process.kill;
184
+ const verdict = verifyProcessGeneration(pid, recordedToken, dependencies.readToken);
185
+ if (verdict !== "same")
186
+ return { sent: false, verdict };
187
+ try {
188
+ kill(pid, signal);
189
+ return { sent: true, verdict };
190
+ }
191
+ catch {
192
+ return { sent: false, verdict };
193
+ }
194
+ }
154
195
  /**
155
196
  * Do two tokens describe the same run?
156
197
  *
@@ -1 +1 @@
1
- {"version":3,"file":"stale-sweep.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/stale-sweep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAkEH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG;IAC7C,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qBAAqB,EAAE,MAAM,CAAC;CAC/B,CAyJA"}
1
+ {"version":3,"file":"stale-sweep.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/stale-sweep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAyIH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG;IAC7C,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qBAAqB,EAAE,MAAM,CAAC;CAC/B,CAuPA"}
@@ -9,12 +9,15 @@
9
9
  * HARNERY_AGENT_COORD_FRESHNESS env var or `.harnery/config.jsonc`
10
10
  * `coord.freshness_seconds` (see `coordFreshnessSeconds`).
11
11
  */
12
- import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
13
- import { join } from "node:path";
12
+ import { existsSync, lstatSync, readdirSync } from "node:fs";
13
+ import { basename, dirname, join, resolve } from "node:path";
14
14
  import { coordFreshnessSeconds } from "../../config.js";
15
15
  import { eventAdapterIdV3FromPlatform } from "../../events/v3/adapter-id.js";
16
+ import { withGenerationCustodyV3 } from "../../events/v3/generation-custody.js";
17
+ import { liveInstanceIdV3 } from "../../events/v3/live-routing.js";
16
18
  import { writeProducerDiagnosticV3 } from "../../events/v3/producers/intake.js";
17
19
  import { recordLiveSweepObservationV3 } from "../live-lifecycle-v3.js";
20
+ import { captureFileRevision, deleteFileIfUnchanged, recoverInterruptedFileDeletions, } from "./unchanged-file.js";
18
21
  /** platform → adapter, for the swept-event envelope (mirrors heartbeat-writer's adapterOf). */
19
22
  /**
20
23
  * Record the sweep before deleting disposable cache state.
@@ -25,13 +28,14 @@ import { recordLiveSweepObservationV3 } from "../live-lifecycle-v3.js";
25
28
  * diagnostics spool instead. If neither durable record can be written, fail
26
29
  * closed and keep the file.
27
30
  */
28
- function emitSwept(coordRoot, instanceId, adapter, sessionId, reason, ageSecs) {
31
+ function emitSwept(coordRoot, instanceId, adapter, sessionId, expectedGenerationId, reason, ageSecs) {
29
32
  try {
30
33
  recordLiveSweepObservationV3({
31
34
  coordRoot,
32
35
  owner: instanceId,
33
36
  nativeSessionId: sessionId,
34
37
  adapter,
38
+ expectedGenerationId,
35
39
  observation: reason === "stale"
36
40
  ? "stale_heartbeat"
37
41
  : reason === "unparseable"
@@ -53,14 +57,58 @@ function emitSwept(coordRoot, instanceId, adapter, sessionId, reason, ageSecs) {
53
57
  }));
54
58
  }
55
59
  }
56
- /** File mtime in epoch-seconds, or +Infinity if it can't be read (treat as fresh → don't reap). */
57
- function mtimeSecs(path) {
60
+ /** Any retained or unreadable owner row prevents dependent cache deletion. */
61
+ function retainsHeartbeat(coordRoot, activeDir, owner, uncertainOwners) {
62
+ if (uncertainOwners.has("*") || uncertainOwners.has(owner))
63
+ return true;
64
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(owner))
65
+ return true;
66
+ const heartbeat = join(activeDir, `${owner}.json`);
58
67
  try {
59
- return Math.floor(statSync(path).mtimeMs / 1000);
68
+ lstatSync(heartbeat);
69
+ return true;
70
+ }
71
+ catch (error) {
72
+ if (error.code !== "ENOENT")
73
+ return true;
74
+ }
75
+ try {
76
+ if (!lstatSync(activeDir).isDirectory())
77
+ return true;
78
+ }
79
+ catch (error) {
80
+ return error.code !== "ENOENT";
81
+ }
82
+ // Callers hold this owner's custody. A sweeper that died after the initial
83
+ // directory scan may have left its heartbeat hidden under a private pin.
84
+ for (const recovery of recoverInterruptedFileDeletions(activeDir, (source) => source === heartbeat)) {
85
+ if (recovery.sourcePath && recovery.sourcePath !== heartbeat)
86
+ continue;
87
+ if (recovery.state !== "restored") {
88
+ uncertainOwners.add(recovery.sourcePath ? owner : "*");
89
+ recordCleanupFailure(coordRoot, recovery.sourcePath ?? activeDir, recovery);
90
+ }
91
+ return true;
92
+ }
93
+ return false;
94
+ }
95
+ function recordCleanupFailure(coordRoot, sourcePath, result) {
96
+ if (result.state !== "error" &&
97
+ result.state !== "recovery-required" &&
98
+ result.state !== "retained") {
99
+ return;
60
100
  }
61
- catch {
62
- return Number.POSITIVE_INFINITY;
101
+ writeProducerDiagnosticV3(coordRoot, "heartbeat_sweep_unrecorded", {
102
+ signal: "cache.cleanup",
103
+ state: "cache_cleanup_pending",
104
+ reason: `${result.state}:${result.error ?? "file_cleanup_pending"}; source=${sourcePath}; recovery=${result.recoveryPath ?? "unavailable"}`,
105
+ });
106
+ }
107
+ function recordHeartbeatCleanupResult(coordRoot, sourcePath, owner, result, uncertainOwners) {
108
+ if (result.state === "recovery-required" || result.state === "error") {
109
+ uncertainOwners.add(owner);
63
110
  }
111
+ recordCleanupFailure(coordRoot, sourcePath, result);
64
112
  }
65
113
  export function staleSweep(coordRoot) {
66
114
  const heartbeatsRemoved = [];
@@ -86,81 +134,129 @@ export function staleSweep(coordRoot) {
86
134
  // lifecycle.sweep_observed; a terminal or unavailable generation gets a
87
135
  // heartbeat_sweep_unrecorded producer diagnostic without being resurrected.
88
136
  const liveInstanceIds = new Set();
89
- const d = join(coordRoot, ".harnery", "active");
137
+ const uncertainOwners = new Set();
138
+ const d = resolve(coordRoot, ".harnery", "active");
139
+ for (const recovery of existsSync(d)
140
+ ? recoverInterruptedFileDeletions(d, (source) => dirname(source) === d && /^[A-Za-z0-9_-]{1,128}\.json$/.test(basename(source)))
141
+ : []) {
142
+ if (recovery.state !== "restored") {
143
+ uncertainOwners.add(recovery.sourcePath ? basename(recovery.sourcePath, ".json") : "*");
144
+ recordCleanupFailure(coordRoot, recovery.sourcePath ?? d, recovery);
145
+ }
146
+ }
90
147
  if (existsSync(d)) {
91
148
  for (const f of readdirSync(d)) {
92
149
  if (!f.endsWith(".json"))
93
150
  continue;
94
151
  const path = join(d, f);
95
152
  const idFromFile = f.replace(/\.json$/, "");
96
- let parsed = null;
153
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(idFromFile))
154
+ continue;
97
155
  try {
98
- parsed = JSON.parse(readFileSync(path, "utf8"));
99
- }
100
- catch {
101
- parsed = null;
102
- }
103
- if (parsed === null) {
104
- // Unparseable: only reap if the file itself is mtime-old.
105
- if (mtimeSecs(path) < cutoff) {
156
+ withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(idFromFile)], () => {
157
+ const revision = captureFileRevision(path);
158
+ if (!revision)
159
+ return;
160
+ const mtimeSecs = Number(revision.mtimeNs / 1000000000n);
161
+ let parsed = null;
106
162
  try {
107
- const ageSecs = nowSec - mtimeSecs(path);
108
- if (!emitSwept(coordRoot, idFromFile, "claude-code", idFromFile, "unparseable", ageSecs)) {
109
- continue;
110
- }
111
- unlinkSync(path);
112
- heartbeatsRemoved.push(f);
163
+ const value = JSON.parse(revision.bytes.toString("utf8"));
164
+ if (value && typeof value === "object" && !Array.isArray(value))
165
+ parsed = value;
113
166
  }
114
167
  catch {
115
- /* swallow */
168
+ parsed = null;
116
169
  }
117
- }
118
- continue;
119
- }
120
- const instanceId = parsed.instance_id ?? idFromFile;
121
- const adapter = eventAdapterIdV3FromPlatform(parsed.platform, { context: "stale-sweep" });
122
- const sessionId = parsed.session_id ?? instanceId;
123
- const ts = parsed.last_heartbeat
124
- ? Math.floor(Date.parse(parsed.last_heartbeat) / 1000)
125
- : Number.NaN;
126
- if (!parsed.last_heartbeat || !Number.isFinite(ts)) {
127
- // No / NaN last_heartbeat: can't trust content; gate on mtime.
128
- if (mtimeSecs(path) < cutoff) {
129
- if (!emitSwept(coordRoot, instanceId, adapter, sessionId, "missing_ts", nowSec - mtimeSecs(path))) {
130
- continue;
170
+ if (parsed === null) {
171
+ // Unparseable: only reap if the file itself is mtime-old.
172
+ if (mtimeSecs < cutoff) {
173
+ const result = deleteFileIfUnchanged(path, revision, () => emitSwept(coordRoot, idFromFile, "claude-code", idFromFile, null, "unparseable", nowSec - mtimeSecs));
174
+ recordHeartbeatCleanupResult(coordRoot, path, idFromFile, result, uncertainOwners);
175
+ if (result.state === "deleted") {
176
+ heartbeatsRemoved.push(f);
177
+ }
178
+ }
179
+ return;
131
180
  }
132
- unlinkSync(path);
133
- heartbeatsRemoved.push(f);
134
- }
135
- else if (parsed.instance_id) {
136
- liveInstanceIds.add(parsed.instance_id);
137
- }
138
- continue;
181
+ // The filename selects the writer's custody scope. A conflicting content
182
+ // identity cannot authorize an audit or deletion for a different owner.
183
+ if (parsed.instance_id !== undefined && parsed.instance_id !== idFromFile)
184
+ return;
185
+ const instanceId = parsed.instance_id ?? idFromFile;
186
+ const adapter = eventAdapterIdV3FromPlatform(parsed.platform, { context: "stale-sweep" });
187
+ const sessionId = parsed.session_id ?? instanceId;
188
+ const generationId = typeof parsed.v3_generation_id === "string" &&
189
+ /^gen_[A-Za-z0-9._-]+$/.test(parsed.v3_generation_id)
190
+ ? parsed.v3_generation_id
191
+ : null;
192
+ const ts = parsed.last_heartbeat
193
+ ? Math.floor(Date.parse(parsed.last_heartbeat) / 1000)
194
+ : Number.NaN;
195
+ if (!parsed.last_heartbeat || !Number.isFinite(ts)) {
196
+ // No / NaN last_heartbeat: can't trust content; gate on mtime.
197
+ if (mtimeSecs < cutoff) {
198
+ const result = deleteFileIfUnchanged(path, revision, () => emitSwept(coordRoot, instanceId, adapter, sessionId, generationId, "missing_ts", nowSec - mtimeSecs));
199
+ recordHeartbeatCleanupResult(coordRoot, path, idFromFile, result, uncertainOwners);
200
+ if (result.state === "deleted") {
201
+ heartbeatsRemoved.push(f);
202
+ }
203
+ }
204
+ else if (parsed.instance_id) {
205
+ liveInstanceIds.add(parsed.instance_id);
206
+ }
207
+ return;
208
+ }
209
+ if (ts < cutoff) {
210
+ // Legitimate stale prune (valid timestamp, past the freshness cutoff).
211
+ const result = deleteFileIfUnchanged(path, revision, () => emitSwept(coordRoot, instanceId, adapter, sessionId, generationId, "stale", nowSec - ts));
212
+ recordHeartbeatCleanupResult(coordRoot, path, idFromFile, result, uncertainOwners);
213
+ if (result.state === "deleted") {
214
+ heartbeatsRemoved.push(f);
215
+ }
216
+ return;
217
+ }
218
+ if (parsed.instance_id)
219
+ liveInstanceIds.add(parsed.instance_id);
220
+ });
139
221
  }
140
- if (ts < cutoff) {
141
- // Legitimate stale prune (valid timestamp, past the freshness cutoff).
142
- if (!emitSwept(coordRoot, instanceId, adapter, sessionId, "stale", nowSec - ts)) {
143
- continue;
144
- }
145
- unlinkSync(path);
146
- heartbeatsRemoved.push(f);
147
- continue;
222
+ catch {
223
+ // Busy or unknown custody retains the cache and its dependent files.
224
+ uncertainOwners.add(idFromFile);
148
225
  }
149
- if (parsed.instance_id)
150
- liveInstanceIds.add(parsed.instance_id);
151
226
  }
152
227
  }
153
228
  // 2. Prune pid-map entries whose instance has no live heartbeat.
154
- const pidmapDir = join(coordRoot, ".harnery", "pid-map");
229
+ const pidmapDir = resolve(coordRoot, ".harnery", "pid-map");
230
+ for (const recovery of existsSync(pidmapDir)
231
+ ? recoverInterruptedFileDeletions(pidmapDir, (source) => dirname(source) === pidmapDir && /^\d+$/.test(basename(source)))
232
+ : [])
233
+ recordCleanupFailure(coordRoot, recovery.sourcePath ?? pidmapDir, recovery);
155
234
  if (existsSync(pidmapDir)) {
156
235
  for (const f of readdirSync(pidmapDir)) {
236
+ if (f.startsWith(".harnery-delete-"))
237
+ continue;
157
238
  const path = join(pidmapDir, f);
158
239
  try {
159
- const row = readFileSync(path, "utf8").trim();
240
+ const revision = captureFileRevision(path);
241
+ if (!revision)
242
+ continue;
243
+ const row = revision.bytes.toString("utf8").trim();
160
244
  const ownerId = row.split("\t")[0]?.trim() ?? "";
161
- if (!ownerId || !liveInstanceIds.has(ownerId)) {
162
- unlinkSync(path);
163
- pidmapsRemoved += 1;
245
+ const prune = () => {
246
+ if (!ownerId ||
247
+ (!liveInstanceIds.has(ownerId) &&
248
+ !retainsHeartbeat(coordRoot, d, ownerId, uncertainOwners))) {
249
+ const result = deleteFileIfUnchanged(path, revision, () => !ownerId || !retainsHeartbeat(coordRoot, d, ownerId, uncertainOwners));
250
+ recordCleanupFailure(coordRoot, path, result);
251
+ if (result.state === "deleted")
252
+ pidmapsRemoved += 1;
253
+ }
254
+ };
255
+ if (/^[A-Za-z0-9_-]{1,128}$/.test(ownerId)) {
256
+ withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(ownerId)], prune);
257
+ }
258
+ else if (!ownerId) {
259
+ prune();
164
260
  }
165
261
  }
166
262
  catch {
@@ -170,7 +266,12 @@ export function staleSweep(coordRoot) {
170
266
  }
171
267
  // 3. Prune prompt hashes for dead owners. Resource hashes retain their own
172
268
  // count so existing peer-hash telemetry keeps its original meaning.
173
- const agentsDir = join(coordRoot, ".harnery");
269
+ const agentsDir = resolve(coordRoot, ".harnery");
270
+ for (const recovery of existsSync(agentsDir)
271
+ ? recoverInterruptedFileDeletions(agentsDir, (source) => dirname(source) === agentsDir &&
272
+ /^\.last-(?:resource|peer)-hash\.[A-Za-z0-9_-]{1,128}$/.test(basename(source)))
273
+ : [])
274
+ recordCleanupFailure(coordRoot, recovery.sourcePath ?? agentsDir, recovery);
174
275
  if (existsSync(agentsDir)) {
175
276
  for (const f of readdirSync(agentsDir)) {
176
277
  const resourceHash = f.startsWith(".last-resource-hash.");
@@ -183,16 +284,25 @@ export function staleSweep(coordRoot) {
183
284
  // retained heartbeat's liveness could not be determined safely.
184
285
  if (!/^[A-Za-z0-9_-]{1,128}$/.test(owner))
185
286
  continue;
186
- if (existsSync(join(d, `${owner}.json`)))
187
- continue;
188
287
  }
189
- if (!liveInstanceIds.has(owner)) {
288
+ if (!liveInstanceIds.has(owner) && /^[A-Za-z0-9_-]{1,128}$/.test(owner)) {
190
289
  try {
191
- unlinkSync(join(agentsDir, f));
192
- if (resourceHash)
193
- resourceHashesRemoved += 1;
194
- else
195
- peerHashesRemoved += 1;
290
+ withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(owner)], () => {
291
+ if (retainsHeartbeat(coordRoot, d, owner, uncertainOwners))
292
+ return;
293
+ const path = join(agentsDir, f);
294
+ const revision = captureFileRevision(path);
295
+ if (!revision)
296
+ return;
297
+ const result = deleteFileIfUnchanged(path, revision, () => !retainsHeartbeat(coordRoot, d, owner, uncertainOwners));
298
+ recordCleanupFailure(coordRoot, path, result);
299
+ if (result.state === "deleted") {
300
+ if (resourceHash)
301
+ resourceHashesRemoved += 1;
302
+ else
303
+ peerHashesRemoved += 1;
304
+ }
305
+ });
196
306
  }
197
307
  catch {
198
308
  /* swallow */
@@ -0,0 +1,44 @@
1
+ export interface FileRevision {
2
+ readonly bytes: Buffer;
3
+ readonly dev: bigint;
4
+ readonly ino: bigint;
5
+ readonly size: bigint;
6
+ readonly mtimeNs: bigint;
7
+ readonly ctimeNs: bigint;
8
+ }
9
+ export type DeleteFileResult = {
10
+ state: "deleted" | "changed" | "missing" | "rejected";
11
+ } | {
12
+ state: "recovery-required";
13
+ recoveryPath: string;
14
+ error?: string;
15
+ } | {
16
+ state: "error";
17
+ recoveryPath?: string;
18
+ error: string;
19
+ };
20
+ export interface FileRecoveryResult {
21
+ state: "restored" | "retained" | "error";
22
+ recoveryPath: string;
23
+ sourcePath?: string;
24
+ error?: string;
25
+ }
26
+ /** A stable read of one regular file, including its exact bytes and revision. */
27
+ export declare function captureFileRevision(filePath: string): FileRevision | null;
28
+ export declare function fileRevisionMatches(filePath: string, expected: FileRevision): boolean;
29
+ /**
30
+ * Writers must publish by atomic rename, never by an open descriptor to the old
31
+ * inode. The private directory is outside their publication namespace. Moving
32
+ * the row gives deletion custody over that inode without owning its live name.
33
+ * The caller owns the state parent and keeps its topology stable. Its platform
34
+ * permissions define which principals can modify it; this protocol does not
35
+ * exclude principals already authorized to change that parent.
36
+ */
37
+ export declare function deleteFileIfUnchanged(filePath: string, expected: FileRevision, validate: (pinned: FileRevision) => boolean): DeleteFileResult;
38
+ /**
39
+ * Recover same-host/platform/PID-namespace pins after process termination. File fsync
40
+ * protects manifest contents; directory entries are not fsynced, so this does
41
+ * not promise recovery after an OS crash or power loss.
42
+ */
43
+ export declare function recoverInterruptedFileDeletions(directoryPath: string, validate: (sourcePath: string) => boolean): FileRecoveryResult[];
44
+ //# sourceMappingURL=unchanged-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unchanged-file.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/unchanged-file.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,CAAA;CAAE,GACzD;IAAE,KAAK,EAAE,mBAAmB,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACpE;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC;IACzC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAsED,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAoCzE;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,OAAO,CAGrF;AAsGD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,OAAO,GAC1C,gBAAgB,CAmFlB;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,GACxC,kBAAkB,EAAE,CA+FtB"}