@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
@@ -0,0 +1,120 @@
1
+ import type { JobsOptions } from "bullmq";
2
+ import type { KernelQueueName } from "../queue/queue-names.js";
3
+ /** What the registry says about one job name added through the outbox (spec §3.5, §5). */
4
+ export interface OutboxJobSpec {
5
+ /** The kernel queue the job is added to, or the queues when one job name serves several. */
6
+ queue: KernelQueueName | readonly KernelQueueName[];
7
+ /** One sentence: why a duplicate delivery of this job is harmless (state check, key, id). */
8
+ idempotent: string;
9
+ /** Repo-relative path of the file whose `@Processor` handles the job. */
10
+ processedIn: string;
11
+ /**
12
+ * BullMQ options the dispatcher adds the job with over the queue defaults
13
+ * (`QUEUE_JOB_DEFAULTS`); the job id and the delay always come from the row.
14
+ */
15
+ opts?: Pick<JobsOptions, "attempts" | "backoff" | "priority" | "removeOnComplete" | "removeOnFail">;
16
+ }
17
+ /**
18
+ * Every job name added through the outbox. The task that introduces a job name adds its entry; a
19
+ * name without one does not type-check as an `OutboxJobName` (`add` does not check it at runtime).
20
+ */
21
+ export declare const OUTBOX_JOBS: {
22
+ readonly run: {
23
+ readonly queue: readonly ["cap-kernel-runs", "cap-kernel-subagent-runs"];
24
+ readonly idempotent: "the run worker starts only a run in `queued` state for this job's runId; a finished run's job only repeats the idempotent post-run ensureRun, a waiting run's does nothing";
25
+ readonly processedIn: "packages/kernel/src/runtime/run-processors.ts";
26
+ readonly opts: {
27
+ readonly attempts: 1;
28
+ readonly removeOnComplete: true;
29
+ readonly removeOnFail: true;
30
+ };
31
+ };
32
+ readonly "ensure-run": {
33
+ readonly queue: "cap-kernel-maintenance";
34
+ readonly idempotent: "ensureRun decides from the conversation's state; with nothing unconsumed it is a no-op";
35
+ readonly processedIn: "packages/kernel/src/runtime/maintenance.ts";
36
+ readonly opts: {
37
+ readonly removeOnComplete: true;
38
+ readonly removeOnFail: true;
39
+ };
40
+ };
41
+ readonly "wait-timeout": {
42
+ readonly queue: "cap-kernel-maintenance";
43
+ readonly idempotent: "a timeout acts only on a still-pending wait (a row lock and status check) and a resume starts only a waiting run with nothing else pending";
44
+ readonly processedIn: "packages/kernel/src/runtime/maintenance.ts";
45
+ readonly opts: {
46
+ readonly attempts: 5;
47
+ readonly backoff: {
48
+ readonly type: "exponential";
49
+ readonly delay: 5000;
50
+ };
51
+ readonly removeOnComplete: true;
52
+ readonly removeOnFail: true;
53
+ };
54
+ };
55
+ readonly "stop-tree": {
56
+ readonly queue: "cap-kernel-maintenance";
57
+ readonly idempotent: "stopping a stopped run is a no-op";
58
+ readonly processedIn: "packages/kernel/src/runtime/maintenance.ts";
59
+ readonly opts: {
60
+ readonly attempts: 5;
61
+ readonly backoff: {
62
+ readonly type: "exponential";
63
+ readonly delay: 5000;
64
+ };
65
+ readonly removeOnComplete: true;
66
+ readonly removeOnFail: true;
67
+ };
68
+ };
69
+ readonly "deliver-to-parent": {
70
+ readonly queue: "cap-kernel-maintenance";
71
+ readonly idempotent: "the delivery's idempotency key is the child run id";
72
+ readonly processedIn: "packages/kernel/src/runtime/maintenance.ts";
73
+ readonly opts: {
74
+ readonly attempts: 5;
75
+ readonly backoff: {
76
+ readonly type: "exponential";
77
+ readonly delay: 5000;
78
+ };
79
+ readonly removeOnComplete: true;
80
+ readonly removeOnFail: true;
81
+ };
82
+ };
83
+ readonly "usage-report": {
84
+ readonly queue: "cap-kernel-usage-report";
85
+ readonly idempotent: "handlers are idempotent by eventId (the event's `<conversationId>/<seq>` or the model_usage row id), and the job's `pending` list keeps a retry from reaching plugins already served";
86
+ readonly processedIn: "packages/kernel/src/runtime/run-processors.ts";
87
+ readonly opts: {
88
+ readonly attempts: 6;
89
+ readonly backoff: {
90
+ readonly type: "exponential";
91
+ readonly delay: 2000;
92
+ };
93
+ readonly removeOnComplete: {
94
+ readonly age: number;
95
+ readonly count: 100000;
96
+ };
97
+ readonly removeOnFail: {
98
+ readonly age: number;
99
+ readonly count: 100000;
100
+ };
101
+ };
102
+ };
103
+ readonly deliver: {
104
+ readonly queue: "cap-kernel-channels";
105
+ readonly idempotent: "a delivery resumes from the conversation's delivered seq under a per-conversation lock and sends with idempotency keys, so a second job sends nothing twice";
106
+ readonly processedIn: "packages/kernel/src/channels/channels.module.ts";
107
+ readonly opts: {
108
+ readonly attempts: 8;
109
+ readonly backoff: {
110
+ readonly type: "deliver";
111
+ };
112
+ readonly priority: 10;
113
+ readonly removeOnComplete: true;
114
+ readonly removeOnFail: true;
115
+ };
116
+ };
117
+ };
118
+ export type OutboxJobName = keyof typeof OUTBOX_JOBS;
119
+ /** The registry's BullMQ options of a job name (none for a name it does not list). */
120
+ export declare function outboxJobOptions(name: string): OutboxJobSpec["opts"];
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Every job name added through the outbox. The task that introduces a job name adds its entry; a
3
+ * name without one does not type-check as an `OutboxJobName` (`add` does not check it at runtime).
4
+ */
5
+ export const OUTBOX_JOBS = {
6
+ // `runtime/run-jobs.ts` RUN_JOB: one name on both run queues (a sub-agent's runs go to the
7
+ // sub-agent queue), under the job ids `runJobId` builds and, for a waiting run's resume
8
+ // (`RunResume.resumeIn`), `resumeJobId`
9
+ run: {
10
+ queue: ["cap-kernel-runs", "cap-kernel-subagent-runs"],
11
+ idempotent: "the run worker starts only a run in `queued` state for this job's runId; a finished run's job only repeats the idempotent post-run ensureRun, a waiting run's does nothing",
12
+ processedIn: "packages/kernel/src/runtime/run-processors.ts",
13
+ // one attempt (the run's own paths handle failures); removed once done, so the next run of the
14
+ // same trigger can take the id again (the post-run ensureRun, R34)
15
+ opts: { attempts: 1, removeOnComplete: true, removeOnFail: true },
16
+ },
17
+ // `runtime/run-jobs.ts` ENSURE_RUN_JOB: the ensure-run backstop owed with every message, queue
18
+ // change, background task finish, sub-agent delivery and run end (spec §6), under the job ids
19
+ // `ensureRunJobId` builds
20
+ "ensure-run": {
21
+ queue: "cap-kernel-maintenance",
22
+ idempotent: "ensureRun decides from the conversation's state; with nothing unconsumed it is a no-op",
23
+ processedIn: "packages/kernel/src/runtime/maintenance.ts",
24
+ // removed once done (the queue defaults keep finished jobs), so a later write under the same
25
+ // seq (a queue change writes no event of its own) owes a job that is not deduped away
26
+ opts: { removeOnComplete: true, removeOnFail: true },
27
+ },
28
+ // `runtime/waits.ts` WAIT_TIMEOUT_JOB: owed with the suspend that sets a run `waiting`, under
29
+ // `waitTimeoutJobId`, with `runAt` = the wait's deadline (spec §6 "Resumes and timeouts")
30
+ "wait-timeout": {
31
+ queue: "cap-kernel-maintenance",
32
+ idempotent: "a timeout acts only on a still-pending wait (a row lock and status check) and a resume starts only a waiting run with nothing else pending",
33
+ processedIn: "packages/kernel/src/runtime/maintenance.ts",
34
+ // retried while PG is briefly unavailable; removed once done
35
+ opts: {
36
+ attempts: 5,
37
+ backoff: { type: "exponential", delay: 5_000 },
38
+ removeOnComplete: true,
39
+ removeOnFail: true,
40
+ },
41
+ },
42
+ // `runtime/subagents/tree-jobs.ts` STOP_TREE_JOB: owed by a run's terminal transaction that
43
+ // leaves an active fork child (or a person-stopped sub-agent's descendants), under
44
+ // `stopChildrenJobId`, and by a root's delete, under `stopDeletedTreeJobId` (spec §6)
45
+ "stop-tree": {
46
+ queue: "cap-kernel-maintenance",
47
+ idempotent: "stopping a stopped run is a no-op",
48
+ processedIn: "packages/kernel/src/runtime/maintenance.ts",
49
+ // retried while PG is briefly unavailable; removed once done
50
+ opts: {
51
+ attempts: 5,
52
+ backoff: { type: "exponential", delay: 5_000 },
53
+ removeOnComplete: true,
54
+ removeOnFail: true,
55
+ },
56
+ },
57
+ // `runtime/subagents/tree-jobs.ts` DELIVER_TO_PARENT_JOB: owed by a sub-agent run's terminal
58
+ // transaction, under `deliverToParentJobId` (spec §6)
59
+ "deliver-to-parent": {
60
+ queue: "cap-kernel-maintenance",
61
+ idempotent: "the delivery's idempotency key is the child run id",
62
+ processedIn: "packages/kernel/src/runtime/maintenance.ts",
63
+ opts: {
64
+ attempts: 5,
65
+ backoff: { type: "exponential", delay: 5_000 },
66
+ removeOnComplete: true,
67
+ removeOnFail: true,
68
+ },
69
+ },
70
+ // `runtime/usage-report-jobs.ts` USAGE_REPORT_JOB: owed with the usage it reports (a `usage`
71
+ // event or a `model_usage` row) while some plugin subscribes, under `usageReportJobId` /
72
+ // `modelUsageJobId` (spec §8.1)
73
+ "usage-report": {
74
+ queue: "cap-kernel-usage-report",
75
+ idempotent: "handlers are idempotent by eventId (the event's `<conversationId>/<seq>` or the model_usage row id), and the job's `pending` list keeps a retry from reaching plugins already served",
76
+ processedIn: "packages/kernel/src/runtime/run-processors.ts",
77
+ // first attempt plus 5 retries, 2 s doubling (F11); finished jobs kept a day so a re-add of the
78
+ // same id (a crash between the add and the row's delete) dedupes; failed ones a week; both
79
+ // capped by count (I-2: a long outage of a billing endpoint cannot grow Redis)
80
+ opts: {
81
+ attempts: 6,
82
+ backoff: { type: "exponential", delay: 2_000 },
83
+ removeOnComplete: { age: 24 * 3600, count: 100_000 },
84
+ removeOnFail: { age: 7 * 86_400, count: 100_000 },
85
+ },
86
+ },
87
+ // `channels/deliver-job.ts` DELIVER_JOB: owed by every event append of a conversation with a
88
+ // channel target (a reply, an approval decision, a question answer, a wait stop) and by binding
89
+ // a conversation to its target, under `deliverJobId` (spec §8.1)
90
+ deliver: {
91
+ queue: "cap-kernel-channels",
92
+ idempotent: "a delivery resumes from the conversation's delivered seq under a per-conversation lock and sends with idempotency keys, so a second job sends nothing twice",
93
+ processedIn: "packages/kernel/src/channels/channels.module.ts",
94
+ // the platform's retry-after, else exponential (the processor's `deliver` backoff strategy);
95
+ // behind inbound jobs; removed once done, so a later append's id is never deduped away
96
+ opts: {
97
+ attempts: 8,
98
+ backoff: { type: "deliver" },
99
+ priority: 10,
100
+ removeOnComplete: true,
101
+ removeOnFail: true,
102
+ },
103
+ },
104
+ };
105
+ /** The registry's BullMQ options of a job name (none for a name it does not list). */
106
+ export function outboxJobOptions(name) {
107
+ return OUTBOX_JOBS[name]?.opts;
108
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The kernel's own BullMQ queues, spelled as literals so infra code (the outbox registry and
3
+ * dispatcher) names a queue without importing feature modules. `KERNEL_QUEUE_NAMES`
4
+ * (admin/health.service.ts) lists the same names from the feature constants;
5
+ * test/infra/queue-names.test.ts keeps the two equal.
6
+ */
7
+ export declare const KERNEL_QUEUES: readonly ["cap-kernel-runs", "cap-kernel-subagent-runs", "cap-kernel-channels", "cap-kernel-maintenance", "cap-kernel-plugin-scope", "cap-kernel-titles", "cap-kernel-usage-report", "cap-kernel-evals"];
8
+ export type KernelQueueName = (typeof KERNEL_QUEUES)[number];
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The kernel's own BullMQ queues, spelled as literals so infra code (the outbox registry and
3
+ * dispatcher) names a queue without importing feature modules. `KERNEL_QUEUE_NAMES`
4
+ * (admin/health.service.ts) lists the same names from the feature constants;
5
+ * test/infra/queue-names.test.ts keeps the two equal.
6
+ */
7
+ export const KERNEL_QUEUES = [
8
+ "cap-kernel-runs",
9
+ "cap-kernel-subagent-runs",
10
+ "cap-kernel-channels",
11
+ "cap-kernel-maintenance",
12
+ "cap-kernel-plugin-scope",
13
+ "cap-kernel-titles",
14
+ "cap-kernel-usage-report",
15
+ "cap-kernel-evals",
16
+ ];
@@ -25,6 +25,7 @@ import { databaseCheck, HEALTH_CHECKS, redisCheck } from "./infra/health/health.
25
25
  import { GracefulShutdownModule } from "./infra/lifecycle/graceful-shutdown.module.js";
26
26
  import { LockModule } from "./infra/lock/lock.module.js";
27
27
  import { LoggingModule } from "./infra/logging/logging.module.js";
28
+ import { OutboxModule } from "./infra/outbox/outbox.module.js";
28
29
  import { PresenceModule } from "./infra/presence/presence.module.js";
29
30
  import { QueueModule } from "./infra/queue/queue.module.js";
30
31
  import { RedisModule } from "./infra/redis/redis.module.js";
@@ -95,6 +96,8 @@ let KernelModule = KernelModule_1 = class KernelModule {
95
96
  ConfigModule.forRoot(config, options.env !== undefined ? { env: options.env } : {}),
96
97
  GracefulShutdownModule,
97
98
  DatabaseModule.forRoot(),
99
+ // owed jobs written in the caller's transaction (transactional outbox spec §3.2)
100
+ OutboxModule,
98
101
  RedisModule,
99
102
  LockModule,
100
103
  CacheModule,
@@ -53,6 +53,10 @@ export declare class DevicePolicyService {
53
53
  features: DeviceFeature[];
54
54
  policy: DeviceLocalPolicySummary | null;
55
55
  } | null>;
56
+ /** Adds `op` to the machine's pending ops, dropping the expired ones. */
57
+ private notePending;
58
+ /** Removes the pending op `opId` from the machine's row. */
59
+ private dropPending;
56
60
  /**
57
61
  * Keeps the policy the machine wrote on its row and audits the change (`confirmed`); with no
58
62
  * policy (the answer never came) only audits the attempt, unconfirmed. The detail's `isolation`
@@ -7,8 +7,10 @@ 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 { randomUUID } from "node:crypto";
10
11
  import { ToolError, validateAllowHost, } from "@capybara-hq/plugin-sdk";
11
12
  import { Inject, Injectable } from "@nestjs/common";
13
+ import { sql } from "kysely";
12
14
  import { ApiError } from "../errors.js";
13
15
  import { audit } from "../identity/audit.js";
14
16
  import { KernelDb } from "../infra/database/kernel-db.js";
@@ -21,6 +23,11 @@ import { isolationOf } from "./policy-summary.js";
21
23
  import { LocalDeviceRelay, ON_MACHINE_PREFIX } from "./relay.js";
22
24
  /** How long the machine may take to apply a policy change. */
23
25
  export const POLICY_DEADLINE_MS = 30_000;
26
+ /**
27
+ * How long a sent op may still explain a summary the machine reports by itself: its deadline and
28
+ * the reconnect grace, with room for an answer that comes only with the next hello.
29
+ */
30
+ const PENDING_OP_MS = POLICY_DEADLINE_MS + 5 * 60_000;
24
31
  /** The prefix the relay puts before a machine's `policy`-kind refusal. */
25
32
  const REFUSED_PREFIX = "Refused by the machine's local policy: ";
26
33
  const OFFLINE_MESSAGE = "The device's machine is offline, so its policy was not changed. Start capybara-device on it.";
@@ -89,6 +96,17 @@ let DevicePolicyService = class DevicePolicyService {
89
96
  return { applied: false, reason: "offline", message: OFFLINE_MESSAGE };
90
97
  }
91
98
  const args = { ...input.change, source: input.source };
99
+ // noted before it is sent: the machine may report the change by itself (a `policy` frame)
100
+ // before its answer is recorded, and the device page lists it once, as the platform's
101
+ const opId = randomUUID();
102
+ await this.notePending(machine.id, {
103
+ id: opId,
104
+ add: input.change.add ?? [],
105
+ remove: input.change.remove ?? [],
106
+ ...(input.change.tier !== undefined && { tier: input.change.tier }),
107
+ ...(input.change.isolation !== undefined && { isolation: input.change.isolation }),
108
+ until: Date.now() + PENDING_OP_MS,
109
+ });
92
110
  let policy;
93
111
  try {
94
112
  ({ policy } = await this.relay.call(input.deviceId, "policy", args, {
@@ -101,11 +119,15 @@ let DevicePolicyService = class DevicePolicyService {
101
119
  if (e instanceof ApiError && e.code === "device.offline") {
102
120
  // the op was sent and the wait ran out: the machine may still apply it, so the attempt is
103
121
  // audited unconfirmed; its next hello reports the policy as it is
104
- await this.record({ ...input, machineId: machine.id, policy: null });
122
+ await this.record({ ...input, machineId: machine.id, opId, policy: null });
123
+ }
124
+ else {
125
+ // refused or failed: nothing changed, so it explains no later change
126
+ await this.dropPending(machine.id, opId);
105
127
  }
106
128
  return this.notApplied(input.deviceId, e);
107
129
  }
108
- await this.record({ ...input, machineId: machine.id, policy });
130
+ await this.record({ ...input, machineId: machine.id, opId, policy });
109
131
  return { applied: true, policy };
110
132
  }
111
133
  /** The active machine's features and policy summary (null when no machine is connected). */
@@ -113,6 +135,27 @@ let DevicePolicyService = class DevicePolicyService {
113
135
  const m = await this.machines.activeFor(deviceId);
114
136
  return m === null ? null : { features: m.features, policy: m.localPolicy };
115
137
  }
138
+ /** Adds `op` to the machine's pending ops, dropping the expired ones. */
139
+ async notePending(machineId, op) {
140
+ await this.db
141
+ .updateTable("device_machines")
142
+ .set({
143
+ pending_policy_ops: sql `(
144
+ SELECT coalesce(jsonb_agg(e), '[]'::jsonb) FROM jsonb_array_elements(pending_policy_ops) e
145
+ WHERE (e->>'until')::bigint > ${Date.now()}
146
+ ) || jsonb_build_array(${JSON.stringify(op)}::jsonb)`,
147
+ })
148
+ .where("id", "=", machineId)
149
+ .execute();
150
+ }
151
+ /** Removes the pending op `opId` from the machine's row. */
152
+ async dropPending(machineId, opId) {
153
+ await this.db
154
+ .updateTable("device_machines")
155
+ .set({ pending_policy_ops: withoutOp(opId) })
156
+ .where("id", "=", machineId)
157
+ .execute();
158
+ }
116
159
  /**
117
160
  * Keeps the policy the machine wrote on its row and audits the change (`confirmed`); with no
118
161
  * policy (the answer never came) only audits the attempt, unconfirmed. The detail's `isolation`
@@ -136,7 +179,11 @@ let DevicePolicyService = class DevicePolicyService {
136
179
  isolationNow = input.policy.isolation;
137
180
  await this.db
138
181
  .updateTable("device_machines")
139
- .set({ local_policy: JSON.stringify(input.policy) })
182
+ // answered: the op no longer explains a summary the machine reports
183
+ .set({
184
+ local_policy: JSON.stringify(input.policy),
185
+ pending_policy_ops: withoutOp(input.opId),
186
+ })
140
187
  .where("id", "=", input.machineId)
141
188
  .where("revoked_at", "is", null)
142
189
  .execute();
@@ -186,6 +233,12 @@ let DevicePolicyService = class DevicePolicyService {
186
233
  };
187
234
  }
188
235
  };
236
+ __decorate([
237
+ Transactional()
238
+ ], DevicePolicyService.prototype, "notePending", null);
239
+ __decorate([
240
+ Transactional()
241
+ ], DevicePolicyService.prototype, "dropPending", null);
189
242
  __decorate([
190
243
  Transactional()
191
244
  ], DevicePolicyService.prototype, "record", null);
@@ -198,6 +251,11 @@ DevicePolicyService = __decorate([
198
251
  __param(4, Inject(KERNEL_LOGGER))
199
252
  ], DevicePolicyService);
200
253
  export { DevicePolicyService };
254
+ /** The machine's pending ops without `opId` (an expired one left for the next note to drop). */
255
+ const withoutOp = (opId) => sql `(
256
+ SELECT coalesce(jsonb_agg(e), '[]'::jsonb) FROM jsonb_array_elements(pending_policy_ops) e
257
+ WHERE e->>'id' <> ${opId}
258
+ )`;
201
259
  /**
202
260
  * Hosts the platform may send: an approval's `add` takes exact hosts only (a card never grants a
203
261
  * wildcard); the owner's own change on the device page (no approval) may add a wildcard (spec §7);
@@ -55,6 +55,8 @@ declare class DeviceConnection {
55
55
  private chain;
56
56
  /** Resolves once the lease is handed back (or was never held). */
57
57
  private released;
58
+ /** The hello's record, then each policy frame's, one after another (never rejects). */
59
+ private recorded;
58
60
  constructor(gw: LocalDeviceGateway, socket: WebSocket, identity: MachineIdentity, credential: string);
59
61
  get deviceId(): string;
60
62
  start(): void;
@@ -101,7 +103,7 @@ declare class DeviceConnection {
101
103
  * sockets are this process's own; everything other replicas need is in Redis.
102
104
  */
103
105
  export declare class LocalDeviceGateway {
104
- readonly machines: Pick<MachinesService, "authenticate" | "recordHello" | "touch">;
106
+ readonly machines: Pick<MachinesService, "authenticate" | "recordHello" | "recordPolicy" | "touch">;
105
107
  readonly locks: Pick<LockService, "hold">;
106
108
  readonly bus: ExtensionContext["bus"];
107
109
  readonly redis: Redis;
@@ -112,7 +114,7 @@ export declare class LocalDeviceGateway {
112
114
  private stopping;
113
115
  /** How often connections are pinged ({@link PING_INTERVAL_MS}; a test seam). */
114
116
  pingIntervalMs: number;
115
- constructor(machines: Pick<MachinesService, "authenticate" | "recordHello" | "touch">, locks: Pick<LockService, "hold">, bus: ExtensionContext["bus"], redis: Redis, config: KernelConfigService, log: Pick<KernelLogger, "info" | "warn">);
117
+ constructor(machines: Pick<MachinesService, "authenticate" | "recordHello" | "recordPolicy" | "touch">, locks: Pick<LockService, "hold">, bus: ExtensionContext["bus"], redis: Redis, config: KernelConfigService, log: Pick<KernelLogger, "info" | "warn">);
116
118
  graceMs(): number;
117
119
  leaseTtlMs(): number;
118
120
  maxFrameBytes(): number;
@@ -114,6 +114,8 @@ class DeviceConnection {
114
114
  chain = Promise.resolve();
115
115
  /** Resolves once the lease is handed back (or was never held). */
116
116
  released;
117
+ /** The hello's record, then each policy frame's, one after another (never rejects). */
118
+ recorded = Promise.resolve();
117
119
  constructor(gw, socket, identity, credential) {
118
120
  this.gw = gw;
119
121
  this.socket = socket;
@@ -174,20 +176,43 @@ class DeviceConnection {
174
176
  }
175
177
  clearTimeout(this.helloTimer);
176
178
  this.state = "claiming";
177
- if (!(await this.claim()))
178
- return;
179
- try {
180
- // the raw summary too: the frame parser drops one it cannot read whole, and its
181
- // isolation must still reach the kernel
182
- const rawPolicy = JSON.parse(text).policy;
183
- await this.gw.machines.recordHello(this.identity.machineId, frame, rawPolicy);
184
- }
185
- catch (e) {
186
- this.gw.log.warn("could not record a device hello", {
187
- deviceId: this.deviceId,
188
- error: errMsg(e),
189
- });
190
- }
179
+ // the raw summary too: the frame parser drops one it cannot read whole, and its
180
+ // isolation must still reach the kernel
181
+ const rawPolicy = JSON.parse(text).policy;
182
+ // set before the first await: a policy frame that follows is recorded after the hello
183
+ this.recorded = (async () => {
184
+ if (!(await this.claim()))
185
+ return;
186
+ try {
187
+ await this.gw.machines.recordHello(this.identity.machineId, frame, rawPolicy);
188
+ }
189
+ catch (e) {
190
+ this.gw.log.warn("could not record a device hello", {
191
+ deviceId: this.deviceId,
192
+ error: errMsg(e),
193
+ });
194
+ }
195
+ })();
196
+ await this.recorded;
197
+ return;
198
+ }
199
+ if (frame.type === "policy" && this.state !== "hello") {
200
+ // after the hello's record and the earlier policy frames, in the order they came
201
+ const rawPolicy = JSON.parse(text).policy;
202
+ this.recorded = this.recorded.then(async () => {
203
+ if (this.state === "closed")
204
+ return;
205
+ try {
206
+ await this.gw.machines.recordPolicy(this.identity.machineId, frame.policy, rawPolicy);
207
+ }
208
+ catch (e) {
209
+ this.gw.log.warn("could not record a device policy change", {
210
+ deviceId: this.deviceId,
211
+ error: errMsg(e),
212
+ });
213
+ }
214
+ });
215
+ await this.recorded;
191
216
  return;
192
217
  }
193
218
  if (frame.type !== "res" || this.state === "hello") {
@@ -94,9 +94,33 @@ export declare class MachinesService {
94
94
  * alone, read from `rawPolicy` (the frame's raw `policy`; {@link isolationOf}: anything but
95
95
  * `sandbox` is `none`), so it never hides that commands run without a sandbox. A reported
96
96
  * isolation that differs from the stored one (none stored counts as `sandbox`) is audited
97
- * `device.isolation_reported` (sandbox dev tiers spec §10.4), once per change.
97
+ * `device.isolation_reported` (sandbox dev tiers spec §10.4), once per change; a change of what
98
+ * the owner sets, made on the machine while it was away, is audited `device.policy_changed`
99
+ * ({@link reportedPolicy}).
98
100
  */
99
101
  recordHello(machineId: string, hello: DeviceHello, rawPolicy?: unknown): Promise<void>;
102
+ /**
103
+ * Stores the local policy summary a connected program pushed after its hello (a `policy` frame:
104
+ * its policy file changed on the machine, its localhost proxy guard flipped, a new tier
105
+ * ceiling), read and audited as the hello's ({@link recordHello}): an unreadable summary keeps
106
+ * its isolation alone (`rawPolicy`); a change the owner made on the machine is audited
107
+ * `device.policy_changed` (`via: "device"`), else a changed isolation `device.isolation_reported`.
108
+ * The next sandbox call then records a new `device_paths` event when the facts the model is told
109
+ * changed. A revoked machine is left alone.
110
+ */
111
+ recordPolicy(machineId: string, summary: DeviceLocalPolicySummary | undefined, rawPolicy?: unknown): Promise<void>;
112
+ /**
113
+ * The summary to store for one a program reported (hello or `policy` frame): the parsed one,
114
+ * else its raw isolation alone, else null. When both the stored and the reported summary are
115
+ * readable and what the owner sets changed ({@link localPolicyDiff}) by no `policy` op the
116
+ * platform sent ({@link opExplains}: its answer may come after the machine's own report), the
117
+ * owner changed it on the machine (`capybara-device policy ...`): audited
118
+ * `device.policy_changed` with `via: "device"`, the isolation with it, so the device page lists
119
+ * it beside the platform's changes; one an op explains is not audited here (the op's answer
120
+ * is). Otherwise a reported isolation that differs from the stored one (none stored counts as
121
+ * `sandbox`) is audited `device.isolation_reported`. A summary reported again is no change. Runs in the caller's transaction, with the machine's row locked.
122
+ */
123
+ private reportedPolicy;
100
124
  /** Marks the machine seen (a ping); writes at most once every 60 seconds. */
101
125
  touch(machineId: string): Promise<void>;
102
126
  /**