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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) 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/agents/agents.module.js +3 -0
  5. package/dist/agents/agents.service.d.ts +6 -1
  6. package/dist/agents/agents.service.js +11 -2
  7. package/dist/catalog/catalog-import.module.js +0 -4
  8. package/dist/catalog/catalog-scope.service.d.ts +63 -26
  9. package/dist/catalog/catalog-scope.service.js +146 -71
  10. package/dist/catalog/catalog.module.js +4 -3
  11. package/dist/catalog/catalog.service.d.ts +11 -4
  12. package/dist/catalog/catalog.service.js +21 -5
  13. package/dist/catalog/cli.d.ts +4 -3
  14. package/dist/catalog/cli.js +8 -13
  15. package/dist/catalog/job-ids.d.ts +8 -20
  16. package/dist/catalog/job-ids.js +11 -16
  17. package/dist/catalog/marketplaces.service.d.ts +1 -1
  18. package/dist/catalog/org-plugin-uploads.d.ts +11 -4
  19. package/dist/catalog/org-plugin-uploads.js +21 -6
  20. package/dist/catalog/owned-items.d.ts +6 -13
  21. package/dist/catalog/owned-items.js +11 -29
  22. package/dist/catalog/owned-plugin-cleanup.d.ts +8 -17
  23. package/dist/catalog/owned-plugin-cleanup.js +12 -66
  24. package/dist/catalog/platform-components.d.ts +56 -34
  25. package/dist/catalog/platform-components.js +68 -78
  26. package/dist/catalog/plugin-import.d.ts +28 -25
  27. package/dist/catalog/plugin-import.js +90 -76
  28. package/dist/catalog/plugin-upgrade.d.ts +4 -4
  29. package/dist/catalog/plugin-upgrade.js +7 -19
  30. package/dist/catalog/scope-jobs.d.ts +59 -0
  31. package/dist/catalog/scope-jobs.js +65 -0
  32. package/dist/catalog/scope-sync-workers.d.ts +22 -0
  33. package/dist/catalog/scope-sync-workers.js +58 -2
  34. package/dist/catalog/scope-sync.d.ts +40 -65
  35. package/dist/catalog/scope-sync.js +114 -122
  36. package/dist/channels/inbound.js +2 -2
  37. package/dist/channels/threads.d.ts +4 -3
  38. package/dist/channels/threads.js +4 -3
  39. package/dist/conversations/conversations.controller.js +4 -2
  40. package/dist/conversations/conversations.service.d.ts +17 -5
  41. package/dist/conversations/conversations.service.js +17 -5
  42. package/dist/conversations/events.d.ts +21 -0
  43. package/dist/conversations/events.js +24 -1
  44. package/dist/conversations/source.d.ts +10 -0
  45. package/dist/conversations/source.js +17 -0
  46. package/dist/conversations/tree.d.ts +10 -0
  47. package/dist/conversations/tree.js +35 -0
  48. package/dist/db/kernel-schema.d.ts +9 -14
  49. package/dist/devices/devices.service.d.ts +11 -8
  50. package/dist/devices/devices.service.js +16 -23
  51. package/dist/evals/run-replay.d.ts +13 -2
  52. package/dist/evals/run-replay.js +28 -1
  53. package/dist/evals/snapshot.d.ts +7 -1
  54. package/dist/evals/snapshot.js +2 -1
  55. package/dist/extensions/hooks.d.ts +22 -3
  56. package/dist/extensions/hooks.js +15 -1
  57. package/dist/extensions/lifecycle-hooks.d.ts +99 -0
  58. package/dist/extensions/lifecycle-hooks.js +273 -0
  59. package/dist/extensions/org-config.d.ts +13 -1
  60. package/dist/extensions/org-config.js +19 -2
  61. package/dist/extensions/permission-view.d.ts +9 -5
  62. package/dist/extensions/permission-view.js +9 -4
  63. package/dist/extensions/point-defaults.d.ts +28 -1
  64. package/dist/extensions/point-defaults.js +44 -13
  65. package/dist/inbox/inbox.service.d.ts +6 -20
  66. package/dist/inbox/inbox.service.js +0 -3
  67. package/dist/inbox/org-notices.d.ts +1 -1
  68. package/dist/inbox/platform-inbox.service.d.ts +3 -32
  69. package/dist/inbox/platform-inbox.service.js +5 -54
  70. package/dist/infra/outbox/registry.d.ts +124 -0
  71. package/dist/infra/outbox/registry.js +126 -0
  72. package/dist/infra/queue/queue-names.d.ts +1 -1
  73. package/dist/infra/queue/queue-names.js +1 -0
  74. package/dist/orgs/directory-sync.js +1 -1
  75. package/dist/orgs/members.service.js +1 -1
  76. package/dist/orgs/offboarding.d.ts +50 -57
  77. package/dist/orgs/offboarding.js +117 -162
  78. package/dist/orgs/orgs.module.js +3 -0
  79. package/dist/orgs/orgs.service.d.ts +11 -5
  80. package/dist/orgs/orgs.service.js +15 -6
  81. package/dist/overrides.d.ts +3 -0
  82. package/dist/overrides.js +2 -0
  83. package/dist/platform/ops/inbox.ops.js +4 -20
  84. package/dist/platform/ops/platform.ops.d.ts +1 -0
  85. package/dist/platform/ops/platform.ops.js +6 -3
  86. package/dist/roles/worker-role.d.ts +10 -5
  87. package/dist/roles/worker-role.js +19 -9
  88. package/dist/roles/worker.module.js +2 -4
  89. package/dist/runtime/context/build.d.ts +3 -0
  90. package/dist/runtime/context/build.js +15 -0
  91. package/dist/runtime/context/plugin-context.d.ts +20 -0
  92. package/dist/runtime/context/plugin-context.js +30 -0
  93. package/dist/runtime/langgraph-loop.js +4 -0
  94. package/dist/runtime/loop.d.ts +8 -0
  95. package/dist/runtime/maintenance.d.ts +5 -3
  96. package/dist/runtime/maintenance.js +36 -11
  97. package/dist/runtime/model-call.d.ts +6 -1
  98. package/dist/runtime/model-call.js +1 -0
  99. package/dist/runtime/plugin-context.service.d.ts +69 -0
  100. package/dist/runtime/plugin-context.service.js +208 -0
  101. package/dist/runtime/run-control.d.ts +6 -1
  102. package/dist/runtime/run-control.js +13 -0
  103. package/dist/runtime/run-queue.d.ts +13 -0
  104. package/dist/runtime/run-queue.js +63 -0
  105. package/dist/runtime/run-worker.d.ts +22 -1
  106. package/dist/runtime/run-worker.js +107 -10
  107. package/dist/runtime/runtime.module.js +2 -0
  108. package/dist/runtime/subagents/dispatch.js +1 -0
  109. package/dist/runtime/subagents/skill-fork.js +3 -0
  110. package/dist/sandbox/jobs.d.ts +20 -10
  111. package/dist/sandbox/jobs.js +23 -17
  112. package/dist/schedules/fire.d.ts +25 -4
  113. package/dist/schedules/fire.js +87 -13
  114. package/dist/schedules/job-ids.d.ts +66 -17
  115. package/dist/schedules/job-ids.js +81 -13
  116. package/dist/schedules/run-tracker.js +2 -1
  117. package/dist/schedules/scan.job.d.ts +13 -32
  118. package/dist/schedules/scan.job.js +35 -144
  119. package/dist/schedules/scheduled-tasks.service.d.ts +6 -6
  120. package/dist/schedules/scheduled-tasks.service.js +17 -14
  121. package/dist/schedules/schedules.module.js +0 -4
  122. package/dist/skills/changes.controller.d.ts +1 -1
  123. package/dist/skills/changes.controller.js +1 -1
  124. package/dist/tools/approvals.service.d.ts +15 -4
  125. package/dist/tools/approvals.service.js +26 -10
  126. package/dist/tools/pipeline.d.ts +11 -2
  127. package/dist/tools/pipeline.js +5 -0
  128. package/dist/tools/questions.service.d.ts +15 -4
  129. package/dist/tools/questions.service.js +21 -19
  130. package/dist/tools/registry.d.ts +8 -1
  131. package/dist/tools/registry.js +9 -0
  132. package/package.json +3 -3
  133. package/src/migrations/0121_drop_platform_migration_runs.sql +12 -0
  134. package/src/migrations/0122_drop_scheduled_task_runs_rechecked_at.sql +5 -0
  135. package/src/migrations/0123_pgvector.sql +27 -0
  136. package/src/migrations/0124_conversation_source.sql +36 -0
  137. package/dist/catalog/platform-migration.d.ts +0 -115
  138. package/dist/catalog/platform-migration.js +0 -583
  139. package/dist/infra/queue/add-job-once.d.ts +0 -14
  140. package/dist/infra/queue/add-job-once.js +0 -16
@@ -0,0 +1,69 @@
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
+ /** Asks the providers and records what changed (see the class); notifies after the commit. */
58
+ record(input: PluginContextInput): Promise<void>;
59
+ /** One provider's call, bounded by the run's signal and the time limit. */
60
+ private ask;
61
+ /**
62
+ * One transaction under the conversation row lock (as `RunContextService`): compares each
63
+ * answer's hash with the plugin's latest `plugin_context` and appends the ones that differ (a
64
+ * plugin with no earlier event and no text writes nothing), plus an empty event for each plugin
65
+ * in `off` whose latest text is not empty. Idem key per run and plugin, so a retried run
66
+ * start finds its events or nothing changed. Returns the last seq written.
67
+ */
68
+ private append;
69
+ }
@@ -0,0 +1,208 @@
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
+ const sha256 = (text) => createHash("sha256").update(text).digest("hex");
22
+ /**
23
+ * Run start's plugin context (memory plugin spec §6.1): asks every `context@1` provider enabled in
24
+ * the org (valid config) for its text, in parallel and outside any transaction, each under the
25
+ * run's signal and {@link CONTEXT_LIMITS}.timeoutMs; then appends a `plugin_context` event for each
26
+ * plugin whose text differs from the latest one it recorded in the conversation. Fail open: a
27
+ * provider that throws, times out or returns too much is skipped for this run (its earlier text
28
+ * stays in effect) and logged. A provider loaded here that recorded text but no longer serves the
29
+ * org (switched off, its stored org config or its effective config invalid) gets an empty event,
30
+ * so its text stops following summaries; a plugin not loaded on this replica is left alone
31
+ * (another replica may still load it during a rollout, and blanking it here would make its text
32
+ * flap), and so is one whose status could not be read (a transient read error must not flip the
33
+ * model's context). Chat runs only: an
34
+ * eval replay reads the recorded events, and a sub-agent gets no plugin context (spec §5.4).
35
+ */
36
+ let PluginContextService = class PluginContextService {
37
+ db;
38
+ eventLog;
39
+ txHost;
40
+ points;
41
+ registry;
42
+ log;
43
+ constructor(db, eventLog, txHost, points, registry, log) {
44
+ this.db = db;
45
+ this.eventLog = eventLog;
46
+ this.txHost = txHost;
47
+ this.points = points;
48
+ this.registry = registry;
49
+ this.log = log;
50
+ }
51
+ /** Asks the providers and records what changed (see the class); notifies after the commit. */
52
+ async record(input) {
53
+ if (input.kind !== "chat")
54
+ return;
55
+ let found;
56
+ try {
57
+ found = await this.points.providersFor(input.orgId, "context");
58
+ }
59
+ catch (err) {
60
+ // fail open: without the org's providers nothing is asked and nothing is switched off
61
+ this.log.warn("context providers could not be listed; plugin context left as recorded", {
62
+ orgId: input.orgId,
63
+ conversationId: input.conversationId,
64
+ runId: input.runId,
65
+ err,
66
+ });
67
+ return;
68
+ }
69
+ const { providers } = found;
70
+ const tools = providers.length > 0 ? await input.toolKeysByPlugin() : new Map();
71
+ const answers = await Promise.all(providers.map(async (p) => {
72
+ const answer = await this.ask(p, input, tools.get(p.extension) ?? []);
73
+ if (!answer.ok) {
74
+ this.log.warn("context provider skipped for this run", {
75
+ orgId: input.orgId,
76
+ conversationId: input.conversationId,
77
+ runId: input.runId,
78
+ plugin: p.extension,
79
+ reason: answer.reason,
80
+ });
81
+ }
82
+ return [p.extension, answer];
83
+ }));
84
+ const texts = new Map();
85
+ for (const [plugin, answer] of answers)
86
+ if (answer.ok)
87
+ texts.set(plugin, answer.text);
88
+ const serving = new Set(providers.map((p) => p.extension));
89
+ const off = new Set(this.registry
90
+ .list("context")
91
+ .map((e) => e.plugin)
92
+ // a provider whose status could not be read (a transient error) keeps its recorded text
93
+ .filter((plugin) => !serving.has(plugin) && !found.failed.has(plugin)));
94
+ // nothing answered and nothing to switch off: no transaction, no row lock
95
+ if (texts.size === 0 && off.size === 0)
96
+ return;
97
+ const seq = await this.append(input, texts, off);
98
+ if (seq !== null)
99
+ await this.eventLog.notify(input.conversationId, seq);
100
+ }
101
+ /** One provider's call, bounded by the run's signal and the time limit. */
102
+ async ask(p, input, tools) {
103
+ const signal = AbortSignal.any([input.signal, AbortSignal.timeout(CONTEXT_LIMITS.timeoutMs)]);
104
+ let result;
105
+ try {
106
+ result = await untilAborted(Promise.resolve().then(() => p.impl.provide({
107
+ orgId: input.orgId,
108
+ accountId: input.triggerAccountId,
109
+ agentId: input.agentId,
110
+ agentName: input.agentName,
111
+ conversationId: input.conversationId,
112
+ runId: input.runId,
113
+ source: input.source,
114
+ role: input.role,
115
+ ownerAccountId: input.ownerAccountId,
116
+ ownerName: input.ownerName,
117
+ tools,
118
+ config: p.config,
119
+ signal,
120
+ })), signal);
121
+ }
122
+ catch (err) {
123
+ if (signal.aborted) {
124
+ return { ok: false, reason: input.signal.aborted ? "run aborted" : "timed out" };
125
+ }
126
+ return { ok: false, reason: `threw: ${err instanceof Error ? err.message : String(err)}` };
127
+ }
128
+ if (result === null)
129
+ return { ok: true, text: "" };
130
+ const text = result?.text;
131
+ if (typeof text !== "string")
132
+ return { ok: false, reason: "returned no text" };
133
+ const bytes = Buffer.byteLength(text, "utf8");
134
+ if (bytes > CONTEXT_LIMITS.maxBytes) {
135
+ return { ok: false, reason: `text of ${bytes} bytes is over ${CONTEXT_LIMITS.maxBytes}` };
136
+ }
137
+ return { ok: true, text };
138
+ }
139
+ /**
140
+ * One transaction under the conversation row lock (as `RunContextService`): compares each
141
+ * answer's hash with the plugin's latest `plugin_context` and appends the ones that differ (a
142
+ * plugin with no earlier event and no text writes nothing), plus an empty event for each plugin
143
+ * in `off` whose latest text is not empty. Idem key per run and plugin, so a retried run
144
+ * start finds its events or nothing changed. Returns the last seq written.
145
+ */
146
+ async append(input, texts, off) {
147
+ const t = this.db.db;
148
+ await t
149
+ .selectFrom("conversations")
150
+ .select("id")
151
+ .where("id", "=", input.conversationId)
152
+ .forUpdate()
153
+ .executeTakeFirst();
154
+ const rows = await t
155
+ .selectFrom("conversation_events")
156
+ .select(["seq", "payload"])
157
+ .where("conversation_id", "=", input.conversationId)
158
+ .where("type", "=", "plugin_context")
159
+ .orderBy("seq", "asc")
160
+ .execute();
161
+ const latest = latestPluginContexts(rows.map((r) => ({ type: "plugin_context", seq: Number(r.seq), payload: r.payload })), Number.POSITIVE_INFINITY);
162
+ const next = new Map(texts);
163
+ for (const [plugin, prev] of latest) {
164
+ if (off.has(plugin) && prev.text !== "")
165
+ next.set(plugin, "");
166
+ }
167
+ const changed = [...next]
168
+ .filter(([plugin, text]) => {
169
+ const prev = latest.get(plugin);
170
+ return prev === undefined ? text !== "" : prev.hash !== sha256(text);
171
+ })
172
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
173
+ if (changed.length === 0)
174
+ return null;
175
+ const written = await this.eventLog.appendIn(input.conversationId, changed.map(([plugin, text]) => ({
176
+ type: "plugin_context",
177
+ runId: input.runId,
178
+ idemKey: pluginContextIdemKey(input.runId, plugin),
179
+ payload: { plugin, text, hash: sha256(text) },
180
+ })));
181
+ return Math.max(...written.map((e) => e.seq));
182
+ }
183
+ };
184
+ __decorate([
185
+ Transactional()
186
+ ], PluginContextService.prototype, "append", null);
187
+ PluginContextService = __decorate([
188
+ Injectable(),
189
+ __param(0, Inject(KernelDb)),
190
+ __param(1, Inject(EventLog)),
191
+ __param(2, Inject(TransactionHost)),
192
+ __param(3, Inject(ExtensionPointDefaults)),
193
+ __param(4, Inject(ExtensionRegistry)),
194
+ __param(5, Inject(KERNEL_LOGGER))
195
+ ], PluginContextService);
196
+ export { PluginContextService };
197
+ /** `p`, or a rejection once `signal` aborts (a provider that ignores its signal never holds a run). */
198
+ function untilAborted(p, signal) {
199
+ return new Promise((resolve, reject) => {
200
+ const onAbort = () => reject(signal.reason);
201
+ if (signal.aborted) {
202
+ onAbort();
203
+ return;
204
+ }
205
+ signal.addEventListener("abort", onAbort, { once: true });
206
+ p.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
207
+ });
208
+ }
@@ -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
@@ -148,6 +148,19 @@ export declare class RunQueue {
148
148
  */
149
149
  abortIfOrphaned(conversationId: string, runId: string, byAccountId: string | null, detail?: RunAbortDetail): Promise<boolean>;
150
150
  private abortOrphanIn;
151
+ /**
152
+ * Ends a `queued` run in PG before any worker started it (an offboarded member's run, ruling
153
+ * R27): with the conversation lock free, under the conversation row lock, a run still `queued`
154
+ * is finished `aborted` with `run_aborted` (`consumed_seq` at least to its trigger), so a job
155
+ * that runs later finds it finished and starts nothing. True when it did; false when a worker
156
+ * holds the conversation or is in the middle of the run's first start (`run_started` written,
157
+ * `started_at` not yet set; it hears the stop), or the run is no longer queued. A run resumed
158
+ * from `waiting` to `queued` is ended like a new one. The caller writes the stop marker first, so a worker that takes the
159
+ * conversation meanwhile still stops; a worker that read the run before this commit re-checks it
160
+ * under the row locks before its `run_started` and never moves it back to `running`.
161
+ */
162
+ abortQueued(conversationId: string, runId: string, byAccountId: string | null): Promise<boolean>;
163
+ private abortQueuedIn;
151
164
  /** 会话当前未结束的运行(调用方已持有会话行锁)。 */
152
165
  private activeRun;
153
166
  /**
@@ -567,6 +567,66 @@ let RunQueue = class RunQueue {
567
567
  },
568
568
  });
569
569
  }
570
+ /**
571
+ * Ends a `queued` run in PG before any worker started it (an offboarded member's run, ruling
572
+ * R27): with the conversation lock free, under the conversation row lock, a run still `queued`
573
+ * is finished `aborted` with `run_aborted` (`consumed_seq` at least to its trigger), so a job
574
+ * that runs later finds it finished and starts nothing. True when it did; false when a worker
575
+ * holds the conversation or is in the middle of the run's first start (`run_started` written,
576
+ * `started_at` not yet set; it hears the stop), or the run is no longer queued. A run resumed
577
+ * from `waiting` to `queued` is ended like a new one. The caller writes the stop marker first, so a worker that takes the
578
+ * conversation meanwhile still stops; a worker that read the run before this commit re-checks it
579
+ * under the row locks before its `run_started` and never moves it back to `running`.
580
+ */
581
+ async abortQueued(conversationId, runId, byAccountId) {
582
+ if ((await this.lock.holder(conversationId)) !== null)
583
+ return false;
584
+ const toSeq = await this.abortQueuedIn(conversationId, runId, byAccountId);
585
+ if (toSeq === null)
586
+ return false;
587
+ await notifyFinished(this.eventLog, conversationId, toSeq, "aborted");
588
+ await this.dropCheckpoints(runId);
589
+ return true;
590
+ }
591
+ async abortQueuedIn(conversationId, runId, byAccountId) {
592
+ const t = this.db;
593
+ const conv = await t
594
+ .selectFrom("conversations")
595
+ .select("id")
596
+ .where("id", "=", conversationId)
597
+ .forUpdate()
598
+ .executeTakeFirst();
599
+ const active = await this.activeRun(t, conversationId);
600
+ if (!conv || !active || active.id !== runId || active.status !== "queued")
601
+ return null;
602
+ // a worker is in the middle of the run's FIRST start (its `run_started` written, `started_at`
603
+ // not set yet; it holds the conversation since): the stop marker ends it. A run resumed from
604
+ // `waiting` back to `queued` has both from its first start and is ended here like a new one:
605
+ // its worker skips `run_started` (it exists) and its conditional `running` update finds the run
606
+ // finished
607
+ const first = await t
608
+ .selectFrom("runs")
609
+ .select("started_at")
610
+ .where("id", "=", runId)
611
+ .executeTakeFirst();
612
+ if (first?.started_at === null) {
613
+ const started = await t
614
+ .selectFrom("conversation_events")
615
+ .select("seq")
616
+ .where("conversation_id", "=", conversationId)
617
+ .where("idem_key", "=", `run_started:${runId}`)
618
+ .executeTakeFirst();
619
+ if (started)
620
+ return null;
621
+ }
622
+ return this.finisher.finishIn({
623
+ conversationId,
624
+ runId,
625
+ status: "aborted",
626
+ consumed: null,
627
+ event: { type: "run_aborted", payload: { byAccountId, reason: "user" } },
628
+ });
629
+ }
570
630
  /** 会话当前未结束的运行(调用方已持有会话行锁)。 */
571
631
  activeRun(t, conversationId) {
572
632
  return t
@@ -604,6 +664,9 @@ __decorate([
604
664
  __decorate([
605
665
  Transactional({ propagation: "requires_new" })
606
666
  ], RunQueue.prototype, "abortOrphanIn", null);
667
+ __decorate([
668
+ Transactional({ propagation: "requires_new" })
669
+ ], RunQueue.prototype, "abortQueuedIn", null);
607
670
  RunQueue = __decorate([
608
671
  Injectable(),
609
672
  __param(0, Inject(KernelDb)),
@@ -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();
@@ -159,6 +175,11 @@ export declare class RunWorker {
159
175
  process(job: Job<RunJobData>, token: string | undefined): Promise<ProcessResult>;
160
176
  private execute;
161
177
  private completed;
178
+ /**
179
+ * The run's extension tool keys by plugin, resolved as a chat run's first step lists them
180
+ * (disabled tools, org switches, the conversation's source): a provider's `tools`.
181
+ */
182
+ private pluginToolKeys;
162
183
  /**
163
184
  * When the conversation was switched to a model that no longer resolves (provider disabled,
164
185
  * model removed), writes `model_switched { model: null, effort: null, reason: "unavailable" }`