@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
@@ -8,7 +8,6 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
10
  import { createHash } from "node:crypto";
11
- import { getQueueToken } from "@nestjs/bullmq";
12
11
  import { Inject, Injectable } from "@nestjs/common";
13
12
  import { sql } from "kysely";
14
13
  import { ApiError } from "../errors.js";
@@ -19,20 +18,21 @@ import { PluginInstallLock } from "../extensions/locks.js";
19
18
  import { audit } from "../identity/audit.js";
20
19
  import { SuperAdminLock } from "../identity/super-admin.js";
21
20
  import { addOrgNotice, itemDisplayName, notifyItemShared } from "../inbox/org-notices.js";
22
- import { afterCommit } from "../infra/database/after-commit.js";
23
21
  import { KernelDb } from "../infra/database/kernel-db.js";
24
22
  import { TransactionHost } from "../infra/database/transaction-host.js";
25
23
  import { Transactional } from "../infra/database/transactional.decorator.js";
24
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
26
25
  import { KERNEL_LOGGER } from "../infra/tokens.js";
27
26
  import { OrgLock } from "../orgs/org-lock.js";
28
- import { MAINTENANCE_QUEUE_NAME } from "../runtime/maintenance-queue.js";
29
27
  import { assertNotPlatformDefault, themesPreview, } from "../themes/theme-usage.js";
30
28
  import { CatalogService } from "./catalog.service.js";
31
29
  import { assertOrgsExist, markVersionApplied } from "./catalog-items.js";
32
30
  import { nameConflictError } from "./component-scope.service.js";
33
31
  import { agentsUsing, COMPONENT_KINDS, componentOffered, componentOn, listedIn, loadComponents, loadScopeState, pluginFactsOf, skillNameClashes, switchOf, themeIdClashes, } from "./component-state.js";
34
- import { ownedPluginCleanupJob } from "./job-ids.js";
32
+ import { oweOwnedPluginCleanup } from "./job-ids.js";
33
+ import { CatalogItemLock } from "./locks.js";
35
34
  import { assertNotAwaiting, assertOwnerListed, assertShareable, assertShareableTarget, itemRemovedError, newlySharedWith, ownerOf, ownersOf, PLATFORM_OWNER, packageOwner, pendingDigest, releaseExtensionClaim, } from "./ownership.js";
35
+ import { PlanStale, PlatformComponents } from "./platform-components.js";
36
36
  import { PluginImport } from "./plugin-import.js";
37
37
  import { PluginInstaller } from "./plugin-install.js";
38
38
  import { PlatformPluginUpdater } from "./plugin-upgrade.js";
@@ -90,10 +90,10 @@ export function conflictsError(conflicts) {
90
90
  }
91
91
  /**
92
92
  * The super admin's plugin scope and deployment-level disable (plugin and component management
93
- * spec §4.1, §5 rules 2-4 and 8), and the import confirm that enqueues the scope sync of the item it
93
+ * spec §4.1, §5 rules 2-4 and 8), and the import confirm that owes the scope sync of the item it
94
94
  * wrote. A change is previewed, then confirmed with the preview's `scopeHash`, recomputed under the
95
95
  * item row lock (a difference -> 409 `plugin.scope_stale`); it bumps `scope_version`, audits
96
- * `catalog.scope_changed` and, after the commit, enqueues the item's scope sync, which installs or
96
+ * `catalog.scope_changed` and owes the item's scope sync in the same transaction, which installs or
97
97
  * uninstalls in every org. Disabling keeps the scope, its org list and the org switches, so
98
98
  * re-enabling (no hash needed) reinstalls where it was on. Builtin items -> 409 `plugin.builtin`.
99
99
  * Stateless, shared by every replica.
@@ -103,6 +103,8 @@ let CatalogScopeService = class CatalogScopeService {
103
103
  adminLock;
104
104
  catalog;
105
105
  pluginImport;
106
+ platform;
107
+ itemLock;
106
108
  installer;
107
109
  extensions;
108
110
  ops;
@@ -111,22 +113,28 @@ let CatalogScopeService = class CatalogScopeService {
111
113
  orgLock;
112
114
  installLock;
113
115
  lifecycle;
114
- maintenance;
116
+ outbox;
115
117
  log;
116
118
  txHost;
117
- constructor(kdb, adminLock, catalog, pluginImport, installer, extensions, ops, sync, updater,
119
+ constructor(kdb, adminLock, catalog, pluginImport,
120
+ /** A confirmed first version's platform components (written in the confirming commit). */
121
+ platform,
122
+ /** The import's item name lock, taken by the confirming commit before the item row. */
123
+ itemLock, installer, extensions, ops, sync, updater,
118
124
  /** The owner org's lock: an org admin withdrawing its pending upload (ruling P3-10). */
119
125
  orgLock,
120
126
  /** The per-name install lock a reject / withdraw releases the name claim under (M1). */
121
127
  installLock,
122
128
  /** This replica's reconcile once a confirmed extension is enabled (before the scope sync). */
123
129
  lifecycle,
124
- /** The removed org plugin's cleanup job, backing the take-out of P3-21 (pre-flight F9). */
125
- maintenance, log, txHost) {
130
+ /** Owes the scope syncs and the removed org plugin's cleanup (P3-21, pre-flight F9). */
131
+ outbox, log, txHost) {
126
132
  this.kdb = kdb;
127
133
  this.adminLock = adminLock;
128
134
  this.catalog = catalog;
129
135
  this.pluginImport = pluginImport;
136
+ this.platform = platform;
137
+ this.itemLock = itemLock;
130
138
  this.installer = installer;
131
139
  this.extensions = extensions;
132
140
  this.ops = ops;
@@ -135,7 +143,7 @@ let CatalogScopeService = class CatalogScopeService {
135
143
  this.orgLock = orgLock;
136
144
  this.installLock = installLock;
137
145
  this.lifecycle = lifecycle;
138
- this.maintenance = maintenance;
146
+ this.outbox = outbox;
139
147
  this.log = log;
140
148
  this.txHost = txHost;
141
149
  }
@@ -144,13 +152,13 @@ let CatalogScopeService = class CatalogScopeService {
144
152
  }
145
153
  /**
146
154
  * Confirms a staged import ({@link PluginImport.confirm}: installs and enables the extension,
147
- * writes the new item's scope), then enqueues the item's scope sync when its scope turns it on
155
+ * writes the new item's scope), then owes the item's scope sync when its scope turns it on
148
156
  * by itself (`all` or `named`; an `optional` item is on only where an org admin switches it on).
149
157
  */
150
158
  async confirmImport(actor, stagingId, input, opts = {}) {
151
159
  const r = await this.pluginImport.confirm(actor, stagingId, input, opts);
152
160
  if (r.item.scope !== "optional")
153
- await this.enqueue(r.item.id);
161
+ await this.oweSync(r.item.id);
154
162
  return r;
155
163
  }
156
164
  /**
@@ -177,6 +185,7 @@ let CatalogScopeService = class CatalogScopeService {
177
185
  "owner_org_id",
178
186
  "awaiting_confirmation",
179
187
  "pending_version",
188
+ "scope",
180
189
  ])
181
190
  .where("id", "=", itemId)
182
191
  .where("deleted_at", "is", null)
@@ -223,10 +232,15 @@ let CatalogScopeService = class CatalogScopeService {
223
232
  if (!disabled || !(e instanceof ApiError && e.code === "extension.not_pending"))
224
233
  throw e;
225
234
  }
235
+ // The permissions are confirmed (a stale hash refused above): the backstop before the enable
236
+ // (ruling R24), so a crash before the post-deploy add below still converges the orgs; only
237
+ // when the item's scope turns it on by itself, as the import's commit
238
+ if (item.scope !== "optional")
239
+ await this.oweBackstop(itemId);
226
240
  // An occupied single-provider point leaves it installed disabled (the admin view says why)
227
241
  if (disabled)
228
242
  await this.ops.enableUnlessOccupied(item.name, actor, opts.actorTokenId);
229
- await this.enqueue(itemId);
243
+ await this.oweSync(itemId);
230
244
  return (await this.catalog.get(itemId)).item;
231
245
  }
232
246
  /**
@@ -236,9 +250,10 @@ let CatalogScopeService = class CatalogScopeService {
236
250
  * pending version (withdrawn, rejected, replaced by one without an extension) -> 409
237
251
  * `extension.not_pending`.
238
252
  *
239
- * A first version: its extension installed, its permissions approved, enabled, then (one
240
- * transaction) the item stops waiting and the owner is told (`upload_confirmed`); after the
241
- * commit its platform components are written and the scope sync enqueued. Each step re-reads
253
+ * A first version: its platform components planned and their files copied (keyed by content),
254
+ * its extension installed, its permissions approved, enabled, then (one transaction) the item
255
+ * stops waiting, its platform component rows are written and the owner is told
256
+ * (`upload_confirmed`); after the commit the scope sync is owed. Each step re-reads
242
257
  * the item under its row lock: removed meanwhile -> 409 `catalog.item_removed`, the pending
243
258
  * version replaced -> `extension.approval_stale`; the extension this confirmation installed is
244
259
  * then taken out again. A new version: the platform update to it
@@ -255,14 +270,13 @@ let CatalogScopeService = class CatalogScopeService {
255
270
  // (the enable does not wait for the reconcile); a sync on a replica that has not loaded it
256
271
  // yet defers and retries (ruling P3-38)
257
272
  await reconcileWithin(this.lifecycle, `confirmed:${item.name}`, CONFIRM_RECONCILE_WAIT_MS, this.log);
258
- await this.pluginImport.writePlatformComponents(item.id, first);
259
- await this.enqueue(item.id);
273
+ await this.oweSync(item.id);
260
274
  }
261
275
  return (await this.catalog.get(item.id)).item;
262
276
  }
263
277
  /**
264
- * {@link confirmOrgUpload} under the item lock; returns the confirmed first version (its
265
- * platform components and scope sync follow after), null for a new version.
278
+ * {@link confirmOrgUpload} under the item lock; returns the confirmed first version (its scope
279
+ * sync follows after), null for a new version.
266
280
  */
267
281
  async confirmLocked(actor, itemId, name, permissionsHash, opts) {
268
282
  // Read again under the lock (the caller's read was outside it)
@@ -358,6 +372,8 @@ let CatalogScopeService = class CatalogScopeService {
358
372
  await this.stillPending(itemId, name, version);
359
373
  let installedHere = false;
360
374
  try {
375
+ // Planned and copied before any transaction; the rows are written by the confirming commit
376
+ const components = await this.platform.prepareSync(itemId, version, { confirming: true });
361
377
  if (install?.sha256 !== sha) {
362
378
  await this.ops.install(name, sha, actor, opts.actorTokenId, {
363
379
  catalogItemId: itemId,
@@ -370,7 +386,7 @@ let CatalogScopeService = class CatalogScopeService {
370
386
  await this.stillPending(itemId, name, version);
371
387
  // An occupied single-provider point leaves it installed disabled (the admin view says why)
372
388
  await this.ops.enableUnlessOccupied(name, actor, opts.actorTokenId);
373
- await this.finishFirstVersion(actor, itemId, version, opts);
389
+ await this.finishFirstVersion(actor, itemId, version, components, opts);
374
390
  }
375
391
  catch (e) {
376
392
  // Taken out only when this confirmation did not win: the item gone or still waiting
@@ -466,47 +482,62 @@ let CatalogScopeService = class CatalogScopeService {
466
482
  /**
467
483
  * An extension a confirmation installed whose item was removed or replaced meanwhile is taken
468
484
  * out again (disabled, uninstalled; P3-21), and the name claim released when the item is gone
469
- * (ruling P3-37). Inline and best effort (a failure is logged); a removed item's cleanup job
470
- * (`owned-plugin-cleanup-<itemId>`, idempotent) is enqueued as well, so a failed take-out is
471
- * retried (pre-flight F9).
485
+ * (ruling P3-37), in one transaction ({@link takeOutIn}). Inline (a failure is logged): when the
486
+ * take-out fails, a removed item's cleanup job (`owned-plugin-cleanup-<itemId>`, idempotent) is
487
+ * owed instead, so the job retries it (pre-flight F9). That owe is the last resort: when it
488
+ * fails too (PG unavailable) nothing retries the take-out, and it is logged as an error.
472
489
  */
473
490
  async takeOut(itemId, name, actor, opts) {
474
491
  try {
475
- await this.ops.disable(name, actor, opts.actorTokenId);
476
- await this.ops.uninstall(name, actor, opts.actorTokenId);
477
- await this.releaseIfGone(itemId, name);
492
+ await this.takeOutIn(itemId, name, actor, opts);
493
+ return;
478
494
  }
479
495
  catch (err) {
480
496
  this.log.error("taking out the extension of a removed org upload failed", { name, err });
481
497
  }
482
- const gone = await this.db
483
- .selectFrom("catalog_items")
484
- .select("id")
485
- .where("id", "=", itemId)
486
- .where("deleted_at", "is not", null)
487
- .executeTakeFirst();
488
- if (gone === undefined)
489
- return;
490
- const job = ownedPluginCleanupJob(itemId);
491
498
  try {
492
- await this.maintenance.add(job.name, job.data, job.opts);
499
+ await this.oweCleanupIfGone(itemId);
493
500
  }
494
501
  catch (err) {
495
- this.log.warn("enqueueing the owned plugin cleanup failed", { itemId, err });
502
+ this.log.error("owing the owned plugin cleanup failed; the take-out is not retried", {
503
+ itemId,
504
+ err,
505
+ });
496
506
  }
497
507
  }
498
- /** Releases the name claim of a removed item (nothing installed under it any more). */
499
- async releaseIfGone(itemId, name) {
508
+ /**
509
+ * {@link takeOut}'s transaction, in the global lock order (ruling P3-50): the super admin lock,
510
+ * the item row, then the extension's install lock (taken by the disable): disabled,
511
+ * uninstalled, and the name claim released when the item is gone.
512
+ */
513
+ async takeOutIn(itemId, name, actor, opts) {
514
+ await this.adminLock.lockAndAuthorize(actor);
500
515
  const row = await this.db
501
516
  .selectFrom("catalog_items")
502
517
  .select(["deleted_at", "package_name"])
503
518
  .where("id", "=", itemId)
504
519
  .forUpdate()
505
520
  .executeTakeFirst();
521
+ await this.ops.disable(name, actor, opts.actorTokenId);
522
+ await this.ops.uninstall(name, actor, opts.actorTokenId);
506
523
  if (row === undefined || row.deleted_at !== null) {
507
524
  await releaseExtensionClaim(this.db, name, row?.package_name ?? null);
508
525
  }
509
526
  }
527
+ /**
528
+ * Owes the cleanup of a removed item; a live item owes nothing (a removal committing later owes
529
+ * its own).
530
+ */
531
+ async oweCleanupIfGone(itemId) {
532
+ const gone = await this.db
533
+ .selectFrom("catalog_items")
534
+ .select("id")
535
+ .where("id", "=", itemId)
536
+ .where("deleted_at", "is not", null)
537
+ .executeTakeFirst();
538
+ if (gone !== undefined)
539
+ await oweOwnedPluginCleanup(this.outbox, itemId);
540
+ }
510
541
  /** The item is still live and pending with `version` (row lock), else 409 (P3-21). */
511
542
  async stillPending(itemId, name, version) {
512
543
  await this.pendingRow(itemId, name, version);
@@ -526,14 +557,20 @@ let CatalogScopeService = class CatalogScopeService {
526
557
  throw pendingStale(name);
527
558
  return row.owner_org_id;
528
559
  }
529
- /** A confirmed first version: the item stops waiting, the owner is told (`upload_confirmed`). */
530
- async finishFirstVersion(actor, itemId, version, opts) {
560
+ /**
561
+ * A confirmed first version: the item stops waiting, its platform component rows are written
562
+ * from `components` (a component changed since they were planned -> 409
563
+ * `extension.approval_stale`: confirm again), the owner is told (`upload_confirmed`). The
564
+ * import's item name lock before the item row, as an import's commit takes them (ruling P3-50).
565
+ */
566
+ async finishFirstVersion(actor, itemId, version, components, opts) {
531
567
  await this.adminLock.lockAndAuthorize(actor);
532
568
  const named = await this.db
533
569
  .selectFrom("catalog_items")
534
570
  .select("name")
535
571
  .where("id", "=", itemId)
536
572
  .executeTakeFirstOrThrow();
573
+ await this.itemLock.lock(named.name);
537
574
  const owner = await this.pendingRow(itemId, named.name, version);
538
575
  await this.db
539
576
  .updateTable("catalog_items")
@@ -549,6 +586,14 @@ let CatalogScopeService = class CatalogScopeService {
549
586
  .where("id", "=", itemId)
550
587
  .execute();
551
588
  await markVersionApplied(this.db, itemId, version);
589
+ try {
590
+ await this.platform.applySyncIn(components);
591
+ }
592
+ catch (e) {
593
+ if (e instanceof PlanStale)
594
+ throw pendingStale(named.name);
595
+ throw e;
596
+ }
552
597
  await addOrgNotice(this.db, owner, "upload_confirmed", {
553
598
  itemId,
554
599
  name: named.name,
@@ -562,6 +607,9 @@ let CatalogScopeService = class CatalogScopeService {
562
607
  target: itemId,
563
608
  detail: { name: named.name, version, ownerOrgId: owner },
564
609
  });
610
+ // The confirmation's commit owes the sync's backstop (ruling R24); the immediate sync follows
611
+ // the commit
612
+ await this.sync.enqueueBackstop(itemId);
565
613
  }
566
614
  /**
567
615
  * The super admin rejects an org upload's pending version (ruling P3-10, `reason` shown to the
@@ -781,7 +829,7 @@ let CatalogScopeService = class CatalogScopeService {
781
829
  ...opts.auditDetail,
782
830
  },
783
831
  });
784
- afterCommit(() => this.enqueue(itemId));
832
+ await this.enqueue(itemId);
785
833
  }
786
834
  async writeDisabled(actor, itemId, disabled, hash, opts) {
787
835
  await this.adminLock.lockAndAuthorize(actor);
@@ -817,13 +865,13 @@ let CatalogScopeService = class CatalogScopeService {
817
865
  ...opts.auditDetail,
818
866
  },
819
867
  });
820
- afterCommit(() => this.enqueue(itemId));
868
+ await this.enqueue(itemId);
821
869
  }
822
870
  /**
823
871
  * Writes an org-owned plugin's 可共享 flag and copies it to its component rows (ruling P3-2).
824
872
  * Turning it off narrows an `optional` / `all` plugin, and each component with its own such
825
- * scope, to the owner plus the orgs where it is on now (ruling P3-25), and enqueues the item's
826
- * scope sync after the commit. The caller authorizes (the owner org's admin, Task 7). A platform
873
+ * scope, to the owner plus the orgs where it is on now (ruling P3-25), and owes the item's
874
+ * scope sync with it. The caller authorizes (the owner org's admin, Task 7). A platform
827
875
  * item (no owner) or a missing one -> 404.
828
876
  */
829
877
  async setShareable(itemId, shareable, actor = null) {
@@ -865,13 +913,13 @@ let CatalogScopeService = class CatalogScopeService {
865
913
  }
866
914
  }
867
915
  if (narrowed)
868
- afterCommit(() => this.enqueue(itemId));
916
+ await this.enqueue(itemId);
869
917
  }
870
918
  /**
871
919
  * Adds one org to a `named` item's list in the caller's transaction (an approved `access`
872
920
  * request, spec §5.1 rule 2): nobody loses access, so no preview. The item row is locked; it
873
921
  * must be live and not builtin (404 / `plugin.builtin`). Bumps `scope_version`, audits
874
- * `catalog.scope_changed` and enqueues the item's scope sync after the commit. Returns whether
922
+ * `catalog.scope_changed` and owes the item's scope sync in that transaction. Returns whether
875
923
  * the list changed (false: not `named`, or listed already).
876
924
  */
877
925
  async addOrg(actor, itemId, orgId, opts = {}) {
@@ -879,24 +927,34 @@ let CatalogScopeService = class CatalogScopeService {
879
927
  assertNotAwaiting(item);
880
928
  const changed = await addScopeOrg(this.db, actor, item, orgId, opts);
881
929
  if (changed)
882
- afterCommit(() => this.enqueue(itemId));
930
+ await this.enqueue(itemId);
883
931
  return changed;
884
932
  }
885
933
  /**
886
- * Enqueues the item's scope sync after a commit made elsewhere (an org upload's confirm,
887
- * `OrgPluginUploads`); a failure only warns.
934
+ * Owes the item's scope sync in the caller's transaction (an ownership transfer, an org upload's
935
+ * confirm in `OrgPluginUploads`).
888
936
  */
889
937
  async enqueueSync(itemId) {
890
938
  await this.enqueue(itemId);
891
939
  }
892
- /** Enqueues the item's scope sync; a failure only warns (the next change or org sync converges). */
940
+ /** Owes the item's scope sync in the transaction that changes its scope (outbox spec §8.1). */
893
941
  async enqueue(itemId) {
894
- try {
895
- await this.sync.enqueueItem(itemId);
896
- }
897
- catch (err) {
898
- this.log.warn("enqueueing the plugin scope sync failed", { itemId, err });
899
- }
942
+ await this.sync.enqueueItem(itemId);
943
+ }
944
+ /**
945
+ * Owes the item's scope sync in a transaction of its own, after a confirmation whose last write
946
+ * (the extension's deploy, or the platform components) committed outside the scope service: the
947
+ * sync must not run before the extension it installs is enabled.
948
+ */
949
+ async oweSync(itemId) {
950
+ await this.enqueue(itemId);
951
+ }
952
+ /**
953
+ * Owes the delayed backstop of the item's sync in a transaction of its own (ruling R24): a
954
+ * permissions confirm writes nothing of the scope service's before its enable.
955
+ */
956
+ async oweBackstop(itemId) {
957
+ await this.sync.enqueueBackstop(itemId);
900
958
  }
901
959
  /**
902
960
  * 409 `theme.platform_default` when a live theme of the item is the platform default and `after`
@@ -1190,7 +1248,10 @@ __decorate([
1190
1248
  ], CatalogScopeService.prototype, "restorePending", null);
1191
1249
  __decorate([
1192
1250
  Transactional()
1193
- ], CatalogScopeService.prototype, "releaseIfGone", null);
1251
+ ], CatalogScopeService.prototype, "takeOutIn", null);
1252
+ __decorate([
1253
+ Transactional()
1254
+ ], CatalogScopeService.prototype, "oweCleanupIfGone", null);
1194
1255
  __decorate([
1195
1256
  Transactional()
1196
1257
  ], CatalogScopeService.prototype, "stillPending", null);
@@ -1215,22 +1276,36 @@ __decorate([
1215
1276
  __decorate([
1216
1277
  Transactional({ propagation: "mandatory" })
1217
1278
  ], CatalogScopeService.prototype, "addOrg", null);
1279
+ __decorate([
1280
+ Transactional({ propagation: "mandatory" })
1281
+ ], CatalogScopeService.prototype, "enqueueSync", null);
1282
+ __decorate([
1283
+ Transactional({ propagation: "mandatory" })
1284
+ ], CatalogScopeService.prototype, "enqueue", null);
1285
+ __decorate([
1286
+ Transactional()
1287
+ ], CatalogScopeService.prototype, "oweSync", null);
1288
+ __decorate([
1289
+ Transactional()
1290
+ ], CatalogScopeService.prototype, "oweBackstop", null);
1218
1291
  CatalogScopeService = __decorate([
1219
1292
  Injectable(),
1220
1293
  __param(0, Inject(KernelDb)),
1221
1294
  __param(1, Inject(SuperAdminLock)),
1222
1295
  __param(2, Inject(CatalogService)),
1223
1296
  __param(3, Inject(PluginImport)),
1224
- __param(4, Inject(PluginInstaller)),
1225
- __param(5, Inject(ExtensionAdmin)),
1226
- __param(6, Inject(ExtensionAdminOps)),
1227
- __param(7, Inject(PluginScopeSync)),
1228
- __param(8, Inject(PlatformPluginUpdater)),
1229
- __param(9, Inject(OrgLock)),
1230
- __param(10, Inject(PluginInstallLock)),
1231
- __param(11, Inject(ExtensionLifecycle)),
1232
- __param(12, Inject(getQueueToken(MAINTENANCE_QUEUE_NAME))),
1233
- __param(13, Inject(KERNEL_LOGGER)),
1234
- __param(14, Inject(TransactionHost))
1297
+ __param(4, Inject(PlatformComponents)),
1298
+ __param(5, Inject(CatalogItemLock)),
1299
+ __param(6, Inject(PluginInstaller)),
1300
+ __param(7, Inject(ExtensionAdmin)),
1301
+ __param(8, Inject(ExtensionAdminOps)),
1302
+ __param(9, Inject(PluginScopeSync)),
1303
+ __param(10, Inject(PlatformPluginUpdater)),
1304
+ __param(11, Inject(OrgLock)),
1305
+ __param(12, Inject(PluginInstallLock)),
1306
+ __param(13, Inject(ExtensionLifecycle)),
1307
+ __param(14, Inject(OutboxService)),
1308
+ __param(15, Inject(KERNEL_LOGGER)),
1309
+ __param(16, Inject(TransactionHost))
1235
1310
  ], CatalogScopeService);
1236
1311
  export { CatalogScopeService };
@@ -54,10 +54,11 @@ CatalogModule = __decorate([
54
54
  StorageModule,
55
55
  SkillsModule,
56
56
  ToolsModule,
57
- // the producer side of the plugin scope sync (`PluginScopeWorkersModule` consumes it)
57
+ // the plugin scope sync's queue: the outbox dispatcher adds the owed jobs through it
58
+ // (`PluginScopeWorkersModule` consumes it)
58
59
  PLUGIN_SCOPE_QUEUE_MODULE,
59
- // a removed org plugin's cleanup job (`OwnedItems`, `CatalogScopeService`; consumed by the
60
- // worker role's `MaintenanceProcessor`)
60
+ // the queue of a removed org plugin's owed cleanup job (`OwnedItems`, `CatalogScopeService`;
61
+ // consumed by the worker role's `MaintenanceProcessor`)
61
62
  MAINTENANCE_QUEUE_MODULE,
62
63
  ],
63
64
  providers: [
@@ -7,6 +7,7 @@ import { SuperAdminLock } from "../identity/super-admin.js";
7
7
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
8
8
  import { KernelDb } from "../infra/database/kernel-db.js";
9
9
  import { TransactionHost } from "../infra/database/transaction-host.js";
10
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
10
11
  import type { TranslationReport } from "./claude/report.js";
11
12
  import { MarketplacesService } from "./marketplaces.service.js";
12
13
  import { type Owner } from "./ownership.js";
@@ -242,6 +243,8 @@ export declare class CatalogService {
242
243
  private readonly marketplaces;
243
244
  /** Serializes a claim release with an install of that name still committing (F10). */
244
245
  private readonly installLock;
246
+ /** A removed org-owned plugin's cleanup job, owed by the removal (transactional outbox §8.1). */
247
+ private readonly outbox;
245
248
  readonly txHost: TransactionHost;
246
249
  constructor(kdb: KernelDb,
247
250
  /** The super admin lock (identity/super-admin.ts): joins the caller's transaction. */
@@ -253,7 +256,9 @@ export declare class CatalogService {
253
256
  /** The token of a custom marketplace (sent only to its URL's origin). */
254
257
  marketplaces: MarketplacesService,
255
258
  /** Serializes a claim release with an install of that name still committing (F10). */
256
- installLock: PluginInstallLock, txHost: TransactionHost);
259
+ installLock: PluginInstallLock,
260
+ /** A removed org-owned plugin's cleanup job, owed by the removal (transactional outbox §8.1). */
261
+ outbox: Pick<OutboxService, "add">, txHost: TransactionHost);
257
262
  /** The kernel-schema handle of the current context (the transaction, else the pool). */
258
263
  private get db();
259
264
  /** Downloads a marketplace archive source, verified against its integrity. */
@@ -335,7 +340,8 @@ export declare class CatalogService {
335
340
  * still enabled on the deployment -> 409 `catalog.extension_enabled` (details
336
341
  * `[{ extension, displayName }]`): disable it on the Extensions page first, as an uninstall
337
342
  * requires. A disabled extension keeps its install record (the super admin uninstalls it on the
338
- * Extensions page); an org-owned item's extension is taken out by `OwnedPluginCleanup`.
343
+ * Extensions page); an org-owned item's extension is taken out by `OwnedPluginCleanup`, the job
344
+ * the removal owes ({@link removeIn}).
339
345
  */
340
346
  remove(actor: string, itemId: string, opts?: ActorOpts): Promise<void>;
341
347
  /**
@@ -356,8 +362,9 @@ export declare class CatalogService {
356
362
  * `OwnedItems.transferOrRemove`, share it): `deleted_at` set, a pending version cleared, the
357
363
  * extension name claim of a first upload still waiting released (ruling P3-37, F10), its themes
358
364
  * soft-deleted (refused with 409 `theme.platform_default` while one is the platform default),
359
- * pending requests cancelled, audit `catalog.removed` (`actor` null: the kernel). Returns the
360
- * removed item's name, or null when it is gone already.
365
+ * pending requests cancelled, an org-owned item's cleanup job owed
366
+ * (`owned-plugin-cleanup-<itemId>`), audit `catalog.removed` (`actor` null: the kernel). Returns
367
+ * the removed item's name, or null when it is gone already.
361
368
  */
362
369
  removeIn(actor: string | null, itemId: string, opts?: ActorOpts & {
363
370
  reason?: string;
@@ -20,10 +20,12 @@ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
20
20
  import { KernelDb } from "../infra/database/kernel-db.js";
21
21
  import { TransactionHost } from "../infra/database/transaction-host.js";
22
22
  import { Transactional } from "../infra/database/transactional.decorator.js";
23
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
23
24
  import { sameRepository } from "../packages/provenance.js";
24
25
  import { assertNotPlatformDefault } from "../themes/theme-usage.js";
25
26
  import { countInstalls, inAuthorOrder, itemConfirmations, itemScopeFacts, itemView, marketplaceLatest, versionUsable, } from "./catalog-items.js";
26
27
  import { expandGitUrl, GitSourceError, gitErrorDetails, normalGitPath, readGitTreeAndOids, resolveGitCommit, } from "./git-source.js";
28
+ import { oweOwnedPluginCleanup } from "./job-ids.js";
27
29
  import { fetchMarketplaceArchive, marketplaceNetPolicy, marketplaceUrlProblem, } from "./marketplace-fetch.js";
28
30
  import { assertEntryNotDeprecated, entryCompatible, entrySourceHost, HOST_VERSIONS, hostAllowed, loadMarketplaceEntry, MarketplacesService, pluginNameFrom, } from "./marketplaces.service.js";
29
31
  import { releaseExtensionClaim } from "./ownership.js";
@@ -95,6 +97,7 @@ let CatalogService = class CatalogService {
95
97
  pluginImport;
96
98
  marketplaces;
97
99
  installLock;
100
+ outbox;
98
101
  txHost;
99
102
  constructor(kdb,
100
103
  /** The super admin lock (identity/super-admin.ts): joins the caller's transaction. */
@@ -106,7 +109,9 @@ let CatalogService = class CatalogService {
106
109
  /** The token of a custom marketplace (sent only to its URL's origin). */
107
110
  marketplaces,
108
111
  /** Serializes a claim release with an install of that name still committing (F10). */
109
- installLock, txHost) {
112
+ installLock,
113
+ /** A removed org-owned plugin's cleanup job, owed by the removal (transactional outbox §8.1). */
114
+ outbox, txHost) {
110
115
  this.kdb = kdb;
111
116
  this.adminLock = adminLock;
112
117
  this.config = config;
@@ -115,6 +120,7 @@ let CatalogService = class CatalogService {
115
120
  this.pluginImport = pluginImport;
116
121
  this.marketplaces = marketplaces;
117
122
  this.installLock = installLock;
123
+ this.outbox = outbox;
118
124
  this.txHost = txHost;
119
125
  }
120
126
  /** The kernel-schema handle of the current context (the transaction, else the pool). */
@@ -469,7 +475,8 @@ let CatalogService = class CatalogService {
469
475
  * still enabled on the deployment -> 409 `catalog.extension_enabled` (details
470
476
  * `[{ extension, displayName }]`): disable it on the Extensions page first, as an uninstall
471
477
  * requires. A disabled extension keeps its install record (the super admin uninstalls it on the
472
- * Extensions page); an org-owned item's extension is taken out by `OwnedPluginCleanup`.
478
+ * Extensions page); an org-owned item's extension is taken out by `OwnedPluginCleanup`, the job
479
+ * the removal owes ({@link removeIn}).
473
480
  */
474
481
  async remove(actor, itemId, opts = {}) {
475
482
  if (!UUID_RE.test(itemId))
@@ -533,8 +540,9 @@ let CatalogService = class CatalogService {
533
540
  * `OwnedItems.transferOrRemove`, share it): `deleted_at` set, a pending version cleared, the
534
541
  * extension name claim of a first upload still waiting released (ruling P3-37, F10), its themes
535
542
  * soft-deleted (refused with 409 `theme.platform_default` while one is the platform default),
536
- * pending requests cancelled, audit `catalog.removed` (`actor` null: the kernel). Returns the
537
- * removed item's name, or null when it is gone already.
543
+ * pending requests cancelled, an org-owned item's cleanup job owed
544
+ * (`owned-plugin-cleanup-<itemId>`), audit `catalog.removed` (`actor` null: the kernel). Returns
545
+ * the removed item's name, or null when it is gone already.
538
546
  */
539
547
  async removeIn(actor, itemId, opts = {}) {
540
548
  const t = this.db;
@@ -572,6 +580,13 @@ let CatalogService = class CatalogService {
572
580
  await this.installLock.lock(row.name);
573
581
  await releaseExtensionClaim(t, row.name, row.package_name);
574
582
  }
583
+ // An org-owned plugin: its extension is taken out (and its name claim released) by the cleanup
584
+ // job the removal owes, whoever removes it (the super admin, its owner, an org deletion). Also
585
+ // while its first upload still waits: a confirmation may have committed its install and not
586
+ // reached its take-out yet (the job is idempotent; nothing installed changes nothing)
587
+ if (row.owner_org_id !== null) {
588
+ await oweOwnedPluginCleanup(this.outbox, itemId);
589
+ }
575
590
  // Its themes go with it (themes spec §6): nothing refers to a theme row (a theme picked by an
576
591
  // org or a person is an id that falls back once no live theme has it)
577
592
  await t
@@ -615,6 +630,7 @@ CatalogService = __decorate([
615
630
  __param(5, Inject(PluginImport)),
616
631
  __param(6, Inject(MarketplacesService)),
617
632
  __param(7, Inject(PluginInstallLock)),
618
- __param(8, Inject(TransactionHost))
633
+ __param(8, Inject(OutboxService)),
634
+ __param(9, Inject(TransactionHost))
619
635
  ], CatalogService);
620
636
  export { CatalogService };
@@ -11,9 +11,10 @@ export interface CatalogCli extends ExtensionCli {
11
11
  /** Plugin import (prepare / confirm). The CLI acts without an account (actor `null`). */
12
12
  imports: PluginImport;
13
13
  /**
14
- * After an import's confirm: enqueues the item's plugin scope sync, as the admin console's
15
- * confirm does, when its scope turns it on by itself (`all` or `named`). A failure only warns
16
- * (the next worker start enqueues every item); returns whether it was enqueued.
14
+ * After an import's confirm (and its deploy): owes the item's plugin scope sync in the outbox, as
15
+ * the admin console's confirm does, when its scope turns it on by itself (`all` or `named`); a
16
+ * worker's relay delivers it. A failure only warns (the confirm's commit owed a backstop that
17
+ * converges the orgs a few minutes later); returns whether it was owed.
17
18
  */
18
19
  enqueueScopeSync(item: {
19
20
  id: string;