@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
@@ -1,13 +1,12 @@
1
- import { WorkerHost } from "@nestjs/bullmq";
2
- import type { Job, Queue, Worker } from "bullmq";
3
1
  import { ExtensionLifecycle } from "../extensions/lifecycle.js";
4
2
  import { ExtensionRegistry } from "../extensions/registry.js";
5
3
  import { KernelDb } from "../infra/database/kernel-db.js";
6
4
  import { TransactionHost } from "../infra/database/transaction-host.js";
7
5
  import { LockService } from "../infra/lock/lock.service.js";
8
- import type { OneJob } from "../infra/queue/add-job-once.js";
6
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
9
7
  import type { KernelLogger } from "../log.js";
10
8
  import { PluginInstaller } from "./plugin-install.js";
9
+ import { type ScopeJob } from "./scope-jobs.js";
11
10
  /**
12
11
  * The plugin scope sync (plugin and component management spec §5, phase 1): converges every org
13
12
  * to each platform plugin's scope. In phase 1 "on in an org" is the org's plugin install, so for
@@ -30,55 +29,29 @@ import { PluginInstaller } from "./plugin-install.js";
30
29
  * super admin's inbox, until a later sync of the pair does not fail) and not retried until the next
31
30
  * enqueue. An error outside those steps (a read, the failure write itself) ends the attempt without
32
31
  * touching the pair's row, so an older failure stays open.
33
- * Every worker start enqueues every live item once ({@link PluginScopeSync.enqueueAll}), which
34
- * also catches a lost enqueue.
35
32
  *
36
33
  * Jobs (queue {@link PLUGIN_SCOPE_QUEUE}): one per item and scope version
37
34
  * (`scope-item-<itemId>-<scope_version>`, every org), one per org (`scope-org-<orgId>`, every item,
38
- * enqueued after the org is created). Both are removed when done, so the next enqueue with the same
35
+ * owed by the org's creation). Both are owed in the outbox by the transaction that changes what
36
+ * they converge to (transactional outbox spec §8.1), so none is lost; a migration that changes a
37
+ * scope inserts its outbox rows itself. A confirmation (import, permissions, org upload) owes the
38
+ * immediate item job after its deploy and, in its commit, a backstop `scope-item-backstop` due
39
+ * five minutes later (ruling R24) that waits, retrying, for the extension to be enabled. Both are removed when done, so the next add with the same
39
40
  * id is not swallowed. Pairs of one org are serialized across replicas by a Redis lock per org; the
40
41
  * installer's org row lock and its one-live-install rule are the backstop.
41
42
  */
42
- /** The scope sync queue (no `:` in the name). */
43
- export declare const PLUGIN_SCOPE_QUEUE = "cap-kernel-plugin-scope";
44
- /** The job converging one item in every org. */
45
- export declare const SCOPE_ITEM_JOB = "scope-item";
46
- /** The job converging every item in one org. */
47
- export declare const SCOPE_ORG_JOB = "scope-org";
48
- /** One item job per scope version (no `:`: item ids are UUIDs). */
49
- export declare const scopeItemJobId: (itemId: string, version: number) => string;
50
- /** One org job at a time per org. */
51
- export declare const scopeOrgJobId: (orgId: string) => string;
52
- /**
53
- * The delayed retry `n` of an item job whose extension this replica had not loaded yet (ruling
54
- * P3-38); deterministic, so replicas deferring the same item add it once.
55
- */
56
- export declare const scopeRetryJobId: (itemId: string, version: number, n: number) => string;
43
+ export { PLUGIN_SCOPE_QUEUE, SCOPE_BACKSTOP_DELAY, SCOPE_BACKSTOP_DELAY_MS, SCOPE_BACKSTOP_JOB, SCOPE_ITEM_JOB, SCOPE_ORG_JOB, type ScopeJob, scopeBackstopJobId, scopeItemJobId, scopeOrgJobId, scopeRetryJobId, } from "./scope-jobs.js";
57
44
  /** How many delayed retries an item job gets while its extension is not loaded here (P3-38). */
58
45
  export declare const SCOPE_LOAD_RETRIES = 6;
59
46
  /** The delay of each such retry. */
60
47
  export declare const SCOPE_LOAD_RETRY_DELAY_MS = 5000;
61
48
  /** How long a pair waits for this replica's reconcile before deferring (P3-38). */
62
49
  export declare const SCOPE_RECONCILE_WAIT_MS = 10000;
63
- /**
64
- * The job's data. `retry`: the item job's delayed retry number (P3-38); at
65
- * {@link SCOPE_LOAD_RETRIES} an extension still not loaded is a failure as before.
66
- */
67
- export type ScopeJob = {
68
- kind: "item";
69
- itemId: string;
70
- retry?: number;
71
- } | {
72
- kind: "org";
73
- orgId: string;
74
- };
75
50
  /**
76
51
  * Awaits this replica's extension reconcile for at most `ms` (a slower one carries on in the
77
52
  * background); a failure only warns.
78
53
  */
79
54
  export declare function reconcileWithin(lifecycle: Pick<ExtensionLifecycle, "reconcile">, reason: string, ms: number, log: Pick<KernelLogger, "warn">): Promise<void>;
80
- /** The item job of a scope version (what {@link PluginScopeSync.enqueueItem} adds). */
81
- export declare function scopeItemJob(itemId: string, version: number): OneJob<ScopeJob>;
82
55
  /**
83
56
  * What a sync of one (item, org) did. `deferred`: the plugin's extension is enabled on the
84
57
  * deployment but not loaded on this replica yet (P3-38); the item job retries later.
@@ -88,7 +61,7 @@ export type ScopeSyncResult = "installed" | "removed" | "needs_config" | "unchan
88
61
  export declare const PLUGIN_SCOPE_CONCURRENCY = 2;
89
62
  export declare class PluginScopeSync {
90
63
  private readonly kdb;
91
- private readonly queue;
64
+ private readonly outbox;
92
65
  private readonly installer;
93
66
  private readonly registry;
94
67
  /** This replica's reconcile, awaited (bounded) before deferring an install (P3-38). */
@@ -96,23 +69,44 @@ export declare class PluginScopeSync {
96
69
  private readonly locks;
97
70
  private readonly log;
98
71
  readonly txHost: TransactionHost;
99
- constructor(kdb: KernelDb, queue: Queue<ScopeJob>, installer: PluginInstaller, registry: ExtensionRegistry,
72
+ /** The backstop's delay (a test seam; default {@link SCOPE_BACKSTOP_DELAY_MS}). */
73
+ private readonly backstopDelayMs;
74
+ constructor(kdb: KernelDb, outbox: Pick<OutboxService, "add">, installer: PluginInstaller, registry: ExtensionRegistry,
100
75
  /** This replica's reconcile, awaited (bounded) before deferring an install (P3-38). */
101
- lifecycle: ExtensionLifecycle, locks: LockService, log: KernelLogger, txHost: TransactionHost);
76
+ lifecycle: ExtensionLifecycle, locks: LockService, log: KernelLogger, txHost: TransactionHost,
77
+ /** The backstop's delay (a test seam; default {@link SCOPE_BACKSTOP_DELAY_MS}). */
78
+ backstopDelayMs?: number);
102
79
  private get db();
103
- /** Enqueues the sync of an item in every org, once per scope version. Call after the commit. */
80
+ /**
81
+ * Owes the sync of an item in every org, once per scope version (the version read in the
82
+ * caller's transaction, so after its own bump), in the transaction that changes the scope.
83
+ */
104
84
  enqueueItem(itemId: string): Promise<void>;
105
85
  /**
106
- * Enqueues the sync of every live, not builtin item (a worker start): retries what failed or was
107
- * never enqueued (a lost `afterCommit` enqueue, a migrated scope). The job ids are the items'
108
- * scope versions, so replicas starting together add each one once.
86
+ * Owes the delayed backstop of the item's sync (ruling R24) in the transaction in context: a
87
+ * confirmation owes it before (or with) its deploy, so a crash before its post-deploy add still
88
+ * converges the orgs.
109
89
  */
110
- enqueueAll(): Promise<void>;
111
- /** Enqueues the sync of every item in an org (a new org). Call after the commit. */
90
+ enqueueBackstop(itemId: string): Promise<void>;
91
+ /** Owes the sync of every item in an org, in the transaction that creates the org. */
112
92
  enqueueOrg(orgId: string): Promise<void>;
113
- /** One job: every org of an item, or every item of an org; `signal` stops between pairs. */
93
+ /**
94
+ * One job: every org of an item, or every item of an org; `signal` stops between pairs. A
95
+ * backstop job first checks the item's extension is deployed ({@link assertDeployed}).
96
+ */
114
97
  process(data: ScopeJob, signal?: AbortSignal): Promise<void>;
115
- /** The delayed retry `n` of an item job (deterministic id, P3-38). */
98
+ /**
99
+ * A backstop job (ruling R24) runs only once the item's extension is loaded here or enabled on
100
+ * the deployment; otherwise it throws, so BullMQ retries it with its backoff (bounded by its
101
+ * attempts) and no pair records a `catalog.not_deployed` failure that nothing would retry. An item without an
102
+ * extension, or one no longer live (deleted, disabled, still waiting, builtin), passes: its pairs
103
+ * converge (or change nothing) as for an immediate job.
104
+ */
105
+ private assertDeployed;
106
+ /**
107
+ * Owes the delayed retry `n` of an item job (deterministic id, P3-38) in a transaction of its
108
+ * own: the job's own follow-up, delivered by the relay once due.
109
+ */
116
110
  private enqueueRetry;
117
111
  /**
118
112
  * Whether the install of the item's `version` may go ahead on this replica (P3-38): its
@@ -155,22 +149,3 @@ export declare class PluginScopeSync {
155
149
  private failed;
156
150
  private recordFailure;
157
151
  }
158
- /** The consumer of {@link PLUGIN_SCOPE_QUEUE}: {@link PluginScopeSync}. */
159
- export declare class PluginScopeSyncProcessor extends WorkerHost<Worker<ScopeJob>> {
160
- private readonly sync;
161
- private readonly log;
162
- private readonly stopping;
163
- constructor(sync: PluginScopeSync, log: Pick<KernelLogger, "error">);
164
- process(job: Job<ScopeJob>): Promise<void>;
165
- /** Starts consuming (worker role, after the extensions loaded; built with `autorun: false`). */
166
- start(): void;
167
- /**
168
- * Enqueues every live item's sync once (the worker role, after the start gate opened), in the
169
- * background; a failure is only logged (the next worker start, or the next change, enqueues).
170
- */
171
- resync(): void;
172
- /** Stops between pairs and closes the worker. */
173
- close(): Promise<void>;
174
- failed(job: Job<ScopeJob> | undefined, err: Error): void;
175
- error(err: Error): void;
176
- }
@@ -7,8 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
- import { getQueueToken, OnWorkerEvent, WorkerHost } from "@nestjs/bullmq";
11
- import { Inject, Injectable } from "@nestjs/common";
10
+ import { Inject, Injectable, Optional } from "@nestjs/common";
12
11
  import { ApiError } from "../errors.js";
13
12
  import { ExtensionLifecycle } from "../extensions/lifecycle.js";
14
13
  import { ExtensionRegistry } from "../extensions/registry.js";
@@ -17,12 +16,13 @@ import { KernelDb } from "../infra/database/kernel-db.js";
17
16
  import { TransactionHost } from "../infra/database/transaction-host.js";
18
17
  import { Transactional } from "../infra/database/transactional.decorator.js";
19
18
  import { LockService } from "../infra/lock/lock.service.js";
20
- import { runJob } from "../infra/queue/run-job.js";
19
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
21
20
  import { KERNEL_LOGGER } from "../infra/tokens.js";
22
21
  import { connectorTemplatesOn, templateIdOfPath } from "./component-state.js";
23
22
  import { isUsable } from "./ownership.js";
24
23
  import { PluginInstaller } from "./plugin-install.js";
25
24
  import { wantedOn } from "./scope.js";
25
+ import { oweScopeItem, PLUGIN_SCOPE_QUEUE, SCOPE_BACKSTOP_DELAY, SCOPE_BACKSTOP_DELAY_MS, SCOPE_ITEM_JOB, SCOPE_ORG_JOB, scopeOrgJobId, scopeRetryJobId, } from "./scope-jobs.js";
26
26
  import { ScopeNoLongerWanted, ScopeStillWanted } from "./scope-state.js";
27
27
  /**
28
28
  * The plugin scope sync (plugin and component management spec §5, phase 1): converges every org
@@ -46,30 +46,18 @@ import { ScopeNoLongerWanted, ScopeStillWanted } from "./scope-state.js";
46
46
  * super admin's inbox, until a later sync of the pair does not fail) and not retried until the next
47
47
  * enqueue. An error outside those steps (a read, the failure write itself) ends the attempt without
48
48
  * touching the pair's row, so an older failure stays open.
49
- * Every worker start enqueues every live item once ({@link PluginScopeSync.enqueueAll}), which
50
- * also catches a lost enqueue.
51
49
  *
52
50
  * Jobs (queue {@link PLUGIN_SCOPE_QUEUE}): one per item and scope version
53
51
  * (`scope-item-<itemId>-<scope_version>`, every org), one per org (`scope-org-<orgId>`, every item,
54
- * enqueued after the org is created). Both are removed when done, so the next enqueue with the same
52
+ * owed by the org's creation). Both are owed in the outbox by the transaction that changes what
53
+ * they converge to (transactional outbox spec §8.1), so none is lost; a migration that changes a
54
+ * scope inserts its outbox rows itself. A confirmation (import, permissions, org upload) owes the
55
+ * immediate item job after its deploy and, in its commit, a backstop `scope-item-backstop` due
56
+ * five minutes later (ruling R24) that waits, retrying, for the extension to be enabled. Both are removed when done, so the next add with the same
55
57
  * id is not swallowed. Pairs of one org are serialized across replicas by a Redis lock per org; the
56
58
  * installer's org row lock and its one-live-install rule are the backstop.
57
59
  */
58
- /** The scope sync queue (no `:` in the name). */
59
- export const PLUGIN_SCOPE_QUEUE = "cap-kernel-plugin-scope";
60
- /** The job converging one item in every org. */
61
- export const SCOPE_ITEM_JOB = "scope-item";
62
- /** The job converging every item in one org. */
63
- export const SCOPE_ORG_JOB = "scope-org";
64
- /** One item job per scope version (no `:`: item ids are UUIDs). */
65
- export const scopeItemJobId = (itemId, version) => `scope-item-${itemId}-${version}`;
66
- /** One org job at a time per org. */
67
- export const scopeOrgJobId = (orgId) => `scope-org-${orgId}`;
68
- /**
69
- * The delayed retry `n` of an item job whose extension this replica had not loaded yet (ruling
70
- * P3-38); deterministic, so replicas deferring the same item add it once.
71
- */
72
- export const scopeRetryJobId = (itemId, version, n) => `scope-item-${itemId}-${version}-r${n}`;
60
+ export { PLUGIN_SCOPE_QUEUE, SCOPE_BACKSTOP_DELAY, SCOPE_BACKSTOP_DELAY_MS, SCOPE_BACKSTOP_JOB, SCOPE_ITEM_JOB, SCOPE_ORG_JOB, scopeBackstopJobId, scopeItemJobId, scopeOrgJobId, scopeRetryJobId, } from "./scope-jobs.js";
73
61
  /** How many delayed retries an item job gets while its extension is not loaded here (P3-38). */
74
62
  export const SCOPE_LOAD_RETRIES = 6;
75
63
  /** The delay of each such retry. */
@@ -98,19 +86,6 @@ export async function reconcileWithin(lifecycle, reason, ms, log) {
98
86
  clearTimeout(timer);
99
87
  }
100
88
  }
101
- /** The item job of a scope version (what {@link PluginScopeSync.enqueueItem} adds). */
102
- export function scopeItemJob(itemId, version) {
103
- return {
104
- name: SCOPE_ITEM_JOB,
105
- data: { kind: "item", itemId },
106
- opts: {
107
- jobId: scopeItemJobId(itemId, version),
108
- attempts: 1,
109
- removeOnComplete: true,
110
- removeOnFail: true,
111
- },
112
- };
113
- }
114
89
  /** How long one org's pairs wait for each other (an install can take a while). */
115
90
  const ORG_LOCK_TTL_MS = 60_000;
116
91
  const ORG_LOCK_WAIT_MS = 10 * 60_000;
@@ -119,62 +94,67 @@ export const PLUGIN_SCOPE_CONCURRENCY = 2;
119
94
  const LIVE = ["installing", "installed", "upgrading"];
120
95
  let PluginScopeSync = class PluginScopeSync {
121
96
  kdb;
122
- queue;
97
+ outbox;
123
98
  installer;
124
99
  registry;
125
100
  lifecycle;
126
101
  locks;
127
102
  log;
128
103
  txHost;
129
- constructor(kdb, queue, installer, registry,
104
+ backstopDelayMs;
105
+ constructor(kdb, outbox, installer, registry,
130
106
  /** This replica's reconcile, awaited (bounded) before deferring an install (P3-38). */
131
- lifecycle, locks, log, txHost) {
107
+ lifecycle, locks, log, txHost,
108
+ /** The backstop's delay (a test seam; default {@link SCOPE_BACKSTOP_DELAY_MS}). */
109
+ backstopDelayMs = SCOPE_BACKSTOP_DELAY_MS) {
132
110
  this.kdb = kdb;
133
- this.queue = queue;
111
+ this.outbox = outbox;
134
112
  this.installer = installer;
135
113
  this.registry = registry;
136
114
  this.lifecycle = lifecycle;
137
115
  this.locks = locks;
138
116
  this.log = log;
139
117
  this.txHost = txHost;
118
+ this.backstopDelayMs = backstopDelayMs;
140
119
  }
141
120
  get db() {
142
121
  return this.kdb.db;
143
122
  }
144
- /** Enqueues the sync of an item in every org, once per scope version. Call after the commit. */
123
+ /**
124
+ * Owes the sync of an item in every org, once per scope version (the version read in the
125
+ * caller's transaction, so after its own bump), in the transaction that changes the scope.
126
+ */
145
127
  async enqueueItem(itemId) {
146
- const row = await this.db
147
- .selectFrom("catalog_items")
148
- .select("scope_version")
149
- .where("id", "=", itemId)
150
- .executeTakeFirst();
151
- if (!row)
152
- return;
153
- const job = scopeItemJob(itemId, row.scope_version);
154
- await this.queue.add(job.name, job.data, job.opts);
128
+ await oweScopeItem(this.outbox, this.db, itemId);
155
129
  }
156
130
  /**
157
- * Enqueues the sync of every live, not builtin item (a worker start): retries what failed or was
158
- * never enqueued (a lost `afterCommit` enqueue, a migrated scope). The job ids are the items'
159
- * scope versions, so replicas starting together add each one once.
131
+ * Owes the delayed backstop of the item's sync (ruling R24) in the transaction in context: a
132
+ * confirmation owes it before (or with) its deploy, so a crash before its post-deploy add still
133
+ * converges the orgs.
160
134
  */
161
- async enqueueAll() {
162
- const items = await this.db
163
- .selectFrom("catalog_items")
164
- .select("id")
165
- .where("deleted_at", "is", null)
166
- .where("builtin", "=", false)
167
- .orderBy("id")
168
- .execute();
169
- for (const i of items)
170
- await this.enqueueItem(i.id);
135
+ async enqueueBackstop(itemId) {
136
+ await oweScopeItem(this.outbox, this.db, itemId, {
137
+ backstop: true,
138
+ backstopDelayMs: this.backstopDelayMs,
139
+ });
171
140
  }
172
- /** Enqueues the sync of every item in an org (a new org). Call after the commit. */
141
+ /** Owes the sync of every item in an org, in the transaction that creates the org. */
173
142
  async enqueueOrg(orgId) {
174
- await this.queue.add(SCOPE_ORG_JOB, { kind: "org", orgId }, { jobId: scopeOrgJobId(orgId), attempts: 1, removeOnComplete: true, removeOnFail: true });
143
+ await this.outbox.add({
144
+ queue: PLUGIN_SCOPE_QUEUE,
145
+ name: SCOPE_ORG_JOB,
146
+ jobId: scopeOrgJobId(orgId),
147
+ data: { kind: "org", orgId },
148
+ orgId,
149
+ });
175
150
  }
176
- /** One job: every org of an item, or every item of an org; `signal` stops between pairs. */
151
+ /**
152
+ * One job: every org of an item, or every item of an org; `signal` stops between pairs. A
153
+ * backstop job first checks the item's extension is deployed ({@link assertDeployed}).
154
+ */
177
155
  async process(data, signal) {
156
+ if (data.kind === "item" && data.backstop === true)
157
+ await this.assertDeployed(data.itemId);
178
158
  const pairs = data.kind === "item"
179
159
  ? (await this.db
180
160
  .selectFrom("orgs")
@@ -214,7 +194,55 @@ let PluginScopeSync = class PluginScopeSync {
214
194
  }
215
195
  }
216
196
  }
217
- /** The delayed retry `n` of an item job (deterministic id, P3-38). */
197
+ /**
198
+ * A backstop job (ruling R24) runs only once the item's extension is loaded here or enabled on
199
+ * the deployment; otherwise it throws, so BullMQ retries it with its backoff (bounded by its
200
+ * attempts) and no pair records a `catalog.not_deployed` failure that nothing would retry. An item without an
201
+ * extension, or one no longer live (deleted, disabled, still waiting, builtin), passes: its pairs
202
+ * converge (or change nothing) as for an immediate job.
203
+ */
204
+ async assertDeployed(itemId) {
205
+ const item = await this.db
206
+ .selectFrom("catalog_items")
207
+ .select([
208
+ "builtin",
209
+ "disabled",
210
+ "deleted_at",
211
+ "awaiting_confirmation",
212
+ "latest_version",
213
+ "platform_synced_version",
214
+ ])
215
+ .where("id", "=", itemId)
216
+ .executeTakeFirst();
217
+ if (!item || item.builtin || !isUsable(item))
218
+ return;
219
+ const version = item.platform_synced_version ?? item.latest_version;
220
+ const v = await this.db
221
+ .selectFrom("catalog_item_versions")
222
+ .select("manifest")
223
+ .where("item_id", "=", itemId)
224
+ .where("version", "=", version)
225
+ .executeTakeFirst();
226
+ const m = v?.manifest;
227
+ if (!m?.extension)
228
+ return;
229
+ // loaded here (a config-declared extension has no install record), or enabled on the
230
+ // deployment (a replica that has not loaded it yet defers as P3-38 says)
231
+ if (this.registry.manifests().some((x) => x.name === m.name))
232
+ return;
233
+ const install = await this.db
234
+ .selectFrom("extension_installs")
235
+ .select("desired_state")
236
+ .where("name", "=", m.name)
237
+ .executeTakeFirst();
238
+ if (install?.desired_state === "enabled")
239
+ return;
240
+ throw new Error(`the extension ${m.name} of catalog item ${itemId} is not enabled on the deployment yet; the scope sync backstop is retried`);
241
+ }
242
+ /**
243
+ * Owes the delayed retry `n` of an item job (deterministic id, P3-38) in a transaction of its
244
+ * own: the job's own follow-up, delivered by the relay once due.
245
+ */
218
246
  async enqueueRetry(itemId, n) {
219
247
  const row = await this.db
220
248
  .selectFrom("catalog_items")
@@ -223,12 +251,12 @@ let PluginScopeSync = class PluginScopeSync {
223
251
  .executeTakeFirst();
224
252
  if (!row)
225
253
  return;
226
- await this.queue.add(SCOPE_ITEM_JOB, { kind: "item", itemId, retry: n }, {
254
+ await this.outbox.add({
255
+ queue: PLUGIN_SCOPE_QUEUE,
256
+ name: SCOPE_ITEM_JOB,
227
257
  jobId: scopeRetryJobId(itemId, row.scope_version, n),
228
- delay: SCOPE_LOAD_RETRY_DELAY_MS,
229
- attempts: 1,
230
- removeOnComplete: true,
231
- removeOnFail: true,
258
+ data: { kind: "item", itemId, retry: n },
259
+ runAt: new Date(Date.now() + SCOPE_LOAD_RETRY_DELAY_MS),
232
260
  });
233
261
  }
234
262
  /**
@@ -509,68 +537,32 @@ let PluginScopeSync = class PluginScopeSync {
509
537
  .execute();
510
538
  }
511
539
  };
540
+ __decorate([
541
+ Transactional({ propagation: "mandatory" })
542
+ ], PluginScopeSync.prototype, "enqueueItem", null);
543
+ __decorate([
544
+ Transactional({ propagation: "mandatory" })
545
+ ], PluginScopeSync.prototype, "enqueueBackstop", null);
546
+ __decorate([
547
+ Transactional({ propagation: "mandatory" })
548
+ ], PluginScopeSync.prototype, "enqueueOrg", null);
549
+ __decorate([
550
+ Transactional()
551
+ ], PluginScopeSync.prototype, "enqueueRetry", null);
512
552
  __decorate([
513
553
  Transactional({ propagation: "requires_new" })
514
554
  ], PluginScopeSync.prototype, "recordFailure", null);
515
555
  PluginScopeSync = __decorate([
516
556
  Injectable(),
517
557
  __param(0, Inject(KernelDb)),
518
- __param(1, Inject(getQueueToken(PLUGIN_SCOPE_QUEUE))),
558
+ __param(1, Inject(OutboxService)),
519
559
  __param(2, Inject(PluginInstaller)),
520
560
  __param(3, Inject(ExtensionRegistry)),
521
561
  __param(4, Inject(ExtensionLifecycle)),
522
562
  __param(5, Inject(LockService)),
523
563
  __param(6, Inject(KERNEL_LOGGER)),
524
- __param(7, Inject(TransactionHost))
564
+ __param(7, Inject(TransactionHost)),
565
+ __param(8, Optional()),
566
+ __param(8, Inject(SCOPE_BACKSTOP_DELAY))
525
567
  ], PluginScopeSync);
526
568
  export { PluginScopeSync };
527
- /** The consumer of {@link PLUGIN_SCOPE_QUEUE}: {@link PluginScopeSync}. */
528
- let PluginScopeSyncProcessor = class PluginScopeSyncProcessor extends WorkerHost {
529
- sync;
530
- log;
531
- stopping = new AbortController();
532
- constructor(sync, log) {
533
- super();
534
- this.sync = sync;
535
- this.log = log;
536
- }
537
- process(job) {
538
- return runJob(PLUGIN_SCOPE_QUEUE, job, () => this.sync.process(job.data, this.stopping.signal));
539
- }
540
- /** Starts consuming (worker role, after the extensions loaded; built with `autorun: false`). */
541
- start() {
542
- void this.worker.run().catch((err) => this.error(err));
543
- }
544
- /**
545
- * Enqueues every live item's sync once (the worker role, after the start gate opened), in the
546
- * background; a failure is only logged (the next worker start, or the next change, enqueues).
547
- */
548
- resync() {
549
- void this.sync
550
- .enqueueAll()
551
- .catch((err) => this.log.error("plugin scope resync failed", { err }));
552
- }
553
- /** Stops between pairs and closes the worker. */
554
- async close() {
555
- this.stopping.abort();
556
- await this.worker.close();
557
- }
558
- failed(job, err) {
559
- this.log.error("plugin scope job failed", { name: job?.name, jobId: job?.id, err });
560
- }
561
- error(err) {
562
- this.log.error("plugin scope worker error", { err });
563
- }
564
- };
565
- __decorate([
566
- OnWorkerEvent("failed")
567
- ], PluginScopeSyncProcessor.prototype, "failed", null);
568
- __decorate([
569
- OnWorkerEvent("error")
570
- ], PluginScopeSyncProcessor.prototype, "error", null);
571
- PluginScopeSyncProcessor = __decorate([
572
- Injectable(),
573
- __param(0, Inject(PluginScopeSync)),
574
- __param(1, Inject(KERNEL_LOGGER))
575
- ], PluginScopeSyncProcessor);
576
- export { PluginScopeSyncProcessor };
@@ -11,6 +11,7 @@ import { randomUUID } from "node:crypto";
11
11
  import { Inject, Injectable } from "@nestjs/common";
12
12
  import { ConversationsService } from "../conversations/conversations.service.js";
13
13
  import { imSenderContext, recipientLocale, } from "../conversations/message-context.js";
14
+ import { channelSource } from "../conversations/source.js";
14
15
  import { DevicesService } from "../devices/devices.service.js";
15
16
  import { DEFAULT_DEVICE_ALIASES, deviceLabel } from "../devices/label.js";
16
17
  import { ApiError } from "../errors.js";
@@ -227,8 +228,7 @@ let ChannelInbound = class ChannelInbound {
227
228
  try {
228
229
  // bound to the person's IM default device, else their default (environments spec
229
230
  // §4.2 item 8); an anonymous sender's stays unbound (no sandbox)
230
- created = (await this.conversations.create(row.orgId, accountId, row.agentId, { im: true }))
231
- .id;
231
+ created = (await this.conversations.create(row.orgId, accountId, row.agentId, { im: true }, { source: channelSource(e.target.chatType) })).id;
232
232
  }
233
233
  catch (err) {
234
234
  if (err instanceof ApiError && err.code === "agent.not_found") {
@@ -21,9 +21,10 @@ export declare class AnonymousGuests {
21
21
  readonly txHost: TransactionHost;
22
22
  constructor(db: KernelDb, guestLock: AnonymousGuestLock, txHost: TransactionHost);
23
23
  /**
24
- * 该组织唯一的匿名访客账号 id;首次使用时创建(`accounts.status = active`、没有任何登录方式、组织角色 guest)。
25
- * 每次都确保成员关系存在(被移出后再次使用会补回 guest):运行的兜底扫描会中止「发起人已不是该组织成员」的运行。
26
- * 跨副本并发首次创建由 {@link AnonymousGuestLock} 串行化,只建一个账号。
24
+ * The org's one anonymous guest account id, created on first use (`accounts.status = active`, no
25
+ * way to sign in, org role guest). Every call makes sure the membership exists (a removed guest
26
+ * is added back on its next use). Concurrent first creations across replicas are serialized by
27
+ * {@link AnonymousGuestLock}: one account.
27
28
  */
28
29
  ensure(orgId: string): Promise<string>;
29
30
  }
@@ -55,9 +55,10 @@ let AnonymousGuests = class AnonymousGuests {
55
55
  this.txHost = txHost;
56
56
  }
57
57
  /**
58
- * 该组织唯一的匿名访客账号 id;首次使用时创建(`accounts.status = active`、没有任何登录方式、组织角色 guest)。
59
- * 每次都确保成员关系存在(被移出后再次使用会补回 guest):运行的兜底扫描会中止「发起人已不是该组织成员」的运行。
60
- * 跨副本并发首次创建由 {@link AnonymousGuestLock} 串行化,只建一个账号。
58
+ * The org's one anonymous guest account id, created on first use (`accounts.status = active`, no
59
+ * way to sign in, org role guest). Every call makes sure the membership exists (a removed guest
60
+ * is added back on its next use). Concurrent first creations across replicas are serialized by
61
+ * {@link AnonymousGuestLock}: one account.
61
62
  */
62
63
  async ensure(orgId) {
63
64
  await this.guestLock.lock(orgId);
@@ -190,7 +190,9 @@ let ConversationsController = class ConversationsController {
190
190
  const pick = body.deviceId === undefined ? {} : { deviceId: body.deviceId };
191
191
  if (body.message === undefined) {
192
192
  return {
193
- conversation: await this.conversations.create(org.orgId, p.account.id, body.agentId, pick),
193
+ conversation: await this.conversations.create(org.orgId, p.account.id, body.agentId, pick, {
194
+ source: "web",
195
+ }),
194
196
  };
195
197
  }
196
198
  const m = body.message;
@@ -204,7 +206,7 @@ let ConversationsController = class ConversationsController {
204
206
  ...(m.skill !== undefined && { skill: m.skill }),
205
207
  ...(m.timeZone !== undefined && { timeZone: m.timeZone }),
206
208
  ...(m.locale !== undefined && { locale: m.locale }),
207
- }, pick);
209
+ }, pick, { source: "web" });
208
210
  reply.status(out.created ? 201 : 200);
209
211
  return { conversation: out.conversation, event: out.event, events: out.events };
210
212
  }
@@ -1,3 +1,4 @@
1
+ import type { ConversationSource } from "@capybara-hq/plugin-sdk";
1
2
  import { AgentLookup } from "../agents/agent-lookup.js";
2
3
  import type { ModelRef } from "../agents/definition.js";
3
4
  import type { AgentAvatar } from "../agents/profile.js";
@@ -146,6 +147,8 @@ export interface RunControl {
146
147
  * 否则返回 false。
147
148
  */
148
149
  abortIfOrphaned(conversationId: string, runId: string, byAccountId: string): Promise<boolean>;
150
+ /** Ends a run still `queued` in PG before a worker started it (`RunQueue.abortQueued`): true. */
151
+ abortQueued(conversationId: string, runId: string, byAccountId: string): Promise<boolean>;
149
152
  /**
150
153
  * 运行在等人处理(waiting)时直接中止:取消等待、写 `run_aborted`、删除 checkpoint,返回 true;
151
154
  * 运行已不是 waiting(例如刚被续跑)→ false。
@@ -228,6 +231,14 @@ export interface InternalMessageOptions {
228
231
  schedule?: ScheduleMarker;
229
232
  timeZoneOverride?: string;
230
233
  }
234
+ /** Options of a conversation created by a kernel caller (never an HTTP body). */
235
+ export interface NewConversationOptions {
236
+ /**
237
+ * Where it comes from (memory plugin spec §6.2), fixed at creation. Required: a channel or group
238
+ * conversation that fell back to `web` would be offered what only the web and DMs may see.
239
+ */
240
+ source: ConversationSource;
241
+ }
231
242
  /**
232
243
  * 会话:只有所有者可见;每次访问都实时复核 agent 对所有者仍可用(spec 3.6 R5),
233
244
  * 不可用时会话也按不存在处理(`conversation.not_found`)。
@@ -280,9 +291,10 @@ export declare class ConversationsService {
280
291
  * Creates an empty conversation bound to its device (`pick`, see
281
292
  * {@link ConversationDevices.forNew}); an agent the account cannot use →
282
293
  * `agent.not_found`, a device it may not use → `device.not_found` /
283
- * `device.unavailable`, and nothing is created.
294
+ * `device.unavailable`, and nothing is created. `opts.source` records where the conversation
295
+ * comes from.
284
296
  */
285
- create(orgId: string, accountId: string, agentId: string, pick?: NewConversationDevice): Promise<ConversationView>;
297
+ create(orgId: string, accountId: string, agentId: string, pick: NewConversationDevice | undefined, opts: NewConversationOptions): Promise<ConversationView>;
286
298
  /**
287
299
  * Creates a conversation together with its first message (UI refresh B, create on first send):
288
300
  * the conversation row and the `user_message` are written in one transaction, keyed by the
@@ -291,12 +303,12 @@ export declare class ConversationsService {
291
303
  * `agent.not_found` and nothing is written. Queues a run afterwards, like `postUserMessage`.
292
304
  * `events` are every event the create wrote, in seq order from seq 1 (a `model_switched` before
293
305
  * the message when the send switched), so a client can seed the new conversation's log whole.
294
- * `internal` (kernel callers only): a scheduled task's marker, which also sets
295
- * `conversations.scheduled_task_id`, and its zone override.
306
+ * `internal`: the conversation's source and, for kernel callers only, a scheduled task's marker,
307
+ * which also sets `conversations.scheduled_task_id`, and its zone override.
296
308
  */
297
309
  createWithMessage(orgId: string, accountId: string, agentId: string, input: UserMessageInput & {
298
310
  clientMessageId: string;
299
- }, pick?: NewConversationDevice, internal?: InternalMessageOptions): Promise<{
311
+ }, pick: NewConversationDevice | undefined, internal: InternalMessageOptions & NewConversationOptions): Promise<{
300
312
  conversation: ConversationView;
301
313
  event: ConversationEvent;
302
314
  events: ConversationEvent[];