@capybara-hq/kernel 0.1.0-rc.25 → 0.1.0-rc.27
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/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/background/wake-rules.d.ts +5 -0
- package/dist/background/wake-rules.js +16 -5
- package/dist/catalog/catalog-content.d.ts +6 -1
- package/dist/catalog/catalog-content.js +2 -1
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/org-catalog.service.js +5 -2
- package/dist/catalog/plugin-install.d.ts +5 -0
- package/dist/catalog/plugin-install.js +37 -1
- 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/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/conversations/conversations.controller.d.ts +4 -2
- package/dist/conversations/conversations.controller.js +4 -2
- package/dist/conversations/conversations.service.d.ts +37 -4
- package/dist/conversations/conversations.service.js +77 -20
- package/dist/conversations/events.d.ts +6 -0
- package/dist/conversations/events.js +6 -0
- package/dist/db/kernel-schema.d.ts +10 -1
- package/dist/devices/devices.service.d.ts +14 -6
- package/dist/devices/devices.service.js +20 -7
- 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/runs.service.d.ts +5 -6
- package/dist/evals/runs.service.js +11 -33
- 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 +52 -0
- package/dist/infra/outbox/registry.js +57 -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/orgs.service.js +4 -1
- package/dist/overrides.d.ts +4 -0
- package/dist/overrides.js +2 -0
- package/dist/platform/ops/conversations.ops.js +1 -1
- package/dist/runtime/maintenance.d.ts +6 -3
- package/dist/runtime/maintenance.js +21 -7
- package/dist/runtime/plugin-context.service.d.ts +9 -3
- package/dist/runtime/plugin-context.service.js +17 -5
- package/dist/runtime/run-context.service.d.ts +7 -3
- package/dist/runtime/run-context.service.js +12 -2
- 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 +23 -31
- package/dist/runtime/run-queue.js +62 -64
- package/dist/runtime/run-status.d.ts +12 -0
- package/dist/runtime/run-status.js +21 -0
- package/dist/runtime/run-worker.d.ts +10 -2
- package/dist/runtime/run-worker.js +44 -19
- package/dist/runtime/step-log.d.ts +6 -5
- package/dist/runtime/step-log.js +6 -5
- 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/schedules/scan.job.d.ts +6 -6
- package/dist/schedules/scan.job.js +6 -6
- package/dist/skills/catalog.d.ts +7 -3
- package/dist/skills/catalog.js +12 -2
- package/dist/tools/approvals.service.d.ts +96 -19
- package/dist/tools/approvals.service.js +300 -67
- package/dist/tools/builtins/sandbox/task-output.js +21 -6
- package/dist/tools/connectors.service.js +0 -4
- 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/tools.module.js +4 -0
- package/package.json +2 -2
- package/src/migrations/0125_conversation_tree_stopped_at.sql +5 -0
|
@@ -16,11 +16,14 @@ import { afterCommit } from "../infra/database/after-commit.js";
|
|
|
16
16
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
17
17
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
18
18
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
19
|
+
import { OutboxService } from "../infra/outbox/outbox.service.js";
|
|
19
20
|
import { KERNEL_BUS, KERNEL_LOGGER } from "../infra/tokens.js";
|
|
21
|
+
import { MAINTENANCE_QUEUE_NAME } from "../runtime/maintenance-queue.js";
|
|
20
22
|
import { LOCAL_PROVIDER } from "../sandbox/providers.js";
|
|
21
23
|
import { deviceNotFound } from "../sandbox/sandbox.service.js";
|
|
22
24
|
import { deviceRevokedTopic } from "./keys.js";
|
|
23
|
-
import {
|
|
25
|
+
import { DEVICE_OP_RESOLVE_JOB, deviceOpReportedJobId } from "./policy-op-jobs.js";
|
|
26
|
+
import { deviceFeaturesOf, isolationOf, localPolicyDiff, localPolicyOf, opExplains, opVisibleIn, pendingPolicyOpsOf, } from "./policy-summary.js";
|
|
24
27
|
import { LocalDeviceRelay } from "./relay.js";
|
|
25
28
|
/** The machine credential's prefix (`cap_dev_` plus 32 random bytes, base64url). */
|
|
26
29
|
export const MACHINE_CREDENTIAL_PREFIX = "cap_dev_";
|
|
@@ -42,12 +45,14 @@ let MachinesService = class MachinesService {
|
|
|
42
45
|
txHost;
|
|
43
46
|
relay;
|
|
44
47
|
bus;
|
|
48
|
+
outbox;
|
|
45
49
|
log;
|
|
46
|
-
constructor(db, txHost, relay, bus, log) {
|
|
50
|
+
constructor(db, txHost, relay, bus, outbox, log) {
|
|
47
51
|
this.db = db;
|
|
48
52
|
this.txHost = txHost;
|
|
49
53
|
this.relay = relay;
|
|
50
54
|
this.bus = bus;
|
|
55
|
+
this.outbox = outbox;
|
|
51
56
|
this.log = log;
|
|
52
57
|
}
|
|
53
58
|
/**
|
|
@@ -164,7 +169,8 @@ let MachinesService = class MachinesService {
|
|
|
164
169
|
* isolation that differs from the stored one (none stored counts as `sandbox`) is audited
|
|
165
170
|
* `device.isolation_reported` (sandbox dev tiers spec §10.4), once per change; a change of what
|
|
166
171
|
* the owner sets, made on the machine while it was away, is audited `device.policy_changed`
|
|
167
|
-
* ({@link reportedPolicy}).
|
|
172
|
+
* ({@link reportedPolicy}). The outcomes of its recent policy ops (`recentOps`) backfill the
|
|
173
|
+
* platform's ops still pending on the row ({@link backfillOps}).
|
|
168
174
|
*/
|
|
169
175
|
async recordHello(machineId, hello, rawPolicy = hello.policy) {
|
|
170
176
|
const self = await this.db
|
|
@@ -227,6 +233,61 @@ let MachinesService = class MachinesService {
|
|
|
227
233
|
.where("id", "=", machineId)
|
|
228
234
|
.where("revoked_at", "is", null)
|
|
229
235
|
.execute();
|
|
236
|
+
if (hello.recentOps !== undefined) {
|
|
237
|
+
await this.backfillOps(machineId, self, hello.recentOps, localPolicyOf(hello.policy));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Hello backfill (transactional outbox spec §7): a pending op of the platform's whose outcome
|
|
242
|
+
* the hello reports (`applied`, `refused`), or that it does not list although the op can no
|
|
243
|
+
* longer reach the machine (`absent`: past its `resolveAt`), is marked `reported` and owes a
|
|
244
|
+
* `device-op-resolve` job (`device-op-reported-<opId>`) that records the outcome. An op the
|
|
245
|
+
* machine logged only as `started` (it stopped between the log and the answer) is judged once
|
|
246
|
+
* past its `resolveAt` by the policy the hello reports: its change there → `applied`, else
|
|
247
|
+
* `interrupted` (within the window, or with no readable policy, it is left). An expired entry
|
|
248
|
+
* (past `until`) is skipped: the machine's reports are no longer the op's. Runs in the caller's
|
|
249
|
+
* transaction, with the machine's row locked; an op already reported is not owed again.
|
|
250
|
+
*/
|
|
251
|
+
async backfillOps(machineId, self, recentOps, policy) {
|
|
252
|
+
const outcomes = new Map(recentOps.map((r) => [r.opId, r.outcome]));
|
|
253
|
+
const now = Date.now();
|
|
254
|
+
const reported = new Map();
|
|
255
|
+
for (const op of pendingPolicyOpsOf(self.pending_policy_ops)) {
|
|
256
|
+
if (op.reported !== undefined || op.until <= now)
|
|
257
|
+
continue;
|
|
258
|
+
const outcome = outcomes.get(op.id);
|
|
259
|
+
if (outcome === "applied" || outcome === "refused")
|
|
260
|
+
reported.set(op.id, { outcome });
|
|
261
|
+
else if (now < op.resolveAt)
|
|
262
|
+
continue;
|
|
263
|
+
else if (outcome === undefined)
|
|
264
|
+
reported.set(op.id, { outcome: "absent" });
|
|
265
|
+
else if (policy !== null) {
|
|
266
|
+
reported.set(op.id, { outcome: opVisibleIn(op, policy) ? "applied" : "interrupted" });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (reported.size === 0)
|
|
270
|
+
return;
|
|
271
|
+
const ops = Array.isArray(self.pending_policy_ops) ? self.pending_policy_ops : [];
|
|
272
|
+
const next = ops.map((e) => {
|
|
273
|
+
const id = e?.id;
|
|
274
|
+
const r = typeof id === "string" ? reported.get(id) : undefined;
|
|
275
|
+
return r === undefined ? e : { ...e, reported: r };
|
|
276
|
+
});
|
|
277
|
+
await this.db
|
|
278
|
+
.updateTable("device_machines")
|
|
279
|
+
.set({ pending_policy_ops: JSON.stringify(next) })
|
|
280
|
+
.where("id", "=", machineId)
|
|
281
|
+
.execute();
|
|
282
|
+
for (const opId of reported.keys()) {
|
|
283
|
+
await this.outbox.add({
|
|
284
|
+
queue: MAINTENANCE_QUEUE_NAME,
|
|
285
|
+
name: DEVICE_OP_RESOLVE_JOB,
|
|
286
|
+
jobId: deviceOpReportedJobId(opId),
|
|
287
|
+
data: { machineId, opId },
|
|
288
|
+
orgId: self.org_id,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
230
291
|
}
|
|
231
292
|
/**
|
|
232
293
|
* Stores the local policy summary a connected program pushed after its hello (a `policy` frame:
|
|
@@ -292,7 +353,6 @@ let MachinesService = class MachinesService {
|
|
|
292
353
|
via: "device",
|
|
293
354
|
machineId,
|
|
294
355
|
approvalId: null,
|
|
295
|
-
confirmed: true,
|
|
296
356
|
},
|
|
297
357
|
});
|
|
298
358
|
return summary;
|
|
@@ -470,7 +530,8 @@ MachinesService = __decorate([
|
|
|
470
530
|
__param(1, Inject(TransactionHost)),
|
|
471
531
|
__param(2, Inject(LocalDeviceRelay)),
|
|
472
532
|
__param(3, Inject(KERNEL_BUS)),
|
|
473
|
-
__param(4, Inject(
|
|
533
|
+
__param(4, Inject(OutboxService)),
|
|
534
|
+
__param(5, Inject(KERNEL_LOGGER))
|
|
474
535
|
], MachinesService);
|
|
475
536
|
export { MachinesService };
|
|
476
537
|
const jsonOrNull = (v) => (v === null ? null : JSON.stringify(v));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settles a device policy op whose outcome was not recorded (transactional outbox spec §7): owed
|
|
3
|
+
* by the op's intent transaction for when the op can no longer reach the machine, and by a hello
|
|
4
|
+
* that reported its outcome. Consumed by `DeviceOpResolver` on the maintenance queue.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEVICE_OP_RESOLVE_JOB = "device-op-resolve";
|
|
7
|
+
/** The `device-op-resolve` job's data. */
|
|
8
|
+
export interface DeviceOpResolveData {
|
|
9
|
+
machineId: string;
|
|
10
|
+
opId: string;
|
|
11
|
+
}
|
|
12
|
+
/** The job id of the resolve job owed with an op's intent. */
|
|
13
|
+
export declare const deviceOpResolveJobId: (opId: string) => string;
|
|
14
|
+
/** The job id of the resolve job owed by a hello that reported the op's outcome. */
|
|
15
|
+
export declare const deviceOpReportedJobId: (opId: string) => string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The device policy op jobs' names and ids: a leaf module (no service imports), read by the
|
|
2
|
+
// producers (DevicePolicyService, MachinesService) and the maintenance processor.
|
|
3
|
+
/**
|
|
4
|
+
* Settles a device policy op whose outcome was not recorded (transactional outbox spec §7): owed
|
|
5
|
+
* by the op's intent transaction for when the op can no longer reach the machine, and by a hello
|
|
6
|
+
* that reported its outcome. Consumed by `DeviceOpResolver` on the maintenance queue.
|
|
7
|
+
*/
|
|
8
|
+
export const DEVICE_OP_RESOLVE_JOB = "device-op-resolve";
|
|
9
|
+
/** The job id of the resolve job owed with an op's intent. */
|
|
10
|
+
export const deviceOpResolveJobId = (opId) => `device-op-resolve-${opId}`;
|
|
11
|
+
/** The job id of the resolve job owed by a hello that reported the op's outcome. */
|
|
12
|
+
export const deviceOpReportedJobId = (opId) => `device-op-reported-${opId}`;
|
|
@@ -38,10 +38,28 @@ export interface LocalPolicyDiff {
|
|
|
38
38
|
}
|
|
39
39
|
/** The {@link LocalPolicyDiff} from `prev` to `next`; null when nothing the owner sets changed. */
|
|
40
40
|
export declare function localPolicyDiff(prev: DeviceLocalPolicySummary, next: DeviceLocalPolicySummary): LocalPolicyDiff | null;
|
|
41
|
+
/** Who asked for a `policy` op: the deciding account, where, and the network ask it answers. */
|
|
42
|
+
export interface PolicyOpSource {
|
|
43
|
+
accountId: string;
|
|
44
|
+
via: "web" | "im";
|
|
45
|
+
approvalId: string | null;
|
|
46
|
+
}
|
|
47
|
+
/** What the machine reported of an op after its answer was lost (its hello's `recentOps`). */
|
|
48
|
+
export interface ReportedPolicyOp {
|
|
49
|
+
/**
|
|
50
|
+
* `absent`: not in the hello's outcomes although the op can no longer reach the machine;
|
|
51
|
+
* `interrupted`: logged `started` only, and its change is not in the policy the hello reports
|
|
52
|
+
* (one whose change is there is `applied`).
|
|
53
|
+
*/
|
|
54
|
+
outcome: "applied" | "refused" | "absent" | "interrupted";
|
|
55
|
+
}
|
|
41
56
|
/**
|
|
42
|
-
*
|
|
43
|
-
* machine's row
|
|
44
|
-
* the
|
|
57
|
+
* The intent of a `policy` op the platform sends to a machine (transactional outbox spec §7), kept
|
|
58
|
+
* on the machine's row (`pending_policy_ops`) from before the op is sent until its outcome is
|
|
59
|
+
* recorded: the change asked for, who asked, and until when (epoch ms) a summary the machine
|
|
60
|
+
* reports by itself may still be that op's. `sent`: on its way or in flight, the
|
|
61
|
+
* `device-op-resolve` job owed for `resolveAt`; `unknown`: no answer came, and only the machine's
|
|
62
|
+
* next hello (`recentOps`) or a later `policyStatus` can tell what became of it.
|
|
45
63
|
*/
|
|
46
64
|
export interface PendingPolicyOp {
|
|
47
65
|
id: string;
|
|
@@ -50,10 +68,25 @@ export interface PendingPolicyOp {
|
|
|
50
68
|
/** Present when the op set the tier (null: follow the org's default). */
|
|
51
69
|
tier?: SandboxTier | null;
|
|
52
70
|
isolation?: "sandbox";
|
|
71
|
+
source: PolicyOpSource;
|
|
72
|
+
actorTokenId?: string;
|
|
73
|
+
status: "sent" | "unknown";
|
|
74
|
+
/** When the intent was written (epoch ms). */
|
|
75
|
+
sentAt: number;
|
|
76
|
+
/** From when the op can no longer reach the machine (epoch ms): the resolve job's `runAt`. */
|
|
77
|
+
resolveAt: number;
|
|
53
78
|
until: number;
|
|
79
|
+
/** Set by a hello that reported the op's outcome (or its absence) before the op was resolved. */
|
|
80
|
+
reported?: ReportedPolicyOp;
|
|
54
81
|
}
|
|
55
82
|
/** The pending ops stored on a machine row (malformed entries dropped). */
|
|
56
83
|
export declare function pendingPolicyOpsOf(v: unknown): PendingPolicyOp[];
|
|
84
|
+
/**
|
|
85
|
+
* Whether the change of `op` is in `policy`: its hosts added and removed, its tier and isolation
|
|
86
|
+
* set. Tells an op the machine logged only as `started` (it stopped between the log and the
|
|
87
|
+
* answer) that was written from one that was not.
|
|
88
|
+
*/
|
|
89
|
+
export declare function opVisibleIn(op: PendingPolicyOp, policy: DeviceLocalPolicySummary): boolean;
|
|
57
90
|
/**
|
|
58
91
|
* Whether `diff` is what the platform's `op` did (its answer, or the machine's own report of it
|
|
59
92
|
* before the answer was recorded): hosts within the op's, the tier the op set, and only what a
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { isSandboxTier, } from "@capybara-hq/plugin-sdk";
|
|
2
|
-
const KNOWN_FEATURES = [
|
|
2
|
+
const KNOWN_FEATURES = [
|
|
3
|
+
"networkReport",
|
|
4
|
+
"policyOp",
|
|
5
|
+
"policyOpId",
|
|
6
|
+
"tiers",
|
|
7
|
+
];
|
|
3
8
|
/**
|
|
4
9
|
* The known features in `v` (a hello's `features` or the stored column), in a fixed order and
|
|
5
10
|
* deduped; anything else (unknown names, a non-array) is dropped.
|
|
@@ -99,17 +104,58 @@ export function pendingPolicyOpsOf(v) {
|
|
|
99
104
|
const o = e;
|
|
100
105
|
if (typeof o.id !== "string" || typeof o.until !== "number")
|
|
101
106
|
continue;
|
|
107
|
+
if (typeof o.sentAt !== "number" || typeof o.resolveAt !== "number")
|
|
108
|
+
continue;
|
|
109
|
+
if (o.status !== "sent" && o.status !== "unknown")
|
|
110
|
+
continue;
|
|
111
|
+
const src = o.source;
|
|
112
|
+
if (typeof src !== "object" || src === null || typeof src.accountId !== "string")
|
|
113
|
+
continue;
|
|
114
|
+
if (src.via !== "web" && src.via !== "im")
|
|
115
|
+
continue;
|
|
116
|
+
const rep = o.reported;
|
|
117
|
+
const outcome = typeof rep === "object" && rep !== null ? rep.outcome : undefined;
|
|
118
|
+
const reported = outcome === "applied" ||
|
|
119
|
+
outcome === "refused" ||
|
|
120
|
+
outcome === "absent" ||
|
|
121
|
+
outcome === "interrupted"
|
|
122
|
+
? { outcome }
|
|
123
|
+
: undefined;
|
|
102
124
|
out.push({
|
|
103
125
|
id: o.id,
|
|
104
126
|
add: hosts(o.add),
|
|
105
127
|
remove: hosts(o.remove),
|
|
106
128
|
...("tier" in o && { tier: isSandboxTier(o.tier) ? o.tier : null }),
|
|
107
129
|
...(o.isolation === "sandbox" && { isolation: "sandbox" }),
|
|
130
|
+
source: {
|
|
131
|
+
accountId: src.accountId,
|
|
132
|
+
via: src.via,
|
|
133
|
+
approvalId: typeof src.approvalId === "string" ? src.approvalId : null,
|
|
134
|
+
},
|
|
135
|
+
...(typeof o.actorTokenId === "string" && { actorTokenId: o.actorTokenId }),
|
|
136
|
+
status: o.status,
|
|
137
|
+
sentAt: o.sentAt,
|
|
138
|
+
resolveAt: o.resolveAt,
|
|
108
139
|
until: o.until,
|
|
140
|
+
...(reported !== undefined && { reported }),
|
|
109
141
|
});
|
|
110
142
|
}
|
|
111
143
|
return out;
|
|
112
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Whether the change of `op` is in `policy`: its hosts added and removed, its tier and isolation
|
|
147
|
+
* set. Tells an op the machine logged only as `started` (it stopped between the log and the
|
|
148
|
+
* answer) that was written from one that was not.
|
|
149
|
+
*/
|
|
150
|
+
export function opVisibleIn(op, policy) {
|
|
151
|
+
if (!op.add.every((h) => policy.allowedDomains.includes(h)))
|
|
152
|
+
return false;
|
|
153
|
+
if (op.remove.some((h) => policy.allowedDomains.includes(h)))
|
|
154
|
+
return false;
|
|
155
|
+
if (op.tier !== undefined && (policy.tier ?? null) !== op.tier)
|
|
156
|
+
return false;
|
|
157
|
+
return op.isolation === undefined || policy.isolation === op.isolation;
|
|
158
|
+
}
|
|
113
159
|
/**
|
|
114
160
|
* Whether `diff` is what the platform's `op` did (its answer, or the machine's own report of it
|
|
115
161
|
* before the answer was recorded): hosts within the op's, the tier the op set, and only what a
|
|
@@ -24,6 +24,7 @@ import { KernelDb } from "../infra/database/kernel-db.js";
|
|
|
24
24
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
25
25
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
26
26
|
import { KernelEvents } from "../infra/events/kernel-events.js";
|
|
27
|
+
import { USAGE_REPORT_QUEUE } from "../runtime/usage-report-jobs.js";
|
|
27
28
|
import { OrgLock } from "./org-lock.js";
|
|
28
29
|
/** 组织 slug:小写字母数字与 `-`,首尾不能是 `-`,1–40 个字符。 */
|
|
29
30
|
export const SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
|
|
@@ -242,11 +243,13 @@ let OrgsService = class OrgsService {
|
|
|
242
243
|
.where("id", "=", orgId)
|
|
243
244
|
.execute();
|
|
244
245
|
// the org's owed jobs go with it; dead letters stay for the overview, and the deletion's own
|
|
245
|
-
// cleanup work carries no org_id (transactional outbox spec §9)
|
|
246
|
+
// cleanup work carries no org_id (transactional outbox spec §9). Usage reports stay owed: the
|
|
247
|
+
// usage happened, and billing plugins are still told about a deleted org's usage
|
|
246
248
|
await this.db
|
|
247
249
|
.deleteFrom("outbox")
|
|
248
250
|
.where("org_id", "=", orgId)
|
|
249
251
|
.where("dead_at", "is", null)
|
|
252
|
+
.where("queue", "<>", USAGE_REPORT_QUEUE)
|
|
250
253
|
.execute();
|
|
251
254
|
// after the org is marked deleted, so it counts as no user of what it shared (P3-18)
|
|
252
255
|
await this.ownedItems.onOrgDeleted(orgId, actor.id, {
|
package/dist/overrides.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { OUTBOX_FAST_PATH_SETTLED } from "./infra/outbox/outbox.module.js";
|
|
|
13
13
|
import { OUTBOX_STRICT_CONFLICTS } from "./infra/outbox/outbox.service.js";
|
|
14
14
|
import { PRESENCE_INTERVAL_MS } from "./infra/presence/replica-presence.js";
|
|
15
15
|
import type { KernelQueueName } from "./infra/queue/queue-names.js";
|
|
16
|
+
import { DEVICE_POLICY_AFTER_CALL } from "./local-devices/device-policy.service.js";
|
|
16
17
|
import { LIST_MODELS_TIMEOUT } from "./runtime/providers.js";
|
|
17
18
|
import { RUN_CONTEXT_CLOCK } from "./runtime/run-context.service.js";
|
|
18
19
|
import { ENSURE_RUN_FAILS } from "./runtime/run-queue.js";
|
|
@@ -47,6 +48,8 @@ import { EMBED_TOKEN_CLOCK } from "./ui/embed-token.js";
|
|
|
47
48
|
* - `OUTBOX_FAST_PATH_SETTLED`: receives each detached outbox fast-path pass (a test awaits them);
|
|
48
49
|
* - `ENSURE_RUN_FAILS`: returns true to make `RunQueue.ensureRun` throw (Redis unreachable, or the
|
|
49
50
|
* direct call after a commit skipped by a crash);
|
|
51
|
+
* - `DEVICE_POLICY_AFTER_CALL`: called after a device policy op's call, before its outcome is
|
|
52
|
+
* recorded (throwing stands for a crash between the call and the resolve transaction);
|
|
50
53
|
* - `SCOPE_BACKSTOP_DELAY`: the delay of a confirmation's scope sync backstop (ruling R24);
|
|
51
54
|
* - `APP_INTERCEPTOR`: one more global interceptor (a probe of the matched routes).
|
|
52
55
|
*/
|
|
@@ -72,6 +75,7 @@ interface KernelSeams {
|
|
|
72
75
|
[OUTBOX_FAST_PATH_SETTLED]: (pass: Promise<void>) => void;
|
|
73
76
|
[ENSURE_RUN_FAILS]: () => boolean;
|
|
74
77
|
[SCOPE_BACKSTOP_DELAY]: number;
|
|
78
|
+
[DEVICE_POLICY_AFTER_CALL]: (opId: string) => void;
|
|
75
79
|
[APP_INTERCEPTOR]: NestInterceptor;
|
|
76
80
|
}
|
|
77
81
|
/**
|
package/dist/overrides.js
CHANGED
|
@@ -10,6 +10,7 @@ import { OUTBOX_DISPATCH_FAILS } from "./infra/outbox/outbox.dispatcher.js";
|
|
|
10
10
|
import { OUTBOX_FAST_PATH_SETTLED } from "./infra/outbox/outbox.module.js";
|
|
11
11
|
import { OUTBOX_STRICT_CONFLICTS } from "./infra/outbox/outbox.service.js";
|
|
12
12
|
import { PRESENCE_INTERVAL_MS } from "./infra/presence/replica-presence.js";
|
|
13
|
+
import { DEVICE_POLICY_AFTER_CALL } from "./local-devices/device-policy.service.js";
|
|
13
14
|
import { LIST_MODELS_TIMEOUT } from "./runtime/providers.js";
|
|
14
15
|
import { RUN_CONTEXT_CLOCK } from "./runtime/run-context.service.js";
|
|
15
16
|
import { ENSURE_RUN_FAILS } from "./runtime/run-queue.js";
|
|
@@ -42,6 +43,7 @@ const SEAM_TOKENS = [
|
|
|
42
43
|
OUTBOX_FAST_PATH_SETTLED,
|
|
43
44
|
ENSURE_RUN_FAILS,
|
|
44
45
|
SCOPE_BACKSTOP_DELAY,
|
|
46
|
+
DEVICE_POLICY_AFTER_CALL,
|
|
45
47
|
APP_INTERCEPTOR,
|
|
46
48
|
];
|
|
47
49
|
const allSeamsListed = true;
|
|
@@ -368,7 +368,7 @@ let ConversationsOperations = class ConversationsOperations {
|
|
|
368
368
|
}),
|
|
369
369
|
defineOperation({
|
|
370
370
|
id: "conversations.abort",
|
|
371
|
-
description: "Stop the run that is active in one of your other conversations (its sub-agents
|
|
371
|
+
description: "Stop the run that is active in one of your other conversations (its sub-agents stop with it; stopping a sub-agent's conversation stops it and those below it). Nothing happens if no run is active. The person confirms every stop; it is never remembered. The conversation you are working in cannot be stopped.",
|
|
372
372
|
input: Id,
|
|
373
373
|
example: { conversationId: "3f2b8a40-6c1d-4e0b-9a77-5d1e2c8b9f10" },
|
|
374
374
|
authz: { orgAction: "agents.use" },
|
|
@@ -7,6 +7,7 @@ import { DirectorySync } from "../orgs/directory-sync.js";
|
|
|
7
7
|
import { Offboarding } from "../orgs/offboarding.js";
|
|
8
8
|
import { SandboxService } from "../sandbox/sandbox.service.js";
|
|
9
9
|
import { ScheduledTaskFirer } from "../schedules/fire.js";
|
|
10
|
+
import { DeviceOpResolver } from "../tools/device-op-resolver.js";
|
|
10
11
|
import { ToolDrift } from "../tools/drift.js";
|
|
11
12
|
import { RunQueue } from "./run-queue.js";
|
|
12
13
|
import { Waits } from "./waits.js";
|
|
@@ -20,11 +21,12 @@ export declare const MAINTENANCE_CONCURRENCY = 5;
|
|
|
20
21
|
* {@link DirectorySync}, `run-stop` → {@link Offboarding.stopRun}, `bg-wake` and `ensure-run` → {@link RunQueue.ensureRun}, `stop-tree` →
|
|
21
22
|
* {@link processStopTree} and `deliver-to-parent` → {@link processDeliverToParent} (both in
|
|
22
23
|
* runtime/subagents/tree-jobs.ts, over {@link Waits}), `owned-plugin-cleanup` →
|
|
23
|
-
* {@link OwnedPluginCleanup}, `sched-fire` and `sched-settle` → {@link ScheduledTaskFirer}
|
|
24
|
+
* {@link OwnedPluginCleanup}, `sched-fire` and `sched-settle` → {@link ScheduledTaskFirer}, `device-op-resolve` →
|
|
25
|
+
* {@link DeviceOpResolver}). The name of a periodic job (a `@LockedCron` / `@LockedInterval` job,
|
|
24
26
|
* such as `sandbox-suspend-idle`) runs that job's body once ({@link ScheduledRuns.runNow}), as the
|
|
25
27
|
* maintenance handlers did when those jobs were schedulers on this queue: an operator's or a
|
|
26
28
|
* test's one-off run, not a second schedule (decision R23 of the Nest kernel spec). Any other
|
|
27
|
-
* name
|
|
29
|
+
* name fails the job (BullMQ retries it; in a rolling deploy a newer replica knows it). The producers are the domains' own (same names, ids and options).
|
|
28
30
|
*/
|
|
29
31
|
export declare class MaintenanceProcessor extends WorkerHost<Worker> {
|
|
30
32
|
private readonly waits;
|
|
@@ -35,10 +37,11 @@ export declare class MaintenanceProcessor extends WorkerHost<Worker> {
|
|
|
35
37
|
private readonly runs;
|
|
36
38
|
private readonly ownedCleanup;
|
|
37
39
|
private readonly scheduledTasks;
|
|
40
|
+
private readonly deviceOps;
|
|
38
41
|
private readonly scheduled;
|
|
39
42
|
private readonly log;
|
|
40
43
|
private readonly queue;
|
|
41
|
-
constructor(waits: Pick<Waits, "processTimeout" | "childFinished" | "stopChildren" | "stopDescendantsOfStopped" | "stopTree">, sandbox: Pick<SandboxService, "destroyDevice" | "resetFileSystem">, drift: Pick<ToolDrift, "check">, directory: Pick<DirectorySync, "runSyncJob">, offboarding: Pick<Offboarding, "stopRun">, runs: Pick<RunQueue, "ensureRun">, ownedCleanup: Pick<OwnedPluginCleanup, "run">, scheduledTasks: Pick<ScheduledTaskFirer, "fire" | "settle">, scheduled: Pick<ScheduledRuns, "runNow">, log: Pick<KernelLogger, "error" | "warn">, queue: Pick<Queue, "removeJobScheduler">);
|
|
44
|
+
constructor(waits: Pick<Waits, "processTimeout" | "childFinished" | "stopChildren" | "stopDescendantsOfStopped" | "stopTree">, sandbox: Pick<SandboxService, "destroyDevice" | "resetFileSystem">, drift: Pick<ToolDrift, "check">, directory: Pick<DirectorySync, "runSyncJob">, offboarding: Pick<Offboarding, "stopRun">, runs: Pick<RunQueue, "ensureRun">, ownedCleanup: Pick<OwnedPluginCleanup, "run">, scheduledTasks: Pick<ScheduledTaskFirer, "fire" | "settle">, deviceOps: Pick<DeviceOpResolver, "resolve">, scheduled: Pick<ScheduledRuns, "runNow">, log: Pick<KernelLogger, "error" | "warn">, queue: Pick<Queue, "removeJobScheduler">);
|
|
42
45
|
/**
|
|
43
46
|
* Removes the retired job schedulers (`RETIRED_SCHEDULER_IDS`, runtime/retired-schedulers.ts): every fixed-period
|
|
44
47
|
* maintenance job is a `@LockedCron` / `@LockedInterval` now. Idempotent; called once when a
|
|
@@ -19,6 +19,7 @@ import { runJob } from "../infra/queue/run-job.js";
|
|
|
19
19
|
import { KernelScheduleModule } from "../infra/schedule/schedule.module.js";
|
|
20
20
|
import { ScheduledRuns } from "../infra/schedule/scheduled-runs.js";
|
|
21
21
|
import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
22
|
+
import { DEVICE_OP_RESOLVE_JOB, } from "../local-devices/policy-op-jobs.js";
|
|
22
23
|
import { DIRECTORY_SYNC_JOB, DirectorySync } from "../orgs/directory-sync.js";
|
|
23
24
|
import { Offboarding, RUN_STOP_JOB } from "../orgs/offboarding.js";
|
|
24
25
|
import { OrgsModule } from "../orgs/orgs.module.js";
|
|
@@ -27,6 +28,7 @@ import { SandboxModule } from "../sandbox/sandbox.module.js";
|
|
|
27
28
|
import { SandboxService } from "../sandbox/sandbox.service.js";
|
|
28
29
|
import { ScheduledTaskFireModule, ScheduledTaskFirer } from "../schedules/fire.js";
|
|
29
30
|
import { SCHED_FIRE_JOB, SCHED_SETTLE_JOB, } from "../schedules/job-ids.js";
|
|
31
|
+
import { DeviceOpResolver } from "../tools/device-op-resolver.js";
|
|
30
32
|
import { ToolDrift } from "../tools/drift.js";
|
|
31
33
|
import { DRIFT_CONNECTOR_JOB } from "../tools/job-ids.js";
|
|
32
34
|
import { ToolsModule } from "../tools/tools.module.js";
|
|
@@ -46,11 +48,12 @@ export const MAINTENANCE_CONCURRENCY = 5;
|
|
|
46
48
|
* {@link DirectorySync}, `run-stop` → {@link Offboarding.stopRun}, `bg-wake` and `ensure-run` → {@link RunQueue.ensureRun}, `stop-tree` →
|
|
47
49
|
* {@link processStopTree} and `deliver-to-parent` → {@link processDeliverToParent} (both in
|
|
48
50
|
* runtime/subagents/tree-jobs.ts, over {@link Waits}), `owned-plugin-cleanup` →
|
|
49
|
-
* {@link OwnedPluginCleanup}, `sched-fire` and `sched-settle` → {@link ScheduledTaskFirer}
|
|
51
|
+
* {@link OwnedPluginCleanup}, `sched-fire` and `sched-settle` → {@link ScheduledTaskFirer}, `device-op-resolve` →
|
|
52
|
+
* {@link DeviceOpResolver}). The name of a periodic job (a `@LockedCron` / `@LockedInterval` job,
|
|
50
53
|
* such as `sandbox-suspend-idle`) runs that job's body once ({@link ScheduledRuns.runNow}), as the
|
|
51
54
|
* maintenance handlers did when those jobs were schedulers on this queue: an operator's or a
|
|
52
55
|
* test's one-off run, not a second schedule (decision R23 of the Nest kernel spec). Any other
|
|
53
|
-
* name
|
|
56
|
+
* name fails the job (BullMQ retries it; in a rolling deploy a newer replica knows it). The producers are the domains' own (same names, ids and options).
|
|
54
57
|
*/
|
|
55
58
|
let MaintenanceProcessor = class MaintenanceProcessor extends WorkerHost {
|
|
56
59
|
waits;
|
|
@@ -61,10 +64,11 @@ let MaintenanceProcessor = class MaintenanceProcessor extends WorkerHost {
|
|
|
61
64
|
runs;
|
|
62
65
|
ownedCleanup;
|
|
63
66
|
scheduledTasks;
|
|
67
|
+
deviceOps;
|
|
64
68
|
scheduled;
|
|
65
69
|
log;
|
|
66
70
|
queue;
|
|
67
|
-
constructor(waits, sandbox, drift, directory, offboarding, runs, ownedCleanup, scheduledTasks, scheduled, log, queue) {
|
|
71
|
+
constructor(waits, sandbox, drift, directory, offboarding, runs, ownedCleanup, scheduledTasks, deviceOps, scheduled, log, queue) {
|
|
68
72
|
super();
|
|
69
73
|
this.waits = waits;
|
|
70
74
|
this.sandbox = sandbox;
|
|
@@ -74,6 +78,7 @@ let MaintenanceProcessor = class MaintenanceProcessor extends WorkerHost {
|
|
|
74
78
|
this.runs = runs;
|
|
75
79
|
this.ownedCleanup = ownedCleanup;
|
|
76
80
|
this.scheduledTasks = scheduledTasks;
|
|
81
|
+
this.deviceOps = deviceOps;
|
|
77
82
|
this.scheduled = scheduled;
|
|
78
83
|
this.log = log;
|
|
79
84
|
this.queue = queue;
|
|
@@ -160,10 +165,18 @@ let MaintenanceProcessor = class MaintenanceProcessor extends WorkerHost {
|
|
|
160
165
|
case SCHED_SETTLE_JOB:
|
|
161
166
|
await this.scheduledTasks.settle(job.data);
|
|
162
167
|
return;
|
|
168
|
+
// a device policy op whose outcome was not recorded (transactional outbox spec §7)
|
|
169
|
+
case DEVICE_OP_RESOLVE_JOB:
|
|
170
|
+
await this.deviceOps.resolve(job.data);
|
|
171
|
+
return;
|
|
163
172
|
default:
|
|
164
173
|
if (await this.scheduled.runNow(job.name))
|
|
165
174
|
return;
|
|
166
|
-
|
|
175
|
+
// never completed unrun: in a rolling deploy an old worker meets a job a newer replica
|
|
176
|
+
// owed (its outbox row is gone once added); failing it leaves it to BullMQ's retries, so a
|
|
177
|
+
// newer replica can take it within the job's attempts (old workers still go first, see
|
|
178
|
+
// docs/deploy/upgrade.md)
|
|
179
|
+
throw new Error(`unknown maintenance job "${job.name}"`);
|
|
167
180
|
}
|
|
168
181
|
}
|
|
169
182
|
failed(job, err) {
|
|
@@ -189,9 +202,10 @@ MaintenanceProcessor = __decorate([
|
|
|
189
202
|
__param(5, Inject(RunQueue)),
|
|
190
203
|
__param(6, Inject(OwnedPluginCleanup)),
|
|
191
204
|
__param(7, Inject(ScheduledTaskFirer)),
|
|
192
|
-
__param(8, Inject(
|
|
193
|
-
__param(9, Inject(
|
|
194
|
-
__param(10, Inject(
|
|
205
|
+
__param(8, Inject(DeviceOpResolver)),
|
|
206
|
+
__param(9, Inject(ScheduledRuns)),
|
|
207
|
+
__param(10, Inject(KERNEL_LOGGER)),
|
|
208
|
+
__param(11, Inject(getQueueToken(MAINTENANCE_QUEUE_NAME)))
|
|
195
209
|
], MaintenanceProcessor);
|
|
196
210
|
export { MaintenanceProcessor };
|
|
197
211
|
/**
|
|
@@ -54,8 +54,12 @@ export declare class PluginContextService {
|
|
|
54
54
|
private readonly registry;
|
|
55
55
|
private readonly log;
|
|
56
56
|
constructor(db: KernelDb, eventLog: Pick<EventLog, "appendIn" | "notify">, txHost: TransactionHost, points: Pick<ExtensionPointDefaults, "providersFor">, registry: Pick<ExtensionRegistry, "list">, log: KernelLogger);
|
|
57
|
-
/**
|
|
58
|
-
|
|
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>;
|
|
59
63
|
/** One provider's call, bounded by the run's signal and the time limit. */
|
|
60
64
|
private ask;
|
|
61
65
|
/**
|
|
@@ -63,7 +67,9 @@ export declare class PluginContextService {
|
|
|
63
67
|
* answer's hash with the plugin's latest `plugin_context` and appends the ones that differ (a
|
|
64
68
|
* plugin with no earlier event and no text writes nothing), plus an empty event for each plugin
|
|
65
69
|
* in `off` whose latest text is not empty. Idem key per run and plugin, so a retried run
|
|
66
|
-
* start finds its events or nothing changed. Returns the last seq written
|
|
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).
|
|
67
73
|
*/
|
|
68
74
|
private append;
|
|
69
75
|
}
|
|
@@ -18,6 +18,7 @@ import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
|
18
18
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
19
19
|
import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
20
20
|
import { latestPluginContexts, pluginContextIdemKey } from "./context/plugin-context.js";
|
|
21
|
+
import { runEndedIn } from "./run-status.js";
|
|
21
22
|
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
22
23
|
/**
|
|
23
24
|
* Run start's plugin context (memory plugin spec §6.1): asks every `context@1` provider enabled in
|
|
@@ -48,10 +49,14 @@ let PluginContextService = class PluginContextService {
|
|
|
48
49
|
this.registry = registry;
|
|
49
50
|
this.log = log;
|
|
50
51
|
}
|
|
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
|
+
*/
|
|
52
57
|
async record(input) {
|
|
53
58
|
if (input.kind !== "chat")
|
|
54
|
-
return;
|
|
59
|
+
return true;
|
|
55
60
|
let found;
|
|
56
61
|
try {
|
|
57
62
|
found = await this.points.providersFor(input.orgId, "context");
|
|
@@ -64,7 +69,7 @@ let PluginContextService = class PluginContextService {
|
|
|
64
69
|
runId: input.runId,
|
|
65
70
|
err,
|
|
66
71
|
});
|
|
67
|
-
return;
|
|
72
|
+
return true;
|
|
68
73
|
}
|
|
69
74
|
const { providers } = found;
|
|
70
75
|
const tools = providers.length > 0 ? await input.toolKeysByPlugin() : new Map();
|
|
@@ -93,10 +98,13 @@ let PluginContextService = class PluginContextService {
|
|
|
93
98
|
.filter((plugin) => !serving.has(plugin) && !found.failed.has(plugin)));
|
|
94
99
|
// nothing answered and nothing to switch off: no transaction, no row lock
|
|
95
100
|
if (texts.size === 0 && off.size === 0)
|
|
96
|
-
return;
|
|
101
|
+
return true;
|
|
97
102
|
const seq = await this.append(input, texts, off);
|
|
103
|
+
if (seq === "ended")
|
|
104
|
+
return false;
|
|
98
105
|
if (seq !== null)
|
|
99
106
|
await this.eventLog.notify(input.conversationId, seq);
|
|
107
|
+
return true;
|
|
100
108
|
}
|
|
101
109
|
/** One provider's call, bounded by the run's signal and the time limit. */
|
|
102
110
|
async ask(p, input, tools) {
|
|
@@ -141,7 +149,9 @@ let PluginContextService = class PluginContextService {
|
|
|
141
149
|
* answer's hash with the plugin's latest `plugin_context` and appends the ones that differ (a
|
|
142
150
|
* plugin with no earlier event and no text writes nothing), plus an empty event for each plugin
|
|
143
151
|
* in `off` whose latest text is not empty. Idem key per run and plugin, so a retried run
|
|
144
|
-
* start finds its events or nothing changed. Returns the last seq written
|
|
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).
|
|
145
155
|
*/
|
|
146
156
|
async append(input, texts, off) {
|
|
147
157
|
const t = this.db.db;
|
|
@@ -151,6 +161,8 @@ let PluginContextService = class PluginContextService {
|
|
|
151
161
|
.where("id", "=", input.conversationId)
|
|
152
162
|
.forUpdate()
|
|
153
163
|
.executeTakeFirst();
|
|
164
|
+
if (await runEndedIn(t, input.runId))
|
|
165
|
+
return "ended";
|
|
154
166
|
const rows = await t
|
|
155
167
|
.selectFrom("conversation_events")
|
|
156
168
|
.select(["seq", "payload"])
|
|
@@ -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)
|