@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
@@ -9,7 +9,6 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
9
9
  };
10
10
  import { randomUUID } from "node:crypto";
11
11
  import { CHANNEL_EXTENSION_POINT } from "@capybara-hq/plugin-sdk";
12
- import { getQueueToken } from "@nestjs/bullmq";
13
12
  import { Inject, Injectable } from "@nestjs/common";
14
13
  import { AgentLookup } from "../agents/agent-lookup.js";
15
14
  import { ConversationAccess } from "../conversations/access.js";
@@ -18,16 +17,15 @@ import { ApiError } from "../errors.js";
18
17
  import { ExtensionRegistry } from "../extensions/registry.js";
19
18
  import { audit } from "../identity/audit.js";
20
19
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
21
- import { afterCommit } from "../infra/database/after-commit.js";
22
20
  import { KernelDb } from "../infra/database/kernel-db.js";
23
21
  import { TransactionHost } from "../infra/database/transaction-host.js";
24
22
  import { Transactional } from "../infra/database/transactional.decorator.js";
23
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
25
24
  import { KERNEL_LOGGER } from "../infra/tokens.js";
26
25
  import { DevicePolicyService } from "../local-devices/device-policy.service.js";
27
26
  import { orgAllows } from "../local-devices/network-grants.js";
28
27
  import { OrgLock } from "../orgs/org-lock.js";
29
28
  import { can, roleAtLeast } from "../orgs/roles.js";
30
- import { MAINTENANCE_QUEUE_NAME } from "../runtime/maintenance-queue.js";
31
29
  import { ProviderRegistry } from "../runtime/providers.js";
32
30
  import { SandboxService } from "../sandbox/sandbox.service.js";
33
31
  import { isTimeZone } from "../time-zone.js";
@@ -36,7 +34,7 @@ import { assembleCalendar, CALENDAR_LIMITS, CALENDAR_RANGE_DAYS, CALENDAR_TASK_R
36
34
  import { readScheduleSettings } from "./effective-settings.js";
37
35
  import { grantNotAllowed, scheduleDisabled, scheduleForbidden, scheduleNotFound, targetInvalid, } from "./errors.js";
38
36
  import { grantRefusal, grantRefusalOf, isNetworkGrantKey, isPolicyGrantKey, isTierGrantKey, NETWORK_GRANT_PREFIX, NETWORK_GRANT_SPEC_HASH, networkGrantHost, networkGrantRefusal, TIER_GRANT_SPEC_HASH, tierGrantRefusal, } from "./grant-rules.js";
39
- import { schedFireJob } from "./job-ids.js";
37
+ import { oweSchedFire, oweSchedSettleUnfired } from "./job-ids.js";
40
38
  import { pauseTaskIn } from "./pause.js";
41
39
  import { jitterForGap, minGapMinutes, nextTick, validateTrigger } from "./schedule-math.js";
42
40
  import { ScheduledTaskStore } from "./store.js";
@@ -75,14 +73,14 @@ let ScheduledTasksService = class ScheduledTasksService {
75
73
  extensions;
76
74
  providers;
77
75
  log;
78
- maintenance;
76
+ outbox;
79
77
  sandbox;
80
78
  devicePolicy;
81
79
  constructor(db, txHost, config, store, orgLock, agents, access, tools, extensions,
82
80
  /** A task's model must resolve for the org and declare its effort (as a message's switch). */
83
81
  providers, log,
84
- /** "Run now" enqueues the fire job (consumed by the worker role). */
85
- maintenance,
82
+ /** "Run now" owes the fire job (consumed by the worker role). */
83
+ outbox,
86
84
  /** Network grants: the org's effective egress (network ask-to-allow spec §8.1). */
87
85
  sandbox,
88
86
  /** Network grants: the task's device's local policy summary (`remoteGrants`). */
@@ -98,7 +96,7 @@ let ScheduledTasksService = class ScheduledTasksService {
98
96
  this.extensions = extensions;
99
97
  this.providers = providers;
100
98
  this.log = log;
101
- this.maintenance = maintenance;
99
+ this.outbox = outbox;
102
100
  this.sandbox = sandbox;
103
101
  this.devicePolicy = devicePolicy;
104
102
  }
@@ -525,6 +523,8 @@ let ScheduledTasksService = class ScheduledTasksService {
525
523
  disabledTools: d.disabledTools ?? [],
526
524
  subagentTools: d.subagentTools,
527
525
  subagents: d.subagents,
526
+ // no conversation at hand: a web one's tools; a run elsewhere still refuses by spec.sources
527
+ source: "web",
528
528
  });
529
529
  const ctx = {
530
530
  role,
@@ -789,6 +789,8 @@ let ScheduledTasksService = class ScheduledTasksService {
789
789
  disabledTools: d.disabledTools ?? [],
790
790
  subagentTools: d.subagentTools,
791
791
  subagents: d.subagents,
792
+ // no conversation at hand: a web one's tools; a run elsewhere still refuses by spec.sources
793
+ source: "web",
792
794
  });
793
795
  return new Map(tools.map((x) => [
794
796
  x.key,
@@ -820,7 +822,7 @@ let ScheduledTasksService = class ScheduledTasksService {
820
822
  /**
821
823
  * "Run now" (spec §6), creator only: a task run for the request time, outside the schedule (no
822
824
  * unique-key conflict with a tick, and `next_run_at` does not move), fired at once through the
823
- * fire job after the commit. `trial`: an attended trial run whose approvals become the task's
825
+ * fire job its transaction owes. `trial`: an attended trial run whose approvals become the task's
824
826
  * grants (§7). Counted in the rate limit like any run. An ended task cannot run; a paused one
825
827
  * can (only its creator gets here: the run is `manual`, which the fire's paused check lets by).
826
828
  */
@@ -850,10 +852,9 @@ let ScheduledTasksService = class ScheduledTasksService {
850
852
  target: task.id,
851
853
  detail: { taskRunId: run.id, trial: opts.trial },
852
854
  });
853
- const job = schedFireJob(run.id);
854
- afterCommit(async () => {
855
- await this.maintenance.add(job.name, job.data, job.opts);
856
- });
855
+ await oweSchedFire(this.outbox, run.id, orgId);
856
+ // a fire job that fails for good before its claim leaves the run to this check (`removed`)
857
+ await oweSchedSettleUnfired(this.outbox, run.id, orgId, run.scheduledFor, null);
857
858
  return run;
858
859
  }
859
860
  /** The live role of a member of the org; not a member → `schedule.not_found`. */
@@ -1083,6 +1084,8 @@ let ScheduledTasksService = class ScheduledTasksService {
1083
1084
  disabledTools: d.disabledTools ?? [],
1084
1085
  subagentTools: d.subagentTools,
1085
1086
  subagents: d.subagents,
1087
+ // no conversation at hand: a web one's tools; a run elsewhere still refuses by spec.sources
1088
+ source: "web",
1086
1089
  });
1087
1090
  for (const g of toolGrants) {
1088
1091
  const tool = tools.find((x) => x.key === g.toolKey);
@@ -1168,7 +1171,7 @@ ScheduledTasksService = __decorate([
1168
1171
  __param(8, Inject(ExtensionRegistry)),
1169
1172
  __param(9, Inject(ProviderRegistry)),
1170
1173
  __param(10, Inject(KERNEL_LOGGER)),
1171
- __param(11, Inject(getQueueToken(MAINTENANCE_QUEUE_NAME))),
1174
+ __param(11, Inject(OutboxService)),
1172
1175
  __param(12, Inject(SandboxService)),
1173
1176
  __param(13, Inject(DevicePolicyService))
1174
1177
  ], ScheduledTasksService);
@@ -11,10 +11,8 @@ import { Inject, Module } from "@nestjs/common";
11
11
  import { AgentLookupModule } from "../agents/agent-lookup.js";
12
12
  import { ConversationAccessModule } from "../conversations/access.js";
13
13
  import { ExtensionRegistryModule } from "../extensions/registry.js";
14
- import { QueueModule } from "../infra/queue/queue.module.js";
15
14
  import { LocalDevicesModule } from "../local-devices/local-devices.module.js";
16
15
  import { OrgLockModule } from "../orgs/org-lock.js";
17
- import { MAINTENANCE_QUEUE_NAME } from "../runtime/maintenance-queue.js";
18
16
  import { ProvidersModule } from "../runtime/providers.js";
19
17
  import { SandboxModule } from "../sandbox/sandbox.module.js";
20
18
  import { ScheduleToolsPort } from "../tools/builtins/schedules.js";
@@ -53,8 +51,6 @@ SchedulesModule = __decorate([
53
51
  // network grants: the org's effective egress and the task device's policy summary
54
52
  SandboxModule,
55
53
  LocalDevicesModule,
56
- // "run now" enqueues the fire job
57
- QueueModule.registerQueue(MAINTENANCE_QUEUE_NAME),
58
54
  ],
59
55
  providers: [ScheduledTasksService],
60
56
  exports: [ScheduledTasksService, ScheduledTaskStoreModule],
@@ -40,7 +40,7 @@ export declare class InboxController {
40
40
  private readonly inbox;
41
41
  constructor(inbox: InboxService);
42
42
  list(principal: Principal, org: OrgContext): Promise<InboxView>;
43
- /** An org admin dismisses an org notice (`skill_renamed`) -> 204. */
43
+ /** An org admin dismisses an org notice -> 204. */
44
44
  dismissNotice(principal: Principal, org: OrgContext, params: z.infer<typeof NoticeParamsSchema>): Promise<void>;
45
45
  }
46
46
  export {};
@@ -86,7 +86,7 @@ let InboxController = class InboxController {
86
86
  const p = requireUser(principal);
87
87
  return this.inbox.list(org.orgId, p.account.id);
88
88
  }
89
- /** An org admin dismisses an org notice (`skill_renamed`) -> 204. */
89
+ /** An org admin dismisses an org notice -> 204. */
90
90
  async dismissNotice(principal, org, params) {
91
91
  const p = requireUser(principal);
92
92
  await this.inbox.dismissNotice(org.orgId, p.account.id, params.id);
@@ -338,11 +338,22 @@ export declare class ApprovalsService {
338
338
  */
339
339
  cancelPendingIn(runId: string): Promise<ApprovalRecord[]>;
340
340
  /**
341
- * 在调用方的事务内(`mandatory`)取消某组织里指定由 `accountId` 处理(`requested_for`)的全部 pending 审批(成员离职),
342
- * 走与 {@link cancelPendingIn} 相同的取消路径:置 cancelled、写 `approval_decided{cancelled}`(处理人为 null)。
343
- * 返回被取消的记录与各会话写到的最大 seq;调用方提交后发布事件(并可续跑对应运行)。
341
+ * The conversations of `orgId` holding a pending approval `accountId` is asked to decide
342
+ * (`requested_for`), read without locks: offboarding locks them, by tree root first
343
+ * (`lockConversationTrees`), before {@link cancelPendingFor}.
344
344
  */
345
- cancelPendingFor(orgId: string, accountId: string): Promise<{
345
+ pendingConversationsFor(orgId: string, accountId: string): Promise<string[]>;
346
+ /**
347
+ * In the caller's transaction (`mandatory`; offboarding): cancels every pending approval of
348
+ * `orgId` that `accountId` is asked to decide (`requested_for`) in `conversationIds`, the
349
+ * conversations the caller already locked (by tree root first, `lockConversationTrees`; lock
350
+ * order: conversations, then approval rows), along the same path as {@link cancelPendingIn}:
351
+ * `cancelled`, `approval_decided{cancelled}` with no decider. An approval asked meanwhile in
352
+ * another conversation is left to its run's stop, so no conversation is locked after a row.
353
+ * Returns the cancelled records and the highest seq written per conversation; the caller
354
+ * publishes after the commit.
355
+ */
356
+ cancelPendingFor(orgId: string, accountId: string, conversationIds: readonly string[]): Promise<{
346
357
  records: ApprovalRecord[];
347
358
  seqs: Map<string, number>;
348
359
  }>;
@@ -13,6 +13,7 @@ import { Inject, Injectable } from "@nestjs/common";
13
13
  import { sql } from "kysely";
14
14
  import { EventLog } from "../conversations/event-log.js";
15
15
  import { NETWORK_ASK_MAX_HOSTS, NETWORK_HOST_MAX_CHARS, } from "../conversations/events.js";
16
+ import { conversationSourceOf } from "../conversations/source.js";
16
17
  import { ApiError } from "../errors.js";
17
18
  import { audit } from "../identity/audit.js";
18
19
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
@@ -814,6 +815,8 @@ let ApprovalsService = class ApprovalsService {
814
815
  disabledTools: def?.disabledTools ?? [],
815
816
  subagentTools: def?.subagentTools,
816
817
  subagents: def?.subagents,
818
+ // the tools of the trial run's conversation, where the approval was asked
819
+ source: await conversationSourceOf(t, a.conversationId),
817
820
  });
818
821
  const tool = tools.find((x) => x.key === a.toolKey);
819
822
  const { forbiddenTools } = await readScheduleSettings(t, this.config, a.orgId);
@@ -975,21 +978,34 @@ let ApprovalsService = class ApprovalsService {
975
978
  return out;
976
979
  }
977
980
  /**
978
- * 在调用方的事务内(`mandatory`)取消某组织里指定由 `accountId` 处理(`requested_for`)的全部 pending 审批(成员离职),
979
- * 走与 {@link cancelPendingIn} 相同的取消路径:置 cancelled、写 `approval_decided{cancelled}`(处理人为 null)。
980
- * 返回被取消的记录与各会话写到的最大 seq;调用方提交后发布事件(并可续跑对应运行)。
981
+ * The conversations of `orgId` holding a pending approval `accountId` is asked to decide
982
+ * (`requested_for`), read without locks: offboarding locks them, by tree root first
983
+ * (`lockConversationTrees`), before {@link cancelPendingFor}.
981
984
  */
982
- async cancelPendingFor(orgId, accountId) {
983
- const t = this.db.db;
984
- // the conversations first, in id order (lock order: conversation, then approval rows)
985
- const convs = await this.lockConversations(t
985
+ async pendingConversationsFor(orgId, accountId) {
986
+ const rows = await this.db
986
987
  .selectFrom("approvals")
987
988
  .select("conversation_id")
989
+ .distinct()
988
990
  .where("org_id", "=", orgId)
989
991
  .where("requested_for", "=", accountId)
990
- .where("status", "=", "pending"));
991
- // only the conversations locked above: an approval asked meanwhile in another one is left to
992
- // the run's stop or the sweep, so no conversation is locked after a row
992
+ .where("status", "=", "pending")
993
+ .execute();
994
+ return rows.map((r) => r.conversation_id);
995
+ }
996
+ /**
997
+ * In the caller's transaction (`mandatory`; offboarding): cancels every pending approval of
998
+ * `orgId` that `accountId` is asked to decide (`requested_for`) in `conversationIds`, the
999
+ * conversations the caller already locked (by tree root first, `lockConversationTrees`; lock
1000
+ * order: conversations, then approval rows), along the same path as {@link cancelPendingIn}:
1001
+ * `cancelled`, `approval_decided{cancelled}` with no decider. An approval asked meanwhile in
1002
+ * another conversation is left to its run's stop, so no conversation is locked after a row.
1003
+ * Returns the cancelled records and the highest seq written per conversation; the caller
1004
+ * publishes after the commit.
1005
+ */
1006
+ async cancelPendingFor(orgId, accountId, conversationIds) {
1007
+ const t = this.db.db;
1008
+ const convs = [...conversationIds];
993
1009
  if (convs.length === 0)
994
1010
  return { records: [], seqs: new Map() };
995
1011
  const rows = await t
@@ -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;
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.25",
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.19"
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'));