@x12i/memorix-service 3.0.6 → 3.2.0
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/README.md +8 -1
- package/dist/ai-services/associate-prep.d.ts +37 -0
- package/dist/ai-services/associate-prep.d.ts.map +1 -0
- package/dist/ai-services/associate-prep.js +208 -0
- package/dist/ai-services/associate-prep.js.map +1 -0
- package/dist/ai-services/chunk-markdown.d.ts +22 -0
- package/dist/ai-services/chunk-markdown.d.ts.map +1 -0
- package/dist/ai-services/chunk-markdown.js +159 -0
- package/dist/ai-services/chunk-markdown.js.map +1 -0
- package/dist/ai-services/client.d.ts +31 -0
- package/dist/ai-services/client.d.ts.map +1 -0
- package/dist/ai-services/client.js +104 -0
- package/dist/ai-services/client.js.map +1 -0
- package/dist/ai-services/config.d.ts +17 -0
- package/dist/ai-services/config.d.ts.map +1 -0
- package/dist/ai-services/config.js +44 -0
- package/dist/ai-services/config.js.map +1 -0
- package/dist/ai-services/normalize-output.d.ts +29 -0
- package/dist/ai-services/normalize-output.d.ts.map +1 -0
- package/dist/ai-services/normalize-output.js +428 -0
- package/dist/ai-services/normalize-output.js.map +1 -0
- package/dist/ai-services/skill-execution.d.ts +24 -0
- package/dist/ai-services/skill-execution.d.ts.map +1 -0
- package/dist/ai-services/skill-execution.js +74 -0
- package/dist/ai-services/skill-execution.js.map +1 -0
- package/dist/ai-services/types.d.ts +62 -0
- package/dist/ai-services/types.d.ts.map +1 -0
- package/dist/ai-services/types.js +3 -0
- package/dist/ai-services/types.js.map +1 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +41 -6
- package/dist/app.js.map +1 -1
- package/dist/assist/ask.d.ts +35 -0
- package/dist/assist/ask.d.ts.map +1 -0
- package/dist/assist/ask.js +346 -0
- package/dist/assist/ask.js.map +1 -0
- package/dist/assist/facts.d.ts +5 -0
- package/dist/assist/facts.d.ts.map +1 -0
- package/dist/assist/facts.js +237 -0
- package/dist/assist/facts.js.map +1 -0
- package/dist/assist/merge-ask-proposal.d.ts +9 -0
- package/dist/assist/merge-ask-proposal.d.ts.map +1 -0
- package/dist/assist/merge-ask-proposal.js +57 -0
- package/dist/assist/merge-ask-proposal.js.map +1 -0
- package/dist/assist/types.d.ts +60 -0
- package/dist/assist/types.d.ts.map +1 -0
- package/dist/assist/types.js +57 -0
- package/dist/assist/types.js.map +1 -0
- package/dist/assist/validate.d.ts +4 -0
- package/dist/assist/validate.d.ts.map +1 -0
- package/dist/assist/validate.js +319 -0
- package/dist/assist/validate.js.map +1 -0
- package/dist/cli.js +29 -6
- package/dist/cli.js.map +1 -1
- package/dist/connector-catalog.d.ts +68 -0
- package/dist/connector-catalog.d.ts.map +1 -0
- package/dist/connector-catalog.js +258 -0
- package/dist/connector-catalog.js.map +1 -0
- package/dist/connector-role.d.ts +22 -0
- package/dist/connector-role.d.ts.map +1 -0
- package/dist/connector-role.js +71 -0
- package/dist/connector-role.js.map +1 -0
- package/dist/destination-runtime.d.ts +8 -0
- package/dist/destination-runtime.d.ts.map +1 -0
- package/dist/destination-runtime.js +11 -0
- package/dist/destination-runtime.js.map +1 -0
- package/dist/execution-target.d.ts +141 -0
- package/dist/execution-target.d.ts.map +1 -0
- package/dist/execution-target.js +853 -0
- package/dist/execution-target.js.map +1 -0
- package/dist/generic-connectors.d.ts +41 -0
- package/dist/generic-connectors.d.ts.map +1 -0
- package/dist/generic-connectors.js +158 -0
- package/dist/generic-connectors.js.map +1 -0
- package/dist/isolation.d.ts +1 -1
- package/dist/isolation.d.ts.map +1 -1
- package/dist/jobs-scope.d.ts +21 -0
- package/dist/jobs-scope.d.ts.map +1 -0
- package/dist/jobs-scope.js +83 -0
- package/dist/jobs-scope.js.map +1 -0
- package/dist/journey/collect.js +2 -2
- package/dist/journey/collect.js.map +1 -1
- package/dist/journey/coverage.js +3 -3
- package/dist/journey/coverage.js.map +1 -1
- package/dist/known-packs.d.ts +1 -1
- package/dist/known-packs.d.ts.map +1 -1
- package/dist/known-packs.js +1 -1
- package/dist/known-packs.js.map +1 -1
- package/dist/live.d.ts +13 -1
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +866 -2
- package/dist/live.js.map +1 -1
- package/dist/magit/magit-pack-service.d.ts +112 -0
- package/dist/magit/magit-pack-service.d.ts.map +1 -1
- package/dist/magit/magit-pack-service.js +494 -12
- package/dist/magit/magit-pack-service.js.map +1 -1
- package/dist/magit/pack-to-patchset.d.ts +20 -1
- package/dist/magit/pack-to-patchset.d.ts.map +1 -1
- package/dist/magit/pack-to-patchset.js +82 -9
- package/dist/magit/pack-to-patchset.js.map +1 -1
- package/dist/magit/package-catalog-bridge.d.ts +58 -0
- package/dist/magit/package-catalog-bridge.d.ts.map +1 -0
- package/dist/magit/package-catalog-bridge.js +220 -0
- package/dist/magit/package-catalog-bridge.js.map +1 -0
- package/dist/magit/proposal-to-pack.d.ts +22 -0
- package/dist/magit/proposal-to-pack.d.ts.map +1 -0
- package/dist/magit/proposal-to-pack.js +68 -0
- package/dist/magit/proposal-to-pack.js.map +1 -0
- package/dist/mongo-metadata-store.d.ts.map +1 -1
- package/dist/mongo-metadata-store.js +9 -3
- package/dist/mongo-metadata-store.js.map +1 -1
- package/dist/openapi/openapi.json +6262 -3139
- package/dist/org-sources.d.ts +14 -0
- package/dist/org-sources.d.ts.map +1 -0
- package/dist/org-sources.js +31 -0
- package/dist/org-sources.js.map +1 -0
- package/dist/pipeline-compile.d.ts +67 -1
- package/dist/pipeline-compile.d.ts.map +1 -1
- package/dist/pipeline-compile.js +455 -29
- package/dist/pipeline-compile.js.map +1 -1
- package/dist/pipeline-invoke.d.ts +63 -0
- package/dist/pipeline-invoke.d.ts.map +1 -0
- package/dist/pipeline-invoke.js +114 -0
- package/dist/pipeline-invoke.js.map +1 -0
- package/dist/pipeline-ports.d.ts +13 -0
- package/dist/pipeline-ports.d.ts.map +1 -0
- package/dist/pipeline-ports.js +104 -0
- package/dist/pipeline-ports.js.map +1 -0
- package/dist/platform.d.ts +47 -3
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +309 -26
- package/dist/platform.js.map +1 -1
- package/dist/routes/ai-services.d.ts +4 -0
- package/dist/routes/ai-services.d.ts.map +1 -0
- package/dist/routes/ai-services.js +239 -0
- package/dist/routes/ai-services.js.map +1 -0
- package/dist/routes/assist.d.ts +4 -0
- package/dist/routes/assist.d.ts.map +1 -0
- package/dist/routes/assist.js +75 -0
- package/dist/routes/assist.js.map +1 -0
- package/dist/routes/connector-invocations.d.ts.map +1 -1
- package/dist/routes/connector-invocations.js +18 -21
- package/dist/routes/connector-invocations.js.map +1 -1
- package/dist/routes/data-journey.js +11 -11
- package/dist/routes/data-journey.js.map +1 -1
- package/dist/routes/intelligence.d.ts.map +1 -1
- package/dist/routes/intelligence.js +19 -1
- package/dist/routes/intelligence.js.map +1 -1
- package/dist/routes/jobs.d.ts +4 -0
- package/dist/routes/jobs.d.ts.map +1 -0
- package/dist/routes/jobs.js +888 -0
- package/dist/routes/jobs.js.map +1 -0
- package/dist/routes/memory.d.ts.map +1 -1
- package/dist/routes/memory.js +278 -80
- package/dist/routes/memory.js.map +1 -1
- package/dist/routes/metadata.d.ts.map +1 -1
- package/dist/routes/metadata.js +770 -21
- package/dist/routes/metadata.js.map +1 -1
- package/dist/routes/pipelines.d.ts.map +1 -1
- package/dist/routes/pipelines.js +98 -22
- package/dist/routes/pipelines.js.map +1 -1
- package/dist/routes/relationships.d.ts.map +1 -1
- package/dist/routes/relationships.js +109 -2
- package/dist/routes/relationships.js.map +1 -1
- package/dist/routes/simulation.d.ts +7 -0
- package/dist/routes/simulation.d.ts.map +1 -0
- package/dist/routes/simulation.js +22 -0
- package/dist/routes/simulation.js.map +1 -0
- package/dist/runx/client.d.ts +18 -0
- package/dist/runx/client.d.ts.map +1 -0
- package/dist/runx/client.js +59 -0
- package/dist/runx/client.js.map +1 -0
- package/dist/simulation.d.ts +26 -0
- package/dist/simulation.d.ts.map +1 -0
- package/dist/simulation.js +46 -0
- package/dist/simulation.js.map +1 -0
- package/openapi/openapi.json +6262 -3139
- package/package.json +18 -8
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { evaluateWork, fingerprintPlan, previewWork, previewWorkSelector, promoteHeldRuns, recoverBatch, requeueRun, } from "@x12i/memorix-pipeline/v2";
|
|
3
|
+
import { ServiceError } from "../errors.js";
|
|
4
|
+
import { agentMatches, resolveConcreteOrgId, resolveJobsScope, toMemorixScope, } from "../jobs-scope.js";
|
|
5
|
+
import { listExecutionTargets, parseExecutionTarget, resolveExecutionTarget, resolvePipelineIdAsTarget, } from "../execution-target.js";
|
|
6
|
+
const PER_ORG_LIST_CAP = 200;
|
|
7
|
+
const MERGED_LIST_CAP = 500;
|
|
8
|
+
function asRecord(value) {
|
|
9
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
10
|
+
? value
|
|
11
|
+
: null;
|
|
12
|
+
}
|
|
13
|
+
function parseLimit(raw, fallback = 50, max = 200) {
|
|
14
|
+
const n = Number(raw ?? fallback);
|
|
15
|
+
if (!Number.isFinite(n) || n < 1)
|
|
16
|
+
return fallback;
|
|
17
|
+
return Math.min(Math.floor(n), max);
|
|
18
|
+
}
|
|
19
|
+
function createListAbstract(platform, scope) {
|
|
20
|
+
return async (input) => {
|
|
21
|
+
if (!platform.client) {
|
|
22
|
+
return { items: [] };
|
|
23
|
+
}
|
|
24
|
+
const ports = platform.portsFor(scope);
|
|
25
|
+
const listed = await ports.reader.listAbstract(scope, {
|
|
26
|
+
abstractType: input.abstractType,
|
|
27
|
+
filters: input.filters,
|
|
28
|
+
limit: input.limit,
|
|
29
|
+
cursor: input.cursor,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
items: listed.items.map((item) => ({
|
|
33
|
+
objectType: item.objectType,
|
|
34
|
+
recordId: item.recordId,
|
|
35
|
+
contentType: item.contentType || input.contentType,
|
|
36
|
+
})),
|
|
37
|
+
nextCursor: listed.nextCursor,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function createWorkEvaluatorDeps(platform, scope) {
|
|
42
|
+
const stores = platform.operationalStoresFor(scope.orgId);
|
|
43
|
+
const ledger = platform.pipelineLedgerFor(scope.orgId);
|
|
44
|
+
return {
|
|
45
|
+
orgId: scope.orgId,
|
|
46
|
+
agentIds: scope.agentIds,
|
|
47
|
+
ledger,
|
|
48
|
+
workDefinitions: stores.workDefinitions,
|
|
49
|
+
workEvaluations: stores.workEvaluations,
|
|
50
|
+
batches: stores.batches,
|
|
51
|
+
queueSettings: stores.queueSettings,
|
|
52
|
+
listAbstract: createListAbstract(platform, scope),
|
|
53
|
+
resolvePipeline: async (agentIds, pipelineId) => {
|
|
54
|
+
try {
|
|
55
|
+
const resolved = await resolvePipelineIdAsTarget(platform, {
|
|
56
|
+
agentIds,
|
|
57
|
+
orgId: scope.orgId,
|
|
58
|
+
pipelineId,
|
|
59
|
+
});
|
|
60
|
+
return resolved.compiled;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function parseWorkSelector(body) {
|
|
69
|
+
const rec = asRecord(body) ?? {};
|
|
70
|
+
const selector = asRecord(rec.selector) ?? rec;
|
|
71
|
+
const abstractType = String(selector.abstractType ?? "").trim();
|
|
72
|
+
if (!abstractType) {
|
|
73
|
+
throw new ServiceError("VALIDATION", "selector.abstractType required", 400);
|
|
74
|
+
}
|
|
75
|
+
const contentType = typeof selector.contentType === "string" && selector.contentType.trim()
|
|
76
|
+
? selector.contentType.trim()
|
|
77
|
+
: undefined;
|
|
78
|
+
const filter = Array.isArray(selector.filter)
|
|
79
|
+
? selector.filter
|
|
80
|
+
: undefined;
|
|
81
|
+
return { abstractType, contentType, filter };
|
|
82
|
+
}
|
|
83
|
+
async function resolveOrgIds(platform, scope) {
|
|
84
|
+
if (scope.orgId)
|
|
85
|
+
return [scope.orgId];
|
|
86
|
+
return platform.listKnownOperationalOrgIds();
|
|
87
|
+
}
|
|
88
|
+
function requireScopedOrg(scope, action) {
|
|
89
|
+
if (!scope.orgId) {
|
|
90
|
+
throw new ServiceError("SCOPE_INVALID", `${action} requires a concrete organization (not all)`, 400);
|
|
91
|
+
}
|
|
92
|
+
return scope.orgId;
|
|
93
|
+
}
|
|
94
|
+
function emptyCounts() {
|
|
95
|
+
return {
|
|
96
|
+
total: 0,
|
|
97
|
+
held: 0,
|
|
98
|
+
ready: 0,
|
|
99
|
+
leased: 0,
|
|
100
|
+
pending: 0,
|
|
101
|
+
running: 0,
|
|
102
|
+
succeeded: 0,
|
|
103
|
+
failed: 0,
|
|
104
|
+
cancelled: 0,
|
|
105
|
+
workDefinitions: 0,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function requireConfirm(body, action) {
|
|
109
|
+
const rec = asRecord(body);
|
|
110
|
+
if (rec?.confirm !== true) {
|
|
111
|
+
throw new ServiceError("CONFIRMATION_REQUIRED", `confirm:true required for ${action}`, 400);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function resolveTargetOrThrow(platform, opts) {
|
|
115
|
+
let target;
|
|
116
|
+
try {
|
|
117
|
+
target = parseExecutionTarget(opts.body);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
throw new ServiceError("VALIDATION", err instanceof Error ? err.message : String(err), 400);
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
return await resolveExecutionTarget(platform, {
|
|
124
|
+
agentIds: opts.agentIds,
|
|
125
|
+
orgId: opts.orgId,
|
|
126
|
+
target,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
131
|
+
const notFound = /not found|No origin configured/i.test(message);
|
|
132
|
+
throw new ServiceError(notFound ? "NOT_FOUND" : "VALIDATION", message, notFound ? 404 : 400);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export async function registerJobsRoutes(app, platform) {
|
|
136
|
+
app.get("/api/jobs/v1/execution-targets", async (req) => {
|
|
137
|
+
const scope = resolveJobsScope(req);
|
|
138
|
+
const agentIds = scope.agentIds?.length ? scope.agentIds : ["*"];
|
|
139
|
+
const catalog = await listExecutionTargets(platform, {
|
|
140
|
+
agentIds,
|
|
141
|
+
orgId: scope.orgId,
|
|
142
|
+
});
|
|
143
|
+
return { scope, ...catalog };
|
|
144
|
+
});
|
|
145
|
+
app.get("/api/jobs/v1/overview", async (req) => {
|
|
146
|
+
const scope = resolveJobsScope(req);
|
|
147
|
+
const orgIds = await resolveOrgIds(platform, scope);
|
|
148
|
+
const counts = emptyCounts();
|
|
149
|
+
let stuckLeases = 0;
|
|
150
|
+
const failureRows = [];
|
|
151
|
+
const inProgressRows = [];
|
|
152
|
+
const completedRows = [];
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
let settings = null;
|
|
155
|
+
for (const orgId of orgIds) {
|
|
156
|
+
const stores = platform.operationalStoresFor(orgId);
|
|
157
|
+
const ledger = platform.pipelineLedgerFor(orgId);
|
|
158
|
+
if (scope.orgId) {
|
|
159
|
+
settings = await stores.queueSettings.get(orgId);
|
|
160
|
+
}
|
|
161
|
+
const work = await stores.workDefinitions.list(orgId, {
|
|
162
|
+
limit: PER_ORG_LIST_CAP,
|
|
163
|
+
});
|
|
164
|
+
const filteredWork = work.filter((w) => agentMatches(w.agentIds, scope.agentIds));
|
|
165
|
+
counts.workDefinitions += filteredWork.length;
|
|
166
|
+
const workNameById = new Map(filteredWork.map((w) => [w.workId, w.name?.trim() || ""]));
|
|
167
|
+
const runs = await ledger.listRuns(orgId, { limit: PER_ORG_LIST_CAP });
|
|
168
|
+
for (const run of runs) {
|
|
169
|
+
if (!agentMatches(run.agentIds, scope.agentIds))
|
|
170
|
+
continue;
|
|
171
|
+
counts.total += 1;
|
|
172
|
+
const admission = run.admission?.state ?? "ready";
|
|
173
|
+
if (admission === "held")
|
|
174
|
+
counts.held += 1;
|
|
175
|
+
else if (admission === "ready")
|
|
176
|
+
counts.ready += 1;
|
|
177
|
+
// Dual-read: published types may still say "claimed"; runtime normalizes to "leased".
|
|
178
|
+
else if (admission === "leased" || admission === "claimed") {
|
|
179
|
+
counts.leased += 1;
|
|
180
|
+
}
|
|
181
|
+
if (run.status === "pending")
|
|
182
|
+
counts.pending += 1;
|
|
183
|
+
else if (run.status === "running")
|
|
184
|
+
counts.running += 1;
|
|
185
|
+
else if (run.status === "succeeded")
|
|
186
|
+
counts.succeeded += 1;
|
|
187
|
+
else if (run.status === "failed")
|
|
188
|
+
counts.failed += 1;
|
|
189
|
+
else if (run.status === "cancelled")
|
|
190
|
+
counts.cancelled += 1;
|
|
191
|
+
const leaseExpiresAt = run.admission?.leaseExpiresAt;
|
|
192
|
+
if ((admission === "leased" || admission === "claimed") &&
|
|
193
|
+
leaseExpiresAt &&
|
|
194
|
+
Date.parse(leaseExpiresAt) < now) {
|
|
195
|
+
stuckLeases += 1;
|
|
196
|
+
}
|
|
197
|
+
const workId = run.workId ?? null;
|
|
198
|
+
const workName = workId && workNameById.get(workId)
|
|
199
|
+
? workNameById.get(workId)
|
|
200
|
+
: null;
|
|
201
|
+
const itemRef = run.itemRef
|
|
202
|
+
? {
|
|
203
|
+
objectType: run.itemRef.objectType ?? undefined,
|
|
204
|
+
recordId: run.itemRef.recordId ?? undefined,
|
|
205
|
+
contentType: run.itemRef.contentType ?? undefined,
|
|
206
|
+
}
|
|
207
|
+
: null;
|
|
208
|
+
const row = {
|
|
209
|
+
runId: run.runId,
|
|
210
|
+
orgId,
|
|
211
|
+
pipelineId: run.pipelineId,
|
|
212
|
+
workId,
|
|
213
|
+
workName,
|
|
214
|
+
batchId: run.batchId ?? null,
|
|
215
|
+
itemRef,
|
|
216
|
+
status: run.status,
|
|
217
|
+
updatedAt: run.updatedAt,
|
|
218
|
+
error: run.error ?? null,
|
|
219
|
+
};
|
|
220
|
+
if (run.status === "failed")
|
|
221
|
+
failureRows.push(row);
|
|
222
|
+
if (run.status === "running" || run.status === "pending") {
|
|
223
|
+
inProgressRows.push(row);
|
|
224
|
+
}
|
|
225
|
+
if (run.status === "succeeded")
|
|
226
|
+
completedRows.push(row);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const byUpdatedDesc = (a, b) => Date.parse(String(b.updatedAt ?? "")) - Date.parse(String(a.updatedAt ?? ""));
|
|
230
|
+
return {
|
|
231
|
+
scope,
|
|
232
|
+
settings,
|
|
233
|
+
counts,
|
|
234
|
+
stuckLeases,
|
|
235
|
+
recentFailures: failureRows.sort(byUpdatedDesc).slice(0, 20),
|
|
236
|
+
recentInProgress: inProgressRows.sort(byUpdatedDesc).slice(0, 20),
|
|
237
|
+
recentCompleted: completedRows.sort(byUpdatedDesc).slice(0, 20),
|
|
238
|
+
orgCount: orgIds.length,
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
app.get("/api/jobs/v1/settings", async (req) => {
|
|
242
|
+
const scope = resolveJobsScope(req);
|
|
243
|
+
const orgId = requireScopedOrg(scope, "queue settings");
|
|
244
|
+
const stores = platform.operationalStoresFor(orgId);
|
|
245
|
+
const settings = await stores.queueSettings.get(orgId);
|
|
246
|
+
return { scope, settings };
|
|
247
|
+
});
|
|
248
|
+
app.get("/api/jobs/v1/work", async (req) => {
|
|
249
|
+
const scope = resolveJobsScope(req);
|
|
250
|
+
const q = req.query;
|
|
251
|
+
const enabled = q.enabled === "true" ? true : q.enabled === "false" ? false : undefined;
|
|
252
|
+
const limit = parseLimit(q.limit, 50, MERGED_LIST_CAP);
|
|
253
|
+
const orgIds = await resolveOrgIds(platform, scope);
|
|
254
|
+
const work = [];
|
|
255
|
+
for (const orgId of orgIds) {
|
|
256
|
+
if (work.length >= limit)
|
|
257
|
+
break;
|
|
258
|
+
const stores = platform.operationalStoresFor(orgId);
|
|
259
|
+
const listed = await stores.workDefinitions.list(orgId, {
|
|
260
|
+
enabled,
|
|
261
|
+
limit: Math.min(PER_ORG_LIST_CAP, limit - work.length),
|
|
262
|
+
});
|
|
263
|
+
for (const row of listed) {
|
|
264
|
+
if (!agentMatches(row.agentIds, scope.agentIds))
|
|
265
|
+
continue;
|
|
266
|
+
work.push({ ...row, orgId: row.orgId ?? orgId });
|
|
267
|
+
if (work.length >= limit)
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { scope, work };
|
|
272
|
+
});
|
|
273
|
+
app.get("/api/jobs/v1/work/:workId", async (req) => {
|
|
274
|
+
const scope = resolveJobsScope(req);
|
|
275
|
+
const { workId } = req.params;
|
|
276
|
+
const orgId = resolveConcreteOrgId(scope, req.query);
|
|
277
|
+
const stores = platform.operationalStoresFor(orgId);
|
|
278
|
+
const work = await stores.workDefinitions.get(orgId, workId);
|
|
279
|
+
if (!work) {
|
|
280
|
+
throw new ServiceError("NOT_FOUND", `work ${workId} not found`, 404);
|
|
281
|
+
}
|
|
282
|
+
if (!agentMatches(work.agentIds, scope.agentIds)) {
|
|
283
|
+
throw new ServiceError("NOT_FOUND", `work ${workId} not found`, 404);
|
|
284
|
+
}
|
|
285
|
+
const evaluations = await stores.workEvaluations.listForWork(orgId, workId, 50);
|
|
286
|
+
return { scope, work: { ...work, orgId: work.orgId ?? orgId }, evaluations };
|
|
287
|
+
});
|
|
288
|
+
app.post("/api/jobs/v1/work/:workId/preview", async (req) => {
|
|
289
|
+
const scope = resolveJobsScope(req);
|
|
290
|
+
const orgId = requireScopedOrg(scope, "work preview");
|
|
291
|
+
const { workId } = req.params;
|
|
292
|
+
const memorixScope = toMemorixScope(orgId, scope.agentIds);
|
|
293
|
+
const deps = createWorkEvaluatorDeps(platform, memorixScope);
|
|
294
|
+
try {
|
|
295
|
+
const preview = await previewWork(deps, workId);
|
|
296
|
+
return { scope, workId, preview };
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
300
|
+
if (/not found/i.test(message)) {
|
|
301
|
+
throw new ServiceError("NOT_FOUND", message, 404);
|
|
302
|
+
}
|
|
303
|
+
throw new ServiceError("VALIDATION", message, 400);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
app.get("/api/jobs/v1/batches", async (req) => {
|
|
307
|
+
const scope = resolveJobsScope(req);
|
|
308
|
+
const q = req.query;
|
|
309
|
+
const limit = parseLimit(q.limit, 50, MERGED_LIST_CAP);
|
|
310
|
+
const orgIds = await resolveOrgIds(platform, scope);
|
|
311
|
+
const batches = [];
|
|
312
|
+
for (const orgId of orgIds) {
|
|
313
|
+
if (batches.length >= limit)
|
|
314
|
+
break;
|
|
315
|
+
const stores = platform.operationalStoresFor(orgId);
|
|
316
|
+
const listed = await stores.batches.list(orgId, {
|
|
317
|
+
workId: q.workId,
|
|
318
|
+
limit: Math.min(PER_ORG_LIST_CAP, limit - batches.length),
|
|
319
|
+
});
|
|
320
|
+
for (const row of listed) {
|
|
321
|
+
batches.push({ ...row, orgId: row.orgId ?? orgId });
|
|
322
|
+
if (batches.length >= limit)
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return { scope, batches };
|
|
327
|
+
});
|
|
328
|
+
app.get("/api/jobs/v1/batches/:batchId", async (req) => {
|
|
329
|
+
const scope = resolveJobsScope(req);
|
|
330
|
+
const { batchId } = req.params;
|
|
331
|
+
const orgId = resolveConcreteOrgId(scope, req.query);
|
|
332
|
+
const stores = platform.operationalStoresFor(orgId);
|
|
333
|
+
const ledger = platform.pipelineLedgerFor(orgId);
|
|
334
|
+
const batch = await stores.batches.get(orgId, batchId);
|
|
335
|
+
if (!batch) {
|
|
336
|
+
throw new ServiceError("NOT_FOUND", `batch ${batchId} not found`, 404);
|
|
337
|
+
}
|
|
338
|
+
const runs = (await ledger.listRuns(orgId, {
|
|
339
|
+
batchId,
|
|
340
|
+
limit: 200,
|
|
341
|
+
})).filter((run) => agentMatches(run.agentIds, scope.agentIds));
|
|
342
|
+
return {
|
|
343
|
+
scope,
|
|
344
|
+
batch: { ...batch, orgId: batch.orgId ?? orgId },
|
|
345
|
+
runs: runs.map((r) => ({ ...r, orgId: r.orgId ?? orgId })),
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
app.get("/api/jobs/v1/runs", async (req) => {
|
|
349
|
+
const scope = resolveJobsScope(req);
|
|
350
|
+
const q = req.query;
|
|
351
|
+
const limit = parseLimit(q.limit, 50, MERGED_LIST_CAP);
|
|
352
|
+
const status = q.status && q.status.includes(",")
|
|
353
|
+
? q.status.split(",").map((s) => s.trim()).filter(Boolean)
|
|
354
|
+
: q.status;
|
|
355
|
+
const orgIds = await resolveOrgIds(platform, scope);
|
|
356
|
+
const runs = [];
|
|
357
|
+
for (const orgId of orgIds) {
|
|
358
|
+
if (runs.length >= limit)
|
|
359
|
+
break;
|
|
360
|
+
const ledger = platform.pipelineLedgerFor(orgId);
|
|
361
|
+
const listed = await ledger.listRuns(orgId, {
|
|
362
|
+
status,
|
|
363
|
+
workId: q.workId,
|
|
364
|
+
batchId: q.batchId,
|
|
365
|
+
pipelineId: q.pipelineId,
|
|
366
|
+
admissionState: q.admissionState,
|
|
367
|
+
objectType: q.objectType?.trim() || undefined,
|
|
368
|
+
recordId: q.recordId?.trim() || undefined,
|
|
369
|
+
limit: Math.min(PER_ORG_LIST_CAP, limit - runs.length),
|
|
370
|
+
});
|
|
371
|
+
for (const run of listed) {
|
|
372
|
+
if (!agentMatches(run.agentIds, scope.agentIds))
|
|
373
|
+
continue;
|
|
374
|
+
runs.push({ ...run, orgId: run.orgId ?? orgId });
|
|
375
|
+
if (runs.length >= limit)
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return { scope, runs };
|
|
380
|
+
});
|
|
381
|
+
app.get("/api/jobs/v1/runs/:runId", async (req) => {
|
|
382
|
+
const scope = resolveJobsScope(req);
|
|
383
|
+
const { runId } = req.params;
|
|
384
|
+
const orgId = resolveConcreteOrgId(scope, req.query);
|
|
385
|
+
const ledger = platform.pipelineLedgerFor(orgId);
|
|
386
|
+
const run = await ledger.getRun(orgId, runId);
|
|
387
|
+
if (!run) {
|
|
388
|
+
throw new ServiceError("NOT_FOUND", `run ${runId} not found`, 404);
|
|
389
|
+
}
|
|
390
|
+
if (!agentMatches(run.agentIds, scope.agentIds)) {
|
|
391
|
+
throw new ServiceError("NOT_FOUND", `run ${runId} not found`, 404);
|
|
392
|
+
}
|
|
393
|
+
const attempts = await ledger.listAttempts(orgId, runId);
|
|
394
|
+
const captured = run.capturedContextRef ?? null;
|
|
395
|
+
return {
|
|
396
|
+
scope,
|
|
397
|
+
runId,
|
|
398
|
+
status: run.status,
|
|
399
|
+
pipelineId: run.pipelineId,
|
|
400
|
+
workId: run.workId ?? null,
|
|
401
|
+
batchId: run.batchId ?? null,
|
|
402
|
+
attempts,
|
|
403
|
+
artifacts: captured ? [captured] : [],
|
|
404
|
+
capturedContextRef: captured,
|
|
405
|
+
run: { ...run, orgId: run.orgId ?? orgId },
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
app.get("/api/jobs/v1/information/preview", async (req) => {
|
|
409
|
+
const scope = resolveJobsScope(req);
|
|
410
|
+
const orgId = requireScopedOrg(scope, "information preview");
|
|
411
|
+
const q = req.query;
|
|
412
|
+
const body = asRecord(req.body);
|
|
413
|
+
const selector = parseWorkSelector({
|
|
414
|
+
abstractType: body?.abstractType ?? q.abstractType,
|
|
415
|
+
contentType: body?.contentType ?? q.contentType,
|
|
416
|
+
filter: body?.filter,
|
|
417
|
+
});
|
|
418
|
+
const memorixScope = toMemorixScope(orgId, scope.agentIds);
|
|
419
|
+
const preview = await previewWorkSelector(createListAbstract(platform, memorixScope), selector, { limit: parseLimit(q.limit ?? body?.limit, 50) });
|
|
420
|
+
return { scope, selector, preview };
|
|
421
|
+
});
|
|
422
|
+
app.post("/api/jobs/v1/information/preview", async (req) => {
|
|
423
|
+
const scope = resolveJobsScope(req);
|
|
424
|
+
const orgId = requireScopedOrg(scope, "information preview");
|
|
425
|
+
const body = asRecord(req.body) ?? {};
|
|
426
|
+
const selector = parseWorkSelector(body);
|
|
427
|
+
const memorixScope = toMemorixScope(orgId, scope.agentIds);
|
|
428
|
+
const preview = await previewWorkSelector(createListAbstract(platform, memorixScope), selector, { limit: parseLimit(body.limit, 50) });
|
|
429
|
+
return { scope, selector, preview };
|
|
430
|
+
});
|
|
431
|
+
// ── P4 writes ────────────────────────────────────────────────────────────
|
|
432
|
+
app.put("/api/jobs/v1/settings", async (req) => {
|
|
433
|
+
const scope = resolveJobsScope(req);
|
|
434
|
+
const orgId = requireScopedOrg(scope, "update queue settings");
|
|
435
|
+
const body = asRecord(req.body) ?? {};
|
|
436
|
+
const stores = platform.operationalStoresFor(orgId);
|
|
437
|
+
const settings = await stores.queueSettings.save(orgId, {
|
|
438
|
+
queueMode: body.queueMode === "paused" || body.queueMode === "running"
|
|
439
|
+
? body.queueMode
|
|
440
|
+
: undefined,
|
|
441
|
+
maxConcurrentRuns: typeof body.maxConcurrentRuns === "number"
|
|
442
|
+
? body.maxConcurrentRuns
|
|
443
|
+
: undefined,
|
|
444
|
+
queueAdmissionIntervalMs: typeof body.queueAdmissionIntervalMs === "number"
|
|
445
|
+
? body.queueAdmissionIntervalMs
|
|
446
|
+
: undefined,
|
|
447
|
+
batchRunMaxAttempts: typeof body.batchRunMaxAttempts === "number"
|
|
448
|
+
? body.batchRunMaxAttempts
|
|
449
|
+
: undefined,
|
|
450
|
+
continuousRunMaxAttempts: typeof body.continuousRunMaxAttempts === "number"
|
|
451
|
+
? body.continuousRunMaxAttempts
|
|
452
|
+
: undefined,
|
|
453
|
+
});
|
|
454
|
+
let promoted = 0;
|
|
455
|
+
if (settings.queueMode === "running") {
|
|
456
|
+
promoted = await promoteHeldRuns({
|
|
457
|
+
ledger: platform.pipelineLedgerFor(orgId),
|
|
458
|
+
orgId,
|
|
459
|
+
queueSettings: stores.queueSettings,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
return { scope, settings, promoted };
|
|
463
|
+
});
|
|
464
|
+
app.post("/api/jobs/v1/work", async (req) => {
|
|
465
|
+
const scope = resolveJobsScope(req);
|
|
466
|
+
const orgId = requireScopedOrg(scope, "create work");
|
|
467
|
+
if (!scope.agentIds?.length) {
|
|
468
|
+
throw new ServiceError("SCOPE_INVALID", "agentIds required to create work", 400);
|
|
469
|
+
}
|
|
470
|
+
const body = asRecord(req.body) ?? {};
|
|
471
|
+
const name = String(body.name ?? "").trim();
|
|
472
|
+
if (!name) {
|
|
473
|
+
throw new ServiceError("VALIDATION", "name required", 400);
|
|
474
|
+
}
|
|
475
|
+
const resolved = await resolveTargetOrThrow(platform, {
|
|
476
|
+
agentIds: scope.agentIds,
|
|
477
|
+
orgId,
|
|
478
|
+
body,
|
|
479
|
+
});
|
|
480
|
+
const selectionMode = body.selectionMode === "none" ? "none" : "items";
|
|
481
|
+
let selector;
|
|
482
|
+
if (selectionMode === "items") {
|
|
483
|
+
selector = parseWorkSelector(body.selector ?? body);
|
|
484
|
+
}
|
|
485
|
+
else if (body.selector != null || (asRecord(body)?.abstractType != null)) {
|
|
486
|
+
try {
|
|
487
|
+
selector = parseWorkSelector(body.selector ?? body);
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
selector = undefined;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const cadenceRec = asRecord(body.cadence) ?? {};
|
|
494
|
+
let cadence;
|
|
495
|
+
if (cadenceRec.kind === "schedule") {
|
|
496
|
+
const scheduleId = String(cadenceRec.scheduleId ?? "").trim();
|
|
497
|
+
if (!scheduleId) {
|
|
498
|
+
throw new ServiceError("VALIDATION", "cadence.scheduleId required", 400);
|
|
499
|
+
}
|
|
500
|
+
cadence = { kind: "schedule", scheduleId };
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
const intervalMinutes = Number(cadenceRec.intervalMinutes ?? 15);
|
|
504
|
+
if (!Number.isFinite(intervalMinutes) || intervalMinutes < 1) {
|
|
505
|
+
throw new ServiceError("VALIDATION", "cadence.intervalMinutes required", 400);
|
|
506
|
+
}
|
|
507
|
+
cadence = { kind: "interval", intervalMinutes };
|
|
508
|
+
}
|
|
509
|
+
const policyRec = asRecord(body.policy) ?? {};
|
|
510
|
+
const priority = Number(policyRec.priority ?? 5);
|
|
511
|
+
if (!Number.isFinite(priority)) {
|
|
512
|
+
throw new ServiceError("VALIDATION", "policy.priority required", 400);
|
|
513
|
+
}
|
|
514
|
+
const now = new Date().toISOString();
|
|
515
|
+
const stores = platform.operationalStoresFor(orgId);
|
|
516
|
+
const work = await stores.workDefinitions.create({
|
|
517
|
+
workId: randomUUID(),
|
|
518
|
+
orgId,
|
|
519
|
+
agentIds: [...scope.agentIds],
|
|
520
|
+
name,
|
|
521
|
+
description: typeof body.description === "string" ? body.description : undefined,
|
|
522
|
+
enabled: body.enabled === false ? false : true,
|
|
523
|
+
pipelineId: resolved.pipelineId,
|
|
524
|
+
executionTarget: resolved.target,
|
|
525
|
+
selectionMode,
|
|
526
|
+
selector,
|
|
527
|
+
cadence,
|
|
528
|
+
policy: {
|
|
529
|
+
priority,
|
|
530
|
+
maxAttempts: typeof policyRec.maxAttempts === "number"
|
|
531
|
+
? policyRec.maxAttempts
|
|
532
|
+
: undefined,
|
|
533
|
+
duplicate: asRecord(policyRec.duplicate),
|
|
534
|
+
},
|
|
535
|
+
inputTemplate: body.inputTemplate,
|
|
536
|
+
nextEvaluationAt: now,
|
|
537
|
+
createdAt: now,
|
|
538
|
+
updatedAt: now,
|
|
539
|
+
});
|
|
540
|
+
return { scope, work };
|
|
541
|
+
});
|
|
542
|
+
app.patch("/api/jobs/v1/work/:workId", async (req) => {
|
|
543
|
+
const scope = resolveJobsScope(req);
|
|
544
|
+
const orgId = requireScopedOrg(scope, "update work");
|
|
545
|
+
const { workId } = req.params;
|
|
546
|
+
const body = asRecord(req.body) ?? {};
|
|
547
|
+
const stores = platform.operationalStoresFor(orgId);
|
|
548
|
+
const existing = await stores.workDefinitions.get(orgId, workId);
|
|
549
|
+
if (!existing || !agentMatches(existing.agentIds, scope.agentIds)) {
|
|
550
|
+
throw new ServiceError("NOT_FOUND", `work ${workId} not found`, 404);
|
|
551
|
+
}
|
|
552
|
+
const patch = { updatedAt: new Date().toISOString() };
|
|
553
|
+
if (typeof body.name === "string")
|
|
554
|
+
patch.name = body.name.trim();
|
|
555
|
+
if (typeof body.description === "string")
|
|
556
|
+
patch.description = body.description;
|
|
557
|
+
if (typeof body.enabled === "boolean")
|
|
558
|
+
patch.enabled = body.enabled;
|
|
559
|
+
if (body.executionTarget != null || typeof body.pipelineId === "string") {
|
|
560
|
+
const resolved = await resolveTargetOrThrow(platform, {
|
|
561
|
+
agentIds: existing.agentIds,
|
|
562
|
+
orgId,
|
|
563
|
+
body: {
|
|
564
|
+
executionTarget: body.executionTarget,
|
|
565
|
+
pipelineId: body.pipelineId,
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
patch.pipelineId = resolved.pipelineId;
|
|
569
|
+
patch.executionTarget = resolved.target;
|
|
570
|
+
}
|
|
571
|
+
if (body.selector)
|
|
572
|
+
patch.selector = parseWorkSelector(body.selector);
|
|
573
|
+
if (body.selectionMode === "none" || body.selectionMode === "items") {
|
|
574
|
+
patch.selectionMode = body.selectionMode;
|
|
575
|
+
}
|
|
576
|
+
if (body.cadence) {
|
|
577
|
+
const cadenceRec = asRecord(body.cadence) ?? {};
|
|
578
|
+
if (cadenceRec.kind === "schedule") {
|
|
579
|
+
patch.cadence = {
|
|
580
|
+
kind: "schedule",
|
|
581
|
+
scheduleId: String(cadenceRec.scheduleId ?? "").trim(),
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
else if (cadenceRec.kind === "interval") {
|
|
585
|
+
patch.cadence = {
|
|
586
|
+
kind: "interval",
|
|
587
|
+
intervalMinutes: Number(cadenceRec.intervalMinutes),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (body.policy) {
|
|
592
|
+
const policyRec = asRecord(body.policy) ?? {};
|
|
593
|
+
patch.policy = {
|
|
594
|
+
...existing.policy,
|
|
595
|
+
...policyRec,
|
|
596
|
+
priority: Number(policyRec.priority ?? existing.policy.priority),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
if (body.inputTemplate !== undefined)
|
|
600
|
+
patch.inputTemplate = body.inputTemplate;
|
|
601
|
+
const work = await stores.workDefinitions.patch(orgId, workId, patch);
|
|
602
|
+
return { scope, work };
|
|
603
|
+
});
|
|
604
|
+
app.delete("/api/jobs/v1/work/:workId", async (req) => {
|
|
605
|
+
const scope = resolveJobsScope(req);
|
|
606
|
+
const orgId = requireScopedOrg(scope, "delete work");
|
|
607
|
+
const { workId } = req.params;
|
|
608
|
+
const stores = platform.operationalStoresFor(orgId);
|
|
609
|
+
const existing = await stores.workDefinitions.get(orgId, workId);
|
|
610
|
+
if (!existing || !agentMatches(existing.agentIds, scope.agentIds)) {
|
|
611
|
+
throw new ServiceError("NOT_FOUND", `work ${workId} not found`, 404);
|
|
612
|
+
}
|
|
613
|
+
await stores.workDefinitions.delete(orgId, workId);
|
|
614
|
+
return { scope, deleted: true, workId };
|
|
615
|
+
});
|
|
616
|
+
app.post("/api/jobs/v1/work/:workId/evaluate", async (req) => {
|
|
617
|
+
const scope = resolveJobsScope(req);
|
|
618
|
+
const orgId = requireScopedOrg(scope, "evaluate work");
|
|
619
|
+
const { workId } = req.params;
|
|
620
|
+
const memorixScope = toMemorixScope(orgId, scope.agentIds ?? []);
|
|
621
|
+
const deps = createWorkEvaluatorDeps(platform, memorixScope);
|
|
622
|
+
try {
|
|
623
|
+
const evaluation = await evaluateWork(deps, workId);
|
|
624
|
+
return { scope, workId, evaluation };
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
627
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
628
|
+
if (/not found/i.test(message)) {
|
|
629
|
+
throw new ServiceError("NOT_FOUND", message, 404);
|
|
630
|
+
}
|
|
631
|
+
throw new ServiceError("VALIDATION", message, 400);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
app.post("/api/jobs/v1/work/:workId/actions/:action", async (req) => {
|
|
635
|
+
const scope = resolveJobsScope(req);
|
|
636
|
+
const orgId = requireScopedOrg(scope, "work action");
|
|
637
|
+
const { workId, action } = req.params;
|
|
638
|
+
const stores = platform.operationalStoresFor(orgId);
|
|
639
|
+
const existing = await stores.workDefinitions.get(orgId, workId);
|
|
640
|
+
if (!existing || !agentMatches(existing.agentIds, scope.agentIds)) {
|
|
641
|
+
throw new ServiceError("NOT_FOUND", `work ${workId} not found`, 404);
|
|
642
|
+
}
|
|
643
|
+
if (action === "pause") {
|
|
644
|
+
const work = await stores.workDefinitions.patch(orgId, workId, {
|
|
645
|
+
enabled: false,
|
|
646
|
+
updatedAt: new Date().toISOString(),
|
|
647
|
+
});
|
|
648
|
+
return { scope, work, action };
|
|
649
|
+
}
|
|
650
|
+
if (action === "resume") {
|
|
651
|
+
const work = await stores.workDefinitions.patch(orgId, workId, {
|
|
652
|
+
enabled: true,
|
|
653
|
+
nextEvaluationAt: new Date().toISOString(),
|
|
654
|
+
updatedAt: new Date().toISOString(),
|
|
655
|
+
});
|
|
656
|
+
return { scope, work, action };
|
|
657
|
+
}
|
|
658
|
+
if (action === "evaluate-now") {
|
|
659
|
+
const memorixScope = toMemorixScope(orgId, scope.agentIds ?? []);
|
|
660
|
+
const evaluation = await evaluateWork(createWorkEvaluatorDeps(platform, memorixScope), workId);
|
|
661
|
+
return { scope, workId, action, evaluation };
|
|
662
|
+
}
|
|
663
|
+
throw new ServiceError("VALIDATION", `unknown action ${action}; use pause|resume|evaluate-now`, 400);
|
|
664
|
+
});
|
|
665
|
+
app.post("/api/jobs/v1/on-demand/single", async (req) => {
|
|
666
|
+
const scope = resolveJobsScope(req);
|
|
667
|
+
const orgId = requireScopedOrg(scope, "on-demand single");
|
|
668
|
+
if (!scope.agentIds?.length) {
|
|
669
|
+
throw new ServiceError("SCOPE_INVALID", "agentIds required", 400);
|
|
670
|
+
}
|
|
671
|
+
const body = asRecord(req.body) ?? {};
|
|
672
|
+
const itemRec = asRecord(body.itemRef) ?? {};
|
|
673
|
+
const objectType = String(itemRec.objectType ?? "").trim();
|
|
674
|
+
const recordId = String(itemRec.recordId ?? "").trim();
|
|
675
|
+
const resolved = await resolveTargetOrThrow(platform, {
|
|
676
|
+
agentIds: scope.agentIds,
|
|
677
|
+
orgId,
|
|
678
|
+
body,
|
|
679
|
+
});
|
|
680
|
+
// Connector pulls do not require an information itemRef.
|
|
681
|
+
if (resolved.target.kind !== "connector" && (!objectType || !recordId)) {
|
|
682
|
+
throw new ServiceError("VALIDATION", "itemRef.objectType/recordId required", 400);
|
|
683
|
+
}
|
|
684
|
+
const itemRef = objectType && recordId
|
|
685
|
+
? {
|
|
686
|
+
objectType,
|
|
687
|
+
recordId,
|
|
688
|
+
contentType: typeof itemRec.contentType === "string"
|
|
689
|
+
? itemRec.contentType
|
|
690
|
+
: undefined,
|
|
691
|
+
}
|
|
692
|
+
: undefined;
|
|
693
|
+
const compiled = resolved.compiled;
|
|
694
|
+
const stores = platform.operationalStoresFor(orgId);
|
|
695
|
+
const settings = await stores.queueSettings.get(orgId);
|
|
696
|
+
const paused = settings.queueMode === "paused";
|
|
697
|
+
const ledger = platform.pipelineLedgerFor(orgId);
|
|
698
|
+
const run = await ledger.createRun({
|
|
699
|
+
orgId,
|
|
700
|
+
agentIds: scope.agentIds,
|
|
701
|
+
pipelineId: resolved.pipelineId,
|
|
702
|
+
version: String(compiled.version ?? "1"),
|
|
703
|
+
planFingerprint: fingerprintPlan(compiled),
|
|
704
|
+
capturedPlan: structuredClone(compiled),
|
|
705
|
+
trigger: "on-demand",
|
|
706
|
+
input: {
|
|
707
|
+
...(asRecord(body.input) ?? {}),
|
|
708
|
+
...(itemRef ? { itemRef } : {}),
|
|
709
|
+
executionTarget: resolved.target,
|
|
710
|
+
},
|
|
711
|
+
itemRef,
|
|
712
|
+
sourceType: "on-demand-single",
|
|
713
|
+
priority: typeof body.priority === "number" ? body.priority : 0,
|
|
714
|
+
admission: { state: paused ? "held" : "ready" },
|
|
715
|
+
attemptBudget: settings.batchRunMaxAttempts,
|
|
716
|
+
runKey: typeof body.idempotencyKey === "string"
|
|
717
|
+
? `ondemand:${body.idempotencyKey}`
|
|
718
|
+
: undefined,
|
|
719
|
+
});
|
|
720
|
+
return { scope, run, executionTarget: resolved.target };
|
|
721
|
+
});
|
|
722
|
+
app.post("/api/jobs/v1/on-demand/batches", async (req) => {
|
|
723
|
+
const scope = resolveJobsScope(req);
|
|
724
|
+
const orgId = requireScopedOrg(scope, "on-demand batch");
|
|
725
|
+
if (!scope.agentIds?.length) {
|
|
726
|
+
throw new ServiceError("SCOPE_INVALID", "agentIds required", 400);
|
|
727
|
+
}
|
|
728
|
+
const body = asRecord(req.body) ?? {};
|
|
729
|
+
const items = Array.isArray(body.itemRefs) ? body.itemRefs : [];
|
|
730
|
+
const resolved = await resolveTargetOrThrow(platform, {
|
|
731
|
+
agentIds: scope.agentIds,
|
|
732
|
+
orgId,
|
|
733
|
+
body,
|
|
734
|
+
});
|
|
735
|
+
if (resolved.target.kind !== "connector" && items.length === 0) {
|
|
736
|
+
throw new ServiceError("VALIDATION", "itemRefs[] required", 400);
|
|
737
|
+
}
|
|
738
|
+
const itemRefs = items.length > 0
|
|
739
|
+
? items.map((raw) => {
|
|
740
|
+
const r = asRecord(raw) ?? {};
|
|
741
|
+
const objectType = String(r.objectType ?? "").trim();
|
|
742
|
+
const recordId = String(r.recordId ?? "").trim();
|
|
743
|
+
if (!objectType || !recordId) {
|
|
744
|
+
throw new ServiceError("VALIDATION", "each itemRef needs objectType and recordId", 400);
|
|
745
|
+
}
|
|
746
|
+
return {
|
|
747
|
+
objectType,
|
|
748
|
+
recordId,
|
|
749
|
+
contentType: typeof r.contentType === "string" ? r.contentType : undefined,
|
|
750
|
+
};
|
|
751
|
+
})
|
|
752
|
+
: [
|
|
753
|
+
{
|
|
754
|
+
objectType: "_connector",
|
|
755
|
+
recordId: resolved.target.kind === "connector"
|
|
756
|
+
? resolved.target.sourceId
|
|
757
|
+
: "batch",
|
|
758
|
+
},
|
|
759
|
+
];
|
|
760
|
+
const compiled = resolved.compiled;
|
|
761
|
+
const planFingerprint = fingerprintPlan(compiled);
|
|
762
|
+
const stores = platform.operationalStoresFor(orgId);
|
|
763
|
+
const settings = await stores.queueSettings.get(orgId);
|
|
764
|
+
const paused = settings.queueMode === "paused";
|
|
765
|
+
const nowIso = new Date().toISOString();
|
|
766
|
+
const batchId = randomUUID();
|
|
767
|
+
const batch = await stores.batches.create({
|
|
768
|
+
batchId,
|
|
769
|
+
orgId,
|
|
770
|
+
agentIds: [...scope.agentIds],
|
|
771
|
+
name: String(body.name ?? `on-demand:${nowIso}`),
|
|
772
|
+
pipelineId: resolved.pipelineId,
|
|
773
|
+
executionTarget: resolved.target,
|
|
774
|
+
source: "on-demand",
|
|
775
|
+
selectionKind: "cohort",
|
|
776
|
+
cohortItemRefs: itemRefs,
|
|
777
|
+
status: "queued",
|
|
778
|
+
counts: {
|
|
779
|
+
total: itemRefs.length,
|
|
780
|
+
pending: itemRefs.length,
|
|
781
|
+
running: 0,
|
|
782
|
+
succeeded: 0,
|
|
783
|
+
failed: 0,
|
|
784
|
+
cancelled: 0,
|
|
785
|
+
},
|
|
786
|
+
exclusions: [],
|
|
787
|
+
policy: { priority: typeof body.priority === "number" ? body.priority : 0 },
|
|
788
|
+
inputTemplate: body.input,
|
|
789
|
+
createdAt: nowIso,
|
|
790
|
+
updatedAt: nowIso,
|
|
791
|
+
});
|
|
792
|
+
const ledger = platform.pipelineLedgerFor(orgId);
|
|
793
|
+
const runs = [];
|
|
794
|
+
for (const itemRef of itemRefs) {
|
|
795
|
+
const run = await ledger.createRun({
|
|
796
|
+
orgId,
|
|
797
|
+
agentIds: scope.agentIds,
|
|
798
|
+
pipelineId: resolved.pipelineId,
|
|
799
|
+
version: String(compiled.version ?? "1"),
|
|
800
|
+
planFingerprint,
|
|
801
|
+
capturedPlan: structuredClone(compiled),
|
|
802
|
+
trigger: "batch",
|
|
803
|
+
input: {
|
|
804
|
+
...(asRecord(body.input) ?? {}),
|
|
805
|
+
itemRef,
|
|
806
|
+
executionTarget: resolved.target,
|
|
807
|
+
},
|
|
808
|
+
batchId,
|
|
809
|
+
itemRef,
|
|
810
|
+
sourceType: "on-demand-batch",
|
|
811
|
+
priority: typeof body.priority === "number" ? body.priority : 0,
|
|
812
|
+
admission: { state: paused ? "held" : "ready" },
|
|
813
|
+
attemptBudget: settings.batchRunMaxAttempts,
|
|
814
|
+
});
|
|
815
|
+
runs.push(run);
|
|
816
|
+
}
|
|
817
|
+
await stores.batches.patch(orgId, batchId, { status: "running" });
|
|
818
|
+
return { scope, batch, runs, executionTarget: resolved.target };
|
|
819
|
+
});
|
|
820
|
+
app.post("/api/jobs/v1/batches/:batchId/recover", async (req) => {
|
|
821
|
+
const scope = resolveJobsScope(req);
|
|
822
|
+
const orgId = requireScopedOrg(scope, "batch recover");
|
|
823
|
+
requireConfirm(req.body, "batch recover");
|
|
824
|
+
const { batchId } = req.params;
|
|
825
|
+
const stores = platform.operationalStoresFor(orgId);
|
|
826
|
+
try {
|
|
827
|
+
const result = await recoverBatch({
|
|
828
|
+
orgId,
|
|
829
|
+
ledger: platform.pipelineLedgerFor(orgId),
|
|
830
|
+
batches: stores.batches,
|
|
831
|
+
queueSettings: stores.queueSettings,
|
|
832
|
+
}, batchId);
|
|
833
|
+
return { scope, ...result };
|
|
834
|
+
}
|
|
835
|
+
catch (err) {
|
|
836
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
837
|
+
if (/not found/i.test(message)) {
|
|
838
|
+
throw new ServiceError("NOT_FOUND", message, 404);
|
|
839
|
+
}
|
|
840
|
+
throw new ServiceError("VALIDATION", message, 400);
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
app.post("/api/jobs/v1/runs/:runId/cancel", async (req) => {
|
|
844
|
+
const scope = resolveJobsScope(req);
|
|
845
|
+
const orgId = requireScopedOrg(scope, "cancel run");
|
|
846
|
+
requireConfirm(req.body, "cancel run");
|
|
847
|
+
const { runId } = req.params;
|
|
848
|
+
const ledger = platform.pipelineLedgerFor(orgId);
|
|
849
|
+
const run = await ledger.getRun(orgId, runId);
|
|
850
|
+
if (!run || !agentMatches(run.agentIds, scope.agentIds)) {
|
|
851
|
+
throw new ServiceError("NOT_FOUND", `run ${runId} not found`, 404);
|
|
852
|
+
}
|
|
853
|
+
if (run.status === "succeeded" ||
|
|
854
|
+
run.status === "failed" ||
|
|
855
|
+
run.status === "cancelled") {
|
|
856
|
+
return { scope, run };
|
|
857
|
+
}
|
|
858
|
+
const updated = await ledger.updateRun(orgId, runId, {
|
|
859
|
+
status: "cancelled",
|
|
860
|
+
finishedAt: new Date().toISOString(),
|
|
861
|
+
error: { code: "RUN_CANCELLED", message: "run cancelled" },
|
|
862
|
+
});
|
|
863
|
+
return { scope, run: updated };
|
|
864
|
+
});
|
|
865
|
+
app.post("/api/jobs/v1/runs/:runId/retry", async (req) => {
|
|
866
|
+
const scope = resolveJobsScope(req);
|
|
867
|
+
const orgId = requireScopedOrg(scope, "retry run");
|
|
868
|
+
requireConfirm(req.body, "retry run");
|
|
869
|
+
const { runId } = req.params;
|
|
870
|
+
const stores = platform.operationalStoresFor(orgId);
|
|
871
|
+
try {
|
|
872
|
+
const run = await requeueRun({
|
|
873
|
+
orgId,
|
|
874
|
+
ledger: platform.pipelineLedgerFor(orgId),
|
|
875
|
+
queueSettings: stores.queueSettings,
|
|
876
|
+
}, runId);
|
|
877
|
+
return { scope, run };
|
|
878
|
+
}
|
|
879
|
+
catch (err) {
|
|
880
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
881
|
+
if (/not found/i.test(message)) {
|
|
882
|
+
throw new ServiceError("NOT_FOUND", message, 404);
|
|
883
|
+
}
|
|
884
|
+
throw new ServiceError("VALIDATION", message, 400);
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
//# sourceMappingURL=jobs.js.map
|