@capybara-hq/kernel 0.1.0-rc.24 → 0.1.0-rc.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/admin/outbox-dead.service.d.ts +7 -3
- package/dist/admin/outbox-dead.service.js +17 -6
- package/dist/admin/overview.service.d.ts +6 -6
- package/dist/admin/overview.service.js +7 -7
- 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/background/wake-rules.d.ts +5 -0
- package/dist/background/wake-rules.js +16 -5
- 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/channels.module.d.ts +9 -8
- package/dist/channels/channels.module.js +11 -10
- package/dist/channels/connections.service.d.ts +3 -3
- package/dist/channels/connections.service.js +6 -6
- package/dist/channels/deliver-job.d.ts +10 -3
- package/dist/channels/deliver-job.js +7 -3
- package/dist/channels/inbound.js +2 -2
- package/dist/channels/jobs.d.ts +11 -4
- package/dist/channels/jobs.js +18 -3
- package/dist/channels/outbound.d.ts +5 -2
- package/dist/channels/outbound.js +9 -2
- package/dist/channels/threads.d.ts +4 -3
- package/dist/channels/threads.js +4 -3
- package/dist/conversations/conversations.controller.d.ts +4 -2
- package/dist/conversations/conversations.controller.js +8 -4
- package/dist/conversations/conversations.service.d.ts +54 -9
- package/dist/conversations/conversations.service.js +94 -25
- package/dist/conversations/events.d.ts +27 -0
- package/dist/conversations/events.js +30 -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 +19 -15
- package/dist/devices/devices.service.d.ts +25 -14
- package/dist/devices/devices.service.js +36 -30
- package/dist/evals/evals.module.js +5 -1
- package/dist/evals/jobs.d.ts +14 -6
- package/dist/evals/jobs.js +21 -19
- package/dist/evals/run-replay.d.ts +13 -2
- package/dist/evals/run-replay.js +28 -1
- package/dist/evals/runs.service.d.ts +5 -6
- package/dist/evals/runs.service.js +11 -33
- 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/outbox.service.d.ts +6 -0
- package/dist/infra/outbox/outbox.service.js +51 -17
- package/dist/infra/outbox/registry.d.ts +176 -0
- package/dist/infra/outbox/registry.js +183 -0
- package/dist/infra/queue/queue-names.d.ts +1 -1
- package/dist/infra/queue/queue-names.js +1 -0
- package/dist/local-devices/device-policy.service.d.ts +131 -26
- package/dist/local-devices/device-policy.service.js +341 -145
- package/dist/local-devices/machines.d.ts +17 -2
- package/dist/local-devices/machines.js +66 -5
- package/dist/local-devices/policy-op-jobs.d.ts +15 -0
- package/dist/local-devices/policy-op-jobs.js +12 -0
- package/dist/local-devices/policy-summary.d.ts +36 -3
- package/dist/local-devices/policy-summary.js +47 -1
- 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 +19 -7
- package/dist/overrides.d.ts +7 -0
- package/dist/overrides.js +4 -0
- package/dist/platform/ops/conversations.ops.js +1 -1
- 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 +9 -4
- package/dist/runtime/maintenance.js +52 -13
- 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 +75 -0
- package/dist/runtime/plugin-context.service.js +220 -0
- package/dist/runtime/run-context.service.d.ts +7 -3
- package/dist/runtime/run-context.service.js +12 -2
- package/dist/runtime/run-control.d.ts +6 -1
- package/dist/runtime/run-control.js +13 -0
- package/dist/runtime/run-finish.d.ts +2 -2
- package/dist/runtime/run-finish.js +11 -6
- package/dist/runtime/run-jobs.d.ts +21 -4
- package/dist/runtime/run-jobs.js +29 -6
- package/dist/runtime/run-orphan-check.job.d.ts +14 -11
- package/dist/runtime/run-orphan-check.job.js +16 -11
- package/dist/runtime/run-queue.d.ts +29 -24
- package/dist/runtime/run-queue.js +118 -57
- package/dist/runtime/run-status.d.ts +12 -0
- package/dist/runtime/run-status.js +21 -0
- package/dist/runtime/run-worker.d.ts +32 -3
- package/dist/runtime/run-worker.js +143 -21
- package/dist/runtime/runtime.module.js +2 -0
- package/dist/runtime/step-log.d.ts +6 -5
- package/dist/runtime/step-log.js +6 -5
- package/dist/runtime/subagents/dispatch.js +1 -0
- package/dist/runtime/subagents/skill-fork.js +3 -0
- package/dist/runtime/usage-report.d.ts +3 -1
- package/dist/runtime/usage-report.js +4 -2
- package/dist/runtime/waits.d.ts +16 -6
- package/dist/runtime/waits.js +37 -15
- 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 +16 -35
- package/dist/schedules/scan.job.js +38 -147
- 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/catalog.d.ts +7 -3
- package/dist/skills/catalog.js +12 -2
- package/dist/skills/changes.controller.d.ts +1 -1
- package/dist/skills/changes.controller.js +1 -1
- package/dist/tools/approvals.service.d.ts +111 -23
- package/dist/tools/approvals.service.js +326 -77
- package/dist/tools/builtins/sandbox/task-output.js +21 -6
- package/dist/tools/device-op-resolver.d.ts +40 -0
- package/dist/tools/device-op-resolver.js +121 -0
- package/dist/tools/network-asks.d.ts +1 -1
- package/dist/tools/network-asks.js +4 -3
- 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/dist/tools/tools.module.js +4 -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/src/migrations/0125_conversation_tree_stopped_at.sql +5 -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
package/dist/sandbox/jobs.js
CHANGED
|
@@ -19,10 +19,29 @@ export const SANDBOX_SUSPEND_JOB = "sandbox-suspend-idle";
|
|
|
19
19
|
/** The inactivity destruction sweep (hourly, once per tick across replicas). */
|
|
20
20
|
export const SANDBOX_DESTROY_JOB = "sandbox-destroy-inactive";
|
|
21
21
|
/**
|
|
22
|
-
* One-off job: destroy
|
|
23
|
-
*
|
|
22
|
+
* One-off job: destroy devices now, instance and file system (data `{ deviceIds }`). Owed through
|
|
23
|
+
* the outbox by the transaction that soft-deletes the device (a device's deletion, offboarding),
|
|
24
|
+
* one device per job under {@link deviceDestroyJobId} ({@link oweDeviceDestroy}).
|
|
24
25
|
*/
|
|
25
26
|
export const SANDBOX_DESTROY_FOR_JOB = "sandbox-destroy-for";
|
|
27
|
+
/**
|
|
28
|
+
* The deterministic id of a deleted device's destroy job (no `:`): a device is deleted once, so
|
|
29
|
+
* its destruction is owed once.
|
|
30
|
+
*/
|
|
31
|
+
export const deviceDestroyJobId = (deviceId) => `device-destroy-${deviceId}`;
|
|
32
|
+
/**
|
|
33
|
+
* Owes the destruction of a device in the caller's transaction, the one that soft-deleted it
|
|
34
|
+
* (transactional outbox spec §8.1; throws outside a transaction).
|
|
35
|
+
*/
|
|
36
|
+
export function oweDeviceDestroy(outbox, deviceId, orgId) {
|
|
37
|
+
return outbox.add({
|
|
38
|
+
queue: MAINTENANCE_QUEUE_NAME,
|
|
39
|
+
name: SANDBOX_DESTROY_FOR_JOB,
|
|
40
|
+
jobId: deviceDestroyJobId(deviceId),
|
|
41
|
+
data: { deviceIds: [deviceId] },
|
|
42
|
+
orgId,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
26
45
|
/**
|
|
27
46
|
* One-off job: start a device over with empty files (`resetFileSystem`; data
|
|
28
47
|
* `{ deviceId }`, job id {@link resetFilesJobId}).
|
|
@@ -34,27 +53,14 @@ export const SANDBOX_RESET_FILES_JOB = "sandbox-reset-files";
|
|
|
34
53
|
*/
|
|
35
54
|
export const resetFilesJobId = (deviceId) => `${SANDBOX_RESET_FILES_JOB}-${deviceId}`;
|
|
36
55
|
/**
|
|
37
|
-
* The producer of
|
|
38
|
-
*
|
|
39
|
-
* {@link SANDBOX_RESET_FILES_JOB}; `MaintenanceProcessor` consumes them.
|
|
56
|
+
* The producer of {@link SANDBOX_RESET_FILES_JOB} (destroys are owed through the outbox,
|
|
57
|
+
* {@link oweDeviceDestroy}); `MaintenanceProcessor` consumes both.
|
|
40
58
|
*/
|
|
41
59
|
let SandboxJobs = class SandboxJobs {
|
|
42
60
|
queue;
|
|
43
61
|
constructor(queue) {
|
|
44
62
|
this.queue = queue;
|
|
45
63
|
}
|
|
46
|
-
/**
|
|
47
|
-
* Enqueues the destruction of these devices: a failed batch is retried whole (destroying
|
|
48
|
-
* one already destroyed does nothing).
|
|
49
|
-
*/
|
|
50
|
-
async destroy(deviceIds) {
|
|
51
|
-
await this.queue.add(SANDBOX_DESTROY_FOR_JOB, { deviceIds }, {
|
|
52
|
-
attempts: 5,
|
|
53
|
-
backoff: { type: "exponential", delay: 30_000 },
|
|
54
|
-
removeOnComplete: true,
|
|
55
|
-
removeOnFail: 100,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
64
|
/**
|
|
59
65
|
* Enqueues the reset of a device's files (see {@link resetFilesJobId}): retried on failure
|
|
60
66
|
* (the exclusive lease not obtained in time, the provider failing); a job that failed all its
|
package/dist/schedules/fire.d.ts
CHANGED
|
@@ -7,8 +7,10 @@ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
|
7
7
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
8
8
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
9
9
|
import { LockService } from "../infra/lock/lock.service.js";
|
|
10
|
+
import { OutboxService } from "../infra/outbox/outbox.service.js";
|
|
10
11
|
import type { KernelLogger } from "../log.js";
|
|
11
12
|
import { OrgLock } from "../orgs/org-lock.js";
|
|
13
|
+
import { type SchedSettleJobData } from "./job-ids.js";
|
|
12
14
|
import { ScheduledTaskRunTracker } from "./run-tracker.js";
|
|
13
15
|
import { ScheduledTaskStore } from "./store.js";
|
|
14
16
|
/** A task run's message client id in a conversation's queue (never read back as "the task's"). */
|
|
@@ -21,10 +23,12 @@ export declare function localTimeOf(at: Date, timeZone: string): string;
|
|
|
21
23
|
*
|
|
22
24
|
* 1. The claim, one transaction under the org row lock (which serializes the org's rate limit
|
|
23
25
|
* count), then the task run and the task rows: the checks in order (task paused since the
|
|
24
|
-
* scan, unless a person's "run now"; org
|
|
25
|
-
*
|
|
26
|
+
* scan, unless a person's "run now"; a tick fired more than 10 minutes past the org's catch-up
|
|
27
|
+
* window, `missed`, unless a "run now"; org disabled, creator inactive or not a member, agent
|
|
28
|
+
* gone or not visible, conversation gone, connection inactive,
|
|
26
29
|
* overlap, rate limit, device offline, queue paused), each a `skipped` run with its reason, the
|
|
27
|
-
* lasting ones also pausing the task; then `fired_at` is written, after the rate limit count
|
|
30
|
+
* lasting ones also pausing the task; then `fired_at` is written, after the rate limit count,
|
|
31
|
+
* with the run's delayed settle check owed (`sched-settle`, {@link settle}).
|
|
28
32
|
* A run whose claim committed before (a retried job) goes straight to the start.
|
|
29
33
|
* 2. The start, idempotent: `new` creates the conversation with its first message keyed by the
|
|
30
34
|
* task run id (`create_request_id`); `conversation` queues the prompt in the target
|
|
@@ -51,12 +55,29 @@ export declare class ScheduledTaskFirer {
|
|
|
51
55
|
/** Reads a local device's connection lease (online or not). */
|
|
52
56
|
private readonly locks;
|
|
53
57
|
private readonly log;
|
|
58
|
+
/** The claim owes the run's settle check (ruling R26). */
|
|
59
|
+
private readonly outbox;
|
|
54
60
|
constructor(kdb: KernelDb, txHost: TransactionHost, config: Pick<KernelConfigService, "get">, store: ScheduledTaskStore, tracker: Pick<ScheduledTaskRunTracker, "inFlightIn" | "failStartIn">, orgLock: Pick<OrgLock, "tryLock">, agents: Pick<AgentLookup, "get">, access: Pick<ConversationAccess, "isAccessible">, conversations: Pick<ConversationsService, "createWithMessage" | "create">, queue: Pick<ConversationQueue, "add">, connections: Pick<ChannelConnections, "row" | "channel">,
|
|
55
61
|
/** Reads a local device's connection lease (online or not). */
|
|
56
|
-
locks: Pick<LockService, "holderOf">, log: Pick<KernelLogger, "warn" | "info"
|
|
62
|
+
locks: Pick<LockService, "holderOf">, log: Pick<KernelLogger, "warn" | "info">,
|
|
63
|
+
/** The claim owes the run's settle check (ruling R26). */
|
|
64
|
+
outbox: Pick<OutboxService, "add">);
|
|
57
65
|
private get db();
|
|
58
66
|
/** Fires task run `taskRunId` (see the class); a run not `pending` is left as it is. */
|
|
59
67
|
fire(taskRunId: string): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* The `sched-settle` job (ruling R26; `job-ids.ts`). The claimed check: a task run claimed
|
|
70
|
+
* (`fired_at`) and still `pending` goes through the run tracker's check, which links it to its
|
|
71
|
+
* run, settles it, or releases a run that left no trace (`skipped`, `removed`), so a `once`
|
|
72
|
+
* task's last run never stays pending; one still in flight (its item queued, its message posted)
|
|
73
|
+
* owes the next check an hour on. The unfired check: a run still `pending` that no claim took
|
|
74
|
+
* (its fire job failed for good before the claim) is skipped, `missed` for a tick, `removed` for
|
|
75
|
+
* a "run now", so the overlap check never waits on it. A run claimed, started, ended or skipped
|
|
76
|
+
* since is left as it is (the claimed check follows a claimed run).
|
|
77
|
+
*/
|
|
78
|
+
settle(data: SchedSettleJobData): Promise<void>;
|
|
79
|
+
/** The unfired check (see {@link settle}), task row first (the one lock order). */
|
|
80
|
+
private settleUnfired;
|
|
60
81
|
/** Step 1 (see the class): a skip, the plan, or null when there is nothing to fire. */
|
|
61
82
|
private claim;
|
|
62
83
|
/** Step 2 (see the class). */
|
package/dist/schedules/fire.js
CHANGED
|
@@ -16,19 +16,20 @@ import { ConversationAccess, ConversationAccessModule } from "../conversations/a
|
|
|
16
16
|
import { ConversationsModule } from "../conversations/conversations.module.js";
|
|
17
17
|
import { ConversationsService } from "../conversations/conversations.service.js";
|
|
18
18
|
import { ConversationQueue } from "../conversations/queue.service.js";
|
|
19
|
+
import { channelSource } from "../conversations/source.js";
|
|
19
20
|
import { ApiError } from "../errors.js";
|
|
20
21
|
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
21
22
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
22
23
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
23
24
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
24
25
|
import { LockService } from "../infra/lock/lock.service.js";
|
|
25
|
-
import {
|
|
26
|
+
import { OutboxService } from "../infra/outbox/outbox.service.js";
|
|
26
27
|
import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
27
28
|
import { deviceLeaseKey } from "../local-devices/keys.js";
|
|
28
29
|
import { OrgLock, OrgLockModule } from "../orgs/org-lock.js";
|
|
29
|
-
import { MAINTENANCE_QUEUE_NAME } from "../runtime/maintenance-queue.js";
|
|
30
30
|
import { RuntimeModule } from "../runtime/runtime.module.js";
|
|
31
31
|
import { readScheduleSettings } from "./effective-settings.js";
|
|
32
|
+
import { oweSchedSettle, SCHED_LATE_FIRE_SLACK_MS, SCHED_SETTLE_AFTER_MS, SCHED_SETTLE_LONG_CHECKS, SCHED_SETTLE_RECHECK_MS, } from "./job-ids.js";
|
|
32
33
|
import { pauseTaskIn } from "./pause.js";
|
|
33
34
|
import { lockTaskRow, ScheduledTaskRunTracker, ScheduledTaskTrackerModule } from "./run-tracker.js";
|
|
34
35
|
import { ScheduledTaskScanJob } from "./scan.job.js";
|
|
@@ -69,10 +70,12 @@ const DEVICE_GONE = new Set(["device.not_found", "device.unavailable", "sandbox.
|
|
|
69
70
|
*
|
|
70
71
|
* 1. The claim, one transaction under the org row lock (which serializes the org's rate limit
|
|
71
72
|
* count), then the task run and the task rows: the checks in order (task paused since the
|
|
72
|
-
* scan, unless a person's "run now"; org
|
|
73
|
-
*
|
|
73
|
+
* scan, unless a person's "run now"; a tick fired more than 10 minutes past the org's catch-up
|
|
74
|
+
* window, `missed`, unless a "run now"; org disabled, creator inactive or not a member, agent
|
|
75
|
+
* gone or not visible, conversation gone, connection inactive,
|
|
74
76
|
* overlap, rate limit, device offline, queue paused), each a `skipped` run with its reason, the
|
|
75
|
-
* lasting ones also pausing the task; then `fired_at` is written, after the rate limit count
|
|
77
|
+
* lasting ones also pausing the task; then `fired_at` is written, after the rate limit count,
|
|
78
|
+
* with the run's delayed settle check owed (`sched-settle`, {@link settle}).
|
|
76
79
|
* A run whose claim committed before (a retried job) goes straight to the start.
|
|
77
80
|
* 2. The start, idempotent: `new` creates the conversation with its first message keyed by the
|
|
78
81
|
* task run id (`create_request_id`); `conversation` queues the prompt in the target
|
|
@@ -98,9 +101,12 @@ let ScheduledTaskFirer = class ScheduledTaskFirer {
|
|
|
98
101
|
connections;
|
|
99
102
|
locks;
|
|
100
103
|
log;
|
|
104
|
+
outbox;
|
|
101
105
|
constructor(kdb, txHost, config, store, tracker, orgLock, agents, access, conversations, queue, connections,
|
|
102
106
|
/** Reads a local device's connection lease (online or not). */
|
|
103
|
-
locks, log
|
|
107
|
+
locks, log,
|
|
108
|
+
/** The claim owes the run's settle check (ruling R26). */
|
|
109
|
+
outbox) {
|
|
104
110
|
this.kdb = kdb;
|
|
105
111
|
this.txHost = txHost;
|
|
106
112
|
this.config = config;
|
|
@@ -114,6 +120,7 @@ let ScheduledTaskFirer = class ScheduledTaskFirer {
|
|
|
114
120
|
this.connections = connections;
|
|
115
121
|
this.locks = locks;
|
|
116
122
|
this.log = log;
|
|
123
|
+
this.outbox = outbox;
|
|
117
124
|
}
|
|
118
125
|
get db() {
|
|
119
126
|
return this.kdb.db;
|
|
@@ -137,6 +144,61 @@ let ScheduledTaskFirer = class ScheduledTaskFirer {
|
|
|
137
144
|
return;
|
|
138
145
|
await this.start(claimed.plan);
|
|
139
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* The `sched-settle` job (ruling R26; `job-ids.ts`). The claimed check: a task run claimed
|
|
149
|
+
* (`fired_at`) and still `pending` goes through the run tracker's check, which links it to its
|
|
150
|
+
* run, settles it, or releases a run that left no trace (`skipped`, `removed`), so a `once`
|
|
151
|
+
* task's last run never stays pending; one still in flight (its item queued, its message posted)
|
|
152
|
+
* owes the next check an hour on. The unfired check: a run still `pending` that no claim took
|
|
153
|
+
* (its fire job failed for good before the claim) is skipped, `missed` for a tick, `removed` for
|
|
154
|
+
* a "run now", so the overlap check never waits on it. A run claimed, started, ended or skipped
|
|
155
|
+
* since is left as it is (the claimed check follows a claimed run).
|
|
156
|
+
*/
|
|
157
|
+
async settle(data) {
|
|
158
|
+
const { taskRunId } = data;
|
|
159
|
+
if ("unfired" in data) {
|
|
160
|
+
await this.settleUnfired(taskRunId);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const run = await this.store.updateRun(this.db, taskRunId, {});
|
|
164
|
+
if (run?.status !== "pending" || run.firedAt === null)
|
|
165
|
+
return;
|
|
166
|
+
// inFlightIn takes the task row before the run (the one lock order)
|
|
167
|
+
if (!(await this.tracker.inFlightIn(run)))
|
|
168
|
+
return;
|
|
169
|
+
const now = await this.store.updateRun(this.db, taskRunId, {});
|
|
170
|
+
if (now?.status !== "pending" || now.firedAt === null)
|
|
171
|
+
return;
|
|
172
|
+
const next = data.check + 1;
|
|
173
|
+
if (next === SCHED_SETTLE_LONG_CHECKS) {
|
|
174
|
+
this.log.warn("scheduled task run still in flight a week after its claim", {
|
|
175
|
+
taskId: run.taskId,
|
|
176
|
+
taskRunId,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
await oweSchedSettle(this.outbox, taskRunId, run.orgId, next, SCHED_SETTLE_RECHECK_MS);
|
|
180
|
+
}
|
|
181
|
+
/** The unfired check (see {@link settle}), task row first (the one lock order). */
|
|
182
|
+
async settleUnfired(taskRunId) {
|
|
183
|
+
const t = this.db;
|
|
184
|
+
const owner = await t
|
|
185
|
+
.selectFrom("scheduled_task_runs")
|
|
186
|
+
.select("task_id")
|
|
187
|
+
.where("id", "=", taskRunId)
|
|
188
|
+
.executeTakeFirst();
|
|
189
|
+
if (!owner)
|
|
190
|
+
return;
|
|
191
|
+
await lockTaskRow(t, owner.task_id);
|
|
192
|
+
const run = await t
|
|
193
|
+
.selectFrom("scheduled_task_runs")
|
|
194
|
+
.select(["status", "fired_at", "manual"])
|
|
195
|
+
.where("id", "=", taskRunId)
|
|
196
|
+
.forUpdate()
|
|
197
|
+
.executeTakeFirst();
|
|
198
|
+
if (run?.status !== "pending" || run.fired_at !== null)
|
|
199
|
+
return;
|
|
200
|
+
await this.skipIn(t, taskRunId, owner.task_id, run.manual ? "removed" : "missed");
|
|
201
|
+
}
|
|
140
202
|
/** Step 1 (see the class): a skip, the plan, or null when there is nothing to fire. */
|
|
141
203
|
async claim(taskRunId, orgId, deviceOffline) {
|
|
142
204
|
const t = this.db;
|
|
@@ -179,6 +241,12 @@ let ScheduledTaskFirer = class ScheduledTaskFirer {
|
|
|
179
241
|
if (!resumed && !run.manual && task.status === "paused")
|
|
180
242
|
return skip("task_paused");
|
|
181
243
|
const settings = await readScheduleSettings(t, this.config, orgId);
|
|
244
|
+
// a tick whose fire comes this late (workers down for long) is missed, as the scan records a
|
|
245
|
+
// tick it finds outside the window; a person's "run now" still runs
|
|
246
|
+
const lateAfter = settings.catchUpWindowMinutes * 60_000 + SCHED_LATE_FIRE_SLACK_MS;
|
|
247
|
+
if (!resumed && !run.manual && Date.now() - run.scheduledFor.getTime() > lateAfter) {
|
|
248
|
+
return skip("missed");
|
|
249
|
+
}
|
|
182
250
|
if (!resumed && !settings.enabled)
|
|
183
251
|
return skip("org_disabled");
|
|
184
252
|
if (!resumed && !(await this.creatorActive(orgId, task.accountId))) {
|
|
@@ -260,6 +328,8 @@ let ScheduledTaskFirer = class ScheduledTaskFirer {
|
|
|
260
328
|
firedAt: new Date(),
|
|
261
329
|
...(plan.mode === "conversation" && { conversationId: plan.conversationId }),
|
|
262
330
|
});
|
|
331
|
+
// a start that fails for good leaves the run claimed: its settle check releases or ends it
|
|
332
|
+
await oweSchedSettle(this.outbox, run.id, orgId, 0, SCHED_SETTLE_AFTER_MS);
|
|
263
333
|
return { plan: { ...plan, run: claimed ?? run } };
|
|
264
334
|
}
|
|
265
335
|
/** Step 2 (see the class). */
|
|
@@ -282,7 +352,7 @@ let ScheduledTaskFirer = class ScheduledTaskFirer {
|
|
|
282
352
|
...(task.model !== null && { model: task.model }),
|
|
283
353
|
...(task.effort !== null && { effort: task.effort }),
|
|
284
354
|
};
|
|
285
|
-
const out = await this.conversations.createWithMessage(task.orgId, task.accountId, task.agentId, { text: task.prompt, clientMessageId: run.id, ...pick }, task.deviceId === null ? {} : { deviceId: task.deviceId }, internal);
|
|
355
|
+
const out = await this.conversations.createWithMessage(task.orgId, task.accountId, task.agentId, { text: task.prompt, clientMessageId: run.id, ...pick }, task.deviceId === null ? {} : { deviceId: task.deviceId }, { ...internal, source: "web" });
|
|
286
356
|
// a retried start found the conversation made before: `createWithMessage` already asked
|
|
287
357
|
// for its run (and the first start's message owes the ensure-run backstop)
|
|
288
358
|
await this.setConversation(run.id, out.conversation.id);
|
|
@@ -345,7 +415,7 @@ let ScheduledTaskFirer = class ScheduledTaskFirer {
|
|
|
345
415
|
const { conversationId, created } = await findOrBindThread(t, plan.thread, plan.target, async () => {
|
|
346
416
|
const conv = await this.conversations.create(task.orgId, task.accountId, task.agentId,
|
|
347
417
|
// a chat task takes no device (new mode only): an IM conversation's default
|
|
348
|
-
{ im: true });
|
|
418
|
+
{ im: true }, { source: channelSource(plan.target.chatType) });
|
|
349
419
|
await t
|
|
350
420
|
.updateTable("conversations")
|
|
351
421
|
.set({ scheduled_task_id: task.id })
|
|
@@ -382,15 +452,17 @@ let ScheduledTaskFirer = class ScheduledTaskFirer {
|
|
|
382
452
|
return null;
|
|
383
453
|
if (locked.conversation_id !== null)
|
|
384
454
|
return locked.conversation_id;
|
|
455
|
+
const target = { chatId: plan.chatId, chatType: "group" };
|
|
385
456
|
const conv = await this.conversations.create(task.orgId, task.accountId, task.agentId,
|
|
386
457
|
// a chat task takes no device (new mode only): an IM conversation's default
|
|
387
|
-
{ im: true }
|
|
458
|
+
{ im: true },
|
|
459
|
+
// stored now: the thread to derive it from binds only at the run's first post
|
|
460
|
+
{ source: channelSource(target.chatType) });
|
|
388
461
|
await t
|
|
389
462
|
.updateTable("conversations")
|
|
390
463
|
.set({ scheduled_task_id: task.id })
|
|
391
464
|
.where("id", "=", conv.id)
|
|
392
465
|
.execute();
|
|
393
|
-
const target = { chatId: plan.chatId, chatType: "group" };
|
|
394
466
|
await t
|
|
395
467
|
.insertInto("channel_deliveries")
|
|
396
468
|
.values({
|
|
@@ -558,6 +630,9 @@ let ScheduledTaskFirer = class ScheduledTaskFirer {
|
|
|
558
630
|
return (await this.locks.holderOf(deviceLeaseKey(deviceId))) === null;
|
|
559
631
|
}
|
|
560
632
|
};
|
|
633
|
+
__decorate([
|
|
634
|
+
Transactional()
|
|
635
|
+
], ScheduledTaskFirer.prototype, "settle", null);
|
|
561
636
|
__decorate([
|
|
562
637
|
Transactional()
|
|
563
638
|
], ScheduledTaskFirer.prototype, "claim", null);
|
|
@@ -590,7 +665,8 @@ ScheduledTaskFirer = __decorate([
|
|
|
590
665
|
__param(9, Inject(ConversationQueue)),
|
|
591
666
|
__param(10, Inject(ChannelConnections)),
|
|
592
667
|
__param(11, Inject(LockService)),
|
|
593
|
-
__param(12, Inject(KERNEL_LOGGER))
|
|
668
|
+
__param(12, Inject(KERNEL_LOGGER)),
|
|
669
|
+
__param(13, Inject(OutboxService))
|
|
594
670
|
], ScheduledTaskFirer);
|
|
595
671
|
export { ScheduledTaskFirer };
|
|
596
672
|
/**
|
|
@@ -610,8 +686,6 @@ ScheduledTaskFireModule = __decorate([
|
|
|
610
686
|
OrgLockModule,
|
|
611
687
|
ScheduledTaskStoreModule,
|
|
612
688
|
ScheduledTaskTrackerModule,
|
|
613
|
-
// the scanner enqueues the fire jobs
|
|
614
|
-
QueueModule.registerQueue(MAINTENANCE_QUEUE_NAME),
|
|
615
689
|
],
|
|
616
690
|
providers: [ScheduledTaskFirer, ScheduledTaskScanJob],
|
|
617
691
|
exports: [ScheduledTaskFirer],
|
|
@@ -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,53 +11,35 @@ 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
|
|
19
|
+
* trigger the schedule math cannot evaluate is paused (`invalid_schedule`) instead of failing the
|
|
20
|
+
* scan; a database 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>;
|
|
33
|
+
/** One scan as of `now` (tests pass their own); returns how many due tasks it handled. */
|
|
34
|
+
scanAt(now: Date): Promise<number>;
|
|
37
35
|
/**
|
|
38
|
-
* One
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*/
|
|
42
|
-
scanAt(now: Date, repairBatch?: number): Promise<number>;
|
|
43
|
-
/**
|
|
44
|
-
* One due task in its own transaction (a task whose trigger cannot be evaluated rolls back
|
|
45
|
-
* alone and is paused, so it cannot block the others): locked with `SKIP LOCKED`, so replicas take different tasks. False
|
|
46
|
-
* when no task is due.
|
|
36
|
+
* One due task in its own transaction (a task whose trigger cannot be evaluated rolls back alone
|
|
37
|
+
* and is paused, so it cannot block the others): locked with `SKIP LOCKED`, so replicas take
|
|
38
|
+
* different tasks. False when no task is due.
|
|
47
39
|
*/
|
|
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
|
}
|