harnery 0.44.0 → 0.45.1

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 (138) hide show
  1. package/dist/commands/workflow.d.ts.map +1 -1
  2. package/dist/commands/workflow.js +44 -0
  3. package/dist/core/agents/coord-client.d.ts.map +1 -1
  4. package/dist/core/agents/coord-client.js +18 -0
  5. package/dist/core/agents/live-authority-v3.d.ts +0 -1
  6. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  7. package/dist/core/agents/live-authority-v3.js +0 -11
  8. package/dist/core/agents/live-lifecycle-v3.d.ts +2 -4
  9. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  10. package/dist/core/agents/live-lifecycle-v3.js +7 -28
  11. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  12. package/dist/core/agents/state/heartbeat-writer.js +14 -25
  13. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  14. package/dist/core/agents/state/live-coordination-writer.js +0 -5
  15. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  16. package/dist/core/agents/state/pidmap.js +0 -5
  17. package/dist/core/agents/state/proc-start.d.ts +5 -6
  18. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  19. package/dist/core/agents/state/proc-start.js +13 -9
  20. package/dist/core/agents/state/process-handle.d.ts +26 -0
  21. package/dist/core/agents/state/process-handle.d.ts.map +1 -0
  22. package/dist/core/agents/state/process-handle.js +121 -0
  23. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  24. package/dist/core/agents/state/stale-sweep.js +70 -180
  25. package/dist/core/artifacts/activity.d.ts +4 -1
  26. package/dist/core/artifacts/activity.d.ts.map +1 -1
  27. package/dist/core/artifacts/activity.js +8 -2
  28. package/dist/core/artifacts/index.d.ts.map +1 -1
  29. package/dist/core/artifacts/index.js +38 -12
  30. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  31. package/dist/core/events/v3/authority-outbox.js +0 -29
  32. package/dist/core/events/v3/producers/coordination-recorder.d.ts +0 -1
  33. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  34. package/dist/core/events/v3/producers/coordination-recorder.js +0 -7
  35. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  36. package/dist/core/events/v3/producers/recorder.js +64 -77
  37. package/dist/core/hooks/cli.js +78 -48
  38. package/dist/core/resources/service.d.ts +2 -16
  39. package/dist/core/resources/service.d.ts.map +1 -1
  40. package/dist/core/resources/service.js +1 -78
  41. package/dist/core/semantic/service.d.ts.map +1 -1
  42. package/dist/core/semantic/service.js +2 -9
  43. package/dist/core/servers/index.d.ts +4 -0
  44. package/dist/core/servers/index.d.ts.map +1 -1
  45. package/dist/core/servers/index.js +21 -3
  46. package/dist/core/storage/durable-history.js +56 -22
  47. package/dist/core/storage/segments.d.ts.map +1 -1
  48. package/dist/core/storage/segments.js +73 -17
  49. package/dist/core/supervisor/service.d.ts +2 -4
  50. package/dist/core/supervisor/service.d.ts.map +1 -1
  51. package/dist/core/work/state.js +7 -7
  52. package/dist/core/workflow/engine.d.ts.map +1 -1
  53. package/dist/core/workflow/engine.js +21 -65
  54. package/dist/core/workflow/index.d.ts +4 -2
  55. package/dist/core/workflow/index.d.ts.map +1 -1
  56. package/dist/core/workflow/index.js +2 -1
  57. package/dist/core/workflow/live-session-v3.d.ts +0 -6
  58. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  59. package/dist/core/workflow/live-session-v3.js +5 -126
  60. package/dist/core/workflow/proof.d.ts +4 -4
  61. package/dist/core/workflow/proof.d.ts.map +1 -1
  62. package/dist/core/workflow/proof.js +4 -12
  63. package/dist/core/workflow/recovery-audit.d.ts +30 -0
  64. package/dist/core/workflow/recovery-audit.d.ts.map +1 -0
  65. package/dist/core/workflow/recovery-audit.js +116 -0
  66. package/dist/core/workflow/run-state.d.ts +19 -2
  67. package/dist/core/workflow/run-state.d.ts.map +1 -1
  68. package/dist/core/workflow/run-state.js +113 -8
  69. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  70. package/dist/core/workflow/spawn-claude.js +0 -19
  71. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  72. package/dist/core/workflow/spawn-codex.js +0 -13
  73. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  74. package/dist/core/workflow/spawn-cursor.js +0 -19
  75. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  76. package/dist/core/workflow/spawn-opencode.js +0 -14
  77. package/dist/core/workflow/types.d.ts +11 -54
  78. package/dist/core/workflow/types.d.ts.map +1 -1
  79. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  80. package/dist/core/workflow/workspaces/local-git.js +70 -14
  81. package/dist/core/workflow/workspaces/reclaim.d.ts +6 -1
  82. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  83. package/dist/core/workflow/workspaces/recovery.d.ts +2 -0
  84. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  85. package/dist/core/workflow/workspaces/recovery.js +24 -0
  86. package/dist/lib/identities/assume.d.ts.map +1 -1
  87. package/dist/lib/identities/assume.js +0 -4
  88. package/package.json +1 -1
  89. package/src/commands/workflow.ts +59 -0
  90. package/src/core/agents/coord-client.ts +20 -0
  91. package/src/core/agents/live-authority-v3.ts +0 -17
  92. package/src/core/agents/live-lifecycle-v3.ts +14 -52
  93. package/src/core/agents/state/heartbeat-writer.ts +12 -29
  94. package/src/core/agents/state/live-coordination-writer.ts +0 -29
  95. package/src/core/agents/state/pidmap.ts +0 -13
  96. package/src/core/agents/state/proc-start.ts +14 -11
  97. package/src/core/agents/state/process-handle.ts +165 -0
  98. package/src/core/agents/state/stale-sweep.ts +82 -247
  99. package/src/core/artifacts/activity.ts +9 -2
  100. package/src/core/artifacts/index.ts +45 -12
  101. package/src/core/events/v3/authority-outbox.ts +0 -56
  102. package/src/core/events/v3/producers/coordination-recorder.ts +0 -15
  103. package/src/core/events/v3/producers/recorder.ts +68 -94
  104. package/src/core/hooks/cli.ts +85 -47
  105. package/src/core/resources/service.ts +3 -88
  106. package/src/core/semantic/service.ts +2 -8
  107. package/src/core/servers/index.ts +30 -3
  108. package/src/core/storage/durable-history.ts +52 -20
  109. package/src/core/storage/segments.ts +79 -17
  110. package/src/core/supervisor/service.ts +2 -3
  111. package/src/core/work/state.ts +7 -7
  112. package/src/core/workflow/engine.ts +21 -73
  113. package/src/core/workflow/index.ts +12 -1
  114. package/src/core/workflow/live-session-v3.ts +5 -146
  115. package/src/core/workflow/proof.ts +5 -21
  116. package/src/core/workflow/recovery-audit.ts +181 -0
  117. package/src/core/workflow/run-state.ts +137 -8
  118. package/src/core/workflow/spawn-claude.ts +0 -18
  119. package/src/core/workflow/spawn-codex.ts +0 -13
  120. package/src/core/workflow/spawn-cursor.ts +0 -18
  121. package/src/core/workflow/spawn-opencode.ts +0 -14
  122. package/src/core/workflow/types.ts +11 -41
  123. package/src/core/workflow/workspaces/local-git.ts +86 -15
  124. package/src/core/workflow/workspaces/reclaim.ts +4 -0
  125. package/src/core/workflow/workspaces/recovery.ts +23 -0
  126. package/src/lib/identities/assume.ts +0 -4
  127. package/dist/core/agents/state/unchanged-file.d.ts +0 -44
  128. package/dist/core/agents/state/unchanged-file.d.ts.map +0 -1
  129. package/dist/core/agents/state/unchanged-file.js +0 -390
  130. package/dist/core/events/v3/generation-custody.d.ts +0 -5
  131. package/dist/core/events/v3/generation-custody.d.ts.map +0 -1
  132. package/dist/core/events/v3/generation-custody.js +0 -50
  133. package/dist/core/workflow/failure-evidence.d.ts +0 -16
  134. package/dist/core/workflow/failure-evidence.d.ts.map +0 -1
  135. package/dist/core/workflow/failure-evidence.js +0 -114
  136. package/src/core/agents/state/unchanged-file.ts +0 -456
  137. package/src/core/events/v3/generation-custody.ts +0 -59
  138. package/src/core/workflow/failure-evidence.ts +0 -141
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
2
2
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from "node:fs";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { stateDirMode, stateFileMode } from "../../storage/modes.js";
5
+ import { beginWorkflowRecovery, validateWorkflowRecoveryAuthorization } from "../recovery-audit.js";
5
6
  import { acquireWorkflowResumeLease } from "../run-state.js";
6
7
  import { WorkspaceAttestationError } from "./attestation-error.js";
7
8
  import { cleanupWorkspace } from "./cleanup.js";
@@ -10,7 +11,7 @@ import { acquireNoClobberLease } from "./leases.js";
10
11
  import { claimOverlapsWorkspace, reserveWorkspaceReclaim, } from "./ownership.js";
11
12
  import { assertContainedExisting, assertPathIdentity, candidateUnderRoot, containsPath, createContainedDirectories, descriptorBackedPathsSupported, descriptorPath, filesystemIdentity, openContainedDirectory, sameFilesystemIdentity, validateConfiguredRoot, } from "./paths.js";
12
13
  import { prepareReclaim, WorkflowReclaimError, } from "./reclaim.js";
13
- import { readWorkflowLiveness } from "./recovery.js";
14
+ import { hasActiveWorkflowEvidence, readWorkflowLiveness } from "./recovery.js";
14
15
  import { appendWorkspaceEvent, fileSha256, readWorkflowSupplement, readWorkspaceBinding, readWorkspaceClaim, readWorkspaceClaims, readWorkspaceEvents, stableDigest, workspaceClaimPath, workspaceLockPath, writeWorkspaceBinding, writeWorkspaceClaim, } from "./state.js";
15
16
  import { isWorkspaceAttestation } from "./validate.js";
16
17
  const PROVIDER_ID = "local-git-worktree";
@@ -597,6 +598,7 @@ function reclaimPreview(coordRoot, binding, mode) {
597
598
  run_id: binding.run_id,
598
599
  mode,
599
600
  liveness: readWorkflowLiveness(coordRoot, binding.run_id),
601
+ evidence_sha256: reclaimEvidenceDigest(coordRoot, binding),
600
602
  ownership: "unproven",
601
603
  dirty_paths: null,
602
604
  lease_holder: reclaimLeaseHolders(coordRoot, binding),
@@ -617,13 +619,36 @@ function reclaimPreview(coordRoot, binding, mode) {
617
619
  }
618
620
  return preview;
619
621
  }
620
- function assertReclaimLiveness(coordRoot, binding) {
622
+ function reclaimEvidenceDigest(coordRoot, binding) {
623
+ const evidence = (name) => {
624
+ try {
625
+ return readFileSync(join(coordRoot, ".harnery", "workflows", binding.run_id, name), "utf8");
626
+ }
627
+ catch (error) {
628
+ if (error.code === "ENOENT")
629
+ return null;
630
+ throw error;
631
+ }
632
+ };
633
+ return stableDigest({
634
+ binding,
635
+ proof: evidence("proof.json"),
636
+ transcript: evidence("transcript.jsonl"),
637
+ });
638
+ }
639
+ function assertReclaimLiveness(coordRoot, binding, recovery) {
621
640
  const liveness = readWorkflowLiveness(coordRoot, binding.run_id);
641
+ if (liveness === "active" || (recovery && hasActiveWorkflowEvidence(coordRoot, binding.run_id))) {
642
+ throw new WorkflowReclaimError("workflow_reclaim_run_live", `run ${binding.run_id} has active evidence; its workspace was left untouched`);
643
+ }
644
+ if (recovery && reclaimEvidenceDigest(coordRoot, binding) !== recovery.evidenceSha256) {
645
+ throw new WorkflowReclaimError("workflow_reclaim_recovery_mismatch", "workflow reclaim requires the exact current preview evidence digest");
646
+ }
622
647
  if (liveness === "inactive")
623
648
  return;
624
- throw new WorkflowReclaimError(liveness === "active" ? "workflow_reclaim_run_live" : "workflow_reclaim_liveness_unknown", liveness === "active"
625
- ? `run ${binding.run_id} is active; its workspace was left untouched`
626
- : `run ${binding.run_id} has missing, corrupt, or ambiguous liveness evidence; its workspace was left untouched`);
649
+ if (liveness === "unknown" && recovery)
650
+ return;
651
+ throw new WorkflowReclaimError("workflow_reclaim_liveness_unknown", `run ${binding.run_id} has missing, corrupt, or ambiguous liveness evidence; its workspace was left untouched`);
627
652
  }
628
653
  function assertReclaimOwnership(coordRoot, binding) {
629
654
  try {
@@ -666,10 +691,10 @@ function assertPeerWorkspaceAuthority(coordRoot, claim) {
666
691
  throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", error.message);
667
692
  }
668
693
  }
669
- function reclaimAuthorityGuard(coordRoot, binding, claim, reservation) {
694
+ function reclaimAuthorityGuard(coordRoot, binding, claim, reservation, recovery) {
670
695
  try {
671
696
  reservation.assertCurrent();
672
- assertReclaimLiveness(coordRoot, binding);
697
+ assertReclaimLiveness(coordRoot, binding, recovery);
673
698
  assertReclaimOwnership(coordRoot, binding);
674
699
  assertPeerWorkspaceAuthority(coordRoot, claim);
675
700
  }
@@ -685,9 +710,11 @@ async function reclaim(coordRoot, provider, binding, options) {
685
710
  }
686
711
  if (!options.yes)
687
712
  return reclaimPreview(coordRoot, binding, options.mode);
713
+ if (options.unknownLiveness)
714
+ validateWorkflowRecoveryAuthorization(options.unknownLiveness);
688
715
  // Liveness comes before terminal-proof cleanup helpers. A live run may not
689
716
  // have a proof yet, and a missing proof must never be discovered after reset.
690
- assertReclaimLiveness(coordRoot, binding);
717
+ assertReclaimLiveness(coordRoot, binding, options.unknownLiveness);
691
718
  const claim = assertReclaimOwnership(coordRoot, binding);
692
719
  let reservation;
693
720
  try {
@@ -697,7 +724,7 @@ async function reclaim(coordRoot, provider, binding, options) {
697
724
  throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
698
725
  }
699
726
  try {
700
- reclaimAuthorityGuard(coordRoot, binding, claim, reservation);
727
+ reclaimAuthorityGuard(coordRoot, binding, claim, reservation, options.unknownLiveness);
701
728
  let releaseResume;
702
729
  try {
703
730
  releaseResume = acquireWorkflowResumeLease(coordRoot, binding.run_id);
@@ -722,9 +749,24 @@ async function reclaim(coordRoot, provider, binding, options) {
722
749
  throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
723
750
  }
724
751
  try {
725
- assertReclaimLiveness(coordRoot, binding);
752
+ assertReclaimLiveness(coordRoot, binding, options.unknownLiveness);
726
753
  assertReclaimOwnership(coordRoot, binding);
727
754
  const preview = reclaimPreview(coordRoot, binding, options.mode);
755
+ const audit = options.unknownLiveness
756
+ ? beginWorkflowRecovery(coordRoot, {
757
+ run_id: binding.run_id,
758
+ action: "reclaim_unknown_liveness",
759
+ actor: options.unknownLiveness.actor,
760
+ reason: options.unknownLiveness.reason,
761
+ target_sha256: options.unknownLiveness.evidenceSha256,
762
+ target: {
763
+ binding_id: binding.binding_id,
764
+ workspace_root: binding.workspace_root,
765
+ mode: options.mode,
766
+ liveness: preview.liveness,
767
+ },
768
+ })
769
+ : undefined;
728
770
  let preparation;
729
771
  // Keep ordinary cleanup's terminal-evidence checks, intent and receipts.
730
772
  // Its provider callback uses the leases already held here instead of
@@ -736,11 +778,22 @@ async function reclaim(coordRoot, provider, binding, options) {
736
778
  provider: {
737
779
  ...provider,
738
780
  cleanup: async (candidate, intent) => {
739
- assertReclaimLiveness(coordRoot, candidate);
781
+ assertReclaimLiveness(coordRoot, candidate, options.unknownLiveness);
740
782
  assertReclaimOwnership(coordRoot, candidate);
741
783
  return cleanupUnderLease(coordRoot, candidate, claim, intent, {
742
784
  mode: options.mode,
743
- beforeEffect: () => reclaimAuthorityGuard(coordRoot, candidate, claim, reservation),
785
+ recovery: options.unknownLiveness,
786
+ beforeEffect: () => {
787
+ audit?.assertCurrent();
788
+ reclaimAuthorityGuard(coordRoot, candidate, claim, reservation, options.unknownLiveness);
789
+ },
790
+ beforeRefEffect: () => {
791
+ audit?.assertCurrent();
792
+ reservation.assertCurrent();
793
+ assertReclaimLiveness(coordRoot, candidate, options.unknownLiveness);
794
+ requiredClaim(coordRoot, candidate);
795
+ assertPeerWorkspaceAuthority(coordRoot, claim);
796
+ },
744
797
  prepared: (value) => {
745
798
  preparation = value;
746
799
  },
@@ -754,6 +807,8 @@ async function reclaim(coordRoot, provider, binding, options) {
754
807
  throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", ("reason" in cleanupResult ? cleanupResult.reason : undefined) ??
755
808
  "workspace cleanup preconditions could not be proved");
756
809
  }
810
+ if (cleanupResult.status === "released" || cleanupResult.status === "already_released")
811
+ audit?.complete();
757
812
  return {
758
813
  ...preview,
759
814
  action: "reclaimed",
@@ -891,7 +946,7 @@ function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest, be
891
946
  };
892
947
  }
893
948
  const liveness = readWorkflowLiveness(coordRoot, binding.run_id);
894
- if (liveness !== "inactive") {
949
+ if (liveness !== "inactive" && !(liveness === "unknown" && reclaimRequest?.recovery)) {
895
950
  return {
896
951
  ...base,
897
952
  status: "blocked",
@@ -962,7 +1017,7 @@ function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest, be
962
1017
  };
963
1018
  }
964
1019
  if (reclaimRequest) {
965
- assertReclaimLiveness(coordRoot, binding);
1020
+ assertReclaimLiveness(coordRoot, binding, reclaimRequest.recovery);
966
1021
  assertReclaimOwnership(coordRoot, binding);
967
1022
  revalidateClaimResources(claim, root);
968
1023
  reclaimRequest.prepared(prepareReclaim({
@@ -1002,6 +1057,7 @@ function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest, be
1002
1057
  return { ...base, status: "blocked", reason: "workspace ref changed during cleanup" };
1003
1058
  }
1004
1059
  revalidateClaimResources(claim, root);
1060
+ reclaimRequest?.beforeRefEffect();
1005
1061
  const deleted = gitMaybe(repository.source_root.realpath, [
1006
1062
  "update-ref",
1007
1063
  "-d",
@@ -14,10 +14,11 @@
14
14
  * always has. So reclaim adds no second removal path that could diverge from the
15
15
  * audited one, and a force-delete of live work exists nowhere in the codebase.
16
16
  */
17
+ import type { WorkflowRecoveryAuthorization } from "../recovery-audit.js";
17
18
  import type { WorkflowLiveness } from "./recovery.js";
18
19
  import type { WorkspaceCleanupAttempt, WorkspaceCleanupReceipt } from "./types.js";
19
20
  export type ReclaimMode = "salvage" | "discard";
20
- export type WorkflowReclaimRefusalCode = "workflow_reclaim_run_live" | "workflow_reclaim_liveness_unknown" | "workflow_reclaim_ownership_unproven" | "workflow_reclaim_lease_held";
21
+ export type WorkflowReclaimRefusalCode = "workflow_reclaim_run_live" | "workflow_reclaim_liveness_unknown" | "workflow_reclaim_recovery_mismatch" | "workflow_reclaim_ownership_unproven" | "workflow_reclaim_lease_held";
21
22
  export declare class WorkflowReclaimError extends Error {
22
23
  readonly code: WorkflowReclaimRefusalCode;
23
24
  constructor(code: WorkflowReclaimRefusalCode, message: string);
@@ -25,6 +26,9 @@ export declare class WorkflowReclaimError extends Error {
25
26
  export interface WorkspaceReclaimOptions {
26
27
  mode: ReclaimMode;
27
28
  yes?: boolean;
29
+ unknownLiveness?: WorkflowRecoveryAuthorization & {
30
+ evidenceSha256: string;
31
+ };
28
32
  }
29
33
  export interface ReclaimLeaseHolder {
30
34
  scope: "binding" | "repository" | "resume";
@@ -38,6 +42,7 @@ export interface WorkspaceReclaimPreview {
38
42
  run_id: string;
39
43
  mode: ReclaimMode;
40
44
  liveness: WorkflowLiveness;
45
+ evidence_sha256: string;
41
46
  ownership: "ok" | "unproven";
42
47
  ownership_detail?: string;
43
48
  dirty_paths: string[] | null;
@@ -1 +1 @@
1
- {"version":3,"file":"reclaim.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/reclaim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAEnF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,CAAC;AAEhD,MAAM,MAAM,0BAA0B,GAClC,2BAA2B,GAC3B,mCAAmC,GACnC,qCAAqC,GACrC,6BAA6B,CAAC;AAElC,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,IAAI,EAAE,0BAA0B;gBAAhC,IAAI,EAAE,0BAA0B,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,SAAS,GAAG,YAAY,GAAG,QAAQ,CAAC;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,SAAS,EAAE,IAAI,GAAG,UAAU,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,kBAAkB,EAAE,CAAC;CACpC;AAED,MAAM,MAAM,sBAAsB,GAC9B,uBAAuB,GACvB,CAAC,IAAI,CAAC,uBAAuB,EAAE,QAAQ,CAAC,GAAG;IACzC,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,kBAAkB,CAAC;IAChC,OAAO,EAAE,uBAAuB,GAAG,uBAAuB,CAAC;CAC5D,CAAC,CAAC;AAEP,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtE;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/C,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,kBAAkB,CAwD7E;AAYD;qEACqE;AACrE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD"}
1
+ {"version":3,"file":"reclaim.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/reclaim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;AAE1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAEnF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,CAAC;AAEhD,MAAM,MAAM,0BAA0B,GAClC,2BAA2B,GAC3B,mCAAmC,GACnC,oCAAoC,GACpC,qCAAqC,GACrC,6BAA6B,CAAC;AAElC,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,IAAI,EAAE,0BAA0B;gBAAhC,IAAI,EAAE,0BAA0B,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,eAAe,CAAC,EAAE,6BAA6B,GAAG;QAAE,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9E;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,SAAS,GAAG,YAAY,GAAG,QAAQ,CAAC;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,IAAI,GAAG,UAAU,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,kBAAkB,EAAE,CAAC;CACpC;AAED,MAAM,MAAM,sBAAsB,GAC9B,uBAAuB,GACvB,CAAC,IAAI,CAAC,uBAAuB,EAAE,QAAQ,CAAC,GAAG;IACzC,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,kBAAkB,CAAC;IAChC,OAAO,EAAE,uBAAuB,GAAG,uBAAuB,CAAC;CAC5D,CAAC,CAAC;AAEP,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtE;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/C,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,kBAAkB,CAwD7E;AAYD;qEACqE;AACrE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD"}
@@ -1,3 +1,5 @@
1
1
  export type WorkflowLiveness = "active" | "inactive" | "unknown";
2
+ /** Corruption must not hide a readable active marker from an audited override. */
3
+ export declare function hasActiveWorkflowEvidence(coordRoot: string, runId: string): boolean;
2
4
  export declare function readWorkflowLiveness(coordRoot: string, runId: string): WorkflowLiveness;
3
5
  //# sourceMappingURL=recovery.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/recovery.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAEjE,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,CA6DvF"}
1
+ {"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/recovery.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAEjE,kFAAkF;AAClF,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBnF;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,CA6DvF"}
@@ -1,6 +1,30 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { readWorkflowProof } from "../proof.js";
4
+ /** Corruption must not hide a readable active marker from an audited override. */
5
+ export function hasActiveWorkflowEvidence(coordRoot, runId) {
6
+ const path = join(resolve(coordRoot), ".harnery", "workflows", runId, "transcript.jsonl");
7
+ if (!existsSync(path))
8
+ return false;
9
+ let active = false;
10
+ for (const line of readFileSync(path, "utf8").split("\n")) {
11
+ try {
12
+ const event = JSON.parse(line);
13
+ if (event?.run_id !== runId)
14
+ continue;
15
+ if (["run.start", "run.resume", "agent.start"].includes(event.event))
16
+ active = true;
17
+ if (event.event === "run.parked" ||
18
+ (event.event === "run.end" && typeof event.ok === "boolean") ||
19
+ (event.event === "workspace.reattach.failed" && ["blocked", "lost"].includes(event.status)))
20
+ active = false;
21
+ }
22
+ catch {
23
+ /* Unreadable evidence cannot establish inactivity. */
24
+ }
25
+ }
26
+ return active;
27
+ }
4
28
  export function readWorkflowLiveness(coordRoot, runId) {
5
29
  const root = resolve(coordRoot);
6
30
  const transcriptPath = join(root, ".harnery", "workflows", runId, "transcript.jsonl");
@@ -1 +1 @@
1
- {"version":3,"file":"assume.d.ts","sourceRoot":"","sources":["../../../src/lib/identities/assume.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA0BH,MAAM,MAAM,uBAAuB,GAC/B,eAAe,GACf,iBAAiB,GACjB,sBAAsB,GACtB,oBAAoB,GACpB,kBAAkB,GAClB,oBAAoB,GACpB,aAAa,GACb,mBAAmB,CAAC;AAExB,qBAAa,mBAAoB,SAAQ,KAAK;aAE1B,IAAI,EAAE,uBAAuB;gBAA7B,IAAI,EAAE,uBAAuB,EAC7C,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,+DAA+D;IAC/D,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAOD;+EAC+E;AAC/E,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,SAAa,EAClB,mBAAmB,GAAE,WAAW,CAAC,MAAM,CAAa,GACnD,gBAAgB,GAAG,IAAI,CA6BzB;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,GACzB,OAAO,CA6CT;AAuED,gEAAgE;AAChE,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,GACjC,oBAAoB,CAyHtB"}
1
+ {"version":3,"file":"assume.d.ts","sourceRoot":"","sources":["../../../src/lib/identities/assume.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA0BH,MAAM,MAAM,uBAAuB,GAC/B,eAAe,GACf,iBAAiB,GACjB,sBAAsB,GACtB,oBAAoB,GACpB,kBAAkB,GAClB,oBAAoB,GACpB,aAAa,GACb,mBAAmB,CAAC;AAExB,qBAAa,mBAAoB,SAAQ,KAAK;aAE1B,IAAI,EAAE,uBAAuB;gBAA7B,IAAI,EAAE,uBAAuB,EAC7C,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,+DAA+D;IAC/D,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAOD;+EAC+E;AAC/E,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,SAAa,EAClB,mBAAmB,GAAE,WAAW,CAAC,MAAM,CAAa,GACnD,gBAAgB,GAAG,IAAI,CA6BzB;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,GACzB,OAAO,CAyCT;AAuED,gEAAgE;AAChE,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,GACjC,oBAAoB,CAyHtB"}
@@ -85,7 +85,6 @@ export function reclaimAbandonedLocalConflict(coordRoot, conflict) {
85
85
  let sessionId = conflict.instance_id;
86
86
  let ageSecs;
87
87
  const liveRow = readLiveCoordinationRow(coordRoot, conflict.instance_id);
88
- let expectedGenerationId = liveRow?.v3_generation_id ?? null;
89
88
  if (liveRow) {
90
89
  adapter = adapterOf(liveRow.platform);
91
90
  sessionId = liveRow.session_id || conflict.instance_id;
@@ -100,10 +99,8 @@ export function reclaimAbandonedLocalConflict(coordRoot, conflict) {
100
99
  // It is required to join the exact producer state when recording the sweep.
101
100
  adapter = adapterOf(hb.platform);
102
101
  sessionId = hb.session_id || conflict.instance_id;
103
- expectedGenerationId = hb.v3_generation_id ?? null;
104
102
  }
105
103
  catch {
106
- expectedGenerationId = null;
107
104
  /* content unreadable — still reclaim the file */
108
105
  }
109
106
  try {
@@ -119,7 +116,6 @@ export function reclaimAbandonedLocalConflict(coordRoot, conflict) {
119
116
  owner: conflict.instance_id,
120
117
  nativeSessionId: sessionId,
121
118
  adapter,
122
- expectedGenerationId,
123
119
  observation: "stale_heartbeat",
124
120
  ageMs: Math.max(0, (ageSecs ?? 0) * 1_000),
125
121
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnery",
3
- "version": "0.44.0",
3
+ "version": "0.45.1",
4
4
  "description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
5
5
  "license": "MIT",
6
6
  "author": "Ryan Kelly",
@@ -73,6 +73,15 @@ 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
85
  }
77
86
 
78
87
  export function registerWorkflowCommand(program: Command, emit: EmitContext): void {
@@ -622,6 +631,12 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
622
631
  )
623
632
  .option("--yes", "Confirm that this commits or discards uncommitted work, then releases")
624
633
  .option("--discard", "Throw the uncommitted work away instead of salvaging it")
634
+ .option(
635
+ "--unknown-liveness <sha256>",
636
+ "Recover unknown evidence pinned by the preview digest; active evidence still refuses",
637
+ )
638
+ .option("--actor <id>", "Actor responsible for an audited recovery")
639
+ .option("--reason <text>", "Reason for an audited recovery")
625
640
  .option("--json", "Emit the reclaim preview or confirmed result as JSON")
626
641
  .action(async (runId: string, opts: WorkflowReclaimOpts) => {
627
642
  const coordRoot = resolveCoordRoot();
@@ -648,6 +663,13 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
648
663
  const result = await provider.reclaim(binding, {
649
664
  mode: opts.discard ? "discard" : "salvage",
650
665
  yes: opts.yes,
666
+ unknownLiveness: opts.unknownLiveness
667
+ ? {
668
+ evidenceSha256: opts.unknownLiveness,
669
+ actor: opts.actor!,
670
+ reason: opts.reason!,
671
+ }
672
+ : undefined,
651
673
  });
652
674
  if (opts.json) {
653
675
  emit.config({ format: "json" });
@@ -662,6 +684,7 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
662
684
  emit.text(
663
685
  `reclaim preview: ${result.run_id}\n` +
664
686
  `mode: ${result.mode}\nliveness: ${result.liveness}\nownership: ${result.ownership}\n` +
687
+ `evidence sha256: ${result.evidence_sha256}\n` +
665
688
  `lease holder: ${holders.length > 0 ? holders.join(", ") : "none observed"}\n` +
666
689
  `dirty paths: ${result.dirty_paths === null ? "unknown" : result.dirty_paths.length > 0 ? result.dirty_paths.join(", ") : "none"}\n` +
667
690
  (result.ownership_detail ? `ownership detail: ${result.ownership_detail}\n` : "") +
@@ -685,6 +708,42 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
685
708
  }
686
709
  });
687
710
 
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
+
688
747
  const approvals = program
689
748
  .command("approval")
690
749
  .description("Inspect and resolve durable run-policy approvals.");
@@ -265,7 +265,27 @@ 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;
268
269
  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
+ }
269
289
  const useProcfs = forced === "procfs" || (forced !== "ps" && existsSync("/proc/self/stat"));
270
290
  // One machine, one probe. Falling back to the other on a read failure would
271
291
  // 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;