@abelxiaoxing/cadence 2.0.14 → 2.0.15

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 CHANGED
@@ -332,6 +332,11 @@ Use their current declarations rather than copying DTO parsing, reducers, a seco
332
332
  Ordinary-user Git trust behavior remains unchanged.
333
333
  System services require uid0, bypass user managers/logind entirely and require confirmed native Stop before manager stop or definition replacement; changed state-home inputs, old user units and detached running owners refuse implicit adoption.
334
334
  Registry reservation atomically rejects canonical ancestor/descendant overlaps, including nested Git roots and aliases; a different state-home does not coordinate arbitrary other owners.
335
+ - The short global registry lock never expires or auto-reclaims from PID absence.
336
+ `recover inspect|resolve` handles a valid dead same-host lock explicitly before project recovery, with fresh local-terminal acknowledgments, exact registry/unreserved-project checks and a flushed exclusive evidence receipt.
337
+ Concurrent or interrupted resolvers cannot remove a successor lock; all project reservations, locks and history remain intact.
338
+ Unreserved partial release, unsafe/corrupt registry or receipt bytes, live/remote/ambiguous holders and changed resources stay fenced.
339
+ Readonly diagnosis and project Open name `registry-locked`; see [global registry recovery](docs/service-management.md#residual-global-registry-lock).
335
340
  - Native entry requires fresh raw Enter within1500ms, trusted/idle TUI, no pending modal/messages and no child guard.
336
341
  Browser authentication grants access, not task consent; normal mutation ingress also requires independently built compatible client/service/assets identities.
337
342
  - Boot model choice is absent until explicitly selected.
@@ -450,6 +455,7 @@ Byte values are UTF-8/encoded bounds where specified; they are not targets to si
450
455
  | Navigation | 8192 encoded ASCII bytes; encrypted plaintext6000 bytes; no execution authority |
451
456
  | Live/export | 16 viewers,256 messages/1MiB each; two search/two export readers;120s download idle, not total archive timeout |
452
457
  | Storage | 10GiB checkout quota;512MiB reserved per episode;256MiB free-space floor; no TTL cleanup |
458
+ | Registry recovery | 1024 namespace entries; 128 immutable attempts per lock incarnation; no evidence eviction |
453
459
  | Unified workbench | 128 associated projects; 1024 durable opening receipts; 64 connections; bounded proxy frames and bodies; no eviction |
454
460
  | Assets | 16MiB per member/64MiB closure; fixed complete manifest and all font bodies |
455
461
 
@@ -505,7 +511,7 @@ Preserve failed attempts; correct a demonstrated cause before rerunning, and nev
505
511
  Regression owners:
506
512
 
507
513
  - `service-contract`, `service-current`, `service-model-choice`, `service-auth`: closed DTOs, exact source/choice/current authority, security and compatibility.
508
- - `service-owner`, `service-accepted-work`, `service-stop-continuation`, `service-recovery`: original operations, overlap, Stop, unknown fences and informed recovery.
514
+ - `service-owner`, `service-accepted-work`, `service-stop-continuation`, `service-recovery`, `service-registry-recovery`: original operations, overlap, Stop, unknown fences and informed recovery.
509
515
  - `service-scheduler`, `service-checks`, `service-human-review`, `service-accepting-host`: real SDK overlap/exclusion, native checks, review and publication.
510
516
  - `worker-*`, `lightweight-sdk`, `lightweight-process-diff`: genuine SDK/tool capture, split redaction, no-spill bounds and original native facts.
511
517
  - `service-history-*`: durable ordering, cold/no-index/restarted proof/search/export, corruption/torn/quota and expected-byte publication.
@@ -536,6 +542,21 @@ Review the independently enumerated package member list and every emitted body,
536
542
  Keep public Pi peers external for the extension, the exact Undici/ws runtime edges, the exact optional SDK pins for the standalone owner and all upstream/font/vendor notices.
537
543
  Preserve original license bytes through their precise Git attributes; line-ending normalization must not invalidate audited source/notice hashes.
538
544
 
545
+ ### Release 2.0.15 acceptance snapshot
546
+
547
+ The release candidate passed the complete root suite: 2034 tests in 85 files, with 27 platform skips on Linux.
548
+ The final browser-only frame-readiness adjustment then passed all 9 real-browser scenarios; 130 web tests and the 135-member real-pack audit also passed.
549
+ This is separate-lane evidence, not a claim that one final `verify` invocation was entirely green.
550
+ Observed environment: Linux x86_64, Node26.7.0, Bun1.3.14, Pi SDK0.84.3 and existing Chrome for Testing151.0.7922.34; root runs used `VITEST_MAX_WORKERS=1` without changing test deadlines.
551
+ New coverage includes actual registry-writer and resolver exits, concurrent resolvers, unchanged project fences/history, changed/linked/remote/live resources, a removed stopped workspace, piped/pasted/canceled refusal, genuine native-TTY confirmation and browser Open after explicit recovery without replay.
552
+
553
+ The first integration command was cut off by its 25-minute outer command budget while tests were still running.
554
+ The next completed root run failed one omitted browser-test provenance pin (updated) and three owner/proxy settlement or timeout cases, which passed separately; their timing cause was not established.
555
+ A later complete root/web pass encountered Chromium's first screenshot-capture failure; the browser fixture now waits for painted frames before that capture, without retrying operations or swallowing capture errors, and all browser scenarios passed afterward.
556
+ The last full attempt hit six history-test deadlines in two files; those unchanged files passed all 45 tests in a subsequent diagnostic run, and the earlier complete root pass remains distinct from that later failure.
557
+ Failed logs and isolated fixture evidence were preserved; no test budget was increased, dependency/browser downloaded, consumer lock deleted or service installed/restarted.
558
+ No paid-provider use or new native Windows/macOS qualification is claimed.
559
+
539
560
  ### Release 2.0.14 acceptance snapshot
540
561
 
541
562
  Full `verify` passed on the release candidate before this evidence-only note, with `VITEST_MAX_WORKERS=1`: 2000 root tests in 84 files (27 skipped on Linux), 130 web tests, 9 real-browser scenarios and the 135-member real-pack audit.
@@ -258,7 +258,7 @@ At sign-out or shutdown Windows ends the owner without orderly Stop, which can l
258
258
  Ctrl-C, SIGTERM, machine crash or terminal destruction is **not** the authenticated orderly Stop route and can leave recovery fences.
259
259
  A recovered `stopped` registration means acknowledged fence release, not retroactive native closure certification.
260
260
 
261
- `diagnose` reports fixed pending/recording facts and `proxyAuth: direct|validated-proxy`.
261
+ `diagnose` reports fixed pending/recording facts and `proxyAuth: direct|validated-proxy` when the owner is reachable; a blocked global registry instead reports its fixed lock reason and local recovery hint.
262
262
  No normal status/diagnosis prints access secrets, provider credentials or outbound proxy URLs.
263
263
  Keep the explicitly revealed token link private.
264
264
  Browser entry redirects to a token-free URL and uses an HttpOnly cookie; that does not retract a link already copied into history, a clipboard or external logs.
@@ -300,8 +300,46 @@ cadence recover inspect --workspace /absolute/project
300
300
  cadence recover resolve --workspace /absolute/project
301
301
  ```
302
302
 
303
- Inspection is readonly.
304
- It displays the epoch, exact coordination/history digests, owner PID/host observation and independent process/credential/storage uncertainty.
303
+ Inspection is readonly and handles the global registry lock before project-owner recovery.
304
+
305
+ ### Residual global registry lock
306
+
307
+ `status` and `diagnose` report `reason: "registry-locked"` and the lock holder observation (`live`, `absent`, `ambiguous` or `unreadable`).
308
+ Project Open reports `registry-locked` instead of an opaque `unavailable`; the folder picker explains the local recovery command.
309
+ This lock is shared by every project in the selected state home, so trying a different project does not bypass it.
310
+ Use the same account and `--state-home` as the owner; omit that flag only when it uses the default state home:
311
+
312
+ ```sh
313
+ cadence recover inspect --workspace /absolute/project --state-home /absolute/state-home
314
+ cadence recover resolve --workspace /absolute/project --state-home /absolute/state-home
315
+ ```
316
+
317
+ With a registry lock present, inspection reports `scope: "global-registry"`, the exact lock-incarnation digest, holder PID/host, reservation count and any previous resolver's liveness.
318
+ A project with no selected owner can be used to address this global operation; it still must be an existing suitable directory.
319
+ Resolve requires the same four fresh local-terminal acknowledgments described below, with the displayed lock-incarnation digest instead of a service epoch.
320
+ The holder must be on this host and its PID probe must specifically report absence; age, a remote host, permission denial or a reused live PID never authorizes release.
321
+
322
+ Recovery validates the registry and checks that unreserved project selections have no remaining owner/checkout fences.
323
+ A normally stopped project whose working directory was later removed is checked through its retained private metadata; recovery neither requires nor reconstructs that directory.
324
+ An interrupted reservation remains reserved even when its project initialization is incomplete: unrelated projects can reopen, but that project's uncertain resources are not repaired.
325
+ An older release interrupted after removing a reservation but before releasing its project locks is unsafe and remains blocked, as do corrupt/partial registry bytes, linked files and changed resources.
326
+ Inspection reports `blocked-preserve-state` when it cannot establish those conditions; preserve that state for manual diagnosis rather than deleting locks.
327
+
328
+ Before releasing only `registry.lock`, the resolver exclusively creates, flushes and closes a private receipt under `service-registry-v1/recovery-v1/<lock-incarnation>/<attempt>.json`, retaining the original lock bytes and acknowledged digest.
329
+ It then rechecks the lock incarnation, registry, unreserved project selections, receipt and terminal authority.
330
+ Concurrent resolvers cannot release a successor lock.
331
+ If a resolver dies after the receipt but before release, a new inspection and fresh terminal confirmation may append the next attempt after that resolver's same-host PID is absent; neither receipt nor lock is silently reclaimed.
332
+ Torn or unsafe receipts remain preserved and refused.
333
+ Inspection is bounded to 1024 namespace entries and 128 attempts for one lock incarnation; it never evicts evidence to make space.
334
+
335
+ All project reservations, owner locks, checkout locks, capabilities, history and unknown outcomes remain unchanged.
336
+ No Open/Start is replayed, no process is signaled, and no service is installed or started by recovery.
337
+ After success, explicitly open the desired project again.
338
+ If its own owner is still fenced, run `recover inspect` again and follow project-owner recovery separately; restoring global registry access does not certify or release that owner's work.
339
+
340
+ ### Project-owner recovery
341
+
342
+ With no registry lock present, inspection displays the epoch, exact coordination/history digests, owner PID/host observation and independent process/credential/storage uncertainty.
305
343
  Those digests identify preserved resources, not successful work or credential validity.
306
344
  A journal with no unmatched intent does not prove every escaped descendant or external credential writer stopped.
307
345
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abelxiaoxing/cadence",
3
- "version": "2.0.14",
3
+ "version": "2.0.15",
4
4
  "description": "Shared Pi task workspace: native terminal and WebUI, persistent service, checks and human review.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,5 +2,5 @@
2
2
  export const nativeBuild = {
3
3
  kind: "native",
4
4
  contractHash:
5
- "9c917c085d034e08764de359cee49aea748c395a63ed629fe67d149b97850fa1",
5
+ "944f3b53bd5b7f1d3f30868e0947d900e356ae9a2bcfb9f591edd988b3d5eb3a",
6
6
  } as const;
@@ -150,6 +150,42 @@ function names(path: string, allowed: readonly string[]): void {
150
150
  dir.closeSync();
151
151
  }
152
152
  }
153
+ interface RegistryLock {
154
+ schema: "cadence-service-registry-lock/1";
155
+ token: string;
156
+ pid: number;
157
+ host: string;
158
+ }
159
+ function decodeRegistryLock(snapshot: FileSnapshot): RegistryLock {
160
+ const o = closed(json(snapshot), ["schema", "token", "pid", "host"]);
161
+ return {
162
+ schema: literal(o.schema, "cadence-service-registry-lock/1"),
163
+ token: decodeId(o.token),
164
+ // kill(0)/negative process-group probes must never be possible.
165
+ pid: integer(o.pid, 1, 2147483647),
166
+ host: text(o.host, 255, true),
167
+ };
168
+ }
169
+ export interface RegistryLockDiagnostic {
170
+ reason: "registry-locked";
171
+ owner: "live" | "absent" | "ambiguous" | "unreadable";
172
+ }
173
+ export interface RegistryRecoveryInspection {
174
+ scope: "global-registry";
175
+ epoch: string; // exact lock incarnation, not a service epoch
176
+ coordinationDigest: string;
177
+ owner: "live" | "absent" | "ambiguous";
178
+ process: { pid: number; host: string };
179
+ reservations: number;
180
+ previousResolver: "live" | "absent" | "ambiguous" | null;
181
+ resolution: "local-interactive-only";
182
+ }
183
+ const registryNames = [
184
+ "format.json",
185
+ "roots.json",
186
+ "registry.lock",
187
+ "recovery-v1",
188
+ ];
153
189
  interface RegistryView {
154
190
  format: FileSnapshot;
155
191
  snapshot: FileSnapshot;
@@ -240,11 +276,7 @@ export class ServiceCoordination {
240
276
  }
241
277
  private registry(): RegistryView | undefined {
242
278
  if (!this.files.directory(this.registryDirectory)) return undefined;
243
- names(this.registryDirectory, [
244
- "format.json",
245
- "roots.json",
246
- "registry.lock",
247
- ]);
279
+ names(this.registryDirectory, registryNames);
248
280
  const format = this.files.read(this.registryPath("format.json"));
249
281
  const snapshot = this.files.read(
250
282
  this.registryPath("roots.json"),
@@ -283,9 +315,9 @@ export class ServiceCoordination {
283
315
  },
284
316
  };
285
317
  }
286
- assertNamespace(walks?: DirectoryWalks): void {
287
- if (!this.files.directory(this.directory, false, walks)) refused();
288
- names(this.directory, [
318
+ assertNamespace(walks?: DirectoryWalks, directory = this.directory): void {
319
+ if (!this.files.directory(directory, false, walks)) refused();
320
+ names(directory, [
289
321
  "format.json",
290
322
  "config.json",
291
323
  "registration.json",
@@ -297,18 +329,19 @@ export class ServiceCoordination {
297
329
  "history-v2",
298
330
  "conversations-v1",
299
331
  ]);
300
- this.files.directory(join(this.directory, "history-v2"), false, walks);
332
+ this.files.directory(join(directory, "history-v2"), false, walks);
301
333
  }
302
- private selected(): Selected | undefined {
334
+ private selected(
335
+ directory = this.directory,
336
+ root = this.store.root,
337
+ stoppedOnly = false,
338
+ ): Selected | undefined {
303
339
  const walks = new DirectoryWalks();
304
- if (!this.files.directory(this.directory, false, walks)) return undefined;
305
- this.assertNamespace(walks);
306
- const manifest = this.files.read(
307
- this.path("format.json"),
308
- undefined,
309
- walks,
310
- );
311
- const config = this.files.read(this.path("config.json"), undefined, walks);
340
+ if (!this.files.directory(directory, false, walks)) return undefined;
341
+ this.assertNamespace(walks, directory);
342
+ const path = (name: string) => join(directory, name);
343
+ const manifest = this.files.read(path("format.json"), undefined, walks);
344
+ const config = this.files.read(path("config.json"), undefined, walks);
312
345
  if (!manifest || !config) refused();
313
346
  const f = closed(json(manifest), [
314
347
  "schema",
@@ -317,39 +350,45 @@ export class ServiceCoordination {
317
350
  "configIdentity",
318
351
  ]);
319
352
  literal(f.schema, "cadence-service-format/1");
320
- literal(f.root, this.store.root);
353
+ literal(f.root, root);
321
354
  literal(f.stateHome, this.stateHome);
322
355
  literal(f.configIdentity, config.identity);
323
356
  const c = decodeOwnerConfig(json(config));
324
357
  const hostConfig = this.files.read(
325
- this.path("host-config.json"),
358
+ path("host-config.json"),
326
359
  undefined,
327
360
  walks,
328
361
  );
329
362
  if (hostConfig && hostConfig.identity !== c.policyIdentity) refused();
330
363
  const registration = this.files.read(
331
- this.path("registration.json"),
364
+ path("registration.json"),
332
365
  undefined,
333
366
  walks,
334
367
  );
335
- const owner = this.files.read(this.path("owner.lock"), undefined, walks);
368
+ const owner = this.files.read(path("owner.lock"), undefined, walks);
336
369
  const capability = this.files.read(
337
- this.path("local-capability.json"),
370
+ path("local-capability.json"),
338
371
  undefined,
339
372
  walks,
340
373
  );
341
- const checkout = this.files.read(this.store.lockPath, 8192, walks);
374
+ const checkout = this.files.read(
375
+ join(dirname(directory), "checkout.lock"),
376
+ 8192,
377
+ walks,
378
+ );
342
379
  const value = registration
343
380
  ? decodeRegistration(json(registration))
344
381
  : undefined;
345
382
  if (
346
383
  value &&
347
- (value.root !== this.store.root ||
384
+ (value.root !== root ||
348
385
  value.stateHome !== this.stateHome ||
349
386
  value.configIdentity !== config.identity ||
350
387
  !endpointMatches(c.endpoint, value.endpoint))
351
388
  )
352
389
  refused();
390
+ if (stoppedOnly && (checkout || (value && value.phase !== "stopped")))
391
+ throw new ContractError("quarantine");
353
392
  if (value && value.phase !== "stopped") {
354
393
  if (!owner || !checkout || !capability) refused();
355
394
  const lock = closed(json(owner), [
@@ -448,21 +487,24 @@ export class ServiceCoordination {
448
487
  ) => T,
449
488
  ): T {
450
489
  this.files.directory(this.registryDirectory, true);
451
- names(this.registryDirectory, [
452
- "format.json",
453
- "roots.json",
454
- "registry.lock",
455
- ]);
490
+ names(this.registryDirectory, registryNames);
456
491
  const lockPath = this.registryPath("registry.lock");
457
- const lock = this.files.create(
458
- lockPath,
459
- encoded({
460
- schema: "cadence-service-registry-lock/1",
461
- token: randomUUID(),
462
- pid: process.pid,
463
- host: hostname(),
464
- }),
465
- );
492
+ let lock: FileSnapshot;
493
+ try {
494
+ lock = this.files.create(
495
+ lockPath,
496
+ encoded({
497
+ schema: "cadence-service-registry-lock/1",
498
+ token: randomUUID(),
499
+ pid: process.pid,
500
+ host: hostname(),
501
+ }),
502
+ );
503
+ } catch (error) {
504
+ if (error instanceof Error && "code" in error && error.code === "EEXIST")
505
+ throw new ContractError("registry-locked");
506
+ throw error;
507
+ }
466
508
  let dirty = false;
467
509
  try {
468
510
  let format = this.files.read(this.registryPath("format.json"));
@@ -499,11 +541,7 @@ export class ServiceCoordination {
499
541
  () => {
500
542
  this.files.assert(lockPath, lock);
501
543
  this.files.assert(this.registryPath("format.json"), format);
502
- names(this.registryDirectory, [
503
- "format.json",
504
- "roots.json",
505
- "registry.lock",
506
- ]);
544
+ names(this.registryDirectory, registryNames);
507
545
  },
508
546
  );
509
547
  this.files.remove(lockPath, lock);
@@ -696,6 +734,199 @@ export class ServiceCoordination {
696
734
  });
697
735
  });
698
736
  }
737
+ /** Readonly and secret-free; never probes another host or reclaims on age. */
738
+ registryLockDiagnostic(): RegistryLockDiagnostic | undefined {
739
+ try {
740
+ const lock = this.files.read(this.registryPath("registry.lock"));
741
+ if (!lock) return undefined;
742
+ const value = decodeRegistryLock(lock);
743
+ return {
744
+ reason: "registry-locked",
745
+ owner: recoveryOwnerStatus(value.pid, value.host),
746
+ };
747
+ } catch {
748
+ return { reason: "registry-locked", owner: "unreadable" };
749
+ }
750
+ }
751
+ private registryRecoveryState() {
752
+ const lock = this.files.read(this.registryPath("registry.lock"));
753
+ if (!lock) throw new ContractError("conflict");
754
+ const value = decodeRegistryLock(lock);
755
+ names(this.registryDirectory, registryNames);
756
+ const format = this.files.read(this.registryPath("format.json"));
757
+ const roots = this.files.read(
758
+ this.registryPath("roots.json"),
759
+ REGISTRY_BYTES,
760
+ );
761
+ // A crash immediately after exclusive creation may precede initialization.
762
+ const registry = !format && !roots ? undefined : this.registry();
763
+ const entries = registry?.value.entries ?? [];
764
+ const protectedRoots = new Set(entries.map((row) => hash(row.root)));
765
+ const proofs: unknown[] = [];
766
+ // Older releases removed a reservation BEFORE the checkout fence. A dead
767
+ // short-lock PID cannot make those unreserved, partially released projects
768
+ // safe. Verify every unreserved service selection; never repair it here.
769
+ const directory = opendirSync(this.files.namespace);
770
+ try {
771
+ let count = 0;
772
+ for (
773
+ let entry = directory.readSync();
774
+ entry;
775
+ entry = directory.readSync()
776
+ ) {
777
+ if (++count > 1024) throw new ContractError("capacity");
778
+ if (
779
+ !/^[a-f0-9]{64}$/.test(entry.name) ||
780
+ protectedRoots.has(entry.name)
781
+ )
782
+ continue;
783
+ const service = join(this.files.namespace, entry.name, "service-v1");
784
+ if (!this.files.directory(service)) continue;
785
+ const manifest = this.files.read(join(service, "format.json"));
786
+ if (!manifest) refused();
787
+ const f = closed(json(manifest), [
788
+ "schema",
789
+ "root",
790
+ "stateHome",
791
+ "configIdentity",
792
+ ]);
793
+ const root = pathValue(f.root);
794
+ if (hash(root) !== entry.name) refused();
795
+ // Retained stopped metadata remains checkable even when the consumer
796
+ // deleted/moved that old working directory. Never resolve/recreate it.
797
+ const selected = this.selected(service, root, true);
798
+ if (!selected) throw new ContractError("quarantine");
799
+ proofs.push([
800
+ entry.name,
801
+ ...[
802
+ selected.manifest,
803
+ selected.config,
804
+ selected.hostConfig,
805
+ selected.registration,
806
+ ].map((file) => (file ? [file.identity, file.stamp] : null)),
807
+ ]);
808
+ }
809
+ } finally {
810
+ directory.closeSync();
811
+ }
812
+ proofs.sort((a, b) =>
813
+ JSON.stringify(a).localeCompare(JSON.stringify(b), "en"),
814
+ );
815
+ const stateDigest = hash(
816
+ encoded([
817
+ lock.identity,
818
+ lock.stamp,
819
+ format ? [format.identity, format.stamp] : null,
820
+ roots ? [roots.identity, roots.stamp] : null,
821
+ proofs,
822
+ ]),
823
+ );
824
+ const epoch = hash(encoded([lock.identity, lock.stamp]));
825
+ const archive = this.registryPath(join("recovery-v1", epoch));
826
+ const attempts: FileSnapshot[] = [];
827
+ let previousResolver: RegistryRecoveryInspection["previousResolver"] = null;
828
+ if (this.files.directory(archive)) {
829
+ // Immutable, exclusive attempts serialize resolvers. If one crashes before
830
+ // release, a fresh confirmation may append the next attempt ONLY after its
831
+ // PID is absent. No stale secondary lock needs deletion or takeover.
832
+ names(
833
+ archive,
834
+ Array.from({ length: 128 }, (_, n) => `${n}.json`),
835
+ );
836
+ for (let n = 0; n < 128; n++) {
837
+ const attempt = this.files.read(join(archive, `${n}.json`), 32768);
838
+ if (!attempt) {
839
+ names(
840
+ archive,
841
+ attempts.map((_, i) => `${i}.json`),
842
+ );
843
+ break;
844
+ }
845
+ const o = closed(json(attempt, 32768), [
846
+ "schema",
847
+ "lock",
848
+ "pid",
849
+ "host",
850
+ "digest",
851
+ ]);
852
+ literal(o.schema, "cadence-registry-recovery/1");
853
+ literal(o.lock, lock.bytes.toString("utf8"));
854
+ decodeHash(o.digest);
855
+ previousResolver = recoveryOwnerStatus(
856
+ integer(o.pid, 1, 2147483647),
857
+ text(o.host, 255, true),
858
+ );
859
+ attempts.push(attempt);
860
+ }
861
+ }
862
+ const inspection: RegistryRecoveryInspection = {
863
+ scope: "global-registry",
864
+ epoch,
865
+ coordinationDigest: hash(
866
+ encoded([stateDigest, attempts.map((a) => [a.identity, a.stamp])]),
867
+ ),
868
+ owner: recoveryOwnerStatus(value.pid, value.host),
869
+ process: { pid: value.pid, host: value.host },
870
+ reservations: entries.length,
871
+ previousResolver,
872
+ resolution: "local-interactive-only",
873
+ };
874
+ return { lock, archive, attempts, stateDigest, inspection };
875
+ }
876
+ inspectRegistryRecovery(): RegistryRecoveryInspection {
877
+ return this.registryRecoveryState().inspection;
878
+ }
879
+ /** Local native management only. Retire just the proven dead short lock after
880
+ * durable evidence; reservations, owner/checkout locks and history stay intact. */
881
+ resolveRegistryRecovery(
882
+ expected: RegistryRecoveryInspection,
883
+ assertAuthority: () => void,
884
+ ) {
885
+ assertAuthority();
886
+ const state = this.registryRecoveryState();
887
+ if (encoded(state.inspection) !== encoded(expected))
888
+ throw new ContractError("conflict");
889
+ if (
890
+ expected.owner !== "absent" ||
891
+ (expected.previousResolver !== null &&
892
+ expected.previousResolver !== "absent")
893
+ )
894
+ throw new ContractError("quarantine");
895
+ if (state.attempts.length >= 128) throw new ContractError("capacity");
896
+ assertAuthority();
897
+ const receiptPath = join(state.archive, `${state.attempts.length}.json`);
898
+ const receipt = this.files.create(
899
+ receiptPath,
900
+ encoded({
901
+ schema: "cadence-registry-recovery/1",
902
+ lock: state.lock.bytes.toString("utf8"),
903
+ pid: process.pid,
904
+ host: hostname(),
905
+ digest: expected.coordinationDigest,
906
+ }),
907
+ 32768,
908
+ );
909
+ // create fsyncs/closes the original receipt before any release effect.
910
+ assertAuthority();
911
+ const current = this.registryRecoveryState();
912
+ if (
913
+ current.stateDigest !== state.stateDigest ||
914
+ current.inspection.owner !== "absent" ||
915
+ current.attempts.length !== state.attempts.length + 1
916
+ )
917
+ throw new ContractError("conflict");
918
+ for (const [n, attempt] of state.attempts.entries())
919
+ this.files.assert(join(state.archive, `${n}.json`), attempt, 32768);
920
+ this.files.assert(receiptPath, receipt, 32768);
921
+ assertAuthority();
922
+ this.files.remove(this.registryPath("registry.lock"), state.lock);
923
+ return {
924
+ status: "resolved",
925
+ scope: "global-registry",
926
+ recoveryId: expected.epoch,
927
+ notice: "registry-only-project-fences-preserved",
928
+ } as const;
929
+ }
699
930
  /** Capture exact private resources BEFORE the append/confirmation awaits. The
700
931
  * closure is internal local management authority, never a wire capability.
701
932
  * A durable bridge must be verified by the caller before EVERY release effect. */