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.
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +44 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +18 -0
- package/dist/core/agents/live-authority-v3.d.ts +0 -1
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +0 -11
- package/dist/core/agents/live-lifecycle-v3.d.ts +2 -4
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/live-lifecycle-v3.js +7 -28
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +14 -25
- package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-writer.js +0 -5
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +0 -5
- package/dist/core/agents/state/proc-start.d.ts +5 -6
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +13 -9
- package/dist/core/agents/state/process-handle.d.ts +26 -0
- package/dist/core/agents/state/process-handle.d.ts.map +1 -0
- package/dist/core/agents/state/process-handle.js +121 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +70 -180
- package/dist/core/artifacts/activity.d.ts +4 -1
- package/dist/core/artifacts/activity.d.ts.map +1 -1
- package/dist/core/artifacts/activity.js +8 -2
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +38 -12
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +0 -29
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +0 -1
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +0 -7
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +64 -77
- package/dist/core/hooks/cli.js +78 -48
- package/dist/core/resources/service.d.ts +2 -16
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +1 -78
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +2 -9
- package/dist/core/servers/index.d.ts +4 -0
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +21 -3
- package/dist/core/storage/durable-history.js +56 -22
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +73 -17
- package/dist/core/supervisor/service.d.ts +2 -4
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/work/state.js +7 -7
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +21 -65
- package/dist/core/workflow/index.d.ts +4 -2
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -1
- package/dist/core/workflow/live-session-v3.d.ts +0 -6
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +5 -126
- package/dist/core/workflow/proof.d.ts +4 -4
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +4 -12
- package/dist/core/workflow/recovery-audit.d.ts +30 -0
- package/dist/core/workflow/recovery-audit.d.ts.map +1 -0
- package/dist/core/workflow/recovery-audit.js +116 -0
- package/dist/core/workflow/run-state.d.ts +19 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +113 -8
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +0 -19
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +0 -13
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +0 -19
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +0 -14
- package/dist/core/workflow/types.d.ts +11 -54
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +70 -14
- package/dist/core/workflow/workspaces/reclaim.d.ts +6 -1
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.d.ts +2 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +24 -0
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +0 -4
- package/package.json +1 -1
- package/src/commands/workflow.ts +59 -0
- package/src/core/agents/coord-client.ts +20 -0
- package/src/core/agents/live-authority-v3.ts +0 -17
- package/src/core/agents/live-lifecycle-v3.ts +14 -52
- package/src/core/agents/state/heartbeat-writer.ts +12 -29
- package/src/core/agents/state/live-coordination-writer.ts +0 -29
- package/src/core/agents/state/pidmap.ts +0 -13
- package/src/core/agents/state/proc-start.ts +14 -11
- package/src/core/agents/state/process-handle.ts +165 -0
- package/src/core/agents/state/stale-sweep.ts +82 -247
- package/src/core/artifacts/activity.ts +9 -2
- package/src/core/artifacts/index.ts +45 -12
- package/src/core/events/v3/authority-outbox.ts +0 -56
- package/src/core/events/v3/producers/coordination-recorder.ts +0 -15
- package/src/core/events/v3/producers/recorder.ts +68 -94
- package/src/core/hooks/cli.ts +85 -47
- package/src/core/resources/service.ts +3 -88
- package/src/core/semantic/service.ts +2 -8
- package/src/core/servers/index.ts +30 -3
- package/src/core/storage/durable-history.ts +52 -20
- package/src/core/storage/segments.ts +79 -17
- package/src/core/supervisor/service.ts +2 -3
- package/src/core/work/state.ts +7 -7
- package/src/core/workflow/engine.ts +21 -73
- package/src/core/workflow/index.ts +12 -1
- package/src/core/workflow/live-session-v3.ts +5 -146
- package/src/core/workflow/proof.ts +5 -21
- package/src/core/workflow/recovery-audit.ts +181 -0
- package/src/core/workflow/run-state.ts +137 -8
- package/src/core/workflow/spawn-claude.ts +0 -18
- package/src/core/workflow/spawn-codex.ts +0 -13
- package/src/core/workflow/spawn-cursor.ts +0 -18
- package/src/core/workflow/spawn-opencode.ts +0 -14
- package/src/core/workflow/types.ts +11 -41
- package/src/core/workflow/workspaces/local-git.ts +86 -15
- package/src/core/workflow/workspaces/reclaim.ts +4 -0
- package/src/core/workflow/workspaces/recovery.ts +23 -0
- package/src/lib/identities/assume.ts +0 -4
- package/dist/core/agents/state/unchanged-file.d.ts +0 -44
- package/dist/core/agents/state/unchanged-file.d.ts.map +0 -1
- package/dist/core/agents/state/unchanged-file.js +0 -390
- package/dist/core/events/v3/generation-custody.d.ts +0 -5
- package/dist/core/events/v3/generation-custody.d.ts.map +0 -1
- package/dist/core/events/v3/generation-custody.js +0 -50
- package/dist/core/workflow/failure-evidence.d.ts +0 -16
- package/dist/core/workflow/failure-evidence.d.ts.map +0 -1
- package/dist/core/workflow/failure-evidence.js +0 -114
- package/src/core/agents/state/unchanged-file.ts +0 -456
- package/src/core/events/v3/generation-custody.ts +0 -59
- 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
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
-
|
|
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;
|
|
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,
|
|
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
package/src/commands/workflow.ts
CHANGED
|
@@ -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:
|
|
29
|
+
input: LiveLifecycleObservationBaseV3 & {
|
|
30
|
+
observation: "stale_heartbeat" | "killed" | "unparseable_heartbeat" | "missing_timestamp";
|
|
31
|
+
ageMs: number;
|
|
32
|
+
},
|
|
37
33
|
): LiveLifecycleObservationV3Result {
|
|
38
|
-
input
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
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;
|