@abelxiaoxing/cadence 2.0.14 → 2.0.16
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/README.md +76 -7
- package/docs/service-management.md +96 -5
- package/package.json +1 -1
- package/src/native-build.ts +1 -1
- package/src/service/coordination.ts +301 -53
- package/src/service-cli.mjs +2169 -1546
- package/src/service-host.mjs +539 -293
- package/src/worker-public.mjs +1 -0
- package/web/dist/assets/cadence.css +1 -1
- package/web/dist/assets/cadence.js +10 -10
- package/web/dist/manifest.json +7 -7
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import { opendirSync
|
|
2
|
+
import { opendirSync } from "node:fs";
|
|
3
3
|
import { hostname } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
5
5
|
import {
|
|
6
6
|
type CheckoutLock,
|
|
7
|
+
canonicalAmbientRoot,
|
|
8
|
+
defaultStateHome,
|
|
7
9
|
resolveWorkspace,
|
|
8
10
|
TaskStore,
|
|
9
11
|
} from "../task-store.ts";
|
|
@@ -150,6 +152,42 @@ function names(path: string, allowed: readonly string[]): void {
|
|
|
150
152
|
dir.closeSync();
|
|
151
153
|
}
|
|
152
154
|
}
|
|
155
|
+
interface RegistryLock {
|
|
156
|
+
schema: "cadence-service-registry-lock/1";
|
|
157
|
+
token: string;
|
|
158
|
+
pid: number;
|
|
159
|
+
host: string;
|
|
160
|
+
}
|
|
161
|
+
function decodeRegistryLock(snapshot: FileSnapshot): RegistryLock {
|
|
162
|
+
const o = closed(json(snapshot), ["schema", "token", "pid", "host"]);
|
|
163
|
+
return {
|
|
164
|
+
schema: literal(o.schema, "cadence-service-registry-lock/1"),
|
|
165
|
+
token: decodeId(o.token),
|
|
166
|
+
// kill(0)/negative process-group probes must never be possible.
|
|
167
|
+
pid: integer(o.pid, 1, 2147483647),
|
|
168
|
+
host: text(o.host, 255, true),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export interface RegistryLockDiagnostic {
|
|
172
|
+
reason: "registry-locked";
|
|
173
|
+
owner: "live" | "absent" | "ambiguous" | "unreadable";
|
|
174
|
+
}
|
|
175
|
+
export interface RegistryRecoveryInspection {
|
|
176
|
+
scope: "global-registry";
|
|
177
|
+
epoch: string; // exact lock incarnation, not a service epoch
|
|
178
|
+
coordinationDigest: string;
|
|
179
|
+
owner: "live" | "absent" | "ambiguous";
|
|
180
|
+
process: { pid: number; host: string };
|
|
181
|
+
reservations: number;
|
|
182
|
+
previousResolver: "live" | "absent" | "ambiguous" | null;
|
|
183
|
+
resolution: "local-interactive-only";
|
|
184
|
+
}
|
|
185
|
+
const registryNames = [
|
|
186
|
+
"format.json",
|
|
187
|
+
"roots.json",
|
|
188
|
+
"registry.lock",
|
|
189
|
+
"recovery-v1",
|
|
190
|
+
];
|
|
153
191
|
interface RegistryView {
|
|
154
192
|
format: FileSnapshot;
|
|
155
193
|
snapshot: FileSnapshot;
|
|
@@ -209,19 +247,30 @@ export type CoordinationStatus =
|
|
|
209
247
|
* task creation, stale reclaim or accepting execution route lives here.
|
|
210
248
|
*/
|
|
211
249
|
export class ServiceCoordination {
|
|
212
|
-
readonly
|
|
250
|
+
private readonly project?: TaskStore;
|
|
213
251
|
readonly files: PrivateFiles;
|
|
214
|
-
readonly directory: string;
|
|
215
252
|
readonly registryDirectory: string;
|
|
216
253
|
readonly stateHome: string;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
254
|
+
/** null addresses only account-wide registry recovery, never an implicit cwd. */
|
|
255
|
+
constructor(workspace: string | null, stateHome?: string) {
|
|
256
|
+
this.project =
|
|
257
|
+
workspace === null
|
|
258
|
+
? undefined
|
|
259
|
+
: new TaskStore(resolveWorkspace(workspace).root, stateHome);
|
|
260
|
+
this.stateHome = this.project
|
|
261
|
+
? dirname(dirname(this.project.privateRoot))
|
|
262
|
+
: canonicalAmbientRoot(stateHome ?? defaultStateHome());
|
|
263
|
+
const namespace = join(this.stateHome, "cadence-assistant-v1");
|
|
220
264
|
this.files = new PrivateFiles(namespace);
|
|
221
|
-
this.stateHome = dirname(namespace);
|
|
222
|
-
this.directory = join(this.store.privateRoot, "service-v1");
|
|
223
265
|
this.registryDirectory = join(namespace, "service-registry-v1");
|
|
224
266
|
}
|
|
267
|
+
get store(): TaskStore {
|
|
268
|
+
if (!this.project) throw new ContractError("forbidden");
|
|
269
|
+
return this.project;
|
|
270
|
+
}
|
|
271
|
+
get directory(): string {
|
|
272
|
+
return join(this.store.privateRoot, "service-v1");
|
|
273
|
+
}
|
|
225
274
|
path(
|
|
226
275
|
name:
|
|
227
276
|
| "format.json"
|
|
@@ -240,11 +289,7 @@ export class ServiceCoordination {
|
|
|
240
289
|
}
|
|
241
290
|
private registry(): RegistryView | undefined {
|
|
242
291
|
if (!this.files.directory(this.registryDirectory)) return undefined;
|
|
243
|
-
names(this.registryDirectory,
|
|
244
|
-
"format.json",
|
|
245
|
-
"roots.json",
|
|
246
|
-
"registry.lock",
|
|
247
|
-
]);
|
|
292
|
+
names(this.registryDirectory, registryNames);
|
|
248
293
|
const format = this.files.read(this.registryPath("format.json"));
|
|
249
294
|
const snapshot = this.files.read(
|
|
250
295
|
this.registryPath("roots.json"),
|
|
@@ -257,9 +302,12 @@ export class ServiceCoordination {
|
|
|
257
302
|
const o = closed(json(snapshot, REGISTRY_BYTES), ["schema", "entries"]);
|
|
258
303
|
const entries = list(o.entries, REGISTRY_ENTRIES, decodeReservation);
|
|
259
304
|
for (const [i, row] of entries.entries()) {
|
|
305
|
+
// A missing consumer directory does not erase its reservation. Validate
|
|
306
|
+
// its canonical existing ancestors without recreating the missing tail;
|
|
307
|
+
// aliases (including dangling ones), permissions and overlaps still refuse.
|
|
260
308
|
if (
|
|
261
309
|
row.stateHome !== this.stateHome ||
|
|
262
|
-
|
|
310
|
+
canonicalAmbientRoot(row.root) !== row.root
|
|
263
311
|
)
|
|
264
312
|
refused();
|
|
265
313
|
// An endpoint cannot be shared by two supported owners, even for disjoint roots.
|
|
@@ -283,9 +331,9 @@ export class ServiceCoordination {
|
|
|
283
331
|
},
|
|
284
332
|
};
|
|
285
333
|
}
|
|
286
|
-
assertNamespace(walks?: DirectoryWalks): void {
|
|
287
|
-
if (!this.files.directory(
|
|
288
|
-
names(
|
|
334
|
+
assertNamespace(walks?: DirectoryWalks, directory = this.directory): void {
|
|
335
|
+
if (!this.files.directory(directory, false, walks)) refused();
|
|
336
|
+
names(directory, [
|
|
289
337
|
"format.json",
|
|
290
338
|
"config.json",
|
|
291
339
|
"registration.json",
|
|
@@ -297,18 +345,19 @@ export class ServiceCoordination {
|
|
|
297
345
|
"history-v2",
|
|
298
346
|
"conversations-v1",
|
|
299
347
|
]);
|
|
300
|
-
this.files.directory(join(
|
|
348
|
+
this.files.directory(join(directory, "history-v2"), false, walks);
|
|
301
349
|
}
|
|
302
|
-
private selected(
|
|
350
|
+
private selected(
|
|
351
|
+
directory = this.directory,
|
|
352
|
+
root = this.store.root,
|
|
353
|
+
stoppedOnly = false,
|
|
354
|
+
): Selected | undefined {
|
|
303
355
|
const walks = new DirectoryWalks();
|
|
304
|
-
if (!this.files.directory(
|
|
305
|
-
this.assertNamespace(walks);
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
walks,
|
|
310
|
-
);
|
|
311
|
-
const config = this.files.read(this.path("config.json"), undefined, walks);
|
|
356
|
+
if (!this.files.directory(directory, false, walks)) return undefined;
|
|
357
|
+
this.assertNamespace(walks, directory);
|
|
358
|
+
const path = (name: string) => join(directory, name);
|
|
359
|
+
const manifest = this.files.read(path("format.json"), undefined, walks);
|
|
360
|
+
const config = this.files.read(path("config.json"), undefined, walks);
|
|
312
361
|
if (!manifest || !config) refused();
|
|
313
362
|
const f = closed(json(manifest), [
|
|
314
363
|
"schema",
|
|
@@ -317,39 +366,45 @@ export class ServiceCoordination {
|
|
|
317
366
|
"configIdentity",
|
|
318
367
|
]);
|
|
319
368
|
literal(f.schema, "cadence-service-format/1");
|
|
320
|
-
literal(f.root,
|
|
369
|
+
literal(f.root, root);
|
|
321
370
|
literal(f.stateHome, this.stateHome);
|
|
322
371
|
literal(f.configIdentity, config.identity);
|
|
323
372
|
const c = decodeOwnerConfig(json(config));
|
|
324
373
|
const hostConfig = this.files.read(
|
|
325
|
-
|
|
374
|
+
path("host-config.json"),
|
|
326
375
|
undefined,
|
|
327
376
|
walks,
|
|
328
377
|
);
|
|
329
378
|
if (hostConfig && hostConfig.identity !== c.policyIdentity) refused();
|
|
330
379
|
const registration = this.files.read(
|
|
331
|
-
|
|
380
|
+
path("registration.json"),
|
|
332
381
|
undefined,
|
|
333
382
|
walks,
|
|
334
383
|
);
|
|
335
|
-
const owner = this.files.read(
|
|
384
|
+
const owner = this.files.read(path("owner.lock"), undefined, walks);
|
|
336
385
|
const capability = this.files.read(
|
|
337
|
-
|
|
386
|
+
path("local-capability.json"),
|
|
338
387
|
undefined,
|
|
339
388
|
walks,
|
|
340
389
|
);
|
|
341
|
-
const checkout = this.files.read(
|
|
390
|
+
const checkout = this.files.read(
|
|
391
|
+
join(dirname(directory), "checkout.lock"),
|
|
392
|
+
8192,
|
|
393
|
+
walks,
|
|
394
|
+
);
|
|
342
395
|
const value = registration
|
|
343
396
|
? decodeRegistration(json(registration))
|
|
344
397
|
: undefined;
|
|
345
398
|
if (
|
|
346
399
|
value &&
|
|
347
|
-
(value.root !==
|
|
400
|
+
(value.root !== root ||
|
|
348
401
|
value.stateHome !== this.stateHome ||
|
|
349
402
|
value.configIdentity !== config.identity ||
|
|
350
403
|
!endpointMatches(c.endpoint, value.endpoint))
|
|
351
404
|
)
|
|
352
405
|
refused();
|
|
406
|
+
if (stoppedOnly && (checkout || (value && value.phase !== "stopped")))
|
|
407
|
+
throw new ContractError("quarantine");
|
|
353
408
|
if (value && value.phase !== "stopped") {
|
|
354
409
|
if (!owner || !checkout || !capability) refused();
|
|
355
410
|
const lock = closed(json(owner), [
|
|
@@ -448,21 +503,24 @@ export class ServiceCoordination {
|
|
|
448
503
|
) => T,
|
|
449
504
|
): T {
|
|
450
505
|
this.files.directory(this.registryDirectory, true);
|
|
451
|
-
names(this.registryDirectory,
|
|
452
|
-
"format.json",
|
|
453
|
-
"roots.json",
|
|
454
|
-
"registry.lock",
|
|
455
|
-
]);
|
|
506
|
+
names(this.registryDirectory, registryNames);
|
|
456
507
|
const lockPath = this.registryPath("registry.lock");
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
508
|
+
let lock: FileSnapshot;
|
|
509
|
+
try {
|
|
510
|
+
lock = this.files.create(
|
|
511
|
+
lockPath,
|
|
512
|
+
encoded({
|
|
513
|
+
schema: "cadence-service-registry-lock/1",
|
|
514
|
+
token: randomUUID(),
|
|
515
|
+
pid: process.pid,
|
|
516
|
+
host: hostname(),
|
|
517
|
+
}),
|
|
518
|
+
);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
if (error instanceof Error && "code" in error && error.code === "EEXIST")
|
|
521
|
+
throw new ContractError("registry-locked");
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
466
524
|
let dirty = false;
|
|
467
525
|
try {
|
|
468
526
|
let format = this.files.read(this.registryPath("format.json"));
|
|
@@ -499,11 +557,7 @@ export class ServiceCoordination {
|
|
|
499
557
|
() => {
|
|
500
558
|
this.files.assert(lockPath, lock);
|
|
501
559
|
this.files.assert(this.registryPath("format.json"), format);
|
|
502
|
-
names(this.registryDirectory,
|
|
503
|
-
"format.json",
|
|
504
|
-
"roots.json",
|
|
505
|
-
"registry.lock",
|
|
506
|
-
]);
|
|
560
|
+
names(this.registryDirectory, registryNames);
|
|
507
561
|
},
|
|
508
562
|
);
|
|
509
563
|
this.files.remove(lockPath, lock);
|
|
@@ -523,6 +577,7 @@ export class ServiceCoordination {
|
|
|
523
577
|
* selected host-config snapshot); only then may the identity change. */
|
|
524
578
|
reselectExisting?: (hostConfig: FileSnapshot) => void,
|
|
525
579
|
): OwnerLease {
|
|
580
|
+
void this.store; // An account-only recovery target cannot acquire an owner.
|
|
526
581
|
const config = decodeOwnerConfig(input);
|
|
527
582
|
const bound = endpoint(resolvedEndpoint);
|
|
528
583
|
if (!endpointMatches(config.endpoint, bound))
|
|
@@ -696,6 +751,199 @@ export class ServiceCoordination {
|
|
|
696
751
|
});
|
|
697
752
|
});
|
|
698
753
|
}
|
|
754
|
+
/** Readonly and secret-free; never probes another host or reclaims on age. */
|
|
755
|
+
registryLockDiagnostic(): RegistryLockDiagnostic | undefined {
|
|
756
|
+
try {
|
|
757
|
+
const lock = this.files.read(this.registryPath("registry.lock"));
|
|
758
|
+
if (!lock) return undefined;
|
|
759
|
+
const value = decodeRegistryLock(lock);
|
|
760
|
+
return {
|
|
761
|
+
reason: "registry-locked",
|
|
762
|
+
owner: recoveryOwnerStatus(value.pid, value.host),
|
|
763
|
+
};
|
|
764
|
+
} catch {
|
|
765
|
+
return { reason: "registry-locked", owner: "unreadable" };
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
private registryRecoveryState() {
|
|
769
|
+
const lock = this.files.read(this.registryPath("registry.lock"));
|
|
770
|
+
if (!lock) throw new ContractError("conflict");
|
|
771
|
+
const value = decodeRegistryLock(lock);
|
|
772
|
+
names(this.registryDirectory, registryNames);
|
|
773
|
+
const format = this.files.read(this.registryPath("format.json"));
|
|
774
|
+
const roots = this.files.read(
|
|
775
|
+
this.registryPath("roots.json"),
|
|
776
|
+
REGISTRY_BYTES,
|
|
777
|
+
);
|
|
778
|
+
// A crash immediately after exclusive creation may precede initialization.
|
|
779
|
+
const registry = !format && !roots ? undefined : this.registry();
|
|
780
|
+
const entries = registry?.value.entries ?? [];
|
|
781
|
+
const protectedRoots = new Set(entries.map((row) => hash(row.root)));
|
|
782
|
+
const proofs: unknown[] = [];
|
|
783
|
+
// Older releases removed a reservation BEFORE the checkout fence. A dead
|
|
784
|
+
// short-lock PID cannot make those unreserved, partially released projects
|
|
785
|
+
// safe. Verify every unreserved service selection; never repair it here.
|
|
786
|
+
const directory = opendirSync(this.files.namespace);
|
|
787
|
+
try {
|
|
788
|
+
let count = 0;
|
|
789
|
+
for (
|
|
790
|
+
let entry = directory.readSync();
|
|
791
|
+
entry;
|
|
792
|
+
entry = directory.readSync()
|
|
793
|
+
) {
|
|
794
|
+
if (++count > 1024) throw new ContractError("capacity");
|
|
795
|
+
if (
|
|
796
|
+
!/^[a-f0-9]{64}$/.test(entry.name) ||
|
|
797
|
+
protectedRoots.has(entry.name)
|
|
798
|
+
)
|
|
799
|
+
continue;
|
|
800
|
+
const service = join(this.files.namespace, entry.name, "service-v1");
|
|
801
|
+
if (!this.files.directory(service)) continue;
|
|
802
|
+
const manifest = this.files.read(join(service, "format.json"));
|
|
803
|
+
if (!manifest) refused();
|
|
804
|
+
const f = closed(json(manifest), [
|
|
805
|
+
"schema",
|
|
806
|
+
"root",
|
|
807
|
+
"stateHome",
|
|
808
|
+
"configIdentity",
|
|
809
|
+
]);
|
|
810
|
+
const root = pathValue(f.root);
|
|
811
|
+
if (hash(root) !== entry.name) refused();
|
|
812
|
+
// Retained stopped metadata remains checkable even when the consumer
|
|
813
|
+
// deleted/moved that old working directory. Never resolve/recreate it.
|
|
814
|
+
const selected = this.selected(service, root, true);
|
|
815
|
+
if (!selected) throw new ContractError("quarantine");
|
|
816
|
+
proofs.push([
|
|
817
|
+
entry.name,
|
|
818
|
+
...[
|
|
819
|
+
selected.manifest,
|
|
820
|
+
selected.config,
|
|
821
|
+
selected.hostConfig,
|
|
822
|
+
selected.registration,
|
|
823
|
+
].map((file) => (file ? [file.identity, file.stamp] : null)),
|
|
824
|
+
]);
|
|
825
|
+
}
|
|
826
|
+
} finally {
|
|
827
|
+
directory.closeSync();
|
|
828
|
+
}
|
|
829
|
+
proofs.sort((a, b) =>
|
|
830
|
+
JSON.stringify(a).localeCompare(JSON.stringify(b), "en"),
|
|
831
|
+
);
|
|
832
|
+
const stateDigest = hash(
|
|
833
|
+
encoded([
|
|
834
|
+
lock.identity,
|
|
835
|
+
lock.stamp,
|
|
836
|
+
format ? [format.identity, format.stamp] : null,
|
|
837
|
+
roots ? [roots.identity, roots.stamp] : null,
|
|
838
|
+
proofs,
|
|
839
|
+
]),
|
|
840
|
+
);
|
|
841
|
+
const epoch = hash(encoded([lock.identity, lock.stamp]));
|
|
842
|
+
const archive = this.registryPath(join("recovery-v1", epoch));
|
|
843
|
+
const attempts: FileSnapshot[] = [];
|
|
844
|
+
let previousResolver: RegistryRecoveryInspection["previousResolver"] = null;
|
|
845
|
+
if (this.files.directory(archive)) {
|
|
846
|
+
// Immutable, exclusive attempts serialize resolvers. If one crashes before
|
|
847
|
+
// release, a fresh confirmation may append the next attempt ONLY after its
|
|
848
|
+
// PID is absent. No stale secondary lock needs deletion or takeover.
|
|
849
|
+
names(
|
|
850
|
+
archive,
|
|
851
|
+
Array.from({ length: 128 }, (_, n) => `${n}.json`),
|
|
852
|
+
);
|
|
853
|
+
for (let n = 0; n < 128; n++) {
|
|
854
|
+
const attempt = this.files.read(join(archive, `${n}.json`), 32768);
|
|
855
|
+
if (!attempt) {
|
|
856
|
+
names(
|
|
857
|
+
archive,
|
|
858
|
+
attempts.map((_, i) => `${i}.json`),
|
|
859
|
+
);
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
const o = closed(json(attempt, 32768), [
|
|
863
|
+
"schema",
|
|
864
|
+
"lock",
|
|
865
|
+
"pid",
|
|
866
|
+
"host",
|
|
867
|
+
"digest",
|
|
868
|
+
]);
|
|
869
|
+
literal(o.schema, "cadence-registry-recovery/1");
|
|
870
|
+
literal(o.lock, lock.bytes.toString("utf8"));
|
|
871
|
+
decodeHash(o.digest);
|
|
872
|
+
previousResolver = recoveryOwnerStatus(
|
|
873
|
+
integer(o.pid, 1, 2147483647),
|
|
874
|
+
text(o.host, 255, true),
|
|
875
|
+
);
|
|
876
|
+
attempts.push(attempt);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const inspection: RegistryRecoveryInspection = {
|
|
880
|
+
scope: "global-registry",
|
|
881
|
+
epoch,
|
|
882
|
+
coordinationDigest: hash(
|
|
883
|
+
encoded([stateDigest, attempts.map((a) => [a.identity, a.stamp])]),
|
|
884
|
+
),
|
|
885
|
+
owner: recoveryOwnerStatus(value.pid, value.host),
|
|
886
|
+
process: { pid: value.pid, host: value.host },
|
|
887
|
+
reservations: entries.length,
|
|
888
|
+
previousResolver,
|
|
889
|
+
resolution: "local-interactive-only",
|
|
890
|
+
};
|
|
891
|
+
return { lock, archive, attempts, stateDigest, inspection };
|
|
892
|
+
}
|
|
893
|
+
inspectRegistryRecovery(): RegistryRecoveryInspection {
|
|
894
|
+
return this.registryRecoveryState().inspection;
|
|
895
|
+
}
|
|
896
|
+
/** Local native management only. Retire just the proven dead short lock after
|
|
897
|
+
* durable evidence; reservations, owner/checkout locks and history stay intact. */
|
|
898
|
+
resolveRegistryRecovery(
|
|
899
|
+
expected: RegistryRecoveryInspection,
|
|
900
|
+
assertAuthority: () => void,
|
|
901
|
+
) {
|
|
902
|
+
assertAuthority();
|
|
903
|
+
const state = this.registryRecoveryState();
|
|
904
|
+
if (encoded(state.inspection) !== encoded(expected))
|
|
905
|
+
throw new ContractError("conflict");
|
|
906
|
+
if (
|
|
907
|
+
expected.owner !== "absent" ||
|
|
908
|
+
(expected.previousResolver !== null &&
|
|
909
|
+
expected.previousResolver !== "absent")
|
|
910
|
+
)
|
|
911
|
+
throw new ContractError("quarantine");
|
|
912
|
+
if (state.attempts.length >= 128) throw new ContractError("capacity");
|
|
913
|
+
assertAuthority();
|
|
914
|
+
const receiptPath = join(state.archive, `${state.attempts.length}.json`);
|
|
915
|
+
const receipt = this.files.create(
|
|
916
|
+
receiptPath,
|
|
917
|
+
encoded({
|
|
918
|
+
schema: "cadence-registry-recovery/1",
|
|
919
|
+
lock: state.lock.bytes.toString("utf8"),
|
|
920
|
+
pid: process.pid,
|
|
921
|
+
host: hostname(),
|
|
922
|
+
digest: expected.coordinationDigest,
|
|
923
|
+
}),
|
|
924
|
+
32768,
|
|
925
|
+
);
|
|
926
|
+
// create fsyncs/closes the original receipt before any release effect.
|
|
927
|
+
assertAuthority();
|
|
928
|
+
const current = this.registryRecoveryState();
|
|
929
|
+
if (
|
|
930
|
+
current.stateDigest !== state.stateDigest ||
|
|
931
|
+
current.inspection.owner !== "absent" ||
|
|
932
|
+
current.attempts.length !== state.attempts.length + 1
|
|
933
|
+
)
|
|
934
|
+
throw new ContractError("conflict");
|
|
935
|
+
for (const [n, attempt] of state.attempts.entries())
|
|
936
|
+
this.files.assert(join(state.archive, `${n}.json`), attempt, 32768);
|
|
937
|
+
this.files.assert(receiptPath, receipt, 32768);
|
|
938
|
+
assertAuthority();
|
|
939
|
+
this.files.remove(this.registryPath("registry.lock"), state.lock);
|
|
940
|
+
return {
|
|
941
|
+
status: "resolved",
|
|
942
|
+
scope: "global-registry",
|
|
943
|
+
recoveryId: expected.epoch,
|
|
944
|
+
notice: "registry-only-project-fences-preserved",
|
|
945
|
+
} as const;
|
|
946
|
+
}
|
|
699
947
|
/** Capture exact private resources BEFORE the append/confirmation awaits. The
|
|
700
948
|
* closure is internal local management authority, never a wire capability.
|
|
701
949
|
* A durable bridge must be verified by the caller before EVERY release effect. */
|