harnery 0.45.1 → 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.
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +6 -45
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +0 -18
- package/dist/core/agents/state/pidmap.js +2 -2
- package/dist/core/agents/state/proc-start.d.ts +20 -31
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +37 -70
- package/dist/core/agents/state/service-process.d.ts +61 -0
- package/dist/core/agents/state/service-process.d.ts.map +1 -0
- package/dist/core/agents/state/service-process.js +143 -0
- package/dist/core/context/index.d.ts +6 -4
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +194 -256
- package/dist/core/context/observation.d.ts +9 -9
- package/dist/core/context/observation.d.ts.map +1 -1
- package/dist/core/context/observation.js +37 -6
- package/dist/core/events/v3/runtime-build.d.ts +14 -0
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +44 -0
- package/dist/core/governor/index.d.ts +2 -1
- package/dist/core/governor/index.d.ts.map +1 -1
- package/dist/core/governor/index.js +2 -1
- package/dist/core/governor/service-read.d.ts +3 -10
- package/dist/core/governor/service-read.d.ts.map +1 -1
- package/dist/core/governor/service-read.js +10 -94
- package/dist/core/governor/service.d.ts +3 -7
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +17 -182
- package/dist/core/hooks/resolve/owner.js +2 -2
- package/dist/core/resources/sampler.js +2 -2
- package/dist/core/resources/service-status.d.ts +2 -14
- package/dist/core/resources/service-status.d.ts.map +1 -1
- package/dist/core/resources/service-status.js +3 -23
- package/dist/core/resources/service.d.ts +3 -7
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +12 -101
- package/dist/core/resources/status.d.ts.map +1 -1
- package/dist/core/resources/status.js +3 -2
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +9 -2
- package/dist/core/servers/index.d.ts +0 -2
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +3 -18
- package/dist/core/supervisor/service.d.ts +4 -7
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +23 -122
- package/dist/core/supervisor/services.js +2 -2
- package/dist/core/supervisor/status.d.ts +3 -17
- package/dist/core/supervisor/status.d.ts.map +1 -1
- package/dist/core/supervisor/status.js +7 -70
- package/dist/core/workflow/engine.js +2 -1
- package/dist/core/workflow/index.d.ts +3 -4
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -2
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +30 -74
- package/dist/core/workflow/resume-lock.d.ts +17 -0
- package/dist/core/workflow/resume-lock.d.ts.map +1 -0
- package/dist/core/workflow/resume-lock.js +158 -0
- package/dist/core/workflow/run-state.d.ts +0 -21
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +3 -174
- package/dist/core/workflow/workspaces/leases.d.ts +4 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +1 -1
- package/dist/core/workflow/workspaces/local-git.js +91 -210
- package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
- package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/ownership.js +3 -130
- package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/reclaim.js +0 -4
- package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +1 -1
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +1 -4
- package/package.json +1 -1
- package/src/commands/workflow.ts +8 -58
- package/src/core/agents/coord-client.ts +0 -20
- package/src/core/agents/state/pidmap.ts +2 -2
- package/src/core/agents/state/proc-start.ts +41 -69
- package/src/core/agents/state/service-process.ts +217 -0
- package/src/core/context/index.ts +207 -272
- package/src/core/context/observation.ts +40 -9
- package/src/core/events/v3/runtime-build.ts +62 -0
- package/src/core/governor/index.ts +1 -2
- package/src/core/governor/service-read.ts +15 -114
- package/src/core/governor/service.ts +36 -220
- package/src/core/hooks/resolve/owner.ts +2 -2
- package/src/core/resources/sampler.ts +3 -2
- package/src/core/resources/service-status.ts +6 -35
- package/src/core/resources/service.ts +23 -133
- package/src/core/resources/status.ts +3 -2
- package/src/core/semantic/service.ts +8 -2
- package/src/core/servers/index.ts +3 -23
- package/src/core/supervisor/service.ts +33 -155
- package/src/core/supervisor/services.ts +2 -2
- package/src/core/supervisor/status.ts +10 -85
- package/src/core/workflow/engine.ts +1 -1
- package/src/core/workflow/index.ts +2 -13
- package/src/core/workflow/proof.ts +35 -83
- package/src/core/workflow/resume-lock.ts +179 -0
- package/src/core/workflow/run-state.ts +3 -207
- package/src/core/workflow/workspaces/leases.ts +1 -1
- package/src/core/workflow/workspaces/local-git.ts +120 -239
- package/src/core/workflow/workspaces/ownership.ts +3 -158
- package/src/core/workflow/workspaces/reclaim.ts +3 -9
- package/src/core/workflow/workspaces/recovery.ts +1 -1
- package/src/core/workflow/workspaces/state.ts +1 -5
- package/dist/core/agents/state/process-handle.d.ts +0 -26
- package/dist/core/agents/state/process-handle.d.ts.map +0 -1
- package/dist/core/agents/state/process-handle.js +0 -121
- package/dist/core/workflow/recovery-audit.d.ts +0 -30
- package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
- package/dist/core/workflow/recovery-audit.js +0 -116
- package/src/core/agents/state/process-handle.ts +0 -165
- package/src/core/workflow/recovery-audit.ts +0 -181
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { hostname } from "node:os";
|
|
3
4
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
5
|
import { stateDirMode, stateFileMode } from "../../storage/modes.js";
|
|
5
|
-
import {
|
|
6
|
-
import { acquireWorkflowResumeLease } from "../run-state.js";
|
|
6
|
+
import { acquireWorkflowResumeLease, readWorkflowResumeLock, workflowResumeLockPath, } from "../resume-lock.js";
|
|
7
7
|
import { WorkspaceAttestationError } from "./attestation-error.js";
|
|
8
8
|
import { cleanupWorkspace } from "./cleanup.js";
|
|
9
9
|
import { git, gitMaybe, gitMaybeWithDirectoryDescriptor, gitOperations, isAncestor, nulEntries, overwriteRiskPaths, parseWorktreeInventory, worktreeInventory, } from "./git.js";
|
|
10
|
-
import { acquireNoClobberLease } from "./leases.js";
|
|
11
|
-
import { claimOverlapsWorkspace
|
|
10
|
+
import { acquireNoClobberLease, readOwner } from "./leases.js";
|
|
11
|
+
import { claimOverlapsWorkspace } from "./ownership.js";
|
|
12
12
|
import { assertContainedExisting, assertPathIdentity, candidateUnderRoot, containsPath, createContainedDirectories, descriptorBackedPathsSupported, descriptorPath, filesystemIdentity, openContainedDirectory, sameFilesystemIdentity, validateConfiguredRoot, } from "./paths.js";
|
|
13
13
|
import { prepareReclaim, WorkflowReclaimError, } from "./reclaim.js";
|
|
14
14
|
import { hasActiveWorkflowEvidence, readWorkflowLiveness } from "./recovery.js";
|
|
@@ -598,7 +598,6 @@ function reclaimPreview(coordRoot, binding, mode) {
|
|
|
598
598
|
run_id: binding.run_id,
|
|
599
599
|
mode,
|
|
600
600
|
liveness: readWorkflowLiveness(coordRoot, binding.run_id),
|
|
601
|
-
evidence_sha256: reclaimEvidenceDigest(coordRoot, binding),
|
|
602
601
|
ownership: "unproven",
|
|
603
602
|
dirty_paths: null,
|
|
604
603
|
lease_holder: reclaimLeaseHolders(coordRoot, binding),
|
|
@@ -619,34 +618,12 @@ function reclaimPreview(coordRoot, binding, mode) {
|
|
|
619
618
|
}
|
|
620
619
|
return preview;
|
|
621
620
|
}
|
|
622
|
-
function
|
|
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
|
+
function assertReclaimLiveness(coordRoot, binding, force = false) {
|
|
640
622
|
const liveness = readWorkflowLiveness(coordRoot, binding.run_id);
|
|
641
|
-
if (liveness === "active" || (
|
|
623
|
+
if (liveness === "active" || (force && hasActiveWorkflowEvidence(coordRoot, binding.run_id))) {
|
|
642
624
|
throw new WorkflowReclaimError("workflow_reclaim_run_live", `run ${binding.run_id} has active evidence; its workspace was left untouched`);
|
|
643
625
|
}
|
|
644
|
-
if (
|
|
645
|
-
throw new WorkflowReclaimError("workflow_reclaim_recovery_mismatch", "workflow reclaim requires the exact current preview evidence digest");
|
|
646
|
-
}
|
|
647
|
-
if (liveness === "inactive")
|
|
648
|
-
return;
|
|
649
|
-
if (liveness === "unknown" && recovery)
|
|
626
|
+
if (liveness === "inactive" || force)
|
|
650
627
|
return;
|
|
651
628
|
throw new WorkflowReclaimError("workflow_reclaim_liveness_unknown", `run ${binding.run_id} has missing, corrupt, or ambiguous liveness evidence; its workspace was left untouched`);
|
|
652
629
|
}
|
|
@@ -677,7 +654,7 @@ function assertPeerWorkspaceAuthority(coordRoot, claim) {
|
|
|
677
654
|
}
|
|
678
655
|
if (!claimOverlapsWorkspace(peer, claim.workspace_root))
|
|
679
656
|
continue;
|
|
680
|
-
// Process termination, archival and an expired
|
|
657
|
+
// Process termination, archival and an expired lease do not release
|
|
681
658
|
// a resource. Require the provider's release record AND current absence.
|
|
682
659
|
if (binding && events.some((event) => event.event === "released")) {
|
|
683
660
|
const current = attestSync(coordRoot, binding);
|
|
@@ -691,185 +668,99 @@ function assertPeerWorkspaceAuthority(coordRoot, claim) {
|
|
|
691
668
|
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", error.message);
|
|
692
669
|
}
|
|
693
670
|
}
|
|
694
|
-
function reclaimAuthorityGuard(coordRoot, binding, claim, reservation, recovery) {
|
|
695
|
-
try {
|
|
696
|
-
reservation.assertCurrent();
|
|
697
|
-
assertReclaimLiveness(coordRoot, binding, recovery);
|
|
698
|
-
assertReclaimOwnership(coordRoot, binding);
|
|
699
|
-
assertPeerWorkspaceAuthority(coordRoot, claim);
|
|
700
|
-
}
|
|
701
|
-
catch (error) {
|
|
702
|
-
if (error instanceof WorkflowReclaimError)
|
|
703
|
-
throw error;
|
|
704
|
-
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", error.message);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
671
|
async function reclaim(coordRoot, provider, binding, options) {
|
|
708
672
|
if (options.mode !== "salvage" && options.mode !== "discard") {
|
|
709
673
|
throw new Error("unsupported workspace reclaim mode");
|
|
710
674
|
}
|
|
711
675
|
if (!options.yes)
|
|
712
676
|
return reclaimPreview(coordRoot, binding, options.mode);
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
// have a proof yet, and a missing proof must never be discovered after reset.
|
|
717
|
-
assertReclaimLiveness(coordRoot, binding, options.unknownLiveness);
|
|
677
|
+
// Liveness comes before leases and terminal-proof cleanup helpers. A live run
|
|
678
|
+
// may not have a proof yet, and a missing proof must never be found after reset.
|
|
679
|
+
assertReclaimLiveness(coordRoot, binding, options.force);
|
|
718
680
|
const claim = assertReclaimOwnership(coordRoot, binding);
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}
|
|
723
|
-
catch (error) {
|
|
724
|
-
throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
|
|
725
|
-
}
|
|
726
|
-
try {
|
|
727
|
-
reclaimAuthorityGuard(coordRoot, binding, claim, reservation, options.unknownLiveness);
|
|
728
|
-
let releaseResume;
|
|
681
|
+
assertPeerWorkspaceAuthority(coordRoot, claim);
|
|
682
|
+
const releases = [];
|
|
683
|
+
const hold = (acquire) => {
|
|
729
684
|
try {
|
|
730
|
-
|
|
685
|
+
releases.unshift(acquire());
|
|
731
686
|
}
|
|
732
687
|
catch (error) {
|
|
733
688
|
throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
|
|
734
689
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
|
|
750
|
-
}
|
|
751
|
-
try {
|
|
752
|
-
assertReclaimLiveness(coordRoot, binding, options.unknownLiveness);
|
|
753
|
-
assertReclaimOwnership(coordRoot, binding);
|
|
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;
|
|
770
|
-
let preparation;
|
|
771
|
-
// Keep ordinary cleanup's terminal-evidence checks, intent and receipts.
|
|
772
|
-
// Its provider callback uses the leases already held here instead of
|
|
773
|
-
// trying to acquire them again. Preparation occurs only after all of
|
|
774
|
-
// cleanup's path, ownership, ref and registration checks have succeeded.
|
|
775
|
-
const cleanupResult = await cleanupWorkspace({
|
|
776
|
-
coordRoot,
|
|
777
|
-
runId: binding.run_id,
|
|
778
|
-
provider: {
|
|
779
|
-
...provider,
|
|
780
|
-
cleanup: async (candidate, intent) => {
|
|
781
|
-
assertReclaimLiveness(coordRoot, candidate, options.unknownLiveness);
|
|
782
|
-
assertReclaimOwnership(coordRoot, candidate);
|
|
783
|
-
return cleanupUnderLease(coordRoot, candidate, claim, intent, {
|
|
784
|
-
mode: options.mode,
|
|
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
|
-
},
|
|
797
|
-
prepared: (value) => {
|
|
798
|
-
preparation = value;
|
|
799
|
-
},
|
|
800
|
-
});
|
|
801
|
-
},
|
|
802
|
-
},
|
|
803
|
-
});
|
|
804
|
-
if (!preparation &&
|
|
805
|
-
cleanupResult.status !== "released" &&
|
|
806
|
-
cleanupResult.status !== "already_released") {
|
|
807
|
-
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", ("reason" in cleanupResult ? cleanupResult.reason : undefined) ??
|
|
808
|
-
"workspace cleanup preconditions could not be proved");
|
|
809
|
-
}
|
|
810
|
-
if (cleanupResult.status === "released" || cleanupResult.status === "already_released")
|
|
811
|
-
audit?.complete();
|
|
812
|
-
return {
|
|
813
|
-
...preview,
|
|
814
|
-
action: "reclaimed",
|
|
815
|
-
lease_holder: [],
|
|
816
|
-
preparation: preparation ?? {
|
|
817
|
-
action: "already_gone",
|
|
818
|
-
detail: "the workspace is already released",
|
|
819
|
-
},
|
|
820
|
-
cleanup: cleanupResult,
|
|
821
|
-
};
|
|
822
|
-
}
|
|
823
|
-
finally {
|
|
824
|
-
releaseRepository();
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
finally {
|
|
828
|
-
releaseBinding();
|
|
829
|
-
}
|
|
690
|
+
};
|
|
691
|
+
try {
|
|
692
|
+
hold(() => acquireWorkflowResumeLease(coordRoot, binding.run_id));
|
|
693
|
+
hold(() => acquireLease(coordRoot, workspaceLockPath(coordRoot, PROVIDER_ID, binding.binding_id), claim, "binding"));
|
|
694
|
+
hold(() => acquireRepositoryLease(coordRoot, claim));
|
|
695
|
+
const preview = reclaimPreview(coordRoot, binding, options.mode);
|
|
696
|
+
if (options.force && preview.liveness !== "inactive") {
|
|
697
|
+
appendWorkspaceEvent(coordRoot, claim, "reclaim_forced", {
|
|
698
|
+
run_id: binding.run_id,
|
|
699
|
+
mode: options.mode,
|
|
700
|
+
liveness: preview.liveness,
|
|
701
|
+
pid: process.pid,
|
|
702
|
+
host: hostname(),
|
|
703
|
+
});
|
|
830
704
|
}
|
|
831
|
-
|
|
832
|
-
|
|
705
|
+
let preparation;
|
|
706
|
+
// Keep ordinary cleanup's terminal-evidence checks, intent and receipts.
|
|
707
|
+
// Its provider callback uses the leases already held here. Preparation
|
|
708
|
+
// occurs only after cleanup's liveness, ownership, path, ref and
|
|
709
|
+
// registration checks have succeeded under those leases.
|
|
710
|
+
const cleanupResult = await cleanupWorkspace({
|
|
711
|
+
coordRoot,
|
|
712
|
+
runId: binding.run_id,
|
|
713
|
+
provider: {
|
|
714
|
+
...provider,
|
|
715
|
+
cleanup: async (candidate, intent) => cleanupUnderLease(coordRoot, candidate, claim, intent, {
|
|
716
|
+
mode: options.mode,
|
|
717
|
+
force: options.force,
|
|
718
|
+
prepared: (value) => {
|
|
719
|
+
preparation = value;
|
|
720
|
+
},
|
|
721
|
+
}),
|
|
722
|
+
},
|
|
723
|
+
});
|
|
724
|
+
if (!preparation &&
|
|
725
|
+
cleanupResult.status !== "released" &&
|
|
726
|
+
cleanupResult.status !== "already_released") {
|
|
727
|
+
throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", ("reason" in cleanupResult ? cleanupResult.reason : undefined) ??
|
|
728
|
+
"workspace cleanup preconditions could not be proved");
|
|
833
729
|
}
|
|
730
|
+
return {
|
|
731
|
+
...preview,
|
|
732
|
+
action: "reclaimed",
|
|
733
|
+
lease_holder: [],
|
|
734
|
+
preparation: preparation ?? {
|
|
735
|
+
action: "already_gone",
|
|
736
|
+
detail: "the workspace is already released",
|
|
737
|
+
},
|
|
738
|
+
cleanup: cleanupResult,
|
|
739
|
+
};
|
|
834
740
|
}
|
|
835
741
|
finally {
|
|
836
|
-
|
|
742
|
+
for (const release of releases)
|
|
743
|
+
release();
|
|
837
744
|
}
|
|
838
745
|
}
|
|
839
746
|
function reclaimLeaseHolders(coordRoot, binding) {
|
|
840
|
-
const
|
|
747
|
+
const holders = [];
|
|
748
|
+
if (existsSync(workflowResumeLockPath(coordRoot, binding.run_id))) {
|
|
749
|
+
const owner = readWorkflowResumeLock(coordRoot, binding.run_id);
|
|
750
|
+
holders.push(owner
|
|
751
|
+
? { scope: "resume", pid: owner.pid, host: owner.host }
|
|
752
|
+
: { scope: "resume", detail: "lease holder could not be read" });
|
|
753
|
+
}
|
|
754
|
+
const leases = [
|
|
841
755
|
["binding", workspaceLockPath(coordRoot, PROVIDER_ID, binding.binding_id)],
|
|
842
756
|
];
|
|
843
757
|
if (binding.repository) {
|
|
844
|
-
|
|
845
|
-
paths.push([
|
|
846
|
-
"repository",
|
|
847
|
-
join(coordRoot, ".harnery", "workspaces", PROVIDER_ID, ".leases", `repository-${key}.lock`),
|
|
848
|
-
]);
|
|
849
|
-
}
|
|
850
|
-
const holders = [];
|
|
851
|
-
try {
|
|
852
|
-
const owner = JSON.parse(readFileSync(join(coordRoot, ".harnery", "workflows", binding.run_id, "resume.lock"), "utf8"));
|
|
853
|
-
holders.push({
|
|
854
|
-
scope: "resume",
|
|
855
|
-
pid: typeof owner.pid === "number" ? owner.pid : undefined,
|
|
856
|
-
host: typeof owner.host === "string" ? owner.host : undefined,
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
|
-
catch (error) {
|
|
860
|
-
if (error.code !== "ENOENT") {
|
|
861
|
-
holders.push({ scope: "resume", detail: "lease holder could not be read" });
|
|
862
|
-
}
|
|
758
|
+
leases.push(["repository", repositoryLeasePath(coordRoot, binding.repository)]);
|
|
863
759
|
}
|
|
864
|
-
for (const [scope, path] of
|
|
760
|
+
for (const [scope, path] of leases) {
|
|
865
761
|
try {
|
|
866
|
-
const owner =
|
|
867
|
-
holders.push({
|
|
868
|
-
scope,
|
|
869
|
-
owner_id: typeof owner.owner_id === "string" ? owner.owner_id : undefined,
|
|
870
|
-
pid: typeof owner.pid === "number" ? owner.pid : undefined,
|
|
871
|
-
host: typeof owner.host === "string" ? owner.host : undefined,
|
|
872
|
-
});
|
|
762
|
+
const { owner } = readOwner(join(path, "current"));
|
|
763
|
+
holders.push({ scope, owner_id: owner.owner_id, pid: owner.pid, host: owner.host });
|
|
873
764
|
}
|
|
874
765
|
catch (error) {
|
|
875
766
|
if (error.code !== "ENOENT") {
|
|
@@ -887,18 +778,7 @@ async function cleanup(coordRoot, binding, intent) {
|
|
|
887
778
|
try {
|
|
888
779
|
const releaseRepository = acquireRepositoryLease(coordRoot, claim);
|
|
889
780
|
try {
|
|
890
|
-
|
|
891
|
-
try {
|
|
892
|
-
const authorityGuard = () => {
|
|
893
|
-
reservation.assertCurrent();
|
|
894
|
-
assertPeerWorkspaceAuthority(coordRoot, claim);
|
|
895
|
-
};
|
|
896
|
-
authorityGuard();
|
|
897
|
-
return cleanupUnderLease(coordRoot, binding, claim, intent, undefined, authorityGuard);
|
|
898
|
-
}
|
|
899
|
-
finally {
|
|
900
|
-
reservation.release();
|
|
901
|
-
}
|
|
781
|
+
return cleanupUnderLease(coordRoot, binding, claim, intent);
|
|
902
782
|
}
|
|
903
783
|
finally {
|
|
904
784
|
releaseRepository();
|
|
@@ -908,7 +788,7 @@ async function cleanup(coordRoot, binding, intent) {
|
|
|
908
788
|
releaseBinding();
|
|
909
789
|
}
|
|
910
790
|
}
|
|
911
|
-
function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest
|
|
791
|
+
function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest) {
|
|
912
792
|
const repository = binding.repository;
|
|
913
793
|
if (!repository ||
|
|
914
794
|
intent.schema_version !== 1 ||
|
|
@@ -946,7 +826,7 @@ function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest, be
|
|
|
946
826
|
};
|
|
947
827
|
}
|
|
948
828
|
const liveness = readWorkflowLiveness(coordRoot, binding.run_id);
|
|
949
|
-
if (liveness !== "inactive" && !(liveness === "unknown" && reclaimRequest?.
|
|
829
|
+
if (liveness !== "inactive" && !(liveness === "unknown" && reclaimRequest?.force)) {
|
|
950
830
|
return {
|
|
951
831
|
...base,
|
|
952
832
|
status: "blocked",
|
|
@@ -1017,14 +897,14 @@ function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest, be
|
|
|
1017
897
|
};
|
|
1018
898
|
}
|
|
1019
899
|
if (reclaimRequest) {
|
|
1020
|
-
|
|
1021
|
-
|
|
900
|
+
// The last liveness check before a destructive reset.
|
|
901
|
+
assertReclaimLiveness(coordRoot, binding, reclaimRequest.force);
|
|
1022
902
|
revalidateClaimResources(claim, root);
|
|
1023
903
|
reclaimRequest.prepared(prepareReclaim({
|
|
1024
904
|
worktreePath: binding.workspace_root,
|
|
1025
905
|
mode: reclaimRequest.mode,
|
|
1026
906
|
runId: binding.run_id,
|
|
1027
|
-
beforeEffect:
|
|
907
|
+
beforeEffect: () => assertPeerWorkspaceAuthority(coordRoot, claim),
|
|
1028
908
|
}));
|
|
1029
909
|
}
|
|
1030
910
|
appendWorkspaceEvent(coordRoot, claim, "cleanup_pending", {
|
|
@@ -1036,8 +916,7 @@ function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest, be
|
|
|
1036
916
|
target_ref_oid: targetRef.out,
|
|
1037
917
|
});
|
|
1038
918
|
if (before.workspace_exists) {
|
|
1039
|
-
|
|
1040
|
-
beforeRemoval?.();
|
|
919
|
+
assertPeerWorkspaceAuthority(coordRoot, claim);
|
|
1041
920
|
revalidateClaimResources(claim, root);
|
|
1042
921
|
assertContainedExisting(root, binding.workspace_root, "workspace cleanup path");
|
|
1043
922
|
git(repository.source_root.realpath, ["worktree", "remove", binding.workspace_root]);
|
|
@@ -1057,7 +936,6 @@ function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest, be
|
|
|
1057
936
|
return { ...base, status: "blocked", reason: "workspace ref changed during cleanup" };
|
|
1058
937
|
}
|
|
1059
938
|
revalidateClaimResources(claim, root);
|
|
1060
|
-
reclaimRequest?.beforeRefEffect();
|
|
1061
939
|
const deleted = gitMaybe(repository.source_root.realpath, [
|
|
1062
940
|
"update-ref",
|
|
1063
941
|
"-d",
|
|
@@ -1810,9 +1688,13 @@ function capabilityDigestForClaim(claim) {
|
|
|
1810
1688
|
void claim;
|
|
1811
1689
|
return stableDigest(basis);
|
|
1812
1690
|
}
|
|
1691
|
+
function repositoryLeasePath(coordRoot, repository) {
|
|
1692
|
+
const key = stableDigest({ common: repository.common_dir.identity });
|
|
1693
|
+
return join(resolve(coordRoot), ".harnery", "workspaces", PROVIDER_ID, ".leases", `repository-${key}.lock`);
|
|
1694
|
+
}
|
|
1813
1695
|
function acquireRepositoryLease(coordRoot, claim) {
|
|
1814
|
-
const
|
|
1815
|
-
mkdirSync(
|
|
1696
|
+
const path = repositoryLeasePath(coordRoot, claim.repository);
|
|
1697
|
+
mkdirSync(dirname(path), { recursive: true, mode: stateDirMode() });
|
|
1816
1698
|
// Mutual exclusion keys on the Git common directory alone. `git worktree add`,
|
|
1817
1699
|
// `prune`, and the shared-`config` migration all write to the common directory's
|
|
1818
1700
|
// administrative area, and once worktrees and submodules are allowed, several
|
|
@@ -1821,8 +1703,7 @@ function acquireRepositoryLease(coordRoot, claim) {
|
|
|
1821
1703
|
// nested — writable roots. Including the writable root in the key would hand those
|
|
1822
1704
|
// agents different locks and let their admin-area writes race. The writable root
|
|
1823
1705
|
// stays in the lease metadata for diagnostics, not in the exclusion key.
|
|
1824
|
-
|
|
1825
|
-
return acquireLease(coordRoot, join(leaseDir, `repository-${key}.lock`), claim, "repository");
|
|
1706
|
+
return acquireLease(coordRoot, path, claim, "repository");
|
|
1826
1707
|
}
|
|
1827
1708
|
function acquireLease(coordRoot, path, claim, scope) {
|
|
1828
1709
|
const authoritySha256 = stableDigest({
|
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import type { WorkspaceClaim } from "./state.js";
|
|
2
|
-
export interface WorkspaceReclaimReservation {
|
|
3
|
-
assertCurrent(): void;
|
|
4
|
-
release(): void;
|
|
5
|
-
}
|
|
6
|
-
/** The immutable claim is the authority; the lease reserves its destructive path. */
|
|
7
|
-
export declare function reserveWorkspaceReclaim(coordRoot: string, claim: WorkspaceClaim): WorkspaceReclaimReservation;
|
|
8
|
-
export declare function publishWorkspaceClaim<T>(coordRoot: string, claim: WorkspaceClaim, publish: () => T): T;
|
|
9
2
|
export declare function assertClaimAuthority(claim: WorkspaceClaim, providerId: string, id: string): void;
|
|
10
3
|
export declare function claimOverlapsWorkspace(claim: WorkspaceClaim, workspaceRoot: string): boolean;
|
|
11
4
|
//# sourceMappingURL=ownership.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ownership.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/ownership.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ownership.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/ownership.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGjD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAwBhG;AAmCD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAU5F"}
|
|
@@ -1,135 +1,8 @@
|
|
|
1
|
-
import { lstatSync,
|
|
2
|
-
import { basename, dirname, isAbsolute,
|
|
3
|
-
import {
|
|
4
|
-
import { acquireNoClobberLease } from "./leases.js";
|
|
1
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { stableDigest } from "../durable-record.js";
|
|
5
4
|
import { containsPath } from "./paths.js";
|
|
6
5
|
import { isWorkspaceAllocationRequest } from "./validate.js";
|
|
7
|
-
const STALE_MS = 5 * 60 * 1_000;
|
|
8
|
-
const RESERVATION_SCOPE = "workspace-reclaim";
|
|
9
|
-
function leaseRoot(coordRoot, providerId) {
|
|
10
|
-
return join(resolve(coordRoot), ".harnery", "workspaces", providerId, ".leases");
|
|
11
|
-
}
|
|
12
|
-
/** Serialize only publication and reservation registration, never Git work. */
|
|
13
|
-
function withPublicationGate(coordRoot, providerId, operation) {
|
|
14
|
-
const authority = stableDigest({ provider_id: providerId, scope: "workspace-publication" });
|
|
15
|
-
const gate = acquireNoClobberLease({
|
|
16
|
-
path: join(leaseRoot(coordRoot, providerId), "publication.lock"),
|
|
17
|
-
scope: "workspace-publication",
|
|
18
|
-
authoritySha256: authority,
|
|
19
|
-
staleAfterMs: STALE_MS,
|
|
20
|
-
validateStaleOwner: (owner) => owner.authority_sha256 === authority,
|
|
21
|
-
});
|
|
22
|
-
try {
|
|
23
|
-
return operation();
|
|
24
|
-
}
|
|
25
|
-
finally {
|
|
26
|
-
gate.release();
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function reservationPath(coordRoot, claim) {
|
|
30
|
-
return join(leaseRoot(coordRoot, claim.provider_id), `reclaim-${claim.binding_id}.lock`);
|
|
31
|
-
}
|
|
32
|
-
function acquireReservation(coordRoot, claim) {
|
|
33
|
-
const authority = stableDigest(claim);
|
|
34
|
-
return acquireNoClobberLease({
|
|
35
|
-
path: reservationPath(coordRoot, claim),
|
|
36
|
-
scope: RESERVATION_SCOPE,
|
|
37
|
-
authoritySha256: authority,
|
|
38
|
-
staleAfterMs: STALE_MS,
|
|
39
|
-
metadata: { binding_id: claim.binding_id, request_sha256: claim.request_sha256 },
|
|
40
|
-
validateStaleOwner: (owner) => owner.authority_sha256 === authority &&
|
|
41
|
-
owner.metadata?.binding_id === claim.binding_id &&
|
|
42
|
-
owner.metadata?.request_sha256 === claim.request_sha256,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
/** The immutable claim is the authority; the lease reserves its destructive path. */
|
|
46
|
-
export function reserveWorkspaceReclaim(coordRoot, claim) {
|
|
47
|
-
assertClaimAuthority(claim, claim.provider_id, claim.binding_id);
|
|
48
|
-
const lease = withPublicationGate(coordRoot, claim.provider_id, () => acquireReservation(coordRoot, claim));
|
|
49
|
-
return {
|
|
50
|
-
assertCurrent() {
|
|
51
|
-
const path = reservationPath(coordRoot, claim);
|
|
52
|
-
const current = join(path, "current");
|
|
53
|
-
const pin = join(path, lease.owner.owner_file);
|
|
54
|
-
const a = lstatSync(current, { bigint: true });
|
|
55
|
-
const b = lstatSync(pin, { bigint: true });
|
|
56
|
-
if (!a.isFile() ||
|
|
57
|
-
!b.isFile() ||
|
|
58
|
-
a.dev !== b.dev ||
|
|
59
|
-
a.ino !== b.ino ||
|
|
60
|
-
readFileSync(current, "utf8") !== readFileSync(pin, "utf8") ||
|
|
61
|
-
stableDigest(readJsonRecord(current, "reclaim reservation")) !== stableDigest(lease.owner)) {
|
|
62
|
-
throw new Error("workspace reclaim reservation owner changed");
|
|
63
|
-
}
|
|
64
|
-
const persisted = readJsonRecord(join(dirname(dirname(path)), claim.binding_id, "claim.json"), "reserved workspace claim");
|
|
65
|
-
if (stableDigest(persisted) !== stableDigest(claim)) {
|
|
66
|
-
throw new Error("workspace reclaim reservation claim changed");
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
release: () => lease.release(),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
export function publishWorkspaceClaim(coordRoot, claim, publish) {
|
|
73
|
-
assertClaimAuthority(claim, claim.provider_id, claim.binding_id);
|
|
74
|
-
return withPublicationGate(coordRoot, claim.provider_id, () => {
|
|
75
|
-
for (const entry of readdirSync(leaseRoot(coordRoot, claim.provider_id), {
|
|
76
|
-
withFileTypes: true,
|
|
77
|
-
})) {
|
|
78
|
-
if (!entry.name.startsWith("reclaim-") || !entry.name.endsWith(".lock"))
|
|
79
|
-
continue;
|
|
80
|
-
if (!entry.isDirectory())
|
|
81
|
-
throw new Error("workspace reclaim reservation is not a directory");
|
|
82
|
-
const path = join(leaseRoot(coordRoot, claim.provider_id), entry.name);
|
|
83
|
-
const current = join(path, "current");
|
|
84
|
-
try {
|
|
85
|
-
lstatSync(current);
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
if (error.code === "ENOENT")
|
|
89
|
-
continue;
|
|
90
|
-
throw error;
|
|
91
|
-
}
|
|
92
|
-
const id = entry.name.slice("reclaim-".length, -".lock".length);
|
|
93
|
-
const reserved = readJsonRecord(join(dirname(leaseRoot(coordRoot, claim.provider_id)), id, "claim.json"), "reserved workspace claim");
|
|
94
|
-
assertClaimAuthority(reserved, claim.provider_id, id);
|
|
95
|
-
// Unknown reservation authority is never interpreted as an unrelated owner.
|
|
96
|
-
const owner = readJsonRecord(current, "reclaim reservation");
|
|
97
|
-
if (owner.schema_version !== 1 ||
|
|
98
|
-
typeof owner.owner_id !== "string" ||
|
|
99
|
-
!/^[a-f0-9-]{36}$/.test(owner.owner_id) ||
|
|
100
|
-
owner.owner_file !== `owner-${owner.owner_id}.json` ||
|
|
101
|
-
!Number.isSafeInteger(owner.pid) ||
|
|
102
|
-
owner.pid <= 0 ||
|
|
103
|
-
typeof owner.host !== "string" ||
|
|
104
|
-
owner.host.length === 0 ||
|
|
105
|
-
!Number.isFinite(Date.parse(owner.created_at)) ||
|
|
106
|
-
!Number.isFinite(Date.parse(owner.expires_at)) ||
|
|
107
|
-
Date.parse(owner.expires_at) < Date.parse(owner.created_at) ||
|
|
108
|
-
owner.scope !== RESERVATION_SCOPE ||
|
|
109
|
-
owner.authority_sha256 !== stableDigest(reserved) ||
|
|
110
|
-
owner.metadata?.binding_id !== id ||
|
|
111
|
-
owner.metadata?.request_sha256 !== reserved.request_sha256) {
|
|
112
|
-
throw new Error("workspace reclaim reservation authority is unproven");
|
|
113
|
-
}
|
|
114
|
-
const a = lstatSync(current, { bigint: true });
|
|
115
|
-
const pin = join(path, owner.owner_file);
|
|
116
|
-
const b = lstatSync(pin, { bigint: true });
|
|
117
|
-
if (!a.isFile() ||
|
|
118
|
-
!b.isFile() ||
|
|
119
|
-
a.dev !== b.dev ||
|
|
120
|
-
a.ino !== b.ino ||
|
|
121
|
-
readFileSync(current, "utf8") !== readFileSync(pin, "utf8"))
|
|
122
|
-
throw new Error("workspace reclaim reservation owner is unproven");
|
|
123
|
-
if (!claimOverlapsWorkspace(claim, reserved.workspace_root))
|
|
124
|
-
continue;
|
|
125
|
-
// This succeeds only for an expired, provably dead, exact prior owner.
|
|
126
|
-
// It retires the reservation, never the immutable claim or resource.
|
|
127
|
-
const recovered = acquireReservation(coordRoot, reserved);
|
|
128
|
-
recovered.release();
|
|
129
|
-
}
|
|
130
|
-
return publish();
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
6
|
export function assertClaimAuthority(claim, providerId, id) {
|
|
134
7
|
if (claim?.schema_version !== 1 ||
|
|
135
8
|
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(providerId) ||
|
|
@@ -14,11 +14,10 @@
|
|
|
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";
|
|
18
17
|
import type { WorkflowLiveness } from "./recovery.js";
|
|
19
18
|
import type { WorkspaceCleanupAttempt, WorkspaceCleanupReceipt } from "./types.js";
|
|
20
19
|
export type ReclaimMode = "salvage" | "discard";
|
|
21
|
-
export type WorkflowReclaimRefusalCode = "workflow_reclaim_run_live" | "workflow_reclaim_liveness_unknown" | "
|
|
20
|
+
export type WorkflowReclaimRefusalCode = "workflow_reclaim_run_live" | "workflow_reclaim_liveness_unknown" | "workflow_reclaim_ownership_unproven" | "workflow_reclaim_lease_held";
|
|
22
21
|
export declare class WorkflowReclaimError extends Error {
|
|
23
22
|
readonly code: WorkflowReclaimRefusalCode;
|
|
24
23
|
constructor(code: WorkflowReclaimRefusalCode, message: string);
|
|
@@ -26,9 +25,8 @@ export declare class WorkflowReclaimError extends Error {
|
|
|
26
25
|
export interface WorkspaceReclaimOptions {
|
|
27
26
|
mode: ReclaimMode;
|
|
28
27
|
yes?: boolean;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
};
|
|
28
|
+
/** Admit missing or corrupt liveness evidence; active evidence still refuses. */
|
|
29
|
+
force?: boolean;
|
|
32
30
|
}
|
|
33
31
|
export interface ReclaimLeaseHolder {
|
|
34
32
|
scope: "binding" | "repository" | "resume";
|
|
@@ -42,7 +40,6 @@ export interface WorkspaceReclaimPreview {
|
|
|
42
40
|
run_id: string;
|
|
43
41
|
mode: ReclaimMode;
|
|
44
42
|
liveness: WorkflowLiveness;
|
|
45
|
-
evidence_sha256: string;
|
|
46
43
|
ownership: "ok" | "unproven";
|
|
47
44
|
ownership_detail?: string;
|
|
48
45
|
dirty_paths: string[] | null;
|
|
@@ -72,7 +69,7 @@ export interface PrepareReclaimInput {
|
|
|
72
69
|
worktreePath: string;
|
|
73
70
|
mode: ReclaimMode;
|
|
74
71
|
runId: string;
|
|
75
|
-
/**
|
|
72
|
+
/** Recheck peer authority once, immediately before the first destructive step. */
|
|
76
73
|
beforeEffect?: () => void;
|
|
77
74
|
}
|
|
78
75
|
/**
|
|
@@ -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;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;IACd,iFAAiF;IACjF,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;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,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,kBAAkB,CAoD7E;AAYD;qEACqE;AACrE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD"}
|