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
@@ -10,12 +10,21 @@
10
10
  * `coord.freshness_seconds` (see `coordFreshnessSeconds`).
11
11
  */
12
12
 
13
- import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
14
- import { join } from "node:path";
13
+ import { existsSync, lstatSync, readdirSync } from "node:fs";
14
+ import { basename, dirname, join, resolve } from "node:path";
15
15
  import { coordFreshnessSeconds } from "../../config.ts";
16
16
  import { type EventAdapterIdV3, eventAdapterIdV3FromPlatform } from "../../events/v3/adapter-id.ts";
17
+ import { withGenerationCustodyV3 } from "../../events/v3/generation-custody.ts";
18
+ import { liveInstanceIdV3 } from "../../events/v3/live-routing.ts";
17
19
  import { writeProducerDiagnosticV3 } from "../../events/v3/producers/intake.ts";
18
20
  import { recordLiveSweepObservationV3 } from "../live-lifecycle-v3.ts";
21
+ import {
22
+ captureFileRevision,
23
+ type DeleteFileResult,
24
+ deleteFileIfUnchanged,
25
+ type FileRecoveryResult,
26
+ recoverInterruptedFileDeletions,
27
+ } from "./unchanged-file.ts";
19
28
 
20
29
  /** platform → adapter, for the swept-event envelope (mirrors heartbeat-writer's adapterOf). */
21
30
  /**
@@ -32,6 +41,7 @@ function emitSwept(
32
41
  instanceId: string,
33
42
  adapter: EventAdapterIdV3,
34
43
  sessionId: string,
44
+ expectedGenerationId: `gen_${string}` | null,
35
45
  reason: "stale" | "unparseable" | "missing_ts",
36
46
  ageSecs?: number,
37
47
  ): boolean {
@@ -41,6 +51,7 @@ function emitSwept(
41
51
  owner: instanceId,
42
52
  nativeSessionId: sessionId,
43
53
  adapter,
54
+ expectedGenerationId,
44
55
  observation:
45
56
  reason === "stale"
46
57
  ? "stale_heartbeat"
@@ -65,13 +76,73 @@ function emitSwept(
65
76
  }
66
77
  }
67
78
 
68
- /** File mtime in epoch-seconds, or +Infinity if it can't be read (treat as fresh → don't reap). */
69
- function mtimeSecs(path: string): number {
79
+ /** Any retained or unreadable owner row prevents dependent cache deletion. */
80
+ function retainsHeartbeat(
81
+ coordRoot: string,
82
+ activeDir: string,
83
+ owner: string,
84
+ uncertainOwners: Set<string>,
85
+ ): boolean {
86
+ if (uncertainOwners.has("*") || uncertainOwners.has(owner)) return true;
87
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(owner)) return true;
88
+ const heartbeat = join(activeDir, `${owner}.json`);
89
+ try {
90
+ lstatSync(heartbeat);
91
+ return true;
92
+ } catch (error) {
93
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") return true;
94
+ }
70
95
  try {
71
- return Math.floor(statSync(path).mtimeMs / 1000);
72
- } catch {
73
- return Number.POSITIVE_INFINITY;
96
+ if (!lstatSync(activeDir).isDirectory()) return true;
97
+ } catch (error) {
98
+ return (error as NodeJS.ErrnoException).code !== "ENOENT";
99
+ }
100
+ // Callers hold this owner's custody. A sweeper that died after the initial
101
+ // directory scan may have left its heartbeat hidden under a private pin.
102
+ for (const recovery of recoverInterruptedFileDeletions(
103
+ activeDir,
104
+ (source) => source === heartbeat,
105
+ )) {
106
+ if (recovery.sourcePath && recovery.sourcePath !== heartbeat) continue;
107
+ if (recovery.state !== "restored") {
108
+ uncertainOwners.add(recovery.sourcePath ? owner : "*");
109
+ recordCleanupFailure(coordRoot, recovery.sourcePath ?? activeDir, recovery);
110
+ }
111
+ return true;
112
+ }
113
+ return false;
114
+ }
115
+
116
+ function recordCleanupFailure(
117
+ coordRoot: string,
118
+ sourcePath: string,
119
+ result: DeleteFileResult | FileRecoveryResult,
120
+ ): void {
121
+ if (
122
+ result.state !== "error" &&
123
+ result.state !== "recovery-required" &&
124
+ result.state !== "retained"
125
+ ) {
126
+ return;
74
127
  }
128
+ writeProducerDiagnosticV3(coordRoot, "heartbeat_sweep_unrecorded", {
129
+ signal: "cache.cleanup",
130
+ state: "cache_cleanup_pending",
131
+ reason: `${result.state}:${result.error ?? "file_cleanup_pending"}; source=${sourcePath}; recovery=${result.recoveryPath ?? "unavailable"}`,
132
+ });
133
+ }
134
+
135
+ function recordHeartbeatCleanupResult(
136
+ coordRoot: string,
137
+ sourcePath: string,
138
+ owner: string,
139
+ result: DeleteFileResult,
140
+ uncertainOwners: Set<string>,
141
+ ): void {
142
+ if (result.state === "recovery-required" || result.state === "error") {
143
+ uncertainOwners.add(owner);
144
+ }
145
+ recordCleanupFailure(coordRoot, sourcePath, result);
75
146
  }
76
147
 
77
148
  export function staleSweep(coordRoot: string): {
@@ -105,98 +176,171 @@ export function staleSweep(coordRoot: string): {
105
176
  // lifecycle.sweep_observed; a terminal or unavailable generation gets a
106
177
  // heartbeat_sweep_unrecorded producer diagnostic without being resurrected.
107
178
  const liveInstanceIds = new Set<string>();
108
- const d = join(coordRoot, ".harnery", "active");
179
+ const uncertainOwners = new Set<string>();
180
+ const d = resolve(coordRoot, ".harnery", "active");
181
+ for (const recovery of existsSync(d)
182
+ ? recoverInterruptedFileDeletions(
183
+ d,
184
+ (source) => dirname(source) === d && /^[A-Za-z0-9_-]{1,128}\.json$/.test(basename(source)),
185
+ )
186
+ : []) {
187
+ if (recovery.state !== "restored") {
188
+ uncertainOwners.add(recovery.sourcePath ? basename(recovery.sourcePath, ".json") : "*");
189
+ recordCleanupFailure(coordRoot, recovery.sourcePath ?? d, recovery);
190
+ }
191
+ }
109
192
  if (existsSync(d)) {
110
193
  for (const f of readdirSync(d)) {
111
194
  if (!f.endsWith(".json")) continue;
112
195
  const path = join(d, f);
113
196
  const idFromFile = f.replace(/\.json$/, "");
114
- let parsed: {
115
- instance_id?: string;
116
- last_heartbeat?: string;
117
- platform?: unknown;
118
- session_id?: string;
119
- } | null = null;
197
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(idFromFile)) continue;
120
198
  try {
121
- parsed = JSON.parse(readFileSync(path, "utf8"));
122
- } catch {
123
- parsed = null;
124
- }
125
-
126
- if (parsed === null) {
127
- // Unparseable: only reap if the file itself is mtime-old.
128
- if (mtimeSecs(path) < cutoff) {
199
+ withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(idFromFile)], () => {
200
+ const revision = captureFileRevision(path);
201
+ if (!revision) return;
202
+ const mtimeSecs = Number(revision.mtimeNs / 1_000_000_000n);
203
+ let parsed: {
204
+ instance_id?: string;
205
+ last_heartbeat?: string;
206
+ platform?: unknown;
207
+ session_id?: string;
208
+ v3_generation_id?: unknown;
209
+ } | null = null;
129
210
  try {
130
- const ageSecs = nowSec - mtimeSecs(path);
131
- if (
132
- !emitSwept(coordRoot, idFromFile, "claude-code", idFromFile, "unparseable", ageSecs)
133
- ) {
134
- continue;
135
- }
136
- unlinkSync(path);
137
- heartbeatsRemoved.push(f);
211
+ const value = JSON.parse(revision.bytes.toString("utf8"));
212
+ if (value && typeof value === "object" && !Array.isArray(value)) parsed = value;
138
213
  } catch {
139
- /* swallow */
214
+ parsed = null;
140
215
  }
141
- }
142
- continue;
143
- }
144
216
 
145
- const instanceId = parsed.instance_id ?? idFromFile;
146
- const adapter = eventAdapterIdV3FromPlatform(parsed.platform, { context: "stale-sweep" });
147
- const sessionId = parsed.session_id ?? instanceId;
148
- const ts = parsed.last_heartbeat
149
- ? Math.floor(Date.parse(parsed.last_heartbeat) / 1000)
150
- : Number.NaN;
217
+ if (parsed === null) {
218
+ // Unparseable: only reap if the file itself is mtime-old.
219
+ if (mtimeSecs < cutoff) {
220
+ const result = deleteFileIfUnchanged(path, revision, () =>
221
+ emitSwept(
222
+ coordRoot,
223
+ idFromFile,
224
+ "claude-code",
225
+ idFromFile,
226
+ null,
227
+ "unparseable",
228
+ nowSec - mtimeSecs,
229
+ ),
230
+ );
231
+ recordHeartbeatCleanupResult(coordRoot, path, idFromFile, result, uncertainOwners);
232
+ if (result.state === "deleted") {
233
+ heartbeatsRemoved.push(f);
234
+ }
235
+ }
236
+ return;
237
+ }
151
238
 
152
- if (!parsed.last_heartbeat || !Number.isFinite(ts)) {
153
- // No / NaN last_heartbeat: can't trust content; gate on mtime.
154
- if (mtimeSecs(path) < cutoff) {
155
- if (
156
- !emitSwept(
157
- coordRoot,
158
- instanceId,
159
- adapter,
160
- sessionId,
161
- "missing_ts",
162
- nowSec - mtimeSecs(path),
163
- )
164
- ) {
165
- continue;
239
+ // The filename selects the writer's custody scope. A conflicting content
240
+ // identity cannot authorize an audit or deletion for a different owner.
241
+ if (parsed.instance_id !== undefined && parsed.instance_id !== idFromFile) return;
242
+ const instanceId = parsed.instance_id ?? idFromFile;
243
+ const adapter = eventAdapterIdV3FromPlatform(parsed.platform, { context: "stale-sweep" });
244
+ const sessionId = parsed.session_id ?? instanceId;
245
+ const generationId =
246
+ typeof parsed.v3_generation_id === "string" &&
247
+ /^gen_[A-Za-z0-9._-]+$/.test(parsed.v3_generation_id)
248
+ ? (parsed.v3_generation_id as `gen_${string}`)
249
+ : null;
250
+ const ts = parsed.last_heartbeat
251
+ ? Math.floor(Date.parse(parsed.last_heartbeat) / 1000)
252
+ : Number.NaN;
253
+
254
+ if (!parsed.last_heartbeat || !Number.isFinite(ts)) {
255
+ // No / NaN last_heartbeat: can't trust content; gate on mtime.
256
+ if (mtimeSecs < cutoff) {
257
+ const result = deleteFileIfUnchanged(path, revision, () =>
258
+ emitSwept(
259
+ coordRoot,
260
+ instanceId,
261
+ adapter,
262
+ sessionId,
263
+ generationId,
264
+ "missing_ts",
265
+ nowSec - mtimeSecs,
266
+ ),
267
+ );
268
+ recordHeartbeatCleanupResult(coordRoot, path, idFromFile, result, uncertainOwners);
269
+ if (result.state === "deleted") {
270
+ heartbeatsRemoved.push(f);
271
+ }
272
+ } else if (parsed.instance_id) {
273
+ liveInstanceIds.add(parsed.instance_id);
274
+ }
275
+ return;
166
276
  }
167
- unlinkSync(path);
168
- heartbeatsRemoved.push(f);
169
- } else if (parsed.instance_id) {
170
- liveInstanceIds.add(parsed.instance_id);
171
- }
172
- continue;
173
- }
174
277
 
175
- if (ts < cutoff) {
176
- // Legitimate stale prune (valid timestamp, past the freshness cutoff).
177
- if (!emitSwept(coordRoot, instanceId, adapter, sessionId, "stale", nowSec - ts)) {
178
- continue;
179
- }
180
- unlinkSync(path);
181
- heartbeatsRemoved.push(f);
182
- continue;
183
- }
278
+ if (ts < cutoff) {
279
+ // Legitimate stale prune (valid timestamp, past the freshness cutoff).
280
+ const result = deleteFileIfUnchanged(path, revision, () =>
281
+ emitSwept(
282
+ coordRoot,
283
+ instanceId,
284
+ adapter,
285
+ sessionId,
286
+ generationId,
287
+ "stale",
288
+ nowSec - ts,
289
+ ),
290
+ );
291
+ recordHeartbeatCleanupResult(coordRoot, path, idFromFile, result, uncertainOwners);
292
+ if (result.state === "deleted") {
293
+ heartbeatsRemoved.push(f);
294
+ }
295
+ return;
296
+ }
184
297
 
185
- if (parsed.instance_id) liveInstanceIds.add(parsed.instance_id);
298
+ if (parsed.instance_id) liveInstanceIds.add(parsed.instance_id);
299
+ });
300
+ } catch {
301
+ // Busy or unknown custody retains the cache and its dependent files.
302
+ uncertainOwners.add(idFromFile);
303
+ }
186
304
  }
187
305
  }
188
306
 
189
307
  // 2. Prune pid-map entries whose instance has no live heartbeat.
190
- const pidmapDir = join(coordRoot, ".harnery", "pid-map");
308
+ const pidmapDir = resolve(coordRoot, ".harnery", "pid-map");
309
+ for (const recovery of existsSync(pidmapDir)
310
+ ? recoverInterruptedFileDeletions(
311
+ pidmapDir,
312
+ (source) => dirname(source) === pidmapDir && /^\d+$/.test(basename(source)),
313
+ )
314
+ : [])
315
+ recordCleanupFailure(coordRoot, recovery.sourcePath ?? pidmapDir, recovery);
191
316
  if (existsSync(pidmapDir)) {
192
317
  for (const f of readdirSync(pidmapDir)) {
318
+ if (f.startsWith(".harnery-delete-")) continue;
193
319
  const path = join(pidmapDir, f);
194
320
  try {
195
- const row = readFileSync(path, "utf8").trim();
321
+ const revision = captureFileRevision(path);
322
+ if (!revision) continue;
323
+ const row = revision.bytes.toString("utf8").trim();
196
324
  const ownerId = row.split("\t")[0]?.trim() ?? "";
197
- if (!ownerId || !liveInstanceIds.has(ownerId)) {
198
- unlinkSync(path);
199
- pidmapsRemoved += 1;
325
+ const prune = () => {
326
+ if (
327
+ !ownerId ||
328
+ (!liveInstanceIds.has(ownerId) &&
329
+ !retainsHeartbeat(coordRoot, d, ownerId, uncertainOwners))
330
+ ) {
331
+ const result = deleteFileIfUnchanged(
332
+ path,
333
+ revision,
334
+ () => !ownerId || !retainsHeartbeat(coordRoot, d, ownerId, uncertainOwners),
335
+ );
336
+ recordCleanupFailure(coordRoot, path, result);
337
+ if (result.state === "deleted") pidmapsRemoved += 1;
338
+ }
339
+ };
340
+ if (/^[A-Za-z0-9_-]{1,128}$/.test(ownerId)) {
341
+ withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(ownerId)], prune);
342
+ } else if (!ownerId) {
343
+ prune();
200
344
  }
201
345
  } catch {
202
346
  /* swallow */
@@ -206,7 +350,16 @@ export function staleSweep(coordRoot: string): {
206
350
 
207
351
  // 3. Prune prompt hashes for dead owners. Resource hashes retain their own
208
352
  // count so existing peer-hash telemetry keeps its original meaning.
209
- const agentsDir = join(coordRoot, ".harnery");
353
+ const agentsDir = resolve(coordRoot, ".harnery");
354
+ for (const recovery of existsSync(agentsDir)
355
+ ? recoverInterruptedFileDeletions(
356
+ agentsDir,
357
+ (source) =>
358
+ dirname(source) === agentsDir &&
359
+ /^\.last-(?:resource|peer)-hash\.[A-Za-z0-9_-]{1,128}$/.test(basename(source)),
360
+ )
361
+ : [])
362
+ recordCleanupFailure(coordRoot, recovery.sourcePath ?? agentsDir, recovery);
210
363
  if (existsSync(agentsDir)) {
211
364
  for (const f of readdirSync(agentsDir)) {
212
365
  const resourceHash = f.startsWith(".last-resource-hash.");
@@ -217,13 +370,25 @@ export function staleSweep(coordRoot: string): {
217
370
  // Ignore atomic-write temporary files and preserve hashes when a
218
371
  // retained heartbeat's liveness could not be determined safely.
219
372
  if (!/^[A-Za-z0-9_-]{1,128}$/.test(owner)) continue;
220
- if (existsSync(join(d, `${owner}.json`))) continue;
221
373
  }
222
- if (!liveInstanceIds.has(owner)) {
374
+ if (!liveInstanceIds.has(owner) && /^[A-Za-z0-9_-]{1,128}$/.test(owner)) {
223
375
  try {
224
- unlinkSync(join(agentsDir, f));
225
- if (resourceHash) resourceHashesRemoved += 1;
226
- else peerHashesRemoved += 1;
376
+ withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(owner)], () => {
377
+ if (retainsHeartbeat(coordRoot, d, owner, uncertainOwners)) return;
378
+ const path = join(agentsDir, f);
379
+ const revision = captureFileRevision(path);
380
+ if (!revision) return;
381
+ const result = deleteFileIfUnchanged(
382
+ path,
383
+ revision,
384
+ () => !retainsHeartbeat(coordRoot, d, owner, uncertainOwners),
385
+ );
386
+ recordCleanupFailure(coordRoot, path, result);
387
+ if (result.state === "deleted") {
388
+ if (resourceHash) resourceHashesRemoved += 1;
389
+ else peerHashesRemoved += 1;
390
+ }
391
+ });
227
392
  } catch {
228
393
  /* swallow */
229
394
  }