@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ContentBlock, type Logger, type ModelRequest, type ModelStreamEvent, type ModelUsage, type StopReason } from "@capybara-hq/plugin-sdk";
|
|
1
|
+
import { type ContentBlock, type ConversationSource, type Logger, type ModelRequest, type ModelStreamEvent, type ModelUsage, type StopReason } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
import type { AgentDefinition } from "../agents/definition.js";
|
|
3
3
|
import type { AppendEvent, EventLog } from "../conversations/event-log.js";
|
|
4
4
|
import { type ConversationEvent } from "../conversations/events.js";
|
|
@@ -104,6 +104,11 @@ export interface ModelStepInput {
|
|
|
104
104
|
/** Hook payload scope: the run's agent, its trigger account and the conversation kind. */
|
|
105
105
|
agentId?: string;
|
|
106
106
|
accountId?: string;
|
|
107
|
+
/**
|
|
108
|
+
* The conversation's source (memory plugin spec §6.2): extension tools limited by
|
|
109
|
+
* `spec.sources` are listed by it.
|
|
110
|
+
*/
|
|
111
|
+
source: ConversationSource;
|
|
107
112
|
/** Hooks fire for `chat` and `subagent` (never for eval replays). */
|
|
108
113
|
conversationKind?: ConversationKindForHooks;
|
|
109
114
|
/** A sub-agent's run: where it was dispatched from (hook payloads carry it as `parent`). */
|
|
@@ -159,6 +159,7 @@ wrapUpReason) {
|
|
|
159
159
|
forkChild: input.parent?.forSkill === true,
|
|
160
160
|
sandboxConversationId: input.parent?.rootConversationId ?? input.conversationId,
|
|
161
161
|
replay: input.conversationKind === "eval",
|
|
162
|
+
source: input.source,
|
|
162
163
|
skillLoad: skillLoadOffered(events.findLast((e) => e.type === "skills_catalog"), hasInvocation(events)),
|
|
163
164
|
});
|
|
164
165
|
const tools = {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type ConversationSource } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import { EventLog } from "../conversations/event-log.js";
|
|
3
|
+
import { ExtensionPointDefaults } from "../extensions/point-defaults.js";
|
|
4
|
+
import { ExtensionRegistry } from "../extensions/registry.js";
|
|
5
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
6
|
+
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
7
|
+
import type { KernelLogger } from "../log.js";
|
|
8
|
+
import type { OrgRole } from "../orgs/roles.js";
|
|
9
|
+
/** What a run start tells {@link PluginContextService.record}. */
|
|
10
|
+
export interface PluginContextInput {
|
|
11
|
+
orgId: string;
|
|
12
|
+
conversationId: string;
|
|
13
|
+
runId: string;
|
|
14
|
+
agentId: string;
|
|
15
|
+
/** The run's agent definition name. */
|
|
16
|
+
agentName: string;
|
|
17
|
+
/** The conversation's owner (`conversations.account_id`) and display name. */
|
|
18
|
+
ownerAccountId: string;
|
|
19
|
+
ownerName: string;
|
|
20
|
+
/** The account whose message started the run. */
|
|
21
|
+
triggerAccountId: string;
|
|
22
|
+
source: ConversationSource;
|
|
23
|
+
/** The trigger account's role in the org. */
|
|
24
|
+
role: OrgRole;
|
|
25
|
+
/** The conversation's kind: only a chat run asks the providers. */
|
|
26
|
+
kind: "chat" | "eval" | "subagent";
|
|
27
|
+
/**
|
|
28
|
+
* The keys of the run's resolved tool set, by plugin (resolved with `forAgent` once, and only
|
|
29
|
+
* when some provider is asked).
|
|
30
|
+
*/
|
|
31
|
+
toolKeysByPlugin: () => Promise<Map<string, string[]>>;
|
|
32
|
+
/** Aborts every provider call (with its own time limit). */
|
|
33
|
+
signal: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Run start's plugin context (memory plugin spec §6.1): asks every `context@1` provider enabled in
|
|
37
|
+
* the org (valid config) for its text, in parallel and outside any transaction, each under the
|
|
38
|
+
* run's signal and {@link CONTEXT_LIMITS}.timeoutMs; then appends a `plugin_context` event for each
|
|
39
|
+
* plugin whose text differs from the latest one it recorded in the conversation. Fail open: a
|
|
40
|
+
* provider that throws, times out or returns too much is skipped for this run (its earlier text
|
|
41
|
+
* stays in effect) and logged. A provider loaded here that recorded text but no longer serves the
|
|
42
|
+
* org (switched off, its stored org config or its effective config invalid) gets an empty event,
|
|
43
|
+
* so its text stops following summaries; a plugin not loaded on this replica is left alone
|
|
44
|
+
* (another replica may still load it during a rollout, and blanking it here would make its text
|
|
45
|
+
* flap), and so is one whose status could not be read (a transient read error must not flip the
|
|
46
|
+
* model's context). Chat runs only: an
|
|
47
|
+
* eval replay reads the recorded events, and a sub-agent gets no plugin context (spec §5.4).
|
|
48
|
+
*/
|
|
49
|
+
export declare class PluginContextService {
|
|
50
|
+
private readonly db;
|
|
51
|
+
private readonly eventLog;
|
|
52
|
+
readonly txHost: TransactionHost;
|
|
53
|
+
private readonly points;
|
|
54
|
+
private readonly registry;
|
|
55
|
+
private readonly log;
|
|
56
|
+
constructor(db: KernelDb, eventLog: Pick<EventLog, "appendIn" | "notify">, txHost: TransactionHost, points: Pick<ExtensionPointDefaults, "providersFor">, registry: Pick<ExtensionRegistry, "list">, log: KernelLogger);
|
|
57
|
+
/**
|
|
58
|
+
* Asks the providers and records what changed (see the class); notifies after the commit. False:
|
|
59
|
+
* the run had ended under the locks (a stop landed while the providers were asked), so nothing
|
|
60
|
+
* was appended and the run must not go on (as `RunContextService.record`).
|
|
61
|
+
*/
|
|
62
|
+
record(input: PluginContextInput): Promise<boolean>;
|
|
63
|
+
/** One provider's call, bounded by the run's signal and the time limit. */
|
|
64
|
+
private ask;
|
|
65
|
+
/**
|
|
66
|
+
* One transaction under the conversation row lock (as `RunContextService`): compares each
|
|
67
|
+
* answer's hash with the plugin's latest `plugin_context` and appends the ones that differ (a
|
|
68
|
+
* plugin with no earlier event and no text writes nothing), plus an empty event for each plugin
|
|
69
|
+
* in `off` whose latest text is not empty. Idem key per run and plugin, so a retried run
|
|
70
|
+
* start finds its events or nothing changed. Returns the last seq written; `ended` when the run
|
|
71
|
+
* is terminal under the locks (the providers are asked outside the transaction, so a stop may
|
|
72
|
+
* have ended it meanwhile: nothing is appended after its end).
|
|
73
|
+
*/
|
|
74
|
+
private append;
|
|
75
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { CONTEXT_LIMITS, } from "@capybara-hq/plugin-sdk";
|
|
12
|
+
import { Inject, Injectable } from "@nestjs/common";
|
|
13
|
+
import { EventLog } from "../conversations/event-log.js";
|
|
14
|
+
import { ExtensionPointDefaults } from "../extensions/point-defaults.js";
|
|
15
|
+
import { ExtensionRegistry } from "../extensions/registry.js";
|
|
16
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
17
|
+
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
18
|
+
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
19
|
+
import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
20
|
+
import { latestPluginContexts, pluginContextIdemKey } from "./context/plugin-context.js";
|
|
21
|
+
import { runEndedIn } from "./run-status.js";
|
|
22
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
23
|
+
/**
|
|
24
|
+
* Run start's plugin context (memory plugin spec §6.1): asks every `context@1` provider enabled in
|
|
25
|
+
* the org (valid config) for its text, in parallel and outside any transaction, each under the
|
|
26
|
+
* run's signal and {@link CONTEXT_LIMITS}.timeoutMs; then appends a `plugin_context` event for each
|
|
27
|
+
* plugin whose text differs from the latest one it recorded in the conversation. Fail open: a
|
|
28
|
+
* provider that throws, times out or returns too much is skipped for this run (its earlier text
|
|
29
|
+
* stays in effect) and logged. A provider loaded here that recorded text but no longer serves the
|
|
30
|
+
* org (switched off, its stored org config or its effective config invalid) gets an empty event,
|
|
31
|
+
* so its text stops following summaries; a plugin not loaded on this replica is left alone
|
|
32
|
+
* (another replica may still load it during a rollout, and blanking it here would make its text
|
|
33
|
+
* flap), and so is one whose status could not be read (a transient read error must not flip the
|
|
34
|
+
* model's context). Chat runs only: an
|
|
35
|
+
* eval replay reads the recorded events, and a sub-agent gets no plugin context (spec §5.4).
|
|
36
|
+
*/
|
|
37
|
+
let PluginContextService = class PluginContextService {
|
|
38
|
+
db;
|
|
39
|
+
eventLog;
|
|
40
|
+
txHost;
|
|
41
|
+
points;
|
|
42
|
+
registry;
|
|
43
|
+
log;
|
|
44
|
+
constructor(db, eventLog, txHost, points, registry, log) {
|
|
45
|
+
this.db = db;
|
|
46
|
+
this.eventLog = eventLog;
|
|
47
|
+
this.txHost = txHost;
|
|
48
|
+
this.points = points;
|
|
49
|
+
this.registry = registry;
|
|
50
|
+
this.log = log;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Asks the providers and records what changed (see the class); notifies after the commit. False:
|
|
54
|
+
* the run had ended under the locks (a stop landed while the providers were asked), so nothing
|
|
55
|
+
* was appended and the run must not go on (as `RunContextService.record`).
|
|
56
|
+
*/
|
|
57
|
+
async record(input) {
|
|
58
|
+
if (input.kind !== "chat")
|
|
59
|
+
return true;
|
|
60
|
+
let found;
|
|
61
|
+
try {
|
|
62
|
+
found = await this.points.providersFor(input.orgId, "context");
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
// fail open: without the org's providers nothing is asked and nothing is switched off
|
|
66
|
+
this.log.warn("context providers could not be listed; plugin context left as recorded", {
|
|
67
|
+
orgId: input.orgId,
|
|
68
|
+
conversationId: input.conversationId,
|
|
69
|
+
runId: input.runId,
|
|
70
|
+
err,
|
|
71
|
+
});
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
const { providers } = found;
|
|
75
|
+
const tools = providers.length > 0 ? await input.toolKeysByPlugin() : new Map();
|
|
76
|
+
const answers = await Promise.all(providers.map(async (p) => {
|
|
77
|
+
const answer = await this.ask(p, input, tools.get(p.extension) ?? []);
|
|
78
|
+
if (!answer.ok) {
|
|
79
|
+
this.log.warn("context provider skipped for this run", {
|
|
80
|
+
orgId: input.orgId,
|
|
81
|
+
conversationId: input.conversationId,
|
|
82
|
+
runId: input.runId,
|
|
83
|
+
plugin: p.extension,
|
|
84
|
+
reason: answer.reason,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return [p.extension, answer];
|
|
88
|
+
}));
|
|
89
|
+
const texts = new Map();
|
|
90
|
+
for (const [plugin, answer] of answers)
|
|
91
|
+
if (answer.ok)
|
|
92
|
+
texts.set(plugin, answer.text);
|
|
93
|
+
const serving = new Set(providers.map((p) => p.extension));
|
|
94
|
+
const off = new Set(this.registry
|
|
95
|
+
.list("context")
|
|
96
|
+
.map((e) => e.plugin)
|
|
97
|
+
// a provider whose status could not be read (a transient error) keeps its recorded text
|
|
98
|
+
.filter((plugin) => !serving.has(plugin) && !found.failed.has(plugin)));
|
|
99
|
+
// nothing answered and nothing to switch off: no transaction, no row lock
|
|
100
|
+
if (texts.size === 0 && off.size === 0)
|
|
101
|
+
return true;
|
|
102
|
+
const seq = await this.append(input, texts, off);
|
|
103
|
+
if (seq === "ended")
|
|
104
|
+
return false;
|
|
105
|
+
if (seq !== null)
|
|
106
|
+
await this.eventLog.notify(input.conversationId, seq);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/** One provider's call, bounded by the run's signal and the time limit. */
|
|
110
|
+
async ask(p, input, tools) {
|
|
111
|
+
const signal = AbortSignal.any([input.signal, AbortSignal.timeout(CONTEXT_LIMITS.timeoutMs)]);
|
|
112
|
+
let result;
|
|
113
|
+
try {
|
|
114
|
+
result = await untilAborted(Promise.resolve().then(() => p.impl.provide({
|
|
115
|
+
orgId: input.orgId,
|
|
116
|
+
accountId: input.triggerAccountId,
|
|
117
|
+
agentId: input.agentId,
|
|
118
|
+
agentName: input.agentName,
|
|
119
|
+
conversationId: input.conversationId,
|
|
120
|
+
runId: input.runId,
|
|
121
|
+
source: input.source,
|
|
122
|
+
role: input.role,
|
|
123
|
+
ownerAccountId: input.ownerAccountId,
|
|
124
|
+
ownerName: input.ownerName,
|
|
125
|
+
tools,
|
|
126
|
+
config: p.config,
|
|
127
|
+
signal,
|
|
128
|
+
})), signal);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
if (signal.aborted) {
|
|
132
|
+
return { ok: false, reason: input.signal.aborted ? "run aborted" : "timed out" };
|
|
133
|
+
}
|
|
134
|
+
return { ok: false, reason: `threw: ${err instanceof Error ? err.message : String(err)}` };
|
|
135
|
+
}
|
|
136
|
+
if (result === null)
|
|
137
|
+
return { ok: true, text: "" };
|
|
138
|
+
const text = result?.text;
|
|
139
|
+
if (typeof text !== "string")
|
|
140
|
+
return { ok: false, reason: "returned no text" };
|
|
141
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
142
|
+
if (bytes > CONTEXT_LIMITS.maxBytes) {
|
|
143
|
+
return { ok: false, reason: `text of ${bytes} bytes is over ${CONTEXT_LIMITS.maxBytes}` };
|
|
144
|
+
}
|
|
145
|
+
return { ok: true, text };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* One transaction under the conversation row lock (as `RunContextService`): compares each
|
|
149
|
+
* answer's hash with the plugin's latest `plugin_context` and appends the ones that differ (a
|
|
150
|
+
* plugin with no earlier event and no text writes nothing), plus an empty event for each plugin
|
|
151
|
+
* in `off` whose latest text is not empty. Idem key per run and plugin, so a retried run
|
|
152
|
+
* start finds its events or nothing changed. Returns the last seq written; `ended` when the run
|
|
153
|
+
* is terminal under the locks (the providers are asked outside the transaction, so a stop may
|
|
154
|
+
* have ended it meanwhile: nothing is appended after its end).
|
|
155
|
+
*/
|
|
156
|
+
async append(input, texts, off) {
|
|
157
|
+
const t = this.db.db;
|
|
158
|
+
await t
|
|
159
|
+
.selectFrom("conversations")
|
|
160
|
+
.select("id")
|
|
161
|
+
.where("id", "=", input.conversationId)
|
|
162
|
+
.forUpdate()
|
|
163
|
+
.executeTakeFirst();
|
|
164
|
+
if (await runEndedIn(t, input.runId))
|
|
165
|
+
return "ended";
|
|
166
|
+
const rows = await t
|
|
167
|
+
.selectFrom("conversation_events")
|
|
168
|
+
.select(["seq", "payload"])
|
|
169
|
+
.where("conversation_id", "=", input.conversationId)
|
|
170
|
+
.where("type", "=", "plugin_context")
|
|
171
|
+
.orderBy("seq", "asc")
|
|
172
|
+
.execute();
|
|
173
|
+
const latest = latestPluginContexts(rows.map((r) => ({ type: "plugin_context", seq: Number(r.seq), payload: r.payload })), Number.POSITIVE_INFINITY);
|
|
174
|
+
const next = new Map(texts);
|
|
175
|
+
for (const [plugin, prev] of latest) {
|
|
176
|
+
if (off.has(plugin) && prev.text !== "")
|
|
177
|
+
next.set(plugin, "");
|
|
178
|
+
}
|
|
179
|
+
const changed = [...next]
|
|
180
|
+
.filter(([plugin, text]) => {
|
|
181
|
+
const prev = latest.get(plugin);
|
|
182
|
+
return prev === undefined ? text !== "" : prev.hash !== sha256(text);
|
|
183
|
+
})
|
|
184
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
185
|
+
if (changed.length === 0)
|
|
186
|
+
return null;
|
|
187
|
+
const written = await this.eventLog.appendIn(input.conversationId, changed.map(([plugin, text]) => ({
|
|
188
|
+
type: "plugin_context",
|
|
189
|
+
runId: input.runId,
|
|
190
|
+
idemKey: pluginContextIdemKey(input.runId, plugin),
|
|
191
|
+
payload: { plugin, text, hash: sha256(text) },
|
|
192
|
+
})));
|
|
193
|
+
return Math.max(...written.map((e) => e.seq));
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
__decorate([
|
|
197
|
+
Transactional()
|
|
198
|
+
], PluginContextService.prototype, "append", null);
|
|
199
|
+
PluginContextService = __decorate([
|
|
200
|
+
Injectable(),
|
|
201
|
+
__param(0, Inject(KernelDb)),
|
|
202
|
+
__param(1, Inject(EventLog)),
|
|
203
|
+
__param(2, Inject(TransactionHost)),
|
|
204
|
+
__param(3, Inject(ExtensionPointDefaults)),
|
|
205
|
+
__param(4, Inject(ExtensionRegistry)),
|
|
206
|
+
__param(5, Inject(KERNEL_LOGGER))
|
|
207
|
+
], PluginContextService);
|
|
208
|
+
export { PluginContextService };
|
|
209
|
+
/** `p`, or a rejection once `signal` aborts (a provider that ignores its signal never holds a run). */
|
|
210
|
+
function untilAborted(p, signal) {
|
|
211
|
+
return new Promise((resolve, reject) => {
|
|
212
|
+
const onAbort = () => reject(signal.reason);
|
|
213
|
+
if (signal.aborted) {
|
|
214
|
+
onAbort();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
218
|
+
p.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -41,12 +41,16 @@ export declare class RunContextService {
|
|
|
41
41
|
private readonly now;
|
|
42
42
|
constructor(db: KernelDb, eventLog: Pick<EventLog, "appendIn" | "notify">, txHost: TransactionHost, now?: () => number);
|
|
43
43
|
private clock;
|
|
44
|
-
/**
|
|
45
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Records the run's context (see the class) and notifies after the commit. False: the run had
|
|
46
|
+
* ended (a stop got there first), so nothing was appended and the run must not go on.
|
|
47
|
+
*/
|
|
48
|
+
record(input: RunContextInput): Promise<boolean>;
|
|
46
49
|
/**
|
|
47
50
|
* One transaction under the conversation row lock (as `lockLatestCatalog`): fold, read the
|
|
48
51
|
* current values, append when something differs, with the idem key `run_context:<runId>` (a
|
|
49
|
-
* retried run start finds its event, or finds nothing changed). Returns the seq written
|
|
52
|
+
* retried run start finds its event, or finds nothing changed). Returns the seq written;
|
|
53
|
+
* `ended` when the run is terminal under the locks (nothing is appended after its end).
|
|
50
54
|
*/
|
|
51
55
|
private append;
|
|
52
56
|
/** A root run: agent, org, and the date, zone and language of the run's person (§3.4). */
|
|
@@ -17,6 +17,7 @@ import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
|
17
17
|
import { readOrgSettings } from "../orgs/settings.service.js";
|
|
18
18
|
import { isTimeZone } from "../time-zone.js";
|
|
19
19
|
import { currentRunContext, dateIn, runContextChange, sanitizeSenderName, snapshotDescription, } from "./context/run-context.js";
|
|
20
|
+
import { runEndedIn } from "./run-status.js";
|
|
20
21
|
/**
|
|
21
22
|
* Optional provider: the clock run start reads the date from and the `CurrentTime` builtin reads
|
|
22
23
|
* the time from (default `Date.now`; tests).
|
|
@@ -46,16 +47,23 @@ let RunContextService = class RunContextService {
|
|
|
46
47
|
clock() {
|
|
47
48
|
return new Date(this.now());
|
|
48
49
|
}
|
|
49
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Records the run's context (see the class) and notifies after the commit. False: the run had
|
|
52
|
+
* ended (a stop got there first), so nothing was appended and the run must not go on.
|
|
53
|
+
*/
|
|
50
54
|
async record(input) {
|
|
51
55
|
const seq = await this.append(input);
|
|
56
|
+
if (seq === "ended")
|
|
57
|
+
return false;
|
|
52
58
|
if (seq !== null)
|
|
53
59
|
await this.eventLog.notify(input.conversationId, seq);
|
|
60
|
+
return true;
|
|
54
61
|
}
|
|
55
62
|
/**
|
|
56
63
|
* One transaction under the conversation row lock (as `lockLatestCatalog`): fold, read the
|
|
57
64
|
* current values, append when something differs, with the idem key `run_context:<runId>` (a
|
|
58
|
-
* retried run start finds its event, or finds nothing changed). Returns the seq written
|
|
65
|
+
* retried run start finds its event, or finds nothing changed). Returns the seq written;
|
|
66
|
+
* `ended` when the run is terminal under the locks (nothing is appended after its end).
|
|
59
67
|
*/
|
|
60
68
|
async append(input) {
|
|
61
69
|
const t = this.db.db;
|
|
@@ -65,6 +73,8 @@ let RunContextService = class RunContextService {
|
|
|
65
73
|
.where("id", "=", input.conversationId)
|
|
66
74
|
.forUpdate()
|
|
67
75
|
.executeTakeFirst();
|
|
76
|
+
if (await runEndedIn(t, input.runId))
|
|
77
|
+
return "ended";
|
|
68
78
|
const prev = await foldOf(t, input.conversationId);
|
|
69
79
|
const next = input.subagent === undefined
|
|
70
80
|
? await this.rootContext(t, input)
|
|
@@ -17,7 +17,7 @@ export declare class RunControl {
|
|
|
17
17
|
/** Owes a deleted root's tree stop in the delete's transaction. */
|
|
18
18
|
private readonly outbox;
|
|
19
19
|
private readonly log;
|
|
20
|
-
constructor(queue: Pick<RunQueue, "ensureRun" | "oweEnsureRun" | "abortIfOrphaned" | "wakeHeldTree" | "oweHeldTreeIn">, waits: Pick<Waits, "abortWaiting" | "childFinished" | "stopChildren" | "stopTree">, abort: Pick<RunAbort, "request">,
|
|
20
|
+
constructor(queue: Pick<RunQueue, "ensureRun" | "oweEnsureRun" | "abortIfOrphaned" | "abortQueued" | "wakeHeldTree" | "oweHeldTreeIn">, waits: Pick<Waits, "abortWaiting" | "childFinished" | "stopChildren" | "stopTree">, abort: Pick<RunAbort, "request">,
|
|
21
21
|
/** The resume of a waiting run in a decision's transaction (a reply's skips). */
|
|
22
22
|
resumes: Pick<RunResume, "resumeIn">,
|
|
23
23
|
/** Owes a deleted root's tree stop in the delete's transaction. */
|
|
@@ -61,6 +61,11 @@ export declare class RunControl {
|
|
|
61
61
|
* stops its children (sub-agents spec §12).
|
|
62
62
|
*/
|
|
63
63
|
abortIfOrphaned(conversationId: string, runId: string, byAccountId: string | null): Promise<boolean>;
|
|
64
|
+
/**
|
|
65
|
+
* Ends a run still `queued` in PG ({@link RunQueue.abortQueued}; the caller wrote the stop marker
|
|
66
|
+
* first): true when it did. A stopped sub-agent then reports to its parent at once.
|
|
67
|
+
*/
|
|
68
|
+
abortQueued(conversationId: string, runId: string, byAccountId: string | null): Promise<boolean>;
|
|
64
69
|
/**
|
|
65
70
|
* 运行在等人处理(waiting)时直接中止:取消等待、写 `run_aborted`、删除 checkpoint,返回 true;
|
|
66
71
|
* 运行已不是 waiting(例如刚被续跑)→ false. The stop's transaction owes the IM delivery that
|
|
@@ -110,6 +110,19 @@ let RunControl = class RunControl {
|
|
|
110
110
|
}
|
|
111
111
|
return r;
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Ends a run still `queued` in PG ({@link RunQueue.abortQueued}; the caller wrote the stop marker
|
|
115
|
+
* first): true when it did. A stopped sub-agent then reports to its parent at once.
|
|
116
|
+
*/
|
|
117
|
+
async abortQueued(conversationId, runId, byAccountId) {
|
|
118
|
+
const r = await this.queue.abortQueued(conversationId, runId, byAccountId);
|
|
119
|
+
if (r) {
|
|
120
|
+
await this.waits
|
|
121
|
+
.childFinished(runId)
|
|
122
|
+
.catch((err) => this.log.error("cannot report finished sub-agent run to its parent", { runId, err }));
|
|
123
|
+
}
|
|
124
|
+
return r;
|
|
125
|
+
}
|
|
113
126
|
/**
|
|
114
127
|
* 运行在等人处理(waiting)时直接中止:取消等待、写 `run_aborted`、删除 checkpoint,返回 true;
|
|
115
128
|
* 运行已不是 waiting(例如刚被续跑)→ false. The stop's transaction owes the IM delivery that
|
|
@@ -58,8 +58,8 @@ export declare class RunFinish {
|
|
|
58
58
|
* invocation gets its report (`pairStoppedForks`; an `Agent` call has its result from its
|
|
59
59
|
* dispatch), so no terminal path leaves an open call. After it, the stops and the delivery the
|
|
60
60
|
* run's end causes are owed here (runtime/subagents/tree-jobs.ts), the caller still doing them
|
|
61
|
-
* right after its commit: `stop-tree` when an active fork child remains or a
|
|
62
|
-
*
|
|
61
|
+
* right after its commit: `stop-tree` when an active fork child remains or a run a person
|
|
62
|
+
* stopped (a root's or a sub-agent's) has sub-agents below it, `deliver-to-parent` for a sub-agent's run. The
|
|
63
63
|
* delivery is a job, not written here: this transaction holds the child's conversation and run
|
|
64
64
|
* rows, and the parent's conversation row comes first in the dispatch's lock order (parent
|
|
65
65
|
* conversation, parent run, budget run, child conversation).
|
|
@@ -63,8 +63,8 @@ let RunFinish = class RunFinish {
|
|
|
63
63
|
* invocation gets its report (`pairStoppedForks`; an `Agent` call has its result from its
|
|
64
64
|
* dispatch), so no terminal path leaves an open call. After it, the stops and the delivery the
|
|
65
65
|
* run's end causes are owed here (runtime/subagents/tree-jobs.ts), the caller still doing them
|
|
66
|
-
* right after its commit: `stop-tree` when an active fork child remains or a
|
|
67
|
-
*
|
|
66
|
+
* right after its commit: `stop-tree` when an active fork child remains or a run a person
|
|
67
|
+
* stopped (a root's or a sub-agent's) has sub-agents below it, `deliver-to-parent` for a sub-agent's run. The
|
|
68
68
|
* delivery is a job, not written here: this transaction holds the child's conversation and run
|
|
69
69
|
* rows, and the parent's conversation row comes first in the dispatch's lock order (parent
|
|
70
70
|
* conversation, parent run, budget run, child conversation).
|
|
@@ -102,9 +102,14 @@ let RunFinish = class RunFinish {
|
|
|
102
102
|
const [ev] = await this.eventLog.appendIn(a.conversationId, [
|
|
103
103
|
{ ...a.event, runId: a.runId, idemKey: `${a.event.type}:${a.runId}` },
|
|
104
104
|
]);
|
|
105
|
+
// a root run a person stopped holds the root's tree (agent map spec §6, acceptance
|
|
106
|
+
// 2026-11-13): marked with its terminal state, under the same row lock, so no wake decided
|
|
107
|
+
// after this commit starts the root from a sub-agent's delivery
|
|
108
|
+
const holdTree = conv?.kind === "chat" && personStopped(a.event);
|
|
105
109
|
await t
|
|
106
110
|
.updateTable("conversations")
|
|
107
111
|
.set({
|
|
112
|
+
...(holdTree && { tree_stopped_at: sql `now()` }),
|
|
108
113
|
consumed_seq: a.floorToTrigger === false
|
|
109
114
|
? sql `GREATEST(consumed_seq, ${a.consumed ?? 0}::bigint)`
|
|
110
115
|
: sql `GREATEST(consumed_seq, ${a.consumed ?? 0}::bigint, (SELECT r.trigger_seq FROM capybara.runs r WHERE r.id = ${a.runId}))`,
|
|
@@ -145,8 +150,8 @@ let RunFinish = class RunFinish {
|
|
|
145
150
|
.where("cr.status", "in", ACTIVE_RUN_STATUSES)
|
|
146
151
|
.limit(1)
|
|
147
152
|
.executeTakeFirst();
|
|
148
|
-
|
|
149
|
-
|
|
153
|
+
// a root's too (agent map spec §6, changed in acceptance 2026-11-13)
|
|
154
|
+
const stoppedWithChildren = personStopped(a.event) &&
|
|
150
155
|
(await t
|
|
151
156
|
.selectFrom("conversations")
|
|
152
157
|
.select("id")
|
|
@@ -186,8 +191,8 @@ RunFinish = __decorate([
|
|
|
186
191
|
], RunFinish);
|
|
187
192
|
export { RunFinish };
|
|
188
193
|
/**
|
|
189
|
-
* A `run_aborted` a person caused (an account and no `detail`): a
|
|
190
|
-
*
|
|
194
|
+
* A `run_aborted` a person caused (an account and no `detail`): a run stopped that way stops its
|
|
195
|
+
* descendants (`Waits.stopDescendantsOfStopped`, agent map spec §6; a root's, those of its windows).
|
|
191
196
|
*/
|
|
192
197
|
function personStopped(event) {
|
|
193
198
|
if (event.type !== "run_aborted")
|
|
@@ -15,10 +15,7 @@ export declare const SUBAGENT_RUN_QUEUE_NAME = "cap-kernel-subagent-runs";
|
|
|
15
15
|
export declare const runQueueNameFor: (kind: ConversationKind) => typeof RUN_QUEUE_NAME | typeof SUBAGENT_RUN_QUEUE_NAME;
|
|
16
16
|
/** 未结束的运行状态(waiting:等人处理审批 / 输入请求,没有任务、不占会话锁与配额)。 */
|
|
17
17
|
export declare const ACTIVE_RUN_STATUSES: readonly ["queued", "running", "waiting"];
|
|
18
|
-
|
|
19
|
-
export type TerminalStatus = "completed" | "failed" | "aborted";
|
|
20
|
-
/** The terminal run statuses (here, not in `run-finish.ts`, which the sub-agent modules import). */
|
|
21
|
-
export declare const TERMINAL_STATUSES: ReadonlySet<string>;
|
|
18
|
+
export { TERMINAL_STATUSES, type TerminalStatus } from "./run-status.js";
|
|
22
19
|
/**
|
|
23
20
|
* Locks a run's approval and input request rows (`FOR UPDATE`, in id order) in the caller's
|
|
24
21
|
* transaction, after its conversation row and before its run row: the one lock order of every path
|
|
@@ -89,7 +86,27 @@ export declare class RunJobs {
|
|
|
89
86
|
owe(data: RunJobData, jobId: string, kind: ConversationKind, orgId: string): Promise<boolean>;
|
|
90
87
|
/** {@link oweEnsureRun} in the caller's transaction. */
|
|
91
88
|
oweEnsureRun(conversationId: string, seq: number | string, orgId: string): Promise<boolean>;
|
|
89
|
+
/**
|
|
90
|
+
* Owes the delayed `bg-wake` job at the end of the conversation's wake window in the caller's
|
|
91
|
+
* transaction (the `ensureRun` that decided `later`): one job per conversation and window end
|
|
92
|
+
* ({@link bgWakeJobId}), so finishes inside one window share it; `retry` > 0 is a job that fired
|
|
93
|
+
* before its window ended. The relay adds it with the remaining delay (a row due later skips the
|
|
94
|
+
* fast path); `runAt` carries {@link WAKE_JOB_MARGIN_MS}, since the window is read on the
|
|
95
|
+
* database's clock and the delay runs on this process's and Redis's. Every `ensureRun` inside the
|
|
96
|
+
* window (each append that calls it) owes it again: a no-op while the row is pending, and once it
|
|
97
|
+
* was delivered a new row whose add BullMQ dedupes by the job id while the delayed job exists.
|
|
98
|
+
*/
|
|
99
|
+
oweBgWake(conversationId: string, w: {
|
|
100
|
+
windowEnd: number;
|
|
101
|
+
delayMs: number;
|
|
102
|
+
}, retry: number, orgId: string): Promise<boolean>;
|
|
92
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Margin added to the delayed wake job: its window is read on the database's clock, the delay runs
|
|
106
|
+
* on this process's and Redis's, so the job does not usually fire just before the window ends (one
|
|
107
|
+
* that does reschedules itself under its next retry id, `bgWakeJobId`).
|
|
108
|
+
*/
|
|
109
|
+
export declare const WAKE_JOB_MARGIN_MS = 1000;
|
|
93
110
|
/**
|
|
94
111
|
* What {@link RunResume.resumeIn} found: `queued`, the run was waiting on nothing else and now is
|
|
95
112
|
* queued with its resume job owed; `still-running`, the run has not reached `waiting` yet (its
|
package/dist/runtime/run-jobs.js
CHANGED
|
@@ -10,6 +10,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
10
10
|
import { getQueueToken } from "@nestjs/bullmq";
|
|
11
11
|
import { Inject, Injectable, Module } from "@nestjs/common";
|
|
12
12
|
import { sql } from "kysely";
|
|
13
|
+
import { BG_WAKE_JOB, bgWakeJobId } from "../background/wake-rules.js";
|
|
13
14
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
14
15
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
15
16
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
@@ -29,12 +30,8 @@ export const SUBAGENT_RUN_QUEUE_NAME = "cap-kernel-subagent-runs";
|
|
|
29
30
|
export const runQueueNameFor = (kind) => kind === "subagent" ? SUBAGENT_RUN_QUEUE_NAME : RUN_QUEUE_NAME;
|
|
30
31
|
/** 未结束的运行状态(waiting:等人处理审批 / 输入请求,没有任务、不占会话锁与配额)。 */
|
|
31
32
|
export const ACTIVE_RUN_STATUSES = ["queued", "running", "waiting"];
|
|
32
|
-
|
|
33
|
-
export
|
|
34
|
-
"completed",
|
|
35
|
-
"failed",
|
|
36
|
-
"aborted",
|
|
37
|
-
]);
|
|
33
|
+
// The terminal run statuses (here, not in `run-finish.ts`, which the sub-agent modules import)
|
|
34
|
+
export { TERMINAL_STATUSES } from "./run-status.js";
|
|
38
35
|
/**
|
|
39
36
|
* Locks a run's approval and input request rows (`FOR UPDATE`, in id order) in the caller's
|
|
40
37
|
* transaction, after its conversation row and before its run row: the one lock order of every path
|
|
@@ -144,6 +141,26 @@ let RunJobs = class RunJobs {
|
|
|
144
141
|
oweEnsureRun(conversationId, seq, orgId) {
|
|
145
142
|
return oweEnsureRun(this.outbox, conversationId, seq, orgId);
|
|
146
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Owes the delayed `bg-wake` job at the end of the conversation's wake window in the caller's
|
|
146
|
+
* transaction (the `ensureRun` that decided `later`): one job per conversation and window end
|
|
147
|
+
* ({@link bgWakeJobId}), so finishes inside one window share it; `retry` > 0 is a job that fired
|
|
148
|
+
* before its window ended. The relay adds it with the remaining delay (a row due later skips the
|
|
149
|
+
* fast path); `runAt` carries {@link WAKE_JOB_MARGIN_MS}, since the window is read on the
|
|
150
|
+
* database's clock and the delay runs on this process's and Redis's. Every `ensureRun` inside the
|
|
151
|
+
* window (each append that calls it) owes it again: a no-op while the row is pending, and once it
|
|
152
|
+
* was delivered a new row whose add BullMQ dedupes by the job id while the delayed job exists.
|
|
153
|
+
*/
|
|
154
|
+
oweBgWake(conversationId, w, retry, orgId) {
|
|
155
|
+
return this.outbox.add({
|
|
156
|
+
queue: MAINTENANCE_QUEUE_NAME,
|
|
157
|
+
name: BG_WAKE_JOB,
|
|
158
|
+
jobId: bgWakeJobId(conversationId, w.windowEnd, retry),
|
|
159
|
+
data: { conversationId, ...(retry > 0 && { retry }) },
|
|
160
|
+
runAt: new Date(Date.now() + w.delayMs + WAKE_JOB_MARGIN_MS),
|
|
161
|
+
orgId,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
147
164
|
};
|
|
148
165
|
RunJobs = __decorate([
|
|
149
166
|
Injectable(),
|
|
@@ -153,6 +170,12 @@ RunJobs = __decorate([
|
|
|
153
170
|
__param(3, Inject(OutboxService))
|
|
154
171
|
], RunJobs);
|
|
155
172
|
export { RunJobs };
|
|
173
|
+
/**
|
|
174
|
+
* Margin added to the delayed wake job: its window is read on the database's clock, the delay runs
|
|
175
|
+
* on this process's and Redis's, so the job does not usually fire just before the window ends (one
|
|
176
|
+
* that does reschedules itself under its next retry id, `bgWakeJobId`).
|
|
177
|
+
*/
|
|
178
|
+
export const WAKE_JOB_MARGIN_MS = 1000;
|
|
156
179
|
/**
|
|
157
180
|
* The resume of a waiting run (transactional outbox spec §6 "Resumes and timeouts", "Decisions
|
|
158
181
|
* resume in their own transaction"), in the caller's transaction. A decision (approvals, questions,
|
|
@@ -13,7 +13,7 @@ import { RunFinish } from "./run-finish.js";
|
|
|
13
13
|
import { RunJobs } from "./run-jobs.js";
|
|
14
14
|
import { RunQueue } from "./run-queue.js";
|
|
15
15
|
import { Waits } from "./waits.js";
|
|
16
|
-
/** The check's `@LockedInterval` job name (its run lock key
|
|
16
|
+
/** The check's `@LockedInterval` job name (its run lock key: `cron:run-orphan-check`). */
|
|
17
17
|
export declare const RUN_ORPHAN_CHECK_JOB = "run-orphan-check";
|
|
18
18
|
/** The check's period: a fixed constant, not a config field. */
|
|
19
19
|
export declare const RUN_ORPHAN_CHECK_PERIOD_MS = 60000;
|
|
@@ -55,7 +55,10 @@ export interface OrphanCandidate {
|
|
|
55
55
|
/**
|
|
56
56
|
* One page of the active runs of chat and sub-agent conversations in `statuses` that entered their
|
|
57
57
|
* state more than `minAgeMs` ago, by (state start, id), after `after` (an eval's hidden
|
|
58
|
-
* conversation drives its runs itself).
|
|
58
|
+
* conversation drives its runs itself). A deleted org's runs are never candidates, running ones
|
|
59
|
+
* included: its deletion purged its owed jobs on purpose, so their absence is no lost job and
|
|
60
|
+
* nothing is re-added, and the org's data is invisible from then on, so a running orphan is left
|
|
61
|
+
* as it is rather than failed.
|
|
59
62
|
*/
|
|
60
63
|
export declare function orphanCandidates(db: KernelSchemaDb, statuses: readonly ("queued" | "running")[], minAgeMs: number, limit: number, after?: Pick<OrphanCandidate, "since" | "runId">): Promise<OrphanCandidate[]>;
|
|
61
64
|
/**
|
|
@@ -65,12 +68,12 @@ export declare function orphanCandidates(db: KernelSchemaDb, statuses: readonly
|
|
|
65
68
|
*/
|
|
66
69
|
export declare function jobInOutbox(db: KernelSchemaDb, queue: string, jobId: string): Promise<boolean>;
|
|
67
70
|
/**
|
|
68
|
-
* The `queued` runs (in their state for over `minAgeMs`) whose job is neither in the outbox
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* run again after both (a job that finished took its run out of `queued` first), so a run
|
|
72
|
-
* flight is never reported. The `run-orphan-check` uses it; so does the dual-replica test
|
|
73
|
-
* which asserts at its teardown that the kernel left none (`minAgeMs` 0).
|
|
71
|
+
* The `queued` runs (in their state for over `minAgeMs`) whose job is neither in the outbox (owed,
|
|
72
|
+
* or dead) nor in BullMQ (`jobExists`), and that are still queued after both looks: the run queue
|
|
73
|
+
* lost their jobs. The outbox is read before BullMQ (a row is deleted only after its job was added)
|
|
74
|
+
* and the run again after both (a job that finished took its run out of `queued` first), so a run
|
|
75
|
+
* in flight is never reported. The `run-orphan-check` uses it; so does the dual-replica test
|
|
76
|
+
* harness, which asserts at its teardown that the kernel left none (`minAgeMs` 0).
|
|
74
77
|
*/
|
|
75
78
|
export declare function queuedRunsWithoutJob(db: KernelSchemaDb, jobExists: (queue: string, jobId: string) => Promise<boolean>, opts: {
|
|
76
79
|
minAgeMs: number;
|
|
@@ -85,9 +88,9 @@ export declare function queuedRunsWithoutJob(db: KernelSchemaDb, jobExists: (que
|
|
|
85
88
|
* `run.orphaned` (its terminal transaction cancels its waits and owes what every run end owes),
|
|
86
89
|
* then, after the commit, its checkpoints are dropped, its fork child stopped (`run.orphaned`), a
|
|
87
90
|
* sub-agent's failure delivered to its parent at once and the conversation's next run started.
|
|
88
|
-
* - `queued`, with no outbox row either (pending or dead): a canary. Every queued run has its job
|
|
89
|
-
* own transaction, so this means Redis lost data. It is logged as an error, counted
|
|
90
|
-
* overview ({@link QueueLostJobs}) and its job owed again so the run is not stuck.
|
|
91
|
+
* - `queued`, with no outbox row either (pending or dead): a canary. Every queued run has its job
|
|
92
|
+
* owed in its own transaction, so this means Redis lost data. It is logged as an error, counted
|
|
93
|
+
* for the admin overview ({@link QueueLostJobs}) and its job owed again so the run is not stuck.
|
|
91
94
|
* Redis is read before each repair's transaction, which then re-reads the run under its
|
|
92
95
|
* conversation row lock and acts only if it is still in the same state since the same time. Read
|
|
93
96
|
* paths never repair runs.
|