@capybara-hq/kernel 0.1.0-rc.23 → 0.1.0-rc.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/bootstrap.json +25 -25
  2. package/dist/admin/admin.module.d.ts +2 -1
  3. package/dist/admin/admin.module.js +9 -3
  4. package/dist/admin/health.service.d.ts +2 -2
  5. package/dist/admin/health.service.js +6 -2
  6. package/dist/admin/outbox-dead.controller.d.ts +23 -0
  7. package/dist/admin/outbox-dead.controller.js +62 -0
  8. package/dist/admin/outbox-dead.schemas.d.ts +8 -0
  9. package/dist/admin/outbox-dead.schemas.js +18 -0
  10. package/dist/admin/outbox-dead.service.d.ts +66 -0
  11. package/dist/admin/outbox-dead.service.js +179 -0
  12. package/dist/admin/overview.service.d.ts +40 -5
  13. package/dist/admin/overview.service.js +41 -7
  14. package/dist/agents/agents.module.js +3 -0
  15. package/dist/agents/agents.service.d.ts +6 -1
  16. package/dist/agents/agents.service.js +11 -2
  17. package/dist/agents/from-template.js +1 -1
  18. package/dist/background/finish.d.ts +5 -2
  19. package/dist/background/finish.js +14 -3
  20. package/dist/background/launch.d.ts +2 -1
  21. package/dist/background/launch.js +5 -2
  22. package/dist/background/store.d.ts +8 -0
  23. package/dist/background/store.js +24 -0
  24. package/dist/catalog/admin-catalog.controller.d.ts +21 -2
  25. package/dist/catalog/admin-catalog.controller.js +45 -6
  26. package/dist/catalog/admin-catalog.schemas.d.ts +5 -0
  27. package/dist/catalog/admin-catalog.schemas.js +8 -0
  28. package/dist/catalog/catalog-import.module.js +0 -4
  29. package/dist/catalog/catalog-scope.service.d.ts +63 -26
  30. package/dist/catalog/catalog-scope.service.js +146 -71
  31. package/dist/catalog/catalog.module.d.ts +2 -1
  32. package/dist/catalog/catalog.module.js +9 -4
  33. package/dist/catalog/catalog.service.d.ts +11 -4
  34. package/dist/catalog/catalog.service.js +21 -5
  35. package/dist/catalog/cli.d.ts +4 -3
  36. package/dist/catalog/cli.js +8 -13
  37. package/dist/catalog/job-ids.d.ts +8 -20
  38. package/dist/catalog/job-ids.js +11 -16
  39. package/dist/catalog/marketplaces.service.d.ts +1 -1
  40. package/dist/catalog/org-plugin-uploads.d.ts +11 -4
  41. package/dist/catalog/org-plugin-uploads.js +21 -6
  42. package/dist/catalog/owned-items.d.ts +6 -13
  43. package/dist/catalog/owned-items.js +11 -29
  44. package/dist/catalog/owned-plugin-cleanup.d.ts +8 -17
  45. package/dist/catalog/owned-plugin-cleanup.js +12 -66
  46. package/dist/catalog/platform-components.d.ts +56 -34
  47. package/dist/catalog/platform-components.js +68 -78
  48. package/dist/catalog/platform-import-update.d.ts +50 -0
  49. package/dist/catalog/platform-import-update.js +116 -0
  50. package/dist/catalog/plugin-import.d.ts +64 -26
  51. package/dist/catalog/plugin-import.js +113 -77
  52. package/dist/catalog/plugin-upgrade.d.ts +22 -4
  53. package/dist/catalog/plugin-upgrade.js +26 -19
  54. package/dist/catalog/scope-jobs.d.ts +59 -0
  55. package/dist/catalog/scope-jobs.js +65 -0
  56. package/dist/catalog/scope-sync-workers.d.ts +22 -0
  57. package/dist/catalog/scope-sync-workers.js +58 -2
  58. package/dist/catalog/scope-sync.d.ts +40 -65
  59. package/dist/catalog/scope-sync.js +114 -122
  60. package/dist/channels/channels.module.d.ts +5 -2
  61. package/dist/channels/channels.module.js +9 -2
  62. package/dist/channels/deliver-job.d.ts +25 -0
  63. package/dist/channels/deliver-job.js +25 -0
  64. package/dist/channels/inbound.d.ts +1 -1
  65. package/dist/channels/inbound.js +4 -17
  66. package/dist/channels/jobs.d.ts +28 -18
  67. package/dist/channels/jobs.js +63 -26
  68. package/dist/channels/threads.d.ts +4 -3
  69. package/dist/channels/threads.js +4 -3
  70. package/dist/config/schema.d.ts +0 -3
  71. package/dist/config/schema.js +11 -20
  72. package/dist/conversations/agent-tree.d.ts +16 -0
  73. package/dist/conversations/agent-tree.js +36 -0
  74. package/dist/conversations/conversations.controller.js +4 -2
  75. package/dist/conversations/conversations.service.d.ts +61 -19
  76. package/dist/conversations/conversations.service.js +69 -43
  77. package/dist/conversations/event-log.d.ts +7 -10
  78. package/dist/conversations/event-log.js +24 -15
  79. package/dist/conversations/events.d.ts +21 -0
  80. package/dist/conversations/events.js +26 -2
  81. package/dist/conversations/queue.service.d.ts +8 -3
  82. package/dist/conversations/queue.service.js +37 -13
  83. package/dist/conversations/source.d.ts +10 -0
  84. package/dist/conversations/source.js +17 -0
  85. package/dist/conversations/titles.d.ts +21 -8
  86. package/dist/conversations/titles.js +42 -15
  87. package/dist/conversations/tree.d.ts +10 -0
  88. package/dist/conversations/tree.js +35 -0
  89. package/dist/db/kernel-schema.d.ts +35 -22
  90. package/dist/devices/devices.service.d.ts +23 -11
  91. package/dist/devices/devices.service.js +34 -26
  92. package/dist/errors.d.ts +11 -0
  93. package/dist/errors.js +11 -0
  94. package/dist/evals/run-replay.d.ts +13 -2
  95. package/dist/evals/run-replay.js +42 -9
  96. package/dist/evals/snapshot.d.ts +7 -1
  97. package/dist/evals/snapshot.js +2 -1
  98. package/dist/extensions/extensions.module.js +3 -0
  99. package/dist/extensions/hooks.d.ts +22 -3
  100. package/dist/extensions/hooks.js +15 -1
  101. package/dist/extensions/lifecycle-hooks.d.ts +99 -0
  102. package/dist/extensions/lifecycle-hooks.js +273 -0
  103. package/dist/extensions/lifecycle.d.ts +6 -1
  104. package/dist/extensions/lifecycle.js +9 -2
  105. package/dist/extensions/org-config.d.ts +13 -1
  106. package/dist/extensions/org-config.js +19 -2
  107. package/dist/extensions/permission-view.d.ts +9 -5
  108. package/dist/extensions/permission-view.js +9 -4
  109. package/dist/extensions/point-defaults.d.ts +28 -1
  110. package/dist/extensions/point-defaults.js +44 -13
  111. package/dist/extensions/usage-subscribers.d.ts +43 -0
  112. package/dist/extensions/usage-subscribers.js +108 -0
  113. package/dist/http/api.module.js +2 -0
  114. package/dist/inbox/inbox.service.d.ts +6 -20
  115. package/dist/inbox/inbox.service.js +0 -3
  116. package/dist/inbox/org-notices.d.ts +1 -1
  117. package/dist/inbox/platform-inbox.service.d.ts +3 -32
  118. package/dist/inbox/platform-inbox.service.js +5 -54
  119. package/dist/index.d.ts +1 -0
  120. package/dist/index.js +1 -0
  121. package/dist/infra/cli/kernel-cli.module.js +5 -0
  122. package/dist/infra/events/kernel-events.d.ts +9 -35
  123. package/dist/infra/outbox/outbox-relay.module.d.ts +6 -0
  124. package/dist/infra/outbox/outbox-relay.module.js +18 -0
  125. package/dist/infra/outbox/outbox.dispatcher.d.ts +103 -0
  126. package/dist/infra/outbox/outbox.dispatcher.js +310 -0
  127. package/dist/infra/outbox/outbox.module.d.ts +24 -0
  128. package/dist/infra/outbox/outbox.module.js +50 -0
  129. package/dist/infra/outbox/outbox.relay.d.ts +31 -0
  130. package/dist/infra/outbox/outbox.relay.js +76 -0
  131. package/dist/infra/outbox/outbox.service.d.ts +51 -0
  132. package/dist/infra/outbox/outbox.service.js +131 -0
  133. package/dist/infra/outbox/registry.d.ts +244 -0
  134. package/dist/infra/outbox/registry.js +234 -0
  135. package/dist/infra/queue/queue-names.d.ts +8 -0
  136. package/dist/infra/queue/queue-names.js +17 -0
  137. package/dist/kernel.module.js +3 -0
  138. package/dist/local-devices/device-policy.service.d.ts +4 -0
  139. package/dist/local-devices/device-policy.service.js +61 -3
  140. package/dist/local-devices/machines.d.ts +13 -5
  141. package/dist/local-devices/machines.js +45 -7
  142. package/dist/local-devices/policy-summary.d.ts +44 -1
  143. package/dist/local-devices/policy-summary.js +69 -0
  144. package/dist/models/embeddings.d.ts +9 -9
  145. package/dist/models/embeddings.js +11 -18
  146. package/dist/models/models.module.js +3 -0
  147. package/dist/orgs/directory-sync.js +1 -1
  148. package/dist/orgs/members.service.js +1 -1
  149. package/dist/orgs/offboarding.d.ts +50 -57
  150. package/dist/orgs/offboarding.js +117 -162
  151. package/dist/orgs/orgs.module.js +3 -0
  152. package/dist/orgs/orgs.service.d.ts +11 -5
  153. package/dist/orgs/orgs.service.js +22 -6
  154. package/dist/overrides.d.ts +23 -3
  155. package/dist/overrides.js +12 -2
  156. package/dist/platform/exclusions.js +5 -0
  157. package/dist/platform/ops/admin.ops.js +18 -2
  158. package/dist/platform/ops/catalog-admin.ops.d.ts +3 -1
  159. package/dist/platform/ops/catalog-admin.ops.js +32 -4
  160. package/dist/platform/ops/inbox.ops.js +4 -20
  161. package/dist/platform/ops/outbox.ops.d.ts +21 -0
  162. package/dist/platform/ops/outbox.ops.js +125 -0
  163. package/dist/platform/ops/platform.ops.d.ts +1 -0
  164. package/dist/platform/ops/platform.ops.js +6 -3
  165. package/dist/platform/ops/schedules.ops.js +40 -1
  166. package/dist/platform/platform-ops.module.d.ts +2 -1
  167. package/dist/platform/platform-ops.module.js +3 -0
  168. package/dist/platform/route-schemas.js +3 -0
  169. package/dist/roles/worker-role.d.ts +10 -5
  170. package/dist/roles/worker-role.js +19 -9
  171. package/dist/roles/worker.module.js +11 -8
  172. package/dist/runtime/context/build.d.ts +3 -0
  173. package/dist/runtime/context/build.js +15 -0
  174. package/dist/runtime/context/plugin-context.d.ts +20 -0
  175. package/dist/runtime/context/plugin-context.js +30 -0
  176. package/dist/runtime/langgraph-loop.js +17 -5
  177. package/dist/runtime/limits.d.ts +15 -0
  178. package/dist/runtime/limits.js +25 -0
  179. package/dist/runtime/loop.d.ts +16 -3
  180. package/dist/runtime/loop.js +2 -1
  181. package/dist/runtime/maintenance.d.ts +7 -3
  182. package/dist/runtime/maintenance.js +52 -12
  183. package/dist/runtime/model-call.d.ts +20 -17
  184. package/dist/runtime/model-call.js +15 -24
  185. package/dist/runtime/plugin-context.service.d.ts +69 -0
  186. package/dist/runtime/plugin-context.service.js +208 -0
  187. package/dist/runtime/queue-lost-jobs.d.ts +24 -0
  188. package/dist/runtime/queue-lost-jobs.js +51 -0
  189. package/dist/runtime/run-control.d.ts +47 -5
  190. package/dist/runtime/run-control.js +79 -12
  191. package/dist/runtime/run-finish.d.ts +39 -12
  192. package/dist/runtime/run-finish.js +97 -14
  193. package/dist/runtime/run-jobs.d.ts +95 -13
  194. package/dist/runtime/run-jobs.js +199 -28
  195. package/dist/runtime/run-orphan-check.job.d.ts +142 -0
  196. package/dist/runtime/run-orphan-check.job.js +339 -0
  197. package/dist/runtime/run-processors.d.ts +16 -9
  198. package/dist/runtime/run-processors.js +1 -1
  199. package/dist/runtime/run-queue.d.ts +75 -53
  200. package/dist/runtime/run-queue.js +189 -263
  201. package/dist/runtime/run-steps.d.ts +1 -2
  202. package/dist/runtime/run-steps.js +8 -11
  203. package/dist/runtime/run-worker.d.ts +26 -16
  204. package/dist/runtime/run-worker.js +132 -37
  205. package/dist/runtime/runtime.module.d.ts +2 -3
  206. package/dist/runtime/runtime.module.js +7 -6
  207. package/dist/runtime/step-log.d.ts +16 -4
  208. package/dist/runtime/step-log.js +26 -6
  209. package/dist/runtime/stream.controller.d.ts +2 -4
  210. package/dist/runtime/stream.controller.js +5 -11
  211. package/dist/runtime/subagents/child.js +1 -1
  212. package/dist/runtime/subagents/definition.d.ts +2 -1
  213. package/dist/runtime/subagents/definition.js +4 -2
  214. package/dist/runtime/subagents/deliveries.d.ts +6 -3
  215. package/dist/runtime/subagents/deliveries.js +16 -7
  216. package/dist/runtime/subagents/dispatch.d.ts +12 -12
  217. package/dist/runtime/subagents/dispatch.js +14 -38
  218. package/dist/runtime/subagents/messages.d.ts +5 -1
  219. package/dist/runtime/subagents/messages.js +11 -3
  220. package/dist/runtime/subagents/skill-fork.d.ts +3 -3
  221. package/dist/runtime/subagents/skill-fork.js +8 -5
  222. package/dist/runtime/subagents/subagents.service.js +1 -1
  223. package/dist/runtime/subagents/tree-jobs.d.ts +61 -0
  224. package/dist/runtime/subagents/tree-jobs.js +86 -0
  225. package/dist/runtime/usage-report-jobs.d.ts +23 -48
  226. package/dist/runtime/usage-report-jobs.js +67 -119
  227. package/dist/runtime/usage-report.d.ts +28 -77
  228. package/dist/runtime/usage-report.js +35 -213
  229. package/dist/runtime/waits.d.ts +48 -198
  230. package/dist/runtime/waits.js +103 -805
  231. package/dist/sandbox/jobs.d.ts +20 -10
  232. package/dist/sandbox/jobs.js +23 -17
  233. package/dist/sandbox/sandbox.service.d.ts +17 -4
  234. package/dist/sandbox/sandbox.service.js +55 -14
  235. package/dist/schedules/calendar.d.ts +134 -0
  236. package/dist/schedules/calendar.js +410 -0
  237. package/dist/schedules/fire.d.ts +26 -7
  238. package/dist/schedules/fire.js +91 -22
  239. package/dist/schedules/job-ids.d.ts +66 -17
  240. package/dist/schedules/job-ids.js +81 -13
  241. package/dist/schedules/run-tracker.d.ts +0 -5
  242. package/dist/schedules/run-tracker.js +2 -14
  243. package/dist/schedules/scan.job.d.ts +13 -32
  244. package/dist/schedules/scan.job.js +50 -164
  245. package/dist/schedules/schedule-math.d.ts +42 -0
  246. package/dist/schedules/schedule-math.js +52 -0
  247. package/dist/schedules/scheduled-tasks.service.d.ts +22 -6
  248. package/dist/schedules/scheduled-tasks.service.js +70 -14
  249. package/dist/schedules/schedules.controller.d.ts +5 -1
  250. package/dist/schedules/schedules.controller.js +15 -1
  251. package/dist/schedules/schedules.module.js +0 -4
  252. package/dist/schedules/schedules.schemas.d.ts +35 -0
  253. package/dist/schedules/schedules.schemas.js +27 -0
  254. package/dist/schedules/store.d.ts +5 -0
  255. package/dist/schedules/store.js +20 -0
  256. package/dist/skills/changes.controller.d.ts +1 -1
  257. package/dist/skills/changes.controller.js +1 -1
  258. package/dist/tools/approvals.service.d.ts +41 -11
  259. package/dist/tools/approvals.service.js +108 -14
  260. package/dist/tools/builtin-names.d.ts +5 -3
  261. package/dist/tools/builtin-names.js +12 -4
  262. package/dist/tools/builtins/builtin-tools.d.ts +1 -2
  263. package/dist/tools/builtins/builtin-tools.js +21 -17
  264. package/dist/tools/builtins/sandbox/bash.js +9 -3
  265. package/dist/tools/builtins/sandbox/common.d.ts +18 -1
  266. package/dist/tools/builtins/sandbox/common.js +14 -1
  267. package/dist/tools/builtins/sandbox/index.d.ts +2 -1
  268. package/dist/tools/builtins/sandbox/index.js +4 -1
  269. package/dist/tools/builtins/sandbox/task-list.d.ts +12 -0
  270. package/dist/tools/builtins/sandbox/task-list.js +130 -0
  271. package/dist/tools/builtins/sandbox/task-output.d.ts +8 -1
  272. package/dist/tools/builtins/sandbox/task-output.js +21 -3
  273. package/dist/tools/builtins/web/extract.d.ts +2 -4
  274. package/dist/tools/builtins/web/extract.js +2 -5
  275. package/dist/tools/pipeline.d.ts +12 -3
  276. package/dist/tools/pipeline.js +6 -1
  277. package/dist/tools/questions.service.d.ts +30 -10
  278. package/dist/tools/questions.service.js +41 -27
  279. package/dist/tools/registry.d.ts +12 -3
  280. package/dist/tools/registry.js +13 -3
  281. package/dist/tools/tools.module.js +4 -3
  282. package/package.json +3 -3
  283. package/src/migrations/0118_device_policy_ops.sql +6 -0
  284. package/src/migrations/0119_outbox.sql +22 -0
  285. package/src/migrations/0120_drop_usage_report_sweep.sql +7 -0
  286. package/src/migrations/0121_drop_platform_migration_runs.sql +12 -0
  287. package/src/migrations/0122_drop_scheduled_task_runs_rechecked_at.sql +5 -0
  288. package/src/migrations/0123_pgvector.sql +27 -0
  289. package/src/migrations/0124_conversation_source.sql +36 -0
  290. package/dist/catalog/platform-migration.d.ts +0 -115
  291. package/dist/catalog/platform-migration.js +0 -583
  292. package/dist/infra/queue/add-job-once.d.ts +0 -14
  293. package/dist/infra/queue/add-job-once.js +0 -16
@@ -1,12 +1,11 @@
1
- import type { Redis } from "ioredis";
2
1
  import { EventLog } from "../conversations/event-log.js";
2
+ import { LifecycleHooks } from "../extensions/lifecycle-hooks.js";
3
3
  import { ExtensionRegistry } from "../extensions/registry.js";
4
4
  import { SessionService } from "../identity/sessions.js";
5
5
  import { KernelDb } from "../infra/database/kernel-db.js";
6
6
  import { TransactionHost } from "../infra/database/transaction-host.js";
7
- import { LockService } from "../infra/lock/lock.service.js";
7
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
8
8
  import type { KernelLogger } from "../log.js";
9
- import { SandboxJobs } from "../sandbox/jobs.js";
10
9
  import { ApprovalsService } from "../tools/approvals.service.js";
11
10
  import { Credentials } from "../tools/credentials.js";
12
11
  import { QuestionsService } from "../tools/questions.service.js";
@@ -26,11 +25,11 @@ export interface OffboardResult {
26
25
  revokedSessionIds: string[];
27
26
  /** 写了 cancelled 事件的会话 → 最大 seq(提交后发布)。 */
28
27
  notify: Map<string, number>;
29
- /** 此人在该组织进行中的运行(queued / running / waiting),提交后中止。 */
28
+ /** The person's active runs in the org (queued / running / waiting): a `run-stop` job each is owed. */
30
29
  runs: InFlightRun[];
31
30
  /** 负责的 agent 转交给的所有者;没有转交时为 null。 */
32
31
  newOwnerId: string | null;
33
- /** The person's devices in the org, soft-deleted here; destroyed by a job after commit. */
32
+ /** The person's devices in the org, soft-deleted here; a destroy job each is owed. */
34
33
  devices: string[];
35
34
  /** Conversations whose queued messages were deleted (`changed: queue` after commit). */
36
35
  queueConversations: string[];
@@ -39,21 +38,28 @@ export interface OffboardResult {
39
38
  export interface OffboardingRunControl {
40
39
  abortWaiting(conversationId: string, runId: string, byAccountId: string | null): Promise<boolean>;
41
40
  abortIfOrphaned(conversationId: string, runId: string, byAccountId: string | null): Promise<boolean>;
41
+ abortQueued(conversationId: string, runId: string, byAccountId: string | null): Promise<boolean>;
42
42
  requestAbort(runId: string, byAccountId: string | null): Promise<void>;
43
43
  }
44
- /** The safety-net sweep: cron job name (no `:`), every 10 minutes, and the rows per page. */
45
- export declare const OFFBOARD_SWEEP_JOB = "offboard-sweep";
46
- export declare const OFFBOARD_SWEEP_BATCH = 500;
47
- /** 一次兜底扫描最多翻的页数(每页 {@link OFFBOARD_SWEEP_BATCH} 条),防止单次任务无限长。 */
48
- export declare const OFFBOARD_SWEEP_MAX_PAGES = 20;
49
- /** 已经广播中止的运行的标记保留时长(秒),长于扫描周期:期间扫描不再重复广播。 */
50
- export declare const OFFBOARD_ABORT_MARK_TTL_SECONDS = 1800;
51
- /** 「已请求中止」标记(Redis 键)。 */
52
- export declare const offboardAbortKey: (runId: string) => string;
44
+ /**
45
+ * The stop of a departed member's run (transactional outbox spec §8.1), owed by the offboard
46
+ * transaction on the maintenance queue under {@link runStopJobId}; `MaintenanceProcessor` runs
47
+ * {@link Offboarding.stopRun}.
48
+ */
49
+ export declare const RUN_STOP_JOB = "run-stop";
50
+ /** The `run-stop` job's data. */
51
+ export interface RunStopJobData {
52
+ conversationId: string;
53
+ runId: string;
54
+ }
55
+ /** The `run-stop` job id: a run is stopped once (no `:`). */
56
+ export declare const runStopJobId: (runId: string) => string;
53
57
  /**
54
58
  * 成员离职(spec 3.3):管理员移出、本人离开与通讯录同步共用这一条路径,全部写入在调用方的同一个事务内,
55
- * 要么都生效要么都不生效。Redis 吊销标记、事件发布与中止进行中的运行是提交后的尽力而为({@link afterOffboard}),
56
- * 漏掉的运行由兜底扫描({@link sweep}:发起人已不是该组织成员的进行中运行)中止。框架无关。
59
+ * 要么都生效要么都不生效。The stops of the person's runs and the destruction of their devices are owed
60
+ * in that transaction (outbox jobs, transactional outbox spec §8.1); the Redis revocation markers
61
+ * and the event publishes after the commit are hints ({@link afterOffboard}: `revoked_at` in PG is
62
+ * the authority).
57
63
  *
58
64
  * 已接受的窄窗口:离职提交时正在进行的 OAuth token 刷新(`Credentials` 刷新锁内)可能在提交之后把此人的
59
65
  * 个人凭据重新写回(upsert)。凭据只在此人以成员身份调用工具时使用,而其运行已被中止、成员关系已删除,
@@ -63,22 +69,25 @@ export declare class Offboarding {
63
69
  private readonly db;
64
70
  readonly txHost: TransactionHost;
65
71
  private readonly sessions;
66
- private readonly redis;
67
72
  private readonly registry;
68
73
  private readonly eventLog;
69
- /** The departed member's devices: one-off maintenance jobs, destroyed by a worker. */
70
- private readonly sandboxJobs;
71
- /** Stops the departed member's runs (`by: null`, a system action). */
74
+ /** Owes the run stops and device destroys in the offboard transaction. */
75
+ private readonly outbox;
76
+ /** Stops the departed member's runs (`by: null`, a system action) in the `run-stop` job. */
72
77
  private readonly runs;
73
78
  private readonly credentials;
74
79
  private readonly approvals;
75
80
  private readonly questions;
81
+ /** Owes `org.member_removed` to the subscribing plugins, in the offboard transaction. */
82
+ private readonly lifecycle;
76
83
  readonly log: KernelLogger;
77
- constructor(db: KernelDb, txHost: TransactionHost, sessions: Pick<SessionService, "markRevoked">, redis: Pick<Redis, "set" | "exists">, registry: Pick<ExtensionRegistry, "list">, eventLog: Pick<EventLog, "notify" | "publishChanged">,
78
- /** The departed member's devices: one-off maintenance jobs, destroyed by a worker. */
79
- sandboxJobs: Pick<SandboxJobs, "destroy">,
80
- /** Stops the departed member's runs (`by: null`, a system action). */
81
- runs: OffboardingRunControl, credentials: Pick<Credentials, "clearAccount">, approvals: Pick<ApprovalsService, "cancelPendingFor">, questions: Pick<QuestionsService, "cancelPendingFor">, log: KernelLogger);
84
+ constructor(db: KernelDb, txHost: TransactionHost, sessions: Pick<SessionService, "markRevoked">, registry: Pick<ExtensionRegistry, "list">, eventLog: Pick<EventLog, "notify" | "publishChanged">,
85
+ /** Owes the run stops and device destroys in the offboard transaction. */
86
+ outbox: Pick<OutboxService, "add">,
87
+ /** Stops the departed member's runs (`by: null`, a system action) in the `run-stop` job. */
88
+ runs: OffboardingRunControl, credentials: Pick<Credentials, "clearAccount">, approvals: Pick<ApprovalsService, "pendingConversationsFor" | "cancelPendingFor">, questions: Pick<QuestionsService, "pendingConversationsFor" | "cancelPendingFor">,
89
+ /** Owes `org.member_removed` to the subscribing plugins, in the offboard transaction. */
90
+ lifecycle: Pick<LifecycleHooks, "recordIn">, log: KernelLogger);
82
91
  /**
83
92
  * In the caller's transaction (`mandatory`: the caller holds the org row lock, order org row →
84
93
  * membership, and did the "last owner" checks):
@@ -87,7 +96,8 @@ export declare class Offboarding {
87
96
  * account (both are account-wide, so conservatively all); a person who leaves (`left`) keeps
88
97
  * them, and the session's current org is cleared (the membership check ends the access);
89
98
  * 3. 删除其在该组织所有连接器上的个人凭据;
90
- * 4. 取消其作为处理人的 pending 审批与提问(写 cancelled 事件),记下此人在该组织进行中的运行(提交后中止);
99
+ * 4. 取消其作为处理人的 pending 审批与提问(写 cancelled 事件); owes a `run-stop` job for each of the
100
+ * person's active runs in the org;
91
101
  * 5. 其负责的该组织 agent 转给组织所有者(最早加入的 owner)并置 `needs_reassignment = true`;
92
102
  * 6. 归档其在该组织各渠道连接上的 IM 话题行(重新加入后在 IM 里开始新的对话,不接着旧会话),
93
103
  * 删除其在该组织的「以后都允许」工具授权(`tool_grants`);
@@ -95,9 +105,10 @@ export declare class Offboarding {
95
105
  * 以及成员关系自身的 `directory:<插件>` 来源,不论此人当初是不是由通讯录加入(手动重新加入后再被移出也不会被同步加回);
96
106
  * 同步不再把此人加回,直到有人手动重新加入({@link MembersService.add} 删除排除);
97
107
  * 8. 审计 `org.member.offboarded`;
98
- * 9. Soft-deletes the person's devices in the org and clears their default row (the
99
- * instances and files are destroyed after the commit, best effort; the hourly sweep finishes
100
- * any left).
108
+ * 9. Soft-deletes the person's devices in the org and clears their default row, and owes the
109
+ * destruction of each (instance and files, `device-destroy-<deviceId>`);
110
+ * 10. Owes the `org.member_removed` lifecycle hook deliveries (outbox jobs, one per subscribing
111
+ * plugin).
101
112
  */
102
113
  offboard(orgId: string, accountId: string, reason: OffboardReason, actor?: {
103
114
  actorAccountId?: string;
@@ -107,36 +118,18 @@ export declare class Offboarding {
107
118
  private exclusionPlugins;
108
119
  /**
109
120
  * After the commit (callers register it with the infra `afterCommit` inside their transaction):
110
- * writes the session revocation markers, publishes the cancelled events, stops the person's
111
- * runs in the org and schedules destroying their devices. A failing step only logs a
112
- * warning.
121
+ * writes the session revocation markers and publishes the cancelled events and queue changes,
122
+ * all hints (the run stops and device destroys were owed in the transaction). A failing step
123
+ * only logs a warning.
113
124
  */
114
125
  afterOffboard(r: OffboardResult): Promise<void>;
115
126
  /**
116
- * 中止一个运行,顺序同会话中止接口:waiting → 直接取消等待并写终态;孤儿 → 直接写终态;
117
- * 否则经 Redis 广播中止(运行所在副本收到后结束;尚未开始的运行开始时补查中止标记)。失败只记 warn。
118
- * 不像会话中止那样随后起新一轮运行:离职者的待处理消息不再处理。
127
+ * The `run-stop` job (`MaintenanceProcessor`): stops a departed member's run the way the
128
+ * conversation's stop does (`POST .../abort`), by no account: a waiting run is aborted directly,
129
+ * a queued run is ended in PG (ruling R27: it never starts later, however long it was held), an
130
+ * orphan is finished directly, a running run is asked to stop through Redis (its replica ends it). A finished run is left alone. No
131
+ * new run follows, as after a person's stop: the departed member's queued messages were deleted.
132
+ * A failure throws (the job is retried).
119
133
  */
120
- private stopRun;
121
- /**
122
- * 兜底扫描(维护队列每 10 分钟):进行中、且发起人已不是该会话所属组织成员的运行 → 中止
123
- * (提交后的中止没做成时,如进程崩溃)。按 `(runs.created_at, runs.id)` 键集分页翻完(每页 500 条,
124
- * 至多 20 页):经广播中止、尚未真正结束的运行仍是进行中,若只取前 500 条,超过 500 条卡住的运行会
125
- * 一直占满这一页,后面的永远轮不到。已广播中止、仍带 `capk:offboard-abort:<runId>` 标记(30 分钟)的运行跳过,
126
- * 不每轮重复广播;标记过期仍未结束的再处理一次。返回本次处理(未跳过)的运行数。
127
- * 现任成员(含匿名访客这样的 guest 成员)发起的运行不受影响。
128
- */
129
- sweep(batch?: number): Promise<number>;
130
- private recentlyAborted;
131
- }
132
- /**
133
- * The offboarding safety-net sweep ({@link Offboarding.sweep}) every 10 minutes, once per tick
134
- * across replicas; a worker-role job (`WorkerModule`).
135
- */
136
- export declare class OffboardSweepJob {
137
- private readonly offboarding;
138
- readonly lockService: LockService;
139
- readonly log: KernelLogger;
140
- constructor(offboarding: Pick<Offboarding, "sweep">, lockService: LockService, log: KernelLogger);
141
- run(): Promise<void>;
134
+ stopRun(data: RunStopJobData): Promise<void>;
142
135
  }
@@ -12,38 +12,41 @@ import { Inject, Injectable } from "@nestjs/common";
12
12
  import { sql } from "kysely";
13
13
  import { retargetSetupRequests } from "../agents/pending-connector-requests.js";
14
14
  import { EventLog } from "../conversations/event-log.js";
15
+ import { lockConversationTrees } from "../conversations/tree.js";
15
16
  import { ApiError } from "../errors.js";
16
17
  import { enabledExtensionsQuery } from "../extensions/enabled.js";
18
+ import { LifecycleHooks } from "../extensions/lifecycle-hooks.js";
17
19
  import { ExtensionRegistry } from "../extensions/registry.js";
18
20
  import { audit } from "../identity/audit.js";
19
21
  import { SessionService } from "../identity/sessions.js";
20
22
  import { KernelDb } from "../infra/database/kernel-db.js";
21
23
  import { TransactionHost } from "../infra/database/transaction-host.js";
22
24
  import { Transactional } from "../infra/database/transactional.decorator.js";
23
- import { LockService } from "../infra/lock/lock.service.js";
24
- import { LockedCron } from "../infra/schedule/locked-cron.decorator.js";
25
- import { KERNEL_LOGGER, REDIS } from "../infra/tokens.js";
25
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
26
+ import { KERNEL_LOGGER } from "../infra/tokens.js";
27
+ import { MAINTENANCE_QUEUE_NAME } from "../runtime/maintenance-queue.js";
26
28
  import { RunControl } from "../runtime/run-control.js";
27
- import { SandboxJobs } from "../sandbox/jobs.js";
29
+ import { oweDeviceDestroy } from "../sandbox/jobs.js";
28
30
  import { ApprovalsService } from "../tools/approvals.service.js";
29
31
  import { Credentials } from "../tools/credentials.js";
30
32
  import { QuestionsService } from "../tools/questions.service.js";
31
- /** The safety-net sweep: cron job name (no `:`), every 10 minutes, and the rows per page. */
32
- export const OFFBOARD_SWEEP_JOB = "offboard-sweep";
33
- export const OFFBOARD_SWEEP_BATCH = 500;
34
- /** 一次兜底扫描最多翻的页数(每页 {@link OFFBOARD_SWEEP_BATCH} 条),防止单次任务无限长。 */
35
- export const OFFBOARD_SWEEP_MAX_PAGES = 20;
36
- /** 已经广播中止的运行的标记保留时长(秒),长于扫描周期:期间扫描不再重复广播。 */
37
- export const OFFBOARD_ABORT_MARK_TTL_SECONDS = 1800;
38
- /** 「已请求中止」标记(Redis 键)。 */
39
- export const offboardAbortKey = (runId) => `capk:offboard-abort:${runId}`;
33
+ /**
34
+ * The stop of a departed member's run (transactional outbox spec §8.1), owed by the offboard
35
+ * transaction on the maintenance queue under {@link runStopJobId}; `MaintenanceProcessor` runs
36
+ * {@link Offboarding.stopRun}.
37
+ */
38
+ export const RUN_STOP_JOB = "run-stop";
39
+ /** The `run-stop` job id: a run is stopped once (no `:`). */
40
+ export const runStopJobId = (runId) => `run-stop-${runId}`;
40
41
  const ACTIVE = ["queued", "running", "waiting"];
41
42
  /** 通讯录同步加入的成员关系的 `source` 前缀(`directory:<插件名>`)。 */
42
43
  const DIRECTORY_SOURCE_PREFIX = "directory:";
43
44
  /**
44
45
  * 成员离职(spec 3.3):管理员移出、本人离开与通讯录同步共用这一条路径,全部写入在调用方的同一个事务内,
45
- * 要么都生效要么都不生效。Redis 吊销标记、事件发布与中止进行中的运行是提交后的尽力而为({@link afterOffboard}),
46
- * 漏掉的运行由兜底扫描({@link sweep}:发起人已不是该组织成员的进行中运行)中止。框架无关。
46
+ * 要么都生效要么都不生效。The stops of the person's runs and the destruction of their devices are owed
47
+ * in that transaction (outbox jobs, transactional outbox spec §8.1); the Redis revocation markers
48
+ * and the event publishes after the commit are hints ({@link afterOffboard}: `revoked_at` in PG is
49
+ * the authority).
47
50
  *
48
51
  * 已接受的窄窗口:离职提交时正在进行的 OAuth token 刷新(`Credentials` 刷新锁内)可能在提交之后把此人的
49
52
  * 个人凭据重新写回(upsert)。凭据只在此人以成员身份调用工具时使用,而其运行已被中止、成员关系已删除,
@@ -53,31 +56,33 @@ let Offboarding = class Offboarding {
53
56
  db;
54
57
  txHost;
55
58
  sessions;
56
- redis;
57
59
  registry;
58
60
  eventLog;
59
- sandboxJobs;
61
+ outbox;
60
62
  runs;
61
63
  credentials;
62
64
  approvals;
63
65
  questions;
66
+ lifecycle;
64
67
  log;
65
- constructor(db, txHost, sessions, redis, registry, eventLog,
66
- /** The departed member's devices: one-off maintenance jobs, destroyed by a worker. */
67
- sandboxJobs,
68
- /** Stops the departed member's runs (`by: null`, a system action). */
69
- runs, credentials, approvals, questions, log) {
68
+ constructor(db, txHost, sessions, registry, eventLog,
69
+ /** Owes the run stops and device destroys in the offboard transaction. */
70
+ outbox,
71
+ /** Stops the departed member's runs (`by: null`, a system action) in the `run-stop` job. */
72
+ runs, credentials, approvals, questions,
73
+ /** Owes `org.member_removed` to the subscribing plugins, in the offboard transaction. */
74
+ lifecycle, log) {
70
75
  this.db = db;
71
76
  this.txHost = txHost;
72
77
  this.sessions = sessions;
73
- this.redis = redis;
74
78
  this.registry = registry;
75
79
  this.eventLog = eventLog;
76
- this.sandboxJobs = sandboxJobs;
80
+ this.outbox = outbox;
77
81
  this.runs = runs;
78
82
  this.credentials = credentials;
79
83
  this.approvals = approvals;
80
84
  this.questions = questions;
85
+ this.lifecycle = lifecycle;
81
86
  this.log = log;
82
87
  }
83
88
  /**
@@ -88,7 +93,8 @@ let Offboarding = class Offboarding {
88
93
  * account (both are account-wide, so conservatively all); a person who leaves (`left`) keeps
89
94
  * them, and the session's current org is cleared (the membership check ends the access);
90
95
  * 3. 删除其在该组织所有连接器上的个人凭据;
91
- * 4. 取消其作为处理人的 pending 审批与提问(写 cancelled 事件),记下此人在该组织进行中的运行(提交后中止);
96
+ * 4. 取消其作为处理人的 pending 审批与提问(写 cancelled 事件); owes a `run-stop` job for each of the
97
+ * person's active runs in the org;
92
98
  * 5. 其负责的该组织 agent 转给组织所有者(最早加入的 owner)并置 `needs_reassignment = true`;
93
99
  * 6. 归档其在该组织各渠道连接上的 IM 话题行(重新加入后在 IM 里开始新的对话,不接着旧会话),
94
100
  * 删除其在该组织的「以后都允许」工具授权(`tool_grants`);
@@ -96,9 +102,10 @@ let Offboarding = class Offboarding {
96
102
  * 以及成员关系自身的 `directory:<插件>` 来源,不论此人当初是不是由通讯录加入(手动重新加入后再被移出也不会被同步加回);
97
103
  * 同步不再把此人加回,直到有人手动重新加入({@link MembersService.add} 删除排除);
98
104
  * 8. 审计 `org.member.offboarded`;
99
- * 9. Soft-deletes the person's devices in the org and clears their default row (the
100
- * instances and files are destroyed after the commit, best effort; the hourly sweep finishes
101
- * any left).
105
+ * 9. Soft-deletes the person's devices in the org and clears their default row, and owes the
106
+ * destruction of each (instance and files, `device-destroy-<deviceId>`);
107
+ * 10. Owes the `org.member_removed` lifecycle hook deliveries (outbox jobs, one per subscribing
108
+ * plugin).
102
109
  */
103
110
  async offboard(orgId, accountId, reason, actor = {}) {
104
111
  const deleted = await this.db
@@ -126,17 +133,26 @@ let Offboarding = class Offboarding {
126
133
  .orderBy("id")
127
134
  .forNoKeyUpdate()
128
135
  .execute();
129
- // then the member's conversations in id order, before any of their queue rows (final review
130
- // m2): queue dispatch, send-now and remove lock a conversation, then its queue rows; deleting
131
- // the rows first and updating the conversations after could deadlock with them (40P01)
132
- await this.db
136
+ // then, in one pass, the member's conversations and those holding a pending approval or
137
+ // question this offboarding cancels, before any of their queue, approval or question rows
138
+ // (final review m2: queue dispatch, send-now and remove lock a conversation, then its queue
139
+ // rows). Locked tree by tree, root first (`lockConversationTrees`), not in id order: a reply
140
+ // locks its root and then the sub-agents' conversations whose network asks it skips, and a
141
+ // child's id may sort before its root's (40P01)
142
+ const own = await this.db
133
143
  .selectFrom("conversations")
134
144
  .select("id")
135
145
  .where("org_id", "=", orgId)
136
146
  .where("account_id", "=", accountId)
137
- .orderBy("id")
138
- .forNoKeyUpdate()
139
147
  .execute();
148
+ const approvalConvs = await this.approvals.pendingConversationsFor(orgId, accountId);
149
+ const questionConvs = await this.questions.pendingConversationsFor(orgId, accountId);
150
+ const locked = await lockConversationTrees(this.db.db, [
151
+ ...own.map((c) => c.id),
152
+ ...approvalConvs,
153
+ ...questionConvs,
154
+ ]);
155
+ const isLocked = new Set(locked);
140
156
  const owner = await this.db
141
157
  .selectFrom("memberships")
142
158
  .select("account_id")
@@ -180,8 +196,9 @@ let Offboarding = class Offboarding {
180
196
  : [];
181
197
  // in this transaction (the approvals and questions services join it: `mandatory`)
182
198
  const credentials = await this.credentials.clearAccount(orgId, accountId);
183
- const approvals = await this.approvals.cancelPendingFor(orgId, accountId);
184
- const questions = await this.questions.cancelPendingFor(orgId, accountId);
199
+ // only in the conversations locked above (one asked meanwhile elsewhere waits for its run's stop)
200
+ const approvals = await this.approvals.cancelPendingFor(orgId, accountId, approvalConvs.filter((c) => isLocked.has(c)));
201
+ const questions = await this.questions.cancelPendingFor(orgId, accountId, questionConvs.filter((c) => isLocked.has(c)));
185
202
  const notify = new Map(approvals.seqs);
186
203
  for (const [c, seq] of questions.seqs)
187
204
  notify.set(c, Math.max(notify.get(c) ?? 0, seq));
@@ -201,6 +218,16 @@ let Offboarding = class Offboarding {
201
218
  eb("conversations.account_id", "=", accountId),
202
219
  ]))
203
220
  .execute();
221
+ // their stops are owed with the offboarding (the job reads the run's state when it runs)
222
+ for (const r of runs) {
223
+ await this.outbox.add({
224
+ queue: MAINTENANCE_QUEUE_NAME,
225
+ name: RUN_STOP_JOB,
226
+ jobId: runStopJobId(r.id),
227
+ data: { conversationId: r.conversation_id, runId: r.id },
228
+ orgId,
229
+ });
230
+ }
204
231
  const agents = await this.db
205
232
  .updateTable("agents")
206
233
  .set({
@@ -268,6 +295,8 @@ let Offboarding = class Offboarding {
268
295
  .where("deleted_at", "is", null)
269
296
  .returning("id")
270
297
  .execute();
298
+ for (const d of devices)
299
+ await oweDeviceDestroy(this.outbox, d.id, orgId);
271
300
  await this.db
272
301
  .deleteFrom("device_defaults")
273
302
  .where("org_id", "=", orgId)
@@ -297,6 +326,10 @@ let Offboarding = class Offboarding {
297
326
  ...(queued.length > 0 && { queuedMessagesDeleted: queued.length }),
298
327
  },
299
328
  });
329
+ await this.lifecycle.recordIn("org.member_removed", {
330
+ orgId,
331
+ accountId,
332
+ });
300
333
  return {
301
334
  role: deleted.role,
302
335
  revokedSessionIds: sessions.map((s) => s.id),
@@ -335,9 +368,9 @@ let Offboarding = class Offboarding {
335
368
  }
336
369
  /**
337
370
  * After the commit (callers register it with the infra `afterCommit` inside their transaction):
338
- * writes the session revocation markers, publishes the cancelled events, stops the person's
339
- * runs in the org and schedules destroying their devices. A failing step only logs a
340
- * warning.
371
+ * writes the session revocation markers and publishes the cancelled events and queue changes,
372
+ * all hints (the run stops and device destroys were owed in the transaction). A failing step
373
+ * only logs a warning.
341
374
  */
342
375
  async afterOffboard(r) {
343
376
  const { sessions, log, eventLog } = this;
@@ -363,102 +396,51 @@ let Offboarding = class Offboarding {
363
396
  log.warn("offboarding: queue change notify failed", { conversationId, err });
364
397
  }
365
398
  }
366
- for (const run of r.runs)
367
- await this.stopRun(run);
368
- if (r.devices.length > 0) {
369
- try {
370
- await this.sandboxJobs.destroy(r.devices);
371
- }
372
- catch (err) {
373
- log.warn("offboarding: cannot schedule device destruction", {
374
- count: r.devices.length,
375
- err,
376
- });
377
- }
378
- }
379
399
  }
380
400
  /**
381
- * 中止一个运行,顺序同会话中止接口:waiting → 直接取消等待并写终态;孤儿 → 直接写终态;
382
- * 否则经 Redis 广播中止(运行所在副本收到后结束;尚未开始的运行开始时补查中止标记)。失败只记 warn。
383
- * 不像会话中止那样随后起新一轮运行:离职者的待处理消息不再处理。
401
+ * The `run-stop` job (`MaintenanceProcessor`): stops a departed member's run the way the
402
+ * conversation's stop does (`POST .../abort`), by no account: a waiting run is aborted directly,
403
+ * a queued run is ended in PG (ruling R27: it never starts later, however long it was held), an
404
+ * orphan is finished directly, a running run is asked to stop through Redis (its replica ends it). A finished run is left alone. No
405
+ * new run follows, as after a person's stop: the departed member's queued messages were deleted.
406
+ * A failure throws (the job is retried).
384
407
  */
385
- async stopRun(run) {
408
+ async stopRun(data) {
409
+ const run = await this.db
410
+ .selectFrom("runs")
411
+ .select("status")
412
+ .where("id", "=", data.runId)
413
+ .executeTakeFirst();
414
+ if (run === undefined || !ACTIVE.includes(run.status))
415
+ return;
386
416
  const ctl = this.runs;
387
- try {
388
- if (run.status === "waiting" &&
389
- (await ctl.abortWaiting(run.conversationId, run.runId, null))) {
390
- return;
391
- }
392
- if (await ctl.abortIfOrphaned(run.conversationId, run.runId, null))
393
- return;
394
- await ctl.requestAbort(run.runId, null);
395
- // 广播中止的运行要等所在副本到达步骤边界才真正结束:广播成功后记标记,扫描在其有效期内不再重复广播
396
- // (广播失败不记,下一轮扫描重试)
397
- await this.redis
398
- .set(offboardAbortKey(run.runId), "1", "EX", OFFBOARD_ABORT_MARK_TTL_SECONDS)
399
- .catch((err) => this.log.warn("offboarding: cannot mark abort", { err }));
400
- }
401
- catch (err) {
402
- this.log.warn("offboarding: cannot stop run", { runId: run.runId, err });
417
+ if (run.status === "waiting" &&
418
+ (await ctl.abortWaiting(data.conversationId, data.runId, null))) {
419
+ return;
403
420
  }
404
- }
405
- /**
406
- * 兜底扫描(维护队列每 10 分钟):进行中、且发起人已不是该会话所属组织成员的运行 → 中止
407
- * (提交后的中止没做成时,如进程崩溃)。按 `(runs.created_at, runs.id)` 键集分页翻完(每页 500 条,
408
- * 至多 20 页):经广播中止、尚未真正结束的运行仍是进行中,若只取前 500 条,超过 500 条卡住的运行会
409
- * 一直占满这一页,后面的永远轮不到。已广播中止、仍带 `capk:offboard-abort:<runId>` 标记(30 分钟)的运行跳过,
410
- * 不每轮重复广播;标记过期仍未结束的再处理一次。返回本次处理(未跳过)的运行数。
411
- * 现任成员(含匿名访客这样的 guest 成员)发起的运行不受影响。
412
- */
413
- async sweep(batch = OFFBOARD_SWEEP_BATCH) {
414
- const db = this.db;
415
- // 游标的时间用文本保存完整的微秒精度(JS Date 只有毫秒,会让同一行再被取到)
416
- let after;
417
- let total = 0;
418
- for (let page = 0; page < OFFBOARD_SWEEP_MAX_PAGES; page++) {
419
- const cursor = after;
420
- const rows = await db
421
+ if (run.status === "queued") {
422
+ // a queued run may wait for hours: it is ended in PG, never left to the stop marker's TTL
423
+ // (ruling R27); the marker first, so a worker that takes it meanwhile stops too
424
+ await ctl.requestAbort(data.runId, null);
425
+ if (await ctl.abortQueued(data.conversationId, data.runId, null))
426
+ return;
427
+ // a worker took it (now running) or it moved on: the marker stops it; re-read and go on
428
+ const now = await this.db
421
429
  .selectFrom("runs")
422
- .innerJoin("conversations", "conversations.id", "runs.conversation_id")
423
- .select(["runs.id", "runs.conversation_id", "runs.status"])
424
- .select(sql `runs.created_at::text`.as("created_at_text"))
425
- .where("runs.status", "in", ACTIVE)
426
- // a removed member's sub-agents run with their identity and credentials: stopped too
427
- .where("conversations.kind", "in", ["chat", "subagent"])
428
- .where(({ not, exists, selectFrom }) => not(exists(selectFrom("memberships")
429
- .select(sql `1`.as("one"))
430
- .whereRef("memberships.org_id", "=", "conversations.org_id")
431
- .whereRef("memberships.account_id", "=", "runs.trigger_account_id"))))
432
- .$if(cursor !== undefined, (q) => q.where(sql `(runs.created_at, runs.id) > (${cursor?.createdAt}::timestamptz, ${cursor?.id})`))
433
- .orderBy("runs.created_at")
434
- .orderBy("runs.id")
435
- .limit(batch)
436
- .execute();
437
- for (const r of rows) {
438
- if (await this.recentlyAborted(r.id))
439
- continue;
440
- total++;
441
- this.log.warn("offboard sweep: stopping run of a former member", { runId: r.id });
442
- await this.stopRun({
443
- conversationId: r.conversation_id,
444
- runId: r.id,
445
- status: r.status,
446
- });
430
+ .select("status")
431
+ .where("id", "=", data.runId)
432
+ .executeTakeFirst();
433
+ if (now?.status === "queued") {
434
+ throw new Error(`run ${data.runId} is still queued; the run-stop job retries`);
447
435
  }
448
- const last = rows[rows.length - 1];
449
- if (rows.length < batch || !last)
450
- break;
451
- after = { createdAt: last.created_at_text, id: last.id };
452
- }
453
- return total;
454
- }
455
- async recentlyAborted(runId) {
456
- try {
457
- return (await this.redis.exists(offboardAbortKey(runId))) === 1;
458
- }
459
- catch {
460
- return false;
436
+ if (now?.status !== "waiting")
437
+ return;
438
+ await ctl.abortWaiting(data.conversationId, data.runId, null);
439
+ return;
461
440
  }
441
+ if (await ctl.abortIfOrphaned(data.conversationId, data.runId, null))
442
+ return;
443
+ await ctl.requestAbort(data.runId, null);
462
444
  }
463
445
  };
464
446
  __decorate([
@@ -469,41 +451,14 @@ Offboarding = __decorate([
469
451
  __param(0, Inject(KernelDb)),
470
452
  __param(1, Inject(TransactionHost)),
471
453
  __param(2, Inject(SessionService)),
472
- __param(3, Inject(REDIS)),
473
- __param(4, Inject(ExtensionRegistry)),
474
- __param(5, Inject(EventLog)),
475
- __param(6, Inject(SandboxJobs)),
476
- __param(7, Inject(RunControl)),
477
- __param(8, Inject(Credentials)),
478
- __param(9, Inject(ApprovalsService)),
479
- __param(10, Inject(QuestionsService)),
454
+ __param(3, Inject(ExtensionRegistry)),
455
+ __param(4, Inject(EventLog)),
456
+ __param(5, Inject(OutboxService)),
457
+ __param(6, Inject(RunControl)),
458
+ __param(7, Inject(Credentials)),
459
+ __param(8, Inject(ApprovalsService)),
460
+ __param(9, Inject(QuestionsService)),
461
+ __param(10, Inject(LifecycleHooks)),
480
462
  __param(11, Inject(KERNEL_LOGGER))
481
463
  ], Offboarding);
482
464
  export { Offboarding };
483
- /**
484
- * The offboarding safety-net sweep ({@link Offboarding.sweep}) every 10 minutes, once per tick
485
- * across replicas; a worker-role job (`WorkerModule`).
486
- */
487
- let OffboardSweepJob = class OffboardSweepJob {
488
- offboarding;
489
- lockService;
490
- log;
491
- constructor(offboarding, lockService, log) {
492
- this.offboarding = offboarding;
493
- this.lockService = lockService;
494
- this.log = log;
495
- }
496
- async run() {
497
- await this.offboarding.sweep();
498
- }
499
- };
500
- __decorate([
501
- LockedCron("*/10 * * * *", { name: OFFBOARD_SWEEP_JOB })
502
- ], OffboardSweepJob.prototype, "run", null);
503
- OffboardSweepJob = __decorate([
504
- Injectable(),
505
- __param(0, Inject(Offboarding)),
506
- __param(1, Inject(LockService)),
507
- __param(2, Inject(KERNEL_LOGGER))
508
- ], OffboardSweepJob);
509
- export { OffboardSweepJob };
@@ -9,6 +9,7 @@ import { CatalogModule } from "../catalog/catalog.module.js";
9
9
  import { EventLogModule } from "../conversations/event-log.js";
10
10
  import { EvalBudgetModule } from "../evals/budget.js";
11
11
  import { ExtensionsModule } from "../extensions/extensions.module.js";
12
+ import { LifecycleHooksModule } from "../extensions/lifecycle-hooks.js";
12
13
  import { IdentityModule } from "../identity/identity.module.js";
13
14
  import { QueueModule } from "../infra/queue/queue.module.js";
14
15
  import { MAINTENANCE_QUEUE_NAME } from "../runtime/maintenance-queue.js";
@@ -37,6 +38,8 @@ OrgsModule = __decorate([
37
38
  OrgLockModule,
38
39
  OrgSettingsModule,
39
40
  ExtensionsModule,
41
+ // offboarding and the org delete record lifecycle hook deliveries
42
+ LifecycleHooksModule,
40
43
  // offboarding clears credentials and cancels approvals and questions
41
44
  ToolsModule,
42
45
  // offboarding stops the member's runs