harnery 0.45.0 → 0.45.2

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 (235) hide show
  1. package/dist/commander.js +0 -1
  2. package/dist/commands/files.d.ts +2 -2
  3. package/dist/commands/files.d.ts.map +1 -1
  4. package/dist/commands/files.js +4 -7
  5. package/dist/commands/servers.d.ts.map +1 -1
  6. package/dist/commands/servers.js +17 -11
  7. package/dist/commands/tunnel.d.ts +0 -15
  8. package/dist/commands/tunnel.d.ts.map +1 -1
  9. package/dist/commands/tunnel.js +153 -453
  10. package/dist/commands/workflow.d.ts.map +1 -1
  11. package/dist/commands/workflow.js +6 -45
  12. package/dist/core/agents/coord-client.d.ts.map +1 -1
  13. package/dist/core/agents/coord-client.js +0 -18
  14. package/dist/core/agents/live-authority-v3.d.ts +0 -1
  15. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  16. package/dist/core/agents/live-authority-v3.js +0 -11
  17. package/dist/core/agents/live-lifecycle-v3.d.ts +2 -4
  18. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  19. package/dist/core/agents/live-lifecycle-v3.js +7 -28
  20. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  21. package/dist/core/agents/state/heartbeat-writer.js +14 -25
  22. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  23. package/dist/core/agents/state/live-coordination-writer.js +0 -5
  24. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  25. package/dist/core/agents/state/pidmap.js +2 -7
  26. package/dist/core/agents/state/proc-start.d.ts +20 -31
  27. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  28. package/dist/core/agents/state/proc-start.js +37 -70
  29. package/dist/core/agents/state/service-process.d.ts +61 -0
  30. package/dist/core/agents/state/service-process.d.ts.map +1 -0
  31. package/dist/core/agents/state/service-process.js +143 -0
  32. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  33. package/dist/core/agents/state/stale-sweep.js +70 -180
  34. package/dist/core/artifacts/activity.d.ts +4 -1
  35. package/dist/core/artifacts/activity.d.ts.map +1 -1
  36. package/dist/core/artifacts/activity.js +8 -2
  37. package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
  38. package/dist/core/artifacts/delivery-card.js +1 -2
  39. package/dist/core/artifacts/index.d.ts.map +1 -1
  40. package/dist/core/artifacts/index.js +38 -12
  41. package/dist/core/context/index.d.ts +6 -4
  42. package/dist/core/context/index.d.ts.map +1 -1
  43. package/dist/core/context/index.js +194 -256
  44. package/dist/core/context/observation.d.ts +9 -9
  45. package/dist/core/context/observation.d.ts.map +1 -1
  46. package/dist/core/context/observation.js +37 -6
  47. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  48. package/dist/core/events/v3/authority-outbox.js +0 -29
  49. package/dist/core/events/v3/producers/coordination-recorder.d.ts +0 -1
  50. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  51. package/dist/core/events/v3/producers/coordination-recorder.js +0 -7
  52. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  53. package/dist/core/events/v3/producers/recorder.js +64 -77
  54. package/dist/core/events/v3/runtime-build.d.ts +14 -0
  55. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  56. package/dist/core/events/v3/runtime-build.js +44 -0
  57. package/dist/core/governor/index.d.ts +2 -1
  58. package/dist/core/governor/index.d.ts.map +1 -1
  59. package/dist/core/governor/index.js +2 -1
  60. package/dist/core/governor/service-read.d.ts +3 -10
  61. package/dist/core/governor/service-read.d.ts.map +1 -1
  62. package/dist/core/governor/service-read.js +10 -94
  63. package/dist/core/governor/service.d.ts +3 -7
  64. package/dist/core/governor/service.d.ts.map +1 -1
  65. package/dist/core/governor/service.js +17 -182
  66. package/dist/core/hooks/cli.js +78 -48
  67. package/dist/core/hooks/resolve/owner.js +2 -2
  68. package/dist/core/resources/sampler.js +2 -2
  69. package/dist/core/resources/service-status.d.ts +2 -14
  70. package/dist/core/resources/service-status.d.ts.map +1 -1
  71. package/dist/core/resources/service-status.js +3 -23
  72. package/dist/core/resources/service.d.ts +3 -7
  73. package/dist/core/resources/service.d.ts.map +1 -1
  74. package/dist/core/resources/service.js +12 -101
  75. package/dist/core/resources/status.d.ts.map +1 -1
  76. package/dist/core/resources/status.js +3 -2
  77. package/dist/core/semantic/service.d.ts.map +1 -1
  78. package/dist/core/semantic/service.js +9 -2
  79. package/dist/core/servers/index.d.ts +11 -10
  80. package/dist/core/servers/index.d.ts.map +1 -1
  81. package/dist/core/servers/index.js +82 -192
  82. package/dist/core/servers/tunnels.d.ts +3 -5
  83. package/dist/core/servers/tunnels.d.ts.map +1 -1
  84. package/dist/core/servers/tunnels.js +2 -45
  85. package/dist/core/storage/durable-history.js +56 -22
  86. package/dist/core/storage/segments.d.ts.map +1 -1
  87. package/dist/core/storage/segments.js +73 -17
  88. package/dist/core/supervisor/service.d.ts +4 -7
  89. package/dist/core/supervisor/service.d.ts.map +1 -1
  90. package/dist/core/supervisor/service.js +23 -122
  91. package/dist/core/supervisor/services.js +2 -2
  92. package/dist/core/supervisor/status.d.ts +3 -17
  93. package/dist/core/supervisor/status.d.ts.map +1 -1
  94. package/dist/core/supervisor/status.js +7 -70
  95. package/dist/core/work/state.js +7 -7
  96. package/dist/core/workflow/engine.d.ts.map +1 -1
  97. package/dist/core/workflow/engine.js +23 -66
  98. package/dist/core/workflow/index.d.ts +3 -4
  99. package/dist/core/workflow/index.d.ts.map +1 -1
  100. package/dist/core/workflow/index.js +2 -2
  101. package/dist/core/workflow/live-session-v3.d.ts +0 -6
  102. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  103. package/dist/core/workflow/live-session-v3.js +5 -126
  104. package/dist/core/workflow/proof.d.ts +4 -4
  105. package/dist/core/workflow/proof.d.ts.map +1 -1
  106. package/dist/core/workflow/proof.js +34 -86
  107. package/dist/core/workflow/resume-lock.d.ts +17 -0
  108. package/dist/core/workflow/resume-lock.d.ts.map +1 -0
  109. package/dist/core/workflow/resume-lock.js +158 -0
  110. package/dist/core/workflow/run-state.d.ts +0 -21
  111. package/dist/core/workflow/run-state.d.ts.map +1 -1
  112. package/dist/core/workflow/run-state.js +3 -174
  113. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  114. package/dist/core/workflow/spawn-claude.js +0 -19
  115. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  116. package/dist/core/workflow/spawn-codex.js +0 -13
  117. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  118. package/dist/core/workflow/spawn-cursor.js +0 -19
  119. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  120. package/dist/core/workflow/spawn-opencode.js +0 -14
  121. package/dist/core/workflow/types.d.ts +11 -54
  122. package/dist/core/workflow/types.d.ts.map +1 -1
  123. package/dist/core/workflow/workspaces/leases.d.ts +4 -0
  124. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  125. package/dist/core/workflow/workspaces/leases.js +1 -1
  126. package/dist/core/workflow/workspaces/local-git.js +91 -210
  127. package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
  128. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
  129. package/dist/core/workflow/workspaces/ownership.js +3 -130
  130. package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
  131. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  132. package/dist/core/workflow/workspaces/reclaim.js +0 -4
  133. package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
  134. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  135. package/dist/core/workflow/workspaces/recovery.js +1 -1
  136. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  137. package/dist/core/workflow/workspaces/state.js +1 -4
  138. package/dist/lib/identities/assume.d.ts.map +1 -1
  139. package/dist/lib/identities/assume.js +0 -4
  140. package/dist/lib/tunnel/state.d.ts +6 -13
  141. package/dist/lib/tunnel/state.d.ts.map +1 -1
  142. package/dist/lib/tunnel/state.js +12 -58
  143. package/package.json +1 -6
  144. package/src/commander.ts +0 -10
  145. package/src/commands/files.ts +5 -13
  146. package/src/commands/servers.ts +23 -12
  147. package/src/commands/tunnel.ts +166 -533
  148. package/src/commands/workflow.ts +8 -58
  149. package/src/core/agents/coord-client.ts +0 -20
  150. package/src/core/agents/live-authority-v3.ts +0 -17
  151. package/src/core/agents/live-lifecycle-v3.ts +14 -52
  152. package/src/core/agents/state/heartbeat-writer.ts +12 -29
  153. package/src/core/agents/state/live-coordination-writer.ts +0 -29
  154. package/src/core/agents/state/pidmap.ts +2 -15
  155. package/src/core/agents/state/proc-start.ts +41 -69
  156. package/src/core/agents/state/service-process.ts +217 -0
  157. package/src/core/agents/state/stale-sweep.ts +82 -247
  158. package/src/core/artifacts/activity.ts +9 -2
  159. package/src/core/artifacts/delivery-card.ts +1 -7
  160. package/src/core/artifacts/index.ts +45 -12
  161. package/src/core/context/index.ts +207 -272
  162. package/src/core/context/observation.ts +40 -9
  163. package/src/core/events/v3/authority-outbox.ts +0 -56
  164. package/src/core/events/v3/producers/coordination-recorder.ts +0 -15
  165. package/src/core/events/v3/producers/recorder.ts +68 -94
  166. package/src/core/events/v3/runtime-build.ts +62 -0
  167. package/src/core/governor/index.ts +1 -2
  168. package/src/core/governor/service-read.ts +15 -114
  169. package/src/core/governor/service.ts +36 -220
  170. package/src/core/hooks/cli.ts +85 -47
  171. package/src/core/hooks/resolve/owner.ts +2 -2
  172. package/src/core/resources/sampler.ts +3 -2
  173. package/src/core/resources/service-status.ts +6 -35
  174. package/src/core/resources/service.ts +23 -133
  175. package/src/core/resources/status.ts +3 -2
  176. package/src/core/semantic/service.ts +8 -2
  177. package/src/core/servers/index.ts +86 -228
  178. package/src/core/servers/tunnels.ts +3 -55
  179. package/src/core/storage/durable-history.ts +52 -20
  180. package/src/core/storage/segments.ts +79 -17
  181. package/src/core/supervisor/service.ts +33 -155
  182. package/src/core/supervisor/services.ts +2 -2
  183. package/src/core/supervisor/status.ts +10 -85
  184. package/src/core/work/state.ts +7 -7
  185. package/src/core/workflow/engine.ts +22 -74
  186. package/src/core/workflow/index.ts +2 -13
  187. package/src/core/workflow/live-session-v3.ts +5 -146
  188. package/src/core/workflow/proof.ts +40 -104
  189. package/src/core/workflow/resume-lock.ts +179 -0
  190. package/src/core/workflow/run-state.ts +3 -207
  191. package/src/core/workflow/spawn-claude.ts +0 -18
  192. package/src/core/workflow/spawn-codex.ts +0 -13
  193. package/src/core/workflow/spawn-cursor.ts +0 -18
  194. package/src/core/workflow/spawn-opencode.ts +0 -14
  195. package/src/core/workflow/types.ts +11 -41
  196. package/src/core/workflow/workspaces/leases.ts +1 -1
  197. package/src/core/workflow/workspaces/local-git.ts +120 -239
  198. package/src/core/workflow/workspaces/ownership.ts +3 -158
  199. package/src/core/workflow/workspaces/reclaim.ts +3 -9
  200. package/src/core/workflow/workspaces/recovery.ts +1 -1
  201. package/src/core/workflow/workspaces/state.ts +1 -5
  202. package/src/lib/identities/assume.ts +0 -4
  203. package/src/lib/tunnel/state.ts +15 -70
  204. package/dist/commands/turn-rollback.d.ts +0 -5
  205. package/dist/commands/turn-rollback.d.ts.map +0 -1
  206. package/dist/commands/turn-rollback.js +0 -76
  207. package/dist/core/agents/state/process-handle.d.ts +0 -26
  208. package/dist/core/agents/state/process-handle.d.ts.map +0 -1
  209. package/dist/core/agents/state/process-handle.js +0 -121
  210. package/dist/core/agents/state/unchanged-file.d.ts +0 -44
  211. package/dist/core/agents/state/unchanged-file.d.ts.map +0 -1
  212. package/dist/core/agents/state/unchanged-file.js +0 -390
  213. package/dist/core/events/v3/generation-custody.d.ts +0 -5
  214. package/dist/core/events/v3/generation-custody.d.ts.map +0 -1
  215. package/dist/core/events/v3/generation-custody.js +0 -50
  216. package/dist/core/turn-rollback/content.d.ts +0 -53
  217. package/dist/core/turn-rollback/content.d.ts.map +0 -1
  218. package/dist/core/turn-rollback/content.js +0 -396
  219. package/dist/core/turn-rollback/index.d.ts +0 -72
  220. package/dist/core/turn-rollback/index.d.ts.map +0 -1
  221. package/dist/core/turn-rollback/index.js +0 -443
  222. package/dist/core/workflow/failure-evidence.d.ts +0 -16
  223. package/dist/core/workflow/failure-evidence.d.ts.map +0 -1
  224. package/dist/core/workflow/failure-evidence.js +0 -114
  225. package/dist/core/workflow/recovery-audit.d.ts +0 -30
  226. package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
  227. package/dist/core/workflow/recovery-audit.js +0 -116
  228. package/src/commands/turn-rollback.ts +0 -111
  229. package/src/core/agents/state/process-handle.ts +0 -165
  230. package/src/core/agents/state/unchanged-file.ts +0 -456
  231. package/src/core/events/v3/generation-custody.ts +0 -59
  232. package/src/core/turn-rollback/content.ts +0 -499
  233. package/src/core/turn-rollback/index.ts +0 -580
  234. package/src/core/workflow/failure-evidence.ts +0 -141
  235. package/src/core/workflow/recovery-audit.ts +0 -181
@@ -73,15 +73,7 @@ interface WorkflowConfirmedMutationOpts {
73
73
 
74
74
  interface WorkflowReclaimOpts extends WorkflowConfirmedMutationOpts {
75
75
  discard?: boolean;
76
- unknownLiveness?: string;
77
- actor?: string;
78
- reason?: string;
79
- }
80
-
81
- interface WorkflowUnlockOpts extends WorkflowConfirmedMutationOpts {
82
- lockSha256?: string;
83
- actor?: string;
84
- reason?: string;
76
+ force?: boolean;
85
77
  }
86
78
 
87
79
  export function registerWorkflowCommand(program: Command, emit: EmitContext): void {
@@ -632,11 +624,9 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
632
624
  .option("--yes", "Confirm that this commits or discards uncommitted work, then releases")
633
625
  .option("--discard", "Throw the uncommitted work away instead of salvaging it")
634
626
  .option(
635
- "--unknown-liveness <sha256>",
636
- "Recover unknown evidence pinned by the preview digest; active evidence still refuses",
627
+ "--force",
628
+ "With --yes, admit missing or corrupt liveness evidence (active evidence still refuses) and record it",
637
629
  )
638
- .option("--actor <id>", "Actor responsible for an audited recovery")
639
- .option("--reason <text>", "Reason for an audited recovery")
640
630
  .option("--json", "Emit the reclaim preview or confirmed result as JSON")
641
631
  .action(async (runId: string, opts: WorkflowReclaimOpts) => {
642
632
  const coordRoot = resolveCoordRoot();
@@ -663,13 +653,7 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
663
653
  const result = await provider.reclaim(binding, {
664
654
  mode: opts.discard ? "discard" : "salvage",
665
655
  yes: opts.yes,
666
- unknownLiveness: opts.unknownLiveness
667
- ? {
668
- evidenceSha256: opts.unknownLiveness,
669
- actor: opts.actor!,
670
- reason: opts.reason!,
671
- }
672
- : undefined,
656
+ force: opts.force,
673
657
  });
674
658
  if (opts.json) {
675
659
  emit.config({ format: "json" });
@@ -684,11 +668,13 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
684
668
  emit.text(
685
669
  `reclaim preview: ${result.run_id}\n` +
686
670
  `mode: ${result.mode}\nliveness: ${result.liveness}\nownership: ${result.ownership}\n` +
687
- `evidence sha256: ${result.evidence_sha256}\n` +
688
671
  `lease holder: ${holders.length > 0 ? holders.join(", ") : "none observed"}\n` +
689
672
  `dirty paths: ${result.dirty_paths === null ? "unknown" : result.dirty_paths.length > 0 ? result.dirty_paths.join(", ") : "none"}\n` +
690
673
  (result.ownership_detail ? `ownership detail: ${result.ownership_detail}\n` : "") +
691
- `Pass --yes to confirm ${result.mode} and release after all checks pass.\n`,
674
+ `Pass --yes to confirm ${result.mode} and release after all checks pass.\n` +
675
+ (result.liveness === "unknown"
676
+ ? "Liveness is unknown; add --force with --yes only if the run is known to be stopped.\n"
677
+ : ""),
692
678
  );
693
679
  return;
694
680
  }
@@ -708,42 +694,6 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
708
694
  }
709
695
  });
710
696
 
711
- workflow
712
- .command("unlock <run-id>")
713
- .description(
714
- "Preview or explicitly retire one exact resume lock whose local owner is proven dead.",
715
- )
716
- .option("--lock-sha256 <sha256>", "Exact lock identity digest returned by the preview")
717
- .option("--actor <id>", "Actor responsible for the recovery")
718
- .option("--reason <text>", "Reason for the recovery")
719
- .option("--yes", "Confirm removal after dead-owner and exact-lock checks")
720
- .option("--json", "Emit the recovery preview or result as JSON")
721
- .action(async (runId: string, opts: WorkflowUnlockOpts) => {
722
- try {
723
- const coordRoot = resolveCoordRoot();
724
- if (!coordRoot) throw new Error("no .harnery/ coordination root found");
725
- const { recoverWorkflowResumeLease } = await import("../core/workflow/index.ts");
726
- const result = recoverWorkflowResumeLease(coordRoot, runId, opts);
727
- if (opts.json) {
728
- emit.config({ format: "json" });
729
- emit.data(result);
730
- return;
731
- }
732
- emit.text(
733
- `resume lock ${result.action}: ${runId}\nowner: ${result.owner_state}\nlock sha256: ${result.lock_sha256 ?? "absent"}\n` +
734
- (result.action === "preview"
735
- ? "Pass --lock-sha256, --actor, --reason and --yes to confirm exact dead-owner recovery.\n"
736
- : `recovery: ${result.recovery_id}\n`),
737
- );
738
- } catch (error) {
739
- emit.error({
740
- code: "workflow_resume_unlock_failed",
741
- message: error instanceof Error ? error.message : String(error),
742
- });
743
- process.exit(1);
744
- }
745
- });
746
-
747
697
  const approvals = program
748
698
  .command("approval")
749
699
  .description("Inspect and resolve durable run-policy approvals.");
@@ -265,27 +265,7 @@ export function parsePidmapRowStartToken(row: string): string | undefined {
265
265
  * a test can hold this one against `processStartToken` and fail on drift.
266
266
  */
267
267
  export function pidStartToken(pid: number): string | null {
268
- if (!Number.isInteger(pid) || pid <= 0) return null;
269
268
  const forced = process.env.HARNERY_PID_PROBE;
270
- if (process.platform === "win32" && forced !== "procfs" && forced !== "ps") {
271
- try {
272
- const out = spawnSync(
273
- "powershell.exe",
274
- [
275
- "-NoLogo",
276
- "-NoProfile",
277
- "-NonInteractive",
278
- "-Command",
279
- `$process = [Diagnostics.Process]::GetProcessById(${pid}); try { 'w' + $process.StartTime.ToFileTimeUtc().ToString([Globalization.CultureInfo]::InvariantCulture) } finally { $process.Dispose() }`,
280
- ],
281
- { encoding: "utf8", timeout: 10_000, windowsHide: true },
282
- );
283
- const token = String(out.stdout ?? "").trim();
284
- return out.status === 0 && /^w\d+$/.test(token) ? token : null;
285
- } catch {
286
- return null;
287
- }
288
- }
289
269
  const useProcfs = forced === "procfs" || (forced !== "ps" && existsSync("/proc/self/stat"));
290
270
  // One machine, one probe. Falling back to the other on a read failure would
291
271
  // answer in the wrong dialect and read as a recycled pid.
@@ -88,7 +88,6 @@ interface LiveAuthorityBaseV3 {
88
88
  nativeSessionId: string;
89
89
  adapter: Adapter;
90
90
  observationId?: string;
91
- expectedGenerationId?: `gen_${string}`;
92
91
  }
93
92
 
94
93
  /**
@@ -649,13 +648,6 @@ function recordLiveAuthority<S extends CoordinationAuthoritySignalV3>(
649
648
  platform: livePlatformV3(),
650
649
  expected_prior_state_digest: coordinationAuthorityStateDigestV3(expected),
651
650
  desired_state_digest: coordinationAuthorityStateDigestV3(desired),
652
- expected_generation_id:
653
- input.expectedGenerationId ??
654
- ((subject === input.owner
655
- ? expected.v3_generation_id
656
- : readLiveCoordinationRow(input.coordRoot, input.owner)?.v3_generation_id) as
657
- | `gen_${string}`
658
- | undefined),
659
651
  reconciler: {
660
652
  readStateDigest: () => {
661
653
  const heartbeat = readHeartbeat(input.coordRoot, subject);
@@ -689,12 +681,6 @@ export function liveCoordinationWriteModeV3(coordRoot: string): EventV3WriteMode
689
681
  }
690
682
 
691
683
  function requireHeartbeat(input: LiveAuthorityBaseV3, owner: string): Heartbeat {
692
- if (
693
- input.expectedGenerationId &&
694
- readLiveCoordinationRow(input.coordRoot, owner)?.v3_generation_id !== input.expectedGenerationId
695
- ) {
696
- throw new LiveCoordinationAuthorityV3Error("expected_generation_changed");
697
- }
698
684
  const heartbeat = ensureLiveCoordinationHeartbeat(
699
685
  input.coordRoot,
700
686
  owner,
@@ -702,9 +688,6 @@ function requireHeartbeat(input: LiveAuthorityBaseV3, owner: string): Heartbeat
702
688
  input.adapter,
703
689
  );
704
690
  if (!heartbeat) throw new LiveCoordinationAuthorityV3Error(`heartbeat_missing:${owner}`);
705
- if (input.expectedGenerationId && heartbeat.v3_generation_id !== input.expectedGenerationId) {
706
- throw new LiveCoordinationAuthorityV3Error("expected_generation_changed");
707
- }
708
691
  return heartbeat;
709
692
  }
710
693
 
@@ -5,7 +5,6 @@ import { normalizeNativeIdV3 } from "../events/v3/canonical.ts";
5
5
  import type { EventV3 } from "../events/v3/contract.ts";
6
6
  import { readEventV3ControlState } from "../events/v3/control.ts";
7
7
  import { fingerprintContextV3 } from "../events/v3/fingerprint-keys.ts";
8
- import { withGenerationCustodyV3 } from "../events/v3/generation-custody.ts";
9
8
  import {
10
9
  liveInstanceIdV3,
11
10
  livePlatformV3,
@@ -26,34 +25,17 @@ interface LiveLifecycleObservationBaseV3 {
26
25
  observedAt?: string;
27
26
  }
28
27
 
29
- interface LiveSweepObservationV3Input extends LiveLifecycleObservationBaseV3 {
30
- expectedGenerationId: `gen_${string}` | null;
31
- observation: "stale_heartbeat" | "killed" | "unparseable_heartbeat" | "missing_timestamp";
32
- ageMs: number;
33
- }
34
-
35
28
  export function recordLiveSweepObservationV3(
36
- input: LiveSweepObservationV3Input,
29
+ input: LiveLifecycleObservationBaseV3 & {
30
+ observation: "stale_heartbeat" | "killed" | "unparseable_heartbeat" | "missing_timestamp";
31
+ ageMs: number;
32
+ },
37
33
  ): LiveLifecycleObservationV3Result {
38
- input = { ...input };
39
- // Reject stale observations before preparing durable custody state. The
40
- // in-custody checks cover replacement while acquiring custody and reentry.
41
- requireSweepHookState(input);
42
- return withGenerationCustodyV3(input.coordRoot, [liveInstanceIdV3(input.owner)], () => {
43
- requireSweepHookState(input);
44
- return recordObservation(
45
- input,
46
- "lifecycle.sweep_observed",
47
- {
48
- subject_instance_id: liveInstanceIdV3(input.owner),
49
- observation: input.observation,
50
- provisional: true,
51
- age_ms: Math.max(0, Math.floor(input.ageMs)),
52
- },
53
- () => {
54
- requireSweepHookState(input);
55
- },
56
- );
34
+ return recordObservation(input, "lifecycle.sweep_observed", {
35
+ subject_instance_id: liveInstanceIdV3(input.owner),
36
+ observation: input.observation,
37
+ provisional: true,
38
+ age_ms: Math.max(0, Math.floor(input.ageMs)),
57
39
  });
58
40
  }
59
41
 
@@ -62,16 +44,11 @@ export function recordLiveResumeObservationV3(
62
44
  ): LiveLifecycleObservationV3Result {
63
45
  const normalized = withAttestedAdapter(input);
64
46
  const hook = requireHookState(normalized);
65
- return recordObservation(
66
- normalized,
67
- "session.resumed",
68
- {
69
- prior_generation_id: hook.generation_id,
70
- continuity: "native",
71
- evidence_reference: "heartbeat_recreated",
72
- },
73
- () => {},
74
- );
47
+ return recordObservation(normalized, "session.resumed", {
48
+ prior_generation_id: hook.generation_id,
49
+ continuity: "native",
50
+ evidence_reference: "heartbeat_recreated",
51
+ });
75
52
  }
76
53
 
77
54
  function recordObservation<T extends "lifecycle.sweep_observed" | "session.resumed">(
@@ -89,7 +66,6 @@ function recordObservation<T extends "lifecycle.sweep_observed" | "session.resum
89
66
  continuity: "native";
90
67
  evidence_reference: string;
91
68
  },
92
- beforeWrite: () => void,
93
69
  ): LiveLifecycleObservationV3Result {
94
70
  const route = resolveLiveEventLedgerRouteV3(input.coordRoot);
95
71
  if (route.state === "blocked") throw new LiveCoordinationAuthorityV3Error(route.reason);
@@ -145,7 +121,6 @@ function recordObservation<T extends "lifecycle.sweep_observed" | "session.resum
145
121
  monotonic_ns: process.hrtime.bigint().toString(),
146
122
  };
147
123
  const event = buildEventV3(eventType, { ...common, payload } as never) as EventV3;
148
- beforeWrite();
149
124
  const durability = writeEventV3(input.coordRoot, event, {
150
125
  expectedGenesisId: control.genesis.event.payload.genesis_id as `gex_${string}`,
151
126
  });
@@ -155,19 +130,6 @@ function recordObservation<T extends "lifecycle.sweep_observed" | "session.resum
155
130
  return { state: "recorded", event };
156
131
  }
157
132
 
158
- function requireSweepHookState(input: LiveSweepObservationV3Input): void {
159
- if (
160
- typeof input.expectedGenerationId !== "string" ||
161
- !/^gen_[a-zA-Z0-9._-]+$/.test(input.expectedGenerationId)
162
- ) {
163
- throw new LiveCoordinationAuthorityV3Error("hook_generation_not_joinable");
164
- }
165
- const hook = requireHookState(withAttestedAdapter(input));
166
- if (hook.generation_id !== input.expectedGenerationId) {
167
- throw new LiveCoordinationAuthorityV3Error("expected_generation_changed");
168
- }
169
- }
170
-
171
133
  function withAttestedAdapter<T extends LiveLifecycleObservationBaseV3>(input: T): T {
172
134
  const adapter = liveCoordinationAdapterV3(input.coordRoot, input.owner);
173
135
  if (!adapter) return input;
@@ -24,11 +24,6 @@ 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";
32
27
  import { canonicalClaimPath } from "../claim-path.ts";
33
28
  import { type Heartbeat, heartbeatPath, readHeartbeat } from "./heartbeat-reader.ts";
34
29
  import type { TaskState } from "./session-state.ts";
@@ -68,15 +63,12 @@ function mutate(
68
63
  instanceId: string,
69
64
  fn: (hb: Heartbeat) => Heartbeat,
70
65
  ): Heartbeat | null {
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
- });
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;
80
72
  }
81
73
 
82
74
  export function setTask(coordRoot: string, instanceId: string, task: string): Heartbeat | null {
@@ -108,15 +100,12 @@ export function setIdentityCache(
108
100
  name: string,
109
101
  agentId: string,
110
102
  ): Heartbeat | null {
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
- });
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;
120
109
  }
121
110
 
122
111
  /** Restore the pool-assigned display name on a generation that was
@@ -339,12 +328,6 @@ export function clearCoordinationCache(coordRoot: string, instanceId: string): b
339
328
  }
340
329
 
341
330
  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 {
348
331
  const dir = join(coordRoot, ".harnery", "pid-map");
349
332
  mkdirSync(dir, { recursive: true });
350
333
  const hb = readHeartbeat(coordRoot, instanceId);
@@ -5,10 +5,6 @@ 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";
12
8
  import { liveInstanceIdV3, resolveLiveEventLedgerRouteV3 } from "../../events/v3/live-routing.ts";
13
9
  import { type Heartbeat, readHeartbeat } from "./heartbeat-reader.ts";
14
10
  import {
@@ -68,26 +64,6 @@ function materializeLiveCoordinationHeartbeat(
68
64
  _adapter: Adapter,
69
65
  model: string | undefined,
70
66
  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,
91
67
  ): Heartbeat | null {
92
68
  const route = resolveLiveEventLedgerRouteV3(coordRoot);
93
69
  if (route.state === "blocked") throw new Error(`event_v3_coordination_view:${route.reason}`);
@@ -157,11 +133,6 @@ function materializeHeartbeatUnderCustody(
157
133
  v3_projection_event_id: generation.last_event_id,
158
134
  v3_task_state: generation.task_state === "set" ? "set" : "cleared",
159
135
  };
160
- assertCurrentGenerationV3(
161
- coordRoot,
162
- liveInstanceIdV3(nativeInstanceId),
163
- generation.generation_id,
164
- );
165
136
  writeHeartbeatCache(coordRoot, nativeInstanceId, materialized);
166
137
  return materialized;
167
138
  }
@@ -24,9 +24,7 @@ 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";
29
- import { checkPidToken, processStartToken } from "./proc-start.ts";
27
+ import { processStartToken, verifyProcessGeneration } from "./proc-start.ts";
30
28
 
31
29
  /**
32
30
  * Row count that triggers a dead-row sweep on the next write.
@@ -61,7 +59,7 @@ export function parsePidmapRow(row: string): {
61
59
  */
62
60
  function rowStillNamesItsProcess(pid: number, startToken: string | undefined): boolean {
63
61
  if (!pidIsAlive(pid)) return false;
64
- return checkPidToken(pid, startToken) !== "mismatch";
62
+ return verifyProcessGeneration(pid, startToken) !== "different";
65
63
  }
66
64
 
67
65
  function atomicWrite(path: string, content: string): void {
@@ -128,17 +126,6 @@ export function writePidmapRow(
128
126
  pid: number,
129
127
  instanceId: string,
130
128
  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,
142
129
  ): void {
143
130
  const dir = join(coordRoot, ".harnery", "pid-map");
144
131
  const path = join(dir, String(pid));
@@ -34,10 +34,9 @@
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";
38
37
 
39
38
  /** Which probe answers "when did this process start?" on this machine. */
40
- export type StartTokenProbe = "procfs" | "ps" | "windows";
39
+ export type StartTokenProbe = "procfs" | "ps";
41
40
 
42
41
  /**
43
42
  * The `ps` probe is text formatted by `strftime("%c", localtime(...))`, so its
@@ -61,7 +60,6 @@ let cachedBootId: string | null | undefined;
61
60
  export function startTokenProbe(): StartTokenProbe {
62
61
  const override = coordEnv("PID_PROBE");
63
62
  if (override === "procfs" || override === "ps") return override;
64
- if (process.platform === "win32") return "windows";
65
63
  if (cachedProbe === undefined) {
66
64
  cachedProbe = existsSync("/proc/self/stat") ? "procfs" : "ps";
67
65
  }
@@ -104,7 +102,7 @@ export function resetStartTokenCaches(): void {
104
102
  * than by splitting the whole line, because comm is the executable name in
105
103
  * parentheses and may itself contain spaces and parens. The boot segment is
106
104
  * dropped rather than faked when the kernel will not name its boot, which
107
- * yields the pre-boot-id shape that `tokensAgree` still accepts.
105
+ * yields the pre-boot-id shape that `verifyProcessGeneration` compares by ticks.
108
106
  */
109
107
  export function procfsStartToken(pid: number): string | null {
110
108
  let stat: string;
@@ -159,19 +157,26 @@ function defaultPsRun(pid: number): { status: number | null; stdout: string } {
159
157
  */
160
158
  export function processStartToken(pid: number): string | null {
161
159
  if (!Number.isFinite(pid) || pid <= 0) return null;
162
- const probe = startTokenProbe();
163
- return probe === "windows"
164
- ? windowsProcessStartToken(pid)
165
- : probe === "procfs"
166
- ? procfsStartToken(pid)
167
- : psStartToken(pid);
160
+ return startTokenProbe() === "procfs" ? procfsStartToken(pid) : psStartToken(pid);
168
161
  }
169
162
 
170
163
  export type ProcessGenerationVerdict = "same" | "different" | "unknown";
171
164
 
172
165
  type ReadProcessStartToken = (pid: number) => string | null;
173
166
 
174
- /** A missing or unreadable token never establishes process identity. */
167
+ /**
168
+ * Does `pid` still name the run that wrote `recordedToken`?
169
+ *
170
+ * - `"same"`: both tokens present and equal.
171
+ * - `"different"`: the pid was recycled. A liveness check cannot see this,
172
+ * because a recycled pid is alive.
173
+ * - `"unknown"`: a token is missing, malformed, or unreadable. Never treat
174
+ * this as proof of identity.
175
+ *
176
+ * A Linux token written without a boot segment cannot identify a run across
177
+ * reboots, so it is compared on its tick count: a different count proves a
178
+ * different run, and an equal count stays unknown.
179
+ */
175
180
  export function verifyProcessGeneration(
176
181
  pid: number,
177
182
  recordedToken: string | null | undefined,
@@ -184,26 +189,38 @@ export function verifyProcessGeneration(
184
189
  !recordedToken.trim()
185
190
  )
186
191
  return "unknown";
187
- // A bootless Linux token cannot identify a process across machine boots.
188
- if (/^l\d+$/.test(recordedToken)) return "unknown";
192
+ let currentToken: string | null;
189
193
  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
+ currentToken = readToken(pid);
194
195
  } catch {
195
196
  return "unknown";
196
197
  }
198
+ if (typeof currentToken !== "string" || !currentToken.trim()) return "unknown";
199
+ const bootless = (token: string) => /^l\d+$/.test(token);
200
+ if (
201
+ recordedToken[0] === "l" &&
202
+ currentToken[0] === "l" &&
203
+ (bootless(recordedToken) || bootless(currentToken))
204
+ )
205
+ return ticksOf(recordedToken) === ticksOf(currentToken) ? "unknown" : "different";
206
+ return currentToken === recordedToken ? "same" : "different";
207
+ }
208
+
209
+ function ticksOf(token: string): string {
210
+ const dot = token.indexOf(".");
211
+ return dot === -1 ? token.slice(1) : token.slice(dot + 1);
197
212
  }
198
213
 
199
214
  export interface ProcessGenerationSignalDependencies {
200
215
  readToken?: ReadProcessStartToken;
201
- signalProcess?: typeof signalProcessWithHandle;
216
+ kill?: (pid: number, signal: NodeJS.Signals | number) => unknown;
202
217
  }
203
218
 
204
219
  /**
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.
220
+ * Recheck the process generation immediately before sending the signal.
221
+ *
222
+ * The process can still exit and its PID can be reused between the token read
223
+ * and the signal. Eliminating that race requires a generation-bound OS handle.
207
224
  */
208
225
  export function signalIfSameGeneration(
209
226
  pid: number,
@@ -211,58 +228,13 @@ export function signalIfSameGeneration(
211
228
  signal: NodeJS.Signals | number,
212
229
  dependencies: ProcessGenerationSignalDependencies = {},
213
230
  ): { sent: boolean; verdict: ProcessGenerationVerdict } {
231
+ const kill = dependencies.kill ?? process.kill;
214
232
  const verdict = verifyProcessGeneration(pid, recordedToken, dependencies.readToken);
215
233
  if (verdict !== "same") return { sent: false, verdict };
216
234
  try {
217
- return (dependencies.signalProcess ?? signalProcessWithHandle)(pid, recordedToken!, signal);
235
+ kill(pid, signal);
236
+ return { sent: true, verdict };
218
237
  } catch {
219
- return { sent: false, verdict: "unknown" };
238
+ return { sent: false, verdict };
220
239
  }
221
240
  }
222
-
223
- /**
224
- * Do two tokens describe the same run?
225
- *
226
- * Equality, plus one concession: a row written before tokens carried a boot
227
- * segment recorded the tick count alone, so it is compared on the part it
228
- * actually recorded. Refusing that would call every pre-upgrade row a recycled
229
- * pid and prune a working machine's live rows the first time the new code ran.
230
- * Such a row loses its reboot protection until the next write rewrites it,
231
- * which is a strictly better position than the one it came from.
232
- */
233
- export function tokensAgree(recorded: string, current: string): boolean {
234
- if (recorded === current) return true;
235
- // Only the Linux form has a boot segment to disagree about, and only when
236
- // exactly one side carries one is this a shape difference rather than two
237
- // genuinely different start instants.
238
- if (recorded[0] !== "l" || current[0] !== "l") return false;
239
- if (recorded.includes(".") === current.includes(".")) return false;
240
- return ticksOf(recorded) === ticksOf(current);
241
- }
242
-
243
- function ticksOf(token: string): string {
244
- const dot = token.indexOf(".");
245
- return dot === -1 ? token.slice(1) : token.slice(dot + 1);
246
- }
247
-
248
- /**
249
- * Does `pid` still name the process a row recorded?
250
- *
251
- * Three answers, and the difference between the last two matters:
252
- *
253
- * - `"match"` — recorded token, current token, same run. The row is trustworthy.
254
- * - `"mismatch"` — both present and different. The pid was recycled. This is
255
- * the case a liveness check cannot see, because a recycled pid is alive.
256
- * - `"unverified"` — no recorded token (a row written before tokens existed) or
257
- * no current one (an unsupported platform). Callers treat this the way they
258
- * treated every row previously: trust it if the pid is alive.
259
- */
260
- export function checkPidToken(
261
- pid: number,
262
- recorded: string | undefined,
263
- ): "match" | "mismatch" | "unverified" {
264
- if (!recorded) return "unverified";
265
- const current = processStartToken(pid);
266
- if (!current) return "unverified";
267
- return tokensAgree(recorded, current) ? "match" : "mismatch";
268
- }