@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
@@ -1,4 +1,4 @@
1
- import { type ContentBlock, type Logger, type ModelRequest, type ModelStreamEvent, type ModelUsage, type StopReason } from "@capybara-hq/plugin-sdk";
1
+ import { type ContentBlock, type ConversationSource, type Logger, type ModelRequest, type ModelStreamEvent, type ModelUsage, type StopReason } from "@capybara-hq/plugin-sdk";
2
2
  import type { AgentDefinition } from "../agents/definition.js";
3
3
  import type { AppendEvent, EventLog } from "../conversations/event-log.js";
4
4
  import { type ConversationEvent } from "../conversations/events.js";
@@ -104,6 +104,11 @@ export interface ModelStepInput {
104
104
  /** Hook payload scope: the run's agent, its trigger account and the conversation kind. */
105
105
  agentId?: string;
106
106
  accountId?: string;
107
+ /**
108
+ * The conversation's source (memory plugin spec §6.2): extension tools limited by
109
+ * `spec.sources` are listed by it.
110
+ */
111
+ source: ConversationSource;
107
112
  /** Hooks fire for `chat` and `subagent` (never for eval replays). */
108
113
  conversationKind?: ConversationKindForHooks;
109
114
  /** A sub-agent's run: where it was dispatched from (hook payloads carry it as `parent`). */
@@ -159,6 +159,7 @@ wrapUpReason) {
159
159
  forkChild: input.parent?.forSkill === true,
160
160
  sandboxConversationId: input.parent?.rootConversationId ?? input.conversationId,
161
161
  replay: input.conversationKind === "eval",
162
+ source: input.source,
162
163
  skillLoad: skillLoadOffered(events.findLast((e) => e.type === "skills_catalog"), hasInvocation(events)),
163
164
  });
164
165
  const tools = {
@@ -0,0 +1,75 @@
1
+ import { type ConversationSource } from "@capybara-hq/plugin-sdk";
2
+ import { EventLog } from "../conversations/event-log.js";
3
+ import { ExtensionPointDefaults } from "../extensions/point-defaults.js";
4
+ import { ExtensionRegistry } from "../extensions/registry.js";
5
+ import { KernelDb } from "../infra/database/kernel-db.js";
6
+ import { TransactionHost } from "../infra/database/transaction-host.js";
7
+ import type { KernelLogger } from "../log.js";
8
+ import type { OrgRole } from "../orgs/roles.js";
9
+ /** What a run start tells {@link PluginContextService.record}. */
10
+ export interface PluginContextInput {
11
+ orgId: string;
12
+ conversationId: string;
13
+ runId: string;
14
+ agentId: string;
15
+ /** The run's agent definition name. */
16
+ agentName: string;
17
+ /** The conversation's owner (`conversations.account_id`) and display name. */
18
+ ownerAccountId: string;
19
+ ownerName: string;
20
+ /** The account whose message started the run. */
21
+ triggerAccountId: string;
22
+ source: ConversationSource;
23
+ /** The trigger account's role in the org. */
24
+ role: OrgRole;
25
+ /** The conversation's kind: only a chat run asks the providers. */
26
+ kind: "chat" | "eval" | "subagent";
27
+ /**
28
+ * The keys of the run's resolved tool set, by plugin (resolved with `forAgent` once, and only
29
+ * when some provider is asked).
30
+ */
31
+ toolKeysByPlugin: () => Promise<Map<string, string[]>>;
32
+ /** Aborts every provider call (with its own time limit). */
33
+ signal: AbortSignal;
34
+ }
35
+ /**
36
+ * Run start's plugin context (memory plugin spec §6.1): asks every `context@1` provider enabled in
37
+ * the org (valid config) for its text, in parallel and outside any transaction, each under the
38
+ * run's signal and {@link CONTEXT_LIMITS}.timeoutMs; then appends a `plugin_context` event for each
39
+ * plugin whose text differs from the latest one it recorded in the conversation. Fail open: a
40
+ * provider that throws, times out or returns too much is skipped for this run (its earlier text
41
+ * stays in effect) and logged. A provider loaded here that recorded text but no longer serves the
42
+ * org (switched off, its stored org config or its effective config invalid) gets an empty event,
43
+ * so its text stops following summaries; a plugin not loaded on this replica is left alone
44
+ * (another replica may still load it during a rollout, and blanking it here would make its text
45
+ * flap), and so is one whose status could not be read (a transient read error must not flip the
46
+ * model's context). Chat runs only: an
47
+ * eval replay reads the recorded events, and a sub-agent gets no plugin context (spec §5.4).
48
+ */
49
+ export declare class PluginContextService {
50
+ private readonly db;
51
+ private readonly eventLog;
52
+ readonly txHost: TransactionHost;
53
+ private readonly points;
54
+ private readonly registry;
55
+ private readonly log;
56
+ constructor(db: KernelDb, eventLog: Pick<EventLog, "appendIn" | "notify">, txHost: TransactionHost, points: Pick<ExtensionPointDefaults, "providersFor">, registry: Pick<ExtensionRegistry, "list">, log: KernelLogger);
57
+ /**
58
+ * Asks the providers and records what changed (see the class); notifies after the commit. False:
59
+ * the run had ended under the locks (a stop landed while the providers were asked), so nothing
60
+ * was appended and the run must not go on (as `RunContextService.record`).
61
+ */
62
+ record(input: PluginContextInput): Promise<boolean>;
63
+ /** One provider's call, bounded by the run's signal and the time limit. */
64
+ private ask;
65
+ /**
66
+ * One transaction under the conversation row lock (as `RunContextService`): compares each
67
+ * answer's hash with the plugin's latest `plugin_context` and appends the ones that differ (a
68
+ * plugin with no earlier event and no text writes nothing), plus an empty event for each plugin
69
+ * in `off` whose latest text is not empty. Idem key per run and plugin, so a retried run
70
+ * start finds its events or nothing changed. Returns the last seq written; `ended` when the run
71
+ * is terminal under the locks (the providers are asked outside the transaction, so a stop may
72
+ * have ended it meanwhile: nothing is appended after its end).
73
+ */
74
+ private append;
75
+ }
@@ -0,0 +1,220 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { createHash } from "node:crypto";
11
+ import { CONTEXT_LIMITS, } from "@capybara-hq/plugin-sdk";
12
+ import { Inject, Injectable } from "@nestjs/common";
13
+ import { EventLog } from "../conversations/event-log.js";
14
+ import { ExtensionPointDefaults } from "../extensions/point-defaults.js";
15
+ import { ExtensionRegistry } from "../extensions/registry.js";
16
+ import { KernelDb } from "../infra/database/kernel-db.js";
17
+ import { TransactionHost } from "../infra/database/transaction-host.js";
18
+ import { Transactional } from "../infra/database/transactional.decorator.js";
19
+ import { KERNEL_LOGGER } from "../infra/tokens.js";
20
+ import { latestPluginContexts, pluginContextIdemKey } from "./context/plugin-context.js";
21
+ import { runEndedIn } from "./run-status.js";
22
+ const sha256 = (text) => createHash("sha256").update(text).digest("hex");
23
+ /**
24
+ * Run start's plugin context (memory plugin spec §6.1): asks every `context@1` provider enabled in
25
+ * the org (valid config) for its text, in parallel and outside any transaction, each under the
26
+ * run's signal and {@link CONTEXT_LIMITS}.timeoutMs; then appends a `plugin_context` event for each
27
+ * plugin whose text differs from the latest one it recorded in the conversation. Fail open: a
28
+ * provider that throws, times out or returns too much is skipped for this run (its earlier text
29
+ * stays in effect) and logged. A provider loaded here that recorded text but no longer serves the
30
+ * org (switched off, its stored org config or its effective config invalid) gets an empty event,
31
+ * so its text stops following summaries; a plugin not loaded on this replica is left alone
32
+ * (another replica may still load it during a rollout, and blanking it here would make its text
33
+ * flap), and so is one whose status could not be read (a transient read error must not flip the
34
+ * model's context). Chat runs only: an
35
+ * eval replay reads the recorded events, and a sub-agent gets no plugin context (spec §5.4).
36
+ */
37
+ let PluginContextService = class PluginContextService {
38
+ db;
39
+ eventLog;
40
+ txHost;
41
+ points;
42
+ registry;
43
+ log;
44
+ constructor(db, eventLog, txHost, points, registry, log) {
45
+ this.db = db;
46
+ this.eventLog = eventLog;
47
+ this.txHost = txHost;
48
+ this.points = points;
49
+ this.registry = registry;
50
+ this.log = log;
51
+ }
52
+ /**
53
+ * Asks the providers and records what changed (see the class); notifies after the commit. False:
54
+ * the run had ended under the locks (a stop landed while the providers were asked), so nothing
55
+ * was appended and the run must not go on (as `RunContextService.record`).
56
+ */
57
+ async record(input) {
58
+ if (input.kind !== "chat")
59
+ return true;
60
+ let found;
61
+ try {
62
+ found = await this.points.providersFor(input.orgId, "context");
63
+ }
64
+ catch (err) {
65
+ // fail open: without the org's providers nothing is asked and nothing is switched off
66
+ this.log.warn("context providers could not be listed; plugin context left as recorded", {
67
+ orgId: input.orgId,
68
+ conversationId: input.conversationId,
69
+ runId: input.runId,
70
+ err,
71
+ });
72
+ return true;
73
+ }
74
+ const { providers } = found;
75
+ const tools = providers.length > 0 ? await input.toolKeysByPlugin() : new Map();
76
+ const answers = await Promise.all(providers.map(async (p) => {
77
+ const answer = await this.ask(p, input, tools.get(p.extension) ?? []);
78
+ if (!answer.ok) {
79
+ this.log.warn("context provider skipped for this run", {
80
+ orgId: input.orgId,
81
+ conversationId: input.conversationId,
82
+ runId: input.runId,
83
+ plugin: p.extension,
84
+ reason: answer.reason,
85
+ });
86
+ }
87
+ return [p.extension, answer];
88
+ }));
89
+ const texts = new Map();
90
+ for (const [plugin, answer] of answers)
91
+ if (answer.ok)
92
+ texts.set(plugin, answer.text);
93
+ const serving = new Set(providers.map((p) => p.extension));
94
+ const off = new Set(this.registry
95
+ .list("context")
96
+ .map((e) => e.plugin)
97
+ // a provider whose status could not be read (a transient error) keeps its recorded text
98
+ .filter((plugin) => !serving.has(plugin) && !found.failed.has(plugin)));
99
+ // nothing answered and nothing to switch off: no transaction, no row lock
100
+ if (texts.size === 0 && off.size === 0)
101
+ return true;
102
+ const seq = await this.append(input, texts, off);
103
+ if (seq === "ended")
104
+ return false;
105
+ if (seq !== null)
106
+ await this.eventLog.notify(input.conversationId, seq);
107
+ return true;
108
+ }
109
+ /** One provider's call, bounded by the run's signal and the time limit. */
110
+ async ask(p, input, tools) {
111
+ const signal = AbortSignal.any([input.signal, AbortSignal.timeout(CONTEXT_LIMITS.timeoutMs)]);
112
+ let result;
113
+ try {
114
+ result = await untilAborted(Promise.resolve().then(() => p.impl.provide({
115
+ orgId: input.orgId,
116
+ accountId: input.triggerAccountId,
117
+ agentId: input.agentId,
118
+ agentName: input.agentName,
119
+ conversationId: input.conversationId,
120
+ runId: input.runId,
121
+ source: input.source,
122
+ role: input.role,
123
+ ownerAccountId: input.ownerAccountId,
124
+ ownerName: input.ownerName,
125
+ tools,
126
+ config: p.config,
127
+ signal,
128
+ })), signal);
129
+ }
130
+ catch (err) {
131
+ if (signal.aborted) {
132
+ return { ok: false, reason: input.signal.aborted ? "run aborted" : "timed out" };
133
+ }
134
+ return { ok: false, reason: `threw: ${err instanceof Error ? err.message : String(err)}` };
135
+ }
136
+ if (result === null)
137
+ return { ok: true, text: "" };
138
+ const text = result?.text;
139
+ if (typeof text !== "string")
140
+ return { ok: false, reason: "returned no text" };
141
+ const bytes = Buffer.byteLength(text, "utf8");
142
+ if (bytes > CONTEXT_LIMITS.maxBytes) {
143
+ return { ok: false, reason: `text of ${bytes} bytes is over ${CONTEXT_LIMITS.maxBytes}` };
144
+ }
145
+ return { ok: true, text };
146
+ }
147
+ /**
148
+ * One transaction under the conversation row lock (as `RunContextService`): compares each
149
+ * answer's hash with the plugin's latest `plugin_context` and appends the ones that differ (a
150
+ * plugin with no earlier event and no text writes nothing), plus an empty event for each plugin
151
+ * in `off` whose latest text is not empty. Idem key per run and plugin, so a retried run
152
+ * start finds its events or nothing changed. Returns the last seq written; `ended` when the run
153
+ * is terminal under the locks (the providers are asked outside the transaction, so a stop may
154
+ * have ended it meanwhile: nothing is appended after its end).
155
+ */
156
+ async append(input, texts, off) {
157
+ const t = this.db.db;
158
+ await t
159
+ .selectFrom("conversations")
160
+ .select("id")
161
+ .where("id", "=", input.conversationId)
162
+ .forUpdate()
163
+ .executeTakeFirst();
164
+ if (await runEndedIn(t, input.runId))
165
+ return "ended";
166
+ const rows = await t
167
+ .selectFrom("conversation_events")
168
+ .select(["seq", "payload"])
169
+ .where("conversation_id", "=", input.conversationId)
170
+ .where("type", "=", "plugin_context")
171
+ .orderBy("seq", "asc")
172
+ .execute();
173
+ const latest = latestPluginContexts(rows.map((r) => ({ type: "plugin_context", seq: Number(r.seq), payload: r.payload })), Number.POSITIVE_INFINITY);
174
+ const next = new Map(texts);
175
+ for (const [plugin, prev] of latest) {
176
+ if (off.has(plugin) && prev.text !== "")
177
+ next.set(plugin, "");
178
+ }
179
+ const changed = [...next]
180
+ .filter(([plugin, text]) => {
181
+ const prev = latest.get(plugin);
182
+ return prev === undefined ? text !== "" : prev.hash !== sha256(text);
183
+ })
184
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
185
+ if (changed.length === 0)
186
+ return null;
187
+ const written = await this.eventLog.appendIn(input.conversationId, changed.map(([plugin, text]) => ({
188
+ type: "plugin_context",
189
+ runId: input.runId,
190
+ idemKey: pluginContextIdemKey(input.runId, plugin),
191
+ payload: { plugin, text, hash: sha256(text) },
192
+ })));
193
+ return Math.max(...written.map((e) => e.seq));
194
+ }
195
+ };
196
+ __decorate([
197
+ Transactional()
198
+ ], PluginContextService.prototype, "append", null);
199
+ PluginContextService = __decorate([
200
+ Injectable(),
201
+ __param(0, Inject(KernelDb)),
202
+ __param(1, Inject(EventLog)),
203
+ __param(2, Inject(TransactionHost)),
204
+ __param(3, Inject(ExtensionPointDefaults)),
205
+ __param(4, Inject(ExtensionRegistry)),
206
+ __param(5, Inject(KERNEL_LOGGER))
207
+ ], PluginContextService);
208
+ export { PluginContextService };
209
+ /** `p`, or a rejection once `signal` aborts (a provider that ignores its signal never holds a run). */
210
+ function untilAborted(p, signal) {
211
+ return new Promise((resolve, reject) => {
212
+ const onAbort = () => reject(signal.reason);
213
+ if (signal.aborted) {
214
+ onAbort();
215
+ return;
216
+ }
217
+ signal.addEventListener("abort", onAbort, { once: true });
218
+ p.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
219
+ });
220
+ }
@@ -41,12 +41,16 @@ export declare class RunContextService {
41
41
  private readonly now;
42
42
  constructor(db: KernelDb, eventLog: Pick<EventLog, "appendIn" | "notify">, txHost: TransactionHost, now?: () => number);
43
43
  private clock;
44
- /** Records the run's context (see the class) and notifies after the commit. */
45
- record(input: RunContextInput): Promise<void>;
44
+ /**
45
+ * Records the run's context (see the class) and notifies after the commit. False: the run had
46
+ * ended (a stop got there first), so nothing was appended and the run must not go on.
47
+ */
48
+ record(input: RunContextInput): Promise<boolean>;
46
49
  /**
47
50
  * One transaction under the conversation row lock (as `lockLatestCatalog`): fold, read the
48
51
  * current values, append when something differs, with the idem key `run_context:<runId>` (a
49
- * retried run start finds its event, or finds nothing changed). Returns the seq written.
52
+ * retried run start finds its event, or finds nothing changed). Returns the seq written;
53
+ * `ended` when the run is terminal under the locks (nothing is appended after its end).
50
54
  */
51
55
  private append;
52
56
  /** A root run: agent, org, and the date, zone and language of the run's person (§3.4). */
@@ -17,6 +17,7 @@ import { Transactional } from "../infra/database/transactional.decorator.js";
17
17
  import { readOrgSettings } from "../orgs/settings.service.js";
18
18
  import { isTimeZone } from "../time-zone.js";
19
19
  import { currentRunContext, dateIn, runContextChange, sanitizeSenderName, snapshotDescription, } from "./context/run-context.js";
20
+ import { runEndedIn } from "./run-status.js";
20
21
  /**
21
22
  * Optional provider: the clock run start reads the date from and the `CurrentTime` builtin reads
22
23
  * the time from (default `Date.now`; tests).
@@ -46,16 +47,23 @@ let RunContextService = class RunContextService {
46
47
  clock() {
47
48
  return new Date(this.now());
48
49
  }
49
- /** Records the run's context (see the class) and notifies after the commit. */
50
+ /**
51
+ * Records the run's context (see the class) and notifies after the commit. False: the run had
52
+ * ended (a stop got there first), so nothing was appended and the run must not go on.
53
+ */
50
54
  async record(input) {
51
55
  const seq = await this.append(input);
56
+ if (seq === "ended")
57
+ return false;
52
58
  if (seq !== null)
53
59
  await this.eventLog.notify(input.conversationId, seq);
60
+ return true;
54
61
  }
55
62
  /**
56
63
  * One transaction under the conversation row lock (as `lockLatestCatalog`): fold, read the
57
64
  * current values, append when something differs, with the idem key `run_context:<runId>` (a
58
- * retried run start finds its event, or finds nothing changed). Returns the seq written.
65
+ * retried run start finds its event, or finds nothing changed). Returns the seq written;
66
+ * `ended` when the run is terminal under the locks (nothing is appended after its end).
59
67
  */
60
68
  async append(input) {
61
69
  const t = this.db.db;
@@ -65,6 +73,8 @@ let RunContextService = class RunContextService {
65
73
  .where("id", "=", input.conversationId)
66
74
  .forUpdate()
67
75
  .executeTakeFirst();
76
+ if (await runEndedIn(t, input.runId))
77
+ return "ended";
68
78
  const prev = await foldOf(t, input.conversationId);
69
79
  const next = input.subagent === undefined
70
80
  ? await this.rootContext(t, input)
@@ -17,7 +17,7 @@ export declare class RunControl {
17
17
  /** Owes a deleted root's tree stop in the delete's transaction. */
18
18
  private readonly outbox;
19
19
  private readonly log;
20
- constructor(queue: Pick<RunQueue, "ensureRun" | "oweEnsureRun" | "abortIfOrphaned" | "wakeHeldTree" | "oweHeldTreeIn">, waits: Pick<Waits, "abortWaiting" | "childFinished" | "stopChildren" | "stopTree">, abort: Pick<RunAbort, "request">,
20
+ constructor(queue: Pick<RunQueue, "ensureRun" | "oweEnsureRun" | "abortIfOrphaned" | "abortQueued" | "wakeHeldTree" | "oweHeldTreeIn">, waits: Pick<Waits, "abortWaiting" | "childFinished" | "stopChildren" | "stopTree">, abort: Pick<RunAbort, "request">,
21
21
  /** The resume of a waiting run in a decision's transaction (a reply's skips). */
22
22
  resumes: Pick<RunResume, "resumeIn">,
23
23
  /** Owes a deleted root's tree stop in the delete's transaction. */
@@ -61,6 +61,11 @@ export declare class RunControl {
61
61
  * stops its children (sub-agents spec §12).
62
62
  */
63
63
  abortIfOrphaned(conversationId: string, runId: string, byAccountId: string | null): Promise<boolean>;
64
+ /**
65
+ * Ends a run still `queued` in PG ({@link RunQueue.abortQueued}; the caller wrote the stop marker
66
+ * first): true when it did. A stopped sub-agent then reports to its parent at once.
67
+ */
68
+ abortQueued(conversationId: string, runId: string, byAccountId: string | null): Promise<boolean>;
64
69
  /**
65
70
  * 运行在等人处理(waiting)时直接中止:取消等待、写 `run_aborted`、删除 checkpoint,返回 true;
66
71
  * 运行已不是 waiting(例如刚被续跑)→ false. The stop's transaction owes the IM delivery that
@@ -110,6 +110,19 @@ let RunControl = class RunControl {
110
110
  }
111
111
  return r;
112
112
  }
113
+ /**
114
+ * Ends a run still `queued` in PG ({@link RunQueue.abortQueued}; the caller wrote the stop marker
115
+ * first): true when it did. A stopped sub-agent then reports to its parent at once.
116
+ */
117
+ async abortQueued(conversationId, runId, byAccountId) {
118
+ const r = await this.queue.abortQueued(conversationId, runId, byAccountId);
119
+ if (r) {
120
+ await this.waits
121
+ .childFinished(runId)
122
+ .catch((err) => this.log.error("cannot report finished sub-agent run to its parent", { runId, err }));
123
+ }
124
+ return r;
125
+ }
113
126
  /**
114
127
  * 运行在等人处理(waiting)时直接中止:取消等待、写 `run_aborted`、删除 checkpoint,返回 true;
115
128
  * 运行已不是 waiting(例如刚被续跑)→ false. The stop's transaction owes the IM delivery that
@@ -58,8 +58,8 @@ export declare class RunFinish {
58
58
  * invocation gets its report (`pairStoppedForks`; an `Agent` call has its result from its
59
59
  * dispatch), so no terminal path leaves an open call. After it, the stops and the delivery the
60
60
  * run's end causes are owed here (runtime/subagents/tree-jobs.ts), the caller still doing them
61
- * right after its commit: `stop-tree` when an active fork child remains or a sub-agent run a
62
- * person stopped has sub-agents below it, `deliver-to-parent` for a sub-agent's run. The
61
+ * right after its commit: `stop-tree` when an active fork child remains or a run a person
62
+ * stopped (a root's or a sub-agent's) has sub-agents below it, `deliver-to-parent` for a sub-agent's run. The
63
63
  * delivery is a job, not written here: this transaction holds the child's conversation and run
64
64
  * rows, and the parent's conversation row comes first in the dispatch's lock order (parent
65
65
  * conversation, parent run, budget run, child conversation).
@@ -63,8 +63,8 @@ let RunFinish = class RunFinish {
63
63
  * invocation gets its report (`pairStoppedForks`; an `Agent` call has its result from its
64
64
  * dispatch), so no terminal path leaves an open call. After it, the stops and the delivery the
65
65
  * run's end causes are owed here (runtime/subagents/tree-jobs.ts), the caller still doing them
66
- * right after its commit: `stop-tree` when an active fork child remains or a sub-agent run a
67
- * person stopped has sub-agents below it, `deliver-to-parent` for a sub-agent's run. The
66
+ * right after its commit: `stop-tree` when an active fork child remains or a run a person
67
+ * stopped (a root's or a sub-agent's) has sub-agents below it, `deliver-to-parent` for a sub-agent's run. The
68
68
  * delivery is a job, not written here: this transaction holds the child's conversation and run
69
69
  * rows, and the parent's conversation row comes first in the dispatch's lock order (parent
70
70
  * conversation, parent run, budget run, child conversation).
@@ -102,9 +102,14 @@ let RunFinish = class RunFinish {
102
102
  const [ev] = await this.eventLog.appendIn(a.conversationId, [
103
103
  { ...a.event, runId: a.runId, idemKey: `${a.event.type}:${a.runId}` },
104
104
  ]);
105
+ // a root run a person stopped holds the root's tree (agent map spec §6, acceptance
106
+ // 2026-11-13): marked with its terminal state, under the same row lock, so no wake decided
107
+ // after this commit starts the root from a sub-agent's delivery
108
+ const holdTree = conv?.kind === "chat" && personStopped(a.event);
105
109
  await t
106
110
  .updateTable("conversations")
107
111
  .set({
112
+ ...(holdTree && { tree_stopped_at: sql `now()` }),
108
113
  consumed_seq: a.floorToTrigger === false
109
114
  ? sql `GREATEST(consumed_seq, ${a.consumed ?? 0}::bigint)`
110
115
  : sql `GREATEST(consumed_seq, ${a.consumed ?? 0}::bigint, (SELECT r.trigger_seq FROM capybara.runs r WHERE r.id = ${a.runId}))`,
@@ -145,8 +150,8 @@ let RunFinish = class RunFinish {
145
150
  .where("cr.status", "in", ACTIVE_RUN_STATUSES)
146
151
  .limit(1)
147
152
  .executeTakeFirst();
148
- const stoppedWithChildren = kind === "subagent" &&
149
- personStopped(a.event) &&
153
+ // a root's too (agent map spec §6, changed in acceptance 2026-11-13)
154
+ const stoppedWithChildren = personStopped(a.event) &&
150
155
  (await t
151
156
  .selectFrom("conversations")
152
157
  .select("id")
@@ -186,8 +191,8 @@ RunFinish = __decorate([
186
191
  ], RunFinish);
187
192
  export { RunFinish };
188
193
  /**
189
- * A `run_aborted` a person caused (an account and no `detail`): a sub-agent run stopped that way
190
- * stops its descendants (`Waits.stopDescendantsOfStopped`, agent map spec §6).
194
+ * A `run_aborted` a person caused (an account and no `detail`): a run stopped that way stops its
195
+ * descendants (`Waits.stopDescendantsOfStopped`, agent map spec §6; a root's, those of its windows).
191
196
  */
192
197
  function personStopped(event) {
193
198
  if (event.type !== "run_aborted")
@@ -15,10 +15,7 @@ export declare const SUBAGENT_RUN_QUEUE_NAME = "cap-kernel-subagent-runs";
15
15
  export declare const runQueueNameFor: (kind: ConversationKind) => typeof RUN_QUEUE_NAME | typeof SUBAGENT_RUN_QUEUE_NAME;
16
16
  /** 未结束的运行状态(waiting:等人处理审批 / 输入请求,没有任务、不占会话锁与配额)。 */
17
17
  export declare const ACTIVE_RUN_STATUSES: readonly ["queued", "running", "waiting"];
18
- /** 运行的终态。 */
19
- export type TerminalStatus = "completed" | "failed" | "aborted";
20
- /** The terminal run statuses (here, not in `run-finish.ts`, which the sub-agent modules import). */
21
- export declare const TERMINAL_STATUSES: ReadonlySet<string>;
18
+ export { TERMINAL_STATUSES, type TerminalStatus } from "./run-status.js";
22
19
  /**
23
20
  * Locks a run's approval and input request rows (`FOR UPDATE`, in id order) in the caller's
24
21
  * transaction, after its conversation row and before its run row: the one lock order of every path
@@ -89,7 +86,27 @@ export declare class RunJobs {
89
86
  owe(data: RunJobData, jobId: string, kind: ConversationKind, orgId: string): Promise<boolean>;
90
87
  /** {@link oweEnsureRun} in the caller's transaction. */
91
88
  oweEnsureRun(conversationId: string, seq: number | string, orgId: string): Promise<boolean>;
89
+ /**
90
+ * Owes the delayed `bg-wake` job at the end of the conversation's wake window in the caller's
91
+ * transaction (the `ensureRun` that decided `later`): one job per conversation and window end
92
+ * ({@link bgWakeJobId}), so finishes inside one window share it; `retry` > 0 is a job that fired
93
+ * before its window ended. The relay adds it with the remaining delay (a row due later skips the
94
+ * fast path); `runAt` carries {@link WAKE_JOB_MARGIN_MS}, since the window is read on the
95
+ * database's clock and the delay runs on this process's and Redis's. Every `ensureRun` inside the
96
+ * window (each append that calls it) owes it again: a no-op while the row is pending, and once it
97
+ * was delivered a new row whose add BullMQ dedupes by the job id while the delayed job exists.
98
+ */
99
+ oweBgWake(conversationId: string, w: {
100
+ windowEnd: number;
101
+ delayMs: number;
102
+ }, retry: number, orgId: string): Promise<boolean>;
92
103
  }
104
+ /**
105
+ * Margin added to the delayed wake job: its window is read on the database's clock, the delay runs
106
+ * on this process's and Redis's, so the job does not usually fire just before the window ends (one
107
+ * that does reschedules itself under its next retry id, `bgWakeJobId`).
108
+ */
109
+ export declare const WAKE_JOB_MARGIN_MS = 1000;
93
110
  /**
94
111
  * What {@link RunResume.resumeIn} found: `queued`, the run was waiting on nothing else and now is
95
112
  * queued with its resume job owed; `still-running`, the run has not reached `waiting` yet (its
@@ -10,6 +10,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
10
10
  import { getQueueToken } from "@nestjs/bullmq";
11
11
  import { Inject, Injectable, Module } from "@nestjs/common";
12
12
  import { sql } from "kysely";
13
+ import { BG_WAKE_JOB, bgWakeJobId } from "../background/wake-rules.js";
13
14
  import { KernelDb } from "../infra/database/kernel-db.js";
14
15
  import { TransactionHost } from "../infra/database/transaction-host.js";
15
16
  import { Transactional } from "../infra/database/transactional.decorator.js";
@@ -29,12 +30,8 @@ export const SUBAGENT_RUN_QUEUE_NAME = "cap-kernel-subagent-runs";
29
30
  export const runQueueNameFor = (kind) => kind === "subagent" ? SUBAGENT_RUN_QUEUE_NAME : RUN_QUEUE_NAME;
30
31
  /** 未结束的运行状态(waiting:等人处理审批 / 输入请求,没有任务、不占会话锁与配额)。 */
31
32
  export const ACTIVE_RUN_STATUSES = ["queued", "running", "waiting"];
32
- /** The terminal run statuses (here, not in `run-finish.ts`, which the sub-agent modules import). */
33
- export const TERMINAL_STATUSES = new Set([
34
- "completed",
35
- "failed",
36
- "aborted",
37
- ]);
33
+ // The terminal run statuses (here, not in `run-finish.ts`, which the sub-agent modules import)
34
+ export { TERMINAL_STATUSES } from "./run-status.js";
38
35
  /**
39
36
  * Locks a run's approval and input request rows (`FOR UPDATE`, in id order) in the caller's
40
37
  * transaction, after its conversation row and before its run row: the one lock order of every path
@@ -144,6 +141,26 @@ let RunJobs = class RunJobs {
144
141
  oweEnsureRun(conversationId, seq, orgId) {
145
142
  return oweEnsureRun(this.outbox, conversationId, seq, orgId);
146
143
  }
144
+ /**
145
+ * Owes the delayed `bg-wake` job at the end of the conversation's wake window in the caller's
146
+ * transaction (the `ensureRun` that decided `later`): one job per conversation and window end
147
+ * ({@link bgWakeJobId}), so finishes inside one window share it; `retry` > 0 is a job that fired
148
+ * before its window ended. The relay adds it with the remaining delay (a row due later skips the
149
+ * fast path); `runAt` carries {@link WAKE_JOB_MARGIN_MS}, since the window is read on the
150
+ * database's clock and the delay runs on this process's and Redis's. Every `ensureRun` inside the
151
+ * window (each append that calls it) owes it again: a no-op while the row is pending, and once it
152
+ * was delivered a new row whose add BullMQ dedupes by the job id while the delayed job exists.
153
+ */
154
+ oweBgWake(conversationId, w, retry, orgId) {
155
+ return this.outbox.add({
156
+ queue: MAINTENANCE_QUEUE_NAME,
157
+ name: BG_WAKE_JOB,
158
+ jobId: bgWakeJobId(conversationId, w.windowEnd, retry),
159
+ data: { conversationId, ...(retry > 0 && { retry }) },
160
+ runAt: new Date(Date.now() + w.delayMs + WAKE_JOB_MARGIN_MS),
161
+ orgId,
162
+ });
163
+ }
147
164
  };
148
165
  RunJobs = __decorate([
149
166
  Injectable(),
@@ -153,6 +170,12 @@ RunJobs = __decorate([
153
170
  __param(3, Inject(OutboxService))
154
171
  ], RunJobs);
155
172
  export { RunJobs };
173
+ /**
174
+ * Margin added to the delayed wake job: its window is read on the database's clock, the delay runs
175
+ * on this process's and Redis's, so the job does not usually fire just before the window ends (one
176
+ * that does reschedules itself under its next retry id, `bgWakeJobId`).
177
+ */
178
+ export const WAKE_JOB_MARGIN_MS = 1000;
156
179
  /**
157
180
  * The resume of a waiting run (transactional outbox spec §6 "Resumes and timeouts", "Decisions
158
181
  * resume in their own transaction"), in the caller's transaction. A decision (approvals, questions,
@@ -13,7 +13,7 @@ import { RunFinish } from "./run-finish.js";
13
13
  import { RunJobs } from "./run-jobs.js";
14
14
  import { RunQueue } from "./run-queue.js";
15
15
  import { Waits } from "./waits.js";
16
- /** The check's `@LockedInterval` job name (its run lock key is the default `cron:run-orphan-check`). */
16
+ /** The check's `@LockedInterval` job name (its run lock key: `cron:run-orphan-check`). */
17
17
  export declare const RUN_ORPHAN_CHECK_JOB = "run-orphan-check";
18
18
  /** The check's period: a fixed constant, not a config field. */
19
19
  export declare const RUN_ORPHAN_CHECK_PERIOD_MS = 60000;
@@ -55,7 +55,10 @@ export interface OrphanCandidate {
55
55
  /**
56
56
  * One page of the active runs of chat and sub-agent conversations in `statuses` that entered their
57
57
  * state more than `minAgeMs` ago, by (state start, id), after `after` (an eval's hidden
58
- * conversation drives its runs itself).
58
+ * conversation drives its runs itself). A deleted org's runs are never candidates, running ones
59
+ * included: its deletion purged its owed jobs on purpose, so their absence is no lost job and
60
+ * nothing is re-added, and the org's data is invisible from then on, so a running orphan is left
61
+ * as it is rather than failed.
59
62
  */
60
63
  export declare function orphanCandidates(db: KernelSchemaDb, statuses: readonly ("queued" | "running")[], minAgeMs: number, limit: number, after?: Pick<OrphanCandidate, "since" | "runId">): Promise<OrphanCandidate[]>;
61
64
  /**
@@ -65,12 +68,12 @@ export declare function orphanCandidates(db: KernelSchemaDb, statuses: readonly
65
68
  */
66
69
  export declare function jobInOutbox(db: KernelSchemaDb, queue: string, jobId: string): Promise<boolean>;
67
70
  /**
68
- * The `queued` runs (in their state for over `minAgeMs`) whose job is neither in the outbox
69
- * (owed, or dead) nor in BullMQ (`jobExists`), and that are still queued after both looks: the run
70
- * queue lost their jobs. The outbox is read before BullMQ (a row is deleted only after its job was added) and the
71
- * run again after both (a job that finished took its run out of `queued` first), so a run in
72
- * flight is never reported. The `run-orphan-check` uses it; so does the dual-replica test harness,
73
- * which asserts at its teardown that the kernel left none (`minAgeMs` 0).
71
+ * The `queued` runs (in their state for over `minAgeMs`) whose job is neither in the outbox (owed,
72
+ * or dead) nor in BullMQ (`jobExists`), and that are still queued after both looks: the run queue
73
+ * lost their jobs. The outbox is read before BullMQ (a row is deleted only after its job was added)
74
+ * and the run again after both (a job that finished took its run out of `queued` first), so a run
75
+ * in flight is never reported. The `run-orphan-check` uses it; so does the dual-replica test
76
+ * harness, which asserts at its teardown that the kernel left none (`minAgeMs` 0).
74
77
  */
75
78
  export declare function queuedRunsWithoutJob(db: KernelSchemaDb, jobExists: (queue: string, jobId: string) => Promise<boolean>, opts: {
76
79
  minAgeMs: number;
@@ -85,9 +88,9 @@ export declare function queuedRunsWithoutJob(db: KernelSchemaDb, jobExists: (que
85
88
  * `run.orphaned` (its terminal transaction cancels its waits and owes what every run end owes),
86
89
  * then, after the commit, its checkpoints are dropped, its fork child stopped (`run.orphaned`), a
87
90
  * sub-agent's failure delivered to its parent at once and the conversation's next run started.
88
- * - `queued`, with no outbox row either (pending or dead): a canary. Every queued run has its job owed in its
89
- * own transaction, so this means Redis lost data. It is logged as an error, counted for the admin
90
- * overview ({@link QueueLostJobs}) and its job owed again so the run is not stuck.
91
+ * - `queued`, with no outbox row either (pending or dead): a canary. Every queued run has its job
92
+ * owed in its own transaction, so this means Redis lost data. It is logged as an error, counted
93
+ * for the admin overview ({@link QueueLostJobs}) and its job owed again so the run is not stuck.
91
94
  * Redis is read before each repair's transaction, which then re-reads the run under its
92
95
  * conversation row lock and acts only if it is still in the same state since the same time. Read
93
96
  * paths never repair runs.