@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
@@ -9,11 +9,12 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
9
9
  };
10
10
  import { Inject, Injectable, Module } from "@nestjs/common";
11
11
  import { sql } from "kysely";
12
+ import { oweDelivery } from "../channels/deliver-job.js";
12
13
  import { ApiError } from "../errors.js";
13
14
  import { KernelDb } from "../infra/database/kernel-db.js";
14
15
  import { TransactionHost } from "../infra/database/transaction-host.js";
15
16
  import { Transactional } from "../infra/database/transactional.decorator.js";
16
- import { KernelEvents } from "../infra/events/kernel-events.js";
17
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
17
18
  import { KERNEL_BUS, KERNEL_LOGGER } from "../infra/tokens.js";
18
19
  import { ConversationEventSchema, } from "./events.js";
19
20
  /**
@@ -75,21 +76,21 @@ const EVENT_COLUMNS = [
75
76
  * seq 由 `conversations.last_seq` 在会话行锁内连续分配(多副本并发写同一会话时串行化);
76
77
  * 幂等键的「先查后写」也在同一把行锁内完成。
77
78
  *
78
- * Every published advance (`notify`) is also announced in-process as `conversation.event-appended`
79
- * on {@link KernelEvents} (the channel outbound delivery enqueues from it), on this replica, right
80
- * after the bus publish, in append order.
79
+ * An append to a conversation with a channel target (`channel_deliveries`, read with the row lock)
80
+ * owes the conversation's IM delivery in the append's transaction (transactional outbox spec
81
+ * §8.1); a conversation without one writes nothing more.
81
82
  */
82
83
  let EventLog = class EventLog {
83
84
  db;
84
85
  bus;
85
86
  log;
86
- events;
87
+ outbox;
87
88
  txHost;
88
- constructor(db, bus, log, events, txHost) {
89
+ constructor(db, bus, log, outbox, txHost) {
89
90
  this.db = db;
90
91
  this.bus = bus;
91
92
  this.log = log;
92
- this.events = events;
93
+ this.outbox = outbox;
93
94
  this.txHost = txHost;
94
95
  }
95
96
  /**
@@ -126,10 +127,7 @@ let EventLog = class EventLog {
126
127
  const { events: out, toSeq } = await this.write(conversationId, events, bodies);
127
128
  return { events: out, toSeq };
128
129
  }
129
- /**
130
- * 发布「日志已推进到 toSeq」;发布失败只记日志(事件已提交,订阅方可按 seq 补读)。Then announces
131
- * it in-process (`conversation.event-appended`); a failing subscriber is logged by KernelEvents.
132
- */
130
+ /** 发布「日志已推进到 toSeq」;发布失败只记日志(事件已提交,订阅方可按 seq 补读)。 */
133
131
  async notify(conversationId, toSeq) {
134
132
  const msg = { kind: "events", toSeq };
135
133
  try {
@@ -138,7 +136,6 @@ let EventLog = class EventLog {
138
136
  catch (err) {
139
137
  this.log.warn("conversation event notify failed", { conversationId, toSeq, err });
140
138
  }
141
- await this.events.emit("conversation.event-appended", { conversationId, toSeq });
142
139
  }
143
140
  /**
144
141
  * Publishes a `changed` frame on the conversation topic (rename, generated title, selection, ...).
@@ -300,10 +297,20 @@ let EventLog = class EventLog {
300
297
  /** The append itself, in the context's transaction (both callers open or join one). */
301
298
  async write(conversationId, events, bodies) {
302
299
  const t = this.db;
303
- // 先锁会话行:之后的幂等查询与 seq 分配都在这把锁内,并发 append 依次执行。
300
+ // 先锁会话行:之后的幂等查询与 seq 分配都在这把锁内,并发 append 依次执行。The same statement
301
+ // reads whether the conversation has a channel target (its primary key), under the lock that
302
+ // `ChannelJobs.bind` takes too
304
303
  const conv = await t
305
304
  .selectFrom("conversations")
306
- .select("id")
305
+ .select((eb) => [
306
+ "org_id",
307
+ eb
308
+ .exists(eb
309
+ .selectFrom("channel_deliveries")
310
+ .select("channel_deliveries.conversation_id")
311
+ .whereRef("channel_deliveries.conversation_id", "=", "conversations.id"))
312
+ .as("im"),
313
+ ])
307
314
  .where("id", "=", conversationId)
308
315
  .forUpdate()
309
316
  .executeTakeFirst();
@@ -378,6 +385,8 @@ let EventLog = class EventLog {
378
385
  }))
379
386
  .returning(EVENT_COLUMNS)
380
387
  .execute();
388
+ if (conv.im)
389
+ await oweDelivery(this.outbox, { conversationId, orgId: conv.org_id, seq: toSeq });
381
390
  const bySeq = new Map(inserted.map((r) => [Number(r.seq), toEvent(r)]));
382
391
  return {
383
392
  events: plan.map((p) => p.kind === "old" ? p.event : bySeq.get(base + p.index + 1)),
@@ -400,7 +409,7 @@ EventLog = __decorate([
400
409
  __param(0, Inject(KernelDb)),
401
410
  __param(1, Inject(KERNEL_BUS)),
402
411
  __param(2, Inject(KERNEL_LOGGER)),
403
- __param(3, Inject(KernelEvents)),
412
+ __param(3, Inject(OutboxService)),
404
413
  __param(4, Inject(TransactionHost))
405
414
  ], EventLog);
406
415
  export { EventLog };
@@ -776,7 +776,8 @@ export const ConversationEventSchema = z.discriminatedUnion("type", [
776
776
  * naming a template's skills the child loads first (skills default-on spec §6.1).
777
777
  */
778
778
  preamble: z.string(),
779
- maxSteps: z.number().int().positive(),
779
+ /** The child's step limit; 0 = no limit (`runtime.maxSteps` 0, no template limit). */
780
+ maxSteps: z.number().int().nonnegative(),
780
781
  warnings: z.array(z.string()).optional(),
781
782
  })),
782
783
  /**
@@ -12,12 +12,12 @@ export declare class ConversationQueue {
12
12
  private readonly db;
13
13
  private readonly eventLog;
14
14
  private readonly conversations;
15
- /** Starts the conversation's next run (`ensureRun`). */
15
+ /** Starts the conversation's next run after a commit (a failure is logged, never thrown). */
16
16
  private readonly runs;
17
17
  readonly txHost: TransactionHost;
18
18
  constructor(db: KernelDb, eventLog: Pick<EventLog, "appendIn" | "notify" | "publishChanged" | "findByIdem">, conversations: Pick<ConversationsService, "get" | "writable" | "resolveExtra" | "bindIn" | "checkPick" | "prepareQueuedSkill" | "skipWaitsIn" | "announceSkipped">,
19
- /** Starts the conversation's next run (`ensureRun`). */
20
- runs: Pick<RunControl, "ensureRun" | "wakeHeldTree">, txHost: TransactionHost);
19
+ /** Starts the conversation's next run after a commit (a failure is logged, never thrown). */
20
+ runs: Pick<RunControl, "ensureRunAfterCommit" | "oweEnsureRun" | "wakeHeldTree" | "oweHeldTreeIn">, txHost: TransactionHost);
21
21
  list(orgId: string, conversationId: string, accountId: string): Promise<{
22
22
  items: QueueItem[];
23
23
  paused: boolean;
@@ -60,6 +60,11 @@ export declare class ConversationQueue {
60
60
  private sendNowIn;
61
61
  /** "Continue sending": clears the pause and lets `ensureRun` send the head item. */
62
62
  resume(orgId: string, conversationId: string, accountId: string): Promise<void>;
63
+ /**
64
+ * Clears the pause and owes the ensure-run backstop that sends the head item (no event of its
65
+ * own, so under the conversation's newest seq; transactional outbox spec §6).
66
+ */
67
+ private resumeIn;
63
68
  /** A queued item or a sent message for this client id (outside any transaction). */
64
69
  private prior;
65
70
  private lockConversation;
@@ -31,7 +31,7 @@ let ConversationQueue = class ConversationQueue {
31
31
  runs;
32
32
  txHost;
33
33
  constructor(db, eventLog, conversations,
34
- /** Starts the conversation's next run (`ensureRun`). */
34
+ /** Starts the conversation's next run after a commit (a failure is logged, never thrown). */
35
35
  runs, txHost) {
36
36
  this.db = db;
37
37
  this.eventLog = eventLog;
@@ -101,16 +101,16 @@ let ConversationQueue = class ConversationQueue {
101
101
  }
102
102
  if (out.created) {
103
103
  await this.eventLog.publishChanged(conversationId, "queue");
104
- await this.runs.ensureRun(conversationId);
104
+ await this.runs.ensureRunAfterCommit(conversationId);
105
105
  }
106
106
  return { item: toItem(out.row), created: out.created };
107
107
  }
108
108
  // its own transaction: the caller notifies and publishes after the commit
109
109
  async addIn(orgId, conversationId, accountId, input, files, skill, extra, internal) {
110
110
  const t = this.db.db;
111
- await t
111
+ const conv = await t
112
112
  .selectFrom("conversations")
113
- .select("id")
113
+ .select("last_seq")
114
114
  .where("id", "=", conversationId)
115
115
  .forUpdate()
116
116
  .executeTakeFirstOrThrow();
@@ -168,6 +168,9 @@ let ConversationQueue = class ConversationQueue {
168
168
  })
169
169
  .returning(COLUMNS)
170
170
  .executeTakeFirstOrThrow();
171
+ // on an idle conversation the item goes out at once: owed with it (no event of its own, so
172
+ // under the conversation's newest seq; transactional outbox spec §6)
173
+ await this.runs.oweEnsureRun(conversationId, conv.last_seq, orgId);
171
174
  return { row, created: true };
172
175
  }
173
176
  /**
@@ -253,6 +256,8 @@ let ConversationQueue = class ConversationQueue {
253
256
  // a task's queued message deleted by a person: its run is skipped (scheduled tasks §6)
254
257
  await releaseQueuedTaskRunIn(t, gone.schedule);
255
258
  await unpauseIfEmpty(t, conversationId);
259
+ // the wakes an unpause releases for the tree are owed with it (transactional outbox spec §6)
260
+ await this.runs.oweHeldTreeIn(conversationId);
256
261
  }
257
262
  /**
258
263
  * Sends a queued item now: it leaves the queue and is posted as a user message in the same
@@ -263,7 +268,7 @@ let ConversationQueue = class ConversationQueue {
263
268
  */
264
269
  async sendNow(orgId, conversationId, accountId, itemId) {
265
270
  await this.conversations.writable(orgId, conversationId, accountId);
266
- const { event, fresh, switched, unarchived, skipped, toSeq } = await this.sendNowIn(conversationId, itemId);
271
+ const { event, fresh, switched, unarchived, skipped, toSeq } = await this.sendNowIn(orgId, conversationId, itemId);
267
272
  if (fresh)
268
273
  await this.eventLog.notify(conversationId, toSeq);
269
274
  await this.conversations.announceSkipped(skipped);
@@ -272,12 +277,12 @@ let ConversationQueue = class ConversationQueue {
272
277
  if (unarchived)
273
278
  await this.eventLog.publishChanged(conversationId, "archived");
274
279
  await this.eventLog.publishChanged(conversationId, "queue");
275
- await this.runs.ensureRun(conversationId);
280
+ await this.runs.ensureRunAfterCommit(conversationId);
276
281
  await this.runs.wakeHeldTree(conversationId);
277
282
  return event;
278
283
  }
279
284
  // its own transaction: the caller notifies and publishes after the commit
280
- async sendNowIn(conversationId, itemId) {
285
+ async sendNowIn(orgId, conversationId, itemId) {
281
286
  const t = this.db.db;
282
287
  await this.lockConversation(conversationId);
283
288
  const head = await t
@@ -313,6 +318,10 @@ let ConversationQueue = class ConversationQueue {
313
318
  const skips = sent
314
319
  ? { skipped: NO_REPLY_SKIPS, toSeq: null }
315
320
  : await this.conversations.skipWaitsIn(conversationId, row.account_id, out.event.seq);
321
+ // the run the sent message starts is owed with it (transactional outbox spec §6), and so are
322
+ // the wakes the unpause releases for the tree
323
+ await this.runs.oweEnsureRun(conversationId, out.event.seq, orgId);
324
+ await this.runs.oweHeldTreeIn(conversationId);
316
325
  return {
317
326
  ...out,
318
327
  fresh: !sent,
@@ -323,16 +332,28 @@ let ConversationQueue = class ConversationQueue {
323
332
  /** "Continue sending": clears the pause and lets `ensureRun` send the head item. */
324
333
  async resume(orgId, conversationId, accountId) {
325
334
  await this.conversations.writable(orgId, conversationId, accountId);
326
- await this.db
327
- .updateTable("conversations")
328
- .set({ queue_paused: false, queue_pause_reason: null })
329
- .where("id", "=", conversationId)
330
- .execute();
335
+ await this.resumeIn(orgId, conversationId);
331
336
  await this.eventLog.publishChanged(conversationId, "queue");
332
- await this.runs.ensureRun(conversationId);
337
+ await this.runs.ensureRunAfterCommit(conversationId);
333
338
  // a paused root holds the wakes of its whole tree (agent map spec §5.3)
334
339
  await this.runs.wakeHeldTree(conversationId);
335
340
  }
341
+ /**
342
+ * Clears the pause and owes the ensure-run backstop that sends the head item (no event of its
343
+ * own, so under the conversation's newest seq; transactional outbox spec §6).
344
+ */
345
+ // its own transaction: the caller publishes and starts the run after the commit
346
+ async resumeIn(orgId, conversationId) {
347
+ const conv = await this.db.db
348
+ .updateTable("conversations")
349
+ .set({ queue_paused: false, queue_pause_reason: null })
350
+ .where("id", "=", conversationId)
351
+ .returning("last_seq")
352
+ .executeTakeFirstOrThrow();
353
+ await this.runs.oweEnsureRun(conversationId, conv.last_seq, orgId);
354
+ // the wakes the root held for its tree (agent map spec §5.3) are owed with the unpause
355
+ await this.runs.oweHeldTreeIn(conversationId);
356
+ }
336
357
  /** A queued item or a sent message for this client id (outside any transaction). */
337
358
  async prior(conversationId, clientMessageId) {
338
359
  const row = await this.db
@@ -370,6 +391,9 @@ __decorate([
370
391
  __decorate([
371
392
  Transactional({ propagation: "requires_new" })
372
393
  ], ConversationQueue.prototype, "sendNowIn", null);
394
+ __decorate([
395
+ Transactional({ propagation: "requires_new" })
396
+ ], ConversationQueue.prototype, "resumeIn", null);
373
397
  __decorate([
374
398
  Transactional({ propagation: "mandatory" })
375
399
  ], ConversationQueue.prototype, "lockConversation", null);
@@ -5,7 +5,7 @@ import { KernelDb } from "../infra/database/kernel-db.js";
5
5
  import { TransactionHost } from "../infra/database/transaction-host.js";
6
6
  import { ModelPurposesService } from "../models/purposes.js";
7
7
  import { type AuditEntry, type HookRunner } from "../runtime/hook-calls.js";
8
- import { UsageReporter } from "../runtime/usage-report.js";
8
+ import { UsageReportJobs } from "../runtime/usage-report-jobs.js";
9
9
  import { EventLog } from "./event-log.js";
10
10
  import { type ConversationEvent } from "./events.js";
11
11
  /**
@@ -56,7 +56,7 @@ export declare function titleExcerpt(firstUser: string, firstReply: string): str
56
56
  export declare function cleanTitle(reply: string): string | null;
57
57
  export interface TitleDeps {
58
58
  db: KernelSchemaDb;
59
- eventLog: Pick<EventLog, "append" | "appendIn" | "readAfter" | "latestOfType" | "notify" | "publishChanged">;
59
+ eventLog: Pick<EventLog, "appendInReport" | "readAfter" | "latestOfType" | "notify" | "publishChanged">;
60
60
  purposes: Pick<ModelPurposesService, "chat">;
61
61
  log: Pick<Logger, "warn">;
62
62
  /**
@@ -64,11 +64,10 @@ export interface TitleDeps {
64
64
  * title and uses up the attempt) and `model.response` with `purpose: "title"`, like compaction.
65
65
  */
66
66
  hooks?: HookRunner;
67
- usageReport?: Pick<UsageReporter, "enqueue">;
68
67
  /** Aborts the call (worker shutdown). */
69
68
  signal?: AbortSignal;
70
69
  /** The job's transactional writes ({@link TitleGenerator}). */
71
- writes: Pick<TitleGenerator, "writeResult" | "auditDenied">;
70
+ writes: Pick<TitleGenerator, "writeResult" | "writeUsage" | "auditDenied">;
72
71
  }
73
72
  /** The attempt number to enqueue for this conversation, or null when no title job is due. */
74
73
  export declare function titleAttemptDue(db: KernelSchemaDb, conversationId: string): Promise<number | null>;
@@ -93,19 +92,22 @@ export declare class TitleGenerator {
93
92
  private readonly purposes;
94
93
  /** The title call fires the plugin hooks (`model.request`, `model.response`). */
95
94
  private readonly host;
96
- private readonly usageReport;
95
+ /** The title call's `usage.report` delivery, owed with its usage event. */
96
+ private readonly usage;
97
97
  private readonly log;
98
98
  readonly txHost: TransactionHost;
99
99
  constructor(kdb: KernelDb, eventLog: TitleDeps["eventLog"], purposes: Pick<ModelPurposesService, "chat">,
100
100
  /** The title call fires the plugin hooks (`model.request`, `model.response`). */
101
101
  host: {
102
102
  readonly hooks?: HookRunner;
103
- } | undefined, usageReport: Pick<UsageReporter, "enqueue"> | undefined, log: Pick<Logger, "warn">, txHost: TransactionHost);
103
+ } | undefined,
104
+ /** The title call's `usage.report` delivery, owed with its usage event. */
105
+ usage: Pick<UsageReportJobs, "oweEvents">, log: Pick<Logger, "warn">, txHost: TransactionHost);
104
106
  /** One attempt ({@link runTitleJob}); `signal` aborts the call (the worker stopping). */
105
107
  run(conversationId: string, attempt: number, signal?: AbortSignal): Promise<void>;
106
108
  /**
107
- * Appends the call's `usage` event and writes the title while `title_source IS NULL` (a rename
108
- * wins), in one transaction.
109
+ * Appends the call's `usage` event (owing its `usage.report` delivery) and writes the title while
110
+ * `title_source IS NULL` (a rename wins), in one transaction.
109
111
  */
110
112
  writeResult(conversationId: string, attempt: number, out: {
111
113
  provider: string;
@@ -115,6 +117,17 @@ export declare class TitleGenerator {
115
117
  written: boolean;
116
118
  usage: ConversationEvent | undefined;
117
119
  }>;
120
+ /**
121
+ * Appends the usage event of a call that failed after it was sent (billed, so reported) and owes
122
+ * its `usage.report` delivery, in one transaction.
123
+ */
124
+ writeUsage(conversationId: string, attempt: number, u: {
125
+ provider: string;
126
+ model: string;
127
+ usage: ModelUsage;
128
+ }): Promise<ConversationEvent | undefined>;
129
+ /** The usage event and its owed delivery (only when this call wrote it, not on a retry's hit). */
130
+ private appendUsage;
118
131
  /** The `hook.denied` audit row of a denied title call. */
119
132
  auditDenied(entry: AuditEntry): Promise<void>;
120
133
  }
@@ -22,7 +22,7 @@ import { KERNEL_LOGGER } from "../infra/tokens.js";
22
22
  import { ModelsModule } from "../models/models.module.js";
23
23
  import { ModelPurposesService, PurposeAttemptError } from "../models/purposes.js";
24
24
  import { fireObserve, hookDeniedAudit, hookDeniedEvent, hookRequestOf, } from "../runtime/hook-calls.js";
25
- import { UsageReporter, UsageReportModule } from "../runtime/usage-report.js";
25
+ import { UsageReportJobs, UsageReportJobsModule } from "../runtime/usage-report-jobs.js";
26
26
  import { HIDDEN_FORMAT_RE } from "../text.js";
27
27
  import { EventLog, EventLogModule } from "./event-log.js";
28
28
  import { typedInvocation } from "./events.js";
@@ -275,10 +275,8 @@ export async function runTitleJob(d, conversationId, attempt) {
275
275
  });
276
276
  }
277
277
  const { written, usage } = await d.writes.writeResult(conversationId, attempt, out, title);
278
- if (usage) {
278
+ if (usage)
279
279
  await d.eventLog.notify(conversationId, usage.seq);
280
- void d.usageReport?.enqueue(conversationId, usage.seq);
281
- }
282
280
  if (written)
283
281
  await d.eventLog.publishChanged(conversationId, "title");
284
282
  }
@@ -291,9 +289,9 @@ const usageEvent = (attempt, u) => ({
291
289
  payload: { step: 0, provider: u.provider, model: u.model, ...u.usage, purpose: "title" },
292
290
  });
293
291
  async function logUsage(d, conversationId, attempt, u) {
294
- const [ev] = await d.eventLog.append(conversationId, [usageEvent(attempt, u)]);
292
+ const ev = await d.writes.writeUsage(conversationId, attempt, u);
295
293
  if (ev)
296
- void d.usageReport?.enqueue(conversationId, ev.seq);
294
+ await d.eventLog.notify(conversationId, ev.seq);
297
295
  }
298
296
  /** Returns a claimed attempt (shutdown before the call was sent). */
299
297
  async function giveBack(db, conversationId, attempt) {
@@ -383,17 +381,19 @@ let TitleGenerator = class TitleGenerator {
383
381
  eventLog;
384
382
  purposes;
385
383
  host;
386
- usageReport;
384
+ usage;
387
385
  log;
388
386
  txHost;
389
387
  constructor(kdb, eventLog, purposes,
390
388
  /** The title call fires the plugin hooks (`model.request`, `model.response`). */
391
- host, usageReport, log, txHost) {
389
+ host,
390
+ /** The title call's `usage.report` delivery, owed with its usage event. */
391
+ usage, log, txHost) {
392
392
  this.kdb = kdb;
393
393
  this.eventLog = eventLog;
394
394
  this.purposes = purposes;
395
395
  this.host = host;
396
- this.usageReport = usageReport;
396
+ this.usage = usage;
397
397
  this.log = log;
398
398
  this.txHost = txHost;
399
399
  }
@@ -409,21 +409,20 @@ let TitleGenerator = class TitleGenerator {
409
409
  purposes: this.purposes,
410
410
  log: this.log,
411
411
  ...(hooks !== undefined && { hooks }),
412
- ...(this.usageReport !== undefined && { usageReport: this.usageReport }),
413
412
  ...(signal !== undefined && { signal }),
414
413
  writes: this,
415
414
  }, conversationId, attempt);
416
415
  }
417
416
  /**
418
- * Appends the call's `usage` event and writes the title while `title_source IS NULL` (a rename
419
- * wins), in one transaction.
417
+ * Appends the call's `usage` event (owing its `usage.report` delivery) and writes the title while
418
+ * `title_source IS NULL` (a rename wins), in one transaction.
420
419
  */
421
420
  async writeResult(conversationId, attempt, out, title) {
422
421
  // Append first: it locks the conversation row FOR UPDATE before this transaction touches it.
423
422
  // Updating the title first held the row FOR NO KEY UPDATE, which lets in another
424
423
  // transaction's foreign key check (FOR KEY SHARE, e.g. inserting an approval); the append's
425
424
  // FOR UPDATE then waited for that one while it waited for the title row: 40P01
426
- const [ev] = await this.eventLog.appendIn(conversationId, [usageEvent(attempt, out)]);
425
+ const ev = await this.appendUsage(conversationId, attempt, out);
427
426
  const row = title === null
428
427
  ? undefined
429
428
  : await this.kdb.db
@@ -435,6 +434,22 @@ let TitleGenerator = class TitleGenerator {
435
434
  .executeTakeFirst();
436
435
  return { written: row !== undefined, usage: ev };
437
436
  }
437
+ /**
438
+ * Appends the usage event of a call that failed after it was sent (billed, so reported) and owes
439
+ * its `usage.report` delivery, in one transaction.
440
+ */
441
+ async writeUsage(conversationId, attempt, u) {
442
+ return this.appendUsage(conversationId, attempt, u);
443
+ }
444
+ /** The usage event and its owed delivery (only when this call wrote it, not on a retry's hit). */
445
+ async appendUsage(conversationId, attempt, u) {
446
+ const { events, fresh } = await this.eventLog.appendInReport(conversationId, [
447
+ usageEvent(attempt, u),
448
+ ]);
449
+ if (fresh[0])
450
+ await this.usage.oweEvents(conversationId, events);
451
+ return events[0];
452
+ }
438
453
  /** The `hook.denied` audit row of a denied title call. */
439
454
  async auditDenied(entry) {
440
455
  await audit(this.kdb.db, entry);
@@ -443,6 +458,12 @@ let TitleGenerator = class TitleGenerator {
443
458
  __decorate([
444
459
  Transactional({ propagation: "requires_new" })
445
460
  ], TitleGenerator.prototype, "writeResult", null);
461
+ __decorate([
462
+ Transactional({ propagation: "requires_new" })
463
+ ], TitleGenerator.prototype, "writeUsage", null);
464
+ __decorate([
465
+ Transactional({ propagation: "mandatory" })
466
+ ], TitleGenerator.prototype, "appendUsage", null);
446
467
  __decorate([
447
468
  Transactional({ propagation: "requires_new" })
448
469
  ], TitleGenerator.prototype, "auditDenied", null);
@@ -452,7 +473,7 @@ TitleGenerator = __decorate([
452
473
  __param(1, Inject(EventLog)),
453
474
  __param(2, Inject(ModelPurposesService)),
454
475
  __param(3, Inject(ExtensionHost)),
455
- __param(4, Inject(UsageReporter)),
476
+ __param(4, Inject(UsageReportJobs)),
456
477
  __param(5, Inject(KERNEL_LOGGER)),
457
478
  __param(6, Inject(TransactionHost))
458
479
  ], TitleGenerator);
@@ -535,7 +556,13 @@ let TitlesModule = class TitlesModule {
535
556
  };
536
557
  TitlesModule = __decorate([
537
558
  Module({
538
- imports: [EventLogModule, ModelsModule, ExtensionsModule, UsageReportModule, TITLE_QUEUE_MODULE],
559
+ imports: [
560
+ EventLogModule,
561
+ ModelsModule,
562
+ ExtensionsModule,
563
+ UsageReportJobsModule,
564
+ TITLE_QUEUE_MODULE,
565
+ ],
539
566
  providers: [TitleGenerator, TitleQueue],
540
567
  exports: [TitleQueue, TITLE_QUEUE_MODULE],
541
568
  })
@@ -939,6 +939,8 @@ export interface DeviceMachinesTable {
939
939
  features: ColumnType<string[], string[] | undefined, string[]>;
940
940
  /** 0114: the program's local network policy summary (hello, refreshed by each `policy` op). */
941
941
  local_policy: ColumnType<unknown | null, string | null | undefined, string | null>;
942
+ /** 0118: the `policy` ops sent whose answer is not recorded yet (`PendingPolicyOp[]`). */
943
+ pending_policy_ops: ColumnType<unknown, string | undefined, string>;
942
944
  }
943
945
  /** 0088_environments.sql, renamed in 0105: the device a member's new conversations use by default. */
944
946
  export interface DeviceDefaultsTable {
@@ -1156,12 +1158,6 @@ export interface ExtensionReplicaStatusTable {
1156
1158
  http_replica_id: string | null;
1157
1159
  heartbeat_at: Ts;
1158
1160
  }
1159
- /** 0040: the usage.report sweeper's watermark (a single row, id = 1). */
1160
- export interface UsageReportSweepTable {
1161
- id: ColumnType<number, number | undefined, never>;
1162
- watermark: Ts;
1163
- updated_at: ColumnType<Date, Date | string | undefined, Date | string>;
1164
- }
1165
1161
  /** A model disabled on the platform (`org_id` null) or in one org (0089, spec B §5.3). */
1166
1162
  export interface ModelDisabledTable {
1167
1163
  org_id: string | null;
@@ -1181,7 +1177,7 @@ export interface ModelUsageTable {
1181
1177
  input_tokens: number;
1182
1178
  occurred_at: Ts;
1183
1179
  reported_at: TsNullable;
1184
- /** Set when the row's `usage.report` job gave up after its last attempt (the sweep skips it). */
1180
+ /** Set when the row's `usage.report` job gave up after its last attempt. */
1185
1181
  report_failed_at: TsNullable;
1186
1182
  }
1187
1183
  type NullableText = ColumnType<string | null, string | null | undefined, string | null>;
@@ -1310,7 +1306,6 @@ export interface KernelDatabase {
1310
1306
  extension_blobs: ExtensionBlobsTable;
1311
1307
  extension_installs: ExtensionInstallsTable;
1312
1308
  extension_replica_status: ExtensionReplicaStatusTable;
1313
- usage_report_sweep: UsageReportSweepTable;
1314
1309
  extension_name_owners: ExtensionNameOwnersTable;
1315
1310
  extension_deployment_configs: ExtensionDeploymentConfigsTable;
1316
1311
  catalog_items: CatalogItemsTable;
@@ -1344,6 +1339,29 @@ export interface KernelDatabase {
1344
1339
  plugin_scope_sync_failures: PluginScopeSyncFailuresTable;
1345
1340
  extension_client_builds: ExtensionClientBuildsTable;
1346
1341
  extension_client_reports: ExtensionClientReportsTable;
1342
+ outbox: OutboxTable;
1343
+ }
1344
+ /**
1345
+ * 0119: the transactional outbox (transactional outbox spec §4). A row is a BullMQ job owed by a
1346
+ * committed transaction; deleted on delivery, so only dead rows (`dead_at` set) stay.
1347
+ */
1348
+ export interface OutboxTable {
1349
+ /** bigint identity; the pg driver reads it as a string. */
1350
+ id: Generated<string>;
1351
+ queue: string;
1352
+ job_name: string;
1353
+ /** Unique per queue: a second add of the same job before delivery is a no-op. */
1354
+ job_id: string;
1355
+ data: Json;
1356
+ /** The job's delay target; null runs it now. */
1357
+ run_at: Date | null;
1358
+ /** Null for platform work. */
1359
+ org_id: string | null;
1360
+ attempts: Generated<number>;
1361
+ next_attempt_at: Generated<Date>;
1362
+ last_error: string | null;
1363
+ dead_at: Date | null;
1364
+ created_at: Generated<Date>;
1347
1365
  }
1348
1366
  /**
1349
1367
  * 0107: the client files of an extension package (client extensions spec §9.1), keyed by the
@@ -111,7 +111,8 @@ export interface DeviceSandboxView {
111
111
  export declare const RECENT_POLICY_CHANGES = 10;
112
112
  /**
113
113
  * One sandbox change of a device, as its owner sees it on the device page (§10.4):
114
- * - `policy` (`device.policy_changed`): a local policy change sent to the machine;
114
+ * - `policy` (`device.policy_changed`): a local policy change sent to the machine (`web`, `im`),
115
+ * or made on the machine by its owner (`device`, `capybara-device policy ...`);
115
116
  * - `tier` (`device.tier_changed`): a cloud device's tier set by its owner;
116
117
  * - `isolation` (`device.isolation_reported`): the machine reported running without a sandbox,
117
118
  * or with one again.
@@ -122,15 +123,23 @@ export interface DeviceSandboxChangeView {
122
123
  /** Hosts added to / removed from the local allowlist (`policy` only; else empty). */
123
124
  add: string[];
124
125
  remove: string[];
125
- /** The tier set (`to: null`: follow the org's default); null when the tier did not change. */
126
+ /**
127
+ * The tier set (`to: null`: follow the org's default; `from`, the tier before, only for a change
128
+ * made on the machine); null when the tier did not change.
129
+ */
126
130
  tier: {
131
+ from?: SandboxTier | null;
127
132
  to: SandboxTier | null;
128
133
  } | null;
129
134
  /** The isolation set or reported; null when it did not change. */
130
135
  isolation: "sandbox" | "none" | null;
136
+ /** The localhost access now (a change made on the machine only); null when it did not change. */
137
+ localhost: DeviceLocalPolicySummary["localhost"] | null;
138
+ /** Whether the machine takes the platform's changes now (made on the machine); null: unchanged. */
139
+ remoteGrants: DeviceLocalPolicySummary["remoteGrants"] | null;
131
140
  /** The deciding account's display name (null when the account is gone). */
132
141
  by: string | null;
133
- /** `device`: reported by the machine itself (`isolation`). */
142
+ /** `device`: made or reported on the machine itself (a `policy` change, an `isolation`). */
134
143
  via: "web" | "im" | "device";
135
144
  /** The machine answered with the policy as written (false: the answer never came). */
136
145
  confirmed: boolean;