@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
@@ -0,0 +1,121 @@
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 { Inject, Injectable } from "@nestjs/common";
11
+ import { KernelDb } from "../infra/database/kernel-db.js";
12
+ import { TransactionHost } from "../infra/database/transaction-host.js";
13
+ import { Transactional } from "../infra/database/transactional.decorator.js";
14
+ import { DevicePolicyService, } from "../local-devices/device-policy.service.js";
15
+ import { opVisibleIn, pendingPolicyOpsOf, } from "../local-devices/policy-summary.js";
16
+ import { ApprovalsService } from "./approvals.service.js";
17
+ /**
18
+ * The `device-op-resolve` job (transactional outbox spec §7): records what became of a device
19
+ * policy op whose outcome its caller did not record (a crash between the call and the resolve
20
+ * transaction, an answer that never came). Owed by the op's intent for when the op can no longer
21
+ * reach the machine, and by a hello that reported the op (`device-op-reported-<opId>`).
22
+ *
23
+ * An op without an entry any more is resolved already: a no-op. Otherwise the outcome is the one
24
+ * the hello reported, else the machine's answer to a read-only `policyStatus`: `applied` →
25
+ * recorded applied; `refused`, never received (`unknown` to the machine, or `absent` from its
26
+ * hello, once the op can no longer reach it) → failed; `started` (the machine stopped between
27
+ * its log and its answer), once the op can no longer reach it → applied when the policy it
28
+ * reports holds the change, else failed (`interrupted`); no answer (`offline`) → `unknown`, left
29
+ * for the machine's next hello. It never sends the change
30
+ * again (a widening after its ask closed is not allowed). An op that answers a network ask
31
+ * decides it when applied and the ask is still pending, else leaves it pending with the reason
32
+ * (`ApprovalsService.settleFromResolver`).
33
+ */
34
+ let DeviceOpResolver = class DeviceOpResolver {
35
+ db;
36
+ txHost;
37
+ policies;
38
+ approvals;
39
+ constructor(db, txHost, policies, approvals) {
40
+ this.db = db;
41
+ this.txHost = txHost;
42
+ this.policies = policies;
43
+ this.approvals = approvals;
44
+ }
45
+ async resolve(data) {
46
+ const row = await this.db
47
+ .selectFrom("device_machines")
48
+ .select(["device_id", "pending_policy_ops"])
49
+ .where("id", "=", data.machineId)
50
+ .executeTakeFirst();
51
+ const op = pendingPolicyOpsOf(row?.pending_policy_ops).find((o) => o.id === data.opId);
52
+ if (row === undefined || op === undefined)
53
+ return;
54
+ const result = op.reported !== undefined
55
+ ? await this.reportedResult(row.device_id, op, op.reported.outcome)
56
+ : statusResult(op, await this.policies.outcomeOf(row.device_id, op.id));
57
+ await this.settle(data, op.source.approvalId, result);
58
+ }
59
+ /**
60
+ * The outcome a hello reported; a refusal's message (a hello carries none) is the machine's
61
+ * answer to `policyStatus` when it is connected.
62
+ */
63
+ async reportedResult(deviceId, op, outcome) {
64
+ if (outcome === "applied")
65
+ return { outcome: "applied" };
66
+ if (outcome === "absent")
67
+ return { outcome: "not_received" };
68
+ if (outcome === "interrupted")
69
+ return { outcome: "interrupted" };
70
+ const s = await this.policies.outcomeOf(deviceId, op.id);
71
+ const message = s.outcome === "refused" ? s.message : undefined;
72
+ return { outcome: "refused", message: (message ?? REFUSED_MESSAGE).slice(0, MAX_MESSAGE) };
73
+ }
74
+ /**
75
+ * The resolve transaction: the op's outcome, and the network ask it answers decided or given
76
+ * the reason in the same transaction (`ApprovalsService.settleFromResolver`).
77
+ */
78
+ async settle(data, approvalId, result) {
79
+ if (approvalId === null)
80
+ await this.policies.settleIn(data, result);
81
+ else
82
+ await this.approvals.settleFromResolver({ ...data, approvalId }, result);
83
+ }
84
+ };
85
+ __decorate([
86
+ Transactional()
87
+ ], DeviceOpResolver.prototype, "settle", null);
88
+ DeviceOpResolver = __decorate([
89
+ Injectable(),
90
+ __param(0, Inject(KernelDb)),
91
+ __param(1, Inject(TransactionHost)),
92
+ __param(2, Inject(DevicePolicyService)),
93
+ __param(3, Inject(ApprovalsService))
94
+ ], DeviceOpResolver);
95
+ export { DeviceOpResolver };
96
+ const REFUSED_MESSAGE = "The device's machine refused the policy change.";
97
+ /** The longest refusal message kept from the machine (as its own op log keeps it). */
98
+ const MAX_MESSAGE = 500;
99
+ function statusResult(op, s) {
100
+ switch (s.outcome) {
101
+ case "applied":
102
+ return { outcome: "applied", ...(s.policy !== undefined && { policy: s.policy }) };
103
+ case "refused":
104
+ return { outcome: "refused", message: (s.message ?? REFUSED_MESSAGE).slice(0, MAX_MESSAGE) };
105
+ case "started":
106
+ // logged `started` only: once the op can no longer reach the machine, the policy it reports
107
+ // tells whether the change was written before it stopped
108
+ if (Date.now() < op.resolveAt || s.policy === undefined)
109
+ return { outcome: "unknown" };
110
+ return opVisibleIn(op, s.policy)
111
+ ? { outcome: "applied", policy: s.policy }
112
+ : { outcome: "interrupted" };
113
+ case "unknown":
114
+ // unknown to the machine: never received, once the op can no longer reach it (a job run
115
+ // early, on a clock running ahead, cannot tell yet)
116
+ return Date.now() >= op.resolveAt ? { outcome: "not_received" } : { outcome: "unknown" };
117
+ default:
118
+ // offline: its next hello tells
119
+ return { outcome: "unknown" };
120
+ }
121
+ }
@@ -85,7 +85,7 @@ export declare function blockedEventHosts(blocked: readonly NetworkBlockedHost[]
85
85
  * the rest:
86
86
  * - `address` and `org` layers: a note each (in an unattended run an `org` host is a missing grant);
87
87
  * `unknown`: nothing (the device's trailer explains);
88
- * - `device` layer on a program without `policyOp`: nothing (no ask, the trailer explains);
88
+ * - `device` layer on a program without `policyOpId`: nothing (no ask, the trailer explains);
89
89
  * - approved by the scheduled task the run serves (`run.taskGranted`) yet refused: a note that
90
90
  * the device ignores the task's grants (never asked, never a missing grant);
91
91
  * - refused earlier in the run tree: the denied note; an unattended run in `fail` mode: its note
@@ -90,7 +90,7 @@ export function blockedEventHosts(blocked) {
90
90
  * the rest:
91
91
  * - `address` and `org` layers: a note each (in an unattended run an `org` host is a missing grant);
92
92
  * `unknown`: nothing (the device's trailer explains);
93
- * - `device` layer on a program without `policyOp`: nothing (no ask, the trailer explains);
93
+ * - `device` layer on a program without `policyOpId`: nothing (no ask, the trailer explains);
94
94
  * - approved by the scheduled task the run serves (`run.taskGranted`) yet refused: a note that
95
95
  * the device ignores the task's grants (never asked, never a missing grant);
96
96
  * - refused earlier in the run tree: the denied note; an unattended run in `fail` mode: its note
@@ -107,9 +107,10 @@ export function planNetworkAsk(blocked, run, ctx) {
107
107
  const taskIgnored = [];
108
108
  const candidates = [];
109
109
  const missingGrants = [];
110
- // a machine that reports blocked hosts and takes policy changes (Docker mode has no report)
110
+ // a machine that reports blocked hosts and takes policy changes under op ids (Docker mode has
111
+ // no report; a program without op ids cannot take 「一直允许」)
111
112
  const canAsk = ctx.device?.features.includes("networkReport") === true &&
112
- ctx.device.features.includes("policyOp");
113
+ ctx.device.features.includes("policyOpId");
113
114
  // no run grant or policy change reaches a device whose owner turned remote grants off
114
115
  const remote = ctx.device?.policy?.remoteGrants !== "off";
115
116
  for (const b of hostsOf(blocked)) {
@@ -1,4 +1,4 @@
1
- import { type FormAnswer, type InputForm, type Logger } from "@capybara-hq/plugin-sdk";
1
+ import { type ConversationSource, type FormAnswer, type InputForm, type Logger } from "@capybara-hq/plugin-sdk";
2
2
  import type { AgentDefinition } from "../agents/definition.js";
3
3
  import { EventLog } from "../conversations/event-log.js";
4
4
  import type { KernelSchemaDb } from "../db/kernel-schema.js";
@@ -222,8 +222,17 @@ export interface ToolStepContext {
222
222
  /** 运行所用的 agent 版本(审计链记录)。 */
223
223
  agentVersion: number;
224
224
  agent: AgentDefinition;
225
- /** 对话发起人(取个人凭据、审计 actor)。 */
225
+ /** The run's trigger account (personal credentials, the audit actor). */
226
226
  accountId: string;
227
+ /**
228
+ * The conversation's source (memory plugin spec §6.2): extension tools limited by
229
+ * `spec.sources` are resolved with it, and plugin handlers receive it.
230
+ */
231
+ source: ConversationSource;
232
+ /** The conversation's owner (`conversations.account_id`), given to plugin handlers. */
233
+ ownerAccountId: string;
234
+ /** The trigger account's current org role, given to plugin handlers. */
235
+ role: OrgRole;
227
236
  /** run 的中止信号(用户中止 / 截止时间)。 */
228
237
  signal: AbortSignal;
229
238
  /** run 的截止时间(epoch 毫秒);传给内置工具(沙箱命令据此收紧超时,Ruling M20)。 */
@@ -314,6 +314,7 @@ let ToolPipeline = class ToolPipeline {
314
314
  forkChild: ctx.parent?.forSkill === true,
315
315
  sandboxConversationId: ctx.parent?.rootConversationId ?? ctx.conversationId,
316
316
  replay: ctx.conversationKind === "eval",
317
+ source: ctx.source,
317
318
  });
318
319
  }
319
320
  return resolved;
@@ -1001,6 +1002,10 @@ let ToolPipeline = class ToolPipeline {
1001
1002
  conversationId: ctx.conversationId,
1002
1003
  callId,
1003
1004
  ...(resume !== undefined && { resume }),
1005
+ // who and where (memory plugin spec §6.2): `accountId` stays the trigger account
1006
+ source: ctx.source,
1007
+ ownerAccountId: ctx.ownerAccountId,
1008
+ role: ctx.role,
1004
1009
  ...(tool.source.kind === "builtin" && stepTools !== undefined && { tools: stepTools }),
1005
1010
  ...(tool.source.kind === "builtin" && { runId: ctx.runId }),
1006
1011
  ...(tool.source.kind === "builtin" &&
@@ -105,11 +105,22 @@ export declare class QuestionsService {
105
105
  */
106
106
  cancelPendingIn(runId: string): Promise<QuestionRecord[]>;
107
107
  /**
108
- * 在调用方的事务内(`mandatory`)取消某组织里由 `accountId` 发起的运行上的全部 pending 输入请求(只有运行发起人能回答,
109
- * 即「其作为处理人」的提问;成员离职时调用),走与 {@link cancelPendingIn} 相同的取消路径:置 cancelled、
110
- * 写 `question_answered{cancelled}`。返回被取消的记录与各会话写到的最大 seq;调用方提交后发布事件。
108
+ * The conversations of `orgId` holding a pending question of a run `accountId` started (only the
109
+ * run's starter answers), read without locks: offboarding locks them, by tree root first
110
+ * (`lockConversationTrees`), before {@link cancelPendingFor}.
111
111
  */
112
- cancelPendingFor(orgId: string, accountId: string): Promise<{
112
+ pendingConversationsFor(orgId: string, accountId: string): Promise<string[]>;
113
+ /**
114
+ * In the caller's transaction (`mandatory`; offboarding): cancels every pending question of a run
115
+ * `accountId` started in `orgId` within `conversationIds`, the conversations the caller already
116
+ * locked (by tree root first, `lockConversationTrees`; lock order D7: conversation, then question
117
+ * rows), along the same path as {@link cancelPendingIn}: `cancelled`,
118
+ * `question_answered{cancelled}`. A question asked meanwhile in another conversation waits for
119
+ * its run's stop (offboarding owes it), so no conversation is locked after a row. Returns the
120
+ * cancelled records and the highest seq written per conversation; the caller publishes after the
121
+ * commit.
122
+ */
123
+ cancelPendingFor(orgId: string, accountId: string, conversationIds: readonly string[]): Promise<{
113
124
  records: QuestionRecord[];
114
125
  seqs: Map<string, number>;
115
126
  }>;
@@ -324,14 +324,12 @@ let QuestionsService = class QuestionsService {
324
324
  return out;
325
325
  }
326
326
  /**
327
- * 在调用方的事务内(`mandatory`)取消某组织里由 `accountId` 发起的运行上的全部 pending 输入请求(只有运行发起人能回答,
328
- * 即「其作为处理人」的提问;成员离职时调用),走与 {@link cancelPendingIn} 相同的取消路径:置 cancelled、
329
- * 写 `question_answered{cancelled}`。返回被取消的记录与各会话写到的最大 seq;调用方提交后发布事件。
327
+ * The conversations of `orgId` holding a pending question of a run `accountId` started (only the
328
+ * run's starter answers), read without locks: offboarding locks them, by tree root first
329
+ * (`lockConversationTrees`), before {@link cancelPendingFor}.
330
330
  */
331
- async cancelPendingFor(orgId, accountId) {
332
- const t = this.db.db;
333
- // the conversations first, in id order (lock order D7: conversation, then question rows)
334
- const convs = await t
331
+ async pendingConversationsFor(orgId, accountId) {
332
+ const rows = await this.db
335
333
  .selectFrom("questions")
336
334
  .select("conversation_id")
337
335
  .distinct()
@@ -339,24 +337,28 @@ let QuestionsService = class QuestionsService {
339
337
  .where("status", "=", "pending")
340
338
  .where("run_id", "in", (eb) => eb.selectFrom("runs").select("id").where("trigger_account_id", "=", accountId))
341
339
  .execute();
342
- if (convs.length > 0) {
343
- await t
344
- .selectFrom("conversations")
345
- .select("id")
346
- .where("id", "in", convs.map((c) => c.conversation_id))
347
- .orderBy("id")
348
- .forUpdate()
349
- .execute();
350
- }
351
- // only the conversations locked above: a question asked meanwhile in another one waits for
352
- // the run's stop (offboarding aborts it) or the sweep, so no conversation is locked after a row
340
+ return rows.map((r) => r.conversation_id);
341
+ }
342
+ /**
343
+ * In the caller's transaction (`mandatory`; offboarding): cancels every pending question of a run
344
+ * `accountId` started in `orgId` within `conversationIds`, the conversations the caller already
345
+ * locked (by tree root first, `lockConversationTrees`; lock order D7: conversation, then question
346
+ * rows), along the same path as {@link cancelPendingIn}: `cancelled`,
347
+ * `question_answered{cancelled}`. A question asked meanwhile in another conversation waits for
348
+ * its run's stop (offboarding owes it), so no conversation is locked after a row. Returns the
349
+ * cancelled records and the highest seq written per conversation; the caller publishes after the
350
+ * commit.
351
+ */
352
+ async cancelPendingFor(orgId, accountId, conversationIds) {
353
+ const t = this.db.db;
354
+ const convs = [...conversationIds];
353
355
  if (convs.length === 0)
354
356
  return { records: [], seqs: new Map() };
355
357
  const rows = await t
356
358
  .selectFrom("questions")
357
359
  .selectAll()
358
360
  .where("org_id", "=", orgId)
359
- .where("conversation_id", "in", convs.map((c) => c.conversation_id))
361
+ .where("conversation_id", "in", convs)
360
362
  .where("status", "=", "pending")
361
363
  .where("run_id", "in", (eb) => eb.selectFrom("runs").select("id").where("trigger_account_id", "=", accountId))
362
364
  .orderBy("created_at")
@@ -1,4 +1,4 @@
1
- import type { Connector, HookParentScope, InputRequired, NetworkBlockedHost, ToolAnnotations, ToolCallContext, ToolResult, ToolSpec } from "@capybara-hq/plugin-sdk";
1
+ import type { Connector, ConversationSource, HookParentScope, InputRequired, NetworkBlockedHost, ToolAnnotations, ToolCallContext, ToolResult, ToolSpec } from "@capybara-hq/plugin-sdk";
2
2
  import { type AgentDefinition, type DisabledToolRef, type SubagentTools } from "../agents/definition.js";
3
3
  import type { ToolView } from "../conversations/events.js";
4
4
  import { EffectiveConfigs } from "../extensions/effective-config.js";
@@ -213,6 +213,8 @@ export declare function withoutDisplayFields<T>(spec: T): T;
213
213
  export declare function specHash(spec: unknown): string;
214
214
  /** 由注解推导默认策略:只读 → allow;其余(破坏性 / 无注解)→ ask。 */
215
215
  export declare function policyFromAnnotations(a: ToolAnnotations | undefined): "allow" | "ask";
216
+ /** Why an extension tool limited by `spec.sources` is not offered in a `source` conversation. */
217
+ export declare const sourceHiddenReason: (source: ConversationSource) => string;
216
218
  /** `forAgent` 的结果。 */
217
219
  export interface AgentTools {
218
220
  tools: ResolvedTool[];
@@ -362,6 +364,11 @@ export declare class ToolRegistry {
362
364
  sandboxConversationId?: string;
363
365
  /** An eval replay's listing: its calls are never executed (the sandbox tools are listed). */
364
366
  replay?: boolean;
367
+ /**
368
+ * The conversation's source (memory plugin spec §6.2): an extension tool whose
369
+ * `spec.sources` leaves it out is hidden. Required, so nothing defaults to the widest.
370
+ */
371
+ source: ConversationSource;
365
372
  }): Promise<AgentTools>;
366
373
  /** 是否启用 `ToolSearch`(见 {@link forAgent});先比阈值,超过才读组织设置。 */
367
374
  private searchActive;
@@ -59,6 +59,8 @@ export function specHash(spec) {
59
59
  export function policyFromAnnotations(a) {
60
60
  return a?.readOnlyHint === true ? "allow" : "ask";
61
61
  }
62
+ /** Why an extension tool limited by `spec.sources` is not offered in a `source` conversation. */
63
+ export const sourceHiddenReason = (source) => `Not available in ${source} conversations`;
62
64
  // `ToolSearch` is added by the registry by org setting and threshold, `Skill` when the log has a
63
65
  // skills catalog snapshot; neither comes from agent definitions (`./builtin-names.ts`).
64
66
  /** 工具定义(模型可见三项)的估算 token 数(与请求估算同一口径)。 */
@@ -348,6 +350,7 @@ let ToolRegistry = class ToolRegistry {
348
350
  }
349
351
  async resolve(orgId, def) {
350
352
  const disabled = def.disabledTools ?? [];
353
+ const { source } = def;
351
354
  const scope = {
352
355
  orgId,
353
356
  ...(def.sandboxConversationId !== undefined && {
@@ -435,6 +438,12 @@ let ToolRegistry = class ToolRegistry {
435
438
  hidden.push({ ref: key, reason: toolTurnedOffReason(key) });
436
439
  continue;
437
440
  }
441
+ // offered only in some conversation sources; the source never changes, so this also
442
+ // refuses a resumed call (the pipeline resolves the step's tools with the same source)
443
+ if (t.spec.sources !== undefined && !t.spec.sources.includes(source)) {
444
+ hidden.push({ ref: key, reason: sourceHiddenReason(source) });
445
+ continue;
446
+ }
438
447
  if (!(await configuredIn(t.plugin))) {
439
448
  hidden.push({ ref: key, reason: `Plugin ${t.plugin} ${NEEDS_CONFIG_REASON}` });
440
449
  continue;
@@ -34,6 +34,7 @@ import { ConnectorAccountPolicies } from "./connector-account-policies.js";
34
34
  import { ConnectorContent } from "./connector-content.js";
35
35
  import { ConnectorsService } from "./connectors.service.js";
36
36
  import { Credentials } from "./credentials.js";
37
+ import { DeviceOpResolver } from "./device-op-resolver.js";
37
38
  import { ToolDrift } from "./drift.js";
38
39
  import { ConversationToolInfo, OrgExtensionTools } from "./info.js";
39
40
  import { OAuthFlow } from "./oauth/flow.js";
@@ -106,6 +107,8 @@ ToolsModule = __decorate([
106
107
  ConversationToolInfo,
107
108
  OrgExtensionTools,
108
109
  OrgToolSwitches,
110
+ // the `device-op-resolve` job's body (it may decide the network ask the op answers)
111
+ DeviceOpResolver,
109
112
  ],
110
113
  exports: [
111
114
  ScheduleToolsPort,
@@ -126,6 +129,7 @@ ToolsModule = __decorate([
126
129
  ConversationToolInfo,
127
130
  OrgExtensionTools,
128
131
  OrgToolSwitches,
132
+ DeviceOpResolver,
129
133
  ],
130
134
  })
131
135
  ], ToolsModule);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capybara-hq/kernel",
3
- "version": "0.1.0-rc.24",
3
+ "version": "0.1.0-rc.26",
4
4
  "description": "Capybara kernel: config, database, plugin host and HTTP API of the Capybara agent platform",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -65,7 +65,7 @@
65
65
  "yaml": "^2.9.1",
66
66
  "yauzl": "3.4.0",
67
67
  "zod": "^4.6.7",
68
- "@capybara-hq/plugin-sdk": "^0.1.0-rc.18"
68
+ "@capybara-hq/plugin-sdk": "^0.1.0-rc.20"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@langchain/langgraph-checkpoint-validation": "^1.2.0",
@@ -74,7 +74,7 @@
74
74
  "@types/ws": "^8.18.2",
75
75
  "@types/yauzl": "3.4.0",
76
76
  "ws": "^8.22.0",
77
- "@capybara-hq/testing": "0.1.0-rc.16"
77
+ "@capybara-hq/testing": "0.1.0-rc.17"
78
78
  },
79
79
  "scripts": {
80
80
  "build": "tsc -b"
@@ -0,0 +1,12 @@
1
+ -- 0121, transactional outbox spec §8.2: the platform components migration job is gone (an import
2
+ -- writes its platform components in its transaction), so nothing writes its runs table any more and
3
+ -- the platform inbox no longer shows a run; the org notice kind `skill_renamed`, which only that job
4
+ -- wrote, goes with it.
5
+ -- capybara:allow-destructive the job that wrote this table is removed in this release and nothing reads it (project not released, no compatibility layer)
6
+ DROP TABLE IF EXISTS platform_migration_runs;
7
+ -- capybara:allow-destructive notices of a kind only the removed job wrote; the inbox no longer renders the kind (project not released)
8
+ DELETE FROM org_notices WHERE kind = 'skill_renamed';
9
+ -- capybara:allow-destructive the check is added back without `skill_renamed` in the next statement; no row has that kind any more
10
+ ALTER TABLE org_notices DROP CONSTRAINT IF EXISTS org_notices_kind_check;
11
+ ALTER TABLE org_notices ADD CONSTRAINT org_notices_kind_check
12
+ CHECK (kind IN ('item_shared', 'shared_item_updated', 'upload_confirmed', 'upload_rejected'));
@@ -0,0 +1,5 @@
1
+ -- 0122, transactional outbox spec §8.1 / §8.2: the transaction that writes a pending task run owes
2
+ -- its fire job in the outbox, so the scheduled task scan's repair is gone, and with it the column
3
+ -- that rotated the repair's rechecks of started runs.
4
+ -- capybara:allow-destructive the repair that read and wrote this column is removed in this release and nothing else reads it (project not released, no compatibility layer)
5
+ ALTER TABLE scheduled_task_runs DROP COLUMN IF EXISTS rechecked_at;
@@ -0,0 +1,27 @@
1
+ -- pgvector, when the server offers it (memory plugin spec §6.4). Plugins store embeddings in a
2
+ -- `vector` column when the type is visible from their search_path (`plugin_<name>, public`) and fall
3
+ -- back to `real[]` otherwise, so pgvector is recommended, not required:
4
+ -- - not offered by the server (no row in pg_available_extensions): nothing to do;
5
+ -- - already installed, in whatever schema a DBA put it: left where it is;
6
+ -- - refused (insufficient_privilege: the migrating role may not create it; feature_not_supported:
7
+ -- a managed server lists it but its vendor allow-list refuses it, e.g. Azure Flexible Server):
8
+ -- a notice naming the SQLSTATE, the migration succeeds. Any other error still fails it.
9
+ -- `SCHEMA public` matters: the migrator's search_path is `capybara, public`, and a bare
10
+ -- CREATE EXTENSION would put the type and `<=>` in `capybara`, invisible to plugin schemas.
11
+ -- capybara:allow-destructive not destructive: a DO block is the only way to make CREATE EXTENSION conditional on availability and to turn a refused CREATE EXTENSION into a notice
12
+ DO $$
13
+ BEGIN
14
+ IF NOT EXISTS (SELECT 1 FROM pg_available_extensions WHERE name = 'vector') THEN
15
+ RAISE NOTICE 'pgvector is not available on this server; plugins fall back to real[] embeddings';
16
+ RETURN;
17
+ END IF;
18
+ IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'vector') THEN
19
+ RETURN;
20
+ END IF;
21
+ BEGIN
22
+ EXECUTE 'CREATE EXTENSION IF NOT EXISTS vector SCHEMA public';
23
+ EXCEPTION WHEN insufficient_privilege OR feature_not_supported THEN
24
+ RAISE NOTICE 'pgvector not installed: CREATE EXTENSION was refused (SQLSTATE %: %); plugins fall back to real[] embeddings', SQLSTATE, SQLERRM;
25
+ END;
26
+ END
27
+ $$;
@@ -0,0 +1,36 @@
1
+ -- Memory plugin spec §6.2: where a conversation comes from (web, channel_dm, channel_group,
2
+ -- subagent), set once at creation and never changed; extension tools and context@1 providers are
3
+ -- offered by it.
4
+ -- The column is added with a constant default, so existing rows read 'web' without a rewrite.
5
+ -- The backfill then sets the other sources, each UPDATE touching only rows still at 'web', so a
6
+ -- re-run updates nothing; later UPDATEs skip rows an earlier one set:
7
+ -- 1. kind 'subagent' -> subagent;
8
+ -- 2. a channel thread row of any archive state (DM /new archives the row, the conversation stays
9
+ -- a DM's): only 'dm:' keys -> channel_dm, any other key -> channel_group (a conversation with
10
+ -- both is a group's: the safer of the two);
11
+ -- 3. a scheduled group run whose thread is still to bind at its first post (no thread row yet):
12
+ -- its delivery's target chat type.
13
+ -- The rest stays web. The CHECK is added last (dropped first, so a re-run re-adds it).
14
+ ALTER TABLE conversations ADD COLUMN IF NOT EXISTS source text NOT NULL DEFAULT 'web';
15
+
16
+ UPDATE conversations SET source = 'subagent'
17
+ WHERE kind = 'subagent' AND source = 'web';
18
+
19
+ UPDATE conversations c
20
+ SET source = CASE WHEN t.dm THEN 'channel_dm' ELSE 'channel_group' END
21
+ FROM (
22
+ SELECT conversation_id, bool_and(thread_key LIKE 'dm:%') AS dm
23
+ FROM channel_threads
24
+ GROUP BY conversation_id
25
+ ) t
26
+ WHERE t.conversation_id = c.id AND c.source = 'web';
27
+
28
+ UPDATE conversations c
29
+ SET source = CASE WHEN d.target->>'chatType' = 'dm' THEN 'channel_dm' ELSE 'channel_group' END
30
+ FROM channel_deliveries d
31
+ WHERE d.conversation_id = c.id AND d.target->>'chatType' IN ('dm', 'group') AND c.source = 'web';
32
+
33
+ -- capybara:allow-destructive the CHECK is re-added unchanged right below (a re-run of this file)
34
+ ALTER TABLE conversations DROP CONSTRAINT IF EXISTS conversations_source_check;
35
+ ALTER TABLE conversations ADD CONSTRAINT conversations_source_check
36
+ CHECK (source IN ('web', 'channel_dm', 'channel_group', 'subagent'));
@@ -0,0 +1,5 @@
1
+ -- Agent map spec §6 (changed in acceptance 2026-11-13): a person's stop of a root conversation
2
+ -- stops its whole sub-agent tree and holds the tree's wakes. Set on the root by that stop, cleared
3
+ -- when the root next starts a run; while set, no sub-agent delivery wakes the root and no
4
+ -- sub-agent of its tree starts a run. Null on every existing row (no stop is held).
5
+ ALTER TABLE conversations ADD COLUMN IF NOT EXISTS tree_stopped_at timestamptz;
@@ -1,115 +0,0 @@
1
- import { KernelConfigService } from "../infra/config/kernel-config.service.js";
2
- import { KernelDb } from "../infra/database/kernel-db.js";
3
- import { TransactionHost } from "../infra/database/transaction-host.js";
4
- import { LockService } from "../infra/lock/lock.service.js";
5
- import type { KernelLogger } from "../log.js";
6
- import { OrgLock } from "../orgs/org-lock.js";
7
- import { PlatformComponents } from "./platform-components.js";
8
- /** The job's name (`@LockedInterval`, run lock `cron:platform-components-migrate`). */
9
- export declare const PLATFORM_MIGRATION_JOB = "platform-components-migrate";
10
- /** The audit action and inbox notice of a renamed skill. */
11
- export declare const SKILL_RENAMED = "skill_renamed";
12
- /** What one run changed (also stored in `platform_migration_runs.stats`). */
13
- export interface PlatformMigrationStats {
14
- /** Catalog items whose platform rows were written (`<name>@<version>`). */
15
- itemsSynced: string[];
16
- /** Catalog items whose sync failed (logged; tried again by the next run). */
17
- itemsFailed: string[];
18
- /** Org skill copies linked to their platform skill, and how many of them were pinned. */
19
- skillsLinked: number;
20
- skillsPinned: number;
21
- /** Org skills renamed because a platform skill of the same name is on in their org. */
22
- skillsRenamed: number;
23
- /** Org template copies linked to their platform template, and how many of them were pinned. */
24
- templatesLinked: number;
25
- templatesPinned: number;
26
- /** Connectors linked to the platform connector template they were made from. */
27
- connectorsLinked: number;
28
- }
29
- /**
30
- * The name a renamed skill gets: `<name>-org`, then `-org-2`, `-org-3`, … (the base shortened
31
- * when the result would be longer than a skill name may be), the first one not in `taken`.
32
- */
33
- export declare function renamedSkillName(name: string, taken: ReadonlySet<string>): string;
34
- /**
35
- * The platform components migration (spec §6, §13.5), a worker job (`platform-components-migrate`,
36
- * `@LockedInterval` with `runOnStart`: once across replicas at every worker start, then daily).
37
- * Steps, each on the rows that still need it:
38
- * 1. every live catalog item never synced (`platform_synced_version` null) gets its platform rows
39
- * (`syncFromVersion`, which records the version) at the highest
40
- * version installed in an org (spec §6: "the plugin's version becomes the highest installed
41
- * one"), else its latest version;
42
- * 2. an org skill copy with plugin provenance (on the skill and its current version) links to the
43
- * platform skill of its plugin and name; it is pinned to its own current version when its files
44
- * differ from the platform version's. A copy whose platform skill is not on in its org is left
45
- * unlinked (it keeps serving what it served) until a later run finds it on: a linked copy,
46
- * pinned or not, follows its platform skill's availability (ruling P2-18);
47
- * 3. one-shot (spec §5 rule 10, ruling P2-5: nothing is renamed outside the migration): only while
48
- * no run has finished (the first run's own execution), an org skill of the org's own (no
49
- * provenance) whose name a platform skill on in its org has stays the org's: it is renamed (`<name>-org`, `-org-2`, …), the org admins get an inbox notice
50
- * (`skill_renamed`) and that platform skill is switched off in the org (a switch the org already
51
- * has is kept). Agents select skills by id, so they keep it; the name changes in new runs. Once a
52
- * run has finished, a clash is only logged as a warning and left alone (the org's skill wins);
53
- * 4. an org agent template links to the platform template of its plugin and name, pinned like
54
- * skills (definitions compared as canonical JSON). Templates are never renamed: an org template
55
- * always has its plugin's type, which is the platform template's;
56
- * 5. a connector made from a connector template (the install ledger's `template_id`) links to that
57
- * platform connector template (ruling E2).
58
- * Plugin switches exist since phase 1 and new platform rows follow the plugin's scope, so nothing
59
- * else is written. Writes run per org under its row lock and re-check each row; nothing is deleted
60
- * or overwritten except the renamed skill's name. Each run is recorded in `platform_migration_runs`
61
- * (rows older than 30 days are pruned, except the first finished run: it marks the renames done).
62
- */
63
- export declare class PlatformMigration {
64
- private readonly kdb;
65
- private readonly orgLock;
66
- private readonly platform;
67
- readonly txHost: TransactionHost;
68
- readonly config: KernelConfigService;
69
- readonly lockService: LockService;
70
- readonly log: KernelLogger;
71
- constructor(kdb: KernelDb, orgLock: OrgLock, platform: PlatformComponents, txHost: TransactionHost, config: KernelConfigService, lockService: LockService, log: KernelLogger);
72
- private get db();
73
- /** The scheduled run (see the class). */
74
- migrate(signal?: AbortSignal): Promise<void>;
75
- /**
76
- * One run of every step; returns what it changed. `signal` (the stop, or a lost run lock) is
77
- * read between items and orgs: an aborted run stops there and its row is deleted (every step is
78
- * idempotent, so the next run carries on; an unfinished run never counts as the first one).
79
- */
80
- runOnce(signal?: AbortSignal): Promise<PlatformMigrationStats>;
81
- /** Deletes finished runs older than {@link RUNS_KEPT_MS}, never the first one (`first`). */
82
- private pruneRuns;
83
- /**
84
- * Step 1: the live catalog items whose platform components were never written
85
- * (`platform_synced_version` null; `syncFromVersion` sets it). An item whose components were
86
- * removed since is not synced again.
87
- */
88
- private syncItems;
89
- /** The highest version of the item installed in an org (and still stored), else `latest`. */
90
- private platformVersion;
91
- /** Step 2: org skill copies with plugin provenance, not linked yet. */
92
- private linkSkills;
93
- /** Links `links` of one org under its row lock (each only while it is still as read). */
94
- private linkSkillsIn;
95
- /**
96
- * Step 3 (one-shot): org skills of the org's own whose name a platform skill on in their org has,
97
- * renamed only in the first run (`rename`: no run has finished yet); later ones are only logged.
98
- */
99
- private renameClashes;
100
- /**
101
- * Renames the clashing skills of one org under its row lock (each only while it still has the
102
- * name and no provenance), switches the clashing platform skills off in the org (unless the org
103
- * has a switch for them already), and writes one inbox notice and one audit event per skill.
104
- */
105
- private renameIn;
106
- /** Step 4: org agent templates not linked yet, by plugin and name. */
107
- private linkTemplates;
108
- /** Links `links` of one org under its row lock (each only while it is still as read). */
109
- private linkTemplatesIn;
110
- /**
111
- * Step 5: connectors not linked yet whose install ledger row names the connector template they
112
- * were made from; one statement (a connector only gains a link, nothing else changes).
113
- */
114
- private linkConnectors;
115
- }