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

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 (206) hide show
  1. package/bootstrap.json +25 -25
  2. package/dist/admin/admin.module.d.ts +2 -1
  3. package/dist/admin/admin.module.js +7 -3
  4. package/dist/admin/outbox-dead.controller.d.ts +23 -0
  5. package/dist/admin/outbox-dead.controller.js +62 -0
  6. package/dist/admin/outbox-dead.schemas.d.ts +8 -0
  7. package/dist/admin/outbox-dead.schemas.js +18 -0
  8. package/dist/admin/outbox-dead.service.d.ts +66 -0
  9. package/dist/admin/outbox-dead.service.js +179 -0
  10. package/dist/admin/overview.service.d.ts +40 -5
  11. package/dist/admin/overview.service.js +41 -7
  12. package/dist/agents/from-template.js +1 -1
  13. package/dist/background/finish.d.ts +5 -2
  14. package/dist/background/finish.js +14 -3
  15. package/dist/background/launch.d.ts +2 -1
  16. package/dist/background/launch.js +5 -2
  17. package/dist/background/store.d.ts +8 -0
  18. package/dist/background/store.js +24 -0
  19. package/dist/catalog/admin-catalog.controller.d.ts +21 -2
  20. package/dist/catalog/admin-catalog.controller.js +45 -6
  21. package/dist/catalog/admin-catalog.schemas.d.ts +5 -0
  22. package/dist/catalog/admin-catalog.schemas.js +8 -0
  23. package/dist/catalog/catalog.module.d.ts +2 -1
  24. package/dist/catalog/catalog.module.js +5 -1
  25. package/dist/catalog/marketplaces.service.d.ts +1 -1
  26. package/dist/catalog/platform-import-update.d.ts +50 -0
  27. package/dist/catalog/platform-import-update.js +116 -0
  28. package/dist/catalog/plugin-import.d.ts +36 -1
  29. package/dist/catalog/plugin-import.js +23 -1
  30. package/dist/catalog/plugin-upgrade.d.ts +18 -0
  31. package/dist/catalog/plugin-upgrade.js +19 -0
  32. package/dist/channels/channels.module.d.ts +5 -2
  33. package/dist/channels/channels.module.js +9 -2
  34. package/dist/channels/deliver-job.d.ts +25 -0
  35. package/dist/channels/deliver-job.js +25 -0
  36. package/dist/channels/inbound.d.ts +1 -1
  37. package/dist/channels/inbound.js +2 -15
  38. package/dist/channels/jobs.d.ts +28 -18
  39. package/dist/channels/jobs.js +63 -26
  40. package/dist/config/schema.d.ts +0 -3
  41. package/dist/config/schema.js +11 -20
  42. package/dist/conversations/agent-tree.d.ts +16 -0
  43. package/dist/conversations/agent-tree.js +36 -0
  44. package/dist/conversations/conversations.service.d.ts +44 -14
  45. package/dist/conversations/conversations.service.js +52 -38
  46. package/dist/conversations/event-log.d.ts +7 -10
  47. package/dist/conversations/event-log.js +24 -15
  48. package/dist/conversations/events.js +2 -1
  49. package/dist/conversations/queue.service.d.ts +8 -3
  50. package/dist/conversations/queue.service.js +37 -13
  51. package/dist/conversations/titles.d.ts +21 -8
  52. package/dist/conversations/titles.js +42 -15
  53. package/dist/db/kernel-schema.d.ts +26 -8
  54. package/dist/devices/devices.service.d.ts +12 -3
  55. package/dist/devices/devices.service.js +18 -3
  56. package/dist/errors.d.ts +11 -0
  57. package/dist/errors.js +11 -0
  58. package/dist/evals/run-replay.js +14 -8
  59. package/dist/extensions/extensions.module.js +3 -0
  60. package/dist/extensions/lifecycle.d.ts +6 -1
  61. package/dist/extensions/lifecycle.js +9 -2
  62. package/dist/extensions/usage-subscribers.d.ts +43 -0
  63. package/dist/extensions/usage-subscribers.js +108 -0
  64. package/dist/http/api.module.js +2 -0
  65. package/dist/index.d.ts +1 -0
  66. package/dist/index.js +1 -0
  67. package/dist/infra/cli/kernel-cli.module.js +5 -0
  68. package/dist/infra/events/kernel-events.d.ts +9 -35
  69. package/dist/infra/outbox/outbox-relay.module.d.ts +6 -0
  70. package/dist/infra/outbox/outbox-relay.module.js +18 -0
  71. package/dist/infra/outbox/outbox.dispatcher.d.ts +103 -0
  72. package/dist/infra/outbox/outbox.dispatcher.js +310 -0
  73. package/dist/infra/outbox/outbox.module.d.ts +24 -0
  74. package/dist/infra/outbox/outbox.module.js +50 -0
  75. package/dist/infra/outbox/outbox.relay.d.ts +31 -0
  76. package/dist/infra/outbox/outbox.relay.js +76 -0
  77. package/dist/infra/outbox/outbox.service.d.ts +51 -0
  78. package/dist/infra/outbox/outbox.service.js +131 -0
  79. package/dist/infra/outbox/registry.d.ts +120 -0
  80. package/dist/infra/outbox/registry.js +108 -0
  81. package/dist/infra/queue/queue-names.d.ts +8 -0
  82. package/dist/infra/queue/queue-names.js +16 -0
  83. package/dist/kernel.module.js +3 -0
  84. package/dist/local-devices/device-policy.service.d.ts +4 -0
  85. package/dist/local-devices/device-policy.service.js +61 -3
  86. package/dist/local-devices/machines.d.ts +13 -5
  87. package/dist/local-devices/machines.js +45 -7
  88. package/dist/local-devices/policy-summary.d.ts +44 -1
  89. package/dist/local-devices/policy-summary.js +69 -0
  90. package/dist/models/embeddings.d.ts +9 -9
  91. package/dist/models/embeddings.js +11 -18
  92. package/dist/models/models.module.js +3 -0
  93. package/dist/orgs/orgs.service.js +7 -0
  94. package/dist/overrides.d.ts +20 -3
  95. package/dist/overrides.js +10 -2
  96. package/dist/platform/exclusions.js +5 -0
  97. package/dist/platform/ops/admin.ops.js +18 -2
  98. package/dist/platform/ops/catalog-admin.ops.d.ts +3 -1
  99. package/dist/platform/ops/catalog-admin.ops.js +32 -4
  100. package/dist/platform/ops/outbox.ops.d.ts +21 -0
  101. package/dist/platform/ops/outbox.ops.js +125 -0
  102. package/dist/platform/ops/schedules.ops.js +40 -1
  103. package/dist/platform/platform-ops.module.d.ts +2 -1
  104. package/dist/platform/platform-ops.module.js +3 -0
  105. package/dist/platform/route-schemas.js +3 -0
  106. package/dist/roles/worker.module.js +9 -4
  107. package/dist/runtime/langgraph-loop.js +13 -5
  108. package/dist/runtime/limits.d.ts +15 -0
  109. package/dist/runtime/limits.js +25 -0
  110. package/dist/runtime/loop.d.ts +8 -3
  111. package/dist/runtime/loop.js +2 -1
  112. package/dist/runtime/maintenance.d.ts +4 -2
  113. package/dist/runtime/maintenance.js +17 -2
  114. package/dist/runtime/model-call.d.ts +14 -16
  115. package/dist/runtime/model-call.js +14 -24
  116. package/dist/runtime/queue-lost-jobs.d.ts +24 -0
  117. package/dist/runtime/queue-lost-jobs.js +51 -0
  118. package/dist/runtime/run-control.d.ts +42 -5
  119. package/dist/runtime/run-control.js +66 -12
  120. package/dist/runtime/run-finish.d.ts +39 -12
  121. package/dist/runtime/run-finish.js +97 -14
  122. package/dist/runtime/run-jobs.d.ts +95 -13
  123. package/dist/runtime/run-jobs.js +199 -28
  124. package/dist/runtime/run-orphan-check.job.d.ts +142 -0
  125. package/dist/runtime/run-orphan-check.job.js +339 -0
  126. package/dist/runtime/run-processors.d.ts +16 -9
  127. package/dist/runtime/run-processors.js +1 -1
  128. package/dist/runtime/run-queue.d.ts +64 -55
  129. package/dist/runtime/run-queue.js +147 -284
  130. package/dist/runtime/run-steps.d.ts +1 -2
  131. package/dist/runtime/run-steps.js +8 -11
  132. package/dist/runtime/run-worker.d.ts +6 -17
  133. package/dist/runtime/run-worker.js +32 -34
  134. package/dist/runtime/runtime.module.d.ts +2 -3
  135. package/dist/runtime/runtime.module.js +5 -6
  136. package/dist/runtime/step-log.d.ts +16 -4
  137. package/dist/runtime/step-log.js +26 -6
  138. package/dist/runtime/stream.controller.d.ts +2 -4
  139. package/dist/runtime/stream.controller.js +5 -11
  140. package/dist/runtime/subagents/child.js +1 -1
  141. package/dist/runtime/subagents/definition.d.ts +2 -1
  142. package/dist/runtime/subagents/definition.js +4 -2
  143. package/dist/runtime/subagents/deliveries.d.ts +6 -3
  144. package/dist/runtime/subagents/deliveries.js +16 -7
  145. package/dist/runtime/subagents/dispatch.d.ts +12 -12
  146. package/dist/runtime/subagents/dispatch.js +13 -38
  147. package/dist/runtime/subagents/messages.d.ts +5 -1
  148. package/dist/runtime/subagents/messages.js +11 -3
  149. package/dist/runtime/subagents/skill-fork.d.ts +3 -3
  150. package/dist/runtime/subagents/skill-fork.js +5 -5
  151. package/dist/runtime/subagents/subagents.service.js +1 -1
  152. package/dist/runtime/subagents/tree-jobs.d.ts +61 -0
  153. package/dist/runtime/subagents/tree-jobs.js +86 -0
  154. package/dist/runtime/usage-report-jobs.d.ts +23 -48
  155. package/dist/runtime/usage-report-jobs.js +67 -119
  156. package/dist/runtime/usage-report.d.ts +28 -77
  157. package/dist/runtime/usage-report.js +35 -213
  158. package/dist/runtime/waits.d.ts +48 -198
  159. package/dist/runtime/waits.js +103 -805
  160. package/dist/sandbox/sandbox.service.d.ts +17 -4
  161. package/dist/sandbox/sandbox.service.js +55 -14
  162. package/dist/schedules/calendar.d.ts +134 -0
  163. package/dist/schedules/calendar.js +410 -0
  164. package/dist/schedules/fire.d.ts +1 -3
  165. package/dist/schedules/fire.js +5 -10
  166. package/dist/schedules/run-tracker.d.ts +0 -5
  167. package/dist/schedules/run-tracker.js +0 -13
  168. package/dist/schedules/scan.job.js +15 -20
  169. package/dist/schedules/schedule-math.d.ts +42 -0
  170. package/dist/schedules/schedule-math.js +52 -0
  171. package/dist/schedules/scheduled-tasks.service.d.ts +16 -0
  172. package/dist/schedules/scheduled-tasks.service.js +53 -0
  173. package/dist/schedules/schedules.controller.d.ts +5 -1
  174. package/dist/schedules/schedules.controller.js +15 -1
  175. package/dist/schedules/schedules.schemas.d.ts +35 -0
  176. package/dist/schedules/schedules.schemas.js +27 -0
  177. package/dist/schedules/store.d.ts +5 -0
  178. package/dist/schedules/store.js +20 -0
  179. package/dist/tools/approvals.service.d.ts +26 -7
  180. package/dist/tools/approvals.service.js +88 -10
  181. package/dist/tools/builtin-names.d.ts +5 -3
  182. package/dist/tools/builtin-names.js +12 -4
  183. package/dist/tools/builtins/builtin-tools.d.ts +1 -2
  184. package/dist/tools/builtins/builtin-tools.js +21 -17
  185. package/dist/tools/builtins/sandbox/bash.js +9 -3
  186. package/dist/tools/builtins/sandbox/common.d.ts +18 -1
  187. package/dist/tools/builtins/sandbox/common.js +14 -1
  188. package/dist/tools/builtins/sandbox/index.d.ts +2 -1
  189. package/dist/tools/builtins/sandbox/index.js +4 -1
  190. package/dist/tools/builtins/sandbox/task-list.d.ts +12 -0
  191. package/dist/tools/builtins/sandbox/task-list.js +130 -0
  192. package/dist/tools/builtins/sandbox/task-output.d.ts +8 -1
  193. package/dist/tools/builtins/sandbox/task-output.js +21 -3
  194. package/dist/tools/builtins/web/extract.d.ts +2 -4
  195. package/dist/tools/builtins/web/extract.js +2 -5
  196. package/dist/tools/pipeline.d.ts +1 -1
  197. package/dist/tools/pipeline.js +1 -1
  198. package/dist/tools/questions.service.d.ts +15 -6
  199. package/dist/tools/questions.service.js +20 -8
  200. package/dist/tools/registry.d.ts +4 -2
  201. package/dist/tools/registry.js +4 -3
  202. package/dist/tools/tools.module.js +4 -3
  203. package/package.json +3 -3
  204. package/src/migrations/0118_device_policy_ops.sql +6 -0
  205. package/src/migrations/0119_outbox.sql +22 -0
  206. package/src/migrations/0120_drop_usage_report_sweep.sql +7 -0
@@ -0,0 +1,116 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
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
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { Inject, Injectable } from "@nestjs/common";
11
+ import { ApiError } from "../errors.js";
12
+ import { KERNEL_LOGGER } from "../infra/tokens.js";
13
+ import { PluginImport } from "./plugin-import.js";
14
+ import { PlatformPluginUpdater, } from "./plugin-upgrade.js";
15
+ const notInstalled = (name) => new ApiError("plugin.upgrade_target_invalid", `Plugin ${name} is not a platform plugin of this deployment yet; install it with the import review instead`, [{ reason: "not_installed" }]);
16
+ /**
17
+ * The platform's update of an installed plugin to a version staged for import (the one wizard of
18
+ * the admin plugin pages, maintainer decision 2026-11-13): the import review (prepared as any
19
+ * import, `PluginImport.prepare`) and the platform update preview of the staged version
20
+ * ({@link preview}: `PlatformPluginUpdater.previewStaged`, the target read from the staging) are
21
+ * confirmed together ({@link confirm} with the review's `reviewHash` and the preview's `diffHash`).
22
+ * Nothing is written before that confirm: cancelling the wizard leaves only the staging row, which
23
+ * expires like any staged import.
24
+ *
25
+ * The confirm holds the item's update lock (`PlatformPluginUpdater.withItemLock`, one per item
26
+ * across replicas: another update -> 409 `plugin.install_in_progress`), recomputes the preview
27
+ * (another `diffHash` -> 409 `plugin.upgrade_stale`, nothing written), commits the import without
28
+ * deploying its extension (the permission snapshot is written as the review approved it), then
29
+ * runs the platform update, which moves the extension with its own rollback and writes the
30
+ * components in one transaction. When that update fails, the platform stays on its version, the
31
+ * extension where it was and its previous approval is put back; the imported version stays in the
32
+ * catalog (its 版本 tab, and 更新到 retries the update with the plain update preview): 409
33
+ * `plugin.imported_not_updated` says so, with the cause in the details. No in-process state.
34
+ */
35
+ let PlatformImportUpdate = class PlatformImportUpdate {
36
+ pluginImport;
37
+ updater;
38
+ log;
39
+ constructor(pluginImport, updater, log) {
40
+ this.pluginImport = pluginImport;
41
+ this.updater = updater;
42
+ this.log = log;
43
+ }
44
+ /**
45
+ * The platform update the staged version would apply (writes nothing). The staging must be a
46
+ * platform staging (else 404 `plugin.staging_not_found`) of a new version of a live
47
+ * platform-owned plugin (else 409 `plugin.upgrade_target_invalid`, reason `not_installed`); the
48
+ * updater's refusals apply as for any update preview. `needsApproval` is false: the confirm
49
+ * approves the permissions the review shows.
50
+ */
51
+ async preview(stagingId) {
52
+ const st = await this.target(stagingId);
53
+ return this.previewOf(st);
54
+ }
55
+ /** Confirms the import and the update together (see the class comment). */
56
+ async confirm(actor, stagingId, input, opts = {}) {
57
+ const st = await this.target(stagingId);
58
+ return this.updater.withItemLock(st.itemId, async () => {
59
+ const preview = await this.previewOf(st);
60
+ if (preview.diffHash !== input.diffHash) {
61
+ throw new ApiError("plugin.upgrade_stale", `What updating plugin ${preview.name} to ${st.version} does changed since the preview; preview it again`);
62
+ }
63
+ // The review hash is checked by the import's confirm (409 plugin.review_stale, nothing
64
+ // written)
65
+ const imported = await this.pluginImport.confirm(actor, stagingId, { reviewHash: input.reviewHash, deploy: false }, opts);
66
+ try {
67
+ const update = await this.updater.updateHeld(actor, st.itemId, { version: st.version, diffHash: input.diffHash }, opts);
68
+ return { item: await this.pluginImport.view(st.itemId), update };
69
+ }
70
+ catch (e) {
71
+ let restored = imported.approval === undefined;
72
+ if (imported.approval !== undefined) {
73
+ try {
74
+ restored = await this.pluginImport.restoreApproval(actor, imported.approval, opts);
75
+ }
76
+ catch (err) {
77
+ this.log.error("cannot restore an extension's approval after a failed update", {
78
+ itemId: st.itemId,
79
+ err,
80
+ });
81
+ }
82
+ }
83
+ const cause = e instanceof ApiError ? e : undefined;
84
+ throw new ApiError("plugin.imported_not_updated", `Version ${st.version} of plugin ${preview.name} was imported, but updating the platform to it failed (${cause?.code ?? "internal"}); the platform stays on ${preview.from ?? "(none)"}. Update to ${st.version} from the plugin's page to retry`, [
85
+ {
86
+ version: st.version,
87
+ from: preview.from,
88
+ code: cause?.code ?? "internal",
89
+ ...(cause !== undefined && { message: cause.message }),
90
+ ...(cause?.details !== undefined && { causeDetails: cause.details }),
91
+ approvalRestored: restored,
92
+ },
93
+ ]);
94
+ }
95
+ });
96
+ }
97
+ /** The staged target, which must be a new version of a live platform-owned plugin. */
98
+ async target(stagingId) {
99
+ const st = await this.pluginImport.stagedTarget(stagingId);
100
+ if (!st.existing) {
101
+ throw notInstalled((await this.pluginImport.staged(stagingId)).review.name);
102
+ }
103
+ return st;
104
+ }
105
+ async previewOf(st) {
106
+ const p = await this.updater.previewStaged(st.itemId, st.version, st.target);
107
+ return p.extension === null ? p : { ...p, extension: { ...p.extension, needsApproval: false } };
108
+ }
109
+ };
110
+ PlatformImportUpdate = __decorate([
111
+ Injectable(),
112
+ __param(0, Inject(PluginImport)),
113
+ __param(1, Inject(PlatformPluginUpdater)),
114
+ __param(2, Inject(KERNEL_LOGGER))
115
+ ], PlatformImportUpdate);
116
+ export { PlatformImportUpdate };
@@ -8,6 +8,7 @@ import { ExtensionBlobs } from "../extensions/blobs.js";
8
8
  import { ExtensionInstalls } from "../extensions/installs.js";
9
9
  import { PluginApproveLock, PluginInstallLock } from "../extensions/locks.js";
10
10
  import { ExtensionPackageError } from "../extensions/package.js";
11
+ import { type PermissionSet } from "../extensions/permission-set.js";
11
12
  import { type PackageWarning, type PermissionGroup, type PermissionItem } from "../extensions/permission-view.js";
12
13
  import { ExtensionRegistry } from "../extensions/registry.js";
13
14
  import type { SingleProviderBlock } from "../extensions/single-provider.js";
@@ -317,6 +318,18 @@ export interface PluginConfirmResult {
317
318
  * extension (applied at once, plan Task 6).
318
319
  */
319
320
  firstVersion: boolean;
321
+ /**
322
+ * A confirm with `deploy: false` that approved the extension's permissions (its snapshot
323
+ * written): what {@link PluginImport.restoreApproval} needs to put the previous approval back
324
+ * when the step that was to deploy it fails. Never sent over HTTP (the route always deploys).
325
+ */
326
+ approval?: HeldApproval;
327
+ }
328
+ /** The approval a confirm without its deploy wrote (see {@link PluginConfirmResult.approval}). */
329
+ export interface HeldApproval {
330
+ review: PluginReview;
331
+ extension: PluginReviewExtension;
332
+ previous: PreviousSnapshot | null;
320
333
  }
321
334
  /** What prepare checks besides the package itself (npm and marketplace imports). */
322
335
  export interface PluginPrepareExpect {
@@ -338,6 +351,14 @@ export interface ReviewProvenance {
338
351
  provenance: ProvenanceResult;
339
352
  warning?: "repository_mismatch";
340
353
  }
354
+ /** The snapshot overwritten at confirm (restored when deploying an upgrade fails). */
355
+ export interface PreviousSnapshot {
356
+ version: string;
357
+ permissions: PermissionSet;
358
+ approvedBy: string | null;
359
+ approvedAt: Date;
360
+ source: "auto" | "web" | "cli" | "bootstrap";
361
+ }
341
362
  /** What an org's staged upload targets (see {@link PluginImport.stagedTarget}). */
342
363
  export interface StagedTarget {
343
364
  itemId: string;
@@ -521,8 +542,10 @@ export declare class PluginImport {
521
542
  * whether the item exists (a new version of the org's own plugin) or the version is imported
522
543
  * already (F4), and the target as the platform updater reads it: the staged manifest and files,
523
544
  * or the imported version's for a retry. Another org's staging -> 404 `plugin.staging_not_found`.
545
+ * Without `owner`: a platform staging, and `existing` means a live platform-owned item (the
546
+ * platform's import and update, `PlatformImportUpdate`).
524
547
  */
525
- stagedTarget(stagingId: string, owner: ImportOwner): Promise<StagedTarget>;
548
+ stagedTarget(stagingId: string, owner?: ImportOwner): Promise<StagedTarget>;
526
549
  /** The catalog item's view (as a confirm returns it). */
527
550
  view(itemId: string): Promise<CatalogItemView>;
528
551
  /**
@@ -553,6 +576,13 @@ export declare class PluginImport {
553
576
  orgs?: string[];
554
577
  /** An org upload's new item: shared with other orgs by its owner (default false, P3-16). */
555
578
  shareable?: boolean;
579
+ /**
580
+ * Deploy the extension after the commit (default true). false (the platform's import and
581
+ * update, `PlatformImportUpdate`) leaves the deployment to the platform update that follows,
582
+ * which moves the extension with its own rollback; the approval written is returned in
583
+ * `approval`.
584
+ */
585
+ deploy?: boolean;
556
586
  }, opts?: ActorOpts & {
557
587
  owner?: ImportOwner;
558
588
  }): Promise<PluginConfirmResult>;
@@ -603,6 +633,11 @@ export declare class PluginImport {
603
633
  * admin (the package is already in the package store) with the sha256 from the result.
604
634
  */
605
635
  private deploy;
636
+ /**
637
+ * Puts back the approval a confirm with `deploy: false` wrote, after the step that was to deploy
638
+ * the extension failed (see {@link restoreSnapshot}; false when someone changed it meanwhile).
639
+ */
640
+ restoreApproval(actor: string | null, held: HeldApproval, opts?: ActorOpts): Promise<boolean>;
606
641
  /**
607
642
  * Restores the snapshot overwritten at confirm (deletes it when there was none before), audits
608
643
  * `extension.permissions_restored`. Compare-and-write (row locked): restores only while the
@@ -610,6 +610,8 @@ let PluginImport = class PluginImport {
610
610
  * whether the item exists (a new version of the org's own plugin) or the version is imported
611
611
  * already (F4), and the target as the platform updater reads it: the staged manifest and files,
612
612
  * or the imported version's for a retry. Another org's staging -> 404 `plugin.staging_not_found`.
613
+ * Without `owner`: a platform staging, and `existing` means a live platform-owned item (the
614
+ * platform's import and update, `PlatformImportUpdate`).
613
615
  */
614
616
  async stagedTarget(stagingId, owner) {
615
617
  if (!UUID_RE.test(stagingId))
@@ -630,7 +632,7 @@ let PluginImport = class PluginImport {
630
632
  .select(["id", "awaiting_confirmation"])
631
633
  .where("id", "=", staged.itemId)
632
634
  .where("deleted_at", "is", null)
633
- .where("owner_org_id", "=", owner.orgId)
635
+ .where((eb) => owner === undefined ? eb("owner_org_id", "is", null) : eb("owner_org_id", "=", owner.orgId))
634
636
  .executeTakeFirst();
635
637
  const imported = staged.retry === true
636
638
  ? await this.db
@@ -753,6 +755,19 @@ let PluginImport = class PluginImport {
753
755
  // update. After the commit: it copies files in object storage
754
756
  if (committed.firstVersion)
755
757
  await this.syncPlatform(committed.itemId, staged.version);
758
+ if (input.deploy === false) {
759
+ return {
760
+ item: await this.itemView(committed.itemId),
761
+ created: committed.created,
762
+ deploy: null,
763
+ pending: false,
764
+ firstVersion: committed.firstVersion,
765
+ ...(now &&
766
+ committed.previous !== undefined && {
767
+ approval: { review, extension: now, previous: committed.previous },
768
+ }),
769
+ };
770
+ }
756
771
  const deploy = now && now.action !== "keep"
757
772
  ? await this.deploy(actor, review, committed.itemId, now, committed.previous, opts, input.enable ?? true)
758
773
  : null;
@@ -1184,6 +1199,13 @@ let PluginImport = class PluginImport {
1184
1199
  return { ...base, result: "failed", ...failure(e) };
1185
1200
  }
1186
1201
  }
1202
+ /**
1203
+ * Puts back the approval a confirm with `deploy: false` wrote, after the step that was to deploy
1204
+ * the extension failed (see {@link restoreSnapshot}; false when someone changed it meanwhile).
1205
+ */
1206
+ async restoreApproval(actor, held, opts = {}) {
1207
+ return this.restoreSnapshot(actor, held.review, held.extension, held.previous, opts);
1208
+ }
1187
1209
  /**
1188
1210
  * Restores the snapshot overwritten at confirm (deletes it when there was none before), audits
1189
1211
  * `extension.permissions_restored`. Compare-and-write (row locked): restores only while the
@@ -230,6 +230,13 @@ export declare class PlatformPluginUpdater {
230
230
  private get db();
231
231
  /** The update preview (see the class comment); writes nothing. */
232
232
  preview(itemId: string, version: string): Promise<PlatformUpdatePreview>;
233
+ /**
234
+ * The update preview of a version staged for import, not imported yet (the platform's import and
235
+ * update, `PlatformImportUpdate`): the same diff as {@link preview}, with the target read from the
236
+ * staging (`target`, whose files are where the import's version row will point), so the
237
+ * `diffHash` is the one {@link updateHeld} recomputes after the import. Writes nothing.
238
+ */
239
+ previewStaged(itemId: string, version: string, target: UpdateTarget): Promise<PlatformUpdatePreview>;
233
240
  /** The preview of a diff with the template guides it changes. */
234
241
  private withGuideChanges;
235
242
  /**
@@ -292,6 +299,17 @@ export declare class PlatformPluginUpdater {
292
299
  * upload runs every step under it (ruling P3-35).
293
300
  */
294
301
  withItemLock<T>(itemId: string, f: () => Promise<T>): Promise<T>;
302
+ /**
303
+ * {@link update} for a caller already holding the item's update lock ({@link withItemLock}):
304
+ * the platform's import and update runs its import and this update under one hold.
305
+ */
306
+ updateHeld(actor: string, itemId: string, input: {
307
+ version: string;
308
+ diffHash: string;
309
+ }, opts?: {
310
+ actorTokenId?: string;
311
+ auditDetail?: Record<string, unknown>;
312
+ }): Promise<PlatformUpdateResult>;
295
313
  /**
296
314
  * Runs an update confirmed with the preview's `diffHash` (see the class comment). A version an
297
315
  * org upload waits with, or an item whose first version waits, -> 409 `catalog.item_pending`
@@ -176,6 +176,18 @@ let PlatformPluginUpdater = class PlatformPluginUpdater {
176
176
  async preview(itemId, version) {
177
177
  return this.withGuideChanges(await this.diff(itemId, version));
178
178
  }
179
+ /**
180
+ * The update preview of a version staged for import, not imported yet (the platform's import and
181
+ * update, `PlatformImportUpdate`): the same diff as {@link preview}, with the target read from the
182
+ * staging (`target`, whose files are where the import's version row will point), so the
183
+ * `diffHash` is the one {@link updateHeld} recomputes after the import. Writes nothing.
184
+ */
185
+ async previewStaged(itemId, version, target) {
186
+ if (target.version !== version) {
187
+ throw new Error(`internal: previewStaged got the staged target of version ${target.version} for version ${version}`);
188
+ }
189
+ return this.withGuideChanges(await this.diff(itemId, version, { target }));
190
+ }
179
191
  /** The preview of a diff with the template guides it changes. */
180
192
  async withGuideChanges(d) {
181
193
  return {
@@ -335,6 +347,13 @@ let PlatformPluginUpdater = class PlatformPluginUpdater {
335
347
  throw itemNotFound();
336
348
  return this.locked(itemId, f);
337
349
  }
350
+ /**
351
+ * {@link update} for a caller already holding the item's update lock ({@link withItemLock}):
352
+ * the platform's import and update runs its import and this update under one hold.
353
+ */
354
+ async updateHeld(actor, itemId, input, opts = {}) {
355
+ return this.updateLocked(actor, itemId, input, opts);
356
+ }
338
357
  /**
339
358
  * Runs an update confirmed with the preview's `diffHash` (see the class comment). A version an
340
359
  * org upload waits with, or an item whose first version waits, -> 409 `catalog.item_pending`
@@ -1,6 +1,6 @@
1
1
  import { WorkerHost } from "@nestjs/bullmq";
2
2
  import { type DynamicModule } from "@nestjs/common";
3
- import type { Job, Worker } from "bullmq";
3
+ import { type Job, type Worker } from "bullmq";
4
4
  import type { KernelConfig } from "../config/schema.js";
5
5
  import { type ProcessorWorkerOptions } from "../infra/queue/configured-processor.js";
6
6
  import { type ChannelJobData } from "./jobs.js";
@@ -14,7 +14,10 @@ import { ChannelOutbound } from "./outbound.js";
14
14
  */
15
15
  export declare class ChannelsModule {
16
16
  }
17
- /** The consumer of `cap-kernel-channels` (deliveries and inbound jobs): {@link ChannelOutbound}. */
17
+ /**
18
+ * The consumer of `cap-kernel-channels`: deliveries (`DELIVER_JOB`, owed through the outbox or
19
+ * hinted) and inbound jobs (`INBOUND_JOB`), both handled by {@link ChannelOutbound}.
20
+ */
18
21
  export declare class ChannelsProcessor extends WorkerHost<Worker<ChannelJobData>> {
19
22
  private readonly outbound;
20
23
  constructor(outbound: ChannelOutbound);
@@ -10,6 +10,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
10
10
  var ChannelWorkersModule_1;
11
11
  import { OnWorkerEvent, WorkerHost } from "@nestjs/bullmq";
12
12
  import { Inject, Injectable, Module } from "@nestjs/common";
13
+ import { UnrecoverableError } from "bullmq";
13
14
  import { ConversationsModule } from "../conversations/conversations.module.js";
14
15
  import { EventLogModule } from "../conversations/event-log.js";
15
16
  import { DevicesModule } from "../devices/devices.module.js";
@@ -25,7 +26,7 @@ import { SchedulesModule } from "../schedules/schedules.module.js";
25
26
  import { ToolsModule } from "../tools/tools.module.js";
26
27
  import { ChannelConnections } from "./connections.service.js";
27
28
  import { ChannelInbound } from "./inbound.js";
28
- import { CHANNEL_QUEUE_NAME, ChannelJobs } from "./jobs.js";
29
+ import { CHANNEL_QUEUE_NAME, ChannelJobs, DELIVER_JOB, INBOUND_JOB, } from "./jobs.js";
29
30
  import { ChannelOutbound, deliverBackoff } from "./outbound.js";
30
31
  import { AnonymousGuestLock, AnonymousGuests } from "./threads.js";
31
32
  /** The channel queue's registration (its producer here; the admin health reads its counts). */
@@ -74,7 +75,10 @@ ChannelsModule = __decorate([
74
75
  })
75
76
  ], ChannelsModule);
76
77
  export { ChannelsModule };
77
- /** The consumer of `cap-kernel-channels` (deliveries and inbound jobs): {@link ChannelOutbound}. */
78
+ /**
79
+ * The consumer of `cap-kernel-channels`: deliveries (`DELIVER_JOB`, owed through the outbox or
80
+ * hinted) and inbound jobs (`INBOUND_JOB`), both handled by {@link ChannelOutbound}.
81
+ */
78
82
  let ChannelsProcessor = class ChannelsProcessor extends WorkerHost {
79
83
  outbound;
80
84
  constructor(outbound) {
@@ -82,6 +86,9 @@ let ChannelsProcessor = class ChannelsProcessor extends WorkerHost {
82
86
  this.outbound = outbound;
83
87
  }
84
88
  process(job, token) {
89
+ if (job.name !== DELIVER_JOB && job.name !== INBOUND_JOB) {
90
+ return Promise.reject(new UnrecoverableError(`unknown channel job "${job.name}"`));
91
+ }
85
92
  return runJob(CHANNEL_QUEUE_NAME, job, () => this.outbound.process(job, token));
86
93
  }
87
94
  failed(job, err) {
@@ -0,0 +1,25 @@
1
+ import type { OutboxService } from "../infra/outbox/outbox.service.js";
2
+ /**
3
+ * The channel delivery job as the outbox owes it (transactional outbox spec §8.1): a leaf module
4
+ * with no BullMQ and no Nest class, so the event log can owe it without importing the channels
5
+ * domain. `jobs.ts` (the producer) re-exports these names.
6
+ */
7
+ /** 渠道队列(出站投递与入站任务,不含 `:`),只在 worker 角色上消费。 */
8
+ export declare const CHANNEL_QUEUE_NAME = "cap-kernel-channels";
9
+ /** 投递任务名。 */
10
+ export declare const DELIVER_JOB = "deliver";
11
+ /** 投递任务 id(不含 `:`):同一会话、同一日志位置只排一个任务。 */
12
+ export declare const deliverJobId: (conversationId: string, seq: number) => string;
13
+ export interface DeliverJobData {
14
+ conversationId: string;
15
+ }
16
+ /**
17
+ * Owes the conversation's delivery job for the log position `seq` in the transaction in context:
18
+ * the caller knows the conversation has a channel target. Shares the job id of
19
+ * `ChannelJobs.enqueue`, so a hint for the same position is deduped by BullMQ.
20
+ */
21
+ export declare function oweDelivery(outbox: Pick<OutboxService, "add">, d: {
22
+ conversationId: string;
23
+ orgId: string;
24
+ seq: number;
25
+ }): Promise<boolean>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The channel delivery job as the outbox owes it (transactional outbox spec §8.1): a leaf module
3
+ * with no BullMQ and no Nest class, so the event log can owe it without importing the channels
4
+ * domain. `jobs.ts` (the producer) re-exports these names.
5
+ */
6
+ /** 渠道队列(出站投递与入站任务,不含 `:`),只在 worker 角色上消费。 */
7
+ export const CHANNEL_QUEUE_NAME = "cap-kernel-channels";
8
+ /** 投递任务名。 */
9
+ export const DELIVER_JOB = "deliver";
10
+ /** 投递任务 id(不含 `:`):同一会话、同一日志位置只排一个任务。 */
11
+ export const deliverJobId = (conversationId, seq) => `deliver-${conversationId}-${seq}`;
12
+ /**
13
+ * Owes the conversation's delivery job for the log position `seq` in the transaction in context:
14
+ * the caller knows the conversation has a channel target. Shares the job id of
15
+ * `ChannelJobs.enqueue`, so a hint for the same position is deduped by BullMQ.
16
+ */
17
+ export function oweDelivery(outbox, d) {
18
+ return outbox.add({
19
+ queue: CHANNEL_QUEUE_NAME,
20
+ name: DELIVER_JOB,
21
+ jobId: deliverJobId(d.conversationId, d.seq),
22
+ data: { conversationId: d.conversationId },
23
+ orgId: d.orgId,
24
+ });
25
+ }
@@ -72,7 +72,7 @@ export declare class ChannelInbound {
72
72
  /** 通讯录同步的投递(按 (组织, 插件) 每分钟去重的维护队列任务)。 */
73
73
  directorySync: Pick<DirectorySync, "enqueue">,
74
74
  /** The channel queue: inbound jobs (`onEvent` only dedupes and enqueues) and deliveries. */
75
- jobs: Pick<ChannelJobs, "enqueue" | "enqueueInbound">,
75
+ jobs: Pick<ChannelJobs, "enqueue" | "enqueueInbound" | "bind">,
76
76
  /** 卡片操作:审批决定与提问回答(授权由这两个服务按点击者映射到的账号判定)。 */
77
77
  approvals: Pick<ApprovalsService, "decide">, questions: Pick<QuestionsService, "answer">, guests: Pick<AnonymousGuests, "ensure">, devices: Pick<DevicesService, "list" | "imDefault" | "setImDefault">, log: Pick<Logger, "warn" | "info">);
78
78
  /**
@@ -277,21 +277,8 @@ let ChannelInbound = class ChannelInbound {
277
277
  }
278
278
  throw err;
279
279
  }
280
- await db
281
- .insertInto("channel_deliveries")
282
- .values({
283
- conversation_id: conversationId,
284
- connection_id: connectionId,
285
- target: JSON.stringify(replyTarget),
286
- last_seq: seq,
287
- })
288
- .onConflict((oc) => oc.column("conversation_id").doUpdateSet({
289
- connection_id: connectionId,
290
- target: JSON.stringify(replyTarget),
291
- updated_at: new Date(),
292
- }))
293
- .execute();
294
- await this.jobs.enqueue(conversationId);
280
+ // the reply target and its delivery, owed in one transaction (transactional outbox spec §8.1)
281
+ await this.jobs.bind(row.orgId, conversationId, connectionId, replyTarget, seq);
295
282
  }
296
283
  /**
297
284
  * Direct-message `/device` (environments spec §4.2 item 8): lists the person's usable devices,
@@ -1,14 +1,12 @@
1
- import type { InboundEvent, Logger } from "@capybara-hq/plugin-sdk";
1
+ import type { ChannelTarget, InboundEvent, Logger } from "@capybara-hq/plugin-sdk";
2
2
  import { type OnApplicationShutdown } from "@nestjs/common";
3
3
  import type { Queue } from "bullmq";
4
4
  import { KernelDb } from "../infra/database/kernel-db.js";
5
+ import { TransactionHost } from "../infra/database/transaction-host.js";
5
6
  import { KernelEvents } from "../infra/events/kernel-events.js";
6
- /** 渠道队列(出站投递与入站任务,不含 `:`),只在 worker 角色上消费。 */
7
- export declare const CHANNEL_QUEUE_NAME = "cap-kernel-channels";
8
- /** 投递任务名。 */
9
- export declare const DELIVER_JOB = "deliver";
10
- /** 投递任务 id(不含 `:`):同一会话、同一日志位置只排一个任务。 */
11
- export declare const deliverJobId: (conversationId: string, seq: number) => string;
7
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
8
+ import { type DeliverJobData } from "./deliver-job.js";
9
+ export { CHANNEL_QUEUE_NAME, DELIVER_JOB, type DeliverJobData, deliverJobId, oweDelivery, } from "./deliver-job.js";
12
10
  /**
13
11
  * 结束后复核排的任务 id:每次唯一(带随机后缀,不含 `:`)。不能复用 `deliver-<会话>-<seq>`——同 id 的任务
14
12
  * 可能正因锁被占而即将直接结束,BullMQ 会把新任务去重进它,于是没人投递。
@@ -20,10 +18,7 @@ export declare const followUpJobId: (conversationId: string, seq: number) => str
20
18
  */
21
19
  export declare function idempotencyKey(raw: string): string;
22
20
  /** 可重试失败(限流、5xx、网络、超时)的总尝试次数。 */
23
- export declare const DELIVER_ATTEMPTS = 8;
24
- export interface DeliverJobData {
25
- conversationId: string;
26
- }
21
+ export declare const DELIVER_ATTEMPTS: 8;
27
22
  /** 入站任务(Ruling A21):插件 `onEvent` 收到的事件,原样排进队列由 worker 处理。 */
28
23
  export interface InboundJobData {
29
24
  connectionId: string;
@@ -37,7 +32,7 @@ export declare const INBOUND_JOB = "inbound";
37
32
  * 用户发来的消息不能排在它们后面。
38
33
  */
39
34
  export declare const INBOUND_PRIORITY = 1;
40
- export declare const DELIVER_PRIORITY = 10;
35
+ export declare const DELIVER_PRIORITY: 10;
41
36
  /** 入站任务的总尝试次数(处理失败由 BullMQ 指数退避重试:1s、2s、4s、8s)。 */
42
37
  export declare const INBOUND_ATTEMPTS = 5;
43
38
  /**
@@ -49,24 +44,39 @@ export declare const inboundJobId: (connectionId: string, messageId: string) =>
49
44
  * The producer side of the channel queue `cap-kernel-channels` (any role): delivery jobs for a
50
45
  * conversation with an IM target, inbound jobs (Ruling A21), and the catch-up of a connection that
51
46
  * can deliver again. Its consumer is {@link ChannelOutbound} through the `@Processor` of the worker
52
- * role. Delivery jobs follow the kernel's domain events (spec §3b): every published log advance
53
- * (`conversation.event-appended`), a run starting here (`run.started`), a stopped wait
54
- * (`run.wait-stopped`), a decided approval and an answered question (their cards change). Each is
55
- * lost-tolerant: the next append enqueues again, and a finished delivery job re-checks the log.
47
+ * role. A delivery is owed (transactional outbox spec §8.1): every event append of a conversation
48
+ * with a channel target adds its deliver job to the outbox in the append's transaction (a reply, an
49
+ * approval decision, a question answer, a wait stop all append), and {@link bind} owes the first
50
+ * one with the target. The direct {@link enqueue} is left for hints and catch-ups: a run starting
51
+ * here (`run.started`, so its streaming drafts start at once), a finished delivery job's re-check,
52
+ * a card click's thread and a connection that can deliver again.
56
53
  */
57
54
  export declare class ChannelJobs implements OnApplicationShutdown {
58
55
  private readonly db;
59
56
  private readonly queue;
60
57
  private readonly log;
58
+ private readonly outbox;
59
+ readonly txHost: TransactionHost;
61
60
  /** Stopped (the stop's channel-outbound phase): enqueues do nothing, inbound enqueues throw. */
62
61
  private closed;
63
62
  private readonly unsubscribe;
64
- constructor(db: KernelDb, queue: Queue<ChannelJobData>, events: Pick<KernelEvents, "on">, log: Pick<Logger, "warn" | "error">);
63
+ constructor(db: KernelDb, queue: Queue<ChannelJobData>, events: Pick<KernelEvents, "on">, log: Pick<Logger, "warn" | "error">, outbox: Pick<OutboxService, "add">, txHost: TransactionHost);
65
64
  /** Unsubscribes after the stop phases: events emitted while they drain are still handled. */
66
65
  onApplicationShutdown(): void;
66
+ /**
67
+ * Makes `target` (on `connectionId`) the conversation's delivery target and owes its delivery
68
+ * from the current log position, in one transaction. A new target starts delivering after
69
+ * `seq` (the message that bound it); a known one keeps its progress. The conversation row is
70
+ * locked first, as an append locks it: an append either committed before (its events are below
71
+ * the owed position) or reads the target (and owes its own job).
72
+ */
73
+ bind(orgId: string, conversationId: string, connectionId: string, target: ChannelTarget, seq: number): Promise<void>;
67
74
  /** 会话有 IM 投递目标时排一个出站任务,不等入队完成;失败只记日志。 */
68
75
  private enqueueSoon;
69
- /** 会话有 IM 投递目标时投递一个出站任务(同一日志位置重复调用只排一个);已关闭时什么都不做。 */
76
+ /**
77
+ * A hint (not owed work, see the class): a delivery job for a conversation with an IM target (one
78
+ * per log position; `followUp` gets a fresh id); does nothing once closed.
79
+ */
70
80
  enqueue(conversationId: string, opts?: {
71
81
  followUp?: boolean;
72
82
  }): Promise<void>;