@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
|
@@ -25,7 +25,7 @@ import { notifyFinished, RunFinish } from "./run-finish.js";
|
|
|
25
25
|
import { RunJobs, runJobId, runQueueNameFor } from "./run-jobs.js";
|
|
26
26
|
import { RunQueue } from "./run-queue.js";
|
|
27
27
|
import { Waits } from "./waits.js";
|
|
28
|
-
/** The check's `@LockedInterval` job name (its run lock key
|
|
28
|
+
/** The check's `@LockedInterval` job name (its run lock key: `cron:run-orphan-check`). */
|
|
29
29
|
export const RUN_ORPHAN_CHECK_JOB = "run-orphan-check";
|
|
30
30
|
/** The check's period: a fixed constant, not a config field. */
|
|
31
31
|
export const RUN_ORPHAN_CHECK_PERIOD_MS = 60_000;
|
|
@@ -49,12 +49,16 @@ const SINCE = sql `GREATEST(COALESCE(r.queued_at, r.created_at), COALESCE(r.star
|
|
|
49
49
|
/**
|
|
50
50
|
* One page of the active runs of chat and sub-agent conversations in `statuses` that entered their
|
|
51
51
|
* state more than `minAgeMs` ago, by (state start, id), after `after` (an eval's hidden
|
|
52
|
-
* conversation drives its runs itself).
|
|
52
|
+
* conversation drives its runs itself). A deleted org's runs are never candidates, running ones
|
|
53
|
+
* included: its deletion purged its owed jobs on purpose, so their absence is no lost job and
|
|
54
|
+
* nothing is re-added, and the org's data is invisible from then on, so a running orphan is left
|
|
55
|
+
* as it is rather than failed.
|
|
53
56
|
*/
|
|
54
57
|
export async function orphanCandidates(db, statuses, minAgeMs, limit, after) {
|
|
55
58
|
let q = db
|
|
56
59
|
.selectFrom("runs as r")
|
|
57
60
|
.innerJoin("conversations as c", "c.id", "r.conversation_id")
|
|
61
|
+
.innerJoin("orgs as o", "o.id", "c.org_id")
|
|
58
62
|
.select([
|
|
59
63
|
"r.id",
|
|
60
64
|
"r.conversation_id",
|
|
@@ -67,6 +71,7 @@ export async function orphanCandidates(db, statuses, minAgeMs, limit, after) {
|
|
|
67
71
|
])
|
|
68
72
|
.where("r.status", "in", statuses)
|
|
69
73
|
.where("c.kind", "in", ["chat", "subagent"])
|
|
74
|
+
.where("o.deleted_at", "is", null)
|
|
70
75
|
.where(SINCE, "<", sql `now() - ${minAgeMs}::integer * interval '1 millisecond'`);
|
|
71
76
|
if (after !== undefined) {
|
|
72
77
|
q = q.where(sql `(${SINCE}, r.id) > (${after.since}::timestamptz, ${after.runId})`);
|
|
@@ -110,12 +115,12 @@ async function* candidatePages(db, statuses, minAgeMs) {
|
|
|
110
115
|
}
|
|
111
116
|
}
|
|
112
117
|
/**
|
|
113
|
-
* The `queued` runs (in their state for over `minAgeMs`) whose job is neither in the outbox
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* run again after both (a job that finished took its run out of `queued` first), so a run
|
|
117
|
-
* flight is never reported. The `run-orphan-check` uses it; so does the dual-replica test
|
|
118
|
-
* which asserts at its teardown that the kernel left none (`minAgeMs` 0).
|
|
118
|
+
* The `queued` runs (in their state for over `minAgeMs`) whose job is neither in the outbox (owed,
|
|
119
|
+
* or dead) nor in BullMQ (`jobExists`), and that are still queued after both looks: the run queue
|
|
120
|
+
* lost their jobs. The outbox is read before BullMQ (a row is deleted only after its job was added)
|
|
121
|
+
* and the run again after both (a job that finished took its run out of `queued` first), so a run
|
|
122
|
+
* in flight is never reported. The `run-orphan-check` uses it; so does the dual-replica test
|
|
123
|
+
* harness, which asserts at its teardown that the kernel left none (`minAgeMs` 0).
|
|
119
124
|
*/
|
|
120
125
|
export async function queuedRunsWithoutJob(db, jobExists, opts) {
|
|
121
126
|
const out = [];
|
|
@@ -146,9 +151,9 @@ export async function queuedRunsWithoutJob(db, jobExists, opts) {
|
|
|
146
151
|
* `run.orphaned` (its terminal transaction cancels its waits and owes what every run end owes),
|
|
147
152
|
* then, after the commit, its checkpoints are dropped, its fork child stopped (`run.orphaned`), a
|
|
148
153
|
* sub-agent's failure delivered to its parent at once and the conversation's next run started.
|
|
149
|
-
* - `queued`, with no outbox row either (pending or dead): a canary. Every queued run has its job
|
|
150
|
-
* own transaction, so this means Redis lost data. It is logged as an error, counted
|
|
151
|
-
* overview ({@link QueueLostJobs}) and its job owed again so the run is not stuck.
|
|
154
|
+
* - `queued`, with no outbox row either (pending or dead): a canary. Every queued run has its job
|
|
155
|
+
* owed in its own transaction, so this means Redis lost data. It is logged as an error, counted
|
|
156
|
+
* for the admin overview ({@link QueueLostJobs}) and its job owed again so the run is not stuck.
|
|
152
157
|
* Redis is read before each repair's transaction, which then re-reads the run under its
|
|
153
158
|
* conversation row lock and acts only if it is still in the same state since the same time. Read
|
|
154
159
|
* paths never repair runs.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Logger } from "@capybara-hq/plugin-sdk";
|
|
2
|
-
import type { Queue } from "bullmq";
|
|
3
2
|
import { EventLog } from "../conversations/event-log.js";
|
|
4
3
|
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
5
4
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
@@ -32,14 +31,10 @@ export declare class RunQueue {
|
|
|
32
31
|
private readonly log;
|
|
33
32
|
readonly txHost: TransactionHost;
|
|
34
33
|
private readonly config;
|
|
35
|
-
/** The producer side of the delayed `bg-wake` job (`MaintenanceProcessor` consumes it). */
|
|
36
|
-
private readonly maintenance;
|
|
37
34
|
private readonly ensureFails;
|
|
38
35
|
constructor(kdb: KernelDb, eventLog: EventLog, lock: Pick<ConversationLock, "holder">, jobs: RunJobs, finisher: Pick<RunFinish, "finishIn">,
|
|
39
36
|
/** An orphan aborted directly loses its checkpoint thread (thread_id = runId). */
|
|
40
|
-
checkpoints: Pick<KyselyCheckpointSaver, "deleteThread">, log: Pick<Logger, "error" | "warn">, txHost: TransactionHost, config: KernelConfigService,
|
|
41
|
-
/** The producer side of the delayed `bg-wake` job (`MaintenanceProcessor` consumes it). */
|
|
42
|
-
maintenance: Pick<Queue, "add">, ensureFails?: () => boolean);
|
|
37
|
+
checkpoints: Pick<KyselyCheckpointSaver, "deleteThread">, log: Pick<Logger, "error" | "warn">, txHost: TransactionHost, config: KernelConfigService, ensureFails?: () => boolean);
|
|
43
38
|
private get db();
|
|
44
39
|
/**
|
|
45
40
|
* 有未消费的用户消息(最新 user_message 的 seq > `consumed_seq`)、会话锁无人持有、
|
|
@@ -51,7 +46,7 @@ export declare class RunQueue {
|
|
|
51
46
|
* once it is sent; with no head either, a wake-eligible finished background task or sub-agent
|
|
52
47
|
* delivery (background Bash spec §3, agent map spec §5.3, {@link wakeDecisionIn}): the run is
|
|
53
48
|
* then attributed to the event's trigger account and joins the tree's budget window, and an
|
|
54
|
-
* event inside the wake window
|
|
49
|
+
* event inside the wake window owes the delayed `bg-wake` job instead. A sub-agent's
|
|
55
50
|
* conversation is only woken this way ({@link childWakeIn}).
|
|
56
51
|
*
|
|
57
52
|
* An active run (queued, running or waiting) is left alone: a run nobody runs any more is the
|
|
@@ -77,8 +72,9 @@ export declare class RunQueue {
|
|
|
77
72
|
private jobIdFor;
|
|
78
73
|
/**
|
|
79
74
|
* Owes the new run's job in {@link ensureRunIn}'s transaction, before its run row is written.
|
|
80
|
-
* False (logged): the same job id is still owed, so the caller writes no run
|
|
81
|
-
*
|
|
75
|
+
* False (logged): the same job id is still owed, so the caller writes no run. A later run of the
|
|
76
|
+
* same trigger has its own `<runJobId>-<n>` ({@link jobIdFor}), so this is a should-not-happen
|
|
77
|
+
* (the conversation has one active run at a time).
|
|
82
78
|
*/
|
|
83
79
|
private oweJob;
|
|
84
80
|
/**
|
|
@@ -89,15 +85,17 @@ export declare class RunQueue {
|
|
|
89
85
|
* racing the run's end starts a new run, never lost); and it is woken by its own background
|
|
90
86
|
* task's finish or a delivery from its own sub-agent, under the same rules as a chat
|
|
91
87
|
* conversation. Neither happens once its last run was stopped (`aborted`): a stopped sub-agent is
|
|
92
|
-
* started again only by its parent
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
88
|
+
* started again only by its parent; nor while a person's stop of the root holds the tree
|
|
89
|
+
* (`conversations.tree_stopped_at`, cleared by the root's next run). Both read the queue and the
|
|
90
|
+
* trigger account's access on the root of its tree; a message needs no wake setting and has no
|
|
91
|
+
* wake window. The run counts against the root's budget window and runs on the sub-agent queue
|
|
92
|
+
* under `runJobId(child, boundary seq)`. An active run is never started past (one nobody runs is
|
|
93
|
+
* the `run-orphan-check`'s).
|
|
96
94
|
*/
|
|
97
95
|
private childWakeIn;
|
|
98
96
|
/**
|
|
99
|
-
* The newest unconsumed message of an idle sub-agent (a `SendMessage` its last run did not
|
|
100
|
-
* as a run boundary, or null: none; the root's queue is paused or the root deleted (the
|
|
97
|
+
* The newest unconsumed message of an idle sub-agent (a `SendMessage` its last run did not
|
|
98
|
+
* absorb) as a run boundary, or null: none; the root's queue is paused or the root deleted (the
|
|
101
99
|
* message waits for the unpause, {@link wakeHeldTree}); or its author can no longer send to the
|
|
102
100
|
* root (it waits for a later run). The caller already refused a stopped sub-agent (last run
|
|
103
101
|
* `aborted`): a message that reached it while running and then stopped is parked, and the next
|
|
@@ -126,13 +124,6 @@ export declare class RunQueue {
|
|
|
126
124
|
* with their org and newest seq. Reads only (no row locks).
|
|
127
125
|
*/
|
|
128
126
|
private heldTree;
|
|
129
|
-
/**
|
|
130
|
-
* Schedules the delayed `bg-wake` job at the end of the conversation's wake window (after the
|
|
131
|
-
* commit): one job per conversation and window end, so finishes inside one window share it. It
|
|
132
|
-
* calls {@link ensureRun}, which wakes the conversation if a finish is still unconsumed. A failed
|
|
133
|
-
* enqueue is logged: the next message, run end or page opening re-derives the wake.
|
|
134
|
-
*/
|
|
135
|
-
private scheduleWake;
|
|
136
127
|
/**
|
|
137
128
|
* Owes the ensure-run backstop of `conversationId` in the caller's transaction
|
|
138
129
|
* ({@link oweEnsureRun}): `seq` is the event the transaction wrote, `orgId` the conversation's.
|
|
@@ -141,13 +132,27 @@ export declare class RunQueue {
|
|
|
141
132
|
/**
|
|
142
133
|
* Stops an orphaned run directly (`POST .../abort`, a tree stop): a `running` run whose
|
|
143
134
|
* conversation lock is free and whose BullMQ job is gone hears no stop broadcast, so its
|
|
144
|
-
* `run_aborted` is written here (status aborted, `consumed_seq` at least to its trigger): true.
|
|
145
|
-
* an orphan (someone runs it; a queued run's job is owed, or lost and re-added by the
|
|
135
|
+
* `run_aborted` is written here (status aborted, `consumed_seq` at least to its trigger): true.
|
|
136
|
+
* Not an orphan (someone runs it; a queued run's job is owed, or lost and re-added by the
|
|
146
137
|
* `run-orphan-check`, whose worker then sees the stop marker) or already finished: false, and the
|
|
147
138
|
* caller broadcasts the stop.
|
|
148
139
|
*/
|
|
149
140
|
abortIfOrphaned(conversationId: string, runId: string, byAccountId: string | null, detail?: RunAbortDetail): Promise<boolean>;
|
|
150
141
|
private abortOrphanIn;
|
|
142
|
+
/**
|
|
143
|
+
* Ends a `queued` run in PG before any worker started it (an offboarded member's run, ruling
|
|
144
|
+
* R27): with the conversation lock free, under the conversation row lock, a run still `queued` is
|
|
145
|
+
* finished `aborted` with `run_aborted` (`consumed_seq` at least to its trigger), so a job that
|
|
146
|
+
* runs later finds it finished and starts nothing. True when it did; false when a worker holds
|
|
147
|
+
* the conversation or is in the middle of the run's first start (`run_started` written,
|
|
148
|
+
* `started_at` not yet set; it hears the stop), or the run is no longer queued. A run resumed
|
|
149
|
+
* from `waiting` to `queued` is ended like a new one. The caller writes the stop marker first, so
|
|
150
|
+
* a worker that takes the conversation meanwhile still stops; a worker that read the run before
|
|
151
|
+
* this commit re-checks it under the row locks before its `run_started` and never moves it back
|
|
152
|
+
* to `running`.
|
|
153
|
+
*/
|
|
154
|
+
abortQueued(conversationId: string, runId: string, byAccountId: string | null): Promise<boolean>;
|
|
155
|
+
private abortQueuedIn;
|
|
151
156
|
/** 会话当前未结束的运行(调用方已持有会话行锁)。 */
|
|
152
157
|
private activeRun;
|
|
153
158
|
/**
|
|
@@ -8,11 +8,10 @@ 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, Optional } from "@nestjs/common";
|
|
13
12
|
import { sql } from "kysely";
|
|
14
13
|
import { visibleTo } from "../agents/agent-lookup.js";
|
|
15
|
-
import {
|
|
14
|
+
import { wakeDecisionIn } from "../background/wake-rules.js";
|
|
16
15
|
import { EventLog } from "../conversations/event-log.js";
|
|
17
16
|
import { dispatchQueuedIn, pauseQueueWith } from "../conversations/queue.js";
|
|
18
17
|
import { conversationTree } from "../conversations/tree.js";
|
|
@@ -25,7 +24,6 @@ import { linkStartedRunIn } from "../schedules/run-tracker.js";
|
|
|
25
24
|
import { scheduledTaskRunAt } from "../schedules/unattended.js";
|
|
26
25
|
import { KyselyCheckpointSaver } from "./checkpointer.js";
|
|
27
26
|
import { ConversationLock } from "./conversation-lock.js";
|
|
28
|
-
import { MAINTENANCE_QUEUE_NAME } from "./maintenance-queue.js";
|
|
29
27
|
import { notifyFinished, RunFinish } from "./run-finish.js";
|
|
30
28
|
import { ACTIVE_RUN_STATUSES, RunJobs, runJobId, runQueueNameFor } from "./run-jobs.js";
|
|
31
29
|
import { rootConversationOf } from "./subagents/child.js";
|
|
@@ -37,12 +35,6 @@ export { ACTIVE_RUN_STATUSES, ENSURE_RUN_BACKSTOP_MS, ENSURE_RUN_JOB, ensureRunJ
|
|
|
37
35
|
* kernel never provides it.
|
|
38
36
|
*/
|
|
39
37
|
export const ENSURE_RUN_FAILS = Symbol("ENSURE_RUN_FAILS");
|
|
40
|
-
/**
|
|
41
|
-
* Margin added to the delayed wake job: its window is read on the database's clock, the delay runs
|
|
42
|
-
* on this process's and Redis's, so the job does not usually fire just before the window ends (one
|
|
43
|
-
* that does reschedules itself under its next retry id, `bgWakeJobId`).
|
|
44
|
-
*/
|
|
45
|
-
const WAKE_JOB_MARGIN_MS = 1000;
|
|
46
38
|
/**
|
|
47
39
|
* 运行队列(生产方):`ensureRun` 在会话有未消费的用户消息、且没有进行中的运行时插入 `runs` 行并投递任务。
|
|
48
40
|
* 「是否已有运行」以 PG 为准(会话行锁内检查 `runs` 中的 queued / running),并参考会话锁的持有者;
|
|
@@ -58,13 +50,10 @@ let RunQueue = class RunQueue {
|
|
|
58
50
|
log;
|
|
59
51
|
txHost;
|
|
60
52
|
config;
|
|
61
|
-
maintenance;
|
|
62
53
|
ensureFails;
|
|
63
54
|
constructor(kdb, eventLog, lock, jobs, finisher,
|
|
64
55
|
/** An orphan aborted directly loses its checkpoint thread (thread_id = runId). */
|
|
65
|
-
checkpoints, log, txHost, config,
|
|
66
|
-
/** The producer side of the delayed `bg-wake` job (`MaintenanceProcessor` consumes it). */
|
|
67
|
-
maintenance, ensureFails = () => false) {
|
|
56
|
+
checkpoints, log, txHost, config, ensureFails = () => false) {
|
|
68
57
|
this.kdb = kdb;
|
|
69
58
|
this.eventLog = eventLog;
|
|
70
59
|
this.lock = lock;
|
|
@@ -74,7 +63,6 @@ let RunQueue = class RunQueue {
|
|
|
74
63
|
this.log = log;
|
|
75
64
|
this.txHost = txHost;
|
|
76
65
|
this.config = config;
|
|
77
|
-
this.maintenance = maintenance;
|
|
78
66
|
this.ensureFails = ensureFails;
|
|
79
67
|
}
|
|
80
68
|
get db() {
|
|
@@ -90,7 +78,7 @@ let RunQueue = class RunQueue {
|
|
|
90
78
|
* once it is sent; with no head either, a wake-eligible finished background task or sub-agent
|
|
91
79
|
* delivery (background Bash spec §3, agent map spec §5.3, {@link wakeDecisionIn}): the run is
|
|
92
80
|
* then attributed to the event's trigger account and joins the tree's budget window, and an
|
|
93
|
-
* event inside the wake window
|
|
81
|
+
* event inside the wake window owes the delayed `bg-wake` job instead. A sub-agent's
|
|
94
82
|
* conversation is only woken this way ({@link childWakeIn}).
|
|
95
83
|
*
|
|
96
84
|
* An active run (queued, running or waiting) is left alone: a run nobody runs any more is the
|
|
@@ -111,12 +99,9 @@ let RunQueue = class RunQueue {
|
|
|
111
99
|
dispatchedSwitch: false,
|
|
112
100
|
dispatchedUnarchived: false,
|
|
113
101
|
pausedNow: false,
|
|
114
|
-
|
|
102
|
+
wakeRetry: opts.wakeRetry ?? 0,
|
|
115
103
|
};
|
|
116
104
|
const created = await this.ensureRunIn(conversationId, st);
|
|
117
|
-
if (st.wakeLater !== null) {
|
|
118
|
-
await this.scheduleWake(conversationId, st.wakeLater, opts.wakeRetry ?? 0);
|
|
119
|
-
}
|
|
120
105
|
const { dispatchedSeq, dispatchedSwitch, dispatchedUnarchived, pausedNow } = st;
|
|
121
106
|
// a pause is published too (batch 5 review I1): the page shows the reason without a reload
|
|
122
107
|
if (pausedNow)
|
|
@@ -144,6 +129,7 @@ let RunQueue = class RunQueue {
|
|
|
144
129
|
"queue_paused",
|
|
145
130
|
"account_id",
|
|
146
131
|
"deleted_at",
|
|
132
|
+
"tree_stopped_at",
|
|
147
133
|
])
|
|
148
134
|
.where("id", "=", conversationId)
|
|
149
135
|
.forUpdate()
|
|
@@ -227,8 +213,11 @@ let RunQueue = class RunQueue {
|
|
|
227
213
|
// Nothing queued either: a finished background task may wake the idle conversation
|
|
228
214
|
// (background Bash spec §3; a person's message or the queue always goes first)
|
|
229
215
|
const wake = await wakeDecisionIn(t, { id: conversationId, orgId: conv.org_id, rootId: conversationId }, Number(conv.consumed_seq), this.config.get().runtime.wakeOnBackgroundTask, (accountId) => authorCanSend(t, conversationId, accountId));
|
|
230
|
-
|
|
231
|
-
|
|
216
|
+
// inside the wake window: the delayed `bg-wake` job is owed here, so the wake survives a
|
|
217
|
+
// process that dies after the commit (transactional outbox spec §3.1)
|
|
218
|
+
if (wake?.kind === "later") {
|
|
219
|
+
await this.jobs.oweBgWake(conversationId, wake, st.wakeRetry, conv.org_id);
|
|
220
|
+
}
|
|
232
221
|
if (wake?.kind !== "wake")
|
|
233
222
|
return null;
|
|
234
223
|
last = { seq: String(wake.seq), accountId: wake.triggerAccountId };
|
|
@@ -284,6 +273,15 @@ let RunQueue = class RunQueue {
|
|
|
284
273
|
// the task run its message started follows this run from now on (spec §6)
|
|
285
274
|
if (taskRunId !== null)
|
|
286
275
|
await linkStartedRunIn(t, taskRunId, runId, conversationId);
|
|
276
|
+
// the root runs again: a person's stop of its tree no longer holds the tree's wakes
|
|
277
|
+
// (agent map spec §6); read with the row lock above, so the hot path issues no statement
|
|
278
|
+
if (conv.tree_stopped_at !== null) {
|
|
279
|
+
await t
|
|
280
|
+
.updateTable("conversations")
|
|
281
|
+
.set({ tree_stopped_at: null })
|
|
282
|
+
.where("id", "=", conversationId)
|
|
283
|
+
.execute();
|
|
284
|
+
}
|
|
287
285
|
return runId;
|
|
288
286
|
}
|
|
289
287
|
/**
|
|
@@ -308,8 +306,9 @@ let RunQueue = class RunQueue {
|
|
|
308
306
|
}
|
|
309
307
|
/**
|
|
310
308
|
* Owes the new run's job in {@link ensureRunIn}'s transaction, before its run row is written.
|
|
311
|
-
* False (logged): the same job id is still owed, so the caller writes no run
|
|
312
|
-
*
|
|
309
|
+
* False (logged): the same job id is still owed, so the caller writes no run. A later run of the
|
|
310
|
+
* same trigger has its own `<runJobId>-<n>` ({@link jobIdFor}), so this is a should-not-happen
|
|
311
|
+
* (the conversation has one active run at a time).
|
|
313
312
|
*/
|
|
314
313
|
async oweJob(conversationId, runId, jobId, kind, orgId) {
|
|
315
314
|
if (await this.jobs.owe({ runId, conversationId }, jobId, kind, orgId))
|
|
@@ -328,10 +327,12 @@ let RunQueue = class RunQueue {
|
|
|
328
327
|
* racing the run's end starts a new run, never lost); and it is woken by its own background
|
|
329
328
|
* task's finish or a delivery from its own sub-agent, under the same rules as a chat
|
|
330
329
|
* conversation. Neither happens once its last run was stopped (`aborted`): a stopped sub-agent is
|
|
331
|
-
* started again only by its parent
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
330
|
+
* started again only by its parent; nor while a person's stop of the root holds the tree
|
|
331
|
+
* (`conversations.tree_stopped_at`, cleared by the root's next run). Both read the queue and the
|
|
332
|
+
* trigger account's access on the root of its tree; a message needs no wake setting and has no
|
|
333
|
+
* wake window. The run counts against the root's budget window and runs on the sub-agent queue
|
|
334
|
+
* under `runJobId(child, boundary seq)`. An active run is never started past (one nobody runs is
|
|
335
|
+
* the `run-orphan-check`'s).
|
|
335
336
|
*/
|
|
336
337
|
async childWakeIn(conversationId, conv, st) {
|
|
337
338
|
const t = this.db;
|
|
@@ -353,10 +354,16 @@ let RunQueue = class RunQueue {
|
|
|
353
354
|
if (last?.status === "aborted")
|
|
354
355
|
return null;
|
|
355
356
|
const rootId = await rootConversationOf(t, conversationId);
|
|
357
|
+
// a tree a person stopped at its root (agent map spec §6, changed in acceptance 2026-11-13)
|
|
358
|
+
// starts no sub-agent run here until the root runs again: an idle sub-agent the stop found
|
|
359
|
+
// with nothing running (or one whose report raced the stop) is not woken by a late finish
|
|
360
|
+
if (await treeStopped(t, rootId))
|
|
361
|
+
return null;
|
|
356
362
|
const wake = (await this.pendingMessageIn(conversationId, Number(conv.consumed_seq), rootId)) ??
|
|
357
363
|
(await wakeDecisionIn(t, { id: conversationId, orgId: conv.org_id, rootId }, Number(conv.consumed_seq), this.config.get().runtime.wakeOnBackgroundTask, (accountId) => authorCanSend(t, rootId, accountId)));
|
|
358
|
-
if (wake?.kind === "later")
|
|
359
|
-
st.
|
|
364
|
+
if (wake?.kind === "later") {
|
|
365
|
+
await this.jobs.oweBgWake(conversationId, wake, st.wakeRetry, conv.org_id);
|
|
366
|
+
}
|
|
360
367
|
if (wake?.kind !== "wake")
|
|
361
368
|
return null;
|
|
362
369
|
const jobId = await this.jobIdFor(conversationId, wake.seq);
|
|
@@ -397,8 +404,8 @@ let RunQueue = class RunQueue {
|
|
|
397
404
|
return runId;
|
|
398
405
|
}
|
|
399
406
|
/**
|
|
400
|
-
* The newest unconsumed message of an idle sub-agent (a `SendMessage` its last run did not
|
|
401
|
-
* as a run boundary, or null: none; the root's queue is paused or the root deleted (the
|
|
407
|
+
* The newest unconsumed message of an idle sub-agent (a `SendMessage` its last run did not
|
|
408
|
+
* absorb) as a run boundary, or null: none; the root's queue is paused or the root deleted (the
|
|
402
409
|
* message waits for the unpause, {@link wakeHeldTree}); or its author can no longer send to the
|
|
403
410
|
* root (it waits for a later run). The caller already refused a stopped sub-agent (last run
|
|
404
411
|
* `aborted`): a message that reached it while running and then stopped is parked, and the next
|
|
@@ -496,27 +503,6 @@ let RunQueue = class RunQueue {
|
|
|
496
503
|
.where("r.status", "in", ACTIVE_RUN_STATUSES))))
|
|
497
504
|
.execute();
|
|
498
505
|
}
|
|
499
|
-
/**
|
|
500
|
-
* Schedules the delayed `bg-wake` job at the end of the conversation's wake window (after the
|
|
501
|
-
* commit): one job per conversation and window end, so finishes inside one window share it. It
|
|
502
|
-
* calls {@link ensureRun}, which wakes the conversation if a finish is still unconsumed. A failed
|
|
503
|
-
* enqueue is logged: the next message, run end or page opening re-derives the wake.
|
|
504
|
-
*/
|
|
505
|
-
async scheduleWake(conversationId, w, retry) {
|
|
506
|
-
const data = { conversationId, ...(retry > 0 && { retry }) };
|
|
507
|
-
try {
|
|
508
|
-
await this.maintenance.add(BG_WAKE_JOB, data, {
|
|
509
|
-
// a job that fired early reschedules under a new id (its own is still taken)
|
|
510
|
-
jobId: bgWakeJobId(conversationId, w.windowEnd, retry),
|
|
511
|
-
delay: w.delayMs + WAKE_JOB_MARGIN_MS,
|
|
512
|
-
removeOnComplete: true,
|
|
513
|
-
removeOnFail: true,
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
catch (err) {
|
|
517
|
-
this.log.error("cannot schedule the background task wake", { conversationId, err });
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
506
|
/**
|
|
521
507
|
* Owes the ensure-run backstop of `conversationId` in the caller's transaction
|
|
522
508
|
* ({@link oweEnsureRun}): `seq` is the event the transaction wrote, `orgId` the conversation's.
|
|
@@ -527,8 +513,8 @@ let RunQueue = class RunQueue {
|
|
|
527
513
|
/**
|
|
528
514
|
* Stops an orphaned run directly (`POST .../abort`, a tree stop): a `running` run whose
|
|
529
515
|
* conversation lock is free and whose BullMQ job is gone hears no stop broadcast, so its
|
|
530
|
-
* `run_aborted` is written here (status aborted, `consumed_seq` at least to its trigger): true.
|
|
531
|
-
* an orphan (someone runs it; a queued run's job is owed, or lost and re-added by the
|
|
516
|
+
* `run_aborted` is written here (status aborted, `consumed_seq` at least to its trigger): true.
|
|
517
|
+
* Not an orphan (someone runs it; a queued run's job is owed, or lost and re-added by the
|
|
532
518
|
* `run-orphan-check`, whose worker then sees the stop marker) or already finished: false, and the
|
|
533
519
|
* caller broadcasts the stop.
|
|
534
520
|
*/
|
|
@@ -567,6 +553,67 @@ let RunQueue = class RunQueue {
|
|
|
567
553
|
},
|
|
568
554
|
});
|
|
569
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* Ends a `queued` run in PG before any worker started it (an offboarded member's run, ruling
|
|
558
|
+
* R27): with the conversation lock free, under the conversation row lock, a run still `queued` is
|
|
559
|
+
* finished `aborted` with `run_aborted` (`consumed_seq` at least to its trigger), so a job that
|
|
560
|
+
* runs later finds it finished and starts nothing. True when it did; false when a worker holds
|
|
561
|
+
* the conversation or is in the middle of the run's first start (`run_started` written,
|
|
562
|
+
* `started_at` not yet set; it hears the stop), or the run is no longer queued. A run resumed
|
|
563
|
+
* from `waiting` to `queued` is ended like a new one. The caller writes the stop marker first, so
|
|
564
|
+
* a worker that takes the conversation meanwhile still stops; a worker that read the run before
|
|
565
|
+
* this commit re-checks it under the row locks before its `run_started` and never moves it back
|
|
566
|
+
* to `running`.
|
|
567
|
+
*/
|
|
568
|
+
async abortQueued(conversationId, runId, byAccountId) {
|
|
569
|
+
if ((await this.lock.holder(conversationId)) !== null)
|
|
570
|
+
return false;
|
|
571
|
+
const toSeq = await this.abortQueuedIn(conversationId, runId, byAccountId);
|
|
572
|
+
if (toSeq === null)
|
|
573
|
+
return false;
|
|
574
|
+
await notifyFinished(this.eventLog, conversationId, toSeq, "aborted");
|
|
575
|
+
await this.dropCheckpoints(runId);
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
async abortQueuedIn(conversationId, runId, byAccountId) {
|
|
579
|
+
const t = this.db;
|
|
580
|
+
const conv = await t
|
|
581
|
+
.selectFrom("conversations")
|
|
582
|
+
.select("id")
|
|
583
|
+
.where("id", "=", conversationId)
|
|
584
|
+
.forUpdate()
|
|
585
|
+
.executeTakeFirst();
|
|
586
|
+
const active = await this.activeRun(t, conversationId);
|
|
587
|
+
if (!conv || !active || active.id !== runId || active.status !== "queued")
|
|
588
|
+
return null;
|
|
589
|
+
// a worker is in the middle of the run's FIRST start (its `run_started` written, `started_at`
|
|
590
|
+
// not set yet; it holds the conversation since): the stop marker ends it. A run resumed from
|
|
591
|
+
// `waiting` back to `queued` has both from its first start and is ended here like a new one:
|
|
592
|
+
// its worker skips `run_started` (it exists) and its conditional `running` update finds the run
|
|
593
|
+
// finished
|
|
594
|
+
const first = await t
|
|
595
|
+
.selectFrom("runs")
|
|
596
|
+
.select("started_at")
|
|
597
|
+
.where("id", "=", runId)
|
|
598
|
+
.executeTakeFirst();
|
|
599
|
+
if (first?.started_at === null) {
|
|
600
|
+
const started = await t
|
|
601
|
+
.selectFrom("conversation_events")
|
|
602
|
+
.select("seq")
|
|
603
|
+
.where("conversation_id", "=", conversationId)
|
|
604
|
+
.where("idem_key", "=", `run_started:${runId}`)
|
|
605
|
+
.executeTakeFirst();
|
|
606
|
+
if (started)
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
return this.finisher.finishIn({
|
|
610
|
+
conversationId,
|
|
611
|
+
runId,
|
|
612
|
+
status: "aborted",
|
|
613
|
+
consumed: null,
|
|
614
|
+
event: { type: "run_aborted", payload: { byAccountId, reason: "user" } },
|
|
615
|
+
});
|
|
616
|
+
}
|
|
570
617
|
/** 会话当前未结束的运行(调用方已持有会话行锁)。 */
|
|
571
618
|
activeRun(t, conversationId) {
|
|
572
619
|
return t
|
|
@@ -604,6 +651,9 @@ __decorate([
|
|
|
604
651
|
__decorate([
|
|
605
652
|
Transactional({ propagation: "requires_new" })
|
|
606
653
|
], RunQueue.prototype, "abortOrphanIn", null);
|
|
654
|
+
__decorate([
|
|
655
|
+
Transactional({ propagation: "requires_new" })
|
|
656
|
+
], RunQueue.prototype, "abortQueuedIn", null);
|
|
607
657
|
RunQueue = __decorate([
|
|
608
658
|
Injectable(),
|
|
609
659
|
__param(0, Inject(KernelDb)),
|
|
@@ -615,9 +665,8 @@ RunQueue = __decorate([
|
|
|
615
665
|
__param(6, Inject(KERNEL_LOGGER)),
|
|
616
666
|
__param(7, Inject(TransactionHost)),
|
|
617
667
|
__param(8, Inject(KernelConfigService)),
|
|
618
|
-
__param(9,
|
|
619
|
-
__param(
|
|
620
|
-
__param(10, Inject(ENSURE_RUN_FAILS))
|
|
668
|
+
__param(9, Optional()),
|
|
669
|
+
__param(9, Inject(ENSURE_RUN_FAILS))
|
|
621
670
|
], RunQueue);
|
|
622
671
|
export { RunQueue };
|
|
623
672
|
/** The conversation's owner is active and can still see its agent (`isAccessible`'s rule). */
|
|
@@ -642,3 +691,15 @@ async function authorCanSend(t, conversationId, accountId) {
|
|
|
642
691
|
.executeTakeFirst();
|
|
643
692
|
return row !== undefined;
|
|
644
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Whether a person's stop of the root conversation holds its tree (`tree_stopped_at`, agent map
|
|
696
|
+
* spec §6): set by the stop, cleared when the root next starts a run.
|
|
697
|
+
*/
|
|
698
|
+
async function treeStopped(t, rootId) {
|
|
699
|
+
const root = await t
|
|
700
|
+
.selectFrom("conversations")
|
|
701
|
+
.select("tree_stopped_at")
|
|
702
|
+
.where("id", "=", rootId)
|
|
703
|
+
.executeTakeFirst();
|
|
704
|
+
return root?.tree_stopped_at != null;
|
|
705
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { KernelSchemaDb } from "../db/kernel-schema.js";
|
|
2
|
+
/** 运行的终态。 */
|
|
3
|
+
export type TerminalStatus = "completed" | "failed" | "aborted";
|
|
4
|
+
/** The terminal run statuses (a leaf module: the event writers of a run start import it too). */
|
|
5
|
+
export declare const TERMINAL_STATUSES: ReadonlySet<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Whether run `runId` has ended, read in the caller's transaction with its row locked: call it
|
|
8
|
+
* after the conversation row lock (lock order conversation, then run, as `RunFinish.finishIn`), so
|
|
9
|
+
* no stop can end the run between this read and the transaction's appends. A missing run is not
|
|
10
|
+
* ended (a caller may record for a run id without a row).
|
|
11
|
+
*/
|
|
12
|
+
export declare function runEndedIn(db: KernelSchemaDb, runId: string): Promise<boolean>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** The terminal run statuses (a leaf module: the event writers of a run start import it too). */
|
|
2
|
+
export const TERMINAL_STATUSES = new Set([
|
|
3
|
+
"completed",
|
|
4
|
+
"failed",
|
|
5
|
+
"aborted",
|
|
6
|
+
]);
|
|
7
|
+
/**
|
|
8
|
+
* Whether run `runId` has ended, read in the caller's transaction with its row locked: call it
|
|
9
|
+
* after the conversation row lock (lock order conversation, then run, as `RunFinish.finishIn`), so
|
|
10
|
+
* no stop can end the run between this read and the transaction's appends. A missing run is not
|
|
11
|
+
* ended (a caller may record for a run id without a row).
|
|
12
|
+
*/
|
|
13
|
+
export async function runEndedIn(db, runId) {
|
|
14
|
+
const row = await db
|
|
15
|
+
.selectFrom("runs")
|
|
16
|
+
.select("status")
|
|
17
|
+
.where("id", "=", runId)
|
|
18
|
+
.forUpdate()
|
|
19
|
+
.executeTakeFirst();
|
|
20
|
+
return row !== undefined && TERMINAL_STATUSES.has(row.status);
|
|
21
|
+
}
|
|
@@ -8,9 +8,11 @@ import { KernelDb } from "../infra/database/kernel-db.js";
|
|
|
8
8
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
9
9
|
import { KernelEvents } from "../infra/events/kernel-events.js";
|
|
10
10
|
import { SkillsCatalogService } from "../skills/catalog.js";
|
|
11
|
+
import { ToolRegistry } from "../tools/registry.js";
|
|
11
12
|
import { RunAbort } from "./abort.js";
|
|
12
13
|
import { KyselyCheckpointSaver } from "./checkpointer.js";
|
|
13
14
|
import { ConversationLock } from "./conversation-lock.js";
|
|
15
|
+
import { PluginContextService } from "./plugin-context.service.js";
|
|
14
16
|
import { ProviderRegistry } from "./providers.js";
|
|
15
17
|
import { type OrgQuota } from "./quota.js";
|
|
16
18
|
import { RunContextService } from "./run-context.service.js";
|
|
@@ -32,6 +34,12 @@ export interface RunHooks {
|
|
|
32
34
|
lockTtlMs?: number;
|
|
33
35
|
/** 任务结束后的每次 ensureRun 尝试之前调用(attempt 从 0 起);抛错即模拟该次尝试失败。 */
|
|
34
36
|
beforeEnsureRun?(conversationId: string, attempt: number): Promise<void>;
|
|
37
|
+
/** Called after a run job read the run's status, before it takes the conversation lock. */
|
|
38
|
+
afterRunRead?(runId: string, conversationId: string): Promise<void>;
|
|
39
|
+
/** Called with the conversation lock held, before a queued run's `run_started` is written. */
|
|
40
|
+
beforeRunStarted?(runId: string, conversationId: string): Promise<void>;
|
|
41
|
+
/** Called with the conversation lock held, before a queued run's status becomes `running`. */
|
|
42
|
+
beforeRunning?(runId: string, conversationId: string): Promise<void>;
|
|
35
43
|
}
|
|
36
44
|
/** 处理函数的返回值:`ensure` 表示任务完成后再调用一次 ensureRun。 */
|
|
37
45
|
export type ProcessResult = "ensure" | undefined;
|
|
@@ -65,6 +73,10 @@ export declare class RunWorker {
|
|
|
65
73
|
private readonly skillsCatalog;
|
|
66
74
|
/** Run start's run context (run context spec §5): who the agent is, the day, the person. */
|
|
67
75
|
private readonly runContext;
|
|
76
|
+
/** Run start's plugin context (memory plugin spec §6.1): the `context@1` providers' text. */
|
|
77
|
+
private readonly pluginContext;
|
|
78
|
+
/** The run's resolved tools, for the plugin context's `tools` (chat runs with providers). */
|
|
79
|
+
private readonly tools;
|
|
68
80
|
private readonly finisher;
|
|
69
81
|
readonly txHost: TransactionHost;
|
|
70
82
|
/** The steps' live drafts this process writes for the runs it executes. */
|
|
@@ -93,7 +105,11 @@ export declare class RunWorker {
|
|
|
93
105
|
/** Run start's skills catalog snapshot (reads the pinned skills' bodies, phase B). */
|
|
94
106
|
skillsCatalog: Pick<SkillsCatalogService, "record">,
|
|
95
107
|
/** Run start's run context (run context spec §5): who the agent is, the day, the person. */
|
|
96
|
-
runContext: Pick<RunContextService, "record">,
|
|
108
|
+
runContext: Pick<RunContextService, "record">,
|
|
109
|
+
/** Run start's plugin context (memory plugin spec §6.1): the `context@1` providers' text. */
|
|
110
|
+
pluginContext: Pick<PluginContextService, "record">,
|
|
111
|
+
/** The run's resolved tools, for the plugin context's `tools` (chat runs with providers). */
|
|
112
|
+
tools: Pick<ToolRegistry, "forAgent">, finisher: Pick<RunFinish, "finish">, txHost: TransactionHost,
|
|
97
113
|
/** The steps' live drafts this process writes for the runs it executes. */
|
|
98
114
|
drafts: RunDrafts, hooks?: RunHooks | undefined);
|
|
99
115
|
private get db();
|
|
@@ -123,8 +139,10 @@ export declare class RunWorker {
|
|
|
123
139
|
/**
|
|
124
140
|
* After any terminal commit of a run that is not a sub-agent's (sub-agents spec §12): stops its
|
|
125
141
|
* active fork invocation child on its behalf (no account); background `Agent` children keep
|
|
126
|
-
* running (agent map spec §5.4)
|
|
127
|
-
*
|
|
142
|
+
* running (agent map spec §5.4), unless a person stopped the run: then the sub-agents of its
|
|
143
|
+
* windows stop too, also one it dispatched after the stop read the tree (agent map spec §6,
|
|
144
|
+
* changed in acceptance 2026-11-13). The terminal transaction owed the same stops
|
|
145
|
+
* (`stop-tree`), the backstop when this call never happens.
|
|
128
146
|
*/
|
|
129
147
|
private stopChildren;
|
|
130
148
|
/**
|
|
@@ -159,6 +177,11 @@ export declare class RunWorker {
|
|
|
159
177
|
process(job: Job<RunJobData>, token: string | undefined): Promise<ProcessResult>;
|
|
160
178
|
private execute;
|
|
161
179
|
private completed;
|
|
180
|
+
/**
|
|
181
|
+
* The run's extension tool keys by plugin, resolved as a chat run's first step lists them
|
|
182
|
+
* (disabled tools, org switches, the conversation's source): a provider's `tools`.
|
|
183
|
+
*/
|
|
184
|
+
private pluginToolKeys;
|
|
162
185
|
/**
|
|
163
186
|
* When the conversation was switched to a model that no longer resolves (provider disabled,
|
|
164
187
|
* model removed), writes `model_switched { model: null, effort: null, reason: "unavailable" }`
|
|
@@ -167,6 +190,12 @@ export declare class RunWorker {
|
|
|
167
190
|
* handles it). Resolved for the conversation's org (its effective provider config).
|
|
168
191
|
*/
|
|
169
192
|
private fallBackIfUnavailable;
|
|
193
|
+
/**
|
|
194
|
+
* Whether the run is gone or terminal, read in the caller's transaction under the locks
|
|
195
|
+
* (conversation, then run, as `RunFinish.finishIn`): the job read its status before taking the
|
|
196
|
+
* conversation lock, and a stop may have ended it since.
|
|
197
|
+
*/
|
|
198
|
+
private endedUnderLocks;
|
|
170
199
|
/**
|
|
171
200
|
* `run_started` under the conversation row lock, with the model / effort switch in effect then
|
|
172
201
|
* (a chat run's `saved` version under the conversation's selection): what the run uses.
|