@sema-agent/server 1.301.0 → 1.303.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/http/server.js +13 -13
- package/dist/main.js +6 -0
- package/dist/plugins/blob-backend.d.ts +5 -0
- package/dist/plugins/blob-backend.js +35 -1
- package/dist/plugins/s3-presign.d.ts +12 -0
- package/dist/plugins/s3-presign.js +19 -0
- package/dist/plugins/task-attachment-store.d.ts +9 -0
- package/dist/plugins/task-attachment-store.js +52 -0
- package/package.json +1 -1
package/dist/http/server.js
CHANGED
|
@@ -3291,11 +3291,11 @@ export function createHttpServer(deps) {
|
|
|
3291
3291
|
scope = decodeURIComponent(url.slice("/v1/memory/sync/".length));
|
|
3292
3292
|
}
|
|
3293
3293
|
catch {
|
|
3294
|
-
sendJson(res, 400, { error: "malformed percent-encoding in :scope", code: "memory_sync_invalid_scope" });
|
|
3294
|
+
sendJson(res, 400, { error: "malformed percent-encoding in :scope", code: "memory_sync_invalid_scope", errorCode: "memory_sync_invalid_scope" });
|
|
3295
3295
|
return;
|
|
3296
3296
|
}
|
|
3297
3297
|
if (scope.length === 0) {
|
|
3298
|
-
sendJson(res, 400, { error: "missing :scope", code: "memory_sync_invalid_scope" });
|
|
3298
|
+
sendJson(res, 400, { error: "missing :scope", code: "memory_sync_invalid_scope", errorCode: "memory_sync_invalid_scope" });
|
|
3299
3299
|
return;
|
|
3300
3300
|
}
|
|
3301
3301
|
if (scope !== formatUserScope(principal) && !explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
@@ -3305,7 +3305,7 @@ export function createHttpServer(deps) {
|
|
|
3305
3305
|
const body = await readJson(req);
|
|
3306
3306
|
const parsed = parseMemorySyncRequest(body, scope);
|
|
3307
3307
|
if (!parsed.ok) {
|
|
3308
|
-
sendJson(res, 422, { error: parsed.error, code: "memory_sync_invalid_body" });
|
|
3308
|
+
sendJson(res, 422, { error: parsed.error, code: "memory_sync_invalid_body", errorCode: "memory_sync_invalid_body" });
|
|
3309
3309
|
return;
|
|
3310
3310
|
}
|
|
3311
3311
|
try {
|
|
@@ -4042,7 +4042,7 @@ export function createHttpServer(deps) {
|
|
|
4042
4042
|
if (typeof fs.blobSizes === "function") {
|
|
4043
4043
|
const known = (await fs.blobSizes([hash])).get(hash);
|
|
4044
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 });
|
|
4045
|
+
sendJson(res, 413, { error: "file exceeds the single-file read limit — download it via the archive endpoint", code: "workspace_file_too_large", errorCode: "workspace_file_too_large", sizeBytes: known, limit: cap });
|
|
4046
4046
|
return;
|
|
4047
4047
|
}
|
|
4048
4048
|
}
|
|
@@ -4052,7 +4052,7 @@ export function createHttpServer(deps) {
|
|
|
4052
4052
|
return;
|
|
4053
4053
|
}
|
|
4054
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 });
|
|
4055
|
+
sendJson(res, 413, { error: "file exceeds the single-file read limit — download it via the archive endpoint", code: "workspace_file_too_large", errorCode: "workspace_file_too_large", sizeBytes: bytes.byteLength, limit: cap });
|
|
4056
4056
|
return;
|
|
4057
4057
|
}
|
|
4058
4058
|
const ct = workspaceContentType(pathParam);
|
|
@@ -4259,7 +4259,7 @@ export function createHttpServer(deps) {
|
|
|
4259
4259
|
}
|
|
4260
4260
|
const put = await fs.putBlob(hash, new Uint8Array(body));
|
|
4261
4261
|
if (!put.ok) {
|
|
4262
|
-
sendJson(res, 502, { error: "blob store write failed", code: put.error.code });
|
|
4262
|
+
sendJson(res, 502, { error: "blob store write failed", code: put.error.code, errorCode: put.error.code });
|
|
4263
4263
|
return;
|
|
4264
4264
|
}
|
|
4265
4265
|
res.writeHead(204).end();
|
|
@@ -4626,7 +4626,7 @@ export function createHttpServer(deps) {
|
|
|
4626
4626
|
}
|
|
4627
4627
|
const allow = deps.config.attachmentMimeAllowlist;
|
|
4628
4628
|
if (allow && !allow.some((a) => a === mime || (a.endsWith("/*") && mime.startsWith(a.slice(0, -1))))) {
|
|
4629
|
-
sendJson(res, 415, { code: "attachment.mime_not_allowed", error: `mime '${mime}' is not in this deployment's allowlist`, allowed: allow });
|
|
4629
|
+
sendJson(res, 415, { code: "attachment.mime_not_allowed", errorCode: "attachment.mime_not_allowed", error: `mime '${mime}' is not in this deployment's allowlist`, allowed: allow });
|
|
4630
4630
|
return;
|
|
4631
4631
|
}
|
|
4632
4632
|
const max = deps.config.attachmentMaxBytes ?? 32 * 1024 * 1024;
|
|
@@ -4636,7 +4636,7 @@ export function createHttpServer(deps) {
|
|
|
4636
4636
|
}
|
|
4637
4637
|
catch (err) {
|
|
4638
4638
|
if (err instanceof HttpError && err.status === 413) {
|
|
4639
|
-
sendJson(res, 413, { code: "attachment.too_large", error: `attachment exceeds the per-file limit (${max} bytes)`, maxBytes: max });
|
|
4639
|
+
sendJson(res, 413, { code: "attachment.too_large", errorCode: "attachment.too_large", error: `attachment exceeds the per-file limit (${max} bytes)`, maxBytes: max });
|
|
4640
4640
|
return;
|
|
4641
4641
|
}
|
|
4642
4642
|
throw err;
|
|
@@ -5260,7 +5260,7 @@ export function createHttpServer(deps) {
|
|
|
5260
5260
|
if (e instanceof HttpError) {
|
|
5261
5261
|
return {
|
|
5262
5262
|
status: 409,
|
|
5263
|
-
body: { error: `resume blocked by an admission policy change: ${e.message}`, code: "resume_blocked_by_policy", ...(e.code ? { blockedBy: e.code } : {}), ...(e.extra ?? {}) },
|
|
5263
|
+
body: { error: `resume blocked by an admission policy change: ${e.message}`, code: "resume_blocked_by_policy", errorCode: "resume_blocked_by_policy", ...(e.code ? { blockedBy: e.code } : {}), ...(e.extra ?? {}) },
|
|
5264
5264
|
};
|
|
5265
5265
|
}
|
|
5266
5266
|
throw e;
|
|
@@ -5646,9 +5646,9 @@ export function createHttpServer(deps) {
|
|
|
5646
5646
|
body: {
|
|
5647
5647
|
error: e.message,
|
|
5648
5648
|
code: e.code,
|
|
5649
|
-
|
|
5650
|
-
?
|
|
5651
|
-
:
|
|
5649
|
+
errorCode: e.code === "checkpoint.invalid_outcome"
|
|
5650
|
+
? (outcome.gate === "policy_ask" ? "approval_binding_mismatch" : "resume_outcome_invalid")
|
|
5651
|
+
: e.code,
|
|
5652
5652
|
...(retriable ? { retriable: true } : {}),
|
|
5653
5653
|
},
|
|
5654
5654
|
};
|
|
@@ -5888,7 +5888,7 @@ export function createHttpServer(deps) {
|
|
|
5888
5888
|
return false;
|
|
5889
5889
|
deps.metrics?.inc("fleet_lease_rejected_total");
|
|
5890
5890
|
res.setHeader("retry-after", String(adm.retryAfterSec));
|
|
5891
|
-
sendJson(res, 429, { error: "budget exhausted (quota lease)", code: "quota_exhausted", retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
|
|
5891
|
+
sendJson(res, 429, { error: "budget exhausted (quota lease)", code: "quota_exhausted", errorCode: "quota_exhausted", retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
|
|
5892
5892
|
return true;
|
|
5893
5893
|
}
|
|
5894
5894
|
function rateLimited(req, res) {
|
package/dist/main.js
CHANGED
|
@@ -1440,6 +1440,12 @@ async function main() {
|
|
|
1440
1440
|
void toolResultStore?.reapOlderThan?.(Date.now() - config.toolResultTtlSec * 1000)?.catch(() => undefined);
|
|
1441
1441
|
void fileSnapshotStore?.sweepOrphanBlobs?.().catch(() => undefined);
|
|
1442
1442
|
void taskAttachmentStore?.reapUnbound(Date.now() - config.attachmentUnboundTtlMs).then(reapCount("attachments_reaped_total", {})).catch(() => undefined);
|
|
1443
|
+
void taskAttachmentStore?.sweepOrphanObjects?.(3_600_000)
|
|
1444
|
+
.then((n) => { if (n > 0) {
|
|
1445
|
+
metrics.inc("attachment_orphan_objects_swept_total", {}, n);
|
|
1446
|
+
logger.info("attachment_orphan_objects_swept", { removed: n });
|
|
1447
|
+
} })
|
|
1448
|
+
.catch((err) => logger.warn("attachment_orphan_sweep_failed", { err: String(err) }));
|
|
1443
1449
|
void backend?.session()?.sweepStagingSessions?.().catch(() => undefined);
|
|
1444
1450
|
void imageBakes?.reapStaleBakes(config.imageBakes.staleMs).then(reapCount("bakes_reaped_total", {})).catch(() => undefined);
|
|
1445
1451
|
void worktreeReap?.();
|
|
@@ -30,6 +30,7 @@ export interface MinioBlobBackendConfig {
|
|
|
30
30
|
region?: string;
|
|
31
31
|
keyPrefix?: string;
|
|
32
32
|
presignTtlSec?: number;
|
|
33
|
+
fetchImpl?: typeof fetch;
|
|
33
34
|
}
|
|
34
35
|
export declare class MinioBlobBackend implements BlobBackend {
|
|
35
36
|
private readonly cfg;
|
|
@@ -46,6 +47,10 @@ export declare class MinioBlobBackend implements BlobBackend {
|
|
|
46
47
|
putBlob(hash: string, bytes: Uint8Array): Promise<void>;
|
|
47
48
|
getBlob(hash: string): Promise<Uint8Array | undefined>;
|
|
48
49
|
hasBlobs(hashes: string[]): Promise<Set<string>>;
|
|
50
|
+
listObjects(): AsyncGenerator<{
|
|
51
|
+
hash: string;
|
|
52
|
+
lastModifiedMs: number;
|
|
53
|
+
}>;
|
|
49
54
|
deleteBlobs(hashes: string[]): Promise<number>;
|
|
50
55
|
}
|
|
51
56
|
//# sourceMappingURL=blob-backend.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { presignS3Url } from "./s3-presign.js";
|
|
2
|
+
import { presignS3Url, presignS3ListUrl } from "./s3-presign.js";
|
|
3
3
|
const BLOB_GC_GRACE_MS = 3_600_000;
|
|
4
4
|
export const SQL_BLOB_DEFAULT_MAX_BYTES = 6_291_456;
|
|
5
5
|
export class BlobTooLargeError extends Error {
|
|
@@ -191,6 +191,40 @@ export class MinioBlobBackend {
|
|
|
191
191
|
});
|
|
192
192
|
return present;
|
|
193
193
|
}
|
|
194
|
+
async *listObjects() {
|
|
195
|
+
const f = this.cfg.fetchImpl ?? fetch;
|
|
196
|
+
let token;
|
|
197
|
+
do {
|
|
198
|
+
const url = presignS3ListUrl({
|
|
199
|
+
endpoint: this.cfg.endpoint,
|
|
200
|
+
bucket: this.cfg.bucket,
|
|
201
|
+
prefix: this.keyPrefix,
|
|
202
|
+
...(token ? { continuationToken: token } : {}),
|
|
203
|
+
maxKeys: 1000,
|
|
204
|
+
accessKey: this.cfg.accessKey,
|
|
205
|
+
secretKey: this.cfg.secretKey,
|
|
206
|
+
...(this.cfg.region ? { region: this.cfg.region } : {}),
|
|
207
|
+
expiresSec: this.ttl,
|
|
208
|
+
});
|
|
209
|
+
const res = await f(url, { method: "GET" });
|
|
210
|
+
if (!res.ok)
|
|
211
|
+
throw new Error(`listObjects: MinIO ${res.status}`);
|
|
212
|
+
const xml = await res.text();
|
|
213
|
+
for (const m of xml.matchAll(/<Contents>([\s\S]*?)<\/Contents>/g)) {
|
|
214
|
+
const block = m[1];
|
|
215
|
+
const key = /<Key>([^<]*)<\/Key>/.exec(block)?.[1];
|
|
216
|
+
const lm = /<LastModified>([^<]*)<\/LastModified>/.exec(block)?.[1];
|
|
217
|
+
if (key === undefined || !key.startsWith(this.keyPrefix))
|
|
218
|
+
continue;
|
|
219
|
+
const ms = lm ? Date.parse(lm) : NaN;
|
|
220
|
+
yield { hash: key.slice(this.keyPrefix.length), lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() };
|
|
221
|
+
}
|
|
222
|
+
const truncated = /<IsTruncated>true<\/IsTruncated>/.test(xml);
|
|
223
|
+
token = truncated ? /<NextContinuationToken>([^<]*)<\/NextContinuationToken>/.exec(xml)?.[1] : undefined;
|
|
224
|
+
if (truncated && token === undefined)
|
|
225
|
+
throw new Error("listObjects: truncated response without NextContinuationToken");
|
|
226
|
+
} while (token !== undefined);
|
|
227
|
+
}
|
|
194
228
|
async deleteBlobs(hashes) {
|
|
195
229
|
if (hashes.length === 0)
|
|
196
230
|
return 0;
|
|
@@ -21,6 +21,18 @@ export interface S3BucketPresignParams {
|
|
|
21
21
|
now?: Date;
|
|
22
22
|
}
|
|
23
23
|
export declare function presignS3BucketUrl(p: S3BucketPresignParams): string;
|
|
24
|
+
export declare function presignS3ListUrl(p: {
|
|
25
|
+
endpoint: string;
|
|
26
|
+
bucket: string;
|
|
27
|
+
prefix: string;
|
|
28
|
+
continuationToken?: string;
|
|
29
|
+
maxKeys?: number;
|
|
30
|
+
accessKey: string;
|
|
31
|
+
secretKey: string;
|
|
32
|
+
region?: string;
|
|
33
|
+
expiresSec?: number;
|
|
34
|
+
now?: Date;
|
|
35
|
+
}): string;
|
|
24
36
|
export interface S3CopyPresignParams {
|
|
25
37
|
endpoint: string;
|
|
26
38
|
bucket: string;
|
|
@@ -35,6 +35,7 @@ function presignCanonical(p) {
|
|
|
35
35
|
const headerNames = [...headerMap.keys()].sort();
|
|
36
36
|
const signedHeaderList = headerNames.join(";");
|
|
37
37
|
const params = {
|
|
38
|
+
...(p.extraQuery ?? {}),
|
|
38
39
|
"X-Amz-Algorithm": "AWS4-HMAC-SHA256",
|
|
39
40
|
"X-Amz-Credential": `${p.accessKey}/${scope}`,
|
|
40
41
|
"X-Amz-Date": amzDate,
|
|
@@ -67,6 +68,24 @@ export function presignS3Url(p) {
|
|
|
67
68
|
export function presignS3BucketUrl(p) {
|
|
68
69
|
return presignCanonical({ ...p, canonicalUri: `/${uriEncode(p.bucket, true)}/` });
|
|
69
70
|
}
|
|
71
|
+
export function presignS3ListUrl(p) {
|
|
72
|
+
return presignCanonical({
|
|
73
|
+
endpoint: p.endpoint,
|
|
74
|
+
canonicalUri: `/${uriEncode(p.bucket, true)}/`,
|
|
75
|
+
method: "GET",
|
|
76
|
+
accessKey: p.accessKey,
|
|
77
|
+
secretKey: p.secretKey,
|
|
78
|
+
...(p.region ? { region: p.region } : {}),
|
|
79
|
+
...(p.expiresSec ? { expiresSec: p.expiresSec } : {}),
|
|
80
|
+
...(p.now ? { now: p.now } : {}),
|
|
81
|
+
extraQuery: {
|
|
82
|
+
"list-type": "2",
|
|
83
|
+
prefix: p.prefix,
|
|
84
|
+
...(p.continuationToken ? { "continuation-token": p.continuationToken } : {}),
|
|
85
|
+
...(p.maxKeys ? { "max-keys": String(p.maxKeys) } : {}),
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
70
89
|
export function presignS3CopyUrl(p) {
|
|
71
90
|
for (const [label, key] of [
|
|
72
91
|
["destination", p.key],
|
|
@@ -22,12 +22,17 @@ export interface TaskAttachmentStore {
|
|
|
22
22
|
delete(owner: string, id: string): Promise<boolean>;
|
|
23
23
|
deleteBySession(sessionId: string): Promise<number>;
|
|
24
24
|
reapUnbound(olderThanMs: number): Promise<number>;
|
|
25
|
+
sweepOrphanObjects?(graceMs: number, nowMs?: number): Promise<number>;
|
|
25
26
|
}
|
|
26
27
|
export declare const TASK_ATTACHMENT_TABLE = "task_attachment";
|
|
27
28
|
export interface AttachmentBytesStore {
|
|
28
29
|
putBlob(hash: string, bytes: Uint8Array): Promise<void>;
|
|
29
30
|
getBlob(hash: string): Promise<Uint8Array | undefined>;
|
|
30
31
|
deleteBlobs(hashes: string[]): Promise<number>;
|
|
32
|
+
listObjects?(): AsyncGenerator<{
|
|
33
|
+
hash: string;
|
|
34
|
+
lastModifiedMs: number;
|
|
35
|
+
}>;
|
|
31
36
|
}
|
|
32
37
|
export declare function materializedRelPaths(atts: TaskAttachmentRecord[]): Map<string, string>;
|
|
33
38
|
export declare function bindAttachmentsForTask(opts: {
|
|
@@ -78,6 +83,8 @@ export declare class TiDBTaskAttachmentStore implements TaskAttachmentStore {
|
|
|
78
83
|
delete(owner: string, id: string): Promise<boolean>;
|
|
79
84
|
deleteBySession(sessionId: string): Promise<number>;
|
|
80
85
|
reapUnbound(olderThanMs: number): Promise<number>;
|
|
86
|
+
sweepOrphanObjects(graceMs: number, nowMs?: number): Promise<number>;
|
|
87
|
+
private referencedShas;
|
|
81
88
|
}
|
|
82
89
|
export declare class PgTaskAttachmentStore implements TaskAttachmentStore {
|
|
83
90
|
private readonly q;
|
|
@@ -94,5 +101,7 @@ export declare class PgTaskAttachmentStore implements TaskAttachmentStore {
|
|
|
94
101
|
delete(owner: string, id: string): Promise<boolean>;
|
|
95
102
|
deleteBySession(sessionId: string): Promise<number>;
|
|
96
103
|
reapUnbound(olderThanMs: number): Promise<number>;
|
|
104
|
+
sweepOrphanObjects(graceMs: number, nowMs?: number): Promise<number>;
|
|
105
|
+
private referencedShas;
|
|
97
106
|
}
|
|
98
107
|
//# sourceMappingURL=task-attachment-store.d.ts.map
|
|
@@ -159,6 +159,32 @@ export class TiDBTaskAttachmentStore {
|
|
|
159
159
|
await this.deleteBytesIfUnreferenced(rows.map((r) => r.sha256));
|
|
160
160
|
return res.affectedRows ?? 0;
|
|
161
161
|
}
|
|
162
|
+
async sweepOrphanObjects(graceMs, nowMs = Date.now()) {
|
|
163
|
+
const lister = this.bytes;
|
|
164
|
+
if (typeof lister.listObjects !== "function")
|
|
165
|
+
return 0;
|
|
166
|
+
const candidates = [];
|
|
167
|
+
for await (const o of lister.listObjects()) {
|
|
168
|
+
if (nowMs - o.lastModifiedMs >= graceMs)
|
|
169
|
+
candidates.push(o.hash);
|
|
170
|
+
}
|
|
171
|
+
let removed = 0;
|
|
172
|
+
for (let i = 0; i < candidates.length; i += 200) {
|
|
173
|
+
const batch = candidates.slice(i, i + 200);
|
|
174
|
+
const referenced = await this.referencedShas(batch);
|
|
175
|
+
const orphans = batch.filter((h) => !referenced.has(h));
|
|
176
|
+
if (orphans.length > 0)
|
|
177
|
+
removed += await this.bytes.deleteBlobs(orphans);
|
|
178
|
+
}
|
|
179
|
+
return removed;
|
|
180
|
+
}
|
|
181
|
+
async referencedShas(shas) {
|
|
182
|
+
if (shas.length === 0)
|
|
183
|
+
return new Set();
|
|
184
|
+
const ph = shas.map(() => "?").join(",");
|
|
185
|
+
const [rows] = await this.pool.query(`SELECT DISTINCT sha256 FROM ${TASK_ATTACHMENT_TABLE} WHERE sha256 IN (${ph})`, shas);
|
|
186
|
+
return new Set(rows.map((r) => String(r.sha256)));
|
|
187
|
+
}
|
|
162
188
|
}
|
|
163
189
|
export class PgTaskAttachmentStore {
|
|
164
190
|
q;
|
|
@@ -220,5 +246,31 @@ export class PgTaskAttachmentStore {
|
|
|
220
246
|
await this.deleteBytesIfUnreferenced(res.rows.map((r) => r.sha256));
|
|
221
247
|
return res.rows.length;
|
|
222
248
|
}
|
|
249
|
+
async sweepOrphanObjects(graceMs, nowMs = Date.now()) {
|
|
250
|
+
const lister = this.bytes;
|
|
251
|
+
if (typeof lister.listObjects !== "function")
|
|
252
|
+
return 0;
|
|
253
|
+
const candidates = [];
|
|
254
|
+
for await (const o of lister.listObjects()) {
|
|
255
|
+
if (nowMs - o.lastModifiedMs >= graceMs)
|
|
256
|
+
candidates.push(o.hash);
|
|
257
|
+
}
|
|
258
|
+
let removed = 0;
|
|
259
|
+
for (let i = 0; i < candidates.length; i += 200) {
|
|
260
|
+
const batch = candidates.slice(i, i + 200);
|
|
261
|
+
const referenced = await this.referencedShas(batch);
|
|
262
|
+
const orphans = batch.filter((h) => !referenced.has(h));
|
|
263
|
+
if (orphans.length > 0)
|
|
264
|
+
removed += await this.bytes.deleteBlobs(orphans);
|
|
265
|
+
}
|
|
266
|
+
return removed;
|
|
267
|
+
}
|
|
268
|
+
async referencedShas(shas) {
|
|
269
|
+
if (shas.length === 0)
|
|
270
|
+
return new Set();
|
|
271
|
+
const ph = shas.map((_, i) => `$${i + 1}`).join(",");
|
|
272
|
+
const r = await this.q(`SELECT DISTINCT sha256 FROM ${TASK_ATTACHMENT_TABLE} WHERE sha256 IN (${ph})`, shas);
|
|
273
|
+
return new Set(r.rows.map((row) => String(row.sha256)));
|
|
274
|
+
}
|
|
223
275
|
}
|
|
224
276
|
//# sourceMappingURL=task-attachment-store.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.303.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",
|