@capybara-hq/kernel 0.1.0-rc.24 → 0.1.0-rc.25

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 (140) hide show
  1. package/dist/admin/admin.module.js +2 -0
  2. package/dist/admin/health.service.d.ts +2 -2
  3. package/dist/admin/health.service.js +6 -2
  4. package/dist/agents/agents.module.js +3 -0
  5. package/dist/agents/agents.service.d.ts +6 -1
  6. package/dist/agents/agents.service.js +11 -2
  7. package/dist/catalog/catalog-import.module.js +0 -4
  8. package/dist/catalog/catalog-scope.service.d.ts +63 -26
  9. package/dist/catalog/catalog-scope.service.js +146 -71
  10. package/dist/catalog/catalog.module.js +4 -3
  11. package/dist/catalog/catalog.service.d.ts +11 -4
  12. package/dist/catalog/catalog.service.js +21 -5
  13. package/dist/catalog/cli.d.ts +4 -3
  14. package/dist/catalog/cli.js +8 -13
  15. package/dist/catalog/job-ids.d.ts +8 -20
  16. package/dist/catalog/job-ids.js +11 -16
  17. package/dist/catalog/marketplaces.service.d.ts +1 -1
  18. package/dist/catalog/org-plugin-uploads.d.ts +11 -4
  19. package/dist/catalog/org-plugin-uploads.js +21 -6
  20. package/dist/catalog/owned-items.d.ts +6 -13
  21. package/dist/catalog/owned-items.js +11 -29
  22. package/dist/catalog/owned-plugin-cleanup.d.ts +8 -17
  23. package/dist/catalog/owned-plugin-cleanup.js +12 -66
  24. package/dist/catalog/platform-components.d.ts +56 -34
  25. package/dist/catalog/platform-components.js +68 -78
  26. package/dist/catalog/plugin-import.d.ts +28 -25
  27. package/dist/catalog/plugin-import.js +90 -76
  28. package/dist/catalog/plugin-upgrade.d.ts +4 -4
  29. package/dist/catalog/plugin-upgrade.js +7 -19
  30. package/dist/catalog/scope-jobs.d.ts +59 -0
  31. package/dist/catalog/scope-jobs.js +65 -0
  32. package/dist/catalog/scope-sync-workers.d.ts +22 -0
  33. package/dist/catalog/scope-sync-workers.js +58 -2
  34. package/dist/catalog/scope-sync.d.ts +40 -65
  35. package/dist/catalog/scope-sync.js +114 -122
  36. package/dist/channels/inbound.js +2 -2
  37. package/dist/channels/threads.d.ts +4 -3
  38. package/dist/channels/threads.js +4 -3
  39. package/dist/conversations/conversations.controller.js +4 -2
  40. package/dist/conversations/conversations.service.d.ts +17 -5
  41. package/dist/conversations/conversations.service.js +17 -5
  42. package/dist/conversations/events.d.ts +21 -0
  43. package/dist/conversations/events.js +24 -1
  44. package/dist/conversations/source.d.ts +10 -0
  45. package/dist/conversations/source.js +17 -0
  46. package/dist/conversations/tree.d.ts +10 -0
  47. package/dist/conversations/tree.js +35 -0
  48. package/dist/db/kernel-schema.d.ts +9 -14
  49. package/dist/devices/devices.service.d.ts +11 -8
  50. package/dist/devices/devices.service.js +16 -23
  51. package/dist/evals/run-replay.d.ts +13 -2
  52. package/dist/evals/run-replay.js +28 -1
  53. package/dist/evals/snapshot.d.ts +7 -1
  54. package/dist/evals/snapshot.js +2 -1
  55. package/dist/extensions/hooks.d.ts +22 -3
  56. package/dist/extensions/hooks.js +15 -1
  57. package/dist/extensions/lifecycle-hooks.d.ts +99 -0
  58. package/dist/extensions/lifecycle-hooks.js +273 -0
  59. package/dist/extensions/org-config.d.ts +13 -1
  60. package/dist/extensions/org-config.js +19 -2
  61. package/dist/extensions/permission-view.d.ts +9 -5
  62. package/dist/extensions/permission-view.js +9 -4
  63. package/dist/extensions/point-defaults.d.ts +28 -1
  64. package/dist/extensions/point-defaults.js +44 -13
  65. package/dist/inbox/inbox.service.d.ts +6 -20
  66. package/dist/inbox/inbox.service.js +0 -3
  67. package/dist/inbox/org-notices.d.ts +1 -1
  68. package/dist/inbox/platform-inbox.service.d.ts +3 -32
  69. package/dist/inbox/platform-inbox.service.js +5 -54
  70. package/dist/infra/outbox/registry.d.ts +124 -0
  71. package/dist/infra/outbox/registry.js +126 -0
  72. package/dist/infra/queue/queue-names.d.ts +1 -1
  73. package/dist/infra/queue/queue-names.js +1 -0
  74. package/dist/orgs/directory-sync.js +1 -1
  75. package/dist/orgs/members.service.js +1 -1
  76. package/dist/orgs/offboarding.d.ts +50 -57
  77. package/dist/orgs/offboarding.js +117 -162
  78. package/dist/orgs/orgs.module.js +3 -0
  79. package/dist/orgs/orgs.service.d.ts +11 -5
  80. package/dist/orgs/orgs.service.js +15 -6
  81. package/dist/overrides.d.ts +3 -0
  82. package/dist/overrides.js +2 -0
  83. package/dist/platform/ops/inbox.ops.js +4 -20
  84. package/dist/platform/ops/platform.ops.d.ts +1 -0
  85. package/dist/platform/ops/platform.ops.js +6 -3
  86. package/dist/roles/worker-role.d.ts +10 -5
  87. package/dist/roles/worker-role.js +19 -9
  88. package/dist/roles/worker.module.js +2 -4
  89. package/dist/runtime/context/build.d.ts +3 -0
  90. package/dist/runtime/context/build.js +15 -0
  91. package/dist/runtime/context/plugin-context.d.ts +20 -0
  92. package/dist/runtime/context/plugin-context.js +30 -0
  93. package/dist/runtime/langgraph-loop.js +4 -0
  94. package/dist/runtime/loop.d.ts +8 -0
  95. package/dist/runtime/maintenance.d.ts +5 -3
  96. package/dist/runtime/maintenance.js +36 -11
  97. package/dist/runtime/model-call.d.ts +6 -1
  98. package/dist/runtime/model-call.js +1 -0
  99. package/dist/runtime/plugin-context.service.d.ts +69 -0
  100. package/dist/runtime/plugin-context.service.js +208 -0
  101. package/dist/runtime/run-control.d.ts +6 -1
  102. package/dist/runtime/run-control.js +13 -0
  103. package/dist/runtime/run-queue.d.ts +13 -0
  104. package/dist/runtime/run-queue.js +63 -0
  105. package/dist/runtime/run-worker.d.ts +22 -1
  106. package/dist/runtime/run-worker.js +107 -10
  107. package/dist/runtime/runtime.module.js +2 -0
  108. package/dist/runtime/subagents/dispatch.js +1 -0
  109. package/dist/runtime/subagents/skill-fork.js +3 -0
  110. package/dist/sandbox/jobs.d.ts +20 -10
  111. package/dist/sandbox/jobs.js +23 -17
  112. package/dist/schedules/fire.d.ts +25 -4
  113. package/dist/schedules/fire.js +87 -13
  114. package/dist/schedules/job-ids.d.ts +66 -17
  115. package/dist/schedules/job-ids.js +81 -13
  116. package/dist/schedules/run-tracker.js +2 -1
  117. package/dist/schedules/scan.job.d.ts +13 -32
  118. package/dist/schedules/scan.job.js +35 -144
  119. package/dist/schedules/scheduled-tasks.service.d.ts +6 -6
  120. package/dist/schedules/scheduled-tasks.service.js +17 -14
  121. package/dist/schedules/schedules.module.js +0 -4
  122. package/dist/skills/changes.controller.d.ts +1 -1
  123. package/dist/skills/changes.controller.js +1 -1
  124. package/dist/tools/approvals.service.d.ts +15 -4
  125. package/dist/tools/approvals.service.js +26 -10
  126. package/dist/tools/pipeline.d.ts +11 -2
  127. package/dist/tools/pipeline.js +5 -0
  128. package/dist/tools/questions.service.d.ts +15 -4
  129. package/dist/tools/questions.service.js +21 -19
  130. package/dist/tools/registry.d.ts +8 -1
  131. package/dist/tools/registry.js +9 -0
  132. package/package.json +3 -3
  133. package/src/migrations/0121_drop_platform_migration_runs.sql +12 -0
  134. package/src/migrations/0122_drop_scheduled_task_runs_rechecked_at.sql +5 -0
  135. package/src/migrations/0123_pgvector.sql +27 -0
  136. package/src/migrations/0124_conversation_source.sql +36 -0
  137. package/dist/catalog/platform-migration.d.ts +0 -115
  138. package/dist/catalog/platform-migration.js +0 -583
  139. package/dist/infra/queue/add-job-once.d.ts +0 -14
  140. package/dist/infra/queue/add-job-once.js +0 -16
@@ -9,8 +9,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
9
9
  };
10
10
  import { createHash, randomUUID } from "node:crypto";
11
11
  import { appearanceIssues, PLUGIN_MANIFEST_FILE, parseSkillMd, pluginDescription, SDK_VERSION, SKILL_ENTRY_FILE, } from "@capybara-hq/plugin-sdk";
12
- import { getQueueToken } from "@nestjs/bullmq";
13
- import { Inject, Injectable } from "@nestjs/common";
12
+ import { Inject, Injectable, Optional } from "@nestjs/common";
14
13
  import { sql } from "kysely";
15
14
  import semver from "semver";
16
15
  import { ClientBuilds } from "../client/client-builds.service.js";
@@ -35,10 +34,10 @@ import { afterCommit } from "../infra/database/after-commit.js";
35
34
  import { KernelDb } from "../infra/database/kernel-db.js";
36
35
  import { TransactionHost } from "../infra/database/transaction-host.js";
37
36
  import { Transactional } from "../infra/database/transactional.decorator.js";
37
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
38
38
  import { KERNEL_LOGGER } from "../infra/tokens.js";
39
39
  import { OrgLock } from "../orgs/org-lock.js";
40
40
  import { packPackageTgz } from "../packages/tgz.js";
41
- import { MAINTENANCE_QUEUE_NAME } from "../runtime/maintenance-queue.js";
42
41
  import { checkSkillFiles, SkillArchiveError } from "../skills/package.js";
43
42
  import { StorageService } from "../storage/storage.service.js";
44
43
  import { canonicalJson } from "../tools/canonical-json.js";
@@ -52,15 +51,13 @@ import { item as reportItem, sortItems, } from "./claude/report.js";
52
51
  import { isClaudePluginTree, translateClaudePlugin } from "./claude/translate.js";
53
52
  import { CatalogItemLock } from "./locks.js";
54
53
  import { assertNameFree, assertPackageFree, assertShareable, packageOwner, releaseExtensionClaim, shareGate, usableBy, } from "./ownership.js";
55
- import { PlatformComponents } from "./platform-components.js";
56
- import { PLATFORM_MIGRATION_JOB } from "./platform-migration.js";
54
+ import { PlanStale, PlatformComponents } from "./platform-components.js";
57
55
  import { PluginPackageError, readPluginFiles, readPluginPackage, themeApiError, } from "./plugin-package.js";
58
56
  import { hasImportRequests, importRequestOf, resolveImportRequests } from "./request-rules.js";
59
57
  import { assertNameNotReserved } from "./reserved-names.js";
58
+ import { oweScopeItem, SCOPE_BACKSTOP_DELAY, SCOPE_BACKSTOP_DELAY_MS } from "./scope-jobs.js";
60
59
  import { addScopeOrg } from "./scope-orgs.js";
61
60
  import { maxSatisfying, satisfiesRange } from "./versions.js";
62
- /** How long after a failed post-import platform sync the migration job runs for the item. */
63
- export const PLATFORM_SYNC_RETRY_MS = 60_000;
64
61
  /**
65
62
  * Platform import of a plugin (package ecosystem spec §4.3): one review for every plugin, whatever
66
63
  * it contains (an extension, skills, connector templates, dependencies), confirmed once.
@@ -86,8 +83,10 @@ export const PLATFORM_SYNC_RETRY_MS = 60_000;
86
83
  * snapshot when it is installed or upgraded by this import (source web, audit
87
84
  * `extension.permissions_approved` carrying the plugin), and creates the item (with the scope the
88
85
  * admin chose, default `optional`) or adds the version (`manifest` = the normalized plugin.json,
89
- * `extension_sha256`, the files under the staging prefix). After the commit the plugin's own
90
- * extension is installed on the deployment and enabled (`ExtensionAdminOps.install` + `enable`;
86
+ * `extension_sha256`, the files under the staging prefix); an item's first usable version also
87
+ * gets its platform component rows there (their files copied before, keyed by content: a failure
88
+ * leaves nothing a retry copies again; no job heals an item later). After the commit the plugin's
89
+ * own extension is installed on the deployment and enabled (`ExtensionAdminOps.install` + `enable`;
91
90
  * each replica reconciles, no restart); a failure is returned, not thrown, and an upgrade that was
92
91
  * not installed gets its old snapshot back.
93
92
  *
@@ -164,8 +163,9 @@ let PluginImport = class PluginImport {
164
163
  log;
165
164
  txHost;
166
165
  platform;
167
- maintenance;
168
166
  clientBuilds;
167
+ outbox;
168
+ backstopDelayMs;
169
169
  /** Where plugin files go (the kernel's object storage; the CLI's own, see {@link useCliStorage}). */
170
170
  files;
171
171
  /**
@@ -183,12 +183,14 @@ let PluginImport = class PluginImport {
183
183
  blobs,
184
184
  /** Config-declared names, loaded connectors and extension versions (the extension registry). */
185
185
  registry, log, txHost,
186
- /** The platform rows of a new item's components, written after the confirm's commit. */
186
+ /** The platform rows of a new item's components, written in the confirm's transaction. */
187
187
  platform,
188
- /** A failed platform sync enqueues a run of the migration job here (it heals the item). */
189
- maintenance,
190
188
  /** The client build of a dropped org upload's package is deleted with it. */
191
- clientBuilds) {
189
+ clientBuilds,
190
+ /** The scope sync's backstop, owed by the confirm's commit (ruling R24). */
191
+ outbox,
192
+ /** The backstop's delay (a test seam; default `SCOPE_BACKSTOP_DELAY_MS`). */
193
+ backstopDelayMs = SCOPE_BACKSTOP_DELAY_MS) {
192
194
  this.kdb = kdb;
193
195
  this.adminLock = adminLock;
194
196
  this.orgLock = orgLock;
@@ -204,8 +206,9 @@ let PluginImport = class PluginImport {
204
206
  this.log = log;
205
207
  this.txHost = txHost;
206
208
  this.platform = platform;
207
- this.maintenance = maintenance;
208
209
  this.clientBuilds = clientBuilds;
210
+ this.outbox = outbox;
211
+ this.backstopDelayMs = backstopDelayMs;
209
212
  this.files = storage;
210
213
  }
211
214
  /** The kernel-schema handle of the current context (the transaction, else the pool). */
@@ -734,11 +737,16 @@ let PluginImport = class PluginImport {
734
737
  const stored = reviewed && reviewed.action !== "keep" && owner === undefined
735
738
  ? await this.readStored(reviewed.sha256)
736
739
  : undefined;
740
+ // The platform components of the item's first usable version: planned and their files copied
741
+ // (keyed by content) before the transaction, their rows written in it
742
+ const prepared = staged.retry === true || (owner !== undefined && reviewed !== undefined)
743
+ ? undefined
744
+ : await this.prepareFirstVersion(staged, row.object_prefix, owner);
737
745
  const committed = await this.commit(actor, { stagingId, review, staged, src, manifest, objectPrefix: row.object_prefix }, {
738
746
  scope,
739
747
  orgs: [...new Set(orgs ?? [])].sort(),
740
748
  ...(owner !== undefined && { owner, shareable: input.shareable ?? false }),
741
- }, stored, opts);
749
+ }, stored, opts, prepared);
742
750
  const { now } = committed;
743
751
  if (committed.pending) {
744
752
  // Waits for the super admin (P3-10): no platform components, nothing deployed
@@ -750,11 +758,6 @@ let PluginImport = class PluginImport {
750
758
  firstVersion: false,
751
759
  };
752
760
  }
753
- // A new item's skills, agent templates and connector templates become platform components
754
- // (plugin scope phase 2); a new version of an existing item leaves them to the platform
755
- // update. After the commit: it copies files in object storage
756
- if (committed.firstVersion)
757
- await this.syncPlatform(committed.itemId, staged.version);
758
761
  if (input.deploy === false) {
759
762
  return {
760
763
  item: await this.itemView(committed.itemId),
@@ -780,12 +783,31 @@ let PluginImport = class PluginImport {
780
783
  };
781
784
  }
782
785
  /**
783
- * Writes the platform components of an item's first usable version after a commit made
784
- * elsewhere (the super admin's confirmation of an org upload, `CatalogScopeService`); a failure
785
- * is logged and retried by the migration job (see {@link syncPlatform}).
786
+ * The platform components of the staged version when its confirm will most likely make it the
787
+ * item's first usable version ({@link commit} decides under its locks): no live item of the name
788
+ * yet, or an org upload replacing its own first upload still awaiting confirmation. Planned from
789
+ * the staged content and their files copied, outside any transaction; otherwise undefined.
786
790
  */
787
- async writePlatformComponents(itemId, version) {
788
- await this.syncPlatform(itemId, version);
791
+ async prepareFirstVersion(staged, objectPrefix, owner) {
792
+ const live = await this.db
793
+ .selectFrom("catalog_items")
794
+ .select(["id", "awaiting_confirmation", "owner_org_id"])
795
+ .where("name", "=", staged.manifest.name)
796
+ .where("deleted_at", "is", null)
797
+ .executeTakeFirst();
798
+ const first = live === undefined ||
799
+ (owner !== undefined && live.awaiting_confirmation && live.owner_org_id === owner.orgId);
800
+ if (!first)
801
+ return undefined;
802
+ return this.platform.prepareImport({
803
+ itemId: live?.id ?? staged.itemId,
804
+ plugin: staged.manifest.name,
805
+ version: staged.version,
806
+ manifest: staged.manifest,
807
+ objectPrefix,
808
+ files: staged.files,
809
+ sourceFormat: staged.sourceFormat ?? "capybara",
810
+ });
789
811
  }
790
812
  /**
791
813
  * The confirm's transaction: the staging row consumed, the extension's action recomputed under
@@ -800,8 +822,15 @@ let PluginImport = class PluginImport {
800
822
  * upload without an extension for an item whose first version still waits (plan Task 6) replaces
801
823
  * that upload and applies at once: the item stops waiting and the claim is released (nothing
802
824
  * was installed under it).
825
+ *
826
+ * The item's first usable version (a new item not waiting, or an awaiting first upload replaced)
827
+ * writes the platform rows of its skills, agent templates, connector templates and themes
828
+ * (plugin scope phase 2) from `prepared` (their files copied before the transaction, keyed by
829
+ * content): a failure rolls the whole confirm back, and a retry copies nothing again. A plan made
830
+ * for another item or version, or a component changed since it -> `plugin.review_stale`. A new
831
+ * version of an existing item leaves them to the platform update.
803
832
  */
804
- async commit(actor, c, scope, stored, opts) {
833
+ async commit(actor, c, scope, stored, opts, prepared) {
805
834
  const { stagingId, review, staged, src, manifest } = c;
806
835
  const reviewed = review.extension;
807
836
  const stale = () => reviewStale(review);
@@ -1034,61 +1063,44 @@ let PluginImport = class PluginImport {
1034
1063
  await markVersionApplied(t, itemId, staged.version);
1035
1064
  await releaseExtensionClaim(t, m.name, existing.package_name);
1036
1065
  }
1066
+ const firstVersion = (created && !pending) || replacedAwaiting;
1067
+ if (firstVersion) {
1068
+ if (prepared?.plan.itemId !== itemId || prepared.plan.version !== staged.version) {
1069
+ throw stale();
1070
+ }
1071
+ try {
1072
+ await this.platform.applySyncIn(prepared);
1073
+ }
1074
+ catch (e) {
1075
+ if (e instanceof PlanStale)
1076
+ throw stale();
1077
+ throw e;
1078
+ }
1079
+ }
1080
+ // The commit owes the scope sync's backstop (ruling R24) when the item's scope turns it on by
1081
+ // itself: the caller owes the immediate sync after the deploy, which a crash may lose
1082
+ if (!pending) {
1083
+ const item = await t
1084
+ .selectFrom("catalog_items")
1085
+ .select("scope")
1086
+ .where("id", "=", itemId)
1087
+ .executeTakeFirstOrThrow();
1088
+ if (item.scope !== "optional") {
1089
+ await oweScopeItem(this.outbox, t, itemId, {
1090
+ backstop: true,
1091
+ backstopDelayMs: this.backstopDelayMs,
1092
+ });
1093
+ }
1094
+ }
1037
1095
  return {
1038
1096
  itemId,
1039
1097
  created,
1040
- firstVersion: (created && !pending) || replacedAwaiting,
1098
+ firstVersion,
1041
1099
  pending,
1042
1100
  now,
1043
1101
  previous,
1044
1102
  };
1045
1103
  }
1046
- /**
1047
- * The platform components of a new item. The item is committed already, so a failure is logged
1048
- * and not thrown (like a failed deploy, the import itself succeeded); a run of the platform
1049
- * components migration job (which writes what is missing) is enqueued on `cap-kernel-maintenance`
1050
- * after {@link PLATFORM_SYNC_RETRY_MS}, one per item (deterministic job id). If that run fails
1051
- * too, its `itemsFailed` shows in the super admin's inbox; the daily run and every worker start
1052
- * try again.
1053
- */
1054
- async syncPlatform(itemId, version) {
1055
- try {
1056
- await this.platform.syncFromVersion(itemId, version, { initial: true });
1057
- }
1058
- catch (err) {
1059
- // Removed meanwhile (an org deletion racing a confirmation, P3-20): nothing to write
1060
- const live = await this.db
1061
- .selectFrom("catalog_items")
1062
- .select("id")
1063
- .where("id", "=", itemId)
1064
- .where("deleted_at", "is", null)
1065
- .executeTakeFirst();
1066
- if (live === undefined) {
1067
- this.log.info("catalog item removed before its platform components were written", {
1068
- itemId,
1069
- version,
1070
- });
1071
- return;
1072
- }
1073
- this.log.error("writing the platform components of a new catalog item failed", {
1074
- itemId,
1075
- version,
1076
- err,
1077
- });
1078
- try {
1079
- await this.maintenance.add(PLATFORM_MIGRATION_JOB, {}, {
1080
- jobId: `${PLATFORM_MIGRATION_JOB}-retry-${itemId}`,
1081
- delay: PLATFORM_SYNC_RETRY_MS,
1082
- attempts: 1,
1083
- removeOnComplete: true,
1084
- removeOnFail: { age: 86_400 },
1085
- });
1086
- }
1087
- catch (e) {
1088
- this.log.warn("cannot enqueue the platform components migration", { itemId, err: e });
1089
- }
1090
- }
1091
- }
1092
1104
  /**
1093
1105
  * Writes the extension's permission snapshot for the version being imported (source web; cli
1094
1106
  * without an actor; bootstrap for the bootstrap step), after
@@ -1727,8 +1739,10 @@ PluginImport = __decorate([
1727
1739
  __param(13, Inject(KERNEL_LOGGER)),
1728
1740
  __param(14, Inject(TransactionHost)),
1729
1741
  __param(15, Inject(PlatformComponents)),
1730
- __param(16, Inject(getQueueToken(MAINTENANCE_QUEUE_NAME))),
1731
- __param(17, Inject(ClientBuilds))
1742
+ __param(16, Inject(ClientBuilds)),
1743
+ __param(17, Inject(OutboxService)),
1744
+ __param(18, Optional()),
1745
+ __param(18, Inject(SCOPE_BACKSTOP_DELAY))
1732
1746
  ], PluginImport);
1733
1747
  export { PluginImport };
1734
1748
  /**
@@ -178,14 +178,14 @@ export type UpdateTarget = VersionRow;
178
178
  * the extension first (`ExtensionAdminOps.install`, enabled when the update installs it; added
179
179
  * permissions wait for the admin's approval as for any extension upgrade), then one transaction
180
180
  * under the item name's lock that recomputes the diff (`plugin.upgrade_stale` on a difference)
181
- * and writes the platform versions (`PlatformComponents.syncFromVersion`, which records
181
+ * and writes the platform versions (`PlatformComponents.applySyncIn`, which records
182
182
  * `platform_synced_version`), the removals (a component no agent uses is soft-deleted, so
183
183
  * unpinned linked copies go with it; pinned copies keep their own version; a removed connector
184
184
  * template leaves its connectors as the orgs' own), clears the pins of linked copies, fans
185
185
  * changed templates out to every connector made from them (`applyTemplateIn`, under each org's
186
186
  * row lock, credentials cleared only for a changed credential shape), moves every org install to the version and audits
187
- * `plugin.platform_updated`; the scope sync of the item and its dependencies is enqueued after
188
- * the commit (orgs get new dependencies by rule 9). A failure rolls the transaction back and
187
+ * `plugin.platform_updated`; the scope sync of the item and its dependencies is owed in that
188
+ * transaction (orgs get new dependencies by rule 9). A failure rolls the transaction back and
189
189
  * puts the extension back where it was (audit `plugin.platform_update_failed`, 502
190
190
  * `plugin.upgrade_failed`); files copied to `deployment/platform/` stay unreferenced.
191
191
  * Storage is never read or written inside the transaction: the target's contents (and the
@@ -363,7 +363,7 @@ export declare class PlatformPluginUpdater {
363
363
  /**
364
364
  * Other platform plugins depending on this one must accept the target (their platform version's
365
365
  * dependency range); returns the catalog items of the target's own dependencies (their scope
366
- * sync is enqueued after the update: rule 9).
366
+ * sync is owed with the update: rule 9).
367
367
  */
368
368
  private checkDependents;
369
369
  /** The plugin's live platform components with their current versions. */
@@ -26,7 +26,6 @@ import { audit } from "../identity/audit.js";
26
26
  import { SuperAdminLock } from "../identity/super-admin.js";
27
27
  import { addOrgNotice, itemDisplayName, notifySharedUpdate } from "../inbox/org-notices.js";
28
28
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
29
- import { afterCommit } from "../infra/database/after-commit.js";
30
29
  import { KernelDb } from "../infra/database/kernel-db.js";
31
30
  import { TransactionHost } from "../infra/database/transaction-host.js";
32
31
  import { Transactional } from "../infra/database/transactional.decorator.js";
@@ -105,14 +104,14 @@ const filesDigest = (row) => skillFilesDigest(row.files.map((f) => ({ path: f.pa
105
104
  * the extension first (`ExtensionAdminOps.install`, enabled when the update installs it; added
106
105
  * permissions wait for the admin's approval as for any extension upgrade), then one transaction
107
106
  * under the item name's lock that recomputes the diff (`plugin.upgrade_stale` on a difference)
108
- * and writes the platform versions (`PlatformComponents.syncFromVersion`, which records
107
+ * and writes the platform versions (`PlatformComponents.applySyncIn`, which records
109
108
  * `platform_synced_version`), the removals (a component no agent uses is soft-deleted, so
110
109
  * unpinned linked copies go with it; pinned copies keep their own version; a removed connector
111
110
  * template leaves its connectors as the orgs' own), clears the pins of linked copies, fans
112
111
  * changed templates out to every connector made from them (`applyTemplateIn`, under each org's
113
112
  * row lock, credentials cleared only for a changed credential shape), moves every org install to the version and audits
114
- * `plugin.platform_updated`; the scope sync of the item and its dependencies is enqueued after
115
- * the commit (orgs get new dependencies by rule 9). A failure rolls the transaction back and
113
+ * `plugin.platform_updated`; the scope sync of the item and its dependencies is owed in that
114
+ * transaction (orgs get new dependencies by rule 9). A failure rolls the transaction back and
116
115
  * puts the extension back where it was (audit `plugin.platform_update_failed`, 502
117
116
  * `plugin.upgrade_failed`); files copied to `deployment/platform/` stay unreferenced.
118
117
  * Storage is never read or written inside the transaction: the target's contents (and the
@@ -669,20 +668,9 @@ let PlatformPluginUpdater = class PlatformPluginUpdater {
669
668
  ...opts.auditDetail,
670
669
  },
671
670
  });
672
- const enqueue = [itemId, ...diff.dependencies];
673
- afterCommit(async () => {
674
- for (const id of enqueue) {
675
- try {
676
- await this.sync.enqueueItem(id);
677
- }
678
- catch (err) {
679
- this.log.warn("enqueueing the scope sync after a platform update failed", {
680
- itemId: id,
681
- err,
682
- });
683
- }
684
- }
685
- });
671
+ // The scope sync of the item and its dependencies is owed with the update (outbox spec §8.1)
672
+ for (const id of [itemId, ...diff.dependencies])
673
+ await this.sync.enqueueItem(id);
686
674
  return diff;
687
675
  }
688
676
  /**
@@ -1340,7 +1328,7 @@ let PlatformPluginUpdater = class PlatformPluginUpdater {
1340
1328
  /**
1341
1329
  * Other platform plugins depending on this one must accept the target (their platform version's
1342
1330
  * dependency range); returns the catalog items of the target's own dependencies (their scope
1343
- * sync is enqueued after the update: rule 9).
1331
+ * sync is owed with the update: rule 9).
1344
1332
  */
1345
1333
  async checkDependents(name, to) {
1346
1334
  const rows = await this.db
@@ -0,0 +1,59 @@
1
+ import type { KernelSchemaDb } from "../db/kernel-schema.js";
2
+ import type { OutboxService } from "../infra/outbox/outbox.service.js";
3
+ /**
4
+ * The plugin scope sync's jobs as the outbox owes them (transactional outbox spec §8.1): a leaf
5
+ * module (type imports only), so the import confirm (`PluginImport`, below the scope service) and
6
+ * the CLI owe them without the sync. `scope-sync.ts` (the consumer side) re-exports these names.
7
+ */
8
+ /** The scope sync queue (no `:` in the name). */
9
+ export declare const PLUGIN_SCOPE_QUEUE = "cap-kernel-plugin-scope";
10
+ /** The job converging one item in every org. */
11
+ export declare const SCOPE_ITEM_JOB = "scope-item";
12
+ /** The job converging every item in one org. */
13
+ export declare const SCOPE_ORG_JOB = "scope-org";
14
+ /**
15
+ * The delayed backstop of an item job owed by a confirmation's commit (ruling R24): it converges
16
+ * the item as {@link SCOPE_ITEM_JOB} does once the extension is deployed, and fails (BullMQ
17
+ * retries it with its backoff) while the extension is not enabled on the deployment yet.
18
+ */
19
+ export declare const SCOPE_BACKSTOP_JOB = "scope-item-backstop";
20
+ /** How long after the confirmation's commit the backstop runs (the deploy has finished by then). */
21
+ export declare const SCOPE_BACKSTOP_DELAY_MS: number;
22
+ /**
23
+ * Optional provider (a test seam): the backstop's delay (ms) instead of
24
+ * {@link SCOPE_BACKSTOP_DELAY_MS}, so a long test file never has a backstop fire in a later test.
25
+ */
26
+ export declare const SCOPE_BACKSTOP_DELAY: unique symbol;
27
+ /** One item job per scope version (no `:`: item ids are UUIDs). */
28
+ export declare const scopeItemJobId: (itemId: string, version: number) => string;
29
+ /** The backstop of the item job of a scope version (distinct from the immediate one). */
30
+ export declare const scopeBackstopJobId: (itemId: string, version: number) => string;
31
+ /** One org job at a time per org. */
32
+ export declare const scopeOrgJobId: (orgId: string) => string;
33
+ /**
34
+ * The delayed retry `n` of an item job whose extension this replica had not loaded yet (ruling
35
+ * P3-38); deterministic, so replicas deferring the same item add it once.
36
+ */
37
+ export declare const scopeRetryJobId: (itemId: string, version: number, n: number) => string;
38
+ /**
39
+ * The job's data. `retry`: the item job's delayed retry number (P3-38). `backstop`: a
40
+ * {@link SCOPE_BACKSTOP_JOB}.
41
+ */
42
+ export type ScopeJob = {
43
+ kind: "item";
44
+ itemId: string;
45
+ retry?: number;
46
+ backstop?: true;
47
+ } | {
48
+ kind: "org";
49
+ orgId: string;
50
+ };
51
+ /**
52
+ * Owes the item's scope sync in the transaction in context, once per scope version (read in that
53
+ * transaction, so after its own bump); `backstop` owes the delayed backstop instead, due
54
+ * `backstopDelayMs` later (default {@link SCOPE_BACKSTOP_DELAY_MS}). A missing item owes nothing.
55
+ */
56
+ export declare function oweScopeItem(outbox: Pick<OutboxService, "add">, db: KernelSchemaDb, itemId: string, opts?: {
57
+ backstop?: boolean;
58
+ backstopDelayMs?: number;
59
+ }): Promise<void>;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The plugin scope sync's jobs as the outbox owes them (transactional outbox spec §8.1): a leaf
3
+ * module (type imports only), so the import confirm (`PluginImport`, below the scope service) and
4
+ * the CLI owe them without the sync. `scope-sync.ts` (the consumer side) re-exports these names.
5
+ */
6
+ /** The scope sync queue (no `:` in the name). */
7
+ export const PLUGIN_SCOPE_QUEUE = "cap-kernel-plugin-scope";
8
+ /** The job converging one item in every org. */
9
+ export const SCOPE_ITEM_JOB = "scope-item";
10
+ /** The job converging every item in one org. */
11
+ export const SCOPE_ORG_JOB = "scope-org";
12
+ /**
13
+ * The delayed backstop of an item job owed by a confirmation's commit (ruling R24): it converges
14
+ * the item as {@link SCOPE_ITEM_JOB} does once the extension is deployed, and fails (BullMQ
15
+ * retries it with its backoff) while the extension is not enabled on the deployment yet.
16
+ */
17
+ export const SCOPE_BACKSTOP_JOB = "scope-item-backstop";
18
+ /** How long after the confirmation's commit the backstop runs (the deploy has finished by then). */
19
+ export const SCOPE_BACKSTOP_DELAY_MS = 5 * 60_000;
20
+ /**
21
+ * Optional provider (a test seam): the backstop's delay (ms) instead of
22
+ * {@link SCOPE_BACKSTOP_DELAY_MS}, so a long test file never has a backstop fire in a later test.
23
+ */
24
+ export const SCOPE_BACKSTOP_DELAY = Symbol("SCOPE_BACKSTOP_DELAY");
25
+ /** One item job per scope version (no `:`: item ids are UUIDs). */
26
+ export const scopeItemJobId = (itemId, version) => `scope-item-${itemId}-${version}`;
27
+ /** The backstop of the item job of a scope version (distinct from the immediate one). */
28
+ export const scopeBackstopJobId = (itemId, version) => `${scopeItemJobId(itemId, version)}-backstop`;
29
+ /** One org job at a time per org. */
30
+ export const scopeOrgJobId = (orgId) => `scope-org-${orgId}`;
31
+ /**
32
+ * The delayed retry `n` of an item job whose extension this replica had not loaded yet (ruling
33
+ * P3-38); deterministic, so replicas deferring the same item add it once.
34
+ */
35
+ export const scopeRetryJobId = (itemId, version, n) => `scope-item-${itemId}-${version}-r${n}`;
36
+ /**
37
+ * Owes the item's scope sync in the transaction in context, once per scope version (read in that
38
+ * transaction, so after its own bump); `backstop` owes the delayed backstop instead, due
39
+ * `backstopDelayMs` later (default {@link SCOPE_BACKSTOP_DELAY_MS}). A missing item owes nothing.
40
+ */
41
+ export async function oweScopeItem(outbox, db, itemId, opts = {}) {
42
+ const row = await db
43
+ .selectFrom("catalog_items")
44
+ .select("scope_version")
45
+ .where("id", "=", itemId)
46
+ .executeTakeFirst();
47
+ if (!row)
48
+ return;
49
+ if (opts.backstop === true) {
50
+ await outbox.add({
51
+ queue: PLUGIN_SCOPE_QUEUE,
52
+ name: SCOPE_BACKSTOP_JOB,
53
+ jobId: scopeBackstopJobId(itemId, row.scope_version),
54
+ data: { kind: "item", itemId, backstop: true },
55
+ runAt: new Date(Date.now() + (opts.backstopDelayMs ?? SCOPE_BACKSTOP_DELAY_MS)),
56
+ });
57
+ return;
58
+ }
59
+ await outbox.add({
60
+ queue: PLUGIN_SCOPE_QUEUE,
61
+ name: SCOPE_ITEM_JOB,
62
+ jobId: scopeItemJobId(itemId, row.scope_version),
63
+ data: { kind: "item", itemId },
64
+ });
65
+ }
@@ -1,3 +1,25 @@
1
+ import { WorkerHost } from "@nestjs/bullmq";
2
+ import type { Job, Worker } from "bullmq";
3
+ import type { KernelLogger } from "../log.js";
4
+ import { PluginScopeSync, type ScopeJob } from "./scope-sync.js";
5
+ /**
6
+ * The consumer of {@link PLUGIN_SCOPE_QUEUE}: {@link PluginScopeSync} runs the `scope-item`,
7
+ * `scope-item-backstop` and `scope-org` jobs the outbox delivers (any other name fails the job; a
8
+ * backstop whose extension is not deployed yet throws, so BullMQ retries it).
9
+ */
10
+ export declare class PluginScopeSyncProcessor extends WorkerHost<Worker<ScopeJob>> {
11
+ private readonly sync;
12
+ private readonly log;
13
+ private readonly stopping;
14
+ constructor(sync: PluginScopeSync, log: Pick<KernelLogger, "error">);
15
+ process(job: Job<ScopeJob>): Promise<void>;
16
+ /** Starts consuming (worker role, after the extensions loaded; built with `autorun: false`). */
17
+ start(): void;
18
+ /** Stops between pairs and closes the worker. */
19
+ close(): Promise<void>;
20
+ failed(job: Job<ScopeJob> | undefined, err: Error): void;
21
+ error(err: Error): void;
22
+ }
1
23
  /**
2
24
  * The plugin scope queue's consumer: {@link PluginScopeSyncProcessor}. Imported only by the worker
3
25
  * role (`WorkerModule`), so other roles get no processor and no BullMQ worker. It never starts by
@@ -4,10 +4,66 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { Module } from "@nestjs/common";
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { OnWorkerEvent, WorkerHost } from "@nestjs/bullmq";
11
+ import { Inject, Injectable, Module } from "@nestjs/common";
8
12
  import { configuredProcessor } from "../infra/queue/configured-processor.js";
13
+ import { runJob } from "../infra/queue/run-job.js";
14
+ import { KERNEL_LOGGER } from "../infra/tokens.js";
9
15
  import { CatalogModule } from "./catalog.module.js";
10
- import { PLUGIN_SCOPE_CONCURRENCY, PLUGIN_SCOPE_QUEUE, PluginScopeSyncProcessor, } from "./scope-sync.js";
16
+ import { PLUGIN_SCOPE_CONCURRENCY, PLUGIN_SCOPE_QUEUE, PluginScopeSync, SCOPE_BACKSTOP_JOB, SCOPE_ITEM_JOB, SCOPE_ORG_JOB, } from "./scope-sync.js";
17
+ /**
18
+ * The consumer of {@link PLUGIN_SCOPE_QUEUE}: {@link PluginScopeSync} runs the `scope-item`,
19
+ * `scope-item-backstop` and `scope-org` jobs the outbox delivers (any other name fails the job; a
20
+ * backstop whose extension is not deployed yet throws, so BullMQ retries it).
21
+ */
22
+ let PluginScopeSyncProcessor = class PluginScopeSyncProcessor extends WorkerHost {
23
+ sync;
24
+ log;
25
+ stopping = new AbortController();
26
+ constructor(sync, log) {
27
+ super();
28
+ this.sync = sync;
29
+ this.log = log;
30
+ }
31
+ process(job) {
32
+ return runJob(PLUGIN_SCOPE_QUEUE, job, () => {
33
+ if (![SCOPE_ITEM_JOB, SCOPE_BACKSTOP_JOB, SCOPE_ORG_JOB].includes(job.name)) {
34
+ throw new Error(`unknown plugin scope job ${job.name}`);
35
+ }
36
+ return this.sync.process(job.data, this.stopping.signal);
37
+ });
38
+ }
39
+ /** Starts consuming (worker role, after the extensions loaded; built with `autorun: false`). */
40
+ start() {
41
+ void this.worker.run().catch((err) => this.error(err));
42
+ }
43
+ /** Stops between pairs and closes the worker. */
44
+ async close() {
45
+ this.stopping.abort();
46
+ await this.worker.close();
47
+ }
48
+ failed(job, err) {
49
+ this.log.error("plugin scope job failed", { name: job?.name, jobId: job?.id, err });
50
+ }
51
+ error(err) {
52
+ this.log.error("plugin scope worker error", { err });
53
+ }
54
+ };
55
+ __decorate([
56
+ OnWorkerEvent("failed")
57
+ ], PluginScopeSyncProcessor.prototype, "failed", null);
58
+ __decorate([
59
+ OnWorkerEvent("error")
60
+ ], PluginScopeSyncProcessor.prototype, "error", null);
61
+ PluginScopeSyncProcessor = __decorate([
62
+ Injectable(),
63
+ __param(0, Inject(PluginScopeSync)),
64
+ __param(1, Inject(KERNEL_LOGGER))
65
+ ], PluginScopeSyncProcessor);
66
+ export { PluginScopeSyncProcessor };
11
67
  /**
12
68
  * The plugin scope queue's consumer: {@link PluginScopeSyncProcessor}. Imported only by the worker
13
69
  * role (`WorkerModule`), so other roles get no processor and no BullMQ worker. It never starts by