@sema-agent/server 1.298.0 → 1.299.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/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/http/server.js +198 -0
- package/dist/http/tar.d.ts +8 -0
- package/dist/http/tar.js +49 -0
- package/dist/plugins/blob-backend.d.ts +1 -0
- package/dist/plugins/blob-backend.js +13 -0
- package/dist/plugins/pg-file-snapshot-store.d.ts +1 -0
- package/dist/plugins/pg-file-snapshot-store.js +4 -1
- package/dist/plugins/tidb-file-snapshot-store.d.ts +1 -0
- package/dist/plugins/tidb-file-snapshot-store.js +4 -1
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -538,6 +538,7 @@ export function loadConfig() {
|
|
|
538
538
|
: undefined,
|
|
539
539
|
snapshotBlobSqlMaxBytes: optFinitePositiveEnv("SNAPSHOT_BLOB_SQL_MAX_BYTES"),
|
|
540
540
|
snapshotBlobAllowSql: process.env.SNAPSHOT_BLOB_ALLOW_SQL_BYTES === "true",
|
|
541
|
+
workspaceFileMaxBytes: optFinitePositiveEnv("WORKSPACE_FILE_MAX_BYTES") ?? 8 * 1024 * 1024,
|
|
541
542
|
sendUserFile: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT) && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
|
|
542
543
|
? {
|
|
543
544
|
endpoint: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT),
|
package/dist/http/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { once } from "node:events";
|
|
|
3
3
|
import { timingSafeEqual, createHash, randomBytes } from "node:crypto";
|
|
4
4
|
import { sessionLogDigest, sessionLogDigestsComparable, uuidv7, sanitizePathComponent, callKeyOrdinal, isThinkingLevel, DEFAULT_EFFORT_LEVELS, expandTiers, materializeMcpTools, runWithVerification, resumeWithVerification, runCascade, CheckpointError, mintCheckpointToken, SessionPolicyError, SessionError, HAND_TOOL_EFFECTS, canonicalToolName, formatUserScope, defaultTaskRegistry, validatePendingSteer, StreamingImportValidator, getWorkflowRun, subscribeWorkflow, deriveAgentDisplayStatus } from "@sema-agent/core";
|
|
5
5
|
import { decideParkedAgent, findParkedAgentForCheckpoint } from "../parked-decide.js";
|
|
6
|
+
import { tarHeader, tarPadding, splitTarPath, TAR_END } from "./tar.js";
|
|
6
7
|
import { matchCatalogModel } from "../model-select.js";
|
|
7
8
|
import { mcpForScenario } from "../sema-registry.js";
|
|
8
9
|
import { HttpError, principalFrom, verifiedPrincipal, setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isUuidV7, isUuidShape, verifyDirectDoorProof, MAX_APPROVAL_REASON_CHARS } from "../security.js";
|
|
@@ -671,6 +672,12 @@ export function createHttpServer(deps) {
|
|
|
671
672
|
fleet: deps.fleetBus
|
|
672
673
|
? { stream: true, sessionScope: true, observe: true, resume: "snapshot", bgNotifyFailClosed: true, steerPriority: "advisory" }
|
|
673
674
|
: false,
|
|
675
|
+
workspace: (() => {
|
|
676
|
+
const wfs = deps.fileSnapshotStore;
|
|
677
|
+
return wfs && typeof wfs.listKeys === "function" && typeof wfs.exportManifest === "function" && typeof wfs.getBlob === "function" && Boolean(deps.sessionStorage?.ownerOf)
|
|
678
|
+
? { browse: true, archive: true, maxFileBytes: deps.config.workspaceFileMaxBytes ?? 8 * 1024 * 1024 }
|
|
679
|
+
: false;
|
|
680
|
+
})(),
|
|
674
681
|
leader: Boolean(deps.leaderEndpoint),
|
|
675
682
|
memory: false,
|
|
676
683
|
memoryWrite: false,
|
|
@@ -3932,6 +3939,180 @@ export function createHttpServer(deps) {
|
|
|
3932
3939
|
return;
|
|
3933
3940
|
}
|
|
3934
3941
|
}
|
|
3942
|
+
{
|
|
3943
|
+
const wm = req.method === "GET" ? /^\/v1\/sessions\/([^/]+)\/workspace(?:\/([^/]+)(?:\/(tree|file|archive))?)?$/.exec(url) : null;
|
|
3944
|
+
if (wm) {
|
|
3945
|
+
const fs = deps.fileSnapshotStore;
|
|
3946
|
+
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
3947
|
+
if (!fs || typeof fs.listKeys !== "function" || typeof fs.exportManifest !== "function" || typeof fs.getBlob !== "function" || !ownerOf) {
|
|
3948
|
+
sendJson(res, 501, { error: "workspace browse requires a durable snapshot store (rewindFiles wiring) and a durable session store" });
|
|
3949
|
+
return;
|
|
3950
|
+
}
|
|
3951
|
+
const scope = sessionOwnerScope(req);
|
|
3952
|
+
if (!scope.fleetWide && scope.gateOwner === null) {
|
|
3953
|
+
sendJson(res, 401, { error: "unauthorized" });
|
|
3954
|
+
return;
|
|
3955
|
+
}
|
|
3956
|
+
const wsSession = safeDecode(wm[1]);
|
|
3957
|
+
if (wsSession === null || !isUuidShape(wsSession)) {
|
|
3958
|
+
sendJson(res, 400, { error: "invalid session id (must be a canonical uuid)" });
|
|
3959
|
+
return;
|
|
3960
|
+
}
|
|
3961
|
+
const wsOwner = await ownerOf(wsSession);
|
|
3962
|
+
if (wsOwner === undefined || (!scope.fleetWide && wsOwner !== scope.gateOwner)) {
|
|
3963
|
+
sendJson(res, 404, { error: "session not found" });
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
const q = new URL(req.url ?? "", "http://x").searchParams;
|
|
3967
|
+
const keys = (await fs.listKeys(wsSession)).sort().reverse();
|
|
3968
|
+
const sub = wm[3];
|
|
3969
|
+
if (wm[2] === undefined) {
|
|
3970
|
+
const limRaw = q.get("limit");
|
|
3971
|
+
if (limRaw !== null && !(/^[0-9]+$/.test(limRaw) && Number(limRaw) >= 1 && Number(limRaw) <= 100)) {
|
|
3972
|
+
sendJson(res, 400, { error: "limit must be a decimal integer in 1..100" });
|
|
3973
|
+
return;
|
|
3974
|
+
}
|
|
3975
|
+
const lim = limRaw === null ? 20 : Number(limRaw);
|
|
3976
|
+
const page = keys.slice(0, lim);
|
|
3977
|
+
const snapshots = [];
|
|
3978
|
+
for (const k of page) {
|
|
3979
|
+
const m = await fs.exportManifest(wsSession, k);
|
|
3980
|
+
if (m === null)
|
|
3981
|
+
continue;
|
|
3982
|
+
let bytes;
|
|
3983
|
+
if (typeof fs.blobSizes === "function") {
|
|
3984
|
+
const sizes = await fs.blobSizes([...new Set(m.values())]);
|
|
3985
|
+
bytes = 0;
|
|
3986
|
+
for (const h of m.values())
|
|
3987
|
+
bytes += sizes.get(h) ?? 0;
|
|
3988
|
+
}
|
|
3989
|
+
snapshots.push({ key: k, files: m.size, ...(bytes !== undefined ? { bytes } : {}) });
|
|
3990
|
+
}
|
|
3991
|
+
sendJson(res, 200, { sessionId: wsSession, snapshots, ...(keys.length > 0 ? { latest: keys[0] } : {}), total: keys.length });
|
|
3992
|
+
return;
|
|
3993
|
+
}
|
|
3994
|
+
const keyRaw = safeDecode(wm[2]);
|
|
3995
|
+
if (keyRaw === null) {
|
|
3996
|
+
sendJson(res, 400, { error: "malformed snapshot key" });
|
|
3997
|
+
return;
|
|
3998
|
+
}
|
|
3999
|
+
const key = keyRaw === "latest" ? keys[0] : keyRaw;
|
|
4000
|
+
const manifest = key !== undefined ? await fs.exportManifest(wsSession, key) : null;
|
|
4001
|
+
if (key === undefined || manifest === null) {
|
|
4002
|
+
sendJson(res, 404, { error: "snapshot not found" });
|
|
4003
|
+
return;
|
|
4004
|
+
}
|
|
4005
|
+
if (sub === "tree") {
|
|
4006
|
+
const limRaw = q.get("limit");
|
|
4007
|
+
if (limRaw !== null && !(/^[0-9]+$/.test(limRaw) && Number(limRaw) >= 1 && Number(limRaw) <= 2000)) {
|
|
4008
|
+
sendJson(res, 400, { error: "limit must be a decimal integer in 1..2000" });
|
|
4009
|
+
return;
|
|
4010
|
+
}
|
|
4011
|
+
const offRaw = q.get("offset");
|
|
4012
|
+
if (offRaw !== null && !/^[0-9]+$/.test(offRaw)) {
|
|
4013
|
+
sendJson(res, 400, { error: "offset must be a non-negative integer" });
|
|
4014
|
+
return;
|
|
4015
|
+
}
|
|
4016
|
+
const lim = limRaw === null ? 500 : Number(limRaw);
|
|
4017
|
+
const off = offRaw === null ? 0 : Number(offRaw);
|
|
4018
|
+
const all = [...manifest.entries()].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
4019
|
+
const page = all.slice(off, off + lim);
|
|
4020
|
+
const sizes = typeof fs.blobSizes === "function" ? await fs.blobSizes([...new Set(page.map(([, h]) => h))]) : undefined;
|
|
4021
|
+
sendJson(res, 200, {
|
|
4022
|
+
sessionId: wsSession,
|
|
4023
|
+
key,
|
|
4024
|
+
entries: page.map(([path, hash]) => ({ path, hash, ...(sizes?.get(hash) !== undefined ? { size: sizes.get(hash) } : {}) })),
|
|
4025
|
+
total: all.length,
|
|
4026
|
+
...(off + lim < all.length ? { nextOffset: off + lim } : {}),
|
|
4027
|
+
});
|
|
4028
|
+
return;
|
|
4029
|
+
}
|
|
4030
|
+
if (sub === "file") {
|
|
4031
|
+
const pathParam = q.get("path");
|
|
4032
|
+
if (!pathParam) {
|
|
4033
|
+
sendJson(res, 400, { error: "missing ?path=<relPath from the tree>" });
|
|
4034
|
+
return;
|
|
4035
|
+
}
|
|
4036
|
+
const hash = manifest.get(pathParam);
|
|
4037
|
+
if (hash === undefined) {
|
|
4038
|
+
sendJson(res, 404, { error: "no such file in this snapshot (paths are the tree's relPaths, verbatim)" });
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
4041
|
+
const cap = deps.config.workspaceFileMaxBytes ?? 8 * 1024 * 1024;
|
|
4042
|
+
if (typeof fs.blobSizes === "function") {
|
|
4043
|
+
const known = (await fs.blobSizes([hash])).get(hash);
|
|
4044
|
+
if (known !== undefined && known > cap) {
|
|
4045
|
+
sendJson(res, 413, { error: "file exceeds the single-file read limit — download it via the archive endpoint", code: "workspace_file_too_large", sizeBytes: known, limit: cap });
|
|
4046
|
+
return;
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
const bytes = await fs.getBlob(hash);
|
|
4050
|
+
if (bytes === undefined) {
|
|
4051
|
+
sendJson(res, 404, { error: "file content no longer available (snapshot raced a reap)" });
|
|
4052
|
+
return;
|
|
4053
|
+
}
|
|
4054
|
+
if (bytes.byteLength > cap) {
|
|
4055
|
+
sendJson(res, 413, { error: "file exceeds the single-file read limit — download it via the archive endpoint", code: "workspace_file_too_large", sizeBytes: bytes.byteLength, limit: cap });
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
4058
|
+
const ct = workspaceContentType(pathParam);
|
|
4059
|
+
const bin = looksBinary(bytes);
|
|
4060
|
+
res.writeHead(200, {
|
|
4061
|
+
"content-type": ct,
|
|
4062
|
+
"content-length": bytes.byteLength,
|
|
4063
|
+
"x-content-type-options": "nosniff",
|
|
4064
|
+
"x-sema-content-binary": bin ? "true" : "false",
|
|
4065
|
+
});
|
|
4066
|
+
res.end(Buffer.from(bytes));
|
|
4067
|
+
return;
|
|
4068
|
+
}
|
|
4069
|
+
if (sub === "archive") {
|
|
4070
|
+
const entries = [...manifest.entries()].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
4071
|
+
const bad = entries.filter(([p]) => splitTarPath(p) === null).map(([p]) => p);
|
|
4072
|
+
if (bad.length > 0) {
|
|
4073
|
+
sendJson(res, 422, { error: `snapshot contains ${bad.length} path(s) not representable in ustar (>255 bytes or unsplittable)`, paths: bad.slice(0, 5) });
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4076
|
+
if (typeof fs.hasBlobs === "function") {
|
|
4077
|
+
const present = await fs.hasBlobs([...new Set(entries.map(([, h]) => h))]);
|
|
4078
|
+
const missing = entries.filter(([, h]) => !present.has(h)).length;
|
|
4079
|
+
if (missing > 0) {
|
|
4080
|
+
sendJson(res, 502, { error: `snapshot is missing ${missing} blob(s) (raced a reap or a partial import) — retry, or pick another snapshot` });
|
|
4081
|
+
return;
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
res.writeHead(200, {
|
|
4085
|
+
"content-type": "application/x-tar",
|
|
4086
|
+
"content-disposition": `attachment; filename="workspace-${key}.tar"`,
|
|
4087
|
+
"x-content-type-options": "nosniff",
|
|
4088
|
+
});
|
|
4089
|
+
const write = async (buf) => {
|
|
4090
|
+
if (res.writableEnded || res.destroyed)
|
|
4091
|
+
throw new Error("client gone");
|
|
4092
|
+
if (!res.write(buf))
|
|
4093
|
+
await new Promise((r) => res.once("drain", r));
|
|
4094
|
+
};
|
|
4095
|
+
try {
|
|
4096
|
+
for (const [p, h] of entries) {
|
|
4097
|
+
const bytes = await fs.getBlob(h);
|
|
4098
|
+
if (bytes === undefined)
|
|
4099
|
+
throw new Error(`blob vanished mid-stream: ${h}`);
|
|
4100
|
+
await write(tarHeader(p, bytes.byteLength, 0));
|
|
4101
|
+
await write(Buffer.from(bytes));
|
|
4102
|
+
await write(tarPadding(bytes.byteLength));
|
|
4103
|
+
}
|
|
4104
|
+
await write(TAR_END);
|
|
4105
|
+
res.end();
|
|
4106
|
+
}
|
|
4107
|
+
catch {
|
|
4108
|
+
res.destroy();
|
|
4109
|
+
}
|
|
4110
|
+
return;
|
|
4111
|
+
}
|
|
4112
|
+
sendJson(res, 404, { error: "unknown workspace verb — use /tree, /file?path=, or /archive" });
|
|
4113
|
+
return;
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
3935
4116
|
{
|
|
3936
4117
|
const sm = /^\/v1\/sessions\/([^/]+)\/sync\/(manifest|entries|import|plan|blobs(?:\/([^/]+))?|snapshots\/([^/]+)\/blobs\/([^/]+)|import\/([^/]+)\/entries)$/.exec(url);
|
|
3937
4118
|
if (sm) {
|
|
@@ -6625,6 +6806,23 @@ function routeLabel(_method, url) {
|
|
|
6625
6806
|
return url;
|
|
6626
6807
|
return "other";
|
|
6627
6808
|
}
|
|
6809
|
+
function workspaceContentType(relPath) {
|
|
6810
|
+
const ext = relPath.slice(relPath.lastIndexOf(".") + 1).toLowerCase();
|
|
6811
|
+
const TABLE = {
|
|
6812
|
+
txt: "text/plain; charset=utf-8", md: "text/plain; charset=utf-8", log: "text/plain; charset=utf-8",
|
|
6813
|
+
json: "application/json; charset=utf-8", csv: "text/csv; charset=utf-8",
|
|
6814
|
+
png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp",
|
|
6815
|
+
pdf: "application/pdf",
|
|
6816
|
+
};
|
|
6817
|
+
return TABLE[ext] ?? "application/octet-stream";
|
|
6818
|
+
}
|
|
6819
|
+
function looksBinary(bytes) {
|
|
6820
|
+
const n = Math.min(bytes.byteLength, 8192);
|
|
6821
|
+
for (let i = 0; i < n; i++)
|
|
6822
|
+
if (bytes[i] === 0)
|
|
6823
|
+
return true;
|
|
6824
|
+
return false;
|
|
6825
|
+
}
|
|
6628
6826
|
function authorized(req, token) {
|
|
6629
6827
|
const h = req.headers.authorization ?? "";
|
|
6630
6828
|
return safeEqual(h, `Bearer ${token}`) || safeEqual(h, `token ${token}`);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function splitTarPath(relPath: string): {
|
|
2
|
+
prefix: string;
|
|
3
|
+
name: string;
|
|
4
|
+
} | null;
|
|
5
|
+
export declare function tarHeader(relPath: string, sizeBytes: number, mtimeSec: number): Buffer;
|
|
6
|
+
export declare function tarPadding(sizeBytes: number): Buffer;
|
|
7
|
+
export declare const TAR_END: Buffer<ArrayBuffer>;
|
|
8
|
+
//# sourceMappingURL=tar.d.ts.map
|
package/dist/http/tar.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const BLOCK = 512;
|
|
2
|
+
function octal(n, width) {
|
|
3
|
+
const s = n.toString(8).padStart(width - 1, "0");
|
|
4
|
+
return Buffer.from(s + "\0", "ascii");
|
|
5
|
+
}
|
|
6
|
+
export function splitTarPath(relPath) {
|
|
7
|
+
const bytes = Buffer.byteLength(relPath, "utf8");
|
|
8
|
+
if (bytes <= 100)
|
|
9
|
+
return { prefix: "", name: relPath };
|
|
10
|
+
if (bytes > 255)
|
|
11
|
+
return null;
|
|
12
|
+
for (let i = relPath.length - 1; i > 0; i--) {
|
|
13
|
+
if (relPath[i] !== "/")
|
|
14
|
+
continue;
|
|
15
|
+
const prefix = relPath.slice(0, i);
|
|
16
|
+
const name = relPath.slice(i + 1);
|
|
17
|
+
if (Buffer.byteLength(name, "utf8") <= 100 && Buffer.byteLength(prefix, "utf8") <= 155 && name.length > 0)
|
|
18
|
+
return { prefix, name };
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
export function tarHeader(relPath, sizeBytes, mtimeSec) {
|
|
23
|
+
const split = splitTarPath(relPath);
|
|
24
|
+
if (split === null)
|
|
25
|
+
throw new Error(`tar: path not representable in ustar (must be ≤255 bytes and splittable at "/"): ${relPath}`);
|
|
26
|
+
const h = Buffer.alloc(BLOCK, 0);
|
|
27
|
+
h.write(split.name, 0, 100, "utf8");
|
|
28
|
+
octal(0o644, 8).copy(h, 100);
|
|
29
|
+
octal(0, 8).copy(h, 108);
|
|
30
|
+
octal(0, 8).copy(h, 116);
|
|
31
|
+
octal(sizeBytes, 12).copy(h, 124);
|
|
32
|
+
octal(Math.max(0, Math.floor(mtimeSec)), 12).copy(h, 136);
|
|
33
|
+
h.fill(0x20, 148, 156);
|
|
34
|
+
h.write("0", 156, 1, "ascii");
|
|
35
|
+
h.write("ustar\0", 257, 6, "ascii");
|
|
36
|
+
h.write("00", 263, 2, "ascii");
|
|
37
|
+
h.write(split.prefix, 345, 155, "utf8");
|
|
38
|
+
let sum = 0;
|
|
39
|
+
for (const b of h)
|
|
40
|
+
sum += b;
|
|
41
|
+
Buffer.from(sum.toString(8).padStart(6, "0") + "\0 ", "ascii").copy(h, 148);
|
|
42
|
+
return h;
|
|
43
|
+
}
|
|
44
|
+
export function tarPadding(sizeBytes) {
|
|
45
|
+
const rem = sizeBytes % BLOCK;
|
|
46
|
+
return Buffer.alloc(rem === 0 ? 0 : BLOCK - rem, 0);
|
|
47
|
+
}
|
|
48
|
+
export const TAR_END = Buffer.alloc(BLOCK * 2, 0);
|
|
49
|
+
//# sourceMappingURL=tar.js.map
|
|
@@ -21,6 +21,7 @@ export declare class SqlBlobBackend implements BlobBackend {
|
|
|
21
21
|
hasBlobs(hashes: string[]): Promise<Set<string>>;
|
|
22
22
|
deleteBlobs(hashes: string[]): Promise<number>;
|
|
23
23
|
}
|
|
24
|
+
export declare function sqlBlobSizes(dialect: "tidb" | "pg", pool: MysqlPool | PgPool, hashes: string[]): Promise<Map<string, number>>;
|
|
24
25
|
export interface MinioBlobBackendConfig {
|
|
25
26
|
endpoint: string;
|
|
26
27
|
bucket: string;
|
|
@@ -71,6 +71,19 @@ async function sqlHasBlobs(dialect, pool, hashes) {
|
|
|
71
71
|
const r = await pool.query(`SELECT blob_hash FROM snapshot_blob WHERE blob_hash IN (${ph})`, distinct);
|
|
72
72
|
return new Set(r.rows.map((row) => row.blob_hash));
|
|
73
73
|
}
|
|
74
|
+
export async function sqlBlobSizes(dialect, pool, hashes) {
|
|
75
|
+
if (hashes.length === 0)
|
|
76
|
+
return new Map();
|
|
77
|
+
const distinct = [...new Set(hashes)];
|
|
78
|
+
if (dialect === "tidb") {
|
|
79
|
+
const ph = distinct.map(() => "?").join(",");
|
|
80
|
+
const [rows] = await pool.query(`SELECT blob_hash, byte_len FROM snapshot_blob WHERE blob_hash IN (${ph})`, distinct);
|
|
81
|
+
return new Map(rows.map((r) => [String(r.blob_hash), Number(r.byte_len)]));
|
|
82
|
+
}
|
|
83
|
+
const ph = distinct.map((_, i) => `$${i + 1}`).join(",");
|
|
84
|
+
const r = await pool.query(`SELECT blob_hash, byte_len FROM snapshot_blob WHERE blob_hash IN (${ph})`, distinct);
|
|
85
|
+
return new Map(r.rows.map((row) => [row.blob_hash, Number(row.byte_len)]));
|
|
86
|
+
}
|
|
74
87
|
async function sqlDeleteIndexRowsPastGrace(dialect, pool, hashes) {
|
|
75
88
|
if (hashes.length === 0)
|
|
76
89
|
return [];
|
|
@@ -18,6 +18,7 @@ export declare class PgFileSnapshotStore implements FileSnapshotStore {
|
|
|
18
18
|
exportManifest(scope: string, key: string): Promise<Map<string, string> | null>;
|
|
19
19
|
getBlob(hash: string): Promise<Uint8Array | undefined>;
|
|
20
20
|
hasBlobs(hashes: string[]): Promise<Set<string>>;
|
|
21
|
+
blobSizes(hashes: string[]): Promise<Map<string, number>>;
|
|
21
22
|
reap(scope: string, keepKeys: string[]): Promise<number>;
|
|
22
23
|
deleteBySession(scope: string): Promise<number>;
|
|
23
24
|
private gcOrphanBlobs;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { captureManifest, applyManifest, DEFAULT_SNAPSHOT_BOUNDS } from "@sema-agent/core";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { SqlBlobBackend } from "./blob-backend.js";
|
|
3
|
+
import { SqlBlobBackend, sqlBlobSizes } from "./blob-backend.js";
|
|
4
4
|
export class PgFileSnapshotStore {
|
|
5
5
|
pool;
|
|
6
6
|
bounds;
|
|
@@ -141,6 +141,9 @@ export class PgFileSnapshotStore {
|
|
|
141
141
|
async hasBlobs(hashes) {
|
|
142
142
|
return this.blobs.hasBlobs(hashes);
|
|
143
143
|
}
|
|
144
|
+
async blobSizes(hashes) {
|
|
145
|
+
return sqlBlobSizes("pg", this.pool, hashes);
|
|
146
|
+
}
|
|
144
147
|
async reap(scope, keepKeys) {
|
|
145
148
|
return this.tx(async (c) => {
|
|
146
149
|
const keys = await c.query("SELECT DISTINCT snap_key FROM snapshot_manifest WHERE scope = $1", [scope]);
|
|
@@ -18,6 +18,7 @@ export declare class TiDBFileSnapshotStore implements FileSnapshotStore {
|
|
|
18
18
|
exportManifest(scope: string, key: string): Promise<Map<string, string> | null>;
|
|
19
19
|
getBlob(hash: string): Promise<Uint8Array | undefined>;
|
|
20
20
|
hasBlobs(hashes: string[]): Promise<Set<string>>;
|
|
21
|
+
blobSizes(hashes: string[]): Promise<Map<string, number>>;
|
|
21
22
|
reap(scope: string, keepKeys: string[]): Promise<number>;
|
|
22
23
|
deleteBySession(scope: string): Promise<number>;
|
|
23
24
|
private gcOrphanBlobs;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { captureManifest, applyManifest, DEFAULT_SNAPSHOT_BOUNDS } from "@sema-agent/core";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { SqlBlobBackend } from "./blob-backend.js";
|
|
3
|
+
import { SqlBlobBackend, sqlBlobSizes } from "./blob-backend.js";
|
|
4
4
|
export class TiDBFileSnapshotStore {
|
|
5
5
|
pool;
|
|
6
6
|
bounds;
|
|
@@ -141,6 +141,9 @@ export class TiDBFileSnapshotStore {
|
|
|
141
141
|
async hasBlobs(hashes) {
|
|
142
142
|
return this.blobs.hasBlobs(hashes);
|
|
143
143
|
}
|
|
144
|
+
async blobSizes(hashes) {
|
|
145
|
+
return sqlBlobSizes("tidb", this.pool, hashes);
|
|
146
|
+
}
|
|
144
147
|
async reap(scope, keepKeys) {
|
|
145
148
|
return this.tx(async (conn) => {
|
|
146
149
|
const [keys] = await conn.query("SELECT DISTINCT snap_key FROM snapshot_manifest WHERE scope = ?", [scope]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.299.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|