@dsh-electron/dsh-plugin-multi-root-workspace 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.en.md +13 -1
  2. package/CHANGELOG.md +13 -1
  3. package/README.en.md +9 -8
  4. package/README.md +9 -8
  5. package/cordis.patch.yml +3 -2
  6. package/lib/auto-review.js +1 -1
  7. package/lib/client.js +96 -49
  8. package/lib/client.js.map +1 -1
  9. package/lib/{command-BGdz7VBH.js → command-CmLekG1M.js} +3 -2
  10. package/lib/command.js +1 -1
  11. package/lib/{compat-BiW-m_q0.js → compat-Bg2HFVMX.js} +3 -0
  12. package/lib/compat.js +1 -1
  13. package/lib/{sandbox-CXk5h-ct.js → dialects-Dr4rDR-H.js} +6 -264
  14. package/lib/{fs-BTXySwSG.js → fs-CfzJq2wd.js} +23 -8
  15. package/lib/fs.js +1 -1
  16. package/lib/index.js +7 -6
  17. package/lib/{instructions-MePoRigy.js → instructions-KsUwUFE0.js} +53 -37
  18. package/lib/instructions.js +1 -1
  19. package/lib/lsp.js +19 -16
  20. package/lib/native-win32-2waCRkPI.js +312 -0
  21. package/lib/process-fence.js +62 -12
  22. package/lib/protocol-4zdwV7u4.js +641 -0
  23. package/lib/{registry-KSsmU393.js → registry-YTUOjmVu.js} +368 -25
  24. package/lib/registry-operation-C2ZV3Jhw.js +116 -0
  25. package/lib/registry.js +3 -2
  26. package/lib/{review-context-AH-SXHmj.js → review-context-NDpKeMCc.js} +1 -1
  27. package/lib/review-context.js +1 -1
  28. package/lib/sandbox-C-vCJCp3.js +278 -0
  29. package/lib/sandbox.js +1 -1
  30. package/lib/scope.js +65 -9
  31. package/lib/skills.js +48 -29
  32. package/lib/types/authority/native-win32.d.ts +7 -0
  33. package/lib/types/authority/native.d.ts +23 -0
  34. package/lib/types/authority/protocol.d.ts +70 -0
  35. package/lib/types/compat/authority-storage.d.ts +6 -0
  36. package/lib/types/compat/dsh-version.d.ts +4 -4
  37. package/lib/types/compat/open-in-app-device.d.ts +1 -1
  38. package/lib/types/compat/process-entrypoints.d.ts +2 -0
  39. package/lib/types/compat/process-profile.d.ts +3 -0
  40. package/lib/types/compat/sandbox-confine.d.ts +2 -0
  41. package/lib/types/fs.d.ts +4 -1
  42. package/lib/types/instructions.d.ts +0 -47
  43. package/lib/types/registry-operation.d.ts +31 -0
  44. package/lib/types/registry.d.ts +47 -6
  45. package/lib/types/scope.d.ts +10 -0
  46. package/lib/types/skills/catalog.d.ts +3 -0
  47. package/lib/types/skills/pool.d.ts +1 -1
  48. package/lib/workspace-files.js +63 -7
  49. package/package.json +38 -31
@@ -1,13 +1,53 @@
1
+ import { a as runPrimaryOperation, n as freezeOperationScope, o as runRegistryOperation } from "./registry-operation-C2ZV3Jhw.js";
1
2
  import { i as isWindowsNetworkOrDevicePath } from "./windows-runner-contract-B5R2K3mM.js";
2
3
  import { a as classifyStoredRoots, d as reservedRootBoundary, f as resolveRootIndex, i as canonicalRoot, m as validateRootCandidate, n as additionalRootId, r as availableRoots, s as expandRootInput, t as RootValidationError, u as removeStatusAt } from "./roots-Ca_W1S9n.js";
3
4
  import { r as validateAliasId, t as migrateRootSkills } from "./metadata-DgqSdVX_.js";
5
+ import { a as snapshotSchema, i as parseArguments, r as AuthorityServer, s as storeIdentity, t as AuthorityClient } from "./protocol-4zdwV7u4.js";
4
6
  import { createRequire } from "node:module";
5
7
  import { closeSync, fstatSync, mkdirSync, openSync, statSync } from "node:fs";
6
8
  import { randomUUID } from "node:crypto";
7
- import { dirname, isAbsolute } from "node:path";
9
+ import { basename, dirname, isAbsolute } from "node:path";
8
10
  import { Service } from "@deepseek-ai/cordis";
9
- import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
11
+ import { DomainFacility, defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
10
12
  import { tryLockExclusive } from "@deepseek-ai/node-addon-system/flock";
13
+ //#region src/compat/authority-storage.ts
14
+ /** Open an explicitly bound medium through public storage APIs only. */
15
+ async function openAuthorityStorage(ctx, root, spec) {
16
+ const { JsonStorageBackend } = await import("@deepseek-ai/dsh-storage-json");
17
+ const backend = new JsonStorageBackend(root);
18
+ const name = `multi-root-${randomUUID()}`;
19
+ const unmount = ctx.get("storage").backend.register(name, backend);
20
+ let facility;
21
+ const fiber = ctx.inject(["storage"], (scope) => {
22
+ facility = new DomainFacility(scope, {
23
+ backend: name,
24
+ routes: {}
25
+ });
26
+ });
27
+ try {
28
+ await fiber;
29
+ if (!facility) throw new Error("shared registry requires the storage hub");
30
+ const domain = await facility.open(spec);
31
+ return {
32
+ table: domain.table.bind(domain),
33
+ async close() {
34
+ try {
35
+ await domain.close();
36
+ } finally {
37
+ unmount();
38
+ await backend.close();
39
+ await fiber.dispose();
40
+ }
41
+ }
42
+ };
43
+ } catch (error) {
44
+ unmount();
45
+ await backend.close();
46
+ await fiber.dispose();
47
+ throw error;
48
+ }
49
+ }
50
+ //#endregion
11
51
  //#region src/compat/storage-zod.ts
12
52
  /**
13
53
  * Build the domain record schemas with the zod module the installed
@@ -209,8 +249,8 @@ var RegistryAuthorityLease = class RegistryAuthorityLease {
209
249
  * only when the operator removes it (or when the store is repaired). Storage
210
250
  * that cannot be parsed at all is backed up and skipped by the domain layer,
211
251
  * with a warning.
212
- * - **Mutations are serialized per primary root.** Every mutation runs its
213
- * whole read → validate → persist sequence inside one queue per key. The
252
+ * - **Mutations and fs admissions are serialized store-wide.** Every mutation
253
+ * runs its whole read → validate → persist sequence inside the store queue. The
214
254
  * storage domain serializes individual `put()`/`delete()` calls, which is not
215
255
  * enough: two concurrent `add()` calls would both read the same snapshot and
216
256
  * the later write would drop the earlier one, and a removal racing an add
@@ -296,6 +336,16 @@ var MultiRootRegistry = class extends Service {
296
336
  ];
297
337
  /** Absolute lock path; absent means the row was composed without `leasePath`. */
298
338
  leasePath;
339
+ sharedStorageRoot;
340
+ authorityStorageContext;
341
+ medium;
342
+ server;
343
+ client;
344
+ connectionFailure;
345
+ connectionGeneration = 0;
346
+ remoteProjectionReady = false;
347
+ remoteRevisions = /* @__PURE__ */ new Map();
348
+ operationProof = Object.freeze({});
299
349
  /** Whether this process currently owns the store. */
300
350
  authorityState = { kind: "contended" };
301
351
  /** Held kernel lease; present once acquire succeeded, including after a failed open. */
@@ -308,8 +358,12 @@ var MultiRootRegistry = class extends Service {
308
358
  cache = /* @__PURE__ */ new Map();
309
359
  /** Signature of the last published grant per key, so a no-op refresh stays a no-op. */
310
360
  published = /* @__PURE__ */ new Map();
311
- /** One promise chain per canonical primary root: mutations never interleave. */
361
+ /** Pending store-ordered work, grouped by primary for teardown accounting. */
312
362
  chains = /* @__PURE__ */ new Map();
363
+ /** Admissions and mutations share one order, including across primary roots. */
364
+ operationChain = Promise.resolve();
365
+ operationEpoch;
366
+ operationRevision = 0;
313
367
  /** Store-wide queue for lease acquire / open / release. */
314
368
  authorityChain = Promise.resolve();
315
369
  /** Change listeners, keyed by their own disposer identity. */
@@ -328,7 +382,10 @@ var MultiRootRegistry = class extends Service {
328
382
  */
329
383
  constructor(ctx, config = {}) {
330
384
  super(ctx, "multiRootRegistry");
385
+ this.authorityStorageContext = ctx;
331
386
  this.leasePath = config.leasePath;
387
+ this.sharedStorageRoot = config.sharedStorageRoot;
388
+ if (config.sharedStorageRoot !== void 0) this.ctx.multiRootScope.requireAdmission(this.operationProof);
332
389
  this.ctx.multiRootScope.setRegistryAuthority(false);
333
390
  const configured = config.maxAdditionalRoots === void 0 ? 16 : config.maxAdditionalRoots;
334
391
  if (Number.isInteger(configured) && configured >= 1 && configured <= 64) this.maxRoots = configured;
@@ -367,8 +424,9 @@ var MultiRootRegistry = class extends Service {
367
424
  */
368
425
  get unavailable() {
369
426
  switch (this.authorityState.kind) {
370
- case "active": return;
371
- case "contended": return REGISTRY_CONTENDED_MESSAGE;
427
+ case "active":
428
+ case "follower": return;
429
+ case "contended": return this.shared && this.connectionFailure ? `shared registry is unavailable: ${this.connectionFailure}` : REGISTRY_CONTENDED_MESSAGE;
372
430
  case "storage-failed":
373
431
  case "config-invalid": return this.authorityState.reason;
374
432
  }
@@ -377,6 +435,15 @@ var MultiRootRegistry = class extends Service {
377
435
  * Whether this process is the Registry Authority for the store-wide medium.
378
436
  * Tests and diagnostics read this; surfaces use {@link unavailable}.
379
437
  */
438
+ get epoch() {
439
+ return this.authorityState.kind === "active" ? this.operationEpoch : this.client?.epoch;
440
+ }
441
+ get shared() {
442
+ return this.sharedStorageRoot !== void 0;
443
+ }
444
+ get available() {
445
+ return this.authorityState.kind === "active" || this.authorityState.kind === "follower";
446
+ }
380
447
  get authority() {
381
448
  return this.authorityState;
382
449
  }
@@ -430,11 +497,47 @@ var MultiRootRegistry = class extends Service {
430
497
  */
431
498
  async refresh(primaryRoot) {
432
499
  await this.ensureAuthority();
500
+ if (this.client) try {
501
+ return await this.remote("snapshot", [primaryRoot]);
502
+ } catch (error) {
503
+ if (this.client) throw error;
504
+ await this.ensureAuthority();
505
+ if (this.client) return await this.remote("snapshot", [primaryRoot]);
506
+ }
433
507
  if (this.authorityState.kind !== "active") return [];
434
508
  const key = canonicalRoot(primaryRoot);
435
509
  return await this.serialize(key, () => this.reclassify(key));
436
510
  }
437
511
  /**
512
+ * Admit one fs invocation in the same order as mutations, then run its I/O
513
+ * OUTSIDE the queue. Revocation does not wait for already-admitted work.
514
+ * Shared followers obtain the same ordered snapshot through attested IPC.
515
+ */
516
+ async withOperation(policy, kind, body) {
517
+ if (this.disposed) throw new Error("multi-root registry: cannot admit an operation after disposal");
518
+ const capturedPolicy = structuredClone(policy);
519
+ await this.ensureAuthority();
520
+ const key = canonicalRoot(capturedPolicy.workspaceRoot);
521
+ if (this.shared && this.available) {
522
+ const snapshot = this.client ? await this.remoteSnapshot("admit", [key]) : await this.captureSnapshot(key);
523
+ const scope = freezeOperationScope(this.ctx.multiRootScope.resolveSnapshot(capturedPolicy, this.projectRoots(snapshot.roots), snapshot.available));
524
+ return await runRegistryOperation(scope, kind, snapshot.epoch, snapshot.revision, body, this.operationProof, capturedPolicy.mode);
525
+ }
526
+ const admission = await this.serialize(key, () => {
527
+ if (this.disposed) throw new Error("multi-root registry: cannot admit an operation after disposal");
528
+ return {
529
+ scope: freezeOperationScope(this.shared ? this.ctx.multiRootScope.resolveSnapshot(capturedPolicy, this.projectRoots(this.cache.get(key) ?? []), false) : this.ctx.multiRootScope.resolve(capturedPolicy)),
530
+ epoch: this.operationEpoch,
531
+ revision: this.operationRevision
532
+ };
533
+ });
534
+ return await runRegistryOperation(admission.scope, kind, admission.epoch, admission.revision, body, this.operationProof, capturedPolicy.mode);
535
+ }
536
+ withPrimaryOperation(policy, body) {
537
+ const scope = this.ctx.multiRootScope.resolveSnapshot(policy, this.projectRoots(this.cache.get(canonicalRoot(policy.workspaceRoot)) ?? []), false);
538
+ return runPrimaryOperation(freezeOperationScope(scope), policy.mode, this.operationProof, body);
539
+ }
540
+ /**
438
541
  * Registers one additional root.
439
542
  * @param primaryRoot - the session workspace root the root belongs to.
440
543
  * @param input - the directory and optional alias.
@@ -443,6 +546,7 @@ var MultiRootRegistry = class extends Service {
443
546
  */
444
547
  async add(primaryRoot, input) {
445
548
  await this.ensureAuthority();
549
+ if (this.client) return await this.remote("add", [primaryRoot, input]);
446
550
  this.requireActive();
447
551
  const key = canonicalRoot(primaryRoot);
448
552
  return await this.serialize(key, async () => {
@@ -513,6 +617,11 @@ var MultiRootRegistry = class extends Service {
513
617
  */
514
618
  async setAccess(primaryRoot, ref, access) {
515
619
  await this.ensureAuthority();
620
+ if (this.client) return await this.remote("setAccess", [
621
+ primaryRoot,
622
+ ref,
623
+ access
624
+ ]);
516
625
  this.requireActive();
517
626
  const key = canonicalRoot(primaryRoot);
518
627
  return await this.serialize(key, async () => {
@@ -554,6 +663,7 @@ var MultiRootRegistry = class extends Service {
554
663
  */
555
664
  async removeAt(primaryRoot, ref) {
556
665
  await this.ensureAuthority();
666
+ if (this.client) return await this.remote("removeAt", [primaryRoot, ref]);
557
667
  this.requireActive();
558
668
  const key = canonicalRoot(primaryRoot);
559
669
  return await this.serialize(key, async () => await this.persist(key, removeStatusAt(this.reclassify(key), ref)));
@@ -568,6 +678,11 @@ var MultiRootRegistry = class extends Service {
568
678
  */
569
679
  async setAlias(primaryRoot, ref, alias) {
570
680
  await this.ensureAuthority();
681
+ if (this.client) return await this.remote("setAlias", [
682
+ primaryRoot,
683
+ ref,
684
+ alias ?? null
685
+ ]);
571
686
  this.requireActive();
572
687
  const key = canonicalRoot(primaryRoot);
573
688
  return await this.serialize(key, async () => {
@@ -587,10 +702,22 @@ var MultiRootRegistry = class extends Service {
587
702
  }
588
703
  /** Change a Skill switch without changing any grant field. */
589
704
  async setSkillsEnabled(primaryRoot, ref, skillsEnabled) {
705
+ await this.ensureAuthority();
706
+ if (this.client) return await this.remote("setSkillsEnabled", [
707
+ primaryRoot,
708
+ ref,
709
+ skillsEnabled
710
+ ]);
590
711
  if (typeof skillsEnabled !== "boolean") throw new RootValidationError("invalid-ref", "skillsEnabled must be a boolean");
591
712
  return await this.setSkillsMetadata(primaryRoot, ref, { skillsEnabled });
592
713
  }
593
714
  async setAliasId(primaryRoot, ref, aliasId) {
715
+ await this.ensureAuthority();
716
+ if (this.client) return await this.remote("setAliasId", [
717
+ primaryRoot,
718
+ ref,
719
+ aliasId
720
+ ]);
594
721
  return await this.setSkillsMetadata(primaryRoot, ref, { aliasId });
595
722
  }
596
723
  async setSkillsMetadata(primaryRoot, ref, patch) {
@@ -617,6 +744,11 @@ var MultiRootRegistry = class extends Service {
617
744
  */
618
745
  async move(primaryRoot, ref, beforeRef) {
619
746
  await this.ensureAuthority();
747
+ if (this.client) return await this.remote("move", [
748
+ primaryRoot,
749
+ ref,
750
+ beforeRef ?? null
751
+ ]);
620
752
  this.requireActive();
621
753
  const key = canonicalRoot(primaryRoot);
622
754
  return await this.serialize(key, async () => {
@@ -645,6 +777,7 @@ var MultiRootRegistry = class extends Service {
645
777
  */
646
778
  async recheck(primaryRoot) {
647
779
  await this.ensureAuthority();
780
+ if (this.client) return await this.remote("recheck", [primaryRoot]);
648
781
  this.requireActive();
649
782
  const key = canonicalRoot(primaryRoot);
650
783
  return await this.serialize(key, async () => await this.persist(key, this.reclassify(key)));
@@ -666,7 +799,10 @@ var MultiRootRegistry = class extends Service {
666
799
  * @returns the canonical paths of every `available` root.
667
800
  */
668
801
  granted(primaryRoot) {
669
- return availableRoots(this.statusesOf(canonicalRoot(primaryRoot)));
802
+ return this.shared ? this.ctx.multiRootScope.snapshot({
803
+ workspaceRoot: primaryRoot,
804
+ mode: "workspace-write"
805
+ }).additionalRoots : availableRoots(this.statusesOf(canonicalRoot(primaryRoot)));
670
806
  }
671
807
  /**
672
808
  * The scope the registry publishes: only usable roots, in registry order.
@@ -709,7 +845,12 @@ var MultiRootRegistry = class extends Service {
709
845
  */
710
846
  lookupRecords(key) {
711
847
  if (this.authorityState.kind !== "active") return void 0;
712
- return this.table?.get(key);
848
+ try {
849
+ return this.table?.get(key);
850
+ } catch (error) {
851
+ if (unknownStorageState(error)) this.enterStorageFailed("registry storage state cannot be confirmed; restart and reconcile");
852
+ throw error;
853
+ }
713
854
  }
714
855
  /**
715
856
  * Re-classify one key from its stored records: every directory is `stat`ed
@@ -734,22 +875,28 @@ var MultiRootRegistry = class extends Service {
734
875
  async persist(key, statuses) {
735
876
  const table = this.requireTable();
736
877
  const durable = persistedPrimaryRoot.parse({ roots: migrateRootSkills(statuses.map(toPersistedRoot)) });
737
- if (statuses.length === 0) await table.delete(key);
738
- else await table.put(key, durable);
878
+ try {
879
+ if (statuses.length === 0) await table.delete(key);
880
+ else await table.put(key, durable);
881
+ } catch (error) {
882
+ if (unknownStorageState(error)) this.enterStorageFailed("registry write result is unknown; restart and reconcile durable state");
883
+ throw error;
884
+ }
739
885
  const classified = statuses.length === 0 ? [] : classifyStoredRoots(key, durable.roots.map(toRegisteredRoot));
740
886
  this.cache.set(key, classified);
741
887
  this.publish(key, classified);
742
888
  return classified;
743
889
  }
744
890
  /**
745
- * Run one job for one primary root after every job already queued for that
746
- * root. The whole read → validate → write sequence belongs inside the job:
891
+ * Run one job after every store job already queued. The whole
892
+ * read → validate → write sequence belongs inside the job:
747
893
  * the storage domain serializes individual writes, so without this the loser
748
894
  * of a race reads a snapshot that is already stale and writes it back.
749
895
  */
750
896
  async serialize(key, job) {
751
- const run = (this.chains.get(key) ?? Promise.resolve()).then(job);
897
+ const run = this.operationChain.then(job);
752
898
  const tail = run.then(() => void 0, () => void 0);
899
+ this.operationChain = tail;
753
900
  this.chains.set(key, tail);
754
901
  try {
755
902
  return await run;
@@ -781,7 +928,7 @@ var MultiRootRegistry = class extends Service {
781
928
  const withheld = statuses.filter((status) => status.state !== "available");
782
929
  const signature = [...statuses.filter((status) => status.state === "available").map((status) => `available:${status.access}:${recordedGrantOf(status)}`), ...withheld.map((status) => `${status.state}:${status.invalidReason ?? ""}:${status.access}:${status.path}:${recordedGrantOf(status) ?? ""}`)].join("\n");
783
930
  const previousMetadata = this.metadataPublished.get(key);
784
- const metadata = JSON.stringify(statuses.map((row) => [
931
+ const metadata = JSON.stringify(this.shared ? statuses : statuses.map((row) => [
785
932
  row.id,
786
933
  row.aliasId,
787
934
  row.skillsEnabled,
@@ -789,11 +936,23 @@ var MultiRootRegistry = class extends Service {
789
936
  ]));
790
937
  this.metadataPublished.set(key, metadata);
791
938
  if ((this.published.get(key) ?? "") === signature) {
792
- if (previousMetadata !== metadata && (previousMetadata !== void 0 || statuses.length > 0)) for (const listener of this.listeners) listener(key);
939
+ if (previousMetadata !== metadata && (previousMetadata !== void 0 || statuses.length > 0)) {
940
+ this.operationRevision++;
941
+ this.notifyShared(key);
942
+ for (const listener of this.listeners) listener(key);
943
+ }
793
944
  return;
794
945
  }
795
946
  this.published.set(key, signature);
796
- const effective = statuses.flatMap((status) => {
947
+ this.operationRevision++;
948
+ const effective = this.projectRoots(statuses);
949
+ this.ctx.multiRootScope.setAdditionalRoots(key, effective);
950
+ if (withheld.length > 0) this.ctx.logger.warn(`multi-root-workspace: ${withheld.length} registered root(s) of ${key} are not writable right now (${withheld.map((status) => `${status.path}: ${status.state}`).join(", ")}); they stay registered and are reported by /workspace-folders`);
951
+ this.notifyShared(key);
952
+ for (const listener of this.listeners) listener(key);
953
+ }
954
+ projectRoots(statuses) {
955
+ return statuses.flatMap((status) => {
797
956
  const recorded = recordedGrantOf(status);
798
957
  if (status.state === "invalid" && status.invalidReason !== "membership-conflict" || recorded === void 0) return [];
799
958
  return [{
@@ -805,9 +964,113 @@ var MultiRootRegistry = class extends Service {
805
964
  ...status.alias === void 0 ? {} : { alias: status.alias }
806
965
  }];
807
966
  });
808
- this.ctx.multiRootScope.setAdditionalRoots(key, effective);
809
- if (withheld.length > 0) this.ctx.logger.warn(`multi-root-workspace: ${withheld.length} registered root(s) of ${key} are not writable right now (${withheld.map((status) => `${status.path}: ${status.state}`).join(", ")}); they stay registered and are reported by /workspace-folders`);
810
- for (const listener of this.listeners) listener(key);
967
+ }
968
+ snapshotOf(primary) {
969
+ return snapshotSchema.parse({
970
+ protocol: 1,
971
+ store: this.medium.id,
972
+ epoch: this.operationEpoch,
973
+ revision: this.operationRevision,
974
+ primary,
975
+ roots: this.cache.get(primary) ?? [],
976
+ available: this.authorityState.kind === "active"
977
+ });
978
+ }
979
+ async captureSnapshot(primary) {
980
+ const key = canonicalRoot(primary);
981
+ return await this.serialize(key, () => {
982
+ this.requireActive();
983
+ this.reclassify(key);
984
+ return this.snapshotOf(key);
985
+ });
986
+ }
987
+ notifyShared(key) {
988
+ if (this.server && this.authorityState.kind === "active") this.server.changed(this.snapshotOf(key));
989
+ }
990
+ applySnapshot(snapshot) {
991
+ if (!this.client || snapshot.store !== this.medium?.id || snapshot.epoch !== this.client.epoch) return;
992
+ const previous = this.remoteRevisions.get(snapshot.primary) ?? -1;
993
+ if (snapshot.revision < previous) return;
994
+ this.remoteRevisions.set(snapshot.primary, snapshot.revision);
995
+ const rows = snapshot.roots.map((row) => Object.fromEntries(Object.entries({
996
+ ...row,
997
+ id: additionalRootId(row.id)
998
+ }).filter(([, value]) => value !== void 0)));
999
+ this.cache.set(snapshot.primary, rows);
1000
+ this.publish(snapshot.primary, rows);
1001
+ this.ctx.multiRootScope.setRegistryAuthority(snapshot.available);
1002
+ this.remoteProjectionReady = snapshot.available;
1003
+ }
1004
+ remoteUnavailable() {
1005
+ this.connectionGeneration++;
1006
+ this.client = void 0;
1007
+ this.remoteRevisions.clear();
1008
+ this.authorityState = { kind: "contended" };
1009
+ this.ctx.multiRootScope.setRegistryAuthority(false);
1010
+ if (!this.remoteProjectionReady) return;
1011
+ this.remoteProjectionReady = false;
1012
+ for (const [primary, rows] of this.cache) this.ctx.multiRootScope.setAdditionalRoots(primary, this.projectRoots(rows).map((root) => ({
1013
+ ...root,
1014
+ withheld: true
1015
+ })));
1016
+ this.published.clear();
1017
+ }
1018
+ async remoteSnapshot(method, args) {
1019
+ const client = this.client, generation = this.connectionGeneration;
1020
+ if (!client) throw new Error("registry authority is unavailable");
1021
+ const value = await client.request(method, args);
1022
+ let snapshot;
1023
+ try {
1024
+ snapshot = snapshotSchema.parse(value);
1025
+ } catch (error) {
1026
+ client.close();
1027
+ throw error;
1028
+ }
1029
+ if (generation !== this.connectionGeneration || client !== this.client || snapshot.store !== this.medium?.id || snapshot.epoch !== client.epoch || snapshot.primary !== canonicalRoot(String(args[0]))) {
1030
+ client.close();
1031
+ throw new Error("stale registry connection or authority epoch");
1032
+ }
1033
+ this.applySnapshot(snapshot);
1034
+ return snapshot;
1035
+ }
1036
+ async remote(method, args) {
1037
+ await this.remoteSnapshot(method, args);
1038
+ return this.list(String(args[0]));
1039
+ }
1040
+ async dispatchShared(method, values) {
1041
+ const args = parseArguments(method, values);
1042
+ const primary = String(args[0]);
1043
+ if (!isAbsolute(primary)) throw new Error("registry primary must be absolute");
1044
+ if (method === "snapshot" || method === "admit") return this.captureSnapshot(primary);
1045
+ const ref = args[1];
1046
+ switch (method) {
1047
+ case "add":
1048
+ await this.add(primary, args[1]);
1049
+ break;
1050
+ case "removeAt":
1051
+ await this.removeAt(primary, ref);
1052
+ break;
1053
+ case "setAccess":
1054
+ await this.setAccess(primary, ref, args[2]);
1055
+ break;
1056
+ case "setAlias":
1057
+ await this.setAlias(primary, ref, args[2] === null ? void 0 : args[2]);
1058
+ break;
1059
+ case "setSkillsEnabled":
1060
+ await this.setSkillsEnabled(primary, ref, args[2]);
1061
+ break;
1062
+ case "setAliasId":
1063
+ await this.setAliasId(primary, ref, args[2]);
1064
+ break;
1065
+ case "move":
1066
+ await this.move(primary, ref, args[2] === null ? void 0 : args[2]);
1067
+ break;
1068
+ case "recheck":
1069
+ await this.recheck(primary);
1070
+ break;
1071
+ default: throw new Error("unsupported registry method");
1072
+ }
1073
+ return this.captureSnapshot(primary);
811
1074
  }
812
1075
  /**
813
1076
  * The open table.
@@ -817,6 +1080,7 @@ var MultiRootRegistry = class extends Service {
817
1080
  * or an error when the service has not started.
818
1081
  */
819
1082
  requireTable() {
1083
+ if (this.authorityState.kind === "follower") throw new Error("a follower never opens registry storage");
820
1084
  if (this.authorityState.kind === "contended") throw new RootValidationError("registry-contended", REGISTRY_CONTENDED_MESSAGE);
821
1085
  if (this.authorityState.kind === "config-invalid") throw new RootValidationError("config-invalid", `the root registry configuration is invalid (${this.authorityState.reason}); no additional root is granted`);
822
1086
  if (this.authorityState.kind === "storage-failed") throw new RootValidationError("storage-unavailable", `the root registry store is unavailable (${this.authorityState.reason}); remove or repair \$DSH_HOME/storages/multi_root_workspace.json and restart dsh`);
@@ -840,9 +1104,10 @@ var MultiRootRegistry = class extends Service {
840
1104
  async ensureAuthority() {
841
1105
  await this.serializeAuthority(async () => {
842
1106
  if (this.disposed) return;
843
- if (this.authorityState.kind === "active") return;
1107
+ if (this.authorityState.kind === "active" || this.client !== void 0) return;
844
1108
  if (this.authorityState.kind === "config-invalid") return;
845
1109
  if (this.lease !== void 0) {
1110
+ if (this.domain !== void 0) return;
846
1111
  await this.openDomain();
847
1112
  return;
848
1113
  }
@@ -855,11 +1120,50 @@ var MultiRootRegistry = class extends Service {
855
1120
  this.enterStorageFailed(`${DOMAIN_NAME}: leasePath must be an absolute path`);
856
1121
  return;
857
1122
  }
1123
+ if (this.sharedStorageRoot !== void 0 && this.medium === void 0) try {
1124
+ if (!isAbsolute(this.sharedStorageRoot)) throw new Error("sharedStorageRoot must be absolute");
1125
+ this.medium = storeIdentity(this.sharedStorageRoot);
1126
+ if (storeIdentity(dirname(leasePath)).id !== this.medium.id || basename(leasePath) !== `multi_root_workspace.lock`) throw new Error("shared registry requires its canonical medium lock, without alternate lock names");
1127
+ } catch (error) {
1128
+ this.enterStorageFailed(String(error));
1129
+ return;
1130
+ }
858
1131
  try {
859
1132
  this.lease = await RegistryAuthorityLease.acquire(leasePath);
860
1133
  } catch (error) {
861
1134
  if (error instanceof RegistryLeaseContendedError) {
862
1135
  this.enterContended();
1136
+ if (this.medium !== void 0) for (const delay of [
1137
+ 0,
1138
+ 50,
1139
+ 150
1140
+ ]) {
1141
+ if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
1142
+ if (this.disposed) return;
1143
+ const generation = ++this.connectionGeneration;
1144
+ let adopted = false;
1145
+ try {
1146
+ const client = await AuthorityClient.connect(this.medium.endpoint, this.medium.id, this.maxRoots, (snapshot) => {
1147
+ if (adopted && generation === this.connectionGeneration) this.applySnapshot(snapshot);
1148
+ }, () => {
1149
+ if (adopted && generation === this.connectionGeneration) this.remoteUnavailable();
1150
+ });
1151
+ if (this.disposed || generation !== this.connectionGeneration) {
1152
+ client.close();
1153
+ return;
1154
+ }
1155
+ this.client = client;
1156
+ adopted = true;
1157
+ this.connectionFailure = void 0;
1158
+ this.authorityState = {
1159
+ kind: "follower",
1160
+ epoch: client.epoch
1161
+ };
1162
+ break;
1163
+ } catch (error) {
1164
+ this.connectionFailure = String(error);
1165
+ }
1166
+ }
863
1167
  return;
864
1168
  }
865
1169
  this.enterStorageFailed(`${DOMAIN_NAME}: cannot acquire the registry lease: ${error instanceof Error ? error.message : String(error)}`);
@@ -876,7 +1180,7 @@ var MultiRootRegistry = class extends Service {
876
1180
  async openDomain() {
877
1181
  let domain;
878
1182
  try {
879
- domain = await this.ctx.storageDomain.open(multiRootDomainSpec);
1183
+ domain = this.medium === void 0 ? await this.ctx.storageDomain.open(multiRootDomainSpec) : await openAuthorityStorage(this.authorityStorageContext, this.medium.root, multiRootDomainSpec);
880
1184
  } catch (error) {
881
1185
  const reason = `${DOMAIN_NAME}: ${error instanceof Error ? error.message : String(error)}`;
882
1186
  this.ctx.logger.error(`multi-root-workspace: cannot open the root registry store (${DOMAIN_NAME}); no additional root is granted until it is repaired or removed: ${reason}`);
@@ -885,6 +1189,8 @@ var MultiRootRegistry = class extends Service {
885
1189
  }
886
1190
  this.domain = domain;
887
1191
  this.table = domain.table(TABLE_NAME);
1192
+ this.operationEpoch = randomUUID();
1193
+ this.operationRevision = 0;
888
1194
  this.authorityState = { kind: "active" };
889
1195
  this.loggedContended = false;
890
1196
  this.cache.clear();
@@ -896,15 +1202,30 @@ var MultiRootRegistry = class extends Service {
896
1202
  await this.table.put(primaryRoot, migrated);
897
1203
  accepted = migrated;
898
1204
  } catch (error) {
1205
+ if (unknownStorageState(error)) {
1206
+ this.enterStorageFailed("Skills migration result is unknown; restart and reconcile");
1207
+ return;
1208
+ }
899
1209
  this.ctx.logger.warn(`multi-root skills: metadata migration failed; Skills remain withheld: ${String(error)}`);
900
1210
  }
901
1211
  this.accept(primaryRoot, accepted);
902
1212
  });
1213
+ if (this.authorityState.kind !== "active") return;
1214
+ if (this.medium !== void 0) try {
1215
+ this.server = await AuthorityServer.listen(this.medium.endpoint, this.medium.id, this.operationEpoch, this.maxRoots, (method, args) => this.dispatchShared(method, args));
1216
+ } catch (error) {
1217
+ await domain.close();
1218
+ this.domain = void 0;
1219
+ this.enterStorageFailed(`cannot start shared registry: ${String(error)}`);
1220
+ return;
1221
+ }
903
1222
  this.ctx.multiRootScope.setRegistryAuthority(true);
904
1223
  }
905
1224
  /** Fail closed: empty scope, no table, contended state. */
906
1225
  enterContended() {
907
- this.withdrawPublished();
1226
+ this.operationEpoch = void 0;
1227
+ if (this.shared) this.remoteUnavailable();
1228
+ else this.withdrawPublished();
908
1229
  this.ctx.multiRootScope.setRegistryAuthority(false);
909
1230
  this.table = void 0;
910
1231
  this.authorityState = { kind: "contended" };
@@ -915,8 +1236,17 @@ var MultiRootRegistry = class extends Service {
915
1236
  }
916
1237
  /** Fail closed: empty scope, no table, storage-failed state. The lease, if held, stays. */
917
1238
  enterStorageFailed(reason) {
918
- this.withdrawPublished();
1239
+ this.operationEpoch = void 0;
919
1240
  this.ctx.multiRootScope.setRegistryAuthority(false);
1241
+ if (this.shared) {
1242
+ for (const [key, rows] of this.cache) this.ctx.multiRootScope.setAdditionalRoots(key, this.projectRoots(rows).map((root) => ({
1243
+ ...root,
1244
+ withheld: true
1245
+ })));
1246
+ this.published.clear();
1247
+ this.server?.close();
1248
+ this.server = void 0;
1249
+ } else this.withdrawPublished();
920
1250
  this.table = void 0;
921
1251
  this.authorityState = {
922
1252
  kind: "storage-failed",
@@ -945,8 +1275,14 @@ var MultiRootRegistry = class extends Service {
945
1275
  */
946
1276
  async releaseAuthority() {
947
1277
  this.disposed = true;
1278
+ this.connectionGeneration++;
1279
+ this.client?.close();
1280
+ this.client = void 0;
948
1281
  await this.serializeAuthority(async () => {
1282
+ this.server?.close();
1283
+ this.server = void 0;
949
1284
  this.table = void 0;
1285
+ this.operationEpoch = void 0;
950
1286
  this.authorityState = { kind: "contended" };
951
1287
  this.ctx.multiRootScope.setRegistryAuthority(false);
952
1288
  await this.drainMutations();
@@ -964,7 +1300,7 @@ var MultiRootRegistry = class extends Service {
964
1300
  });
965
1301
  }
966
1302
  /**
967
- * Wait until no per-primary-root mutation is queued or running.
1303
+ * Wait until no store work is queued or running (operation I/O is outside).
968
1304
  *
969
1305
  * One pass is enough, and only because the authority state has already been
970
1306
  * flipped: no further mutation can be admitted (it would fail in
@@ -1063,5 +1399,12 @@ function normalizeAlias(alias) {
1063
1399
  if (trimmed.length > 120) throw new RootValidationError("invalid-alias", `an alias must be at most 120 characters`);
1064
1400
  return trimmed;
1065
1401
  }
1402
+ /** Structural across storage releases; older versions do not expose this code. */
1403
+ function unknownStorageState(error, seen = /* @__PURE__ */ new Set()) {
1404
+ if (error === null || typeof error !== "object" || seen.has(error)) return false;
1405
+ seen.add(error);
1406
+ const value = error;
1407
+ return value.code === "unknown-state" || unknownStorageState(value.cause, seen) || Array.isArray(value.errors) && value.errors.some((item) => unknownStorageState(item, seen));
1408
+ }
1066
1409
  //#endregion
1067
1410
  export { MultiRootRegistry as a, multiRootDomainSpec as c, MAX_ALIAS_LENGTH as i, RegistryAuthorityLease as l, DOMAIN_NAME as n, REGISTRY_CONTENDED_MESSAGE as o, MAX_ADDITIONAL_ROOTS_CEILING as r, TABLE_NAME as s, DEFAULT_MAX_ADDITIONAL_ROOTS as t, RegistryLeaseContendedError as u };