@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
@@ -177,9 +177,10 @@ let ConversationsService = class ConversationsService {
177
177
  * Creates an empty conversation bound to its device (`pick`, see
178
178
  * {@link ConversationDevices.forNew}); an agent the account cannot use →
179
179
  * `agent.not_found`, a device it may not use → `device.not_found` /
180
- * `device.unavailable`, and nothing is created.
180
+ * `device.unavailable`, and nothing is created. `opts.source` records where the conversation
181
+ * comes from.
181
182
  */
182
- async create(orgId, accountId, agentId, pick = {}) {
183
+ async create(orgId, accountId, agentId, pick = {}, opts) {
183
184
  await this.agents.get(orgId, agentId, accountId);
184
185
  const deviceId = await this.devices.forNew(orgId, accountId, pick);
185
186
  const row = await this.db
@@ -190,6 +191,7 @@ let ConversationsService = class ConversationsService {
190
191
  agent_id: agentId,
191
192
  account_id: accountId,
192
193
  device_id: deviceId,
194
+ source: opts.source,
193
195
  })
194
196
  .returning(COLUMNS)
195
197
  .executeTakeFirstOrThrow();
@@ -203,10 +205,10 @@ let ConversationsService = class ConversationsService {
203
205
  * `agent.not_found` and nothing is written. Queues a run afterwards, like `postUserMessage`.
204
206
  * `events` are every event the create wrote, in seq order from seq 1 (a `model_switched` before
205
207
  * the message when the send switched), so a client can seed the new conversation's log whole.
206
- * `internal` (kernel callers only): a scheduled task's marker, which also sets
207
- * `conversations.scheduled_task_id`, and its zone override.
208
+ * `internal`: the conversation's source and, for kernel callers only, a scheduled task's marker,
209
+ * which also sets `conversations.scheduled_task_id`, and its zone override.
208
210
  */
209
- async createWithMessage(orgId, accountId, agentId, input, pick = {}, internal = {}) {
211
+ async createWithMessage(orgId, accountId, agentId, input, pick = {}, internal) {
210
212
  const agent = await this.agents.get(orgId, agentId, accountId);
211
213
  const prior = await this.findCreated(orgId, accountId, agentId, input.clientMessageId);
212
214
  if (prior)
@@ -242,6 +244,7 @@ let ConversationsService = class ConversationsService {
242
244
  device_id: conv.deviceId,
243
245
  create_request_id: input.clientMessageId,
244
246
  scheduled_task_id: internal.schedule?.taskId ?? null,
247
+ source: internal.source,
245
248
  })
246
249
  .onConflict((oc) => oc
247
250
  .columns(["org_id", "account_id", "create_request_id"])
@@ -1014,6 +1017,15 @@ let ConversationsService = class ConversationsService {
1014
1017
  await runs.ensureRunAfterCommit(conversationId);
1015
1018
  return run.id;
1016
1019
  }
1020
+ // a queued run no worker has started yet is ended in PG (ruling R27), never left to the stop
1021
+ // marker's TTL: the marker first, so a worker that takes it meanwhile stops too
1022
+ if (run.status === "queued") {
1023
+ await runs.requestAbort(run.id, accountId);
1024
+ if (await runs.abortQueued(conversationId, run.id, accountId)) {
1025
+ await runs.ensureRunAfterCommit(conversationId);
1026
+ }
1027
+ return run.id;
1028
+ }
1017
1029
  // 孤儿运行没有 worker 在听中止广播:直接写成 aborted,之后再看有没有等待中的消息要起新一轮。
1018
1030
  if (await runs.abortIfOrphaned(conversationId, run.id, accountId)) {
1019
1031
  await runs.ensureRunAfterCommit(conversationId);
@@ -200,6 +200,20 @@ export declare const RunContextSchema: z.ZodObject<{
200
200
  person: z.ZodString;
201
201
  }, z.core.$strip>>;
202
202
  }, z.core.$strip>;
203
+ /**
204
+ * The most UTF-8 bytes a `plugin_context` event's text may hold (memory spec §6.1): the providers'
205
+ * limit, so the log takes every text a provider may return.
206
+ */
207
+ export declare const PLUGIN_CONTEXT_MAX_BYTES: number;
208
+ /**
209
+ * `plugin_context` (memory spec §6.1): the text a `context@1` provider added at a run start. Empty
210
+ * text means the provider has nothing (any more); `hash` is the sha256 (hex) of `text`.
211
+ */
212
+ export declare const PluginContextSchema: z.ZodObject<{
213
+ plugin: z.ZodString;
214
+ text: z.ZodString;
215
+ hash: z.ZodString;
216
+ }, z.core.$strip>;
203
217
  /**
204
218
  * 会话事件(`type` + `payload`)的判别联合。事件日志只追加,写入前用它校验 payload;
205
219
  * 读出时按写入时的形状原样返回。
@@ -547,6 +561,13 @@ export declare const ConversationEventSchema: z.ZodDiscriminatedUnion<[z.ZodObje
547
561
  person: z.ZodString;
548
562
  }, z.core.$strip>>;
549
563
  }, z.core.$strip>;
564
+ }, z.core.$strip>, z.ZodObject<{
565
+ type: z.ZodLiteral<"plugin_context">;
566
+ payload: z.ZodObject<{
567
+ plugin: z.ZodString;
568
+ text: z.ZodString;
569
+ hash: z.ZodString;
570
+ }, z.core.$strip>;
550
571
  }, z.core.$strip>, z.ZodObject<{
551
572
  type: z.ZodLiteral<"tools_changed">;
552
573
  payload: z.ZodObject<{
@@ -1,4 +1,4 @@
1
- import { DOCUMENT_MEDIA_TYPES, hasControlChars, IMAGE_MEDIA_TYPES, isAttachmentId, isSkillName, MEDIA_NAME_MAX_CHARS, } from "@capybara-hq/plugin-sdk";
1
+ import { CONTEXT_LIMITS, DOCUMENT_MEDIA_TYPES, hasControlChars, IMAGE_MEDIA_TYPES, isAttachmentId, isSkillName, MEDIA_NAME_MAX_CHARS, } from "@capybara-hq/plugin-sdk";
2
2
  import { z } from "zod";
3
3
  import { AgentDefinitionSchema, DisabledSkillRef, DisabledToolRef, ModelRef, } from "../agents/definition.js";
4
4
  import { ORG_ROLES } from "../orgs/roles.js";
@@ -228,6 +228,22 @@ export const RunContextSchema = z.object({
228
228
  .optional(),
229
229
  });
230
230
  const ev = (type, payload) => z.object({ type: z.literal(type), payload });
231
+ /**
232
+ * The most UTF-8 bytes a `plugin_context` event's text may hold (memory spec §6.1): the providers'
233
+ * limit, so the log takes every text a provider may return.
234
+ */
235
+ export const PLUGIN_CONTEXT_MAX_BYTES = CONTEXT_LIMITS.maxBytes;
236
+ /**
237
+ * `plugin_context` (memory spec §6.1): the text a `context@1` provider added at a run start. Empty
238
+ * text means the provider has nothing (any more); `hash` is the sha256 (hex) of `text`.
239
+ */
240
+ export const PluginContextSchema = z.object({
241
+ plugin: z.string().min(1).max(64),
242
+ text: z.string().refine((t) => Buffer.byteLength(t, "utf8") <= PLUGIN_CONTEXT_MAX_BYTES, {
243
+ message: `at most ${PLUGIN_CONTEXT_MAX_BYTES} UTF-8 bytes`,
244
+ }),
245
+ hash: z.string().regex(/^[0-9a-f]{64}$/),
246
+ });
231
247
  /**
232
248
  * An agent definition frozen into a log (`subagent_started`): the current schema, with the
233
249
  * selected and pinned skills of older logs still accepted (skills default-on spec §4: history
@@ -485,6 +501,13 @@ export const ConversationEventSchema = z.discriminatedUnion("type", [
485
501
  * system message (`runContextText`); a summary covering it restores the current values.
486
502
  */
487
503
  ev("run_context", RunContextSchema),
504
+ /**
505
+ * Text a `context@1` provider added at a run start (memory spec §6.1), written only when its
506
+ * hash differs from the plugin's previous one in the conversation. **Model-visible**: rendered
507
+ * at its position as a system message (`pluginContextText`, nothing for empty text); a summary
508
+ * covering it is followed by the latest non-empty one per plugin again.
509
+ */
510
+ ev("plugin_context", PluginContextSchema),
488
511
  /**
489
512
  * The step's resolved tool set differs from the one the conversation's previous model request
490
513
  * offered (`toolSetChange`): tool names as the model sees them, sorted. Written before that
@@ -0,0 +1,10 @@
1
+ import type { ConversationSource } from "@capybara-hq/plugin-sdk";
2
+ import type { KernelSchemaDb } from "../db/kernel-schema.js";
3
+ /**
4
+ * Where a conversation comes from (memory plugin spec §6.2), stored on its row at creation
5
+ * (`conversations.source`, migration 0124) and never changed. A missing conversation throws
6
+ * rather than guessing a source (a guess could offer a group chat what only a DM may see).
7
+ */
8
+ export declare function conversationSourceOf(db: Pick<KernelSchemaDb, "selectFrom">, conversationId: string): Promise<ConversationSource>;
9
+ /** The source of a conversation created for a channel chat of `chatType`. */
10
+ export declare const channelSource: (chatType: "dm" | "group") => ConversationSource;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Where a conversation comes from (memory plugin spec §6.2), stored on its row at creation
3
+ * (`conversations.source`, migration 0124) and never changed. A missing conversation throws
4
+ * rather than guessing a source (a guess could offer a group chat what only a DM may see).
5
+ */
6
+ export async function conversationSourceOf(db, conversationId) {
7
+ const row = await db
8
+ .selectFrom("conversations")
9
+ .select("source")
10
+ .where("id", "=", conversationId)
11
+ .executeTakeFirst();
12
+ if (row === undefined)
13
+ throw new Error(`conversation ${conversationId} not found`);
14
+ return row.source;
15
+ }
16
+ /** The source of a conversation created for a channel chat of `chatType`. */
17
+ export const channelSource = (chatType) => chatType === "dm" ? "channel_dm" : "channel_group";
@@ -15,3 +15,13 @@ export declare function conversationTree(db: KernelSchemaDb, conversationId: str
15
15
  * `conversations.parent_conversation_id`. Empty for a root or an unknown id.
16
16
  */
17
17
  export declare function conversationAncestors(db: KernelSchemaDb, conversationId: string): Promise<string[]>;
18
+ /**
19
+ * Locks the conversations `ids` and every ancestor of theirs (`FOR NO KEY UPDATE`, in the caller's
20
+ * transaction) tree by tree, root first: the trees in root id order, within a tree by level below
21
+ * the root, then id. A reply locks its root conversation and only then the sub-agents' below it
22
+ * (`ConversationsService.skipWaitsIn`), and a dispatch locks a parent before its child; a locker
23
+ * of conversations in plain id order could take a child before its root and wait for the root
24
+ * while such a path holds the root and waits for the child (40P01). Returns the ids locked (the
25
+ * requested ones and their ancestors); an unknown id is skipped.
26
+ */
27
+ export declare function lockConversationTrees(db: KernelSchemaDb, ids: readonly string[]): Promise<string[]>;
@@ -40,3 +40,38 @@ export async function conversationAncestors(db, conversationId) {
40
40
  SELECT id FROM up WHERE level > 0 ORDER BY level`.execute(db);
41
41
  return r.rows.map((row) => row.id);
42
42
  }
43
+ /**
44
+ * Locks the conversations `ids` and every ancestor of theirs (`FOR NO KEY UPDATE`, in the caller's
45
+ * transaction) tree by tree, root first: the trees in root id order, within a tree by level below
46
+ * the root, then id. A reply locks its root conversation and only then the sub-agents' below it
47
+ * (`ConversationsService.skipWaitsIn`), and a dispatch locks a parent before its child; a locker
48
+ * of conversations in plain id order could take a child before its root and wait for the root
49
+ * while such a path holds the root and waits for the child (40P01). Returns the ids locked (the
50
+ * requested ones and their ancestors); an unknown id is skipped.
51
+ */
52
+ export async function lockConversationTrees(db, ids) {
53
+ const wanted = [...new Set(ids)];
54
+ if (wanted.length === 0)
55
+ return [];
56
+ const r = await sql `
57
+ WITH RECURSIVE up(start, id, parent, level) AS (
58
+ SELECT c.id, c.id, c.parent_conversation_id, 0 FROM capybara.conversations c
59
+ WHERE c.id = ANY(${wanted}::text[])
60
+ UNION ALL
61
+ SELECT up.start, c.id, c.parent_conversation_id, up.level + 1
62
+ FROM capybara.conversations c JOIN up ON c.id = up.parent
63
+ WHERE up.level < ${TREE_MAX_LEVELS}
64
+ ),
65
+ tops AS (
66
+ SELECT DISTINCT ON (start) start, id AS root, level AS height
67
+ FROM up ORDER BY start, level DESC
68
+ ),
69
+ nodes AS (
70
+ SELECT DISTINCT up.id, tops.root, tops.height - up.level AS depth
71
+ FROM up JOIN tops ON tops.start = up.start
72
+ )
73
+ SELECT c.id FROM capybara.conversations c JOIN nodes n ON n.id = c.id
74
+ ORDER BY n.root, n.depth, n.id
75
+ FOR NO KEY UPDATE OF c`.execute(db);
76
+ return r.rows.map((row) => row.id);
77
+ }
@@ -1,4 +1,4 @@
1
- import type { FileSystemHandle, ParsedTheme, SandboxTier, SkillInvocation } from "@capybara-hq/plugin-sdk";
1
+ import type { ConversationSource, FileSystemHandle, ParsedTheme, SandboxTier, SkillInvocation } from "@capybara-hq/plugin-sdk";
2
2
  import type { ColumnType, Generated, Kysely } from "kysely";
3
3
  import type { ModelRef } from "../agents/definition.js";
4
4
  import type { ScheduleChat, ScheduleMarker, ScheduleMode, SchedulePauseReason, ScheduleStatus, ScheduleTrigger, TaskRunSkipReason, TaskRunStatus } from "../schedules/types.js";
@@ -263,6 +263,11 @@ export interface ConversationsTable {
263
263
  device_id: ColumnType<string | null, string | null | undefined, string | null>;
264
264
  /** 0113: the scheduled task that created it (`new` mode); null otherwise. */
265
265
  scheduled_task_id: ColumnType<string | null, string | null | undefined, string | null>;
266
+ /**
267
+ * 0124: where the conversation comes from, set at creation and never changed. Required on insert
268
+ * (the column's `web` default is not relied on: a forgotten source must not read as web).
269
+ */
270
+ source: ColumnType<ConversationSource, ConversationSource, never>;
266
271
  }
267
272
  /** 0060: an org's order of file-preview@1 previewers (UI refresh B Task 11). */
268
273
  export interface FilePreviewersTable {
@@ -1224,8 +1229,6 @@ export interface ScheduledTaskRunsTable {
1224
1229
  skip_reason: ColumnType<TaskRunSkipReason | null, TaskRunSkipReason | null | undefined, TaskRunSkipReason | null>;
1225
1230
  error: NullableText;
1226
1231
  missing_grants: ColumnType<string[], string | undefined, string>;
1227
- /** When the scanner's repair last found this started run still in flight. */
1228
- rechecked_at: NullableTs;
1229
1232
  }
1230
1233
  /** 0113: a scheduled task's remembered grants (same shape as `conversation_tool_grants`). */
1231
1234
  export interface ScheduledTaskGrantsTable {
@@ -1333,7 +1336,6 @@ export interface KernelDatabase {
1333
1336
  plugin_install_components: PluginInstallComponentsTable;
1334
1337
  extension_catalog_sources: ExtensionCatalogSourcesTable;
1335
1338
  bootstrap_runs: BootstrapRunsTable;
1336
- platform_migration_runs: PlatformMigrationRunsTable;
1337
1339
  org_notices: OrgNoticesTable;
1338
1340
  platform_notices: PlatformNoticesTable;
1339
1341
  plugin_scope_sync_failures: PluginScopeSyncFailuresTable;
@@ -1397,21 +1399,14 @@ export interface ExtensionClientReportsTable {
1397
1399
  last_at: Ts;
1398
1400
  last_message: string;
1399
1401
  }
1400
- /** 0082: one run of the platform components migration job (`catalog/platform-migration.ts`). */
1401
- export interface PlatformMigrationRunsTable {
1402
- id: string;
1403
- started_at: Ts;
1404
- finished_at: TsNullable;
1405
- stats: Json<Record<string, unknown>>;
1406
- }
1407
- /** 0082: the kinds of {@link OrgNoticesTable}. */
1408
- export type OrgNoticeKind = "skill_renamed" | "item_shared" | "shared_item_updated" | "upload_confirmed" | "upload_rejected";
1402
+ /** 0084 (0121 dropped `skill_renamed`): the kinds of {@link OrgNoticesTable}. */
1403
+ export type OrgNoticeKind = "item_shared" | "shared_item_updated" | "upload_confirmed" | "upload_rejected";
1409
1404
  /** 0082: a notice for an org's admins (an inbox item), until one of them dismisses it. */
1410
1405
  export interface OrgNoticesTable {
1411
1406
  id: string;
1412
1407
  org_id: string;
1413
1408
  kind: OrgNoticeKind;
1414
- /** `skill_renamed`: `{ skillId, from, to, platformSkillId, plugin }`. */
1409
+ /** The kind's shape (`OrgNoticeDetails` in `inbox/org-notices.ts`). */
1415
1410
  detail: Json<Record<string, unknown>>;
1416
1411
  created_at: Ts;
1417
1412
  dismissed_at: TsNullable;
@@ -4,9 +4,9 @@ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
4
4
  import { KernelDb } from "../infra/database/kernel-db.js";
5
5
  import { TransactionHost } from "../infra/database/transaction-host.js";
6
6
  import { LockService } from "../infra/lock/lock.service.js";
7
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
7
8
  import { DevicePolicyService } from "../local-devices/device-policy.service.js";
8
9
  import { MachinesService } from "../local-devices/machines.js";
9
- import type { KernelLogger } from "../log.js";
10
10
  import type { DeviceSandboxLevel } from "../runtime/context/device-paths.js";
11
11
  import { SandboxJobs } from "../sandbox/jobs.js";
12
12
  import { SandboxProviders } from "../sandbox/providers.js";
@@ -181,8 +181,8 @@ export interface DeviceList {
181
181
  * Personal devices (spec 2026-11-07-environments-design §4.2 items 7, 12, 13; §6; §9):
182
182
  * cloud ones on the deployment's sandbox provider and `device` ones on a connected machine; a
183
183
  * person's devices in an org, visible and usable only by their owner (anyone else gets
184
- * `device.not_found`). Deleting one soft-deletes it and destroys its instance and files in a
185
- * `sandbox-destroy-for` job after the commit.
184
+ * `device.not_found`). Deleting one soft-deletes it and owes the destruction of its instance and
185
+ * files (a `sandbox-destroy-for` job) in the same transaction.
186
186
  */
187
187
  export declare class DevicesService {
188
188
  private readonly db;
@@ -191,14 +191,17 @@ export declare class DevicesService {
191
191
  private readonly providers;
192
192
  private readonly sandbox;
193
193
  private readonly sandboxJobs;
194
+ /** Owes a deleted device's destruction in the delete's transaction. */
195
+ private readonly outbox;
194
196
  private readonly access;
195
197
  private readonly displayNames;
196
- private readonly log;
197
198
  /** Reads the device relay's lease (`online`). */
198
199
  private readonly locks;
199
200
  private readonly machines;
200
201
  private readonly policies;
201
- constructor(db: KernelDb, txHost: TransactionHost, config: Pick<KernelConfigService, "get">, providers: Pick<SandboxProviders, "cloud" | "forName">, sandbox: Pick<SandboxService, "deviceLimits" | "limitsState">, sandboxJobs: Pick<SandboxJobs, "destroy" | "resetFiles">, access: Pick<DeviceAccess, "usable">, displayNames: Pick<ExtensionDisplayNames, "many">, log: Pick<KernelLogger, "warn">,
202
+ constructor(db: KernelDb, txHost: TransactionHost, config: Pick<KernelConfigService, "get">, providers: Pick<SandboxProviders, "cloud" | "forName">, sandbox: Pick<SandboxService, "deviceLimits" | "limitsState">, sandboxJobs: Pick<SandboxJobs, "resetFiles">,
203
+ /** Owes a deleted device's destruction in the delete's transaction. */
204
+ outbox: Pick<OutboxService, "add">, access: Pick<DeviceAccess, "usable">, displayNames: Pick<ExtensionDisplayNames, "many">,
202
205
  /** Reads the device relay's lease (`online`). */
203
206
  locks: Pick<LockService, "holderOf">, machines: Pick<MachinesService, "revoke" | "revokeActive">, policies: Pick<DevicePolicyService, "apply" | "summary">);
204
207
  /**
@@ -284,9 +287,9 @@ export declare class DevicesService {
284
287
  /** Renames the device locked by {@link lockOwn} (`before`); see {@link rename}. */
285
288
  private renameLocked;
286
289
  /**
287
- * Deletes one of the person's live devices: soft delete, its IM default row cleared, and
288
- * after the commit a `sandbox-destroy-for` job (the worker waits for running commands, then
289
- * destroys the instance and the files). Conversations bound to it keep pointing at it; their
290
+ * Deletes one of the person's live devices: soft delete, its IM default row cleared, and a
291
+ * `sandbox-destroy-for` job owed in the same transaction (`device-destroy-<deviceId>`; the
292
+ * worker waits for running commands, then destroys the instance and the files). Conversations bound to it keep pointing at it; their
290
293
  * sandbox tools then fail with `device.not_found`. A local device's machine is
291
294
  * revoked in the same transaction; after the commit it is told `stop { reason: "deleted" }`
292
295
  * (while connected) and its connection is closed.
@@ -15,18 +15,17 @@ import { ApiError } from "../errors.js";
15
15
  import { ExtensionDisplayNames } from "../extensions/display-name.js";
16
16
  import { audit } from "../identity/audit.js";
17
17
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
18
- import { afterCommit } from "../infra/database/after-commit.js";
19
18
  import { KernelDb } from "../infra/database/kernel-db.js";
20
19
  import { TransactionHost } from "../infra/database/transaction-host.js";
21
20
  import { Transactional } from "../infra/database/transactional.decorator.js";
22
21
  import { LockService } from "../infra/lock/lock.service.js";
23
- import { KERNEL_LOGGER } from "../infra/tokens.js";
22
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
24
23
  import { DevicePolicyService } from "../local-devices/device-policy.service.js";
25
24
  import { deviceLeaseKey } from "../local-devices/keys.js";
26
25
  import { MachinesService } from "../local-devices/machines.js";
27
26
  import { deviceFeaturesOf, localPolicyOf } from "../local-devices/policy-summary.js";
28
27
  import { readOrgSettings } from "../orgs/settings.service.js";
29
- import { SandboxJobs } from "../sandbox/jobs.js";
28
+ import { oweDeviceDestroy, SandboxJobs } from "../sandbox/jobs.js";
30
29
  import { isInternalProvider, LOCAL_PROVIDER, SandboxProviders } from "../sandbox/providers.js";
31
30
  import { deviceNotFound, SandboxService } from "../sandbox/sandbox.service.js";
32
31
  import { effectiveSandboxPolicy, readDeploymentSandboxPolicy, } from "../sandbox/tiers.js";
@@ -43,8 +42,8 @@ const nameTaken = () => new ApiError("device.name_taken", "You already have a de
43
42
  * Personal devices (spec 2026-11-07-environments-design §4.2 items 7, 12, 13; §6; §9):
44
43
  * cloud ones on the deployment's sandbox provider and `device` ones on a connected machine; a
45
44
  * person's devices in an org, visible and usable only by their owner (anyone else gets
46
- * `device.not_found`). Deleting one soft-deletes it and destroys its instance and files in a
47
- * `sandbox-destroy-for` job after the commit.
45
+ * `device.not_found`). Deleting one soft-deletes it and owes the destruction of its instance and
46
+ * files (a `sandbox-destroy-for` job) in the same transaction.
48
47
  */
49
48
  let DevicesService = class DevicesService {
50
49
  db;
@@ -53,13 +52,15 @@ let DevicesService = class DevicesService {
53
52
  providers;
54
53
  sandbox;
55
54
  sandboxJobs;
55
+ outbox;
56
56
  access;
57
57
  displayNames;
58
- log;
59
58
  locks;
60
59
  machines;
61
60
  policies;
62
- constructor(db, txHost, config, providers, sandbox, sandboxJobs, access, displayNames, log,
61
+ constructor(db, txHost, config, providers, sandbox, sandboxJobs,
62
+ /** Owes a deleted device's destruction in the delete's transaction. */
63
+ outbox, access, displayNames,
63
64
  /** Reads the device relay's lease (`online`). */
64
65
  locks, machines, policies) {
65
66
  this.db = db;
@@ -68,9 +69,9 @@ let DevicesService = class DevicesService {
68
69
  this.providers = providers;
69
70
  this.sandbox = sandbox;
70
71
  this.sandboxJobs = sandboxJobs;
72
+ this.outbox = outbox;
71
73
  this.access = access;
72
74
  this.displayNames = displayNames;
73
- this.log = log;
74
75
  this.locks = locks;
75
76
  this.machines = machines;
76
77
  this.policies = policies;
@@ -388,9 +389,9 @@ let DevicesService = class DevicesService {
388
389
  });
389
390
  }
390
391
  /**
391
- * Deletes one of the person's live devices: soft delete, its IM default row cleared, and
392
- * after the commit a `sandbox-destroy-for` job (the worker waits for running commands, then
393
- * destroys the instance and the files). Conversations bound to it keep pointing at it; their
392
+ * Deletes one of the person's live devices: soft delete, its IM default row cleared, and a
393
+ * `sandbox-destroy-for` job owed in the same transaction (`device-destroy-<deviceId>`; the
394
+ * worker waits for running commands, then destroys the instance and the files). Conversations bound to it keep pointing at it; their
394
395
  * sandbox tools then fail with `device.not_found`. A local device's machine is
395
396
  * revoked in the same transaction; after the commit it is told `stop { reason: "deleted" }`
396
397
  * (while connected) and its connection is closed.
@@ -417,15 +418,7 @@ let DevicesService = class DevicesService {
417
418
  target: id,
418
419
  detail: { name: before.name, isDefault: before.is_default, ...auditDetail },
419
420
  });
420
- afterCommit(async () => {
421
- try {
422
- await this.sandboxJobs.destroy([id]);
423
- }
424
- catch (err) {
425
- // the hourly destruction sweep destroys deleted devices too
426
- this.log.warn("cannot schedule device destruction", { deviceId: id, err });
427
- }
428
- });
421
+ await oweDeviceDestroy(this.outbox, id, orgId);
429
422
  }
430
423
  /**
431
424
  * Disconnects the machine of one of the person's own local devices (it must sign in
@@ -1021,9 +1014,9 @@ DevicesService = __decorate([
1021
1014
  __param(3, Inject(SandboxProviders)),
1022
1015
  __param(4, Inject(SandboxService)),
1023
1016
  __param(5, Inject(SandboxJobs)),
1024
- __param(6, Inject(DeviceAccess)),
1025
- __param(7, Inject(ExtensionDisplayNames)),
1026
- __param(8, Inject(KERNEL_LOGGER)),
1017
+ __param(6, Inject(OutboxService)),
1018
+ __param(7, Inject(DeviceAccess)),
1019
+ __param(8, Inject(ExtensionDisplayNames)),
1027
1020
  __param(9, Inject(LockService)),
1028
1021
  __param(10, Inject(MachinesService)),
1029
1022
  __param(11, Inject(DevicePolicyService))
@@ -1,4 +1,4 @@
1
- import { type ContentBlock, type Logger, type ModelUsage, type StopReason } from "@capybara-hq/plugin-sdk";
1
+ import { type ContentBlock, type ConversationSource, type Logger, type ModelUsage, type StopReason } from "@capybara-hq/plugin-sdk";
2
2
  import { type ModelRef } from "../agents/definition.js";
3
3
  import { EventLog } from "../conversations/event-log.js";
4
4
  import type { KernelSchemaDb } from "../db/kernel-schema.js";
@@ -98,6 +98,8 @@ export interface ReplaySeed {
98
98
  evalRunId: string;
99
99
  orgId: string;
100
100
  agentId: string;
101
+ /** The original conversation's source: the replay offers the tools of this source. */
102
+ source: ConversationSource;
101
103
  accountId: string;
102
104
  conversationId: string;
103
105
  runId: string;
@@ -121,11 +123,20 @@ export interface ReplaySeed {
121
123
  runContext: {
122
124
  payload: unknown;
123
125
  } | undefined;
126
+ /**
127
+ * The original run's `plugin_context` events (memory spec §6.1), in log order, copied as is:
128
+ * replays never call context providers.
129
+ */
130
+ pluginContexts: {
131
+ payload: {
132
+ plugin: string;
133
+ };
134
+ }[];
124
135
  }
125
136
  /**
126
137
  * The seed of a full-run replay (C6): the `kind = 'eval'` conversation with the events before the
127
138
  * original run at their original seqs, the run row and its `run_started` (and the original
128
- * `skills_catalog`), in one transaction of its own.
139
+ * `skills_catalog`, `run_context` and `plugin_context` events), in one transaction of its own.
129
140
  */
130
141
  export declare class EvalReplaySeeder {
131
142
  private readonly db;
@@ -18,6 +18,7 @@ import { ApiError } from "../errors.js";
18
18
  import { KernelDb } from "../infra/database/kernel-db.js";
19
19
  import { TransactionHost } from "../infra/database/transaction-host.js";
20
20
  import { Transactional } from "../infra/database/transactional.decorator.js";
21
+ import { pluginContextIdemKey } from "../runtime/context/plugin-context.js";
21
22
  import { estimateTokens } from "../runtime/context/tokens.js";
22
23
  import { runDeadline, tightestLimit } from "../runtime/limits.js";
23
24
  import { RunError } from "../runtime/model-call.js";
@@ -36,7 +37,7 @@ export class ReplayClaimLost extends Error {
36
37
  /**
37
38
  * The seed of a full-run replay (C6): the `kind = 'eval'` conversation with the events before the
38
39
  * original run at their original seqs, the run row and its `run_started` (and the original
39
- * `skills_catalog`), in one transaction of its own.
40
+ * `skills_catalog`, `run_context` and `plugin_context` events), in one transaction of its own.
40
41
  */
41
42
  let EvalReplaySeeder = class EvalReplaySeeder {
42
43
  db;
@@ -74,6 +75,7 @@ let EvalReplaySeeder = class EvalReplaySeeder {
74
75
  consumed_seq: p.consumed,
75
76
  kind: "eval",
76
77
  eval_run_id: evalRunId,
78
+ source: p.source,
77
79
  })
78
80
  .execute();
79
81
  for (let i = 0; i < p.seeds.length; i += SEED_BATCH) {
@@ -133,6 +135,12 @@ let EvalReplaySeeder = class EvalReplaySeeder {
133
135
  },
134
136
  ]
135
137
  : []),
138
+ ...p.pluginContexts.map((c) => ({
139
+ type: "plugin_context",
140
+ runId,
141
+ idemKey: pluginContextIdemKey(runId, c.payload.plugin),
142
+ payload: c.payload,
143
+ })),
136
144
  ]);
137
145
  if (onSeeded && !(await onSeeded(conversationId)))
138
146
  throw new ReplayClaimLost();
@@ -333,6 +341,9 @@ export async function replayRun(d, s, candidate, evalRunId, signal) {
333
341
  const trigger = replayTrigger(start.payload, seeds, consumed);
334
342
  const catalog = s.events.find((e) => e.runId === s.runId && e.type === "skills_catalog" && e.seq > R);
335
343
  const runContext = s.events.find((e) => e.runId === s.runId && e.type === "run_context" && e.seq > R);
344
+ const pluginContexts = s.events
345
+ .filter((e) => e.runId === s.runId && e.type === "plugin_context" && e.seq > R)
346
+ .map((e) => ({ payload: e.payload }));
336
347
  // a sub-agent's case (spec §15.3) runs the effective definition frozen in its subagent_started
337
348
  // (the request renders that event's preamble and instructions; its tools come from it too), with
338
349
  // only the candidate's model and params, and the logged step limit
@@ -348,12 +359,16 @@ export async function replayRun(d, s, candidate, evalRunId, signal) {
348
359
  const forkRun = triggerEvent?.type === "user_message" &&
349
360
  triggerEvent.payload.skill !== undefined &&
350
361
  triggerEvent.payload.dispatch !== undefined;
362
+ // the original conversation's source (recorded in the case); a case taken before it was
363
+ // recorded replays as its run did by default: a web conversation, or a sub-agent's for a child
364
+ const source = s.source ?? (subagent === undefined ? "web" : "subagent");
351
365
  const conversationId = randomUUID();
352
366
  const runId = randomUUID();
353
367
  await d.seeder.seed({
354
368
  evalRunId,
355
369
  orgId: s.orgId,
356
370
  agentId: s.agentId,
371
+ source,
357
372
  accountId,
358
373
  conversationId,
359
374
  runId,
@@ -365,6 +380,7 @@ export async function replayRun(d, s, candidate, evalRunId, signal) {
365
380
  model: agent.model,
366
381
  catalog,
367
382
  runContext,
383
+ pluginContexts,
368
384
  }, d.onSeeded);
369
385
  await d.hooks?.afterRunSeeded?.(evalRunId, conversationId);
370
386
  // 停止:取消 / 越限 / 熔断 / 失去认领 / 截止时间 → 中止 loop(进行中的请求立即停下,loop 以 aborted 返回)
@@ -542,6 +558,13 @@ export async function replayRun(d, s, candidate, evalRunId, signal) {
542
558
  // a fork invocation's report is the recorded one: a replay never dispatches a sub-agent (A5)
543
559
  const skillForks = recordedSkillForks(eventLog, s.events.filter((e) => e.runId === s.runId));
544
560
  const loop = d.loop({ toolPipeline: pipeline, registry, skillForks, ...(media && { media }) });
561
+ // a replay never executes a tool, so the role only completes the context
562
+ const membership = await db
563
+ .selectFrom("memberships")
564
+ .select("role")
565
+ .where("org_id", "=", s.orgId)
566
+ .where("account_id", "=", accountId)
567
+ .executeTakeFirst();
545
568
  let outcome;
546
569
  try {
547
570
  outcome = await loop.run({
@@ -550,6 +573,10 @@ export async function replayRun(d, s, candidate, evalRunId, signal) {
550
573
  runId,
551
574
  agentId: s.agentId,
552
575
  triggerAccountId: accountId,
576
+ // the seeded conversation's source (the original's): its tools are the original's
577
+ source,
578
+ ownerAccountId: accountId,
579
+ role: membership?.role ?? "guest",
553
580
  agent,
554
581
  agentVersion: candidate.agentVersion,
555
582
  signal: loopSignal,
@@ -1,4 +1,4 @@
1
- import type { ToolSpec } from "@capybara-hq/plugin-sdk";
1
+ import type { ConversationSource, ToolSpec } from "@capybara-hq/plugin-sdk";
2
2
  import { type AgentDefinition } from "../agents/definition.js";
3
3
  import type { ConversationEvent, ConversationEventType } from "../conversations/events.js";
4
4
  import type { KernelSchemaDb } from "../db/kernel-schema.js";
@@ -21,6 +21,12 @@ export interface CaseSnapshot {
21
21
  runId: string;
22
22
  agentId: string;
23
23
  agentVersion: number;
24
+ /**
25
+ * The original conversation's source (memory plugin spec §6.2): a full-run replay offers the
26
+ * tools of this source. Cases taken before it was recorded have none (replayed as `web`, or as
27
+ * `subagent` for a child case).
28
+ */
29
+ source?: ConversationSource;
24
30
  /** seq ≤ 该运行终态事件的 seq,按 seq 升序。 */
25
31
  events: SnapshotEvent[];
26
32
  /** 事件里各次运行(`run_started`)与本运行引用的 agent 版本 → 定义。 */
@@ -17,7 +17,7 @@ const PAGE = 1000;
17
17
  export async function takeSnapshot(db, orgId, conversationId, runId, maxBytes) {
18
18
  const conv = await db
19
19
  .selectFrom("conversations")
20
- .select(["agent_id", "kind"])
20
+ .select(["agent_id", "kind", "source"])
21
21
  .where("id", "=", conversationId)
22
22
  .where("org_id", "=", orgId)
23
23
  .executeTakeFirst();
@@ -126,6 +126,7 @@ export async function takeSnapshot(db, orgId, conversationId, runId, maxBytes) {
126
126
  runId,
127
127
  agentId: conv.agent_id,
128
128
  agentVersion: run.agent_version,
129
+ source: conv.source,
129
130
  events,
130
131
  definitions,
131
132
  toolSpecs,