@capybara-hq/kernel 0.1.0-rc.22 → 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 (234) hide show
  1. package/bootstrap.json +25 -25
  2. package/dist/admin/admin.controller.d.ts +3 -3
  3. package/dist/admin/admin.controller.js +6 -2
  4. package/dist/admin/admin.module.d.ts +2 -1
  5. package/dist/admin/admin.module.js +7 -3
  6. package/dist/admin/admin.schemas.d.ts +13 -0
  7. package/dist/admin/outbox-dead.controller.d.ts +23 -0
  8. package/dist/admin/outbox-dead.controller.js +62 -0
  9. package/dist/admin/outbox-dead.schemas.d.ts +8 -0
  10. package/dist/admin/outbox-dead.schemas.js +18 -0
  11. package/dist/admin/outbox-dead.service.d.ts +66 -0
  12. package/dist/admin/outbox-dead.service.js +179 -0
  13. package/dist/admin/overview.service.d.ts +40 -5
  14. package/dist/admin/overview.service.js +41 -7
  15. package/dist/agents/from-template.js +1 -1
  16. package/dist/background/finish.d.ts +5 -2
  17. package/dist/background/finish.js +14 -3
  18. package/dist/background/launch.d.ts +44 -5
  19. package/dist/background/launch.js +92 -9
  20. package/dist/background/session.d.ts +14 -2
  21. package/dist/background/session.js +26 -4
  22. package/dist/background/status.d.ts +1 -1
  23. package/dist/background/status.js +3 -3
  24. package/dist/background/stop.d.ts +0 -2
  25. package/dist/background/stop.js +1 -21
  26. package/dist/background/store.d.ts +8 -0
  27. package/dist/background/store.js +24 -0
  28. package/dist/catalog/admin-catalog.controller.d.ts +21 -2
  29. package/dist/catalog/admin-catalog.controller.js +45 -6
  30. package/dist/catalog/admin-catalog.schemas.d.ts +5 -0
  31. package/dist/catalog/admin-catalog.schemas.js +8 -0
  32. package/dist/catalog/catalog-content.d.ts +5 -0
  33. package/dist/catalog/catalog-content.js +1 -0
  34. package/dist/catalog/catalog.module.d.ts +2 -1
  35. package/dist/catalog/catalog.module.js +5 -1
  36. package/dist/catalog/marketplaces.service.d.ts +1 -1
  37. package/dist/catalog/platform-import-update.d.ts +50 -0
  38. package/dist/catalog/platform-import-update.js +116 -0
  39. package/dist/catalog/plugin-import.d.ts +36 -1
  40. package/dist/catalog/plugin-import.js +23 -1
  41. package/dist/catalog/plugin-upgrade.d.ts +18 -0
  42. package/dist/catalog/plugin-upgrade.js +19 -0
  43. package/dist/catalog/requests.service.js +2 -2
  44. package/dist/channels/channels.module.d.ts +5 -2
  45. package/dist/channels/channels.module.js +9 -2
  46. package/dist/channels/deliver-job.d.ts +25 -0
  47. package/dist/channels/deliver-job.js +25 -0
  48. package/dist/channels/inbound.d.ts +1 -1
  49. package/dist/channels/inbound.js +2 -15
  50. package/dist/channels/jobs.d.ts +28 -18
  51. package/dist/channels/jobs.js +63 -26
  52. package/dist/config/schema.d.ts +3 -6
  53. package/dist/config/schema.js +20 -27
  54. package/dist/conversations/agent-tree.d.ts +16 -0
  55. package/dist/conversations/agent-tree.js +36 -0
  56. package/dist/conversations/conversations.service.d.ts +44 -14
  57. package/dist/conversations/conversations.service.js +52 -38
  58. package/dist/conversations/event-log.d.ts +7 -10
  59. package/dist/conversations/event-log.js +24 -15
  60. package/dist/conversations/events.js +2 -1
  61. package/dist/conversations/queue.service.d.ts +8 -3
  62. package/dist/conversations/queue.service.js +37 -13
  63. package/dist/conversations/titles.d.ts +21 -8
  64. package/dist/conversations/titles.js +42 -15
  65. package/dist/db/kernel-schema.d.ts +26 -8
  66. package/dist/devices/devices.service.d.ts +12 -3
  67. package/dist/devices/devices.service.js +30 -9
  68. package/dist/errors.d.ts +15 -0
  69. package/dist/errors.js +15 -0
  70. package/dist/evals/run-replay.js +14 -8
  71. package/dist/extensions/extensions.module.js +3 -0
  72. package/dist/extensions/lifecycle.d.ts +6 -1
  73. package/dist/extensions/lifecycle.js +9 -2
  74. package/dist/extensions/usage-subscribers.d.ts +43 -0
  75. package/dist/extensions/usage-subscribers.js +108 -0
  76. package/dist/http/api.module.js +2 -0
  77. package/dist/identity/settings.d.ts +53 -3
  78. package/dist/identity/settings.js +92 -9
  79. package/dist/index.d.ts +1 -0
  80. package/dist/index.js +1 -0
  81. package/dist/infra/cli/kernel-cli.module.js +5 -0
  82. package/dist/infra/config/kernel-config.service.d.ts +7 -0
  83. package/dist/infra/config/kernel-config.service.js +20 -0
  84. package/dist/infra/events/kernel-events.d.ts +9 -35
  85. package/dist/infra/outbox/outbox-relay.module.d.ts +6 -0
  86. package/dist/infra/outbox/outbox-relay.module.js +18 -0
  87. package/dist/infra/outbox/outbox.dispatcher.d.ts +103 -0
  88. package/dist/infra/outbox/outbox.dispatcher.js +310 -0
  89. package/dist/infra/outbox/outbox.module.d.ts +24 -0
  90. package/dist/infra/outbox/outbox.module.js +50 -0
  91. package/dist/infra/outbox/outbox.relay.d.ts +31 -0
  92. package/dist/infra/outbox/outbox.relay.js +76 -0
  93. package/dist/infra/outbox/outbox.service.d.ts +51 -0
  94. package/dist/infra/outbox/outbox.service.js +131 -0
  95. package/dist/infra/outbox/registry.d.ts +120 -0
  96. package/dist/infra/outbox/registry.js +108 -0
  97. package/dist/infra/queue/queue-names.d.ts +8 -0
  98. package/dist/infra/queue/queue-names.js +16 -0
  99. package/dist/kernel.module.js +3 -0
  100. package/dist/local-devices/device-policy.service.d.ts +4 -0
  101. package/dist/local-devices/device-policy.service.js +61 -3
  102. package/dist/local-devices/local-device-gateway.d.ts +4 -2
  103. package/dist/local-devices/local-device-gateway.js +39 -14
  104. package/dist/local-devices/machines.d.ts +25 -1
  105. package/dist/local-devices/machines.js +91 -15
  106. package/dist/local-devices/policy-summary.d.ts +44 -1
  107. package/dist/local-devices/policy-summary.js +69 -0
  108. package/dist/models/embeddings.d.ts +9 -9
  109. package/dist/models/embeddings.js +11 -18
  110. package/dist/models/models.module.js +3 -0
  111. package/dist/orgs/orgs.service.js +7 -0
  112. package/dist/orgs/settings.controller.js +2 -2
  113. package/dist/orgs/settings.service.d.ts +11 -5
  114. package/dist/orgs/settings.service.js +28 -14
  115. package/dist/overrides.d.ts +20 -3
  116. package/dist/overrides.js +10 -2
  117. package/dist/platform/exclusions.js +5 -0
  118. package/dist/platform/ops/admin.ops.js +50 -9
  119. package/dist/platform/ops/catalog-admin.ops.d.ts +3 -1
  120. package/dist/platform/ops/catalog-admin.ops.js +32 -4
  121. package/dist/platform/ops/org-settings.ops.js +3 -3
  122. package/dist/platform/ops/orgs.ops.js +1 -0
  123. package/dist/platform/ops/outbox.ops.d.ts +21 -0
  124. package/dist/platform/ops/outbox.ops.js +125 -0
  125. package/dist/platform/ops/platform.ops.js +1 -1
  126. package/dist/platform/ops/schedules.ops.js +40 -1
  127. package/dist/platform/platform-ops.module.d.ts +2 -1
  128. package/dist/platform/platform-ops.module.js +3 -0
  129. package/dist/platform/route-schemas.js +3 -0
  130. package/dist/roles/worker.module.js +9 -4
  131. package/dist/runtime/langgraph-loop.js +13 -5
  132. package/dist/runtime/limits.d.ts +15 -0
  133. package/dist/runtime/limits.js +25 -0
  134. package/dist/runtime/loop.d.ts +8 -3
  135. package/dist/runtime/loop.js +2 -1
  136. package/dist/runtime/maintenance.d.ts +4 -2
  137. package/dist/runtime/maintenance.js +17 -2
  138. package/dist/runtime/model-call.d.ts +14 -16
  139. package/dist/runtime/model-call.js +14 -24
  140. package/dist/runtime/queue-lost-jobs.d.ts +24 -0
  141. package/dist/runtime/queue-lost-jobs.js +51 -0
  142. package/dist/runtime/run-control.d.ts +42 -5
  143. package/dist/runtime/run-control.js +66 -12
  144. package/dist/runtime/run-finish.d.ts +39 -12
  145. package/dist/runtime/run-finish.js +97 -14
  146. package/dist/runtime/run-jobs.d.ts +95 -13
  147. package/dist/runtime/run-jobs.js +199 -28
  148. package/dist/runtime/run-orphan-check.job.d.ts +142 -0
  149. package/dist/runtime/run-orphan-check.job.js +339 -0
  150. package/dist/runtime/run-processors.d.ts +16 -9
  151. package/dist/runtime/run-processors.js +1 -1
  152. package/dist/runtime/run-queue.d.ts +64 -55
  153. package/dist/runtime/run-queue.js +147 -284
  154. package/dist/runtime/run-steps.d.ts +1 -2
  155. package/dist/runtime/run-steps.js +8 -11
  156. package/dist/runtime/run-worker.d.ts +6 -17
  157. package/dist/runtime/run-worker.js +32 -34
  158. package/dist/runtime/runtime.module.d.ts +2 -3
  159. package/dist/runtime/runtime.module.js +5 -6
  160. package/dist/runtime/step-log.d.ts +16 -4
  161. package/dist/runtime/step-log.js +26 -6
  162. package/dist/runtime/stream.controller.d.ts +2 -4
  163. package/dist/runtime/stream.controller.js +5 -11
  164. package/dist/runtime/subagents/child.js +1 -1
  165. package/dist/runtime/subagents/definition.d.ts +2 -1
  166. package/dist/runtime/subagents/definition.js +4 -2
  167. package/dist/runtime/subagents/deliveries.d.ts +6 -3
  168. package/dist/runtime/subagents/deliveries.js +16 -7
  169. package/dist/runtime/subagents/dispatch.d.ts +12 -12
  170. package/dist/runtime/subagents/dispatch.js +13 -38
  171. package/dist/runtime/subagents/messages.d.ts +5 -1
  172. package/dist/runtime/subagents/messages.js +11 -3
  173. package/dist/runtime/subagents/skill-fork.d.ts +3 -3
  174. package/dist/runtime/subagents/skill-fork.js +5 -5
  175. package/dist/runtime/subagents/subagents.service.js +1 -1
  176. package/dist/runtime/subagents/tree-jobs.d.ts +61 -0
  177. package/dist/runtime/subagents/tree-jobs.js +86 -0
  178. package/dist/runtime/usage-report-jobs.d.ts +23 -48
  179. package/dist/runtime/usage-report-jobs.js +67 -119
  180. package/dist/runtime/usage-report.d.ts +28 -77
  181. package/dist/runtime/usage-report.js +35 -213
  182. package/dist/runtime/waits.d.ts +48 -198
  183. package/dist/runtime/waits.js +103 -805
  184. package/dist/sandbox/platform-tiers.d.ts +86 -0
  185. package/dist/sandbox/platform-tiers.js +97 -0
  186. package/dist/sandbox/sandbox.service.d.ts +18 -5
  187. package/dist/sandbox/sandbox.service.js +75 -20
  188. package/dist/sandbox/tiers.d.ts +14 -2
  189. package/dist/sandbox/tiers.js +8 -0
  190. package/dist/schedules/calendar.d.ts +134 -0
  191. package/dist/schedules/calendar.js +410 -0
  192. package/dist/schedules/fire.d.ts +1 -3
  193. package/dist/schedules/fire.js +5 -10
  194. package/dist/schedules/run-tracker.d.ts +0 -5
  195. package/dist/schedules/run-tracker.js +0 -13
  196. package/dist/schedules/scan.job.js +15 -20
  197. package/dist/schedules/schedule-math.d.ts +42 -0
  198. package/dist/schedules/schedule-math.js +52 -0
  199. package/dist/schedules/scheduled-tasks.service.d.ts +16 -0
  200. package/dist/schedules/scheduled-tasks.service.js +53 -0
  201. package/dist/schedules/schedules.controller.d.ts +5 -1
  202. package/dist/schedules/schedules.controller.js +15 -1
  203. package/dist/schedules/schedules.schemas.d.ts +35 -0
  204. package/dist/schedules/schedules.schemas.js +27 -0
  205. package/dist/schedules/store.d.ts +5 -0
  206. package/dist/schedules/store.js +20 -0
  207. package/dist/tools/approvals.service.d.ts +26 -7
  208. package/dist/tools/approvals.service.js +88 -10
  209. package/dist/tools/builtin-names.d.ts +5 -3
  210. package/dist/tools/builtin-names.js +12 -4
  211. package/dist/tools/builtins/builtin-tools.d.ts +1 -2
  212. package/dist/tools/builtins/builtin-tools.js +21 -17
  213. package/dist/tools/builtins/sandbox/bash.js +9 -3
  214. package/dist/tools/builtins/sandbox/common.d.ts +18 -1
  215. package/dist/tools/builtins/sandbox/common.js +14 -1
  216. package/dist/tools/builtins/sandbox/index.d.ts +2 -1
  217. package/dist/tools/builtins/sandbox/index.js +4 -1
  218. package/dist/tools/builtins/sandbox/task-list.d.ts +12 -0
  219. package/dist/tools/builtins/sandbox/task-list.js +130 -0
  220. package/dist/tools/builtins/sandbox/task-output.d.ts +8 -1
  221. package/dist/tools/builtins/sandbox/task-output.js +21 -3
  222. package/dist/tools/builtins/web/extract.d.ts +2 -4
  223. package/dist/tools/builtins/web/extract.js +2 -5
  224. package/dist/tools/pipeline.d.ts +1 -1
  225. package/dist/tools/pipeline.js +1 -1
  226. package/dist/tools/questions.service.d.ts +15 -6
  227. package/dist/tools/questions.service.js +20 -8
  228. package/dist/tools/registry.d.ts +4 -2
  229. package/dist/tools/registry.js +4 -3
  230. package/dist/tools/tools.module.js +4 -3
  231. package/package.json +3 -3
  232. package/src/migrations/0118_device_policy_ops.sql +6 -0
  233. package/src/migrations/0119_outbox.sql +22 -0
  234. package/src/migrations/0120_drop_usage_report_sweep.sql +7 -0
@@ -1,20 +1,17 @@
1
- import type { Logger } from "@capybara-hq/plugin-sdk";
2
- import { type OnApplicationShutdown } from "@nestjs/common";
3
- import type { JobsOptions, Queue } from "bullmq";
4
- import type { KernelSchemaDb } from "../db/kernel-schema.js";
5
- import { KernelConfigService } from "../infra/config/kernel-config.service.js";
1
+ import type { ConversationEvent } from "../conversations/events.js";
2
+ import { UsageSubscribers } from "../extensions/usage-subscribers.js";
6
3
  import { KernelDb } from "../infra/database/kernel-db.js";
7
- import { KernelEvents } from "../infra/events/kernel-events.js";
4
+ import { TransactionHost } from "../infra/database/transaction-host.js";
5
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
8
6
  /**
9
- * The producer half of `usage.report` delivery for `model_usage` rows, and the queue's ids and job
10
- * options shared with the reporter (`usage-report.ts`). The `ctx.models` broker sits below the
11
- * extension host and must not register a queue there (CLI contexts that build the extensions
12
- * module never connect to Redis eagerly), so it only announces a row (`model_usage.recorded`) and
13
- * {@link ModelUsageJobs} enqueues it here.
7
+ * The producer half of `usage.report` delivery, and the queue's ids shared with the reporter
8
+ * (`usage-report.ts`): a delivery is owed in the transaction that writes the usage it reports
9
+ * (transactional outbox spec §8.1). It registers no queue (the outbox dispatcher adds the job), so
10
+ * the `ctx.models` broker below the extension host and the step writes can use it.
14
11
  */
15
12
  /** Queue of `usage.report` deliveries (no `:` in the name). */
16
13
  export declare const USAGE_REPORT_QUEUE = "cap-kernel-usage-report";
17
- /** The job name of every delivery. */
14
+ /** The job name of every delivery (its outbox registry entry holds the job options). */
18
15
  export declare const USAGE_REPORT_JOB = "usage-report";
19
16
  /** One job per `usage` event (no `:`: conversation ids are UUIDs, seq is a number). */
20
17
  export declare const usageReportJobId: (conversationId: string, seq: number) => string;
@@ -42,47 +39,25 @@ export type UsageReportJob = ({
42
39
  */
43
40
  dropped?: Record<string, string>;
44
41
  };
45
- /** First attempt plus 5 retries (F11). */
46
- export declare const USAGE_REPORT_ATTEMPTS = 6;
47
- /** Retry backoff: 2 s, doubling (2, 4, 8, 16, 32 s). */
48
- export declare const USAGE_REPORT_BACKOFF_MS = 2000;
49
- /** The delivery retry policy: attempts and the first exponential backoff. */
50
- export interface UsageReportRetry {
51
- attempts: number;
52
- backoffMs: number;
53
- }
54
- /** Optional provider: the {@link UsageReportRetry} instead of the defaults above (tests). */
55
- export declare const USAGE_REPORT_RETRY: unique symbol;
56
- /** The options of one delivery job: its id, the retry policy and the retention of finished jobs. */
57
- export declare function usageReportJobOpts(jobId: string, retry: UsageReportRetry, keepHours: number): JobsOptions;
58
- /**
59
- * Plugins that subscribe to `usage.report` according to deployment state: an approved permission
60
- * snapshot that declares the `usage.report` hook, for a plugin the deployment still knows: an enabled
61
- * install, or a config-declared plugin with a replica status row that is not `unloaded`. With `orgId`, only plugins
62
- * the organization enabled (org config rows and required bootstrap plugins).
63
- */
64
- export declare function queryUsageSubscribers(db: KernelSchemaDb, orgId?: string): Promise<Set<string>>;
65
42
  /**
66
- * Enqueues the `usage.report` delivery of `model_usage` rows on `model_usage.recorded` (fast path;
67
- * the reporter's sweep re-enqueues rows still unreported, so a lost event or enqueue is caught up).
43
+ * Owes `usage.report` deliveries in the caller's transaction, only while the deployment has a
44
+ * subscriber ({@link UsageSubscribers}): usage written while no plugin subscribes is never
45
+ * reported.
68
46
  */
69
- export declare class ModelUsageJobs implements OnApplicationShutdown {
47
+ export declare class UsageReportJobs {
70
48
  private readonly kdb;
71
- private readonly queue;
72
- private readonly config;
73
- private readonly log;
74
- private readonly retry;
75
- private readonly unsubscribe;
76
- /** Set at shutdown: a late event enqueues nothing (the sweep offers the row later). */
77
- private closed;
78
- constructor(kdb: KernelDb, queue: Queue<UsageReportJob>, config: Pick<KernelConfigService, "get">, log: Logger, retry: UsageReportRetry | undefined, events: KernelEvents);
79
- onApplicationShutdown(): void;
49
+ private readonly outbox;
50
+ private readonly subscribers;
51
+ readonly txHost: TransactionHost;
52
+ constructor(kdb: KernelDb, outbox: Pick<OutboxService, "add">, subscribers: Pick<UsageSubscribers, "any">, txHost: TransactionHost);
80
53
  /**
81
- * Enqueues the delivery of one row (job `usage-model-<rowId>`). Without any subscriber nothing is
82
- * enqueued (the sweep marks the row). Never throws: a failure only logs; the sweep catches up.
54
+ * Owes the delivery of every `usage` event among `events` (the events this transaction wrote
55
+ * to the conversation's log), job `usage-<conversationId>-<seq>`.
83
56
  */
84
- enqueue(usageId: string): Promise<void>;
57
+ oweEvents(conversationId: string, events: readonly ConversationEvent[]): Promise<void>;
58
+ /** Owes the delivery of one `model_usage` row (job `usage-model-<rowId>`). */
59
+ oweModelUsage(usageId: string, orgId: string): Promise<void>;
85
60
  }
86
- /** The usage report queue and its `model_usage` producer (no dependency on the extension host). */
61
+ /** The usage report producer (no queue, no dependency on the extension host). */
87
62
  export declare class UsageReportJobsModule {
88
63
  }
@@ -7,152 +7,100 @@ 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 } from "@nestjs/bullmq";
11
- import { Inject, Injectable, Module, Optional } from "@nestjs/common";
12
- import { sql } from "kysely";
13
- import { enabledExtensionsQuery } from "../extensions/enabled.js";
14
- import { KernelConfigService } from "../infra/config/kernel-config.service.js";
10
+ import { Inject, Injectable, Module } from "@nestjs/common";
11
+ import { UsageSubscribers, UsageSubscribersModule } from "../extensions/usage-subscribers.js";
15
12
  import { KernelDb } from "../infra/database/kernel-db.js";
16
- import { KernelEvents } from "../infra/events/kernel-events.js";
17
- import { QueueModule } from "../infra/queue/queue.module.js";
18
- import { KERNEL_LOGGER } from "../infra/tokens.js";
13
+ import { TransactionHost } from "../infra/database/transaction-host.js";
14
+ import { Transactional } from "../infra/database/transactional.decorator.js";
15
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
19
16
  /**
20
- * The producer half of `usage.report` delivery for `model_usage` rows, and the queue's ids and job
21
- * options shared with the reporter (`usage-report.ts`). The `ctx.models` broker sits below the
22
- * extension host and must not register a queue there (CLI contexts that build the extensions
23
- * module never connect to Redis eagerly), so it only announces a row (`model_usage.recorded`) and
24
- * {@link ModelUsageJobs} enqueues it here.
17
+ * The producer half of `usage.report` delivery, and the queue's ids shared with the reporter
18
+ * (`usage-report.ts`): a delivery is owed in the transaction that writes the usage it reports
19
+ * (transactional outbox spec §8.1). It registers no queue (the outbox dispatcher adds the job), so
20
+ * the `ctx.models` broker below the extension host and the step writes can use it.
25
21
  */
26
22
  /** Queue of `usage.report` deliveries (no `:` in the name). */
27
23
  export const USAGE_REPORT_QUEUE = "cap-kernel-usage-report";
28
- /** The job name of every delivery. */
24
+ /** The job name of every delivery (its outbox registry entry holds the job options). */
29
25
  export const USAGE_REPORT_JOB = "usage-report";
30
26
  /** One job per `usage` event (no `:`: conversation ids are UUIDs, seq is a number). */
31
27
  export const usageReportJobId = (conversationId, seq) => `usage-${conversationId}-${seq}`;
32
28
  /** One job per `model_usage` row (spec B §5.4; row ids are UUIDs, so no clash with the above). */
33
29
  export const modelUsageJobId = (usageId) => `usage-model-${usageId}`;
34
- /** First attempt plus 5 retries (F11). */
35
- export const USAGE_REPORT_ATTEMPTS = 6;
36
- /** Retry backoff: 2 s, doubling (2, 4, 8, 16, 32 s). */
37
- export const USAGE_REPORT_BACKOFF_MS = 2000;
38
- /** Optional provider: the {@link UsageReportRetry} instead of the defaults above (tests). */
39
- export const USAGE_REPORT_RETRY = Symbol("USAGE_REPORT_RETRY");
40
- /** Failed jobs (given up) are kept for inspection, at most {@link KEEP_FAILED_MAX} of them. */
41
- const KEEP_FAILED_S = 7 * 86_400;
42
- /** Upper bound on kept failed jobs (I-2): a long outage of a billing endpoint cannot grow Redis. */
43
- const KEEP_FAILED_MAX = 100_000;
44
30
  /**
45
- * Upper bound on kept finished jobs (Redis memory). At the default retention it only bites above
46
- * ~1.1 usage events per second sustained for a day; dedupe needs the jobs only for the sweeper overlap.
31
+ * Owes `usage.report` deliveries in the caller's transaction, only while the deployment has a
32
+ * subscriber ({@link UsageSubscribers}): usage written while no plugin subscribes is never
33
+ * reported.
47
34
  */
48
- const KEEP_COMPLETED_MAX = 100_000;
49
- /** The options of one delivery job: its id, the retry policy and the retention of finished jobs. */
50
- export function usageReportJobOpts(jobId, retry, keepHours) {
51
- return {
52
- jobId,
53
- attempts: retry.attempts,
54
- backoff: { type: "exponential", delay: retry.backoffMs },
55
- removeOnComplete: { age: keepHours * 3600, count: KEEP_COMPLETED_MAX },
56
- removeOnFail: { age: KEEP_FAILED_S, count: KEEP_FAILED_MAX },
57
- };
58
- }
59
- /**
60
- * Plugins that subscribe to `usage.report` according to deployment state: an approved permission
61
- * snapshot that declares the `usage.report` hook, for a plugin the deployment still knows: an enabled
62
- * install, or a config-declared plugin with a replica status row that is not `unloaded`. With `orgId`, only plugins
63
- * the organization enabled (org config rows and required bootstrap plugins).
64
- */
65
- export async function queryUsageSubscribers(db, orgId) {
66
- let q = db
67
- .selectFrom("extension_permission_snapshots as s")
68
- .select("s.plugin")
69
- .where(sql `s.permissions -> 'permissions' -> 'hooks' ? 'usage.report'`)
70
- .where((eb) => eb.or([
71
- eb.exists(eb
72
- .selectFrom("extension_installs as i")
73
- .select(sql `1`.as("one"))
74
- .whereRef("i.name", "=", "s.plugin")
75
- .where("i.desired_state", "=", "enabled")),
76
- // config-declared plugins still declared (and not unloaded) on some replica; installs are
77
- // covered by desired_state above (their replica rows linger as `unloaded` when disabled)
78
- eb.exists(eb
79
- .selectFrom("extension_replica_status as r")
80
- .select(sql `1`.as("one"))
81
- .whereRef("r.plugin", "=", "s.plugin")
82
- .where("r.source", "=", "config")
83
- .where("r.state", "<>", "unloaded")),
84
- ]));
85
- if (orgId !== undefined) {
86
- q = q.where("s.plugin", "in", enabledExtensionsQuery(db, orgId));
87
- }
88
- return new Set((await q.execute()).map((r) => r.plugin));
89
- }
90
- /**
91
- * Enqueues the `usage.report` delivery of `model_usage` rows on `model_usage.recorded` (fast path;
92
- * the reporter's sweep re-enqueues rows still unreported, so a lost event or enqueue is caught up).
93
- */
94
- let ModelUsageJobs = class ModelUsageJobs {
35
+ let UsageReportJobs = class UsageReportJobs {
95
36
  kdb;
96
- queue;
97
- config;
98
- log;
99
- retry;
100
- unsubscribe;
101
- /** Set at shutdown: a late event enqueues nothing (the sweep offers the row later). */
102
- closed = false;
103
- constructor(kdb, queue, config, log, retry = {
104
- attempts: USAGE_REPORT_ATTEMPTS,
105
- backoffMs: USAGE_REPORT_BACKOFF_MS,
106
- }, events) {
37
+ outbox;
38
+ subscribers;
39
+ txHost;
40
+ constructor(kdb, outbox, subscribers, txHost) {
107
41
  this.kdb = kdb;
108
- this.queue = queue;
109
- this.config = config;
110
- this.log = log;
111
- this.retry = retry;
112
- this.unsubscribe = events.on("model_usage.recorded", ({ id }) => this.enqueue(id));
113
- }
114
- onApplicationShutdown() {
115
- this.closed = true;
116
- this.unsubscribe();
42
+ this.outbox = outbox;
43
+ this.subscribers = subscribers;
44
+ this.txHost = txHost;
117
45
  }
118
46
  /**
119
- * Enqueues the delivery of one row (job `usage-model-<rowId>`). Without any subscriber nothing is
120
- * enqueued (the sweep marks the row). Never throws: a failure only logs; the sweep catches up.
47
+ * Owes the delivery of every `usage` event among `events` (the events this transaction wrote
48
+ * to the conversation's log), job `usage-<conversationId>-<seq>`.
121
49
  */
122
- async enqueue(usageId) {
123
- if (this.closed)
50
+ async oweEvents(conversationId, events) {
51
+ const usage = events.filter((e) => e.type === "usage");
52
+ if (usage.length === 0 || !(await this.subscribers.any()))
124
53
  return;
125
- try {
126
- if ((await queryUsageSubscribers(this.kdb.db)).size === 0)
127
- return;
128
- await this.queue.add(USAGE_REPORT_JOB, { usageId }, usageReportJobOpts(modelUsageJobId(usageId), this.retry, this.config.get().hooks.usageReportKeepHours));
129
- }
130
- catch (err) {
131
- this.log.warn("cannot enqueue model usage report", { usageId, err });
54
+ const conv = await this.kdb.db
55
+ .selectFrom("conversations")
56
+ .select("org_id")
57
+ .where("id", "=", conversationId)
58
+ .executeTakeFirst();
59
+ for (const e of usage) {
60
+ await this.outbox.add({
61
+ queue: USAGE_REPORT_QUEUE,
62
+ name: USAGE_REPORT_JOB,
63
+ jobId: usageReportJobId(conversationId, e.seq),
64
+ data: { conversationId, seq: e.seq },
65
+ ...(conv !== undefined && { orgId: conv.org_id }),
66
+ });
132
67
  }
133
68
  }
69
+ /** Owes the delivery of one `model_usage` row (job `usage-model-<rowId>`). */
70
+ async oweModelUsage(usageId, orgId) {
71
+ if (!(await this.subscribers.any()))
72
+ return;
73
+ await this.outbox.add({
74
+ queue: USAGE_REPORT_QUEUE,
75
+ name: USAGE_REPORT_JOB,
76
+ jobId: modelUsageJobId(usageId),
77
+ data: { usageId },
78
+ orgId,
79
+ });
80
+ }
134
81
  };
135
- ModelUsageJobs = __decorate([
82
+ __decorate([
83
+ Transactional({ propagation: "mandatory" })
84
+ ], UsageReportJobs.prototype, "oweEvents", null);
85
+ __decorate([
86
+ Transactional({ propagation: "mandatory" })
87
+ ], UsageReportJobs.prototype, "oweModelUsage", null);
88
+ UsageReportJobs = __decorate([
136
89
  Injectable(),
137
90
  __param(0, Inject(KernelDb)),
138
- __param(1, Inject(getQueueToken(USAGE_REPORT_QUEUE))),
139
- __param(2, Inject(KernelConfigService)),
140
- __param(3, Inject(KERNEL_LOGGER)),
141
- __param(4, Optional()),
142
- __param(4, Inject(USAGE_REPORT_RETRY)),
143
- __param(5, Inject(KernelEvents))
144
- ], ModelUsageJobs);
145
- export { ModelUsageJobs };
146
- /** The usage report queue's registration, shared by the producer and the reporter's module. */
147
- const USAGE_REPORT_QUEUE_MODULE = QueueModule.registerQueue(USAGE_REPORT_QUEUE);
148
- /** The usage report queue and its `model_usage` producer (no dependency on the extension host). */
91
+ __param(1, Inject(OutboxService)),
92
+ __param(2, Inject(UsageSubscribers)),
93
+ __param(3, Inject(TransactionHost))
94
+ ], UsageReportJobs);
95
+ export { UsageReportJobs };
96
+ /** The usage report producer (no queue, no dependency on the extension host). */
149
97
  let UsageReportJobsModule = class UsageReportJobsModule {
150
98
  };
151
99
  UsageReportJobsModule = __decorate([
152
100
  Module({
153
- imports: [USAGE_REPORT_QUEUE_MODULE],
154
- providers: [ModelUsageJobs],
155
- exports: [ModelUsageJobs, USAGE_REPORT_QUEUE_MODULE],
101
+ imports: [UsageSubscribersModule],
102
+ providers: [UsageReportJobs],
103
+ exports: [UsageReportJobs],
156
104
  })
157
105
  ], UsageReportJobsModule);
158
106
  export { UsageReportJobsModule };
@@ -5,21 +5,9 @@ import { ExtensionHost } from "../extensions/host.js";
5
5
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
6
6
  import { KernelDb } from "../infra/database/kernel-db.js";
7
7
  import { LockService } from "../infra/lock/lock.service.js";
8
- import type { KernelLogger } from "../log.js";
9
- import { type UsageReportJob, type UsageReportRetry } from "./usage-report-jobs.js";
10
- export { ModelUsageJobs, modelUsageJobId, queryUsageSubscribers, USAGE_REPORT_ATTEMPTS, USAGE_REPORT_BACKOFF_MS, USAGE_REPORT_QUEUE, USAGE_REPORT_RETRY, type UsageReportJob, UsageReportJobsModule, type UsageReportRetry, usageReportJobId, } from "./usage-report-jobs.js";
11
- /** The sweeper's cron job name (no `:`; every minute, once across replicas). */
12
- export declare const USAGE_SWEEP_JOB = "usage-report-sweep";
13
- /** The maintenance scheduler the sweep ran on before it became a `@LockedCron` (retired). */
14
- export { USAGE_SWEEP_SCHEDULER_ID } from "./job-ids.js";
15
- /** The sweep leaves the newest events to the fast path (and to transactions still committing). */
16
- export declare const USAGE_SWEEP_LAG_MS = 30000;
17
- /**
18
- * Each sweep re-scans this far behind the watermark: events whose transaction committed after a
19
- * previous sweep passed their `created_at` are still found. Far inside the dedupe window
20
- * (`hooks.usageReportKeepHours` ≥ 1 h), so re-enqueued events dedupe on the kept job.
21
- */
22
- export declare const USAGE_SWEEP_OVERLAP_MS: number;
8
+ import { type UsageReportJob } from "./usage-report-jobs.js";
9
+ export { queryUsageSubscribers } from "../extensions/usage-subscribers.js";
10
+ export { modelUsageJobId, USAGE_REPORT_JOB, USAGE_REPORT_QUEUE, type UsageReportJob, UsageReportJobs, UsageReportJobsModule, usageReportJobId, } from "./usage-report-jobs.js";
23
11
  /**
24
12
  * Of `plugins`, those that are **definitely** unservable right now (I-2, N-3): live replicas report
25
13
  * them (a `extension_replica_status` row with a heartbeat newer than `staleSeconds`), no live row is
@@ -35,14 +23,16 @@ export declare function queryUnservable(db: KernelSchemaDb, plugins: readonly st
35
23
  */
36
24
  export declare function narrowPending(stored: readonly string[] | undefined, failed: readonly string[]): string[];
37
25
  /**
38
- * Reliable `usage.report` delivery (F11, P-17, R13). Every `usage` event written to the event log (chat
39
- * runs and full-run eval replays) is delivered at least once to every subscribing plugin.
40
- * - **Fast path:** `model-call` enqueues job `usage-<conversationId>-<seq>` right after writing the event
41
- * (never awaited; an enqueue failure only logs). Skipped without I/O when this replica has no
42
- * `usage.report` handler (M-2); the sweeper then offers the event.
43
- * - **Sweeper (R13):** a maintenance job (once across replicas, every minute) re-enqueues the `usage`
44
- * events in `[watermark - 10 min, now - 30 s]` with the same job ids, so a lost fast-path enqueue is
45
- * caught up and duplicates dedupe on the kept job (`hooks.usageReportKeepHours`).
26
+ * Reliable `usage.report` delivery (F11, P-17, transactional outbox spec §8.1). Every `usage` event
27
+ * written to the event log (chat runs and full-run eval replays) is delivered at least once to every
28
+ * subscribing plugin.
29
+ * - **Owed with the usage:** the transaction that writes a `usage` event (`StepLog`'s step writes, a
30
+ * title's writes) or a `model_usage` row also writes the outbox row of job
31
+ * `usage-<conversationId>-<seq>` / `usage-model-<rowId>` (`UsageReportJobs`), while the deployment
32
+ * has at least one subscriber (`UsageSubscribers`: from PG, whatever this replica loaded; cached
33
+ * 30 s per replica, cleared on `PLUGINS_CHANGED`). The outbox fast path adds the job after the
34
+ * commit and the relay catches up what it did not. Usage written while no plugin subscribed is
35
+ * never reported: a subscription starts when the plugin is enabled.
46
36
  * - **Worker** (worker role, `hooks.usageReportConcurrency` per replica): reads the payload from the log
47
37
  * at execution time (the `usage` event, the conversation with `kind`, the run). On the first attempt
48
38
  * the targets are the deployment's subscribers the organization enabled (I-2: not this replica's
@@ -54,49 +44,45 @@ export declare function narrowPending(stored: readonly string[] | undefined, fai
54
44
  * once (I-2). A drop (N-3) is never silent: after delivering to the other targets the job ends
55
45
  * failed with an `UnrecoverableError` whose failedReason names the dropped plugins, plus a
56
46
  * throttled warning. Plugins the organization disabled by delivery time are dropped silently (not
57
- * targets any more). The final failure logs an error (no audit); failed jobs are kept 7 days, at
58
- * most 100 000. Each call runs through the instance's `inflight.run`, limited by `hooks.timeoutMs`;
59
- * when this replica stops, in-flight calls are aborted and the job goes back to the queue without
60
- * using up an attempt.
47
+ * targets any more). The final failure logs an error (no audit); finished jobs are kept a day and
48
+ * failed jobs 7 days, at most 100 000 each (the outbox registry's `usage-report` options). Each
49
+ * call runs through the instance's `inflight.run`, limited by `hooks.timeoutMs`; when this
50
+ * replica stops, in-flight calls are aborted and the job goes back to the queue without using up
51
+ * an attempt.
61
52
  * Handlers must be idempotent by `eventId` (`<conversationId>/<seq>`, or a `model_usage` row id).
62
53
  * Nothing is written to the event log (never model-visible). Compaction calls log usage too (`purpose: "compaction"`) and are reported,
63
54
  * as are conversation title calls (`purpose: "title"`, no run: `runId` is "") and `WebFetch`
64
55
  * extraction calls (`purpose: "web_fetch"`).
65
56
  * Single-step eval replays and judge requests log no usage: not reported (M7).
66
57
  * - **Model usage outside conversations** (spec B §5.4): each `ctx.models.embed` call writes a
67
- * `model_usage` row; on its `model_usage.recorded` event `ModelUsageJobs` (`usage-report-jobs.ts`)
68
- * enqueues job `usage-model-<rowId>` (only while some plugin subscribes); the payload
58
+ * `model_usage` row and owes job `usage-model-<rowId>` with it; the payload
69
59
  * (`purpose: "embedding"`, `eventId` = the row id, the calling `extension`, no conversation fields)
70
60
  * is read from the row when the job runs, and the row's `reported_at` is set once nothing is left
71
- * to deliver; a job that gives up after its last attempt sets `report_failed_at`. The sweep
72
- * re-enqueues recent rows that are neither (see {@link sweep}).
61
+ * to deliver; a job that gives up after its last attempt sets `report_failed_at`.
73
62
  */
74
63
  export declare class UsageReporter {
75
64
  private readonly kdb;
76
65
  private readonly queue;
77
66
  /**
78
67
  * The host's hook registry: handlers of the current local plugin generations (through
79
- * inflight.run), and `has("usage.report")`, the in-process check that lets `enqueue` skip all
80
- * I/O without handlers; and whether a plugin's current instance is active here.
68
+ * inflight.run); and whether a plugin's current instance is active here.
81
69
  */
82
70
  private readonly host;
83
71
  private readonly config;
84
72
  /** The warning throttle's markers (`capk:usage-warn:<plugin>:<kind>`, SET NX PX). */
85
73
  private readonly locks;
86
74
  private readonly log;
87
- private readonly retry;
88
75
  private worker;
89
76
  private closed;
90
77
  private readonly stopping;
91
78
  constructor(kdb: KernelDb, queue: Queue<UsageReportJob>,
92
79
  /**
93
80
  * The host's hook registry: handlers of the current local plugin generations (through
94
- * inflight.run), and `has("usage.report")`, the in-process check that lets `enqueue` skip all
95
- * I/O without handlers; and whether a plugin's current instance is active here.
81
+ * inflight.run); and whether a plugin's current instance is active here.
96
82
  */
97
83
  host: Pick<ExtensionHost, "hooks" | "current">, config: Pick<KernelConfigService, "get">,
98
84
  /** The warning throttle's markers (`capk:usage-warn:<plugin>:<kind>`, SET NX PX). */
99
- locks: Pick<LockService, "lease">, log: Logger, retry?: UsageReportRetry);
85
+ locks: Pick<LockService, "lease">, log: Logger);
100
86
  private get db();
101
87
  private get hooks();
102
88
  /**
@@ -116,30 +102,6 @@ export declare class UsageReporter {
116
102
  * See {@link queryUsageSubscribers}.
117
103
  */
118
104
  subscribers(orgId?: string): Promise<Set<string>>;
119
- private jobOpts;
120
- /**
121
- * Enqueues the delivery of one `usage` event. Returns without a job when no plugin subscribes.
122
- * Never throws: an enqueue failure (e.g. a Redis hiccup) only logs a warning; the sweeper catches up.
123
- */
124
- enqueue(conversationId: string, seq: number): Promise<void>;
125
- /**
126
- * One sweep (R13): re-enqueues the `usage` events created in `[watermark - overlap, now - lag]` in
127
- * pages, then advances the watermark to `now - lag`. With no subscriber it only advances. The first
128
- * sweep starts at `now - lag - overlap`. Also re-enqueues the `model_usage` rows of the same
129
- * window that are neither reported nor given up (spec B §5.4),
130
- * or, with no subscriber, marks those rows reported (nobody to deliver to). Returns the number of
131
- * events and rows offered to the queue.
132
- */
133
- sweep(opts?: {
134
- now?: number;
135
- }): Promise<number>;
136
- /**
137
- * The `model_usage` half of {@link sweep}: rows neither reported nor given up that occurred in
138
- * the same window as the events (`[watermark - overlap, to]`), oldest first. Bounded like the
139
- * event sweep, so a row whose job failed for good is never offered again, even after its failed
140
- * job left Redis (no repeated backlog, no second delivery to plugins already served).
141
- */
142
- private sweepModelUsage;
143
105
  /**
144
106
  * Starts consuming (worker role only): `worker` is the `@Processor` worker of
145
107
  * {@link USAGE_REPORT_QUEUE} (built with the run queue's job lock and stalled detection).
@@ -150,8 +112,8 @@ export declare class UsageReporter {
150
112
  /** A job failed (the worker's `failed` event): logs retries and the final failure. */
151
113
  onFailed(j: Job<UsageReportJob> | undefined, err: Error): void;
152
114
  /**
153
- * Stops: no more enqueues, in-flight calls are aborted (their jobs requeued) and the worker
154
- * closes. The queue itself closes with the application.
115
+ * Stops: in-flight calls are aborted (their jobs requeued) and the worker closes. The queue
116
+ * itself closes with the application.
155
117
  */
156
118
  stop(): Promise<void>;
157
119
  /** The payload of a job: a `usage` event's or a `model_usage` row's; null when it is gone. */
@@ -163,7 +125,7 @@ export declare class UsageReporter {
163
125
  private payloadOfUsage;
164
126
  /** A `model_usage` job is done (delivered, or nothing left to deliver to): marks the row. */
165
127
  private markReported;
166
- /** A `model_usage` job gave up after its last attempt: the sweep never offers the row again. */
128
+ /** A `model_usage` job gave up after its last attempt: the row records it. */
167
129
  private markFailed;
168
130
  /** The payload of one usage event, read from the log; null when the event no longer exists. */
169
131
  private payloadOf;
@@ -183,19 +145,8 @@ export declare class UsageReporter {
183
145
  private call;
184
146
  }
185
147
  /**
186
- * The usage sweeper (R13), every minute, once across replicas: {@link UsageReporter.sweep}.
187
- * Replaces the maintenance scheduler `usage-report-sweep-every` (retired at worker start).
188
- */
189
- export declare class UsageSweepJob {
190
- private readonly reporter;
191
- readonly lockService: LockService;
192
- readonly log: KernelLogger;
193
- constructor(reporter: Pick<UsageReporter, "sweep">, lockService: LockService, log: KernelLogger);
194
- run(): Promise<void>;
195
- }
196
- /**
197
- * `usage.report` delivery: the producer on every role; the consumer and the sweep ({@link UsageSweepJob})
198
- * on the worker role (`WorkerModule`).
148
+ * `usage.report` delivery: the producer (`UsageReportJobs`) and the queue on every role; the
149
+ * consumer's `@Processor` on the worker role (`WorkerModule`).
199
150
  */
200
151
  export declare class UsageReportModule {
201
152
  }