@sema-agent/server 7.115.0 → 7.116.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/MIGRATION.md +167 -4
- package/USAGE.md +6 -3
- package/dist/approval-ask-audit-store.d.ts +25 -6
- package/dist/approval-ask-audit-store.js +6 -1
- package/dist/approval-card.d.ts +8 -4
- package/dist/approval-card.js +2 -0
- package/dist/approval-crash-convergence.d.ts +60 -0
- package/dist/approval-crash-convergence.js +75 -0
- package/dist/approval-deny-reasons.d.ts +13 -0
- package/dist/approval-deny-reasons.js +2 -0
- package/dist/approval.js +2 -1
- package/dist/boot/coordinators.js +0 -12
- package/dist/boot/execution-env.js +2 -1
- package/dist/boot/resolve-spec.js +1 -0
- package/dist/boot/stage-07-capability-layer.js +21 -15
- package/dist/boot/stores.js +4 -4
- package/dist/capabilities/hands-lane.d.ts +0 -23
- package/dist/capabilities/hands-lane.js +3 -2
- package/dist/capabilities/send-user-file-tool.d.ts +10 -22
- package/dist/capabilities/send-user-file-tool.js +1 -8
- package/dist/capability-facts.d.ts +296 -0
- package/dist/capability-facts.js +354 -0
- package/dist/config-center/apply-effective.js +1 -1
- package/dist/config.d.ts +16 -13
- package/dist/config.js +12 -12
- package/dist/http/admission.d.ts +6 -0
- package/dist/http/admission.js +22 -5
- package/dist/http/principal-gate.d.ts +3 -0
- package/dist/http/principal-gate.js +3 -0
- package/dist/http/resume-legs.js +10 -1
- package/dist/http/retired-skill-manifest.d.ts +23 -0
- package/dist/http/retired-skill-manifest.js +22 -0
- package/dist/http/route-table.d.ts +3 -2
- package/dist/http/route-table.js +5 -5
- package/dist/http/routes/a2a-serve.js +14 -21
- package/dist/http/routes/admin-config-refresh.js +7 -2
- package/dist/http/routes/admin-drain.js +7 -2
- package/dist/http/routes/admin-memory-optout.js +20 -6
- package/dist/http/routes/adoption.js +19 -5
- package/dist/http/routes/agents-roster.js +4 -2
- package/dist/http/routes/approvals-assistant.js +41 -18
- package/dist/http/routes/background.d.ts +0 -2
- package/dist/http/routes/background.js +4 -5
- package/dist/http/routes/capabilities.d.ts +2 -1
- package/dist/http/routes/capabilities.js +62 -65
- package/dist/http/routes/config-catalog.d.ts +4 -5
- package/dist/http/routes/config-catalog.js +16 -2
- package/dist/http/routes/devices.js +17 -11
- package/dist/http/routes/diagnostics.js +6 -1
- package/dist/http/routes/fleet.js +4 -2
- package/dist/http/routes/images.js +30 -3
- package/dist/http/routes/leader.js +7 -1
- package/dist/http/routes/mcp-reconnect.js +17 -10
- package/dist/http/routes/memory-bundle.js +20 -6
- package/dist/http/routes/memory-compliance.js +19 -5
- package/dist/http/routes/memory-consolidation.d.ts +9 -12
- package/dist/http/routes/memory-consolidation.js +17 -3
- package/dist/http/routes/memory-origin.js +19 -5
- package/dist/http/routes/notify-wake.js +5 -3
- package/dist/http/routes/observability.js +19 -5
- package/dist/http/routes/retention-ops.js +6 -1
- package/dist/http/routes/rules.js +49 -21
- package/dist/http/routes/run-memory-capture.js +5 -2
- package/dist/http/routes/runs.js +90 -60
- package/dist/http/routes/session-sync.js +64 -53
- package/dist/http/routes/sessions-list.js +8 -5
- package/dist/http/routes/sessions.js +39 -24
- package/dist/http/routes/shared-memory.js +5 -3
- package/dist/http/routes/tasks.js +2 -1
- package/dist/http/routes/trace-usage.js +18 -11
- package/dist/http/routes/workflows.js +36 -13
- package/dist/http/server.js +4 -1
- package/dist/http/wire-types.d.ts +6 -0
- package/dist/namespaced-rule-name.d.ts +54 -0
- package/dist/namespaced-rule-name.js +33 -0
- package/dist/observability/corrupt-read-seat.d.ts +4 -3
- package/dist/observability/corrupt-read-seat.js +4 -3
- package/dist/observability/fail-open.d.ts +5 -1
- package/dist/observability/fail-open.js +5 -1
- package/dist/observability/permission-rule-events.d.ts +3 -0
- package/dist/observability/permission-rule-events.js +4 -0
- package/dist/plugins/file-run-store.d.ts +7 -1
- package/dist/plugins/file-run-store.js +20 -7
- package/dist/plugins/memory-run-store.js +2 -1
- package/dist/plugins/one-time-migrations.d.ts +4 -2
- package/dist/plugins/one-time-migrations.js +26 -3
- package/dist/plugins/pg-session-storage.js +1 -1
- package/dist/plugins/run-store-sql.js +4 -4
- package/dist/plugins/session-policy-store-sql.js +4 -2
- package/dist/plugins/store-backend.d.ts +6 -1
- package/dist/plugins/store-backend.js +2 -1
- package/dist/plugins/store-contracts.d.ts +27 -8
- package/dist/plugins/store-contracts.js +12 -3
- package/dist/plugins/tidb-session-store.js +1 -1
- package/dist/request-key-closure.js +1 -1
- package/dist/run-local.d.ts +37 -3
- package/dist/run-local.js +48 -5
- package/dist/runs.d.ts +0 -58
- package/dist/runs.js +15 -20
- package/dist/session-policy-wire.d.ts +40 -60
- package/dist/session-policy-wire.js +60 -32
- package/dist/spec-fields.d.ts +32 -0
- package/dist/spec-fields.js +31 -16
- package/dist/standing-answer-grant.d.ts +8 -0
- package/dist/standing-answer-grant.js +5 -0
- package/dist/task-settings.d.ts +13 -3
- package/dist/tool-approval.d.ts +16 -0
- package/dist/tool-approval.js +29 -7
- package/dist/trace/core-keyset-guard.d.ts +4 -4
- package/dist/trace/self-minted-wire.d.ts +42 -0
- package/dist/trace/self-minted-wire.js +13 -0
- package/dist/user-input-ledger.d.ts +8 -5
- package/package.json +4 -2
- package/scripts/drain-check-local.mjs +115 -25
- package/scripts/drain-check.mysql.sql +67 -0
- package/scripts/drain-check.pg.sql +71 -0
|
@@ -2,7 +2,8 @@ import { once } from "node:events";
|
|
|
2
2
|
import { randomBytes, createHash } from "node:crypto";
|
|
3
3
|
import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator, validateFileHistoryExport } from "@sema-agent/core";
|
|
4
4
|
import { StringDecoder } from "node:string_decoder";
|
|
5
|
-
import { exportSessionManifest, fileHistoryImportFace, replayFileHistory, replayPolicyRecords,
|
|
5
|
+
import { exportSessionManifest, fileHistoryImportFace, replayFileHistory, replayPolicyRecords, wipeSessionAttendants } from "../../session-sync.js";
|
|
6
|
+
import * as facts from "../../capability-facts.js";
|
|
6
7
|
import { classifySyncRelationshipByIds, SyncConflictError } from "../../session-sync-kernel.js";
|
|
7
8
|
import { isUuidShape } from "../../security.js";
|
|
8
9
|
import { sendJson, sendError, sendSessionError, sendSessionPolicyError, msg } from "../send.js";
|
|
@@ -36,16 +37,13 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
36
37
|
const { pendingStagings, importLeases, touchImportLease, cleanupStaging } = ctx.local.sessionSync;
|
|
37
38
|
{
|
|
38
39
|
{
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
41
|
-
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
const historyStore = deps.fileHistoryStore;
|
|
45
|
-
if (historyStore === undefined) {
|
|
46
|
-
sendError(res, 501, "capability.snapshot_store_required", "session sync is not available on this deployment: no file-history store is wired, so there is no file history to export or import");
|
|
40
|
+
const sync = facts.sessionSync(deps);
|
|
41
|
+
if (!sync.available) {
|
|
42
|
+
sendSessionSyncUnavailable(res, sync.why);
|
|
47
43
|
return;
|
|
48
44
|
}
|
|
45
|
+
const { backend, sessionStorage, historyStore } = sync;
|
|
46
|
+
const ownerOf = sessionStorage.ownerOf.bind(sessionStorage);
|
|
49
47
|
const scope = sessionOwnerScopeForWrite(req);
|
|
50
48
|
if (!scope.fleetWide && scope.gateOwner === null) {
|
|
51
49
|
sendUnauthorized(res, "identity");
|
|
@@ -81,7 +79,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
81
79
|
return;
|
|
82
80
|
let out;
|
|
83
81
|
try {
|
|
84
|
-
out = await exportSessionManifest(sessionId,
|
|
82
|
+
out = await exportSessionManifest(sessionId, backend, historyStore);
|
|
85
83
|
}
|
|
86
84
|
catch (e) {
|
|
87
85
|
if (sendSessionPolicyError(res, e))
|
|
@@ -108,12 +106,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
108
106
|
}
|
|
109
107
|
afterSeq = n;
|
|
110
108
|
}
|
|
111
|
-
const
|
|
112
|
-
if (!stream) {
|
|
113
|
-
sendError(res, 501, "capability.session_store_required", "session entry streaming requires a durable session store");
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const it = await stream(sessionId, afterSeq === undefined ? undefined : { afterSeq });
|
|
109
|
+
const it = await sessionStorage.exportEntriesStream(sessionId, afterSeq === undefined ? undefined : { afterSeq });
|
|
117
110
|
if (it === null) {
|
|
118
111
|
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
119
112
|
return;
|
|
@@ -138,6 +131,11 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
138
131
|
return;
|
|
139
132
|
}
|
|
140
133
|
if (match.decl.id === "session-sync-history-blob") {
|
|
134
|
+
const blobs = facts.sessionSyncFileHistory(deps);
|
|
135
|
+
if (!blobs.available) {
|
|
136
|
+
sendSessionSyncUnavailable(res, blobs.why);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
141
139
|
if (!pullGateOk())
|
|
142
140
|
return;
|
|
143
141
|
const hash = safeDecode(match.params.hash);
|
|
@@ -149,18 +147,14 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
149
147
|
sendError(res, 400, "request.id_invalid", "blob hash must be 64 lowercase-hex chars");
|
|
150
148
|
return;
|
|
151
149
|
}
|
|
152
|
-
const fs =
|
|
153
|
-
if (fs?.syncBlobFaces !== true) {
|
|
154
|
-
sendError(res, 404, "not_found.blob", "blob not found");
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
150
|
+
const fs = blobs.historyStore;
|
|
157
151
|
const envelope = typeof fs.exportHistory === "function" ? await fs.exportHistory(sessionId) : null;
|
|
158
152
|
const referenced = envelope !== null && envelope !== undefined && envelope.tracked.some((t) => t.versions.some((v) => v.blobHash === hash));
|
|
159
153
|
if (!referenced) {
|
|
160
154
|
sendError(res, 404, "not_found.blob", "blob not found");
|
|
161
155
|
return;
|
|
162
156
|
}
|
|
163
|
-
const bytes = await fs.getBlob
|
|
157
|
+
const bytes = await fs.getBlob(hash);
|
|
164
158
|
if (!bytes) {
|
|
165
159
|
sendError(res, 404, "not_found.blob", "blob not found");
|
|
166
160
|
return;
|
|
@@ -172,6 +166,11 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
172
166
|
if (match.decl.id === "session-sync-blob-put") {
|
|
173
167
|
if (rateLimited(req, res))
|
|
174
168
|
return;
|
|
169
|
+
const blobs = facts.sessionSyncFileHistory(deps);
|
|
170
|
+
if (!blobs.available) {
|
|
171
|
+
sendSessionSyncUnavailable(res, blobs.why);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
175
174
|
if (!pushGateOk())
|
|
176
175
|
return;
|
|
177
176
|
const hash = safeDecode(match.params.hash);
|
|
@@ -179,11 +178,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
179
178
|
sendError(res, 400, "request.id_invalid", "blob hash must be 64 lowercase-hex chars");
|
|
180
179
|
return;
|
|
181
180
|
}
|
|
182
|
-
const fs =
|
|
183
|
-
if (fs?.syncBlobFaces !== true || typeof fs.putBlob !== "function") {
|
|
184
|
-
sendError(res, 501, "capability.snapshot_store_required", "blob upload requires a durable file-history store");
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
181
|
+
const fs = blobs.historyStore;
|
|
187
182
|
const body = await readRawBody(req, SYNC_BLOB_MAX_BYTES);
|
|
188
183
|
if (deps.snapshotBlobSqlCapBytes !== undefined && body.byteLength > deps.snapshotBlobSqlCapBytes) {
|
|
189
184
|
sendError(res, 413, "blob_too_large_for_sql", `blob of ${body.byteLength} bytes exceeds this deployment's SQL snapshot-blob cap (${deps.snapshotBlobSqlCapBytes} bytes — TiDB's txn-entry-size-limit is the low wall at 6MiB by default; the mysql-protocol packet limit sits above it) — configure MinIO object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) for large snapshot blobs, or raise SNAPSHOT_BLOB_SQL_MAX_BYTES if your deployment lifted those limits`);
|
|
@@ -213,10 +208,6 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
213
208
|
}
|
|
214
209
|
}).catch(() => { });
|
|
215
210
|
}
|
|
216
|
-
if (!deps.sessionStorage?.beginImportStaging) {
|
|
217
|
-
sendError(res, 501, "capability.session_store_required", "staged session import requires a durable session store");
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
211
|
let body;
|
|
221
212
|
try {
|
|
222
213
|
body = (await readJson(req));
|
|
@@ -237,7 +228,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
237
228
|
if (policyRead.problem !== undefined) {
|
|
238
229
|
const legacy = legacyToolNameProblemOf(policyRead.problem);
|
|
239
230
|
if (legacy !== undefined) {
|
|
240
|
-
sendError(res, 400, "rules.legacy_tool_name", `policy: ${describeSessionRulesRecordProblem(policyRead.problem)}`, {
|
|
231
|
+
sendError(res, 400, "rules.legacy_tool_name", `policy: ${describeSessionRulesRecordProblem(policyRead.problem)}`, { ...legacy });
|
|
241
232
|
return;
|
|
242
233
|
}
|
|
243
234
|
sendError(res, 400, "request.field_invalid", `policy: ${describeSessionRulesRecordProblem(policyRead.problem)}`);
|
|
@@ -261,7 +252,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
261
252
|
const entryIds = body.entryIds;
|
|
262
253
|
const policy = policyRead.records;
|
|
263
254
|
const anchors = body.anchors;
|
|
264
|
-
const dstIds = (await
|
|
255
|
+
const dstIds = (await sessionStorage.listEntryIds?.(sessionId)) ?? null;
|
|
265
256
|
const relA = classifySyncRelationshipByIds(entryIds, dstIds);
|
|
266
257
|
if ((relA.relation === "fork" || relA.relation === "stale") && resolution !== "overwrite-dst") {
|
|
267
258
|
sendError(res, 409, "conflict", "sync conflict", { relation: relA });
|
|
@@ -270,7 +261,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
270
261
|
let payloadVerified = false;
|
|
271
262
|
let digestSaysDiffer = false;
|
|
272
263
|
if (relA.relation === "identical" && srcLogDigest !== undefined) {
|
|
273
|
-
const dstEntries = await
|
|
264
|
+
const dstEntries = await sessionStorage.exportEntries(sessionId).catch(() => null) ?? null;
|
|
274
265
|
const dstDigest = dstEntries === null ? undefined : sessionLogDigest(dstEntries);
|
|
275
266
|
if (sessionLogDigestsComparable(srcLogDigest, dstDigest)) {
|
|
276
267
|
if (srcLogDigest === dstDigest)
|
|
@@ -282,7 +273,8 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
282
273
|
digestSaysDiffer = true;
|
|
283
274
|
}
|
|
284
275
|
if (relA.relation === "identical" && !digestSaysDiffer) {
|
|
285
|
-
|
|
276
|
+
const blobFaces = facts.sessionSyncFileHistory(deps);
|
|
277
|
+
if (fileHistory !== null && blobFaces.available && fileHistoryImportFace(blobFaces.historyStore)) {
|
|
286
278
|
if (resolution === "overwrite-dst") {
|
|
287
279
|
const activeNow = await deps.runStore?.getActiveTaskId(sessionId);
|
|
288
280
|
if (activeNow) {
|
|
@@ -290,8 +282,8 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
290
282
|
return;
|
|
291
283
|
}
|
|
292
284
|
}
|
|
293
|
-
const getBlobA = (h) =>
|
|
294
|
-
const r = await replayFileHistory(
|
|
285
|
+
const getBlobA = (h) => blobFaces.historyStore.getBlob(h);
|
|
286
|
+
const r = await replayFileHistory(blobFaces.historyStore, sessionId, fileHistory, getBlobA, { allowReplace: resolution === "overwrite-dst" });
|
|
295
287
|
const failCode = r.ok ? undefined : r.error.code === "not_found" ? "missing_blob" : r.error.code;
|
|
296
288
|
if (!r.ok) {
|
|
297
289
|
sendError(res, 422, failCode, `file history import failed (${r.error.code})`);
|
|
@@ -343,7 +335,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
343
335
|
return;
|
|
344
336
|
}
|
|
345
337
|
}
|
|
346
|
-
const fss =
|
|
338
|
+
const fss = historyStore;
|
|
347
339
|
if (fileHistory !== null && fileHistoryImportFace(fss) !== undefined) {
|
|
348
340
|
const distinct = new Set();
|
|
349
341
|
for (const t of fileHistory.tracked)
|
|
@@ -351,7 +343,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
351
343
|
if (v.blobHash !== null)
|
|
352
344
|
distinct.add(v.blobHash);
|
|
353
345
|
const all = [...distinct];
|
|
354
|
-
if (typeof fss
|
|
346
|
+
if (typeof fss.hasBlobs === "function") {
|
|
355
347
|
const present = await fss.hasBlobs(all);
|
|
356
348
|
const missing = all.find((h) => !present.has(h));
|
|
357
349
|
if (missing !== undefined) {
|
|
@@ -359,7 +351,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
359
351
|
return;
|
|
360
352
|
}
|
|
361
353
|
}
|
|
362
|
-
else if (typeof fss
|
|
354
|
+
else if (typeof fss.getBlob === "function") {
|
|
363
355
|
for (const hash of all) {
|
|
364
356
|
if (!(await fss.getBlob(hash))) {
|
|
365
357
|
sendError(res, 422, "missing_blob", "missing blob; PUT all referenced blobs before import");
|
|
@@ -371,7 +363,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
371
363
|
const importingPrincipal = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
|
|
372
364
|
const operatorOk = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals);
|
|
373
365
|
const token = randomBytes(8).toString("hex");
|
|
374
|
-
const handle =
|
|
366
|
+
const handle = sessionStorage.beginImportStaging(sessionId, token);
|
|
375
367
|
const stagingId = handle.stagingId;
|
|
376
368
|
importLeases.set(sessionId, { at: Date.now(), stagingId });
|
|
377
369
|
pendingStagings.set(stagingId, {
|
|
@@ -396,7 +388,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
396
388
|
sendError(res, 404, "not_found.staging", "staging not found");
|
|
397
389
|
return;
|
|
398
390
|
}
|
|
399
|
-
const getBlob = (h) =>
|
|
391
|
+
const getBlob = (h) => historyStore.getBlob ? historyStore.getBlob(h) : Promise.resolve(undefined);
|
|
400
392
|
const validator = new StreamingImportValidator();
|
|
401
393
|
let lineBuf = "";
|
|
402
394
|
let pendingBatch = [];
|
|
@@ -481,19 +473,13 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
481
473
|
sendError(res, 409, "session_active", "session started an active run while the import streamed; sync after it settles", { activeTaskId: activeAtCommit });
|
|
482
474
|
return;
|
|
483
475
|
}
|
|
484
|
-
if (pending.resolution !== undefined && overwriteWipeIncapable(deps.sessionPolicyStore)) {
|
|
485
|
-
await pending.handle.abort().catch(() => undefined);
|
|
486
|
-
cleanupStaging(stagingId);
|
|
487
|
-
sendError(res, 501, "capability.policy_purge_required", "overwrite-dst cannot proceed: this deployment's session-policy store has no deleteBySession purge verb, so the destination's stale policy rows cannot be wiped and the import would land a MIXED state (the source's conversation plus the destination's leftover policy rows, which keep gating). Upgrade the policy store (every in-tree store carries deleteBySession since core 1.423), or re-run without resolution overwrite-dst");
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
476
|
try {
|
|
491
477
|
const committed = await pending.handle.commit(pending.owner, pending.resolution ? { resolution: pending.resolution } : undefined);
|
|
492
478
|
if (pending.resolution === "overwrite-dst" && (committed.relation === "fork" || committed.relation === "stale")) {
|
|
493
|
-
await wipeSessionAttendants({ fileHistoryStore:
|
|
479
|
+
await wipeSessionAttendants({ fileHistoryStore: historyStore, sessionPolicyStore: deps.sessionPolicyStore, resumeAnchorStore: deps.resumeAnchorStore }, sessionId);
|
|
494
480
|
}
|
|
495
|
-
if (pending.fileHistory !== null &&
|
|
496
|
-
const r = await replayFileHistory(
|
|
481
|
+
if (pending.fileHistory !== null && fileHistoryImportFace(historyStore)) {
|
|
482
|
+
const r = await replayFileHistory(historyStore, sessionId, pending.fileHistory, getBlob, { allowReplace: pending.resolution === "overwrite-dst" });
|
|
497
483
|
const failCode = r.ok ? undefined : r.error.code === "not_found" ? "missing_blob" : r.error.code;
|
|
498
484
|
if (!r.ok) {
|
|
499
485
|
sendError(res, 422, failCode, `file history import failed (${r.error.code})`);
|
|
@@ -508,7 +494,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
508
494
|
if (anchorStore)
|
|
509
495
|
for (const a of pending.anchors)
|
|
510
496
|
await anchorStore.put(sessionId, a.eventId, a.entryId, pending.owner);
|
|
511
|
-
|
|
497
|
+
sessionStorage.evict?.(sessionId);
|
|
512
498
|
cleanupStaging(stagingId);
|
|
513
499
|
sendJson(res, 200, { relation: committed.relation });
|
|
514
500
|
}
|
|
@@ -541,7 +527,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
541
527
|
sendError(res, 400, "request.field_invalid", "entryIds must be an array of strings");
|
|
542
528
|
return;
|
|
543
529
|
}
|
|
544
|
-
const dst = await
|
|
530
|
+
const dst = await sessionStorage.exportEntries(sessionId);
|
|
545
531
|
const relation = classifySyncRelationshipByIds(body.entryIds, dst ? dst.map((e) => e.id) : null);
|
|
546
532
|
sendJson(res, 200, relation);
|
|
547
533
|
return;
|
|
@@ -550,6 +536,31 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
550
536
|
}
|
|
551
537
|
}
|
|
552
538
|
}
|
|
539
|
+
function sendSessionSyncUnavailable(res, why) {
|
|
540
|
+
switch (why) {
|
|
541
|
+
case "store_backend_absent":
|
|
542
|
+
case "session_owner_face_absent":
|
|
543
|
+
sendError(res, 501, "capability.session_store_required", "session sync requires a durable session store");
|
|
544
|
+
return;
|
|
545
|
+
case "session_export_absent":
|
|
546
|
+
sendError(res, 501, "capability.session_store_required", "session entry streaming requires a durable session store");
|
|
547
|
+
return;
|
|
548
|
+
case "session_import_staging_absent":
|
|
549
|
+
sendError(res, 501, "capability.session_store_required", "staged session import requires a durable session store");
|
|
550
|
+
return;
|
|
551
|
+
case "file_history_store_absent":
|
|
552
|
+
sendError(res, 501, "capability.snapshot_store_required", "session sync is not available on this deployment: no file-history store is wired, so there is no file history to export or import");
|
|
553
|
+
return;
|
|
554
|
+
case "policy_purge_absent":
|
|
555
|
+
sendError(res, 501, "capability.policy_purge_required", "session sync is not available on this deployment: the session-policy store has no deleteBySession purge verb, so an overwrite-dst import could not wipe the destination's stale policy rows. Every in-tree policy store carries it since core 1.423 — upgrade the policy store");
|
|
556
|
+
return;
|
|
557
|
+
case "sync_blob_faces_absent":
|
|
558
|
+
sendError(res, 501, "capability.snapshot_store_required", "file-history blob transfer is not available on this deployment: its file-history store has no blob faces (SQL backends only; capabilities.sessionSyncFileHistory is false here). Sync the conversation without fileHistory");
|
|
559
|
+
return;
|
|
560
|
+
default:
|
|
561
|
+
return assertNever(why);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
553
564
|
export const SESSION_SYNC_ROUTES = [
|
|
554
565
|
{ id: "session-sync-manifest", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/sync\/manifest$/, label: "/v1/sessions/:id/sync/$verb", methods: ["GET"] },
|
|
555
566
|
{ id: "session-sync-entries", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/sync\/entries$/, label: "/v1/sessions/:id/sync/$verb", methods: ["GET"] },
|
|
@@ -4,6 +4,7 @@ import { sendUnauthorized } from "../principal-gate.js";
|
|
|
4
4
|
import { filterFingerprint } from "../cursor-fingerprint.js";
|
|
5
5
|
import { sessionTempoOfRunStatus } from "../../plugins/store-contracts.js";
|
|
6
6
|
import { assertNever } from "../route-ctx.js";
|
|
7
|
+
import * as facts from "../../capability-facts.js";
|
|
7
8
|
export async function handleSessionsList(req, res, match, ctx) {
|
|
8
9
|
const { deps } = ctx;
|
|
9
10
|
const { isFleetWide, sessionOwnerScope } = ctx.helpers;
|
|
@@ -14,11 +15,12 @@ export async function handleSessionsList(req, res, match, ctx) {
|
|
|
14
15
|
sendUnauthorized(res, "identity");
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
|
-
const
|
|
18
|
-
if (!
|
|
18
|
+
const listing = facts.sessionList(deps);
|
|
19
|
+
if (!listing.available) {
|
|
19
20
|
sendError(res, 501, "capability.session_store_required", "session list requires a durable session store (DB_BACKEND=mysql|pg|local)");
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
23
|
+
const lister = listing.lister;
|
|
22
24
|
const includeUnowned = !fleetWide && legacyUnownedAmnestyOpen(deps.config.requirePrincipal, gateOwner);
|
|
23
25
|
await handleSessionList(res, lister, new URLSearchParams(match.search), gateOwner, includeUnowned);
|
|
24
26
|
return;
|
|
@@ -29,7 +31,8 @@ export async function handleSessionsList(req, res, match, ctx) {
|
|
|
29
31
|
sendUnauthorized(res, "identity");
|
|
30
32
|
return;
|
|
31
33
|
}
|
|
32
|
-
|
|
34
|
+
const ledger = facts.sendFileLedger(deps);
|
|
35
|
+
if (!ledger.available) {
|
|
33
36
|
sendError(res, 501, "capability.store_required", "sendfile ledger requires a store backend (DB_BACKEND=mysql|pg|local) and the SendUserFile issuer configured");
|
|
34
37
|
return;
|
|
35
38
|
}
|
|
@@ -64,7 +67,7 @@ export async function handleSessionsList(req, res, match, ctx) {
|
|
|
64
67
|
scope = s;
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
|
-
const rows = await
|
|
70
|
+
const rows = await ledger.ledger.listByScope(scope, limit + 1, before);
|
|
68
71
|
const hasMore = rows.length > limit;
|
|
69
72
|
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
70
73
|
sendJson(res, 200, {
|
|
@@ -117,7 +120,7 @@ async function handleSessionList(res, lister, query, forceOwner, includeUnowned
|
|
|
117
120
|
sendError(res, 400, "request.cursor_filter_mismatch", "cursor was issued under a different filter (owner/q) — restart from the first page without a cursor");
|
|
118
121
|
return;
|
|
119
122
|
}
|
|
120
|
-
const rows = await lister({ ...(owner ? { owner } : {}), ...(includeUnowned ? { includeUnowned: true } : {}), ...(cursor ? { cursor } : {}), limit: limit + 1, ...(q ? { q } : {}) });
|
|
123
|
+
const rows = await lister({ ...(owner !== undefined ? { owner } : {}), ...(includeUnowned ? { includeUnowned: true } : {}), ...(cursor ? { cursor } : {}), limit: limit + 1, ...(q ? { q } : {}) });
|
|
121
124
|
const hasMore = rows.length > limit;
|
|
122
125
|
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
123
126
|
const last = page[page.length - 1];
|
|
@@ -10,6 +10,7 @@ import { readWritableSessionPermissionRules, describeSessionPermissionRulesProbl
|
|
|
10
10
|
import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } from "../send.js";
|
|
11
11
|
import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
|
|
12
12
|
import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
|
|
13
|
+
import * as facts from "../../capability-facts.js";
|
|
13
14
|
import { rosterAgentId } from "./agents-roster.js";
|
|
14
15
|
const MCP_STATUS_TTL_MS = 5_000;
|
|
15
16
|
const MCP_MATERIALIZE_TIMEOUT_MS = 10_000;
|
|
@@ -291,12 +292,13 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
291
292
|
}
|
|
292
293
|
{
|
|
293
294
|
if (match.decl.id === "session-events") {
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
if (!deps.sessionStorage?.getLeafId || !ownerOf || !watch) {
|
|
295
|
+
const evFact = facts.sessionEventStream(deps);
|
|
296
|
+
if (!evFact.available) {
|
|
297
297
|
sendError(res, 501, "capability.session_store_required", "session events require a durable session store (DB_BACKEND=mysql|pg|local)");
|
|
298
298
|
return;
|
|
299
299
|
}
|
|
300
|
+
const ownerOf = evFact.sessionStorage.ownerOf.bind(evFact.sessionStorage);
|
|
301
|
+
const watch = evFact.watch;
|
|
300
302
|
const evSession = safeDecode(match.params.sessionId);
|
|
301
303
|
if (evSession === null) {
|
|
302
304
|
sendError(res, 400, "request.path_malformed", "session id segment is not valid percent-encoding");
|
|
@@ -494,11 +496,12 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
494
496
|
sendPrincipalRefusal(res, req, deps.config);
|
|
495
497
|
return;
|
|
496
498
|
}
|
|
497
|
-
|
|
499
|
+
const auditFact = facts.sessionAuditView(deps);
|
|
500
|
+
if (!auditFact.available) {
|
|
498
501
|
sendError(res, 501, "capability.memory_engine_required", "memory provenance and erasure are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/tidb memory backends do not (their control plane would land on a replica's local disk)");
|
|
499
502
|
return;
|
|
500
503
|
}
|
|
501
|
-
if ((await readOwnedSessionView(
|
|
504
|
+
if ((await readOwnedSessionView(auditFact.sessionAudit, sessionId)) === undefined)
|
|
502
505
|
return;
|
|
503
506
|
const faces = deps.memoryCompliance;
|
|
504
507
|
if (faces === undefined) {
|
|
@@ -546,11 +549,12 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
546
549
|
sendPrincipalRefusal(res, req, deps.config);
|
|
547
550
|
return;
|
|
548
551
|
}
|
|
549
|
-
|
|
552
|
+
const auditFact = facts.sessionAuditView(deps);
|
|
553
|
+
if (!auditFact.available) {
|
|
550
554
|
sendError(res, 501, "capability.memory_engine_required", "session memory status is not available on this deployment — it requires a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the file memory backend does, the pg/tidb memory backends do not (their control plane would land on a replica's local disk)");
|
|
551
555
|
return;
|
|
552
556
|
}
|
|
553
|
-
if ((await readOwnedSessionView(
|
|
557
|
+
if ((await readOwnedSessionView(auditFact.sessionAudit, sessionId)) === undefined)
|
|
554
558
|
return;
|
|
555
559
|
const face = deps.sessionMemoryStatus;
|
|
556
560
|
if (face === undefined) {
|
|
@@ -590,11 +594,12 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
590
594
|
}
|
|
591
595
|
if (match.decl.id === "session-detail") {
|
|
592
596
|
{
|
|
593
|
-
|
|
594
|
-
|
|
597
|
+
const auditFact = facts.sessionAuditView(deps);
|
|
598
|
+
if (!auditFact.available) {
|
|
599
|
+
sendError(res, 501, "capability.session_store_required", "the session audit view requires a durable session store (DB_BACKEND=mysql|pg|local)");
|
|
595
600
|
return;
|
|
596
601
|
}
|
|
597
|
-
const audit = await readOwnedSessionView(
|
|
602
|
+
const audit = await readOwnedSessionView(auditFact.sessionAudit, match.params.sessionId);
|
|
598
603
|
if (audit === undefined)
|
|
599
604
|
return;
|
|
600
605
|
const q = new URLSearchParams(match.search);
|
|
@@ -647,12 +652,13 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
647
652
|
}
|
|
648
653
|
{
|
|
649
654
|
if (match.decl.id === "session-fork") {
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
if (!fork || !ownerOf) {
|
|
655
|
+
const forkFact = facts.sessionFork(deps);
|
|
656
|
+
if (!forkFact.available) {
|
|
653
657
|
sendError(res, 501, "capability.session_store_required", "session fork requires a durable session store (DB_BACKEND=mysql|pg|local)");
|
|
654
658
|
return;
|
|
655
659
|
}
|
|
660
|
+
const fork = forkFact.sessionStorage.fork.bind(forkFact.sessionStorage);
|
|
661
|
+
const ownerOf = forkFact.sessionStorage.ownerOf.bind(forkFact.sessionStorage);
|
|
656
662
|
const scope = sessionOwnerScopeForWrite(req);
|
|
657
663
|
if (!scope.fleetWide && scope.gateOwner === null) {
|
|
658
664
|
sendUnauthorized(res, "identity");
|
|
@@ -684,12 +690,13 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
684
690
|
}
|
|
685
691
|
{
|
|
686
692
|
if (match.decl.id === "session-delete") {
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
if (!purge || !ownerOf) {
|
|
693
|
+
const deleteFact = facts.sessionDelete(deps);
|
|
694
|
+
if (!deleteFact.available) {
|
|
690
695
|
sendError(res, 501, "capability.session_store_required", "session delete requires a durable session store (DB_BACKEND=mysql|pg|local)");
|
|
691
696
|
return;
|
|
692
697
|
}
|
|
698
|
+
const purge = deleteFact.purge;
|
|
699
|
+
const ownerOf = deleteFact.sessionStorage.ownerOf.bind(deleteFact.sessionStorage);
|
|
693
700
|
const scope = sessionOwnerScopeForWrite(req);
|
|
694
701
|
if (!scope.fleetWide && scope.gateOwner === null) {
|
|
695
702
|
sendUnauthorized(res, "identity");
|
|
@@ -729,12 +736,13 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
729
736
|
}
|
|
730
737
|
{
|
|
731
738
|
if (match.decl.id === "session-policy-get" || match.decl.id === "session-policy-put") {
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
if (!store || !ownerOf) {
|
|
739
|
+
const policyFact = facts.sessionPolicy(deps);
|
|
740
|
+
if (!policyFact.available) {
|
|
735
741
|
sendError(res, 501, "capability.session_store_required", "session policy requires a session-store backend");
|
|
736
742
|
return;
|
|
737
743
|
}
|
|
744
|
+
const store = policyFact.policyStore;
|
|
745
|
+
const ownerOf = policyFact.sessionStorage.ownerOf.bind(policyFact.sessionStorage);
|
|
738
746
|
const principal = gatedPrincipal(req, deps.config);
|
|
739
747
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
740
748
|
sendUnauthorized(res, "identity");
|
|
@@ -789,7 +797,10 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
789
797
|
sendError(res, 400, "request.field_invalid", describeSessionPermissionRulesProblem(read.problem));
|
|
790
798
|
return;
|
|
791
799
|
case "legacy-tool-name":
|
|
792
|
-
sendError(res, 400, "rules.legacy_tool_name", describeSessionPermissionRulesProblem(read.problem), {
|
|
800
|
+
sendError(res, 400, "rules.legacy_tool_name", describeSessionPermissionRulesProblem(read.problem), { ...read.problem.refusal });
|
|
801
|
+
return;
|
|
802
|
+
case "unreachable-tool-name":
|
|
803
|
+
sendError(res, 400, "request.field_invalid", describeSessionPermissionRulesProblem(read.problem));
|
|
793
804
|
return;
|
|
794
805
|
default: {
|
|
795
806
|
const unhandled = read.problem;
|
|
@@ -814,10 +825,14 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
814
825
|
return;
|
|
815
826
|
}
|
|
816
827
|
}
|
|
817
|
-
{
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
828
|
+
if (match.decl.id === "session-workspace") {
|
|
829
|
+
const workspace = facts.workspaceBrowse();
|
|
830
|
+
switch (workspace.why) {
|
|
831
|
+
case "workspace_retired":
|
|
832
|
+
sendError(res, 501, "capability.workspace_retired", "workspace browse is retired: the whole-tree snapshot epoch it projected was replaced by per-edited-file rewind history (which holds only files the agent edited, not the workspace). There is no whole-tree manifest to browse anymore.");
|
|
833
|
+
return;
|
|
834
|
+
default:
|
|
835
|
+
return assertNever(workspace.why);
|
|
821
836
|
}
|
|
822
837
|
}
|
|
823
838
|
return assertNever(match.decl.id);
|
|
@@ -2,6 +2,7 @@ import { SHARED_MEMORY_READ_CAP_BYTES, SharedMemoryStoreError } from "@sema-agen
|
|
|
2
2
|
import { sendJson, sendError } from "../send.js";
|
|
3
3
|
import { gatedPrincipal, sendUnauthorized } from "../principal-gate.js";
|
|
4
4
|
import { assertNever } from "../route-ctx.js";
|
|
5
|
+
import * as facts from "../../capability-facts.js";
|
|
5
6
|
const RETRY_AFTER_SEC = 10;
|
|
6
7
|
const HTTP_LIST_LIMIT = 100;
|
|
7
8
|
const FORBIDDEN_PATH_CHARS = /[\p{Cc}\p{Cf}\p{Co}\p{Cn}\p{Default_Ignorable_Code_Point}\u2028\u2029\\]/u;
|
|
@@ -32,11 +33,12 @@ async function bindSnapshot(req, res, ctx) {
|
|
|
32
33
|
sendUnauthorized(res, "identity");
|
|
33
34
|
return undefined;
|
|
34
35
|
}
|
|
35
|
-
const
|
|
36
|
-
if (!
|
|
37
|
-
|
|
36
|
+
const fact = facts.sharedMemoryRead(deps);
|
|
37
|
+
if (!fact.available) {
|
|
38
|
+
sendError(res, 501, "capability.shared_memory_required", "org shared memory is not available on this deployment (it needs both a shared memory store provider and an org membership directory)");
|
|
38
39
|
return undefined;
|
|
39
40
|
}
|
|
41
|
+
const provider = fact.provider;
|
|
40
42
|
const snapshot = await provider.snapshot({ sessionId: `http:${principal ?? "anonymous"}`, taskId: "http-read", ...(principal ? { principal } : {}) }, {});
|
|
41
43
|
if (snapshot.state === "unavailable") {
|
|
42
44
|
refuseUnavailable(res, snapshot.message);
|
|
@@ -27,6 +27,7 @@ import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate
|
|
|
27
27
|
import { failOpenTagForDroppedFrame, recordFailOpen } from "../../observability/fail-open.js";
|
|
28
28
|
import { buildSyncSubmitBody } from "../sync-submit-body.js";
|
|
29
29
|
import { assertNever } from "../route-ctx.js";
|
|
30
|
+
import * as facts from "../../capability-facts.js";
|
|
30
31
|
import { buildCheckpointCtx } from "../checkpoint-ctx.js";
|
|
31
32
|
import { withModelReachScope } from "../../model-reach-scope.js";
|
|
32
33
|
import { createInstructionSelectionLeg } from "../../instruction-selection.js";
|
|
@@ -123,7 +124,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
123
124
|
sendError(res, 400, "request.field_conflict", "verify / cascade are not supported on /v1/tasks/stream (both are multi-attempt, not a single stream) — use /v1/tasks or /v1/runs");
|
|
124
125
|
return;
|
|
125
126
|
}
|
|
126
|
-
const earlyDurableTid = deps.
|
|
127
|
+
const earlyDurableTid = facts.durableRuns(deps).available && prepared.spec.sessionId ? uuidv7() : undefined;
|
|
127
128
|
const streamLegStartedAt = Date.now();
|
|
128
129
|
const detachOnDisconnect = headerStr(req.headers["x-detach-on-disconnect"]) === "true";
|
|
129
130
|
if (detachOnDisconnect && earlyDurableTid === undefined) {
|
|
@@ -8,6 +8,7 @@ import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_
|
|
|
8
8
|
import { filterFingerprint } from "../cursor-fingerprint.js";
|
|
9
9
|
import { gatedPrincipal, sendUnauthorized } from "../principal-gate.js";
|
|
10
10
|
import { assertNever } from "../route-ctx.js";
|
|
11
|
+
import * as facts from "../../capability-facts.js";
|
|
11
12
|
const TOOL_RESULT_LIMIT_CAP = 262_144;
|
|
12
13
|
const TOOL_RESULT_OFFSET_CAP = 2_147_483_646;
|
|
13
14
|
export async function handleTraceUsage(req, res, match, ctx) {
|
|
@@ -18,13 +19,15 @@ export async function handleTraceUsage(req, res, match, ctx) {
|
|
|
18
19
|
sendUnauthorized(res, "operator");
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
+
const runs = facts.durableRuns(deps);
|
|
23
|
+
if (!runs.available) {
|
|
22
24
|
sendError(res, 501, "capability.run_store_required", "trace API requires a durable run store (DB_BACKEND=mysql|pg|local)");
|
|
23
25
|
return;
|
|
24
26
|
}
|
|
27
|
+
const runStore = runs.runStore;
|
|
25
28
|
const q = new URLSearchParams(match.search);
|
|
26
29
|
const sinceSec = Math.min(30 * 86_400, Math.max(60, Number(q.get("sinceSec") ?? 86_400) || 86_400));
|
|
27
|
-
sendJson(res, 200, { sinceSec, rows: await
|
|
30
|
+
sendJson(res, 200, { sinceSec, rows: await runStore.sourceSummary(Date.now() - sinceSec * 1000) });
|
|
28
31
|
return;
|
|
29
32
|
}
|
|
30
33
|
if (match.decl.id === "task-trace-list" ||
|
|
@@ -38,15 +41,17 @@ export async function handleTraceUsage(req, res, match, ctx) {
|
|
|
38
41
|
sendUnauthorized(res, "identity");
|
|
39
42
|
return;
|
|
40
43
|
}
|
|
41
|
-
|
|
44
|
+
const runs = facts.durableRuns(deps);
|
|
45
|
+
if (!runs.available) {
|
|
42
46
|
sendError(res, 501, "capability.run_store_required", "trace API requires a durable run store (DB_BACKEND=mysql|pg|local)");
|
|
43
47
|
return;
|
|
44
48
|
}
|
|
49
|
+
const runStore = runs.runStore;
|
|
45
50
|
const query = new URLSearchParams(match.search);
|
|
46
51
|
const taskId = match.params.taskId;
|
|
47
52
|
let ownedRun;
|
|
48
53
|
if (taskId && gateOwner !== null) {
|
|
49
|
-
const run = await
|
|
54
|
+
const run = await runStore.getRun(taskId);
|
|
50
55
|
if (!run || (run.owner !== null && run.owner !== gateOwner)) {
|
|
51
56
|
sendError(res, 404, "not_found.run", "task not found");
|
|
52
57
|
return;
|
|
@@ -55,19 +60,19 @@ export async function handleTraceUsage(req, res, match, ctx) {
|
|
|
55
60
|
}
|
|
56
61
|
switch (match.decl.id) {
|
|
57
62
|
case "task-trace-list":
|
|
58
|
-
await handleTaskList(res,
|
|
63
|
+
await handleTaskList(res, runStore, query, gateOwner);
|
|
59
64
|
return;
|
|
60
65
|
case "task-trace-turns":
|
|
61
|
-
await handleTaskTurns(res,
|
|
66
|
+
await handleTaskTurns(res, runStore, taskId, query);
|
|
62
67
|
return;
|
|
63
68
|
case "task-trace-stream":
|
|
64
|
-
await streamTaskTrace(req, res,
|
|
69
|
+
await streamTaskTrace(req, res, runStore, taskId, deps.config.runStaleSec * 1000, match.search);
|
|
65
70
|
return;
|
|
66
71
|
case "task-trace-artifacts":
|
|
67
|
-
await handleTaskArtifacts(res,
|
|
72
|
+
await handleTaskArtifacts(res, runStore, taskId);
|
|
68
73
|
return;
|
|
69
74
|
case "task-tool-result":
|
|
70
|
-
await handleToolResult(res, deps.toolResultStore,
|
|
75
|
+
await handleToolResult(res, deps.toolResultStore, runStore, taskId, ownedRun, safeDecode(match.params.ref), query);
|
|
71
76
|
return;
|
|
72
77
|
default:
|
|
73
78
|
return assertNever(match.decl.id);
|
|
@@ -78,10 +83,12 @@ export async function handleTraceUsage(req, res, match, ctx) {
|
|
|
78
83
|
{
|
|
79
84
|
const sub = match.params.sub;
|
|
80
85
|
{
|
|
81
|
-
|
|
86
|
+
const runs = facts.durableRuns(deps);
|
|
87
|
+
if (!runs.available) {
|
|
82
88
|
sendError(res, 501, "capability.run_store_required", "usage analytics requires a run store");
|
|
83
89
|
return;
|
|
84
90
|
}
|
|
91
|
+
const runStore = runs.runStore;
|
|
85
92
|
const fleetWide = isFleetWide(req);
|
|
86
93
|
const jwtOwner = fleetWide ? undefined : gatedPrincipal(req, deps.config);
|
|
87
94
|
if (!fleetWide && !jwtOwner) {
|
|
@@ -104,7 +111,7 @@ export async function handleTraceUsage(req, res, match, ctx) {
|
|
|
104
111
|
}
|
|
105
112
|
const owner = jwtOwner ?? (q.get("principal") ?? undefined);
|
|
106
113
|
try {
|
|
107
|
-
const scan = await
|
|
114
|
+
const scan = await runStore.usageScan(from, to, owner !== undefined ? { owner } : undefined);
|
|
108
115
|
const base = { from: new Date(from).toISOString(), to: new Date(to).toISOString(), ...(scan.truncated ? { truncated: true } : {}) };
|
|
109
116
|
if (sub === "summary") {
|
|
110
117
|
sendJson(res, 200, { ...base, ...usageSummary(scan.rows) });
|