@dsh-electron/dsh-plugin-multi-root-workspace 0.1.14 → 0.1.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.
Files changed (73) hide show
  1. package/CHANGELOG.en.md +32 -1
  2. package/CHANGELOG.md +32 -1
  3. package/README.en.md +25 -8
  4. package/README.md +25 -8
  5. package/cordis.patch.yml +5 -1
  6. package/lib/{llm-message-fYORacvh.js → agent-instructions-BXuKhOzr.js} +1 -84
  7. package/lib/auto-review.js +2 -2
  8. package/lib/client.js +969 -100
  9. package/lib/client.js.map +1 -1
  10. package/lib/{command-Dcz6nULo.js → command-DaVJ-mK3.js} +76 -11
  11. package/lib/command.js +1 -1
  12. package/lib/compat-CTJbpCxu.js +267 -0
  13. package/lib/compat.js +1 -1
  14. package/lib/{dialects-Dr4rDR-H.js → dialects-CdlG3RF9.js} +1 -1
  15. package/lib/{compat-veDqCOoa.js → dsh-version-DCwF7REk.js} +2 -264
  16. package/lib/index.js +10 -8
  17. package/lib/{instructions-KsUwUFE0.js → instructions-Ca80SkZq.js} +696 -27
  18. package/lib/instructions.js +1 -1
  19. package/lib/llm-message-CepyzEBQ.js +84 -0
  20. package/lib/lsp.js +4 -4
  21. package/lib/{metadata-DgqSdVX_.js → metadata-99jgViHQ.js} +1 -1
  22. package/lib/{method-wrapper-CFY3p3j2.js → method-wrapper-D4oUCsEF.js} +22 -2
  23. package/lib/{native-win32-CRNXr11U.js → native-win32-C5oMtt7f.js} +1 -1
  24. package/lib/ops-compatibility.js +58 -0
  25. package/lib/ops-settings-UN2oOKEp.js +40 -0
  26. package/lib/ops-tools-k4JYaOrM.js +152 -0
  27. package/lib/process-fence.js +193 -34
  28. package/lib/{protocol-CnR1KTXy.js → protocol-CwDnFEyK.js} +8 -4
  29. package/lib/{registry-DR_sMuvS.js → registry-BibiukWk.js} +236 -80
  30. package/lib/{registry-lease-win32-DDO-c7ig.js → registry-lease-win32-r4puac9m.js} +1 -1
  31. package/lib/{registry-operation-C2ZV3Jhw.js → registry-operation-CIlmt6_c.js} +5 -1
  32. package/lib/registry.js +2 -2
  33. package/lib/{review-context-NDpKeMCc.js → review-context-OxO-fMWR.js} +24 -7
  34. package/lib/review-context.js +1 -1
  35. package/lib/{roots-Ca_W1S9n.js → roots-CmaWxRVy.js} +112 -22
  36. package/lib/{sandbox-C-vCJCp3.js → sandbox-aobdyQGi.js} +7 -4
  37. package/lib/sandbox.js +1 -1
  38. package/lib/scope.js +162 -12
  39. package/lib/skills-utyuvmwr.js +104 -0
  40. package/lib/skills.js +5 -106
  41. package/lib/{tool-context-CpgIFcCB.js → tool-context-CCeY3mrK.js} +1 -1
  42. package/lib/types/authority/protocol.d.ts +5 -1
  43. package/lib/types/client/files-tree/FilesTree.d.ts +49 -6
  44. package/lib/types/client/files-tree/adapter.d.ts +69 -16
  45. package/lib/types/client/files-tree/face.d.ts +31 -2
  46. package/lib/types/client/files-tree/locate.d.ts +74 -0
  47. package/lib/types/client/files-tree/navigator.d.ts +98 -0
  48. package/lib/types/client/index.d.ts +7 -0
  49. package/lib/types/client/locales.d.ts +30 -0
  50. package/lib/types/client/ops-settings.d.ts +6 -0
  51. package/lib/types/client/skills-settings.d.ts +3 -2
  52. package/lib/types/client/styles.d.ts +1 -1
  53. package/lib/types/command.d.ts +3 -3
  54. package/lib/types/compat/files-tree.d.ts +8 -1
  55. package/lib/types/compat/forge-entrypoints.d.ts +44 -0
  56. package/lib/types/compat/instruction-activity.d.ts +32 -1
  57. package/lib/types/compat/ops-confirmation.d.ts +17 -0
  58. package/lib/types/compat/ops-settings.d.ts +11 -0
  59. package/lib/types/compat/ops-tools.d.ts +25 -0
  60. package/lib/types/compat/session-existence.d.ts +41 -0
  61. package/lib/types/compat/skills-client.d.ts +1 -0
  62. package/lib/types/compat/tool-context.d.ts +4 -0
  63. package/lib/types/compat.d.ts +2 -2
  64. package/lib/types/instructions.d.ts +6 -1
  65. package/lib/types/ops-compatibility.d.ts +34 -0
  66. package/lib/types/registry-operation.d.ts +7 -0
  67. package/lib/types/registry.d.ts +67 -8
  68. package/lib/types/review-context.d.ts +2 -0
  69. package/lib/types/roots.d.ts +116 -20
  70. package/lib/types/scope.d.ts +95 -3
  71. package/lib/workspace-files.js +4 -4
  72. package/lib/{workspace-routing-YPxaQ4V3.js → workspace-routing-D_qwMUh-.js} +4 -4
  73. package/package.json +5 -1
@@ -1,8 +1,8 @@
1
- import { a as runPrimaryOperation, n as freezeOperationScope, o as runRegistryOperation } from "./registry-operation-C2ZV3Jhw.js";
1
+ import { n as freezeOperationScope, o as runPrimaryOperation, s as runRegistryOperation } from "./registry-operation-CIlmt6_c.js";
2
2
  import { i as isWindowsNetworkOrDevicePath } from "./windows-runner-contract-B5R2K3mM.js";
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";
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-CnR1KTXy.js";
3
+ import { _ as samePath, a as classifyStoredRoots, d as primaryKey, f as protectedOverlap, h as resolveRootIndex, i as canonicalRoot, m as reservedRootBoundary, n as additionalRootId, p as removeStatusAt, s as expandRootInput, t as RootValidationError, u as markProtectedPaths, v as singleGrantOf, y as validateRootCandidate } from "./roots-CmaWxRVy.js";
4
+ import { r as validateAliasId, t as migrateRootSkills } from "./metadata-99jgViHQ.js";
5
+ import { a as snapshotSchema, i as parseArguments, r as AuthorityServer, s as storeIdentity, t as AuthorityClient } from "./protocol-CwDnFEyK.js";
6
6
  import { createRequire } from "node:module";
7
7
  import { closeSync, fstatSync, mkdirSync, openSync, statSync } from "node:fs";
8
8
  import { randomUUID } from "node:crypto";
@@ -265,7 +265,7 @@ var RegistryAuthorityLease = class RegistryAuthorityLease {
265
265
  mode: 448
266
266
  });
267
267
  if (process.platform === "win32") {
268
- const { acquireLockHandleWin32 } = await import("./registry-lease-win32-DDO-c7ig.js");
268
+ const { acquireLockHandleWin32 } = await import("./registry-lease-win32-r4puac9m.js");
269
269
  let handle;
270
270
  try {
271
271
  handle = await acquireLockHandleWin32(leasePath);
@@ -317,7 +317,7 @@ var RegistryAuthorityLease = class RegistryAuthorityLease {
317
317
  if (this.released) return;
318
318
  this.released = true;
319
319
  if (this.held.kind === "win32") {
320
- const { releaseLockHandleWin32 } = await import("./registry-lease-win32-DDO-c7ig.js");
320
+ const { releaseLockHandleWin32 } = await import("./registry-lease-win32-r4puac9m.js");
321
321
  await releaseLockHandleWin32(this.held.handle);
322
322
  return;
323
323
  }
@@ -430,11 +430,11 @@ const multiRootDomainSpec = defineDomain({
430
430
  tables: { roots: domainTable(persistedPrimaryRoot) }
431
431
  });
432
432
  /** The registry service: `ctx.multiRootRegistry`. */
433
- var MultiRootRegistry = class extends Service {
433
+ var MultiRootRegistry = class MultiRootRegistry extends Service {
434
434
  static inject = [
435
435
  "multiRootCompat",
436
436
  "multiRootProviders",
437
- "storageDomain",
437
+ "storage",
438
438
  "multiRootScope"
439
439
  ];
440
440
  /** Absolute lock path; absent means the row was composed without `leasePath`. */
@@ -492,8 +492,15 @@ var MultiRootRegistry = class extends Service {
492
492
  /** Keys already warned about a historical over-limit store (one warning each). */
493
493
  warnedOverLimit = /* @__PURE__ */ new Set();
494
494
  /**
495
- * @param ctx - the host context; `storageDomain` and `multiRootScope` are injected.
496
- * @param config - the patch row's `leasePath` and `maxAdditionalRoots`.
495
+ * The canonical value each protected boundary resolved to the FIRST time it
496
+ * could be computed, keyed by boundary kind. Never re-pinned: a later
497
+ * rename or symlink swap of the DSH home (or of the lease directory) must
498
+ * not drop the boundary the registration rules were enforced under.
499
+ */
500
+ pinnedProtectedPaths = /* @__PURE__ */ new Map();
501
+ /**
502
+ * @param ctx - the host context; `storage` and `multiRootScope` are injected.
503
+ * @param config - the patch row's `sharedStorageRoot`, `leasePath`, and `maxAdditionalRoots`.
497
504
  */
498
505
  constructor(ctx, config = {}) {
499
506
  super(ctx, "multiRootRegistry");
@@ -515,6 +522,10 @@ var MultiRootRegistry = class extends Service {
515
522
  });
516
523
  if (config.sharedStorageRoot !== void 0) this.ctx.multiRootScope.requireAdmission(this.operationProof);
517
524
  this.ctx.multiRootScope.setRegistryAuthority(false);
525
+ if (this.sharedStorageRoot === void 0 || this.sharedStorageRoot.trim() === "") this.authorityState = {
526
+ kind: "config-invalid",
527
+ reason: `${DOMAIN_NAME}: sharedStorageRoot is required; the single-process registry mode was removed (ADR-0028)`
528
+ };
518
529
  const configured = config.maxAdditionalRoots === void 0 ? 16 : config.maxAdditionalRoots;
519
530
  if (Number.isInteger(configured) && configured >= 1 && configured <= 64) this.maxRoots = configured;
520
531
  else {
@@ -555,7 +566,7 @@ var MultiRootRegistry = class extends Service {
555
566
  switch (this.authorityState.kind) {
556
567
  case "active":
557
568
  case "follower": return;
558
- case "contended": return this.shared && this.connectionFailure ? `shared registry is unavailable: ${this.connectionFailure}` : REGISTRY_CONTENDED_MESSAGE;
569
+ case "contended": return this.connectionFailure !== void 0 ? `shared registry is unavailable: ${this.connectionFailure}` : REGISTRY_CONTENDED_MESSAGE;
559
570
  case "storage-failed":
560
571
  case "config-invalid": return this.authorityState.reason;
561
572
  }
@@ -635,7 +646,7 @@ var MultiRootRegistry = class extends Service {
635
646
  * @param primaryRoot - the session workspace root (any spelling).
636
647
  */
637
648
  overLimit(primaryRoot) {
638
- return this.statusesOf(canonicalRoot(primaryRoot)).length > this.maxRoots;
649
+ return this.statusesOf(primaryKey(primaryRoot)).length > this.maxRoots;
639
650
  }
640
651
  /**
641
652
  * Every registration of one primary root, in registry order, as last
@@ -646,7 +657,7 @@ var MultiRootRegistry = class extends Service {
646
657
  * @returns the statuses; empty when nothing is registered.
647
658
  */
648
659
  list(primaryRoot) {
649
- return this.statusesOf(canonicalRoot(primaryRoot));
660
+ return this.statusesOf(primaryKey(primaryRoot));
650
661
  }
651
662
  /**
652
663
  * Re-examine every registration of one primary root WITHOUT writing storage:
@@ -680,7 +691,7 @@ var MultiRootRegistry = class extends Service {
680
691
  if (this.client) return await this.remote("snapshot", [primaryRoot]);
681
692
  }
682
693
  if (this.authorityState.kind !== "active") return [];
683
- const key = canonicalRoot(primaryRoot);
694
+ const key = primaryKey(primaryRoot);
684
695
  return await this.serialize(key, () => this.reclassify(key));
685
696
  }
686
697
  /**
@@ -692,16 +703,16 @@ var MultiRootRegistry = class extends Service {
692
703
  if (this.disposed) throw new Error("multi-root registry: cannot admit an operation after disposal");
693
704
  const capturedPolicy = structuredClone(policy);
694
705
  await this.ensureAuthority();
695
- const key = canonicalRoot(capturedPolicy.workspaceRoot);
696
- if (this.shared && this.available) {
706
+ const key = primaryKey(capturedPolicy.workspaceRoot);
707
+ if (this.available) {
697
708
  const snapshot = this.client ? await this.remoteSnapshot("admit", [key]) : await this.captureSnapshot(key);
698
- const scope = freezeOperationScope(this.ctx.multiRootScope.resolveSnapshot(capturedPolicy, this.projectRoots(snapshot.roots), snapshot.available));
709
+ const scope = freezeOperationScope(this.ctx.multiRootScope.resolveSnapshot(capturedPolicy, this.projectRoots(this.localSnapshotRoots(snapshot)), snapshot.available));
699
710
  return await runRegistryOperation(scope, kind, snapshot.epoch, snapshot.revision, body, this.operationProof, capturedPolicy.mode);
700
711
  }
701
712
  const admission = await this.serialize(key, () => {
702
713
  if (this.disposed) throw new Error("multi-root registry: cannot admit an operation after disposal");
703
714
  return {
704
- scope: freezeOperationScope(this.shared ? this.ctx.multiRootScope.resolveSnapshot(capturedPolicy, this.projectRoots(this.cache.get(key) ?? []), false) : this.ctx.multiRootScope.resolve(capturedPolicy)),
715
+ scope: freezeOperationScope(this.ctx.multiRootScope.resolveSnapshot(capturedPolicy, this.projectRoots(this.cache.get(key) ?? []), false)),
705
716
  epoch: this.operationEpoch,
706
717
  revision: this.operationRevision
707
718
  };
@@ -709,10 +720,113 @@ var MultiRootRegistry = class extends Service {
709
720
  return await runRegistryOperation(admission.scope, kind, admission.epoch, admission.revision, body, this.operationProof, capturedPolicy.mode);
710
721
  }
711
722
  withPrimaryOperation(policy, body) {
712
- const scope = this.ctx.multiRootScope.resolveSnapshot(policy, this.projectRoots(this.cache.get(canonicalRoot(policy.workspaceRoot)) ?? []), false);
723
+ const scope = this.ctx.multiRootScope.resolveSnapshot(policy, this.projectRoots(this.cache.get(primaryKey(policy.workspaceRoot)) ?? []), false);
713
724
  return runPrimaryOperation(freezeOperationScope(scope), policy.mode, this.operationProof, body);
714
725
  }
715
726
  /**
727
+ * The spellings one protected boundary contributes: the configured path,
728
+ * its current canonical resolution, and the canonical value pinned the
729
+ * first time it resolved — never re-pinned, so a later rename or symlink
730
+ * swap of the protected directory cannot drop the boundary the
731
+ * registration rules were enforced under. The list is deduplicated by
732
+ * {@link samePath}.
733
+ */
734
+ boundarySpellings(kind, configured) {
735
+ const canonical = canonicalRoot(configured);
736
+ if (!this.pinnedProtectedPaths.has(kind)) this.pinnedProtectedPaths.set(kind, canonical);
737
+ const spellings = [];
738
+ for (const spelling of [
739
+ configured,
740
+ canonical,
741
+ this.pinnedProtectedPaths.get(kind)
742
+ ]) if (!spellings.some((existing) => samePath(existing, spelling))) spellings.push(spelling);
743
+ return spellings;
744
+ }
745
+ /**
746
+ * The best-known protected-boundary set, plus whether it is COMPLETE.
747
+ *
748
+ * The set comes from trusted host configuration, never from a panel or
749
+ * shell claim: the scope row's `dshHome`, the physical medium
750
+ * `storeIdentity` bound, and the lease directory beside it.
751
+ * `complete` is false — and only NEW grants may then be refused — when a
752
+ * boundary cannot be proven: an invalid `dshHome` config, a configured
753
+ * home that is not a directory right now, or a medium that was never bound. Stored classification always consumes whatever IS known
754
+ * (pinned values included), so an unknown boundary never weakens the
755
+ * revalidation of existing records.
756
+ */
757
+ protectedBoundarySet() {
758
+ const paths = [];
759
+ let complete = true;
760
+ const home = this.ctx.multiRootScope.dshHomeConfig;
761
+ switch (home.kind) {
762
+ case "absent": break;
763
+ case "invalid":
764
+ complete = false;
765
+ break;
766
+ case "configured": {
767
+ let isDirectory = false;
768
+ try {
769
+ isDirectory = statSync(home.path).isDirectory();
770
+ } catch {
771
+ isDirectory = false;
772
+ }
773
+ if (isDirectory) for (const spelling of this.boundarySpellings("dsh-home", home.path)) paths.push({
774
+ kind: "dsh-home",
775
+ path: spelling
776
+ });
777
+ else {
778
+ complete = false;
779
+ paths.push({
780
+ kind: "dsh-home",
781
+ path: home.path
782
+ });
783
+ const pinned = this.pinnedProtectedPaths.get("dsh-home");
784
+ if (pinned !== void 0 && !samePath(pinned, home.path)) paths.push({
785
+ kind: "dsh-home",
786
+ path: pinned
787
+ });
788
+ }
789
+ break;
790
+ }
791
+ }
792
+ if (this.medium === void 0) complete = false;
793
+ else paths.push({
794
+ kind: "registry-medium",
795
+ path: this.medium.root
796
+ });
797
+ if (this.leasePath !== void 0) for (const spelling of this.boundarySpellings("registry-lease", dirname(this.leasePath))) paths.push({
798
+ kind: "registry-lease",
799
+ path: spelling
800
+ });
801
+ return {
802
+ paths,
803
+ complete
804
+ };
805
+ }
806
+ /**
807
+ * The protected DSH directories this Host can prove right now, or
808
+ * `undefined` when the set cannot be proven. `undefined` blocks every NEW
809
+ * grant; it never weakens the classification of stored records.
810
+ */
811
+ protectedPaths() {
812
+ const { paths, complete } = this.protectedBoundarySet();
813
+ return complete ? paths : void 0;
814
+ }
815
+ /**
816
+ * Every protected entry this Host can prove — used wherever a partial set
817
+ * must still protect stored records and received snapshots.
818
+ */
819
+ knownProtectedPaths() {
820
+ return this.protectedBoundarySet().paths;
821
+ }
822
+ /**
823
+ * The one refusal shared by the owner mutation path and the follower
824
+ * pre-check: the protected set cannot be proven, so no new grant is safe.
825
+ */
826
+ static protectedBoundaryUnknown() {
827
+ return new RootValidationError("protected-boundary-unknown", "the protected DSH directories cannot be resolved right now; no root can be registered until they can");
828
+ }
829
+ /**
716
830
  * Registers one additional root.
717
831
  * @param primaryRoot - the session workspace root the root belongs to.
718
832
  * @param input - the directory and optional alias.
@@ -721,12 +835,28 @@ var MultiRootRegistry = class extends Service {
721
835
  */
722
836
  async add(primaryRoot, input) {
723
837
  await this.ensureAuthority();
724
- if (this.client) return await this.remote("add", [primaryRoot, input]);
838
+ if (this.client) {
839
+ const protectedPaths = this.protectedPaths();
840
+ if (protectedPaths === void 0) throw MultiRootRegistry.protectedBoundaryUnknown();
841
+ const expanded = expandRootInput(input.path);
842
+ if (process.platform === "win32" && isWindowsNetworkOrDevicePath(expanded)) throw new RootValidationError("unsupported-path", `Windows ACL confinement cannot cover network or device paths: ${expanded}`);
843
+ if (isAbsolute(expanded)) {
844
+ const candidate = canonicalRoot(expanded);
845
+ const conflict = protectedOverlap(candidate, protectedPaths);
846
+ if (conflict !== void 0) throw new RootValidationError("protected-path", `"${candidate}" overlaps the protected DSH directory "${conflict.path}" (${conflict.kind}) and cannot be registered`, {
847
+ conflict: conflict.path,
848
+ reference: candidate
849
+ });
850
+ }
851
+ return await this.remote("add", [primaryRoot, input]);
852
+ }
725
853
  this.requireActive();
726
- const key = canonicalRoot(primaryRoot);
854
+ const key = primaryKey(primaryRoot);
727
855
  return await this.serialize(key, async () => {
856
+ const protectedPaths = this.protectedPaths();
857
+ if (protectedPaths === void 0) throw MultiRootRegistry.protectedBoundaryUnknown();
728
858
  const earlier = this.cache.get(key);
729
- const statuses = this.reclassify(key);
859
+ const statuses = await this.reclassify(key);
730
860
  const alias = normalizeAlias(input.alias);
731
861
  const expanded = expandRootInput(input.path);
732
862
  if (process.platform === "win32" && isWindowsNetworkOrDevicePath(expanded)) throw new RootValidationError("unsupported-path", `Windows ACL confinement cannot cover network or device paths: ${expanded}`);
@@ -737,9 +867,17 @@ var MultiRootRegistry = class extends Service {
737
867
  const before = earlier?.find((candidate) => candidate.id === status.id);
738
868
  return status.state !== "available" || before !== void 0 && before.state !== "available";
739
869
  });
870
+ if (revived !== -1) {
871
+ const grant = singleGrantOf(statuses[revived]);
872
+ if (grant === void 0 || !samePath(preliminary, grant)) throw new RootValidationError("revive-target-changed", `the registration "${statuses[revived].path}" was granted for "${grant ?? "(unknown)"}" but now resolves to "${preliminary}"; remove it and add the new directory explicitly`, {
873
+ ...grant === void 0 ? {} : { conflict: grant },
874
+ reference: preliminary
875
+ });
876
+ }
740
877
  if (revived === -1 && statuses.length >= this.maxRoots) throw new RootValidationError("root-limit-reached", `this workspace already has ${statuses.length} additional roots; the configured limit is ${this.maxRoots}. Remove one first, or raise maxAdditionalRoots.`, { reference: String(this.maxRoots) });
741
878
  const canonical = validateRootCandidate(input.path, {
742
879
  primaryRoot: key,
880
+ protectedPaths,
743
881
  existing: statuses.flatMap((status, index) => {
744
882
  if (index === revived) return [];
745
883
  const boundary = reservedRootBoundary(status);
@@ -798,9 +936,9 @@ var MultiRootRegistry = class extends Service {
798
936
  access
799
937
  ]);
800
938
  this.requireActive();
801
- const key = canonicalRoot(primaryRoot);
939
+ const key = primaryKey(primaryRoot);
802
940
  return await this.serialize(key, async () => {
803
- const statuses = this.reclassify(key);
941
+ const statuses = await this.reclassify(key);
804
942
  const targetIndex = resolveRootIndex(statuses, ref);
805
943
  const target = statuses[targetIndex];
806
944
  if (target.access === access) return statuses;
@@ -840,8 +978,8 @@ var MultiRootRegistry = class extends Service {
840
978
  await this.ensureAuthority();
841
979
  if (this.client) return await this.remote("removeAt", [primaryRoot, ref]);
842
980
  this.requireActive();
843
- const key = canonicalRoot(primaryRoot);
844
- return await this.serialize(key, async () => await this.persist(key, removeStatusAt(this.reclassify(key), ref)));
981
+ const key = primaryKey(primaryRoot);
982
+ return await this.serialize(key, async () => await this.persist(key, removeStatusAt(await this.reclassify(key), ref)));
845
983
  }
846
984
  /**
847
985
  * Set or clear one registration's display alias.
@@ -859,9 +997,9 @@ var MultiRootRegistry = class extends Service {
859
997
  alias ?? null
860
998
  ]);
861
999
  this.requireActive();
862
- const key = canonicalRoot(primaryRoot);
1000
+ const key = primaryKey(primaryRoot);
863
1001
  return await this.serialize(key, async () => {
864
- const statuses = this.reclassify(key);
1002
+ const statuses = await this.reclassify(key);
865
1003
  const targetIndex = resolveRootIndex(statuses, ref);
866
1004
  const normalized = normalizeAlias(alias);
867
1005
  const next = statuses.map((status, index) => {
@@ -898,9 +1036,9 @@ var MultiRootRegistry = class extends Service {
898
1036
  async setSkillsMetadata(primaryRoot, ref, patch) {
899
1037
  await this.ensureAuthority();
900
1038
  this.requireActive();
901
- const key = canonicalRoot(primaryRoot);
1039
+ const key = primaryKey(primaryRoot);
902
1040
  return await this.serialize(key, async () => {
903
- const statuses = this.reclassify(key);
1041
+ const statuses = await this.reclassify(key);
904
1042
  const target = resolveRootIndex(statuses, ref);
905
1043
  if (patch.aliasId !== void 0) validateAliasId(patch.aliasId, statuses.filter((_row, index) => index !== target));
906
1044
  return await this.persist(key, statuses.map((row, index) => index === target ? {
@@ -925,9 +1063,9 @@ var MultiRootRegistry = class extends Service {
925
1063
  beforeRef ?? null
926
1064
  ]);
927
1065
  this.requireActive();
928
- const key = canonicalRoot(primaryRoot);
1066
+ const key = primaryKey(primaryRoot);
929
1067
  return await this.serialize(key, async () => {
930
- const statuses = this.reclassify(key);
1068
+ const statuses = await this.reclassify(key);
931
1069
  const targetIndex = resolveRootIndex(statuses, ref);
932
1070
  const anchorIndex = beforeRef === void 0 ? void 0 : resolveRootIndex(statuses, beforeRef);
933
1071
  if (anchorIndex === targetIndex) return statuses;
@@ -954,12 +1092,12 @@ var MultiRootRegistry = class extends Service {
954
1092
  await this.ensureAuthority();
955
1093
  if (this.client) return await this.remote("recheck", [primaryRoot]);
956
1094
  this.requireActive();
957
- const key = canonicalRoot(primaryRoot);
958
- return await this.serialize(key, async () => await this.persist(key, this.reclassify(key)));
1095
+ const key = primaryKey(primaryRoot);
1096
+ return await this.serialize(key, async () => await this.persist(key, await this.reclassify(key)));
959
1097
  }
960
1098
  /**
961
1099
  * Observe registry changes.
962
- * @param listener - called with the canonical primary root after each durable change.
1100
+ * @param listener - called with the Registry key (`primaryKey()`, case-folded on Windows) after each durable change.
963
1101
  * @returns the disposer that stops observing.
964
1102
  */
965
1103
  onChange(listener) {
@@ -974,10 +1112,10 @@ var MultiRootRegistry = class extends Service {
974
1112
  * @returns the canonical paths of every `available` root.
975
1113
  */
976
1114
  granted(primaryRoot) {
977
- return this.shared ? this.ctx.multiRootScope.snapshot({
1115
+ return this.ctx.multiRootScope.snapshot({
978
1116
  workspaceRoot: primaryRoot,
979
1117
  mode: "workspace-write"
980
- }).additionalRoots : availableRoots(this.statusesOf(canonicalRoot(primaryRoot)));
1118
+ }).additionalRoots;
981
1119
  }
982
1120
  /**
983
1121
  * The scope the registry publishes: only usable roots, in registry order.
@@ -988,8 +1126,7 @@ var MultiRootRegistry = class extends Service {
988
1126
  return this.granted(policy.workspaceRoot);
989
1127
  }
990
1128
  /** Classify one stored record set and cache it. */
991
- accept(primaryRoot, record) {
992
- const key = canonicalRoot(primaryRoot);
1129
+ accept(key, record) {
993
1130
  const statuses = this.classify(key, record);
994
1131
  this.cache.set(key, statuses);
995
1132
  this.publish(key, statuses);
@@ -1001,7 +1138,7 @@ var MultiRootRegistry = class extends Service {
1001
1138
  * staging uses it so a half-opened domain cannot materialize rows.
1002
1139
  */
1003
1140
  classify(key, record) {
1004
- const statuses = classifyStoredRoots(key, record.roots.map(toRegisteredRoot));
1141
+ const statuses = classifyStoredRoots(canonicalRoot(key), record.roots.map(toRegisteredRoot), process.platform, this.knownProtectedPaths());
1005
1142
  if (statuses.length > this.maxRoots && !this.warnedOverLimit.has(key)) {
1006
1143
  this.warnedOverLimit.add(key);
1007
1144
  this.ctx.logger.warn(`multi-root-workspace: ${statuses.length} roots are stored for ${key}, above the configured limit of ${this.maxRoots}; all of them stay granted, only new adds are refused`);
@@ -1017,20 +1154,38 @@ var MultiRootRegistry = class extends Service {
1017
1154
  statusesOf(key) {
1018
1155
  const cached = this.cache.get(key);
1019
1156
  if (cached !== void 0) return cached;
1020
- const record = this.lookupRecords(key);
1021
- if (record === void 0) return [];
1022
- return this.accept(key, record);
1157
+ const found = this.lookupRecords(key);
1158
+ if (found === void 0) return [];
1159
+ return this.accept(key, found.record);
1023
1160
  }
1024
1161
  /**
1025
- * The stored record set of one key, or `undefined` when there is none (or
1026
- * when the store itself could not be read: an unusable store reads as
1027
- * "nothing registered" so reads stay answerable and the surfaces can explain
1028
- * the situation, while every write fails loudly in `requireTable()`).
1162
+ * The stored record set of one key: `{ record, migrated: false }` when the
1163
+ * spelling key holds it; `{ record, migrated: true }` when only the legacy
1164
+ * CANONICAL key does — stores written before the upstream primary spelling
1165
+ * became the key (v0.1.14 and earlier). The fallback follows the CURRENT
1166
+ * resolution ONLY for such a not-yet-migrated store; the first serialized
1167
+ * access copies the record forward under the spelling key (see
1168
+ * {@link reclassify}), after which resolution can never re-key a grant.
1169
+ * `undefined` when there is no record (or when the store itself could not
1170
+ * be read: an unusable store reads as "nothing registered" so reads stay
1171
+ * answerable and the surfaces can explain the situation, while every write
1172
+ * fails loudly in `requireTable()`).
1029
1173
  */
1030
1174
  lookupRecords(key) {
1031
1175
  if (this.authorityState.kind !== "active") return void 0;
1032
1176
  try {
1033
- return this.table?.get(key);
1177
+ const record = this.table?.get(key);
1178
+ if (record !== void 0) return {
1179
+ record,
1180
+ migrated: false
1181
+ };
1182
+ const legacy = canonicalRoot(key);
1183
+ if (legacy === key) return void 0;
1184
+ const migrated = this.table?.get(legacy);
1185
+ return migrated === void 0 ? void 0 : {
1186
+ record: migrated,
1187
+ migrated: true
1188
+ };
1034
1189
  } catch (error) {
1035
1190
  if (unknownStorageState(error)) this.enterStorageFailed("registry storage state cannot be confirmed; restart and reconcile", "quarantine");
1036
1191
  throw error;
@@ -1042,14 +1197,20 @@ var MultiRootRegistry = class extends Service {
1042
1197
  * directory. Updates the cache, republishes the scope, and returns the new
1043
1198
  * list — the shared body of the read-path re-checks.
1044
1199
  */
1045
- reclassify(key) {
1046
- const record = this.lookupRecords(key);
1047
- if (record === void 0) {
1200
+ async reclassify(key) {
1201
+ const found = this.lookupRecords(key);
1202
+ if (found === void 0) {
1048
1203
  this.cache.set(key, []);
1049
1204
  this.publish(key, []);
1050
1205
  return [];
1051
1206
  }
1052
- return this.accept(key, record);
1207
+ if (found.migrated) try {
1208
+ await this.requireTable().put(key, found.record);
1209
+ } catch (error) {
1210
+ if (unknownStorageState(error)) this.enterStorageFailed("registry write result is unknown; restart and reconcile durable state", "quarantine");
1211
+ throw error;
1212
+ }
1213
+ return this.accept(key, found.record);
1053
1214
  }
1054
1215
  /**
1055
1216
  * Persist one status list, then republish it into the scope and to listeners.
@@ -1060,13 +1221,12 @@ var MultiRootRegistry = class extends Service {
1060
1221
  const table = this.requireTable();
1061
1222
  const durable = persistedPrimaryRoot.parse({ roots: migrateRootSkills(statuses.map(toPersistedRoot)) });
1062
1223
  try {
1063
- if (statuses.length === 0) await table.delete(key);
1064
- else await table.put(key, durable);
1224
+ await table.put(key, durable);
1065
1225
  } catch (error) {
1066
1226
  if (unknownStorageState(error)) this.enterStorageFailed("registry write result is unknown; restart and reconcile durable state", "quarantine");
1067
1227
  throw error;
1068
1228
  }
1069
- const classified = statuses.length === 0 ? [] : classifyStoredRoots(key, durable.roots.map(toRegisteredRoot));
1229
+ const classified = statuses.length === 0 ? [] : classifyStoredRoots(canonicalRoot(key), durable.roots.map(toRegisteredRoot), process.platform, this.knownProtectedPaths());
1070
1230
  this.cache.set(key, classified);
1071
1231
  this.publish(key, classified);
1072
1232
  return classified;
@@ -1112,12 +1272,7 @@ var MultiRootRegistry = class extends Service {
1112
1272
  const withheld = statuses.filter((status) => status.state !== "available");
1113
1273
  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");
1114
1274
  const previousMetadata = this.metadataPublished.get(key);
1115
- const metadata = JSON.stringify(this.shared ? statuses : statuses.map((row) => [
1116
- row.id,
1117
- row.aliasId,
1118
- row.skillsEnabled,
1119
- row.state
1120
- ]));
1275
+ const metadata = JSON.stringify(statuses);
1121
1276
  this.metadataPublished.set(key, metadata);
1122
1277
  if ((this.published.get(key) ?? "") === signature) {
1123
1278
  if (previousMetadata !== metadata && (previousMetadata !== void 0 || statuses.length > 0)) {
@@ -1137,7 +1292,7 @@ var MultiRootRegistry = class extends Service {
1137
1292
  projectRoots(statuses) {
1138
1293
  return statuses.flatMap((status) => {
1139
1294
  const recorded = recordedGrantOf(status);
1140
- if (status.state === "invalid" && status.invalidReason !== "membership-conflict" || recorded === void 0) return [];
1295
+ if (status.state === "invalid" && status.invalidReason === void 0 || recorded === void 0) return [];
1141
1296
  return [{
1142
1297
  id: status.id,
1143
1298
  path: status.path,
@@ -1160,10 +1315,10 @@ var MultiRootRegistry = class extends Service {
1160
1315
  });
1161
1316
  }
1162
1317
  async captureSnapshot(primary) {
1163
- const key = canonicalRoot(primary);
1164
- return await this.serialize(key, () => {
1318
+ const key = primaryKey(primary);
1319
+ return await this.serialize(key, async () => {
1165
1320
  this.requireActive();
1166
- this.reclassify(key);
1321
+ await this.reclassify(key);
1167
1322
  return this.snapshotOf(key);
1168
1323
  });
1169
1324
  }
@@ -1205,15 +1360,19 @@ var MultiRootRegistry = class extends Service {
1205
1360
  this.notifyShared(key);
1206
1361
  for (const listener of this.listeners) listener(key);
1207
1362
  }
1363
+ /** Narrow owner rows for this Host, identically for projection and admission. */
1364
+ localSnapshotRoots(snapshot) {
1365
+ return markProtectedPaths(snapshot.roots.map((row) => Object.fromEntries(Object.entries({
1366
+ ...row,
1367
+ id: additionalRootId(row.id)
1368
+ }).filter(([, value]) => value !== void 0))), this.knownProtectedPaths());
1369
+ }
1208
1370
  applySnapshot(snapshot) {
1209
1371
  if (!this.client || snapshot.store !== this.medium?.id || snapshot.epoch !== this.client.epoch) return;
1210
1372
  const previous = this.remoteRevisions.get(snapshot.primary) ?? -1;
1211
1373
  if (snapshot.revision < previous) return;
1212
1374
  this.remoteRevisions.set(snapshot.primary, snapshot.revision);
1213
- const rows = snapshot.roots.map((row) => Object.fromEntries(Object.entries({
1214
- ...row,
1215
- id: additionalRootId(row.id)
1216
- }).filter(([, value]) => value !== void 0)));
1375
+ const rows = this.localSnapshotRoots(snapshot);
1217
1376
  this.commit(() => {
1218
1377
  this.cache.set(snapshot.primary, rows);
1219
1378
  this.publish(snapshot.primary, rows);
@@ -1283,7 +1442,7 @@ var MultiRootRegistry = class extends Service {
1283
1442
  client.close();
1284
1443
  throw error;
1285
1444
  }
1286
- if (generation !== this.connectionGeneration || client !== this.client || snapshot.store !== this.medium?.id || snapshot.epoch !== client.epoch || snapshot.primary !== canonicalRoot(String(args[0]))) {
1445
+ if (generation !== this.connectionGeneration || client !== this.client || snapshot.store !== this.medium?.id || snapshot.epoch !== client.epoch || snapshot.primary !== primaryKey(String(args[0]))) {
1287
1446
  client.close();
1288
1447
  throw new Error("stale registry connection or authority epoch");
1289
1448
  }
@@ -1379,7 +1538,7 @@ var MultiRootRegistry = class extends Service {
1379
1538
  this.enterStorageFailed(`${DOMAIN_NAME}: leasePath must be an absolute path`, "config");
1380
1539
  return;
1381
1540
  }
1382
- if (this.sharedStorageRoot !== void 0 && this.medium === void 0) try {
1541
+ if (this.medium === void 0) try {
1383
1542
  if (!isAbsolute(this.sharedStorageRoot)) throw new SharedConfigError("sharedStorageRoot must be absolute");
1384
1543
  const medium = storeIdentity(this.sharedStorageRoot);
1385
1544
  if (storeIdentity(dirname(leasePath)).id !== medium.id || basename(leasePath) !== `multi_root_workspace.lock`) throw new SharedConfigError("shared registry requires its canonical medium lock, without alternate lock names");
@@ -1454,7 +1613,7 @@ var MultiRootRegistry = class extends Service {
1454
1613
  async openDomain() {
1455
1614
  let domain;
1456
1615
  try {
1457
- domain = this.medium === void 0 ? await this.ctx.storageDomain.open(multiRootDomainSpec) : await openAuthorityStorage(this.authorityStorageContext, this.medium.root, multiRootDomainSpec);
1616
+ domain = await openAuthorityStorage(this.authorityStorageContext, this.medium.root, multiRootDomainSpec);
1458
1617
  } catch (error) {
1459
1618
  const reason = `${DOMAIN_NAME}: ${error instanceof Error ? error.message : String(error)}`;
1460
1619
  if (this.lastOpenFailure !== reason) {
@@ -1483,7 +1642,7 @@ var MultiRootRegistry = class extends Service {
1483
1642
  }
1484
1643
  this.ctx.logger.warn(`multi-root skills: metadata migration failed; Skills remain withheld: ${String(error)}`);
1485
1644
  }
1486
- const key = canonicalRoot(primaryRoot);
1645
+ const key = primaryKey(primaryRoot);
1487
1646
  staged.set(key, this.classify(key, accepted));
1488
1647
  }
1489
1648
  } catch (error) {
@@ -1524,8 +1683,7 @@ var MultiRootRegistry = class extends Service {
1524
1683
  /** Fail closed: empty scope, no table, contended state. */
1525
1684
  enterContended() {
1526
1685
  this.operationEpoch = void 0;
1527
- if (this.shared) this.remoteUnavailable();
1528
- else this.withdrawPublished();
1686
+ this.remoteUnavailable();
1529
1687
  this.ctx.multiRootScope.setRegistryAuthority(false);
1530
1688
  this.table = void 0;
1531
1689
  this.authorityState = { kind: "contended" };
@@ -1538,11 +1696,9 @@ var MultiRootRegistry = class extends Service {
1538
1696
  enterStorageFailed(reason, recovery) {
1539
1697
  this.operationEpoch = void 0;
1540
1698
  this.ctx.multiRootScope.setRegistryAuthority(false);
1541
- if (this.shared) {
1542
- for (const [key, rows] of this.cache) this.withholdScope(key, rows);
1543
- this.server?.close();
1544
- this.server = void 0;
1545
- } else this.withdrawPublished();
1699
+ for (const [key, rows] of this.cache) this.withholdScope(key, rows);
1700
+ this.server?.close();
1701
+ this.server = void 0;
1546
1702
  this.table = void 0;
1547
1703
  this.authorityState = {
1548
1704
  kind: "storage-failed",
@@ -1,4 +1,4 @@
1
- import { c as loadHostKoffi } from "./protocol-CnR1KTXy.js";
1
+ import { c as loadHostKoffi } from "./protocol-CwDnFEyK.js";
2
2
  import { realpathSync } from "node:fs";
3
3
  import { createHash } from "node:crypto";
4
4
  import { basename, dirname, join, resolve } from "node:path";
@@ -10,6 +10,10 @@ function currentRegistryOperation(proof) {
10
10
  const frame = operations.getStore();
11
11
  return frame?.active === true && frame.proof === proof ? frame.operation : void 0;
12
12
  }
13
+ function operationFrameState(proof) {
14
+ const frame = operations.getStore();
15
+ return frame === void 0 || frame.proof !== proof ? "absent" : frame.active === true ? "active" : "expired";
16
+ }
13
17
  /** Capture by value: neither later publication nor a caller can mutate admission. */
14
18
  function freezeOperationScope(scope) {
15
19
  return Object.freeze({
@@ -113,4 +117,4 @@ function operationPermitsProfile(proof, argv, capped) {
113
117
  return frame?.active === true && frame.proof === proof && frame.profiles.some((profile) => profile.capped === capped && profile.argv.length <= argv.length && profile.argv.every((arg, i) => argv[i] === arg));
114
118
  }
115
119
  //#endregion
116
- export { runPrimaryOperation as a, withRegistryRead as c, rememberOperationProfile as i, freezeOperationScope as n, runRegistryOperation as o, operationPermitsProfile as r, withRegistryAdmission as s, currentRegistryOperation as t };
120
+ export { rememberOperationProfile as a, withRegistryAdmission as c, operationPermitsProfile as i, withRegistryRead as l, freezeOperationScope as n, runPrimaryOperation as o, operationFrameState as r, runRegistryOperation as s, currentRegistryOperation as t };
package/lib/registry.js CHANGED
@@ -1,3 +1,3 @@
1
- import { a as MultiRootRegistry, c as multiRootDomainSpec, i as MAX_ALIAS_LENGTH, n as DOMAIN_NAME, o as REGISTRY_CONTENDED_MESSAGE, r as MAX_ADDITIONAL_ROOTS_CEILING, s as TABLE_NAME, t as DEFAULT_MAX_ADDITIONAL_ROOTS } from "./registry-DR_sMuvS.js";
2
- import { o as nativeHostAvailability } from "./protocol-CnR1KTXy.js";
1
+ import { a as MultiRootRegistry, c as multiRootDomainSpec, i as MAX_ALIAS_LENGTH, n as DOMAIN_NAME, o as REGISTRY_CONTENDED_MESSAGE, r as MAX_ADDITIONAL_ROOTS_CEILING, s as TABLE_NAME, t as DEFAULT_MAX_ADDITIONAL_ROOTS } from "./registry-BibiukWk.js";
2
+ import { o as nativeHostAvailability } from "./protocol-CwDnFEyK.js";
3
3
  export { DEFAULT_MAX_ADDITIONAL_ROOTS, DOMAIN_NAME, MAX_ADDITIONAL_ROOTS_CEILING, MAX_ALIAS_LENGTH, MultiRootRegistry, MultiRootRegistry as default, REGISTRY_CONTENDED_MESSAGE, TABLE_NAME, multiRootDomainSpec, nativeHostAvailability };