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

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 (198) hide show
  1. package/dist/admin/admin.module.js +2 -0
  2. package/dist/admin/health.service.d.ts +2 -2
  3. package/dist/admin/health.service.js +6 -2
  4. package/dist/admin/outbox-dead.service.d.ts +7 -3
  5. package/dist/admin/outbox-dead.service.js +17 -6
  6. package/dist/admin/overview.service.d.ts +6 -6
  7. package/dist/admin/overview.service.js +7 -7
  8. package/dist/agents/agents.module.js +3 -0
  9. package/dist/agents/agents.service.d.ts +6 -1
  10. package/dist/agents/agents.service.js +11 -2
  11. package/dist/background/wake-rules.d.ts +5 -0
  12. package/dist/background/wake-rules.js +16 -5
  13. package/dist/catalog/catalog-import.module.js +0 -4
  14. package/dist/catalog/catalog-scope.service.d.ts +63 -26
  15. package/dist/catalog/catalog-scope.service.js +146 -71
  16. package/dist/catalog/catalog.module.js +4 -3
  17. package/dist/catalog/catalog.service.d.ts +11 -4
  18. package/dist/catalog/catalog.service.js +21 -5
  19. package/dist/catalog/cli.d.ts +4 -3
  20. package/dist/catalog/cli.js +8 -13
  21. package/dist/catalog/job-ids.d.ts +8 -20
  22. package/dist/catalog/job-ids.js +11 -16
  23. package/dist/catalog/marketplaces.service.d.ts +1 -1
  24. package/dist/catalog/org-plugin-uploads.d.ts +11 -4
  25. package/dist/catalog/org-plugin-uploads.js +21 -6
  26. package/dist/catalog/owned-items.d.ts +6 -13
  27. package/dist/catalog/owned-items.js +11 -29
  28. package/dist/catalog/owned-plugin-cleanup.d.ts +8 -17
  29. package/dist/catalog/owned-plugin-cleanup.js +12 -66
  30. package/dist/catalog/platform-components.d.ts +56 -34
  31. package/dist/catalog/platform-components.js +68 -78
  32. package/dist/catalog/plugin-import.d.ts +28 -25
  33. package/dist/catalog/plugin-import.js +90 -76
  34. package/dist/catalog/plugin-upgrade.d.ts +4 -4
  35. package/dist/catalog/plugin-upgrade.js +7 -19
  36. package/dist/catalog/scope-jobs.d.ts +59 -0
  37. package/dist/catalog/scope-jobs.js +65 -0
  38. package/dist/catalog/scope-sync-workers.d.ts +22 -0
  39. package/dist/catalog/scope-sync-workers.js +58 -2
  40. package/dist/catalog/scope-sync.d.ts +40 -65
  41. package/dist/catalog/scope-sync.js +114 -122
  42. package/dist/channels/channels.module.d.ts +9 -8
  43. package/dist/channels/channels.module.js +11 -10
  44. package/dist/channels/connections.service.d.ts +3 -3
  45. package/dist/channels/connections.service.js +6 -6
  46. package/dist/channels/deliver-job.d.ts +10 -3
  47. package/dist/channels/deliver-job.js +7 -3
  48. package/dist/channels/inbound.js +2 -2
  49. package/dist/channels/jobs.d.ts +11 -4
  50. package/dist/channels/jobs.js +18 -3
  51. package/dist/channels/outbound.d.ts +5 -2
  52. package/dist/channels/outbound.js +9 -2
  53. package/dist/channels/threads.d.ts +4 -3
  54. package/dist/channels/threads.js +4 -3
  55. package/dist/conversations/conversations.controller.d.ts +4 -2
  56. package/dist/conversations/conversations.controller.js +8 -4
  57. package/dist/conversations/conversations.service.d.ts +54 -9
  58. package/dist/conversations/conversations.service.js +94 -25
  59. package/dist/conversations/events.d.ts +27 -0
  60. package/dist/conversations/events.js +30 -1
  61. package/dist/conversations/source.d.ts +10 -0
  62. package/dist/conversations/source.js +17 -0
  63. package/dist/conversations/tree.d.ts +10 -0
  64. package/dist/conversations/tree.js +35 -0
  65. package/dist/db/kernel-schema.d.ts +19 -15
  66. package/dist/devices/devices.service.d.ts +25 -14
  67. package/dist/devices/devices.service.js +36 -30
  68. package/dist/evals/evals.module.js +5 -1
  69. package/dist/evals/jobs.d.ts +14 -6
  70. package/dist/evals/jobs.js +21 -19
  71. package/dist/evals/run-replay.d.ts +13 -2
  72. package/dist/evals/run-replay.js +28 -1
  73. package/dist/evals/runs.service.d.ts +5 -6
  74. package/dist/evals/runs.service.js +11 -33
  75. package/dist/evals/snapshot.d.ts +7 -1
  76. package/dist/evals/snapshot.js +2 -1
  77. package/dist/extensions/hooks.d.ts +22 -3
  78. package/dist/extensions/hooks.js +15 -1
  79. package/dist/extensions/lifecycle-hooks.d.ts +99 -0
  80. package/dist/extensions/lifecycle-hooks.js +273 -0
  81. package/dist/extensions/org-config.d.ts +13 -1
  82. package/dist/extensions/org-config.js +19 -2
  83. package/dist/extensions/permission-view.d.ts +9 -5
  84. package/dist/extensions/permission-view.js +9 -4
  85. package/dist/extensions/point-defaults.d.ts +28 -1
  86. package/dist/extensions/point-defaults.js +44 -13
  87. package/dist/inbox/inbox.service.d.ts +6 -20
  88. package/dist/inbox/inbox.service.js +0 -3
  89. package/dist/inbox/org-notices.d.ts +1 -1
  90. package/dist/inbox/platform-inbox.service.d.ts +3 -32
  91. package/dist/inbox/platform-inbox.service.js +5 -54
  92. package/dist/infra/outbox/outbox.service.d.ts +6 -0
  93. package/dist/infra/outbox/outbox.service.js +51 -17
  94. package/dist/infra/outbox/registry.d.ts +176 -0
  95. package/dist/infra/outbox/registry.js +183 -0
  96. package/dist/infra/queue/queue-names.d.ts +1 -1
  97. package/dist/infra/queue/queue-names.js +1 -0
  98. package/dist/local-devices/device-policy.service.d.ts +131 -26
  99. package/dist/local-devices/device-policy.service.js +341 -145
  100. package/dist/local-devices/machines.d.ts +17 -2
  101. package/dist/local-devices/machines.js +66 -5
  102. package/dist/local-devices/policy-op-jobs.d.ts +15 -0
  103. package/dist/local-devices/policy-op-jobs.js +12 -0
  104. package/dist/local-devices/policy-summary.d.ts +36 -3
  105. package/dist/local-devices/policy-summary.js +47 -1
  106. package/dist/orgs/directory-sync.js +1 -1
  107. package/dist/orgs/members.service.js +1 -1
  108. package/dist/orgs/offboarding.d.ts +50 -57
  109. package/dist/orgs/offboarding.js +117 -162
  110. package/dist/orgs/orgs.module.js +3 -0
  111. package/dist/orgs/orgs.service.d.ts +11 -5
  112. package/dist/orgs/orgs.service.js +19 -7
  113. package/dist/overrides.d.ts +7 -0
  114. package/dist/overrides.js +4 -0
  115. package/dist/platform/ops/conversations.ops.js +1 -1
  116. package/dist/platform/ops/inbox.ops.js +4 -20
  117. package/dist/platform/ops/platform.ops.d.ts +1 -0
  118. package/dist/platform/ops/platform.ops.js +6 -3
  119. package/dist/roles/worker-role.d.ts +10 -5
  120. package/dist/roles/worker-role.js +19 -9
  121. package/dist/roles/worker.module.js +2 -4
  122. package/dist/runtime/context/build.d.ts +3 -0
  123. package/dist/runtime/context/build.js +15 -0
  124. package/dist/runtime/context/plugin-context.d.ts +20 -0
  125. package/dist/runtime/context/plugin-context.js +30 -0
  126. package/dist/runtime/langgraph-loop.js +4 -0
  127. package/dist/runtime/loop.d.ts +8 -0
  128. package/dist/runtime/maintenance.d.ts +9 -4
  129. package/dist/runtime/maintenance.js +52 -13
  130. package/dist/runtime/model-call.d.ts +6 -1
  131. package/dist/runtime/model-call.js +1 -0
  132. package/dist/runtime/plugin-context.service.d.ts +75 -0
  133. package/dist/runtime/plugin-context.service.js +220 -0
  134. package/dist/runtime/run-context.service.d.ts +7 -3
  135. package/dist/runtime/run-context.service.js +12 -2
  136. package/dist/runtime/run-control.d.ts +6 -1
  137. package/dist/runtime/run-control.js +13 -0
  138. package/dist/runtime/run-finish.d.ts +2 -2
  139. package/dist/runtime/run-finish.js +11 -6
  140. package/dist/runtime/run-jobs.d.ts +21 -4
  141. package/dist/runtime/run-jobs.js +29 -6
  142. package/dist/runtime/run-orphan-check.job.d.ts +14 -11
  143. package/dist/runtime/run-orphan-check.job.js +16 -11
  144. package/dist/runtime/run-queue.d.ts +29 -24
  145. package/dist/runtime/run-queue.js +118 -57
  146. package/dist/runtime/run-status.d.ts +12 -0
  147. package/dist/runtime/run-status.js +21 -0
  148. package/dist/runtime/run-worker.d.ts +32 -3
  149. package/dist/runtime/run-worker.js +143 -21
  150. package/dist/runtime/runtime.module.js +2 -0
  151. package/dist/runtime/step-log.d.ts +6 -5
  152. package/dist/runtime/step-log.js +6 -5
  153. package/dist/runtime/subagents/dispatch.js +1 -0
  154. package/dist/runtime/subagents/skill-fork.js +3 -0
  155. package/dist/runtime/usage-report.d.ts +3 -1
  156. package/dist/runtime/usage-report.js +4 -2
  157. package/dist/runtime/waits.d.ts +16 -6
  158. package/dist/runtime/waits.js +37 -15
  159. package/dist/sandbox/jobs.d.ts +20 -10
  160. package/dist/sandbox/jobs.js +23 -17
  161. package/dist/schedules/fire.d.ts +25 -4
  162. package/dist/schedules/fire.js +87 -13
  163. package/dist/schedules/job-ids.d.ts +66 -17
  164. package/dist/schedules/job-ids.js +81 -13
  165. package/dist/schedules/run-tracker.js +2 -1
  166. package/dist/schedules/scan.job.d.ts +16 -35
  167. package/dist/schedules/scan.job.js +38 -147
  168. package/dist/schedules/scheduled-tasks.service.d.ts +6 -6
  169. package/dist/schedules/scheduled-tasks.service.js +17 -14
  170. package/dist/schedules/schedules.module.js +0 -4
  171. package/dist/skills/catalog.d.ts +7 -3
  172. package/dist/skills/catalog.js +12 -2
  173. package/dist/skills/changes.controller.d.ts +1 -1
  174. package/dist/skills/changes.controller.js +1 -1
  175. package/dist/tools/approvals.service.d.ts +111 -23
  176. package/dist/tools/approvals.service.js +326 -77
  177. package/dist/tools/builtins/sandbox/task-output.js +21 -6
  178. package/dist/tools/device-op-resolver.d.ts +40 -0
  179. package/dist/tools/device-op-resolver.js +121 -0
  180. package/dist/tools/network-asks.d.ts +1 -1
  181. package/dist/tools/network-asks.js +4 -3
  182. package/dist/tools/pipeline.d.ts +11 -2
  183. package/dist/tools/pipeline.js +5 -0
  184. package/dist/tools/questions.service.d.ts +15 -4
  185. package/dist/tools/questions.service.js +21 -19
  186. package/dist/tools/registry.d.ts +8 -1
  187. package/dist/tools/registry.js +9 -0
  188. package/dist/tools/tools.module.js +4 -0
  189. package/package.json +3 -3
  190. package/src/migrations/0121_drop_platform_migration_runs.sql +12 -0
  191. package/src/migrations/0122_drop_scheduled_task_runs_rechecked_at.sql +5 -0
  192. package/src/migrations/0123_pgvector.sql +27 -0
  193. package/src/migrations/0124_conversation_source.sql +36 -0
  194. package/src/migrations/0125_conversation_tree_stopped_at.sql +5 -0
  195. package/dist/catalog/platform-migration.d.ts +0 -115
  196. package/dist/catalog/platform-migration.js +0 -583
  197. package/dist/infra/queue/add-job-once.d.ts +0 -14
  198. package/dist/infra/queue/add-job-once.js +0 -16
@@ -25,11 +25,13 @@ import { KernelEvents } from "../infra/events/kernel-events.js";
25
25
  import { KERNEL_BUS, KERNEL_LOGGER } from "../infra/tokens.js";
26
26
  import { SkillsCatalogService } from "../skills/catalog.js";
27
27
  import { normalizeStoredDefinitionIn } from "../tools/legacy-data.js";
28
+ import { ToolRegistry } from "../tools/registry.js";
28
29
  import { RunAbort } from "./abort.js";
29
30
  import { KyselyCheckpointSaver } from "./checkpointer.js";
30
31
  import { ConversationLock } from "./conversation-lock.js";
31
32
  import { runDeadline, tightestLimit } from "./limits.js";
32
33
  import { RunError } from "./model-call.js";
34
+ import { PluginContextService } from "./plugin-context.service.js";
33
35
  import { ProviderListError, ProviderRegistry } from "./providers.js";
34
36
  import { ORG_QUOTA_LEASE_MS, orgRunLimit, RUN_QUOTA } from "./quota.js";
35
37
  import { RunContextService } from "./run-context.service.js";
@@ -64,6 +66,15 @@ async function requeue(job, token, delayMs) {
64
66
  await job.moveToDelayed(Date.now() + delayMs, token);
65
67
  throw new DelayedError();
66
68
  }
69
+ /** An account's display name ("" when it is gone). */
70
+ async function accountName(db, accountId) {
71
+ const row = await db
72
+ .selectFrom("accounts")
73
+ .select("display_name")
74
+ .where("id", "=", accountId)
75
+ .executeTakeFirst();
76
+ return row?.display_name ?? "";
77
+ }
67
78
  /**
68
79
  * 运行 worker(只在含 `worker` 角色的进程消费):领取 `cap-kernel-runs` 与 `cap-kernel-subagent-runs` 的任务
69
80
  * (两个 `@Processor`,见 run-processors.ts),取会话锁与组织配额,调用 AgentLoop,按结果写终态事件并推进
@@ -87,6 +98,8 @@ let RunWorker = class RunWorker {
87
98
  models;
88
99
  skillsCatalog;
89
100
  runContext;
101
+ pluginContext;
102
+ tools;
90
103
  finisher;
91
104
  txHost;
92
105
  drafts;
@@ -114,7 +127,11 @@ let RunWorker = class RunWorker {
114
127
  /** Run start's skills catalog snapshot (reads the pinned skills' bodies, phase B). */
115
128
  skillsCatalog,
116
129
  /** Run start's run context (run context spec §5): who the agent is, the day, the person. */
117
- runContext, finisher, txHost,
130
+ runContext,
131
+ /** Run start's plugin context (memory plugin spec §6.1): the `context@1` providers' text. */
132
+ pluginContext,
133
+ /** The run's resolved tools, for the plugin context's `tools` (chat runs with providers). */
134
+ tools, finisher, txHost,
118
135
  /** The steps' live drafts this process writes for the runs it executes. */
119
136
  drafts, hooks) {
120
137
  this.kdb = kdb;
@@ -134,6 +151,8 @@ let RunWorker = class RunWorker {
134
151
  this.models = models;
135
152
  this.skillsCatalog = skillsCatalog;
136
153
  this.runContext = runContext;
154
+ this.pluginContext = pluginContext;
155
+ this.tools = tools;
137
156
  this.finisher = finisher;
138
157
  this.txHost = txHost;
139
158
  this.drafts = drafts;
@@ -190,8 +209,10 @@ let RunWorker = class RunWorker {
190
209
  /**
191
210
  * After any terminal commit of a run that is not a sub-agent's (sub-agents spec §12): stops its
192
211
  * active fork invocation child on its behalf (no account); background `Agent` children keep
193
- * running (agent map spec §5.4). The terminal transaction owed the same stop (`stop-tree`), the
194
- * backstop when this call never happens.
212
+ * running (agent map spec §5.4), unless a person stopped the run: then the sub-agents of its
213
+ * windows stop too, also one it dispatched after the stop read the tree (agent map spec §6,
214
+ * changed in acceptance 2026-11-13). The terminal transaction owed the same stops
215
+ * (`stop-tree`), the backstop when this call never happens.
195
216
  */
196
217
  async stopChildren(runId) {
197
218
  try {
@@ -200,6 +221,12 @@ let RunWorker = class RunWorker {
200
221
  catch (err) {
201
222
  this.log.error("cannot stop the sub-agents of a finished run", { runId, err });
202
223
  }
224
+ try {
225
+ await this.waits.stopDescendantsOfStopped(runId);
226
+ }
227
+ catch (err) {
228
+ this.log.error("cannot stop the sub-agents of a stopped run", { runId, err });
229
+ }
203
230
  }
204
231
  /**
205
232
  * After a sub-agent run's terminal state committed (sub-agents spec §7.3): reports it to the
@@ -379,9 +406,10 @@ let RunWorker = class RunWorker {
379
406
  // 运行在等人处理(例如暂停后本任务被 stalled 机制重新投递):不执行,续跑由处理 / 超时发起的新任务负责。
380
407
  if (run.status === "waiting")
381
408
  return;
409
+ await this.hooks?.afterRunRead?.(runId, conversationId);
382
410
  const conv = await this.db
383
411
  .selectFrom("conversations")
384
- .select(["org_id", "agent_id", "kind", "subagent_depth"])
412
+ .select(["org_id", "agent_id", "kind", "subagent_depth", "source", "account_id"])
385
413
  .where("id", "=", conversationId)
386
414
  .executeTakeFirst();
387
415
  if (!conv)
@@ -448,8 +476,6 @@ let RunWorker = class RunWorker {
448
476
  delayMs = QUOTA_FULL_DELAY_MS;
449
477
  }
450
478
  else {
451
- // 运行开始执行(含续跑、stalled 后接手):通知旁路(渠道出站投递据此把输出投到 IM),失败不影响运行
452
- void this.events.emit("run.started", { conversationId, runId });
453
479
  const r = await this.execute(run, { id: conversationId, ...conv }, state).catch(async (err) => {
454
480
  if (state.abandoned)
455
481
  return "abandoned";
@@ -574,6 +600,14 @@ let RunWorker = class RunWorker {
574
600
  saved = await normalizeStoredDefinitionIn(this.db, conv.org_id, version.definition);
575
601
  agent = saved;
576
602
  }
603
+ // the trigger account's role, read once per run for the plugin context and plugin tool
604
+ // handlers (memory plugin spec §6.2); an account no longer in the org counts as a guest
605
+ const role = (await this.db
606
+ .selectFrom("memberships")
607
+ .select("role")
608
+ .where("org_id", "=", conv.org_id)
609
+ .where("account_id", "=", run.trigger_account_id)
610
+ .executeTakeFirst())?.role ?? "guest";
577
611
  let startedAt = run.started_at;
578
612
  // The model / effort switch in effect (UI refresh B): the latest `model_switched` before this
579
613
  // run's `run_started`, read under the conversation lock when the run starts, so a replay
@@ -585,25 +619,36 @@ let RunWorker = class RunWorker {
585
619
  }
586
620
  if (run.status === "queued" || startedAt === null) {
587
621
  if (!existingStart) {
622
+ await this.hooks?.beforeRunStarted?.(runId, conversationId);
588
623
  if (saved !== undefined)
589
624
  await this.fallBackIfUnavailable(conv.org_id, conversationId, runId, saved);
590
625
  const started = await this.appendRunStarted(conversationId, run, agent, saved);
626
+ // the status was read before the conversation lock: a stop that ended the run meanwhile
627
+ // (a queued run's direct abort, an orphan abort) wins, and the run never starts
628
+ if (started.terminal)
629
+ return "finished";
591
630
  agent = started.agent;
592
631
  const toSeq = started.toSeq;
593
632
  if (toSeq !== undefined)
594
633
  await this.eventLog.notify(conversationId, toSeq);
595
634
  }
596
- // 技能目录快照(Ruling M17):在置为 running 之前写,崩溃重试时仍走这里(幂等键去重)
597
- await this.skillsCatalog.record({
635
+ // Skills catalog snapshot (ruling M17): written before the run is running, and again on a
636
+ // crash retry (deduped by its idem key). Both records re-read the run under the conversation
637
+ // and run row locks in their append transactions: a stop that ended it meanwhile (a retried
638
+ // start reads `queued` before any lock) wins, nothing is appended after its end, and the run
639
+ // never starts.
640
+ const catalogged = await this.skillsCatalog.record({
598
641
  orgId: conv.org_id,
599
642
  agentId: conv.agent_id,
600
643
  conversationId,
601
644
  runId,
602
645
  agent,
603
646
  }, { log: this.log });
647
+ if (!catalogged)
648
+ return "finished";
604
649
  // run context (spec §5): after the catalog, before the run is running, so before any
605
650
  // compaction and model call; a retried run start finds its event or no change
606
- await this.runContext.record({
651
+ const contexted = await this.runContext.record({
607
652
  orgId: conv.org_id,
608
653
  conversationId,
609
654
  runId,
@@ -617,14 +662,47 @@ let RunWorker = class RunWorker {
617
662
  },
618
663
  }),
619
664
  });
665
+ if (!contexted)
666
+ return "finished";
667
+ // plugin context (memory plugin spec §6.1): after the run context, still before the run is
668
+ // running; the providers are asked outside any transaction, a chat run only, so a stop may
669
+ // end the run meanwhile: its append transaction re-reads the run under the locks
670
+ const pluginContexted = await this.pluginContext.record({
671
+ orgId: conv.org_id,
672
+ conversationId,
673
+ runId,
674
+ agentId: conv.agent_id,
675
+ agentName: agent.name,
676
+ ownerAccountId: conv.account_id,
677
+ // only a chat run asks the providers
678
+ ownerName: conv.kind === "chat" ? await accountName(this.db, conv.account_id) : "",
679
+ triggerAccountId: run.trigger_account_id,
680
+ source: conv.source,
681
+ role,
682
+ kind: conv.kind,
683
+ toolKeysByPlugin: () => this.pluginToolKeys(conv, agent),
684
+ // the run's own abort watch starts with its execution below: until then, the shutdown
685
+ signal: state.shutdown.signal,
686
+ });
687
+ if (!pluginContexted)
688
+ return "finished";
689
+ await this.hooks?.beforeRunning?.(runId, conversationId);
690
+ // never over a terminal status: a run finished meanwhile stays finished
620
691
  const updated = await this.db
621
692
  .updateTable("runs")
622
693
  .set({ status: "running", started_at: sql `COALESCE(started_at, now())` })
623
694
  .where("id", "=", runId)
695
+ .where("status", "in", ["queued", "running"])
624
696
  .returning("started_at")
625
- .executeTakeFirstOrThrow();
697
+ .executeTakeFirst();
698
+ if (updated === undefined)
699
+ return "finished";
626
700
  startedAt = updated.started_at;
627
701
  }
702
+ // the run executes (a first start, a resume, a stalled takeover): a hint to the side paths (the
703
+ // channel outbound delivers its output to IM), after the start went through, never for a run a
704
+ // stop ended first; a failure does not affect the run
705
+ void this.events.emit("run.started", { conversationId, runId });
628
706
  // 截止时间以首次开始为准:停机后在其他 worker 续跑也不重新计时;等人处理的时长(waited_ms)不计入。
629
707
  // `runtime.maxRunSeconds` 0: no deadline, no timer (the run ends when the model stops or is stopped)
630
708
  const deadline = runDeadline(startedAt.getTime() + Number(run.waited_ms), runtime.maxRunSeconds);
@@ -688,6 +766,9 @@ let RunWorker = class RunWorker {
688
766
  runId,
689
767
  agentId: conv.agent_id,
690
768
  triggerAccountId: run.trigger_account_id,
769
+ source: conv.source,
770
+ ownerAccountId: conv.account_id,
771
+ role,
691
772
  agent,
692
773
  agentVersion: run.agent_version,
693
774
  signal: ac.signal,
@@ -788,6 +869,28 @@ let RunWorker = class RunWorker {
788
869
  // a completed run enqueues the conversation's title job (never throws)
789
870
  void this.titles.maybeEnqueue(conversationId);
790
871
  }
872
+ /**
873
+ * The run's extension tool keys by plugin, resolved as a chat run's first step lists them
874
+ * (disabled tools, org switches, the conversation's source): a provider's `tools`.
875
+ */
876
+ async pluginToolKeys(conv, agent) {
877
+ const { tools } = await this.tools.forAgent(conv.org_id, {
878
+ disabledTools: agent.disabledTools,
879
+ subagentTools: agent.subagentTools,
880
+ subagents: agent.subagents,
881
+ sandboxConversationId: conv.id,
882
+ source: conv.source,
883
+ });
884
+ const out = new Map();
885
+ for (const t of tools) {
886
+ if (t.source.kind !== "plugin")
887
+ continue;
888
+ const keys = out.get(t.source.plugin) ?? [];
889
+ keys.push(t.key);
890
+ out.set(t.source.plugin, keys);
891
+ }
892
+ return out;
893
+ }
791
894
  /**
792
895
  * When the conversation was switched to a model that no longer resolves (provider disabled,
793
896
  * model removed), writes `model_switched { model: null, effort: null, reason: "unavailable" }`
@@ -820,6 +923,26 @@ let RunWorker = class RunWorker {
820
923
  await this.eventLog.notify(conversationId, ev.seq);
821
924
  await this.eventLog.publishChanged(conversationId, "selection");
822
925
  }
926
+ /**
927
+ * Whether the run is gone or terminal, read in the caller's transaction under the locks
928
+ * (conversation, then run, as `RunFinish.finishIn`): the job read its status before taking the
929
+ * conversation lock, and a stop may have ended it since.
930
+ */
931
+ async endedUnderLocks(conversationId, runId) {
932
+ await this.db
933
+ .selectFrom("conversations")
934
+ .select("id")
935
+ .where("id", "=", conversationId)
936
+ .forUpdate()
937
+ .execute();
938
+ const row = await this.db
939
+ .selectFrom("runs")
940
+ .select("status")
941
+ .where("id", "=", runId)
942
+ .forUpdate()
943
+ .executeTakeFirst();
944
+ return !row || TERMINAL_STATUSES.has(row.status);
945
+ }
823
946
  /**
824
947
  * `run_started` under the conversation row lock, with the model / effort switch in effect then
825
948
  * (a chat run's `saved` version under the conversation's selection): what the run uses.
@@ -828,12 +951,9 @@ let RunWorker = class RunWorker {
828
951
  /** The definition so far (a sub-agent's frozen one is final). */
829
952
  current, saved) {
830
953
  const t = this.db;
831
- await t
832
- .selectFrom("conversations")
833
- .select("id")
834
- .where("id", "=", conversationId)
835
- .forUpdate()
836
- .execute();
954
+ if (await this.endedUnderLocks(conversationId, run.id)) {
955
+ return { toSeq: undefined, agent: current, terminal: true };
956
+ }
837
957
  const agent = saved !== undefined
838
958
  ? effectiveDefinition(saved, await readSelection(t, conversationId))
839
959
  : current;
@@ -951,10 +1071,12 @@ RunWorker = __decorate([
951
1071
  __param(16, Inject(ProviderRegistry)),
952
1072
  __param(17, Inject(SkillsCatalogService)),
953
1073
  __param(18, Inject(RunContextService)),
954
- __param(19, Inject(RunFinish)),
955
- __param(20, Inject(TransactionHost)),
956
- __param(21, Inject(RunDrafts)),
957
- __param(22, Optional()),
958
- __param(22, Inject(RUN_HOOKS))
1074
+ __param(19, Inject(PluginContextService)),
1075
+ __param(20, Inject(ToolRegistry)),
1076
+ __param(21, Inject(RunFinish)),
1077
+ __param(22, Inject(TransactionHost)),
1078
+ __param(23, Inject(RunDrafts)),
1079
+ __param(24, Optional()),
1080
+ __param(24, Inject(RUN_HOOKS))
959
1081
  ], RunWorker);
960
1082
  export { RunWorker };
@@ -23,6 +23,7 @@ import { RunAbort } from "./abort.js";
23
23
  import { KyselyCheckpointSaver } from "./checkpointer.js";
24
24
  import { ConversationLock } from "./conversation-lock.js";
25
25
  import { MAINTENANCE_QUEUE_NAME } from "./maintenance-queue.js";
26
+ import { PluginContextService } from "./plugin-context.service.js";
26
27
  import { ProvidersModule } from "./providers.js";
27
28
  import { EVAL_QUOTA, EVAL_QUOTA_PREFIX, OrgQuota, RUN_QUOTA, RUN_QUOTA_PREFIX } from "./quota.js";
28
29
  import { RunContextService } from "./run-context.service.js";
@@ -83,6 +84,7 @@ RuntimeModule = __decorate([
83
84
  AgentLoops,
84
85
  RunSteps,
85
86
  RunContextService,
87
+ PluginContextService,
86
88
  RunWorker,
87
89
  // a finished background task wakes an idle conversation (background Bash spec §3)
88
90
  BackgroundWake,
@@ -9,10 +9,10 @@ import { UsageReportJobs } from "./usage-report-jobs.js";
9
9
  export type StepAuditEntry = Parameters<typeof audit>[1];
10
10
  /**
11
11
  * The writes of a run step that go to the event log together with another row, each in a
12
- * transaction of its own (the caller notifies the log after the commit): a model call with the
13
- * tool definitions it sent, a denial with its audit row, usage events with their owed
14
- * `usage.report` deliveries (transactional outbox spec §8.1), and a sub-agent's usage counted on
15
- * its parent run. The step functions (`model-call.ts`, `hook-calls.ts`) reach it through their deps.
12
+ * transaction of its own (the caller notifies the log after the commit): a model call with the tool
13
+ * definitions it sent, a denial with its audit row, usage events with their owed `usage.report`
14
+ * deliveries (transactional outbox spec §8.1), and a sub-agent's usage counted on its parent run.
15
+ * The step functions (`model-call.ts`, `hook-calls.ts`) reach it through their deps.
16
16
  */
17
17
  export declare class StepLog {
18
18
  private readonly kdb;
@@ -35,7 +35,8 @@ export declare class StepLog {
35
35
  * the whole tree counts on one run): `counter.budgetRunId`, the run's own window
36
36
  * (`SubagentParent.budgetRunId`), else the parent run's ({@link budgetRunOf}). A usage event
37
37
  * already in the log (an idempotent hit after a crash) adds nothing, so each call counts exactly
38
- * once; the new usage events' `usage.report` deliveries are owed in the same transaction. Returns what was written (hits included) and the highest new seq (null: nothing new).
38
+ * once; the new usage events' `usage.report` deliveries are owed in the same transaction. Returns
39
+ * what was written (hits included) and the highest new seq (null: nothing new).
39
40
  */
40
41
  appendCounted(conversationId: string, counter: {
41
42
  parentRunId: string;
@@ -20,10 +20,10 @@ import { budgetRunOf } from "./subagents/limits.js";
20
20
  import { UsageReportJobs, UsageReportJobsModule } from "./usage-report-jobs.js";
21
21
  /**
22
22
  * The writes of a run step that go to the event log together with another row, each in a
23
- * transaction of its own (the caller notifies the log after the commit): a model call with the
24
- * tool definitions it sent, a denial with its audit row, usage events with their owed
25
- * `usage.report` deliveries (transactional outbox spec §8.1), and a sub-agent's usage counted on
26
- * its parent run. The step functions (`model-call.ts`, `hook-calls.ts`) reach it through their deps.
23
+ * transaction of its own (the caller notifies the log after the commit): a model call with the tool
24
+ * definitions it sent, a denial with its audit row, usage events with their owed `usage.report`
25
+ * deliveries (transactional outbox spec §8.1), and a sub-agent's usage counted on its parent run.
26
+ * The step functions (`model-call.ts`, `hook-calls.ts`) reach it through their deps.
27
27
  */
28
28
  let StepLog = class StepLog {
29
29
  kdb;
@@ -53,7 +53,8 @@ let StepLog = class StepLog {
53
53
  * the whole tree counts on one run): `counter.budgetRunId`, the run's own window
54
54
  * (`SubagentParent.budgetRunId`), else the parent run's ({@link budgetRunOf}). A usage event
55
55
  * already in the log (an idempotent hit after a crash) adds nothing, so each call counts exactly
56
- * once; the new usage events' `usage.report` deliveries are owed in the same transaction. Returns what was written (hits included) and the highest new seq (null: nothing new).
56
+ * once; the new usage events' `usage.report` deliveries are owed in the same transaction. Returns
57
+ * what was written (hits included) and the highest new seq (null: nothing new).
57
58
  */
58
59
  async appendCounted(conversationId, counter, events) {
59
60
  const t = this.kdb.db;
@@ -121,6 +121,7 @@ export async function dispatchSubagent(d, a) {
121
121
  parent_conversation_id: ctx.conversationId,
122
122
  subagent_type: entry.type,
123
123
  subagent_depth: childDepth,
124
+ source: "subagent",
124
125
  })
125
126
  .execute();
126
127
  const [, task] = await d.eventLog.appendIn(childId, [
@@ -125,6 +125,9 @@ export async function advanceSkillForks(d, a) {
125
125
  agentVersion: ctx.agentVersion,
126
126
  agent: ctx.agent,
127
127
  accountId: ctx.triggerAccountId,
128
+ source: ctx.source,
129
+ ownerAccountId: ctx.ownerAccountId,
130
+ role: ctx.role,
128
131
  signal: ctx.signal,
129
132
  ...(ctx.limits.deadline !== undefined && { deadline: ctx.limits.deadline }),
130
133
  ...(ctx.conversationKind !== undefined &&
@@ -58,7 +58,9 @@ export declare function narrowPending(stored: readonly string[] | undefined, fai
58
58
  * `model_usage` row and owes job `usage-model-<rowId>` with it; the payload
59
59
  * (`purpose: "embedding"`, `eventId` = the row id, the calling `extension`, no conversation fields)
60
60
  * is read from the row when the job runs, and the row's `reported_at` is set once nothing is left
61
- * to deliver; a job that gives up after its last attempt sets `report_failed_at`.
61
+ * to deliver; a job that gives up after its last attempt sets `report_failed_at`. A row with
62
+ * `reported_at IS NULL` is not necessarily pending: no job is owed when no enabled extension
63
+ * subscribed to `usage.report` at the time of the call, and such a row is never reported.
62
64
  */
63
65
  export declare class UsageReporter {
64
66
  private readonly kdb;
@@ -113,7 +113,9 @@ export function narrowPending(stored, failed) {
113
113
  * `model_usage` row and owes job `usage-model-<rowId>` with it; the payload
114
114
  * (`purpose: "embedding"`, `eventId` = the row id, the calling `extension`, no conversation fields)
115
115
  * is read from the row when the job runs, and the row's `reported_at` is set once nothing is left
116
- * to deliver; a job that gives up after its last attempt sets `report_failed_at`.
116
+ * to deliver; a job that gives up after its last attempt sets `report_failed_at`. A row with
117
+ * `reported_at IS NULL` is not necessarily pending: no job is owed when no enabled extension
118
+ * subscribed to `usage.report` at the time of the call, and such a row is never reported.
117
119
  */
118
120
  let UsageReporter = class UsageReporter {
119
121
  kdb;
@@ -359,7 +361,7 @@ let UsageReporter = class UsageReporter {
359
361
  /** One delivery job (the `@Processor` of {@link USAGE_REPORT_QUEUE} calls it). */
360
362
  async process(j, token) {
361
363
  const payload = await this.payloadOfJob(j.data);
362
- // conversation (or org) deleted meanwhile: nothing to report
364
+ // conversation deleted meanwhile: nothing to report
363
365
  if (payload === null)
364
366
  return;
365
367
  const subscribers = await this.subscribers(payload.orgId);
@@ -230,6 +230,13 @@ export declare class Waits implements OnApplicationShutdown {
230
230
  * background tasks of every covered sub-agent conversation stop, whether it was running or idle.
231
231
  * Returns the number of runs asked to stop.
232
232
  *
233
+ * `windowsUpTo` (a root run's id; only after that root run a person stopped ended,
234
+ * {@link stopDescendantsOfStopped}) narrows the stop to the runs of the budget windows opened up
235
+ * to that run (`runs.budget_run_id`, agent map spec §6): a run of a window a later root run
236
+ * opened (the person's next message, started meanwhile) is never stopped by the late call, and
237
+ * the background task sweep is left out (a stopped child's own end stops its tasks,
238
+ * {@link childFinished}).
239
+ *
233
240
  * Lock order: nothing is held across runs. Each run's stop is its own transaction (or none), and
234
241
  * one that writes the terminal state locks that run's conversation row, then its run row, the
235
242
  * order of `StepLog.appendCounted` (conversation row, then the root's budget run row) and of
@@ -238,14 +245,17 @@ export declare class Waits implements OnApplicationShutdown {
238
245
  */
239
246
  stopTree(conversationId: string, byAccountId: string | null, opts?: {
240
247
  includeSelf?: boolean;
248
+ windowsUpTo?: string;
241
249
  }): Promise<number>;
242
250
  /**
243
- * After a sub-agent run's terminal commit (its worker): a run a person stopped (`run_aborted`
244
- * with an account and no `detail`; only {@link stopTree} stops a sub-agent's run that way) stops
245
- * the active runs below its conversation as well, so a descendant it dispatched after the tree
246
- * stop read the tree still stops (dispatch needs its run `running`, so after its terminal commit
247
- * the set is final). Any other end leaves its children running (agent map spec §5.4). Returns
248
- * the number of runs asked to stop. A root conversation's run never stops its children here.
251
+ * After a run's terminal commit (its worker): a run a person stopped (`run_aborted` with an
252
+ * account and no `detail`; {@link stopTree} or a person's stop of the root) stops the active runs
253
+ * below its conversation as well, so a descendant it dispatched after the tree stop read the tree
254
+ * still stops (dispatch needs its run `running`, so after its terminal commit the set is final).
255
+ * A root's run stops only the runs of the budget windows up to it (`windowsUpTo`): the person's
256
+ * next message may already have started a new run with sub-agents of its own (agent map spec §6,
257
+ * changed in acceptance 2026-11-13). Any other end leaves its children running (§5.4). Returns
258
+ * the number of runs asked to stop.
249
259
  */
250
260
  stopDescendantsOfStopped(runId: string): Promise<number>;
251
261
  /**
@@ -607,6 +607,13 @@ let Waits = class Waits {
607
607
  * background tasks of every covered sub-agent conversation stop, whether it was running or idle.
608
608
  * Returns the number of runs asked to stop.
609
609
  *
610
+ * `windowsUpTo` (a root run's id; only after that root run a person stopped ended,
611
+ * {@link stopDescendantsOfStopped}) narrows the stop to the runs of the budget windows opened up
612
+ * to that run (`runs.budget_run_id`, agent map spec §6): a run of a window a later root run
613
+ * opened (the person's next message, started meanwhile) is never stopped by the late call, and
614
+ * the background task sweep is left out (a stopped child's own end stops its tasks,
615
+ * {@link childFinished}).
616
+ *
610
617
  * Lock order: nothing is held across runs. Each run's stop is its own transaction (or none), and
611
618
  * one that writes the terminal state locks that run's conversation row, then its run row, the
612
619
  * order of `StepLog.appendCounted` (conversation row, then the root's budget run row) and of
@@ -619,12 +626,22 @@ let Waits = class Waits {
619
626
  if (ids.length === 0)
620
627
  return 0;
621
628
  const level = new Map(tree.map((c) => [c.id, c.level]));
622
- const runs = (await this.db
629
+ let q = this.db
623
630
  .selectFrom("runs")
624
631
  .select(["id", "conversation_id", "status"])
625
632
  .where("conversation_id", "in", ids)
626
- .where("status", "in", ACTIVE_RUN_STATUSES)
627
- .execute()).sort((a, b) => (level.get(a.conversation_id) ?? 0) - (level.get(b.conversation_id) ?? 0));
633
+ .where("status", "in", ACTIVE_RUN_STATUSES);
634
+ const upTo = opts.windowsUpTo;
635
+ if (upTo !== undefined) {
636
+ // a window is opened by a root run without a budget run of its own (`budgetWindowOf`)
637
+ q = q.where("budget_run_id", "in", (eb) => eb
638
+ .selectFrom("runs as w")
639
+ .select("w.id")
640
+ .where("w.conversation_id", "=", conversationId)
641
+ .where("w.budget_run_id", "is", null)
642
+ .where("w.created_at", "<=", (eb2) => eb2.selectFrom("runs as s").select("s.created_at").where("s.id", "=", upTo)));
643
+ }
644
+ const runs = (await q.execute()).sort((a, b) => (level.get(a.conversation_id) ?? 0) - (level.get(b.conversation_id) ?? 0));
628
645
  for (const r of runs) {
629
646
  try {
630
647
  // a stopped wait's card updates are owed by its transaction's appends (outbox spec §8.1)
@@ -639,34 +656,39 @@ let Waits = class Waits {
639
656
  // all of each conversation's tasks, as a stopped child's run end does (`childFinished`), with
640
657
  // which this is idempotent (only running tasks are stopped; `stoppedBy: "subagent_end"`
641
658
  // never wakes). A root conversation is skipped (`stopSubagentTasks`); never throws.
642
- await Promise.all(ids.map((id) => this.background.stopSubagentTasks(id)));
659
+ if (upTo === undefined) {
660
+ await Promise.all(ids.map((id) => this.background.stopSubagentTasks(id)));
661
+ }
643
662
  return runs.length;
644
663
  }
645
664
  /**
646
- * After a sub-agent run's terminal commit (its worker): a run a person stopped (`run_aborted`
647
- * with an account and no `detail`; only {@link stopTree} stops a sub-agent's run that way) stops
648
- * the active runs below its conversation as well, so a descendant it dispatched after the tree
649
- * stop read the tree still stops (dispatch needs its run `running`, so after its terminal commit
650
- * the set is final). Any other end leaves its children running (agent map spec §5.4). Returns
651
- * the number of runs asked to stop. A root conversation's run never stops its children here.
665
+ * After a run's terminal commit (its worker): a run a person stopped (`run_aborted` with an
666
+ * account and no `detail`; {@link stopTree} or a person's stop of the root) stops the active runs
667
+ * below its conversation as well, so a descendant it dispatched after the tree stop read the tree
668
+ * still stops (dispatch needs its run `running`, so after its terminal commit the set is final).
669
+ * A root's run stops only the runs of the budget windows up to it (`windowsUpTo`): the person's
670
+ * next message may already have started a new run with sub-agents of its own (agent map spec §6,
671
+ * changed in acceptance 2026-11-13). Any other end leaves its children running (§5.4). Returns
672
+ * the number of runs asked to stop.
652
673
  */
653
674
  async stopDescendantsOfStopped(runId) {
654
675
  const ev = await this.db
655
676
  .selectFrom("runs as r")
656
677
  .innerJoin("conversation_events as e", (j) => j.onRef("e.conversation_id", "=", "r.conversation_id").onRef("e.run_id", "=", "r.id"))
657
678
  .innerJoin("conversations as c", "c.id", "r.conversation_id")
658
- .select(["r.conversation_id", "e.payload"])
679
+ .select(["r.conversation_id", "c.kind", "e.payload"])
659
680
  .where("r.id", "=", runId)
660
- // a root's stop leaves its children running (agent map spec §6): a person-stopped root run
661
- // reaching here (a redelivered job, the failure path) stops nothing
662
- .where("c.kind", "=", "subagent")
681
+ .where("c.kind", "in", ["chat", "subagent"])
663
682
  .where("r.status", "=", "aborted")
664
683
  .where("e.type", "=", "run_aborted")
665
684
  .executeTakeFirst();
666
685
  const p = ev?.payload;
667
686
  if (!ev || typeof p?.byAccountId !== "string" || p.detail !== undefined)
668
687
  return 0;
669
- return this.stopTree(ev.conversation_id, p.byAccountId, { includeSelf: false });
688
+ return this.stopTree(ev.conversation_id, p.byAccountId, {
689
+ includeSelf: false,
690
+ ...(ev.kind === "chat" && { windowsUpTo: runId }),
691
+ });
670
692
  }
671
693
  /**
672
694
  * Stops one active sub-agent run: a `waiting` one through its own {@link abortWaiting}, a
@@ -1,5 +1,6 @@
1
1
  import type { Job, Queue } from "bullmq";
2
2
  import { LockService } from "../infra/lock/lock.service.js";
3
+ import type { OutboxService } from "../infra/outbox/outbox.service.js";
3
4
  import type { KernelLogger } from "../log.js";
4
5
  import { SandboxService } from "./sandbox.service.js";
5
6
  /** The idle suspension sweep (every minute, once per tick across replicas). */
@@ -7,10 +8,25 @@ export declare const SANDBOX_SUSPEND_JOB = "sandbox-suspend-idle";
7
8
  /** The inactivity destruction sweep (hourly, once per tick across replicas). */
8
9
  export declare const SANDBOX_DESTROY_JOB = "sandbox-destroy-inactive";
9
10
  /**
10
- * One-off job: destroy a batch of devices now, instance and file system (device
11
- * deletion, offboarding; data `{ deviceIds }`).
11
+ * One-off job: destroy devices now, instance and file system (data `{ deviceIds }`). Owed through
12
+ * the outbox by the transaction that soft-deletes the device (a device's deletion, offboarding),
13
+ * one device per job under {@link deviceDestroyJobId} ({@link oweDeviceDestroy}).
12
14
  */
13
15
  export declare const SANDBOX_DESTROY_FOR_JOB = "sandbox-destroy-for";
16
+ /** The data of a {@link SANDBOX_DESTROY_FOR_JOB} job. */
17
+ export interface SandboxDestroyForData {
18
+ deviceIds: string[];
19
+ }
20
+ /**
21
+ * The deterministic id of a deleted device's destroy job (no `:`): a device is deleted once, so
22
+ * its destruction is owed once.
23
+ */
24
+ export declare const deviceDestroyJobId: (deviceId: string) => string;
25
+ /**
26
+ * Owes the destruction of a device in the caller's transaction, the one that soft-deleted it
27
+ * (transactional outbox spec §8.1; throws outside a transaction).
28
+ */
29
+ export declare function oweDeviceDestroy(outbox: Pick<OutboxService, "add">, deviceId: string, orgId: string): Promise<boolean>;
14
30
  /**
15
31
  * One-off job: start a device over with empty files (`resetFileSystem`; data
16
32
  * `{ deviceId }`, job id {@link resetFilesJobId}).
@@ -22,18 +38,12 @@ export declare const SANDBOX_RESET_FILES_JOB = "sandbox-reset-files";
22
38
  */
23
39
  export declare const resetFilesJobId: (deviceId: string) => string;
24
40
  /**
25
- * The producer of the sandbox maintenance jobs: {@link SANDBOX_DESTROY_FOR_JOB} (BullMQ's own job
26
- * ids, as before: every enqueue is a new job, also after a batch failed all its attempts) and
27
- * {@link SANDBOX_RESET_FILES_JOB}; `MaintenanceProcessor` consumes them.
41
+ * The producer of {@link SANDBOX_RESET_FILES_JOB} (destroys are owed through the outbox,
42
+ * {@link oweDeviceDestroy}); `MaintenanceProcessor` consumes both.
28
43
  */
29
44
  export declare class SandboxJobs {
30
45
  private readonly queue;
31
46
  constructor(queue: Pick<Queue, "add">);
32
- /**
33
- * Enqueues the destruction of these devices: a failed batch is retried whole (destroying
34
- * one already destroyed does nothing).
35
- */
36
- destroy(deviceIds: string[]): Promise<void>;
37
47
  /**
38
48
  * Enqueues the reset of a device's files (see {@link resetFilesJobId}): retried on failure
39
49
  * (the exclusive lease not obtained in time, the provider failing); a job that failed all its