@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.
- package/dist/admin/admin.module.js +2 -0
- package/dist/admin/health.service.d.ts +2 -2
- package/dist/admin/health.service.js +6 -2
- package/dist/agents/agents.module.js +3 -0
- package/dist/agents/agents.service.d.ts +6 -1
- package/dist/agents/agents.service.js +11 -2
- package/dist/catalog/catalog-import.module.js +0 -4
- package/dist/catalog/catalog-scope.service.d.ts +63 -26
- package/dist/catalog/catalog-scope.service.js +146 -71
- package/dist/catalog/catalog.module.js +4 -3
- package/dist/catalog/catalog.service.d.ts +11 -4
- package/dist/catalog/catalog.service.js +21 -5
- package/dist/catalog/cli.d.ts +4 -3
- package/dist/catalog/cli.js +8 -13
- package/dist/catalog/job-ids.d.ts +8 -20
- package/dist/catalog/job-ids.js +11 -16
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/org-plugin-uploads.d.ts +11 -4
- package/dist/catalog/org-plugin-uploads.js +21 -6
- package/dist/catalog/owned-items.d.ts +6 -13
- package/dist/catalog/owned-items.js +11 -29
- package/dist/catalog/owned-plugin-cleanup.d.ts +8 -17
- package/dist/catalog/owned-plugin-cleanup.js +12 -66
- package/dist/catalog/platform-components.d.ts +56 -34
- package/dist/catalog/platform-components.js +68 -78
- package/dist/catalog/plugin-import.d.ts +28 -25
- package/dist/catalog/plugin-import.js +90 -76
- package/dist/catalog/plugin-upgrade.d.ts +4 -4
- package/dist/catalog/plugin-upgrade.js +7 -19
- package/dist/catalog/scope-jobs.d.ts +59 -0
- package/dist/catalog/scope-jobs.js +65 -0
- package/dist/catalog/scope-sync-workers.d.ts +22 -0
- package/dist/catalog/scope-sync-workers.js +58 -2
- package/dist/catalog/scope-sync.d.ts +40 -65
- package/dist/catalog/scope-sync.js +114 -122
- package/dist/channels/inbound.js +2 -2
- package/dist/channels/threads.d.ts +4 -3
- package/dist/channels/threads.js +4 -3
- package/dist/conversations/conversations.controller.js +4 -2
- package/dist/conversations/conversations.service.d.ts +17 -5
- package/dist/conversations/conversations.service.js +17 -5
- package/dist/conversations/events.d.ts +21 -0
- package/dist/conversations/events.js +24 -1
- package/dist/conversations/source.d.ts +10 -0
- package/dist/conversations/source.js +17 -0
- package/dist/conversations/tree.d.ts +10 -0
- package/dist/conversations/tree.js +35 -0
- package/dist/db/kernel-schema.d.ts +9 -14
- package/dist/devices/devices.service.d.ts +11 -8
- package/dist/devices/devices.service.js +16 -23
- package/dist/evals/run-replay.d.ts +13 -2
- package/dist/evals/run-replay.js +28 -1
- package/dist/evals/snapshot.d.ts +7 -1
- package/dist/evals/snapshot.js +2 -1
- package/dist/extensions/hooks.d.ts +22 -3
- package/dist/extensions/hooks.js +15 -1
- package/dist/extensions/lifecycle-hooks.d.ts +99 -0
- package/dist/extensions/lifecycle-hooks.js +273 -0
- package/dist/extensions/org-config.d.ts +13 -1
- package/dist/extensions/org-config.js +19 -2
- package/dist/extensions/permission-view.d.ts +9 -5
- package/dist/extensions/permission-view.js +9 -4
- package/dist/extensions/point-defaults.d.ts +28 -1
- package/dist/extensions/point-defaults.js +44 -13
- package/dist/inbox/inbox.service.d.ts +6 -20
- package/dist/inbox/inbox.service.js +0 -3
- package/dist/inbox/org-notices.d.ts +1 -1
- package/dist/inbox/platform-inbox.service.d.ts +3 -32
- package/dist/inbox/platform-inbox.service.js +5 -54
- package/dist/infra/outbox/registry.d.ts +124 -0
- package/dist/infra/outbox/registry.js +126 -0
- package/dist/infra/queue/queue-names.d.ts +1 -1
- package/dist/infra/queue/queue-names.js +1 -0
- package/dist/orgs/directory-sync.js +1 -1
- package/dist/orgs/members.service.js +1 -1
- package/dist/orgs/offboarding.d.ts +50 -57
- package/dist/orgs/offboarding.js +117 -162
- package/dist/orgs/orgs.module.js +3 -0
- package/dist/orgs/orgs.service.d.ts +11 -5
- package/dist/orgs/orgs.service.js +15 -6
- package/dist/overrides.d.ts +3 -0
- package/dist/overrides.js +2 -0
- package/dist/platform/ops/inbox.ops.js +4 -20
- package/dist/platform/ops/platform.ops.d.ts +1 -0
- package/dist/platform/ops/platform.ops.js +6 -3
- package/dist/roles/worker-role.d.ts +10 -5
- package/dist/roles/worker-role.js +19 -9
- package/dist/roles/worker.module.js +2 -4
- package/dist/runtime/context/build.d.ts +3 -0
- package/dist/runtime/context/build.js +15 -0
- package/dist/runtime/context/plugin-context.d.ts +20 -0
- package/dist/runtime/context/plugin-context.js +30 -0
- package/dist/runtime/langgraph-loop.js +4 -0
- package/dist/runtime/loop.d.ts +8 -0
- package/dist/runtime/maintenance.d.ts +5 -3
- package/dist/runtime/maintenance.js +36 -11
- package/dist/runtime/model-call.d.ts +6 -1
- package/dist/runtime/model-call.js +1 -0
- package/dist/runtime/plugin-context.service.d.ts +69 -0
- package/dist/runtime/plugin-context.service.js +208 -0
- package/dist/runtime/run-control.d.ts +6 -1
- package/dist/runtime/run-control.js +13 -0
- package/dist/runtime/run-queue.d.ts +13 -0
- package/dist/runtime/run-queue.js +63 -0
- package/dist/runtime/run-worker.d.ts +22 -1
- package/dist/runtime/run-worker.js +107 -10
- package/dist/runtime/runtime.module.js +2 -0
- package/dist/runtime/subagents/dispatch.js +1 -0
- package/dist/runtime/subagents/skill-fork.js +3 -0
- package/dist/sandbox/jobs.d.ts +20 -10
- package/dist/sandbox/jobs.js +23 -17
- package/dist/schedules/fire.d.ts +25 -4
- package/dist/schedules/fire.js +87 -13
- package/dist/schedules/job-ids.d.ts +66 -17
- package/dist/schedules/job-ids.js +81 -13
- package/dist/schedules/run-tracker.js +2 -1
- package/dist/schedules/scan.job.d.ts +13 -32
- package/dist/schedules/scan.job.js +35 -144
- package/dist/schedules/scheduled-tasks.service.d.ts +6 -6
- package/dist/schedules/scheduled-tasks.service.js +17 -14
- package/dist/schedules/schedules.module.js +0 -4
- package/dist/skills/changes.controller.d.ts +1 -1
- package/dist/skills/changes.controller.js +1 -1
- package/dist/tools/approvals.service.d.ts +15 -4
- package/dist/tools/approvals.service.js +26 -10
- package/dist/tools/pipeline.d.ts +11 -2
- package/dist/tools/pipeline.js +5 -0
- package/dist/tools/questions.service.d.ts +15 -4
- package/dist/tools/questions.service.js +21 -19
- package/dist/tools/registry.d.ts +8 -1
- package/dist/tools/registry.js +9 -0
- package/package.json +3 -3
- package/src/migrations/0121_drop_platform_migration_runs.sql +12 -0
- package/src/migrations/0122_drop_scheduled_task_runs_rechecked_at.sql +5 -0
- package/src/migrations/0123_pgvector.sql +27 -0
- package/src/migrations/0124_conversation_source.sql +36 -0
- package/dist/catalog/platform-migration.d.ts +0 -115
- package/dist/catalog/platform-migration.js +0 -583
- package/dist/infra/queue/add-job-once.d.ts +0 -14
- package/dist/infra/queue/add-job-once.js +0 -16
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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:
|
|
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
|
-
*
|
|
13
|
-
*
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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
|
-
|
|
70
|
-
maintenance;
|
|
58
|
+
outbox;
|
|
71
59
|
lockService;
|
|
72
60
|
log;
|
|
73
|
-
constructor(kdb, txHost, config, store,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
|
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
|
|
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(
|
|
294
|
-
__param(5, Inject(
|
|
295
|
-
__param(6, Inject(
|
|
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"
|
|
202
|
-
private readonly
|
|
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"
|
|
211
|
-
|
|
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
|
|
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
|
*/
|