@peers-app/peers-sdk 0.26.4 → 0.26.5

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 (47) hide show
  1. package/dist/contracts/__tests__/contract-resolvers.test.js +24 -0
  2. package/dist/contracts/__tests__/legacy-removal.test.js +38 -0
  3. package/dist/contracts/__tests__/remote-contract-transport.test.js +23 -32
  4. package/dist/contracts/__tests__/system-contracts.test.js +80 -0
  5. package/dist/contracts/contract-resolvers.d.ts +1 -1
  6. package/dist/contracts/contract-resolvers.js +3 -2
  7. package/dist/contracts/create-local-consumer.d.ts +2 -1
  8. package/dist/contracts/create-local-consumer.js +1 -1
  9. package/dist/contracts/index.d.ts +2 -0
  10. package/dist/contracts/index.js +2 -0
  11. package/dist/contracts/remote-contract-transport.d.ts +3 -3
  12. package/dist/contracts/remote-contract-transport.js +19 -17
  13. package/dist/contracts/system/host-shell-contract.d.ts +191 -0
  14. package/dist/contracts/system/host-shell-contract.js +190 -0
  15. package/dist/contracts/system/index.d.ts +4 -0
  16. package/dist/contracts/system/index.js +4 -0
  17. package/dist/contracts/system/packages-contract.d.ts +257 -0
  18. package/dist/contracts/system/packages-contract.js +213 -0
  19. package/dist/data/create-group.d.ts +1 -1
  20. package/dist/data/create-group.js +1 -1
  21. package/dist/data/package-version-resolver.d.ts +8 -2
  22. package/dist/data/package-version-resolver.js +28 -2
  23. package/dist/data/package-version-resolver.test.js +42 -0
  24. package/dist/package-installer/package-author-signer.d.ts +19 -0
  25. package/dist/package-installer/package-author-signer.js +24 -0
  26. package/dist/package-installer/package-creator.d.ts +1 -7
  27. package/dist/package-installer/package-creator.js +0 -12
  28. package/dist/package-installer/package-creator.test.js +0 -16
  29. package/dist/package-installer/package-installer.d.ts +5 -0
  30. package/dist/package-installer/package-installer.js +60 -37
  31. package/dist/package-installer/package-installer.test.js +129 -0
  32. package/dist/package-installer/types.d.ts +5 -11
  33. package/dist/package-loader/index.d.ts +1 -0
  34. package/dist/package-loader/index.js +1 -0
  35. package/dist/package-loader/isolated-runtime.d.ts +18 -0
  36. package/dist/package-loader/package-loader.d.ts +67 -4
  37. package/dist/package-loader/package-loader.js +300 -52
  38. package/dist/package-loader/package-loader.test.js +413 -1
  39. package/dist/package-loader/package-runtime-state.d.ts +112 -0
  40. package/dist/package-loader/package-runtime-state.js +18 -0
  41. package/dist/package-loader/resolve-consumed-contracts.js +5 -4
  42. package/dist/package-loader/resolve-consumed-contracts.test.js +1 -1
  43. package/dist/rpc-types.d.ts +0 -121
  44. package/dist/rpc-types.js +0 -39
  45. package/dist/system-ids.d.ts +13 -0
  46. package/dist/system-ids.js +14 -1
  47. package/package.json +1 -1
@@ -13,14 +13,22 @@ const files_1 = require("../data/files");
13
13
  const package_version_resolver_1 = require("../data/package-version-resolver");
14
14
  const package_versions_1 = require("../data/package-versions");
15
15
  const packages_1 = require("../data/packages");
16
+ const keys_1 = require("../keys");
17
+ const observable_1 = require("../observable");
16
18
  const contract_package_loader_1 = require("./contract-package-loader");
17
19
  const isolated_artifact_1 = require("./isolated-artifact");
18
20
  const isolated_persistent_var_gateway_1 = require("./isolated-persistent-var-gateway");
19
21
  const isolated_runtime_1 = require("./isolated-runtime");
20
22
  const isolated_table_gateway_1 = require("./isolated-table-gateway");
23
+ const package_runtime_state_1 = require("./package-runtime-state");
21
24
  const resolve_consumed_contracts_1 = require("./resolve-consumed-contracts");
22
25
  // Ensure built-in system contracts are cataloged before any PackageLoader installs them.
23
26
  require("../contracts/system");
27
+ /**
28
+ * Build the trusted install options for an isolated artifact from its selected version.
29
+ *
30
+ * @param packageVersion - Selected PackageVersion; `dev` and `beta` publish mutable contracts.
31
+ */
24
32
  function isolatedContractLifecycle(packageVersion) {
25
33
  return {
26
34
  contractLifecycle: packageVersion?.versionTag === "dev" || packageVersion?.versionTag === "beta"
@@ -53,6 +61,17 @@ class PackageLoader {
53
61
  contractInstalledPackages = new Set();
54
62
  /** Packages installed through the isolated runtime (no host eval). */
55
63
  isolatedPackageIds = new Set();
64
+ /** Per-package counter bumped on every runtime-state change; lets slow unloads yield. */
65
+ runtimeStateSequences = new Map();
66
+ /** Hash of the bundle each loaded package was built from, for `onlyIfChanged` loads. */
67
+ loadedBundleHashes = new Map();
68
+ unsubscribePackageHealth;
69
+ /**
70
+ * Observed runtime state of each package this loader has attempted, keyed by package id.
71
+ * In memory only; read merged with isolated worker health through
72
+ * {@link getPackageRuntimeState} and {@link getPackageRuntimeStates}.
73
+ */
74
+ runtimeStates = (0, observable_1.observable)({});
56
75
  /**
57
76
  * In-memory contract providers for this data context. Seeded with system
58
77
  * contracts after catalog readiness; package providers are added via
@@ -100,6 +119,20 @@ class PackageLoader {
100
119
  }),
101
120
  },
102
121
  });
122
+ this.unsubscribePackageHealth = this.isolatedRuntime.subscribePackageHealth?.((packageId) => {
123
+ const previous = this.runtimeStates()[packageId];
124
+ if (!previous || this.disposed)
125
+ return;
126
+ const { worker: _worker, ...state } = previous;
127
+ const worker = state.running?.kind === "isolated"
128
+ ? this.isolatedRuntime?.getPackageHealth?.(packageId)
129
+ : undefined;
130
+ // Health does not supersede a load/unload; keep its sequence unchanged.
131
+ this.runtimeStates({
132
+ ...this.runtimeStates(),
133
+ [packageId]: { ...state, ...(worker ? { worker } : {}), updatedAt: Date.now() },
134
+ });
135
+ });
103
136
  }
104
137
  }
105
138
  /**
@@ -123,6 +156,68 @@ class PackageLoader {
123
156
  supportsIsolatedPackages() {
124
157
  return this.isolatedRuntime !== undefined;
125
158
  }
159
+ /**
160
+ * Return the observed runtime state of one package, merged with isolated worker health.
161
+ *
162
+ * @param packageId - Package to query.
163
+ * @returns The state, or `undefined` when this loader has never attempted the package.
164
+ */
165
+ getPackageRuntimeState(packageId) {
166
+ const state = this.runtimeStates()[packageId];
167
+ if (!state)
168
+ return undefined;
169
+ const worker = state.running?.kind === "isolated"
170
+ ? this.isolatedRuntime?.getPackageHealth?.(packageId)
171
+ : undefined;
172
+ return worker ? { ...state, worker } : state;
173
+ }
174
+ /** Return every recorded package runtime state, merged with isolated worker health. */
175
+ getPackageRuntimeStates() {
176
+ return Object.fromEntries(Object.keys(this.runtimeStates()).map((packageId) => [
177
+ packageId,
178
+ this.getPackageRuntimeState(packageId),
179
+ ]));
180
+ }
181
+ setRuntimeState(state) {
182
+ this.runtimeStateSequences.set(state.packageId, (this.runtimeStateSequences.get(state.packageId) ?? 0) + 1);
183
+ const worker = state.running?.kind === "isolated"
184
+ ? this.isolatedRuntime?.getPackageHealth?.(state.packageId)
185
+ : undefined;
186
+ this.runtimeStates({
187
+ ...this.runtimeStates(),
188
+ [state.packageId]: { ...state, ...(worker ? { worker } : {}) },
189
+ });
190
+ }
191
+ recordRunning(packageId, running) {
192
+ this.setRuntimeState({ packageId, status: "running", running, updatedAt: Date.now() });
193
+ }
194
+ recordStatus(packageId, status, reason) {
195
+ const previous = this.runtimeStates()[packageId];
196
+ this.setRuntimeState({ ...previous, packageId, status, reason, updatedAt: Date.now() });
197
+ }
198
+ /**
199
+ * Record a failed load attempt unless a newer load or unload superseded it. The previous
200
+ * `running` version is kept: a failed replacement leaves the old version serving.
201
+ */
202
+ recordLoadFailure(packageId, packageVersion, error, assertCurrent) {
203
+ if (!isCurrent(assertCurrent))
204
+ return;
205
+ const now = Date.now();
206
+ const previous = this.runtimeStates()[packageId];
207
+ this.setRuntimeState({
208
+ packageId,
209
+ status: "failed",
210
+ running: previous?.running,
211
+ lastError: {
212
+ phase: "load",
213
+ message: (0, package_runtime_state_1.runtimeErrorMessage)(error),
214
+ packageVersionId: packageVersion?.packageVersionId,
215
+ version: packageVersion?.version,
216
+ at: now,
217
+ },
218
+ updatedAt: now,
219
+ });
220
+ }
126
221
  async dispatchToolFromProvider(packageId, target, args, context) {
127
222
  if (!context.authority?.userId ||
128
223
  !context.dataContext ||
@@ -354,9 +449,12 @@ class PackageLoader {
354
449
  * before any consumer call payload is processed.
355
450
  *
356
451
  * @param dependency - Validated package consume declaration.
452
+ * @param callerPackageId - Owning package, supplied by the loader rather than call input.
357
453
  * @returns A live consumer proxy, or `undefined` when no provider matches.
358
454
  */
359
- async resolveConsumedContract(dependency) {
455
+ async resolveConsumedContract(dependency, callerPackageId) {
456
+ if (!callerPackageId)
457
+ throw new Error("A package caller identity is required");
360
458
  await this.catalogReady;
361
459
  const selected = this.contractRegistry.selectProvider(dependency);
362
460
  if (!selected) {
@@ -367,7 +465,7 @@ class PackageLoader {
367
465
  const router = (0, contract_provider_router_1.createContractProviderRouter)(providerTransport, {
368
466
  authorizeCall: () => ({
369
467
  dataContextId: this.dataContext.dataContextId,
370
- context: { dataContext: this.dataContext },
468
+ context: { dataContext: this.dataContext, callerPackageId },
371
469
  }),
372
470
  resolveProviderPackageId: () => providerPackageId,
373
471
  resolveEndpoint: (request) => this.resolveContractEndpoint(request, { providerPackageId }),
@@ -395,9 +493,12 @@ class PackageLoader {
395
493
  * installed package) so the proxy knows the tool names.
396
494
  *
397
495
  * @param declaration - Validated package remote declaration.
496
+ * @param callerPackageId - Owning package, retained for local and remote authorization.
398
497
  * @returns A per-device consumer factory, or `undefined` when the contract is unknown.
399
498
  */
400
- async resolveRemoteContract(declaration) {
499
+ async resolveRemoteContract(declaration, callerPackageId) {
500
+ if (!callerPackageId)
501
+ throw new Error("A package caller identity is required");
401
502
  await this.catalogReady;
402
503
  const definition = this.contractRegistry.getDefinition(declaration.contractId, declaration.version);
403
504
  if (!definition) {
@@ -407,7 +508,7 @@ class PackageLoader {
407
508
  const router = (0, contract_provider_router_1.createContractProviderRouter)(hostTransport, {
408
509
  authorizeCall: () => ({
409
510
  dataContextId: this.dataContext.dataContextId,
410
- context: { dataContext: this.dataContext },
511
+ context: { dataContext: this.dataContext, callerPackageId },
411
512
  }),
412
513
  resolveProviderPackageId: (request) => this.contractRegistry.getProviderPackageId(request.contractId, request.version),
413
514
  resolveEndpoint: (request) => this.resolveContractEndpoint(request),
@@ -441,12 +542,17 @@ class PackageLoader {
441
542
  /**
442
543
  * Stop an isolated worker (if any) and drop the in-memory package instance.
443
544
  *
545
+ * Records the given runtime status unless a newer load recorded state while the unload
546
+ * was in flight.
547
+ *
444
548
  * @param packageId - Package to unload.
549
+ * @param opts - Runtime status and reason to record.
445
550
  */
446
- async unloadPackage(packageId) {
551
+ async unloadPackage(packageId, opts = {}) {
447
552
  // Invalidate loads before the first await, including those still resolving
448
553
  // bundles or consumed contracts outside the catalog publication queue.
449
554
  this.loadGenerations.set(packageId, (this.loadGenerations.get(packageId) ?? 0) + 1);
555
+ const stateSequence = this.runtimeStateSequences.get(packageId);
450
556
  // Uninstall also queues behind an install that has not published yet.
451
557
  await this.isolatedRuntime?.uninstall(packageId);
452
558
  await this.toolCatalog.removeProvider(packageId, () => {
@@ -456,13 +562,23 @@ class PackageLoader {
456
562
  this.isolatedPackageIds.delete(packageId);
457
563
  this.contractInstalledPackages.delete(packageId);
458
564
  delete this.packageInstances[packageId];
565
+ this.loadedBundleHashes.delete(packageId);
459
566
  });
567
+ if (!this.disposed && this.runtimeStateSequences.get(packageId) === stateSequence) {
568
+ this.setRuntimeState({
569
+ packageId,
570
+ status: opts.status ?? "unloaded",
571
+ reason: opts.reason,
572
+ updatedAt: Date.now(),
573
+ });
574
+ }
460
575
  }
461
576
  /**
462
577
  * Dispose the isolated runtime for this data context.
463
578
  */
464
579
  async dispose() {
465
580
  this.disposed = true;
581
+ this.unsubscribePackageHealth?.();
466
582
  for (const router of this.consumedContractRouters)
467
583
  router.dispose();
468
584
  this.consumedContractRouters.clear();
@@ -482,6 +598,9 @@ class PackageLoader {
482
598
  this.isolatedPackageIds.clear();
483
599
  this.contractInstalledPackages.clear();
484
600
  this.packageInstances = {};
601
+ this.loadedBundleHashes.clear();
602
+ this.runtimeStateSequences.clear();
603
+ this.runtimeStates({});
485
604
  }
486
605
  retireHostEndpoints(packageId) {
487
606
  this.hostActivationGenerations.set(packageId, (this.hostActivationGenerations.get(packageId) ?? 0) + 1);
@@ -499,7 +618,7 @@ class PackageLoader {
499
618
  const packages = await (0, packages_1.Packages)(this.dataContext).list();
500
619
  await Promise.all(packages.map(async (pkg) => {
501
620
  if (pkg.disabled) {
502
- await this.unloadPackage(pkg.packageId);
621
+ await this.unloadPackage(pkg.packageId, DISABLED_UNLOAD);
503
622
  return;
504
623
  }
505
624
  try {
@@ -519,63 +638,50 @@ class PackageLoader {
519
638
  * isolated `dist/package.bundle.js`, that disk artifact wins over a stale
520
639
  * stored Files bundle so local-dev packages pick up new contract versions.
521
640
  *
641
+ * With `onlyIfChanged`, a forced load is skipped when the package is running and the
642
+ * selected version and bundle are exactly the ones already loaded. A changed release
643
+ * channel updates the reported label and isolated contract maturity without restarting
644
+ * the worker. Sync reconciliation uses this so an echoed or unrelated row
645
+ * change does not restart the worker and fail calls in flight. Explicit reloads omit it.
646
+ *
522
647
  * @param pkg - Package row.
523
- * @param opts - Optional force reload, local path fallback, and PV id.
648
+ * @param opts - Optional force reload, `onlyIfChanged`, local path fallback, and PV id.
524
649
  */
525
650
  async loadPackage(pkg, opts) {
526
651
  if (pkg.disabled) {
527
- await this.unloadPackage(pkg.packageId);
652
+ await this.unloadPackage(pkg.packageId, DISABLED_UNLOAD);
528
653
  return undefined;
529
654
  }
530
- const assertCurrent = this.guardLoad(pkg.packageId);
531
655
  if (this.packageInstances[pkg.packageId] && !opts?.force) {
532
656
  return this.packageInstances[pkg.packageId];
533
657
  }
658
+ const assertCurrent = this.guardLoad(pkg.packageId);
534
659
  const localPath = opts?.localPath;
535
660
  let selectedPackageVersion;
536
661
  try {
537
- let bundleCode = "";
538
- let usingLocalBundle = false;
539
- let bundleFileId;
540
- const pvId = opts?.packageVersionId ?? pkg.activePackageVersionId;
541
- if (pvId) {
542
- try {
543
- const pv = await (0, package_versions_1.PackageVersions)(this.dataContext).get(pvId);
544
- if (pv) {
545
- selectedPackageVersion = pv;
546
- bundleFileId = pv.packageBundleFileId;
547
- }
548
- }
549
- catch {
550
- /* no version record */
551
- }
552
- }
553
- if (bundleFileId) {
554
- const filesTable = (0, files_1.Files)(this.dataContext);
555
- const bundleBuffer = await filesTable.getFileContents(bundleFileId);
556
- if (bundleBuffer) {
557
- bundleCode = Buffer.from(bundleBuffer).toString("utf8");
558
- }
559
- else {
560
- console.warn(`Package bundle file not found for ${pkg.name} (fileId: ${bundleFileId})`);
561
- }
562
- }
563
- else {
564
- console.warn(`Package ${pkg.name} does not have a bundle file defined.`);
662
+ if (opts?.onlyIfChanged &&
663
+ this.packageInstances[pkg.packageId] &&
664
+ (await this.keepRunningBuild(pkg, opts, assertCurrent))) {
665
+ assertCurrent();
666
+ return this.packageInstances[pkg.packageId];
565
667
  }
566
- if (localPath && selectedPackageVersion?.versionTag === "dev") {
567
- const localBundleCode = this._readLocalBundle(localPath);
568
- if (localBundleCode) {
569
- bundleCode = localBundleCode;
570
- usingLocalBundle = true;
668
+ assertCurrent();
669
+ const selected = await this.readSelectedBundle(pkg, opts, true);
670
+ selectedPackageVersion = selected.packageVersion;
671
+ const { bundleCode, bundleFileId, usingLocalBundle } = selected;
672
+ if (!bundleCode) {
673
+ if (isCurrent(assertCurrent)) {
674
+ this.recordStatus(pkg.packageId, "unavailable", bundleFileId
675
+ ? "The selected version's bundle file is not on this device yet"
676
+ : "No package version with a bundle is selected");
571
677
  }
572
- }
573
- if (!bundleCode)
574
678
  return;
575
- return await this._loadBundle(pkg, bundleCode, selectedPackageVersion, usingLocalBundle, opts?.force === true, assertCurrent);
679
+ }
680
+ return this.rememberBundle(pkg.packageId, bundleCode, await this._loadBundle(pkg, bundleCode, selectedPackageVersion, usingLocalBundle, opts?.force === true, assertCurrent), assertCurrent);
576
681
  }
577
682
  catch (err) {
578
683
  if (isIsolatedArtifactError(err)) {
684
+ this.recordLoadFailure(pkg.packageId, selectedPackageVersion, err, assertCurrent);
579
685
  throw err;
580
686
  }
581
687
  if (localPath && selectedPackageVersion?.versionTag === "dev") {
@@ -583,20 +689,116 @@ class PackageLoader {
583
689
  try {
584
690
  const localBundleCode = this._readLocalBundle(localPath);
585
691
  if (localBundleCode) {
586
- return await this._loadBundle(pkg, localBundleCode, selectedPackageVersion, true, opts?.force === true, assertCurrent);
692
+ return this.rememberBundle(pkg.packageId, localBundleCode, await this._loadBundle(pkg, localBundleCode, selectedPackageVersion, true, opts?.force === true, assertCurrent), assertCurrent);
587
693
  }
588
694
  }
589
695
  catch (localErr) {
590
696
  if (isIsolatedArtifactError(localErr)) {
697
+ this.recordLoadFailure(pkg.packageId, selectedPackageVersion, localErr, assertCurrent);
591
698
  throw localErr;
592
699
  }
593
700
  console.debug(`Could not load local bundle for ${pkg.name}`, localErr);
594
701
  }
595
702
  }
703
+ this.recordLoadFailure(pkg.packageId, selectedPackageVersion, err, assertCurrent);
596
704
  console.debug(`Could not load package bundle for ${pkg.name}`, err);
597
705
  return;
598
706
  }
599
707
  }
708
+ /**
709
+ * Read the bundle a load would use: the selected version's stored file, replaced by the
710
+ * rebuilt local `dist/package.bundle.js` for a dev version with a local path.
711
+ */
712
+ async readSelectedBundle(pkg, opts, warn) {
713
+ let packageVersion;
714
+ let bundleFileId;
715
+ let bundleCode = "";
716
+ let usingLocalBundle = false;
717
+ const pvId = opts?.packageVersionId ?? pkg.activePackageVersionId;
718
+ if (pvId) {
719
+ try {
720
+ const pv = await (0, package_versions_1.PackageVersions)(this.dataContext).get(pvId);
721
+ if (pv) {
722
+ packageVersion = pv;
723
+ bundleFileId = pv.packageBundleFileId;
724
+ }
725
+ }
726
+ catch {
727
+ /* no version record */
728
+ }
729
+ }
730
+ if (bundleFileId) {
731
+ const bundleBuffer = await (0, files_1.Files)(this.dataContext).getFileContents(bundleFileId);
732
+ if (bundleBuffer) {
733
+ bundleCode = Buffer.from(bundleBuffer).toString("utf8");
734
+ }
735
+ else if (warn) {
736
+ console.warn(`Package bundle file not found for ${pkg.name} (fileId: ${bundleFileId})`);
737
+ }
738
+ }
739
+ else if (warn) {
740
+ console.warn(`Package ${pkg.name} does not have a bundle file defined.`);
741
+ }
742
+ if (opts?.localPath && packageVersion?.versionTag === "dev") {
743
+ const localBundleCode = this._readLocalBundle(opts.localPath);
744
+ if (localBundleCode) {
745
+ bundleCode = localBundleCode;
746
+ usingLocalBundle = true;
747
+ }
748
+ }
749
+ return { packageVersion, bundleFileId, bundleCode, usingLocalBundle };
750
+ }
751
+ /**
752
+ * True when the package is running the exact version and bundle a load would select, so an
753
+ * `onlyIfChanged` load can keep it. Promotion refreshes the release-channel label and
754
+ * isolated contract maturity without reloading, since the tag is not part of the bundle.
755
+ */
756
+ async keepRunningBuild(pkg, opts, assertCurrent) {
757
+ const loadedHash = this.loadedBundleHashes.get(pkg.packageId);
758
+ const state = this.runtimeStates()[pkg.packageId];
759
+ if (!loadedHash || state?.status !== "running" || !state.running)
760
+ return false;
761
+ try {
762
+ const { packageVersion, bundleCode } = await this.readSelectedBundle(pkg, opts, false);
763
+ assertCurrent();
764
+ if (!bundleCode ||
765
+ !packageVersion ||
766
+ packageVersion.packageVersionId !== state.running.packageVersionId ||
767
+ packageVersion.version !== state.running.version ||
768
+ (0, keys_1.hashValue)(bundleCode) !== loadedHash) {
769
+ return false;
770
+ }
771
+ if (packageVersion.versionTag !== state.running.versionTag) {
772
+ if (state.running.kind === "isolated") {
773
+ // Promotion must freeze stable contract shapes even though the worker's code
774
+ // stays identical. Older runtime implementations fall back to a normal load.
775
+ if (!this.isolatedRuntime?.updateContractLifecycle)
776
+ return false;
777
+ await this.isolatedRuntime.updateContractLifecycle(pkg.packageId, {
778
+ ...isolatedContractLifecycle(packageVersion),
779
+ assertCurrent,
780
+ });
781
+ assertCurrent();
782
+ }
783
+ this.setRuntimeState({
784
+ ...this.runtimeStates()[pkg.packageId],
785
+ running: { ...state.running, ...runningVersionFields(packageVersion) },
786
+ updatedAt: Date.now(),
787
+ });
788
+ }
789
+ return true;
790
+ }
791
+ catch {
792
+ return false;
793
+ }
794
+ }
795
+ /** Record the bundle a current, successful load ran, and pass its result through. */
796
+ rememberBundle(packageId, bundleCode, instance, assertCurrent) {
797
+ if (instance && isCurrent(assertCurrent)) {
798
+ this.loadedBundleHashes.set(packageId, (0, keys_1.hashValue)(bundleCode));
799
+ }
800
+ return instance;
801
+ }
600
802
  _readLocalBundle(localPath) {
601
803
  try {
602
804
  const _require = this.require ?? defaultRequire ?? (typeof require === "function" ? require : null);
@@ -611,7 +813,8 @@ class PackageLoader {
611
813
  }
612
814
  }
613
815
  guardLoad(packageId) {
614
- const generation = this.loadGenerations.get(packageId) ?? 0;
816
+ const generation = (this.loadGenerations.get(packageId) ?? 0) + 1;
817
+ this.loadGenerations.set(packageId, generation);
615
818
  return () => {
616
819
  if (this.disposed || generation !== (this.loadGenerations.get(packageId) ?? 0))
617
820
  throw new Error(`Package load invalidated: ${packageId}`);
@@ -694,20 +897,24 @@ class PackageLoader {
694
897
  }
695
898
  }
696
899
  if (!this.isolatedRuntime) {
697
- await this.unloadPackage(pkg.packageId);
900
+ await this.unloadPackage(pkg.packageId, {
901
+ status: "unsupported",
902
+ reason: "This host has no isolated package runtime",
903
+ });
698
904
  console.warn(`[PackageLoader] Skipping isolated package "${pkg.name}" because this host does not provide an isolated runtime.`);
699
905
  return undefined;
700
906
  }
701
907
  assertCurrent();
702
908
  const installOptions = { ...isolatedContractLifecycle(selectedPackageVersion), assertCurrent };
909
+ let installed;
703
910
  if (this.isolatedRuntime.isInstalled(pkg.packageId)) {
704
911
  if (!force) {
705
912
  return this.packageInstances[pkg.packageId];
706
913
  }
707
- await this.isolatedRuntime.replace(artifact, installOptions);
914
+ installed = await this.isolatedRuntime.replace(artifact, installOptions);
708
915
  }
709
916
  else {
710
- await this.isolatedRuntime.install(artifact, installOptions);
917
+ installed = await this.isolatedRuntime.install(artifact, installOptions);
711
918
  }
712
919
  assertCurrent();
713
920
  this.retireHostEndpoints(pkg.packageId);
@@ -719,6 +926,13 @@ class PackageLoader {
719
926
  appNavs: artifact.manifest.appNavs,
720
927
  };
721
928
  this.packageInstances[pkg.packageId] = instance;
929
+ this.recordRunning(pkg.packageId, {
930
+ ...runningVersionFields(selectedPackageVersion),
931
+ kind: "isolated",
932
+ executionMode: artifact.manifest.executionMode ?? "sandbox",
933
+ provides: installed?.provided,
934
+ consumes: installed?.consumes,
935
+ });
722
936
  return instance;
723
937
  }
724
938
  async _evaluateBundle(pkg, bundleCode, selectedPackageVersion, force, assertCurrent = this.guardLoad(pkg.packageId)) {
@@ -813,6 +1027,10 @@ class PackageLoader {
813
1027
  this.hostConsumes.set(packageDefinition.packageId, packageDefinition.consumes);
814
1028
  this.contractInstalledPackages.add(packageDefinition.packageId);
815
1029
  this.packageInstances[pkg.packageId] = contractPackageInstance;
1030
+ this.recordRunning(pkg.packageId, {
1031
+ ...runningVersionFields(selectedPackageVersion),
1032
+ kind: "contract",
1033
+ });
816
1034
  },
817
1035
  });
818
1036
  // A package may declare remote/consume handles for its own contracts.
@@ -824,7 +1042,10 @@ class PackageLoader {
824
1042
  }
825
1043
  // Legacy path: extract IPeersPackage for backward compat
826
1044
  const packageInstance = bundleExports?.exports || bundleExports?.package || bundleExports?.default || bundleExports;
827
- if (packageInstance && typeof packageInstance === "object") {
1045
+ if (packageInstance &&
1046
+ typeof packageInstance === "object" &&
1047
+ typeof packageInstance.packageId === "string" &&
1048
+ packageInstance.packageId.length > 0) {
828
1049
  const tools = packageInstance.toolInstances;
829
1050
  if (Array.isArray(tools) && tools.length)
830
1051
  throw new Error(`Package '${pkg.name}' (${pkg.packageId}) ships legacy executable tools, which this runtime no longer runs; upgrade it to definePackage() contract tools (docs/Packages/contracts.md#migrating-from-toolinstances)`);
@@ -841,12 +1062,17 @@ class PackageLoader {
841
1062
  this.contractInstalledPackages.delete(pkg.packageId);
842
1063
  }
843
1064
  this.packageInstances[pkg.packageId] = packageInstance;
1065
+ this.recordRunning(pkg.packageId, {
1066
+ ...runningVersionFields(selectedPackageVersion),
1067
+ kind: "legacy",
1068
+ });
844
1069
  });
845
1070
  return packageInstance;
846
1071
  }
847
- return;
1072
+ throw new Error("Package bundle exports nothing usable");
848
1073
  }
849
1074
  catch (err) {
1075
+ this.recordLoadFailure(pkg.packageId, selectedPackageVersion, err, assertCurrent);
850
1076
  console.warn(`[PackageLoader] Failed to evaluate bundle for "${pkg.name}":`, err.message);
851
1077
  return undefined;
852
1078
  }
@@ -854,6 +1080,28 @@ class PackageLoader {
854
1080
  }
855
1081
  exports.PackageLoader = PackageLoader;
856
1082
  const ISOLATED_ARTIFACT_ERROR = "IsolatedPackageArtifactError";
1083
+ const DISABLED_UNLOAD = {
1084
+ status: "disabled",
1085
+ reason: "The package is disabled in this group",
1086
+ };
1087
+ /** True when a load guard still holds (no newer unload or disposal). */
1088
+ function isCurrent(assertCurrent) {
1089
+ try {
1090
+ assertCurrent();
1091
+ return true;
1092
+ }
1093
+ catch {
1094
+ return false;
1095
+ }
1096
+ }
1097
+ /** Copy the identifying fields of the loaded PackageVersion into a running-version record. */
1098
+ function runningVersionFields(packageVersion) {
1099
+ return {
1100
+ packageVersionId: packageVersion?.packageVersionId,
1101
+ version: packageVersion?.version,
1102
+ versionTag: packageVersion?.versionTag,
1103
+ };
1104
+ }
857
1105
  function wrapIsolatedArtifactError(err) {
858
1106
  const error = err instanceof Error ? err : new Error(String(err));
859
1107
  error.name = ISOLATED_ARTIFACT_ERROR;