@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
@@ -1,4 +1,5 @@
1
- /** The scheduled tasks' jobs (a leaf module: no imports). */
1
+ import type { OutboxService } from "../infra/outbox/outbox.service.js";
2
+ /** The scheduled tasks' jobs (a leaf module: type imports only). */
2
3
  /**
3
4
  * The once-a-minute `@LockedCron` that turns due tasks into task runs (spec §6); its name enqueued
4
5
  * on the maintenance queue runs one scan (`ScheduledRuns.runNow`).
@@ -13,20 +14,68 @@ export interface SchedFireJobData {
13
14
  /** The fire job's id: `sched-fire-<taskRunId>` (no `:`, task run ids are UUIDs). */
14
15
  export declare const schedFireJobId: (taskRunId: string) => string;
15
16
  /**
16
- * The fire job of one task run. Removed when done, so the scanner's repair can enqueue it again
17
- * after a lost job; the fire is idempotent on the task run's status, so retries are safe.
18
- */
19
- export declare function schedFireJob(taskRunId: string): {
20
- name: string;
21
- data: SchedFireJobData;
22
- opts: {
23
- jobId: string;
24
- attempts: number;
25
- backoff: {
26
- type: "exponential";
27
- delay: number;
28
- };
29
- removeOnComplete: true;
30
- removeOnFail: true;
31
- };
17
+ * Owes the fire job of task run `taskRunId` in the transaction in context (transactional outbox
18
+ * spec §8.1): the transaction that writes a pending task run (the scan's tick, a person's "run
19
+ * now") owes its fire, so a tick is never left unfired. The job's options are its outbox registry
20
+ * entry's; the fire is idempotent on the task run's status.
21
+ */
22
+ export declare function oweSchedFire(outbox: Pick<OutboxService, "add">, taskRunId: string, orgId: string): Promise<boolean>;
23
+ /**
24
+ * The delayed job that settles a task run left pending (ruling R26, fix round 1), on
25
+ * `cap-kernel-maintenance`. Two kinds:
26
+ * - the claimed check, owed by the fire job's claim: the run tracker links, settles or releases a
27
+ * run claimed but never started (`sched-settle-<taskRunId>`, then `-<check>`);
28
+ * - the unfired check, owed with the task run itself (the scan's tick, "run now"): a run its fire
29
+ * job never claimed (the job failed for good before its claim) is skipped, `missed` for a tick,
30
+ * `removed` for a "run now", so it never blocks the task's later ticks as an overlap
31
+ * (`sched-settle-<taskRunId>-unfired`).
32
+ */
33
+ export declare const SCHED_SETTLE_JOB = "sched-settle";
34
+ /**
35
+ * The data of a {@link SCHED_SETTLE_JOB}: the claimed check (`check` counts the checks before this
36
+ * one) or the unfired check.
37
+ */
38
+ export type SchedSettleJobData = {
39
+ taskRunId: string;
40
+ check: number;
41
+ } | {
42
+ taskRunId: string;
43
+ unfired: true;
32
44
  };
45
+ /**
46
+ * The first claimed check: 10 minutes after the claim, when the start's retries (seconds) are long
47
+ * over and the run tracker's start grace (`START_GRACE_MS`, 5 minutes) has passed, so a run that
48
+ * left no trace is released at once. Also the unfired check's delay after a "run now".
49
+ */
50
+ export declare const SCHED_SETTLE_AFTER_MS: number;
51
+ /**
52
+ * A claimed run still in flight at a check (its item queued, its message posted and not yet taken
53
+ * by a run) is checked again hourly. The chain ends with the first check that finds the run
54
+ * started, ended or released: the queued item is consumed or removed, the message taken by a run.
55
+ * A chain past {@link SCHED_SETTLE_LONG_CHECKS} checks (a week) is logged as a warning, once.
56
+ */
57
+ export declare const SCHED_SETTLE_RECHECK_MS: number;
58
+ /** A week of hourly checks: the claimed check warns at this one (a run in flight that long). */
59
+ export declare const SCHED_SETTLE_LONG_CHECKS = 168;
60
+ /**
61
+ * How long past the org's catch-up window a tick's fire may come before the tick is missed (the
62
+ * fire job's own rule, and when a tick's unfired check runs): the scan fires a tick still inside
63
+ * the window, and its fire job follows within seconds.
64
+ */
65
+ export declare const SCHED_LATE_FIRE_SLACK_MS: number;
66
+ /** The claimed check's id: `sched-settle-<taskRunId>`, then `-<check>` for the later checks. */
67
+ export declare const schedSettleJobId: (taskRunId: string, check: number) => string;
68
+ /** The unfired check's id: `sched-settle-<taskRunId>-unfired`. */
69
+ export declare const schedSettleUnfiredJobId: (taskRunId: string) => string;
70
+ /**
71
+ * Owes claimed check number `check` of task run `taskRunId` in the transaction in context, due
72
+ * `after` milliseconds from now.
73
+ */
74
+ export declare function oweSchedSettle(outbox: Pick<OutboxService, "add">, taskRunId: string, orgId: string, check: number, after: number): Promise<boolean>;
75
+ /**
76
+ * Owes the unfired check of task run `taskRunId` in the transaction that writes the run: for a
77
+ * tick of the schedule (`catchUpWindowMinutes` given) when the fire job would itself call the
78
+ * tick missed (its tick plus the window plus {@link SCHED_LATE_FIRE_SLACK_MS}, at least
79
+ * {@link SCHED_SETTLE_AFTER_MS} on); for a "run now" (null) {@link SCHED_SETTLE_AFTER_MS} on.
80
+ */
81
+ export declare function oweSchedSettleUnfired(outbox: Pick<OutboxService, "add">, taskRunId: string, orgId: string, scheduledFor: Date, catchUpWindowMinutes: number | null): Promise<boolean>;
@@ -1,4 +1,4 @@
1
- /** The scheduled tasks' jobs (a leaf module: no imports). */
1
+ /** The scheduled tasks' jobs (a leaf module: type imports only). */
2
2
  /**
3
3
  * The once-a-minute `@LockedCron` that turns due tasks into task runs (spec §6); its name enqueued
4
4
  * on the maintenance queue runs one scan (`ScheduledRuns.runNow`).
@@ -9,19 +9,87 @@ export const SCHED_FIRE_JOB = "sched-fire";
9
9
  /** The fire job's id: `sched-fire-<taskRunId>` (no `:`, task run ids are UUIDs). */
10
10
  export const schedFireJobId = (taskRunId) => `${SCHED_FIRE_JOB}-${taskRunId}`;
11
11
  /**
12
- * The fire job of one task run. Removed when done, so the scanner's repair can enqueue it again
13
- * after a lost job; the fire is idempotent on the task run's status, so retries are safe.
12
+ * Owes the fire job of task run `taskRunId` in the transaction in context (transactional outbox
13
+ * spec §8.1): the transaction that writes a pending task run (the scan's tick, a person's "run
14
+ * now") owes its fire, so a tick is never left unfired. The job's options are its outbox registry
15
+ * entry's; the fire is idempotent on the task run's status.
14
16
  */
15
- export function schedFireJob(taskRunId) {
16
- return {
17
+ export function oweSchedFire(outbox, taskRunId, orgId) {
18
+ return outbox.add({
19
+ queue: "cap-kernel-maintenance",
17
20
  name: SCHED_FIRE_JOB,
21
+ jobId: schedFireJobId(taskRunId),
18
22
  data: { taskRunId },
19
- opts: {
20
- jobId: schedFireJobId(taskRunId),
21
- attempts: 3,
22
- backoff: { type: "exponential", delay: 2_000 },
23
- removeOnComplete: true,
24
- removeOnFail: true,
25
- },
26
- };
23
+ orgId,
24
+ });
25
+ }
26
+ /**
27
+ * The delayed job that settles a task run left pending (ruling R26, fix round 1), on
28
+ * `cap-kernel-maintenance`. Two kinds:
29
+ * - the claimed check, owed by the fire job's claim: the run tracker links, settles or releases a
30
+ * run claimed but never started (`sched-settle-<taskRunId>`, then `-<check>`);
31
+ * - the unfired check, owed with the task run itself (the scan's tick, "run now"): a run its fire
32
+ * job never claimed (the job failed for good before its claim) is skipped, `missed` for a tick,
33
+ * `removed` for a "run now", so it never blocks the task's later ticks as an overlap
34
+ * (`sched-settle-<taskRunId>-unfired`).
35
+ */
36
+ export const SCHED_SETTLE_JOB = "sched-settle";
37
+ /**
38
+ * The first claimed check: 10 minutes after the claim, when the start's retries (seconds) are long
39
+ * over and the run tracker's start grace (`START_GRACE_MS`, 5 minutes) has passed, so a run that
40
+ * left no trace is released at once. Also the unfired check's delay after a "run now".
41
+ */
42
+ export const SCHED_SETTLE_AFTER_MS = 10 * 60_000;
43
+ /**
44
+ * A claimed run still in flight at a check (its item queued, its message posted and not yet taken
45
+ * by a run) is checked again hourly. The chain ends with the first check that finds the run
46
+ * started, ended or released: the queued item is consumed or removed, the message taken by a run.
47
+ * A chain past {@link SCHED_SETTLE_LONG_CHECKS} checks (a week) is logged as a warning, once.
48
+ */
49
+ export const SCHED_SETTLE_RECHECK_MS = 60 * 60_000;
50
+ /** A week of hourly checks: the claimed check warns at this one (a run in flight that long). */
51
+ export const SCHED_SETTLE_LONG_CHECKS = 168;
52
+ /**
53
+ * How long past the org's catch-up window a tick's fire may come before the tick is missed (the
54
+ * fire job's own rule, and when a tick's unfired check runs): the scan fires a tick still inside
55
+ * the window, and its fire job follows within seconds.
56
+ */
57
+ export const SCHED_LATE_FIRE_SLACK_MS = 10 * 60_000;
58
+ /** The claimed check's id: `sched-settle-<taskRunId>`, then `-<check>` for the later checks. */
59
+ export const schedSettleJobId = (taskRunId, check) => check === 0 ? `${SCHED_SETTLE_JOB}-${taskRunId}` : `${SCHED_SETTLE_JOB}-${taskRunId}-${check}`;
60
+ /** The unfired check's id: `sched-settle-<taskRunId>-unfired`. */
61
+ export const schedSettleUnfiredJobId = (taskRunId) => `${SCHED_SETTLE_JOB}-${taskRunId}-unfired`;
62
+ /**
63
+ * Owes claimed check number `check` of task run `taskRunId` in the transaction in context, due
64
+ * `after` milliseconds from now.
65
+ */
66
+ export function oweSchedSettle(outbox, taskRunId, orgId, check, after) {
67
+ return outbox.add({
68
+ queue: "cap-kernel-maintenance",
69
+ name: SCHED_SETTLE_JOB,
70
+ jobId: schedSettleJobId(taskRunId, check),
71
+ data: { taskRunId, check },
72
+ runAt: new Date(Date.now() + after),
73
+ orgId,
74
+ });
75
+ }
76
+ /**
77
+ * Owes the unfired check of task run `taskRunId` in the transaction that writes the run: for a
78
+ * tick of the schedule (`catchUpWindowMinutes` given) when the fire job would itself call the
79
+ * tick missed (its tick plus the window plus {@link SCHED_LATE_FIRE_SLACK_MS}, at least
80
+ * {@link SCHED_SETTLE_AFTER_MS} on); for a "run now" (null) {@link SCHED_SETTLE_AFTER_MS} on.
81
+ */
82
+ export function oweSchedSettleUnfired(outbox, taskRunId, orgId, scheduledFor, catchUpWindowMinutes) {
83
+ const soonest = Date.now() + SCHED_SETTLE_AFTER_MS;
84
+ const at = catchUpWindowMinutes === null
85
+ ? soonest
86
+ : Math.max(soonest, scheduledFor.getTime() + catchUpWindowMinutes * 60_000 + SCHED_LATE_FIRE_SLACK_MS);
87
+ return outbox.add({
88
+ queue: "cap-kernel-maintenance",
89
+ name: SCHED_SETTLE_JOB,
90
+ jobId: schedSettleUnfiredJobId(taskRunId),
91
+ data: { taskRunId, unfired: true },
92
+ runAt: new Date(at),
93
+ orgId,
94
+ });
27
95
  }
@@ -114,7 +114,8 @@ let ScheduledTaskRunTracker = class ScheduledTaskRunTracker {
114
114
  await this.settle(tr, run.status);
115
115
  return false;
116
116
  }
117
- // not started yet: its fire job is still to come (the scanner repairs a lost one)
117
+ // not claimed yet: its fire job is still to come (the run's unfired settle check skips it
118
+ // when the job failed for good before its claim)
118
119
  if (tr.fired_at === null)
119
120
  return true;
120
121
  // only a run with a target conversation is ever queued (`conversation`, `chat`): no scan of
@@ -1,10 +1,9 @@
1
- import type { Queue } from "bullmq";
2
1
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
3
2
  import { KernelDb } from "../infra/database/kernel-db.js";
4
3
  import { TransactionHost } from "../infra/database/transaction-host.js";
5
4
  import { LockService } from "../infra/lock/lock.service.js";
5
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
6
6
  import type { KernelLogger } from "../log.js";
7
- import { ScheduledTaskRunTracker } from "./run-tracker.js";
8
7
  import { ScheduledTaskStore } from "./store.js";
9
8
  /**
10
9
  * The scheduled task scanner (spec §6), once a minute across replicas. Each due task in its own
@@ -12,34 +11,27 @@ import { ScheduledTaskStore } from "./store.js";
12
11
  * same task): the ticks missed outside the org's `catchUpWindowMinutes` become one `skipped` run
13
12
  * (`missed`, at the first missed tick), the latest tick in the window gets its run (the unique
14
13
  * `(task_id, scheduled_for)` key makes a second insert a no-op), `next_run_at` moves to the next
15
- * tick plus the task's jitter (or a `once` task ends, or one past its `expires_at`), and after the
16
- * commit `sched-fire-<taskRunId>` is enqueued. A task whose stored trigger the schedule math cannot
17
- * evaluate is paused (`invalid_schedule`) instead of failing the scan; a database error fails the
18
- * pass (the next minute retries) and pauses nothing.
19
- *
20
- * Then the repair: a run still `pending` and unfired 10 minutes after its tick has its fire job
21
- * enqueued again, and is given up (`skipped`, `missed`) after an hour; a run fired an hour ago and
22
- * still `pending` (its start failed for good) goes through the run tracker's check, which links,
23
- * settles or releases it, so a `once` task's last run never stays pending.
14
+ * tick plus the task's jitter (or a `once` task ends, or one past its `expires_at`), and the same
15
+ * transaction owes `sched-fire-<taskRunId>` (transactional outbox spec §8.1), so a written tick is
16
+ * never left unfired and the scan needs no repair of lost fire jobs (a fire delivered too late
17
+ * marks its tick missed itself, `ScheduledTaskFirer`), and the run's unfired settle check (a fire
18
+ * job that failed for good before its claim: the tick is missed, `job-ids.ts`). A task whose stored trigger the schedule
19
+ * math cannot evaluate is paused (`invalid_schedule`) instead of failing the scan; a database
20
+ * error fails the pass (the next minute retries) and pauses nothing.
24
21
  */
25
22
  export declare class ScheduledTaskScanJob {
26
23
  private readonly kdb;
27
24
  readonly txHost: TransactionHost;
28
25
  private readonly config;
29
26
  private readonly store;
30
- private readonly tracker;
31
- private readonly maintenance;
27
+ private readonly outbox;
32
28
  readonly lockService: LockService;
33
29
  readonly log: KernelLogger;
34
- constructor(kdb: KernelDb, txHost: TransactionHost, config: Pick<KernelConfigService, "get">, store: ScheduledTaskStore, tracker: Pick<ScheduledTaskRunTracker, "inFlightIn">, maintenance: Pick<Queue, "add">, lockService: LockService, log: KernelLogger);
30
+ constructor(kdb: KernelDb, txHost: TransactionHost, config: Pick<KernelConfigService, "get">, store: ScheduledTaskStore, outbox: Pick<OutboxService, "add">, lockService: LockService, log: KernelLogger);
35
31
  private get db();
36
32
  scan(): Promise<void>;
37
- /**
38
- * One scan as of `now` (tests pass their own); returns how many due tasks it handled. The repair
39
- * runs even when the scan loop failed (its error is rethrown afterwards). `repairBatch`: how many
40
- * rows each repair step takes (tests pass a small one).
41
- */
42
- scanAt(now: Date, repairBatch?: number): Promise<number>;
33
+ /** One scan as of `now` (tests pass their own); returns how many due tasks it handled. */
34
+ scanAt(now: Date): Promise<number>;
43
35
  /**
44
36
  * One due task in its own transaction (a task whose trigger cannot be evaluated rolls back
45
37
  * alone and is paused, so it cannot block the others): locked with `SKIP LOCKED`, so replicas take different tasks. False
@@ -48,17 +40,6 @@ export declare class ScheduledTaskScanJob {
48
40
  private scanOne;
49
41
  /** A task the scan cannot advance stops being due: paused with `invalid_schedule`. */
50
42
  private quarantine;
51
- /** One due task (see the class); returns the id of the run to fire, if any. */
43
+ /** One due task (see the class); returns the run to fire, if any. */
52
44
  private advance;
53
- /** Enqueues the fire jobs (after the commit; a failure is repaired by a later scan). */
54
- private enqueue;
55
- /** Lost fire jobs (see the class). */
56
- private repair;
57
- /** A run never fired an hour after its tick: given up, task row first (the one lock order). */
58
- private giveUp;
59
- /**
60
- * A started run still pending long after: the tracker links, settles or releases it; one still
61
- * in flight is stamped (`rechecked_at`) and goes to the back of the next rechecks.
62
- */
63
- private recheck;
64
45
  }
@@ -8,31 +8,22 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
10
  import { randomUUID } from "node:crypto";
11
- import { getQueueToken } from "@nestjs/bullmq";
12
11
  import { Inject, Injectable } from "@nestjs/common";
13
- import { sql } from "kysely";
14
12
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
15
- import { afterCommit } from "../infra/database/after-commit.js";
16
13
  import { KernelDb } from "../infra/database/kernel-db.js";
17
14
  import { TransactionHost } from "../infra/database/transaction-host.js";
18
15
  import { Transactional } from "../infra/database/transactional.decorator.js";
19
16
  import { LockService } from "../infra/lock/lock.service.js";
17
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
20
18
  import { LockedCron } from "../infra/schedule/locked-cron.decorator.js";
21
19
  import { KERNEL_LOGGER } from "../infra/tokens.js";
22
- import { MAINTENANCE_QUEUE_NAME } from "../runtime/maintenance-queue.js";
23
20
  import { readScheduleSettings } from "./effective-settings.js";
24
- import { SCHED_SCAN_JOB, schedFireJob } from "./job-ids.js";
21
+ import { oweSchedFire, oweSchedSettleUnfired, SCHED_SCAN_JOB } from "./job-ids.js";
25
22
  import { pauseTaskIn } from "./pause.js";
26
- import { lockTaskRow, ScheduledTaskRunTracker } from "./run-tracker.js";
27
23
  import { scanStep } from "./schedule-math.js";
28
24
  import { ScheduledTaskStore } from "./store.js";
29
25
  /** Due tasks per scan; what is left waits for the next minute. */
30
26
  const MAX_TASKS = 2000;
31
- /** A pending task run never fired this long after its tick: its fire job is enqueued again. */
32
- const REPAIR_AFTER_MS = 10 * 60_000;
33
- /** ... and this long after its tick it is given up (`skipped`, `missed`). */
34
- const GIVE_UP_AFTER_MS = 60 * 60_000;
35
- const REPAIR_BATCH = 100;
36
27
  /**
37
28
  * The schedule math could not evaluate a due task's stored trigger (its transaction rolled back);
38
29
  * the scan pauses it. Only the math raises it: a database error fails the scan pass instead (the
@@ -51,32 +42,28 @@ class TaskScanFailed extends Error {
51
42
  * same task): the ticks missed outside the org's `catchUpWindowMinutes` become one `skipped` run
52
43
  * (`missed`, at the first missed tick), the latest tick in the window gets its run (the unique
53
44
  * `(task_id, scheduled_for)` key makes a second insert a no-op), `next_run_at` moves to the next
54
- * tick plus the task's jitter (or a `once` task ends, or one past its `expires_at`), and after the
55
- * commit `sched-fire-<taskRunId>` is enqueued. A task whose stored trigger the schedule math cannot
56
- * evaluate is paused (`invalid_schedule`) instead of failing the scan; a database error fails the
57
- * pass (the next minute retries) and pauses nothing.
58
- *
59
- * Then the repair: a run still `pending` and unfired 10 minutes after its tick has its fire job
60
- * enqueued again, and is given up (`skipped`, `missed`) after an hour; a run fired an hour ago and
61
- * still `pending` (its start failed for good) goes through the run tracker's check, which links,
62
- * settles or releases it, so a `once` task's last run never stays pending.
45
+ * tick plus the task's jitter (or a `once` task ends, or one past its `expires_at`), and the same
46
+ * transaction owes `sched-fire-<taskRunId>` (transactional outbox spec §8.1), so a written tick is
47
+ * never left unfired and the scan needs no repair of lost fire jobs (a fire delivered too late
48
+ * marks its tick missed itself, `ScheduledTaskFirer`), and the run's unfired settle check (a fire
49
+ * job that failed for good before its claim: the tick is missed, `job-ids.ts`). A task whose stored trigger the schedule
50
+ * math cannot evaluate is paused (`invalid_schedule`) instead of failing the scan; a database
51
+ * error fails the pass (the next minute retries) and pauses nothing.
63
52
  */
64
53
  let ScheduledTaskScanJob = class ScheduledTaskScanJob {
65
54
  kdb;
66
55
  txHost;
67
56
  config;
68
57
  store;
69
- tracker;
70
- maintenance;
58
+ outbox;
71
59
  lockService;
72
60
  log;
73
- constructor(kdb, txHost, config, store, tracker, maintenance, lockService, log) {
61
+ constructor(kdb, txHost, config, store, outbox, lockService, log) {
74
62
  this.kdb = kdb;
75
63
  this.txHost = txHost;
76
64
  this.config = config;
77
65
  this.store = store;
78
- this.tracker = tracker;
79
- this.maintenance = maintenance;
66
+ this.outbox = outbox;
80
67
  this.lockService = lockService;
81
68
  this.log = log;
82
69
  }
@@ -86,34 +73,25 @@ let ScheduledTaskScanJob = class ScheduledTaskScanJob {
86
73
  async scan() {
87
74
  await this.scanAt(new Date());
88
75
  }
89
- /**
90
- * One scan as of `now` (tests pass their own); returns how many due tasks it handled. The repair
91
- * runs even when the scan loop failed (its error is rethrown afterwards). `repairBatch`: how many
92
- * rows each repair step takes (tests pass a small one).
93
- */
94
- async scanAt(now, repairBatch = REPAIR_BATCH) {
76
+ /** One scan as of `now` (tests pass their own); returns how many due tasks it handled. */
77
+ async scanAt(now) {
95
78
  const settings = new Map();
96
79
  let total = 0;
97
- try {
98
- for (; total < MAX_TASKS; total++) {
99
- try {
100
- if (!(await this.scanOne(now, settings)))
101
- break;
102
- }
103
- catch (err) {
104
- if (!(err instanceof TaskScanFailed))
105
- throw err;
106
- // one task that cannot be advanced must not hold the others back every minute
107
- this.log.error("scheduled task cannot be advanced; paused", {
108
- taskId: err.taskId,
109
- err: err.cause,
110
- });
111
- await this.quarantine(err.taskId);
112
- }
80
+ for (; total < MAX_TASKS; total++) {
81
+ try {
82
+ if (!(await this.scanOne(now, settings)))
83
+ break;
84
+ }
85
+ catch (err) {
86
+ if (!(err instanceof TaskScanFailed))
87
+ throw err;
88
+ // one task that cannot be advanced must not hold the others back every minute
89
+ this.log.error("scheduled task cannot be advanced; paused", {
90
+ taskId: err.taskId,
91
+ err: err.cause,
92
+ });
93
+ await this.quarantine(err.taskId);
113
94
  }
114
- }
115
- finally {
116
- await this.repair(now, repairBatch);
117
95
  }
118
96
  return total;
119
97
  }
@@ -134,8 +112,8 @@ let ScheduledTaskScanJob = class ScheduledTaskScanJob {
134
112
  }
135
113
  const fired = await this.advance(t, task, now, s.catchUpWindowMinutes);
136
114
  if (fired !== null) {
137
- const id = fired;
138
- afterCommit(() => this.enqueue([id]));
115
+ await oweSchedFire(this.outbox, fired.id, task.orgId);
116
+ await oweSchedSettleUnfired(this.outbox, fired.id, task.orgId, fired.scheduledFor, s.catchUpWindowMinutes);
139
117
  }
140
118
  return true;
141
119
  }
@@ -143,7 +121,7 @@ let ScheduledTaskScanJob = class ScheduledTaskScanJob {
143
121
  async quarantine(taskId) {
144
122
  await pauseTaskIn(this.db, taskId, "invalid_schedule", null);
145
123
  }
146
- /** One due task (see the class); returns the id of the run to fire, if any. */
124
+ /** One due task (see the class); returns the run to fire, if any. */
147
125
  async advance(t, task, now, catchUpWindowMinutes) {
148
126
  // the schedule math alone may mark the task broken; database errors fail this pass only
149
127
  let step;
@@ -178,7 +156,7 @@ let ScheduledTaskScanJob = class ScheduledTaskScanJob {
178
156
  scheduledFor: step.fire,
179
157
  trial: false,
180
158
  });
181
- fired = run?.id ?? null;
159
+ fired = run ?? null;
182
160
  }
183
161
  const patch = step.end === "done"
184
162
  ? { status: "ended", nextRunAt: null }
@@ -188,86 +166,6 @@ let ScheduledTaskScanJob = class ScheduledTaskScanJob {
188
166
  await this.store.updateTask(t, task.id, patch);
189
167
  return fired;
190
168
  }
191
- /** Enqueues the fire jobs (after the commit; a failure is repaired by a later scan). */
192
- async enqueue(ids) {
193
- for (const id of ids) {
194
- const job = schedFireJob(id);
195
- try {
196
- await this.maintenance.add(job.name, job.data, job.opts);
197
- }
198
- catch (err) {
199
- this.log.warn("enqueueing a scheduled task run failed", { taskRunId: id, err });
200
- }
201
- }
202
- }
203
- /** Lost fire jobs (see the class). */
204
- async repair(now, batch) {
205
- const stale = await this.db
206
- .selectFrom("scheduled_task_runs")
207
- .select(["id", "scheduled_for"])
208
- .where("status", "=", "pending")
209
- .where("fired_at", "is", null)
210
- .where("scheduled_for", "<", new Date(now.getTime() - REPAIR_AFTER_MS))
211
- .orderBy("scheduled_for")
212
- .limit(batch)
213
- .execute();
214
- const retry = [];
215
- const lost = [];
216
- for (const r of stale) {
217
- const at = r.scheduled_for instanceof Date ? r.scheduled_for : new Date(r.scheduled_for);
218
- (now.getTime() - at.getTime() > GIVE_UP_AFTER_MS ? lost : retry).push(r.id);
219
- }
220
- for (const id of lost)
221
- await this.giveUp(id);
222
- await this.enqueue(retry);
223
- // the least recently rechecked first, so runs still in flight do not starve the others
224
- const started = await this.db
225
- .selectFrom("scheduled_task_runs")
226
- .select("id")
227
- .where("status", "=", "pending")
228
- .where("fired_at", "<", new Date(now.getTime() - GIVE_UP_AFTER_MS))
229
- .orderBy(sql `rechecked_at NULLS FIRST`)
230
- .orderBy("scheduled_for")
231
- .limit(batch)
232
- .execute();
233
- for (const r of started)
234
- await this.recheck(r.id, now);
235
- }
236
- /** A run never fired an hour after its tick: given up, task row first (the one lock order). */
237
- async giveUp(taskRunId) {
238
- const owner = await this.db
239
- .selectFrom("scheduled_task_runs")
240
- .select("task_id")
241
- .where("id", "=", taskRunId)
242
- .executeTakeFirst();
243
- if (!owner)
244
- return;
245
- await lockTaskRow(this.db, owner.task_id);
246
- await this.db
247
- .updateTable("scheduled_task_runs")
248
- .set({ status: "skipped", skip_reason: "missed", error: "The run could not be started" })
249
- .where("id", "=", taskRunId)
250
- .where("status", "=", "pending")
251
- .where("fired_at", "is", null)
252
- .execute();
253
- }
254
- /**
255
- * A started run still pending long after: the tracker links, settles or releases it; one still
256
- * in flight is stamped (`rechecked_at`) and goes to the back of the next rechecks.
257
- */
258
- async recheck(taskRunId, now) {
259
- const run = await this.store.updateRun(this.db, taskRunId, {});
260
- if (run?.status !== "pending")
261
- return;
262
- // inFlightIn takes the task row before the run (the one lock order)
263
- if (await this.tracker.inFlightIn(run, now)) {
264
- await this.db
265
- .updateTable("scheduled_task_runs")
266
- .set({ rechecked_at: now })
267
- .where("id", "=", taskRunId)
268
- .execute();
269
- }
270
- }
271
169
  };
272
170
  __decorate([
273
171
  LockedCron("* * * * *", { name: SCHED_SCAN_JOB })
@@ -278,21 +176,14 @@ __decorate([
278
176
  __decorate([
279
177
  Transactional()
280
178
  ], ScheduledTaskScanJob.prototype, "quarantine", null);
281
- __decorate([
282
- Transactional()
283
- ], ScheduledTaskScanJob.prototype, "giveUp", null);
284
- __decorate([
285
- Transactional()
286
- ], ScheduledTaskScanJob.prototype, "recheck", null);
287
179
  ScheduledTaskScanJob = __decorate([
288
180
  Injectable(),
289
181
  __param(0, Inject(KernelDb)),
290
182
  __param(1, Inject(TransactionHost)),
291
183
  __param(2, Inject(KernelConfigService)),
292
184
  __param(3, Inject(ScheduledTaskStore)),
293
- __param(4, Inject(ScheduledTaskRunTracker)),
294
- __param(5, Inject(getQueueToken(MAINTENANCE_QUEUE_NAME))),
295
- __param(6, Inject(LockService)),
296
- __param(7, Inject(KERNEL_LOGGER))
185
+ __param(4, Inject(OutboxService)),
186
+ __param(5, Inject(LockService)),
187
+ __param(6, Inject(KERNEL_LOGGER))
297
188
  ], ScheduledTaskScanJob);
298
189
  export { ScheduledTaskScanJob };
@@ -1,4 +1,3 @@
1
- import type { Queue } from "bullmq";
2
1
  import { AgentLookup } from "../agents/agent-lookup.js";
3
2
  import type { ModelRef } from "../agents/definition.js";
4
3
  import { ConversationAccess } from "../conversations/access.js";
@@ -7,6 +6,7 @@ import { ExtensionRegistry } from "../extensions/registry.js";
7
6
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
8
7
  import { KernelDb } from "../infra/database/kernel-db.js";
9
8
  import { TransactionHost } from "../infra/database/transaction-host.js";
9
+ import { OutboxService } from "../infra/outbox/outbox.service.js";
10
10
  import { DevicePolicyService } from "../local-devices/device-policy.service.js";
11
11
  import type { KernelLogger } from "../log.js";
12
12
  import { OrgLock } from "../orgs/org-lock.js";
@@ -198,8 +198,8 @@ export declare class ScheduledTasksService {
198
198
  /** A task's model must resolve for the org and declare its effort (as a message's switch). */
199
199
  private readonly providers;
200
200
  private readonly log;
201
- /** "Run now" enqueues the fire job (consumed by the worker role). */
202
- private readonly maintenance;
201
+ /** "Run now" owes the fire job (consumed by the worker role). */
202
+ private readonly outbox;
203
203
  /** Network grants: the org's effective egress (network ask-to-allow spec §8.1). */
204
204
  private readonly sandbox;
205
205
  /** Network grants: the task's device's local policy summary (`remoteGrants`). */
@@ -207,8 +207,8 @@ export declare class ScheduledTasksService {
207
207
  constructor(db: KernelDb, txHost: TransactionHost, config: Pick<KernelConfigService, "get">, store: ScheduledTaskStore, orgLock: Pick<OrgLock, "lockAndAuthorize">, agents: Pick<AgentLookup, "get">, access: Pick<ConversationAccess, "isAccessible">, tools: Pick<ToolRegistry, "forAgent">, extensions: Pick<ExtensionRegistry, "list">,
208
208
  /** A task's model must resolve for the org and declare its effort (as a message's switch). */
209
209
  providers: Pick<ProviderRegistry, "resolve">, log: Pick<KernelLogger, "warn">,
210
- /** "Run now" enqueues the fire job (consumed by the worker role). */
211
- maintenance: Pick<Queue, "add">,
210
+ /** "Run now" owes the fire job (consumed by the worker role). */
211
+ outbox: Pick<OutboxService, "add">,
212
212
  /** Network grants: the org's effective egress (network ask-to-allow spec §8.1). */
213
213
  sandbox: Pick<SandboxService, "ceilingLimits" | "devicePolicy">,
214
214
  /** Network grants: the task's device's local policy summary (`remoteGrants`). */
@@ -301,7 +301,7 @@ export declare class ScheduledTasksService {
301
301
  /**
302
302
  * "Run now" (spec §6), creator only: a task run for the request time, outside the schedule (no
303
303
  * unique-key conflict with a tick, and `next_run_at` does not move), fired at once through the
304
- * fire job after the commit. `trial`: an attended trial run whose approvals become the task's
304
+ * fire job its transaction owes. `trial`: an attended trial run whose approvals become the task's
305
305
  * grants (§7). Counted in the rate limit like any run. An ended task cannot run; a paused one
306
306
  * can (only its creator gets here: the run is `manual`, which the fire's paused check lets by).
307
307
  */