@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,7 +25,7 @@ import { notifyFinished, RunFinish } from "./run-finish.js";
25
25
  import { RunJobs, runJobId, runQueueNameFor } from "./run-jobs.js";
26
26
  import { RunQueue } from "./run-queue.js";
27
27
  import { Waits } from "./waits.js";
28
- /** The check's `@LockedInterval` job name (its run lock key is the default `cron:run-orphan-check`). */
28
+ /** The check's `@LockedInterval` job name (its run lock key: `cron:run-orphan-check`). */
29
29
  export const RUN_ORPHAN_CHECK_JOB = "run-orphan-check";
30
30
  /** The check's period: a fixed constant, not a config field. */
31
31
  export const RUN_ORPHAN_CHECK_PERIOD_MS = 60_000;
@@ -49,12 +49,16 @@ const SINCE = sql `GREATEST(COALESCE(r.queued_at, r.created_at), COALESCE(r.star
49
49
  /**
50
50
  * One page of the active runs of chat and sub-agent conversations in `statuses` that entered their
51
51
  * state more than `minAgeMs` ago, by (state start, id), after `after` (an eval's hidden
52
- * conversation drives its runs itself).
52
+ * conversation drives its runs itself). A deleted org's runs are never candidates, running ones
53
+ * included: its deletion purged its owed jobs on purpose, so their absence is no lost job and
54
+ * nothing is re-added, and the org's data is invisible from then on, so a running orphan is left
55
+ * as it is rather than failed.
53
56
  */
54
57
  export async function orphanCandidates(db, statuses, minAgeMs, limit, after) {
55
58
  let q = db
56
59
  .selectFrom("runs as r")
57
60
  .innerJoin("conversations as c", "c.id", "r.conversation_id")
61
+ .innerJoin("orgs as o", "o.id", "c.org_id")
58
62
  .select([
59
63
  "r.id",
60
64
  "r.conversation_id",
@@ -67,6 +71,7 @@ export async function orphanCandidates(db, statuses, minAgeMs, limit, after) {
67
71
  ])
68
72
  .where("r.status", "in", statuses)
69
73
  .where("c.kind", "in", ["chat", "subagent"])
74
+ .where("o.deleted_at", "is", null)
70
75
  .where(SINCE, "<", sql `now() - ${minAgeMs}::integer * interval '1 millisecond'`);
71
76
  if (after !== undefined) {
72
77
  q = q.where(sql `(${SINCE}, r.id) > (${after.since}::timestamptz, ${after.runId})`);
@@ -110,12 +115,12 @@ async function* candidatePages(db, statuses, minAgeMs) {
110
115
  }
111
116
  }
112
117
  /**
113
- * The `queued` runs (in their state for over `minAgeMs`) whose job is neither in the outbox
114
- * (owed, or dead) nor in BullMQ (`jobExists`), and that are still queued after both looks: the run
115
- * queue lost their jobs. The outbox is read before BullMQ (a row is deleted only after its job was added) and the
116
- * run again after both (a job that finished took its run out of `queued` first), so a run in
117
- * flight is never reported. The `run-orphan-check` uses it; so does the dual-replica test harness,
118
- * which asserts at its teardown that the kernel left none (`minAgeMs` 0).
118
+ * The `queued` runs (in their state for over `minAgeMs`) whose job is neither in the outbox (owed,
119
+ * or dead) nor in BullMQ (`jobExists`), and that are still queued after both looks: the run queue
120
+ * lost their jobs. The outbox is read before BullMQ (a row is deleted only after its job was added)
121
+ * and the run again after both (a job that finished took its run out of `queued` first), so a run
122
+ * in flight is never reported. The `run-orphan-check` uses it; so does the dual-replica test
123
+ * harness, which asserts at its teardown that the kernel left none (`minAgeMs` 0).
119
124
  */
120
125
  export async function queuedRunsWithoutJob(db, jobExists, opts) {
121
126
  const out = [];
@@ -146,9 +151,9 @@ export async function queuedRunsWithoutJob(db, jobExists, opts) {
146
151
  * `run.orphaned` (its terminal transaction cancels its waits and owes what every run end owes),
147
152
  * then, after the commit, its checkpoints are dropped, its fork child stopped (`run.orphaned`), a
148
153
  * sub-agent's failure delivered to its parent at once and the conversation's next run started.
149
- * - `queued`, with no outbox row either (pending or dead): a canary. Every queued run has its job owed in its
150
- * own transaction, so this means Redis lost data. It is logged as an error, counted for the admin
151
- * overview ({@link QueueLostJobs}) and its job owed again so the run is not stuck.
154
+ * - `queued`, with no outbox row either (pending or dead): a canary. Every queued run has its job
155
+ * owed in its own transaction, so this means Redis lost data. It is logged as an error, counted
156
+ * for the admin overview ({@link QueueLostJobs}) and its job owed again so the run is not stuck.
152
157
  * Redis is read before each repair's transaction, which then re-reads the run under its
153
158
  * conversation row lock and acts only if it is still in the same state since the same time. Read
154
159
  * paths never repair runs.
@@ -1,5 +1,4 @@
1
1
  import type { Logger } from "@capybara-hq/plugin-sdk";
2
- import type { Queue } from "bullmq";
3
2
  import { EventLog } from "../conversations/event-log.js";
4
3
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
5
4
  import { KernelDb } from "../infra/database/kernel-db.js";
@@ -32,14 +31,10 @@ export declare class RunQueue {
32
31
  private readonly log;
33
32
  readonly txHost: TransactionHost;
34
33
  private readonly config;
35
- /** The producer side of the delayed `bg-wake` job (`MaintenanceProcessor` consumes it). */
36
- private readonly maintenance;
37
34
  private readonly ensureFails;
38
35
  constructor(kdb: KernelDb, eventLog: EventLog, lock: Pick<ConversationLock, "holder">, jobs: RunJobs, finisher: Pick<RunFinish, "finishIn">,
39
36
  /** An orphan aborted directly loses its checkpoint thread (thread_id = runId). */
40
- checkpoints: Pick<KyselyCheckpointSaver, "deleteThread">, log: Pick<Logger, "error" | "warn">, txHost: TransactionHost, config: KernelConfigService,
41
- /** The producer side of the delayed `bg-wake` job (`MaintenanceProcessor` consumes it). */
42
- maintenance: Pick<Queue, "add">, ensureFails?: () => boolean);
37
+ checkpoints: Pick<KyselyCheckpointSaver, "deleteThread">, log: Pick<Logger, "error" | "warn">, txHost: TransactionHost, config: KernelConfigService, ensureFails?: () => boolean);
43
38
  private get db();
44
39
  /**
45
40
  * 有未消费的用户消息(最新 user_message 的 seq > `consumed_seq`)、会话锁无人持有、
@@ -51,7 +46,7 @@ export declare class RunQueue {
51
46
  * once it is sent; with no head either, a wake-eligible finished background task or sub-agent
52
47
  * delivery (background Bash spec §3, agent map spec §5.3, {@link wakeDecisionIn}): the run is
53
48
  * then attributed to the event's trigger account and joins the tree's budget window, and an
54
- * event inside the wake window schedules the delayed `bg-wake` job instead. A sub-agent's
49
+ * event inside the wake window owes the delayed `bg-wake` job instead. A sub-agent's
55
50
  * conversation is only woken this way ({@link childWakeIn}).
56
51
  *
57
52
  * An active run (queued, running or waiting) is left alone: a run nobody runs any more is the
@@ -77,8 +72,9 @@ export declare class RunQueue {
77
72
  private jobIdFor;
78
73
  /**
79
74
  * Owes the new run's job in {@link ensureRunIn}'s transaction, before its run row is written.
80
- * False (logged): the same job id is still owed, so the caller writes no run (a second run of
81
- * one trigger; the conversation's unique active run makes this a should-not-happen).
75
+ * False (logged): the same job id is still owed, so the caller writes no run. A later run of the
76
+ * same trigger has its own `<runJobId>-<n>` ({@link jobIdFor}), so this is a should-not-happen
77
+ * (the conversation has one active run at a time).
82
78
  */
83
79
  private oweJob;
84
80
  /**
@@ -89,15 +85,17 @@ export declare class RunQueue {
89
85
  * racing the run's end starts a new run, never lost); and it is woken by its own background
90
86
  * task's finish or a delivery from its own sub-agent, under the same rules as a chat
91
87
  * conversation. Neither happens once its last run was stopped (`aborted`): a stopped sub-agent is
92
- * started again only by its parent. Both read the queue and the trigger account's access on the root
93
- * of its tree; a message needs no wake setting and has no wake window. The run counts against
94
- * the root's budget window and runs on the sub-agent queue under `runJobId(child, boundary
95
- * seq)`. An active run is never started past (one nobody runs is the `run-orphan-check`'s).
88
+ * started again only by its parent; nor while a person's stop of the root holds the tree
89
+ * (`conversations.tree_stopped_at`, cleared by the root's next run). Both read the queue and the
90
+ * trigger account's access on the root of its tree; a message needs no wake setting and has no
91
+ * wake window. The run counts against the root's budget window and runs on the sub-agent queue
92
+ * under `runJobId(child, boundary seq)`. An active run is never started past (one nobody runs is
93
+ * the `run-orphan-check`'s).
96
94
  */
97
95
  private childWakeIn;
98
96
  /**
99
- * The newest unconsumed message of an idle sub-agent (a `SendMessage` its last run did not absorb)
100
- * as a run boundary, or null: none; the root's queue is paused or the root deleted (the
97
+ * The newest unconsumed message of an idle sub-agent (a `SendMessage` its last run did not
98
+ * absorb) as a run boundary, or null: none; the root's queue is paused or the root deleted (the
101
99
  * message waits for the unpause, {@link wakeHeldTree}); or its author can no longer send to the
102
100
  * root (it waits for a later run). The caller already refused a stopped sub-agent (last run
103
101
  * `aborted`): a message that reached it while running and then stopped is parked, and the next
@@ -126,13 +124,6 @@ export declare class RunQueue {
126
124
  * with their org and newest seq. Reads only (no row locks).
127
125
  */
128
126
  private heldTree;
129
- /**
130
- * Schedules the delayed `bg-wake` job at the end of the conversation's wake window (after the
131
- * commit): one job per conversation and window end, so finishes inside one window share it. It
132
- * calls {@link ensureRun}, which wakes the conversation if a finish is still unconsumed. A failed
133
- * enqueue is logged: the next message, run end or page opening re-derives the wake.
134
- */
135
- private scheduleWake;
136
127
  /**
137
128
  * Owes the ensure-run backstop of `conversationId` in the caller's transaction
138
129
  * ({@link oweEnsureRun}): `seq` is the event the transaction wrote, `orgId` the conversation's.
@@ -141,13 +132,27 @@ export declare class RunQueue {
141
132
  /**
142
133
  * Stops an orphaned run directly (`POST .../abort`, a tree stop): a `running` run whose
143
134
  * conversation lock is free and whose BullMQ job is gone hears no stop broadcast, so its
144
- * `run_aborted` is written here (status aborted, `consumed_seq` at least to its trigger): true. Not
145
- * an orphan (someone runs it; a queued run's job is owed, or lost and re-added by the
135
+ * `run_aborted` is written here (status aborted, `consumed_seq` at least to its trigger): true.
136
+ * Not an orphan (someone runs it; a queued run's job is owed, or lost and re-added by the
146
137
  * `run-orphan-check`, whose worker then sees the stop marker) or already finished: false, and the
147
138
  * caller broadcasts the stop.
148
139
  */
149
140
  abortIfOrphaned(conversationId: string, runId: string, byAccountId: string | null, detail?: RunAbortDetail): Promise<boolean>;
150
141
  private abortOrphanIn;
142
+ /**
143
+ * Ends a `queued` run in PG before any worker started it (an offboarded member's run, ruling
144
+ * R27): with the conversation lock free, under the conversation row lock, a run still `queued` is
145
+ * finished `aborted` with `run_aborted` (`consumed_seq` at least to its trigger), so a job that
146
+ * runs later finds it finished and starts nothing. True when it did; false when a worker holds
147
+ * the conversation or is in the middle of the run's first start (`run_started` written,
148
+ * `started_at` not yet set; it hears the stop), or the run is no longer queued. A run resumed
149
+ * from `waiting` to `queued` is ended like a new one. The caller writes the stop marker first, so
150
+ * a worker that takes the conversation meanwhile still stops; a worker that read the run before
151
+ * this commit re-checks it under the row locks before its `run_started` and never moves it back
152
+ * to `running`.
153
+ */
154
+ abortQueued(conversationId: string, runId: string, byAccountId: string | null): Promise<boolean>;
155
+ private abortQueuedIn;
151
156
  /** 会话当前未结束的运行(调用方已持有会话行锁)。 */
152
157
  private activeRun;
153
158
  /**
@@ -8,11 +8,10 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
10
  import { randomUUID } from "node:crypto";
11
- import { getQueueToken } from "@nestjs/bullmq";
12
11
  import { Inject, Injectable, Optional } from "@nestjs/common";
13
12
  import { sql } from "kysely";
14
13
  import { visibleTo } from "../agents/agent-lookup.js";
15
- import { BG_WAKE_JOB, bgWakeJobId, wakeDecisionIn, } from "../background/wake-rules.js";
14
+ import { wakeDecisionIn } from "../background/wake-rules.js";
16
15
  import { EventLog } from "../conversations/event-log.js";
17
16
  import { dispatchQueuedIn, pauseQueueWith } from "../conversations/queue.js";
18
17
  import { conversationTree } from "../conversations/tree.js";
@@ -25,7 +24,6 @@ import { linkStartedRunIn } from "../schedules/run-tracker.js";
25
24
  import { scheduledTaskRunAt } from "../schedules/unattended.js";
26
25
  import { KyselyCheckpointSaver } from "./checkpointer.js";
27
26
  import { ConversationLock } from "./conversation-lock.js";
28
- import { MAINTENANCE_QUEUE_NAME } from "./maintenance-queue.js";
29
27
  import { notifyFinished, RunFinish } from "./run-finish.js";
30
28
  import { ACTIVE_RUN_STATUSES, RunJobs, runJobId, runQueueNameFor } from "./run-jobs.js";
31
29
  import { rootConversationOf } from "./subagents/child.js";
@@ -37,12 +35,6 @@ export { ACTIVE_RUN_STATUSES, ENSURE_RUN_BACKSTOP_MS, ENSURE_RUN_JOB, ensureRunJ
37
35
  * kernel never provides it.
38
36
  */
39
37
  export const ENSURE_RUN_FAILS = Symbol("ENSURE_RUN_FAILS");
40
- /**
41
- * Margin added to the delayed wake job: its window is read on the database's clock, the delay runs
42
- * on this process's and Redis's, so the job does not usually fire just before the window ends (one
43
- * that does reschedules itself under its next retry id, `bgWakeJobId`).
44
- */
45
- const WAKE_JOB_MARGIN_MS = 1000;
46
38
  /**
47
39
  * 运行队列(生产方):`ensureRun` 在会话有未消费的用户消息、且没有进行中的运行时插入 `runs` 行并投递任务。
48
40
  * 「是否已有运行」以 PG 为准(会话行锁内检查 `runs` 中的 queued / running),并参考会话锁的持有者;
@@ -58,13 +50,10 @@ let RunQueue = class RunQueue {
58
50
  log;
59
51
  txHost;
60
52
  config;
61
- maintenance;
62
53
  ensureFails;
63
54
  constructor(kdb, eventLog, lock, jobs, finisher,
64
55
  /** An orphan aborted directly loses its checkpoint thread (thread_id = runId). */
65
- checkpoints, log, txHost, config,
66
- /** The producer side of the delayed `bg-wake` job (`MaintenanceProcessor` consumes it). */
67
- maintenance, ensureFails = () => false) {
56
+ checkpoints, log, txHost, config, ensureFails = () => false) {
68
57
  this.kdb = kdb;
69
58
  this.eventLog = eventLog;
70
59
  this.lock = lock;
@@ -74,7 +63,6 @@ let RunQueue = class RunQueue {
74
63
  this.log = log;
75
64
  this.txHost = txHost;
76
65
  this.config = config;
77
- this.maintenance = maintenance;
78
66
  this.ensureFails = ensureFails;
79
67
  }
80
68
  get db() {
@@ -90,7 +78,7 @@ let RunQueue = class RunQueue {
90
78
  * once it is sent; with no head either, a wake-eligible finished background task or sub-agent
91
79
  * delivery (background Bash spec §3, agent map spec §5.3, {@link wakeDecisionIn}): the run is
92
80
  * then attributed to the event's trigger account and joins the tree's budget window, and an
93
- * event inside the wake window schedules the delayed `bg-wake` job instead. A sub-agent's
81
+ * event inside the wake window owes the delayed `bg-wake` job instead. A sub-agent's
94
82
  * conversation is only woken this way ({@link childWakeIn}).
95
83
  *
96
84
  * An active run (queued, running or waiting) is left alone: a run nobody runs any more is the
@@ -111,12 +99,9 @@ let RunQueue = class RunQueue {
111
99
  dispatchedSwitch: false,
112
100
  dispatchedUnarchived: false,
113
101
  pausedNow: false,
114
- wakeLater: null,
102
+ wakeRetry: opts.wakeRetry ?? 0,
115
103
  };
116
104
  const created = await this.ensureRunIn(conversationId, st);
117
- if (st.wakeLater !== null) {
118
- await this.scheduleWake(conversationId, st.wakeLater, opts.wakeRetry ?? 0);
119
- }
120
105
  const { dispatchedSeq, dispatchedSwitch, dispatchedUnarchived, pausedNow } = st;
121
106
  // a pause is published too (batch 5 review I1): the page shows the reason without a reload
122
107
  if (pausedNow)
@@ -144,6 +129,7 @@ let RunQueue = class RunQueue {
144
129
  "queue_paused",
145
130
  "account_id",
146
131
  "deleted_at",
132
+ "tree_stopped_at",
147
133
  ])
148
134
  .where("id", "=", conversationId)
149
135
  .forUpdate()
@@ -227,8 +213,11 @@ let RunQueue = class RunQueue {
227
213
  // Nothing queued either: a finished background task may wake the idle conversation
228
214
  // (background Bash spec §3; a person's message or the queue always goes first)
229
215
  const wake = await wakeDecisionIn(t, { id: conversationId, orgId: conv.org_id, rootId: conversationId }, Number(conv.consumed_seq), this.config.get().runtime.wakeOnBackgroundTask, (accountId) => authorCanSend(t, conversationId, accountId));
230
- if (wake?.kind === "later")
231
- st.wakeLater = wake;
216
+ // inside the wake window: the delayed `bg-wake` job is owed here, so the wake survives a
217
+ // process that dies after the commit (transactional outbox spec §3.1)
218
+ if (wake?.kind === "later") {
219
+ await this.jobs.oweBgWake(conversationId, wake, st.wakeRetry, conv.org_id);
220
+ }
232
221
  if (wake?.kind !== "wake")
233
222
  return null;
234
223
  last = { seq: String(wake.seq), accountId: wake.triggerAccountId };
@@ -284,6 +273,15 @@ let RunQueue = class RunQueue {
284
273
  // the task run its message started follows this run from now on (spec §6)
285
274
  if (taskRunId !== null)
286
275
  await linkStartedRunIn(t, taskRunId, runId, conversationId);
276
+ // the root runs again: a person's stop of its tree no longer holds the tree's wakes
277
+ // (agent map spec §6); read with the row lock above, so the hot path issues no statement
278
+ if (conv.tree_stopped_at !== null) {
279
+ await t
280
+ .updateTable("conversations")
281
+ .set({ tree_stopped_at: null })
282
+ .where("id", "=", conversationId)
283
+ .execute();
284
+ }
287
285
  return runId;
288
286
  }
289
287
  /**
@@ -308,8 +306,9 @@ let RunQueue = class RunQueue {
308
306
  }
309
307
  /**
310
308
  * Owes the new run's job in {@link ensureRunIn}'s transaction, before its run row is written.
311
- * False (logged): the same job id is still owed, so the caller writes no run (a second run of
312
- * one trigger; the conversation's unique active run makes this a should-not-happen).
309
+ * False (logged): the same job id is still owed, so the caller writes no run. A later run of the
310
+ * same trigger has its own `<runJobId>-<n>` ({@link jobIdFor}), so this is a should-not-happen
311
+ * (the conversation has one active run at a time).
313
312
  */
314
313
  async oweJob(conversationId, runId, jobId, kind, orgId) {
315
314
  if (await this.jobs.owe({ runId, conversationId }, jobId, kind, orgId))
@@ -328,10 +327,12 @@ let RunQueue = class RunQueue {
328
327
  * racing the run's end starts a new run, never lost); and it is woken by its own background
329
328
  * task's finish or a delivery from its own sub-agent, under the same rules as a chat
330
329
  * conversation. Neither happens once its last run was stopped (`aborted`): a stopped sub-agent is
331
- * started again only by its parent. Both read the queue and the trigger account's access on the root
332
- * of its tree; a message needs no wake setting and has no wake window. The run counts against
333
- * the root's budget window and runs on the sub-agent queue under `runJobId(child, boundary
334
- * seq)`. An active run is never started past (one nobody runs is the `run-orphan-check`'s).
330
+ * started again only by its parent; nor while a person's stop of the root holds the tree
331
+ * (`conversations.tree_stopped_at`, cleared by the root's next run). Both read the queue and the
332
+ * trigger account's access on the root of its tree; a message needs no wake setting and has no
333
+ * wake window. The run counts against the root's budget window and runs on the sub-agent queue
334
+ * under `runJobId(child, boundary seq)`. An active run is never started past (one nobody runs is
335
+ * the `run-orphan-check`'s).
335
336
  */
336
337
  async childWakeIn(conversationId, conv, st) {
337
338
  const t = this.db;
@@ -353,10 +354,16 @@ let RunQueue = class RunQueue {
353
354
  if (last?.status === "aborted")
354
355
  return null;
355
356
  const rootId = await rootConversationOf(t, conversationId);
357
+ // a tree a person stopped at its root (agent map spec §6, changed in acceptance 2026-11-13)
358
+ // starts no sub-agent run here until the root runs again: an idle sub-agent the stop found
359
+ // with nothing running (or one whose report raced the stop) is not woken by a late finish
360
+ if (await treeStopped(t, rootId))
361
+ return null;
356
362
  const wake = (await this.pendingMessageIn(conversationId, Number(conv.consumed_seq), rootId)) ??
357
363
  (await wakeDecisionIn(t, { id: conversationId, orgId: conv.org_id, rootId }, Number(conv.consumed_seq), this.config.get().runtime.wakeOnBackgroundTask, (accountId) => authorCanSend(t, rootId, accountId)));
358
- if (wake?.kind === "later")
359
- st.wakeLater = wake;
364
+ if (wake?.kind === "later") {
365
+ await this.jobs.oweBgWake(conversationId, wake, st.wakeRetry, conv.org_id);
366
+ }
360
367
  if (wake?.kind !== "wake")
361
368
  return null;
362
369
  const jobId = await this.jobIdFor(conversationId, wake.seq);
@@ -397,8 +404,8 @@ let RunQueue = class RunQueue {
397
404
  return runId;
398
405
  }
399
406
  /**
400
- * The newest unconsumed message of an idle sub-agent (a `SendMessage` its last run did not absorb)
401
- * as a run boundary, or null: none; the root's queue is paused or the root deleted (the
407
+ * The newest unconsumed message of an idle sub-agent (a `SendMessage` its last run did not
408
+ * absorb) as a run boundary, or null: none; the root's queue is paused or the root deleted (the
402
409
  * message waits for the unpause, {@link wakeHeldTree}); or its author can no longer send to the
403
410
  * root (it waits for a later run). The caller already refused a stopped sub-agent (last run
404
411
  * `aborted`): a message that reached it while running and then stopped is parked, and the next
@@ -496,27 +503,6 @@ let RunQueue = class RunQueue {
496
503
  .where("r.status", "in", ACTIVE_RUN_STATUSES))))
497
504
  .execute();
498
505
  }
499
- /**
500
- * Schedules the delayed `bg-wake` job at the end of the conversation's wake window (after the
501
- * commit): one job per conversation and window end, so finishes inside one window share it. It
502
- * calls {@link ensureRun}, which wakes the conversation if a finish is still unconsumed. A failed
503
- * enqueue is logged: the next message, run end or page opening re-derives the wake.
504
- */
505
- async scheduleWake(conversationId, w, retry) {
506
- const data = { conversationId, ...(retry > 0 && { retry }) };
507
- try {
508
- await this.maintenance.add(BG_WAKE_JOB, data, {
509
- // a job that fired early reschedules under a new id (its own is still taken)
510
- jobId: bgWakeJobId(conversationId, w.windowEnd, retry),
511
- delay: w.delayMs + WAKE_JOB_MARGIN_MS,
512
- removeOnComplete: true,
513
- removeOnFail: true,
514
- });
515
- }
516
- catch (err) {
517
- this.log.error("cannot schedule the background task wake", { conversationId, err });
518
- }
519
- }
520
506
  /**
521
507
  * Owes the ensure-run backstop of `conversationId` in the caller's transaction
522
508
  * ({@link oweEnsureRun}): `seq` is the event the transaction wrote, `orgId` the conversation's.
@@ -527,8 +513,8 @@ let RunQueue = class RunQueue {
527
513
  /**
528
514
  * Stops an orphaned run directly (`POST .../abort`, a tree stop): a `running` run whose
529
515
  * conversation lock is free and whose BullMQ job is gone hears no stop broadcast, so its
530
- * `run_aborted` is written here (status aborted, `consumed_seq` at least to its trigger): true. Not
531
- * an orphan (someone runs it; a queued run's job is owed, or lost and re-added by the
516
+ * `run_aborted` is written here (status aborted, `consumed_seq` at least to its trigger): true.
517
+ * Not an orphan (someone runs it; a queued run's job is owed, or lost and re-added by the
532
518
  * `run-orphan-check`, whose worker then sees the stop marker) or already finished: false, and the
533
519
  * caller broadcasts the stop.
534
520
  */
@@ -567,6 +553,67 @@ let RunQueue = class RunQueue {
567
553
  },
568
554
  });
569
555
  }
556
+ /**
557
+ * Ends a `queued` run in PG before any worker started it (an offboarded member's run, ruling
558
+ * R27): with the conversation lock free, under the conversation row lock, a run still `queued` is
559
+ * finished `aborted` with `run_aborted` (`consumed_seq` at least to its trigger), so a job that
560
+ * runs later finds it finished and starts nothing. True when it did; false when a worker holds
561
+ * the conversation or is in the middle of the run's first start (`run_started` written,
562
+ * `started_at` not yet set; it hears the stop), or the run is no longer queued. A run resumed
563
+ * from `waiting` to `queued` is ended like a new one. The caller writes the stop marker first, so
564
+ * a worker that takes the conversation meanwhile still stops; a worker that read the run before
565
+ * this commit re-checks it under the row locks before its `run_started` and never moves it back
566
+ * to `running`.
567
+ */
568
+ async abortQueued(conversationId, runId, byAccountId) {
569
+ if ((await this.lock.holder(conversationId)) !== null)
570
+ return false;
571
+ const toSeq = await this.abortQueuedIn(conversationId, runId, byAccountId);
572
+ if (toSeq === null)
573
+ return false;
574
+ await notifyFinished(this.eventLog, conversationId, toSeq, "aborted");
575
+ await this.dropCheckpoints(runId);
576
+ return true;
577
+ }
578
+ async abortQueuedIn(conversationId, runId, byAccountId) {
579
+ const t = this.db;
580
+ const conv = await t
581
+ .selectFrom("conversations")
582
+ .select("id")
583
+ .where("id", "=", conversationId)
584
+ .forUpdate()
585
+ .executeTakeFirst();
586
+ const active = await this.activeRun(t, conversationId);
587
+ if (!conv || !active || active.id !== runId || active.status !== "queued")
588
+ return null;
589
+ // a worker is in the middle of the run's FIRST start (its `run_started` written, `started_at`
590
+ // not set yet; it holds the conversation since): the stop marker ends it. A run resumed from
591
+ // `waiting` back to `queued` has both from its first start and is ended here like a new one:
592
+ // its worker skips `run_started` (it exists) and its conditional `running` update finds the run
593
+ // finished
594
+ const first = await t
595
+ .selectFrom("runs")
596
+ .select("started_at")
597
+ .where("id", "=", runId)
598
+ .executeTakeFirst();
599
+ if (first?.started_at === null) {
600
+ const started = await t
601
+ .selectFrom("conversation_events")
602
+ .select("seq")
603
+ .where("conversation_id", "=", conversationId)
604
+ .where("idem_key", "=", `run_started:${runId}`)
605
+ .executeTakeFirst();
606
+ if (started)
607
+ return null;
608
+ }
609
+ return this.finisher.finishIn({
610
+ conversationId,
611
+ runId,
612
+ status: "aborted",
613
+ consumed: null,
614
+ event: { type: "run_aborted", payload: { byAccountId, reason: "user" } },
615
+ });
616
+ }
570
617
  /** 会话当前未结束的运行(调用方已持有会话行锁)。 */
571
618
  activeRun(t, conversationId) {
572
619
  return t
@@ -604,6 +651,9 @@ __decorate([
604
651
  __decorate([
605
652
  Transactional({ propagation: "requires_new" })
606
653
  ], RunQueue.prototype, "abortOrphanIn", null);
654
+ __decorate([
655
+ Transactional({ propagation: "requires_new" })
656
+ ], RunQueue.prototype, "abortQueuedIn", null);
607
657
  RunQueue = __decorate([
608
658
  Injectable(),
609
659
  __param(0, Inject(KernelDb)),
@@ -615,9 +665,8 @@ RunQueue = __decorate([
615
665
  __param(6, Inject(KERNEL_LOGGER)),
616
666
  __param(7, Inject(TransactionHost)),
617
667
  __param(8, Inject(KernelConfigService)),
618
- __param(9, Inject(getQueueToken(MAINTENANCE_QUEUE_NAME))),
619
- __param(10, Optional()),
620
- __param(10, Inject(ENSURE_RUN_FAILS))
668
+ __param(9, Optional()),
669
+ __param(9, Inject(ENSURE_RUN_FAILS))
621
670
  ], RunQueue);
622
671
  export { RunQueue };
623
672
  /** The conversation's owner is active and can still see its agent (`isAccessible`'s rule). */
@@ -642,3 +691,15 @@ async function authorCanSend(t, conversationId, accountId) {
642
691
  .executeTakeFirst();
643
692
  return row !== undefined;
644
693
  }
694
+ /**
695
+ * Whether a person's stop of the root conversation holds its tree (`tree_stopped_at`, agent map
696
+ * spec §6): set by the stop, cleared when the root next starts a run.
697
+ */
698
+ async function treeStopped(t, rootId) {
699
+ const root = await t
700
+ .selectFrom("conversations")
701
+ .select("tree_stopped_at")
702
+ .where("id", "=", rootId)
703
+ .executeTakeFirst();
704
+ return root?.tree_stopped_at != null;
705
+ }
@@ -0,0 +1,12 @@
1
+ import type { KernelSchemaDb } from "../db/kernel-schema.js";
2
+ /** 运行的终态。 */
3
+ export type TerminalStatus = "completed" | "failed" | "aborted";
4
+ /** The terminal run statuses (a leaf module: the event writers of a run start import it too). */
5
+ export declare const TERMINAL_STATUSES: ReadonlySet<string>;
6
+ /**
7
+ * Whether run `runId` has ended, read in the caller's transaction with its row locked: call it
8
+ * after the conversation row lock (lock order conversation, then run, as `RunFinish.finishIn`), so
9
+ * no stop can end the run between this read and the transaction's appends. A missing run is not
10
+ * ended (a caller may record for a run id without a row).
11
+ */
12
+ export declare function runEndedIn(db: KernelSchemaDb, runId: string): Promise<boolean>;
@@ -0,0 +1,21 @@
1
+ /** The terminal run statuses (a leaf module: the event writers of a run start import it too). */
2
+ export const TERMINAL_STATUSES = new Set([
3
+ "completed",
4
+ "failed",
5
+ "aborted",
6
+ ]);
7
+ /**
8
+ * Whether run `runId` has ended, read in the caller's transaction with its row locked: call it
9
+ * after the conversation row lock (lock order conversation, then run, as `RunFinish.finishIn`), so
10
+ * no stop can end the run between this read and the transaction's appends. A missing run is not
11
+ * ended (a caller may record for a run id without a row).
12
+ */
13
+ export async function runEndedIn(db, runId) {
14
+ const row = await db
15
+ .selectFrom("runs")
16
+ .select("status")
17
+ .where("id", "=", runId)
18
+ .forUpdate()
19
+ .executeTakeFirst();
20
+ return row !== undefined && TERMINAL_STATUSES.has(row.status);
21
+ }
@@ -8,9 +8,11 @@ import { KernelDb } from "../infra/database/kernel-db.js";
8
8
  import { TransactionHost } from "../infra/database/transaction-host.js";
9
9
  import { KernelEvents } from "../infra/events/kernel-events.js";
10
10
  import { SkillsCatalogService } from "../skills/catalog.js";
11
+ import { ToolRegistry } from "../tools/registry.js";
11
12
  import { RunAbort } from "./abort.js";
12
13
  import { KyselyCheckpointSaver } from "./checkpointer.js";
13
14
  import { ConversationLock } from "./conversation-lock.js";
15
+ import { PluginContextService } from "./plugin-context.service.js";
14
16
  import { ProviderRegistry } from "./providers.js";
15
17
  import { type OrgQuota } from "./quota.js";
16
18
  import { RunContextService } from "./run-context.service.js";
@@ -32,6 +34,12 @@ export interface RunHooks {
32
34
  lockTtlMs?: number;
33
35
  /** 任务结束后的每次 ensureRun 尝试之前调用(attempt 从 0 起);抛错即模拟该次尝试失败。 */
34
36
  beforeEnsureRun?(conversationId: string, attempt: number): Promise<void>;
37
+ /** Called after a run job read the run's status, before it takes the conversation lock. */
38
+ afterRunRead?(runId: string, conversationId: string): Promise<void>;
39
+ /** Called with the conversation lock held, before a queued run's `run_started` is written. */
40
+ beforeRunStarted?(runId: string, conversationId: string): Promise<void>;
41
+ /** Called with the conversation lock held, before a queued run's status becomes `running`. */
42
+ beforeRunning?(runId: string, conversationId: string): Promise<void>;
35
43
  }
36
44
  /** 处理函数的返回值:`ensure` 表示任务完成后再调用一次 ensureRun。 */
37
45
  export type ProcessResult = "ensure" | undefined;
@@ -65,6 +73,10 @@ export declare class RunWorker {
65
73
  private readonly skillsCatalog;
66
74
  /** Run start's run context (run context spec §5): who the agent is, the day, the person. */
67
75
  private readonly runContext;
76
+ /** Run start's plugin context (memory plugin spec §6.1): the `context@1` providers' text. */
77
+ private readonly pluginContext;
78
+ /** The run's resolved tools, for the plugin context's `tools` (chat runs with providers). */
79
+ private readonly tools;
68
80
  private readonly finisher;
69
81
  readonly txHost: TransactionHost;
70
82
  /** The steps' live drafts this process writes for the runs it executes. */
@@ -93,7 +105,11 @@ export declare class RunWorker {
93
105
  /** Run start's skills catalog snapshot (reads the pinned skills' bodies, phase B). */
94
106
  skillsCatalog: Pick<SkillsCatalogService, "record">,
95
107
  /** Run start's run context (run context spec §5): who the agent is, the day, the person. */
96
- runContext: Pick<RunContextService, "record">, finisher: Pick<RunFinish, "finish">, txHost: TransactionHost,
108
+ runContext: Pick<RunContextService, "record">,
109
+ /** Run start's plugin context (memory plugin spec §6.1): the `context@1` providers' text. */
110
+ pluginContext: Pick<PluginContextService, "record">,
111
+ /** The run's resolved tools, for the plugin context's `tools` (chat runs with providers). */
112
+ tools: Pick<ToolRegistry, "forAgent">, finisher: Pick<RunFinish, "finish">, txHost: TransactionHost,
97
113
  /** The steps' live drafts this process writes for the runs it executes. */
98
114
  drafts: RunDrafts, hooks?: RunHooks | undefined);
99
115
  private get db();
@@ -123,8 +139,10 @@ export declare class RunWorker {
123
139
  /**
124
140
  * After any terminal commit of a run that is not a sub-agent's (sub-agents spec §12): stops its
125
141
  * active fork invocation child on its behalf (no account); background `Agent` children keep
126
- * running (agent map spec §5.4). The terminal transaction owed the same stop (`stop-tree`), the
127
- * backstop when this call never happens.
142
+ * running (agent map spec §5.4), unless a person stopped the run: then the sub-agents of its
143
+ * windows stop too, also one it dispatched after the stop read the tree (agent map spec §6,
144
+ * changed in acceptance 2026-11-13). The terminal transaction owed the same stops
145
+ * (`stop-tree`), the backstop when this call never happens.
128
146
  */
129
147
  private stopChildren;
130
148
  /**
@@ -159,6 +177,11 @@ export declare class RunWorker {
159
177
  process(job: Job<RunJobData>, token: string | undefined): Promise<ProcessResult>;
160
178
  private execute;
161
179
  private completed;
180
+ /**
181
+ * The run's extension tool keys by plugin, resolved as a chat run's first step lists them
182
+ * (disabled tools, org switches, the conversation's source): a provider's `tools`.
183
+ */
184
+ private pluginToolKeys;
162
185
  /**
163
186
  * When the conversation was switched to a model that no longer resolves (provider disabled,
164
187
  * model removed), writes `model_switched { model: null, effort: null, reason: "unavailable" }`
@@ -167,6 +190,12 @@ export declare class RunWorker {
167
190
  * handles it). Resolved for the conversation's org (its effective provider config).
168
191
  */
169
192
  private fallBackIfUnavailable;
193
+ /**
194
+ * Whether the run is gone or terminal, read in the caller's transaction under the locks
195
+ * (conversation, then run, as `RunFinish.finishIn`): the job read its status before taking the
196
+ * conversation lock, and a stop may have ended it since.
197
+ */
198
+ private endedUnderLocks;
170
199
  /**
171
200
  * `run_started` under the conversation row lock, with the model / effort switch in effect then
172
201
  * (a chat run's `saved` version under the conversation's selection): what the run uses.