@sema-agent/server 1.313.0 → 1.314.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.
Files changed (38) hide show
  1. package/dist/auth-bridge.d.ts +3 -2
  2. package/dist/bench/s1/live-deps.js +2 -2
  3. package/dist/budget.d.ts +4 -76
  4. package/dist/budget.js +4 -83
  5. package/dist/capabilities/sandbox-file-send.d.ts +2 -1
  6. package/dist/config-types.d.ts +1 -1
  7. package/dist/elicitation.d.ts +5 -3
  8. package/dist/elicitation.js +3 -2
  9. package/dist/fleet-lease.d.ts +1 -1
  10. package/dist/fleet-lease.js +1 -1
  11. package/dist/hooks/hook-llm.d.ts +5 -4
  12. package/dist/hooks/hook-runner.d.ts +1 -1
  13. package/dist/hooks/hook-runner.js +1 -1
  14. package/dist/http/server.js +67 -85
  15. package/dist/key-resolver.d.ts +1 -1
  16. package/dist/key-resolver.js +1 -1
  17. package/dist/leader/grader-env-factory.d.ts +1 -1
  18. package/dist/leader/grader-env-factory.js +2 -2
  19. package/dist/leader/wire.d.ts +1 -1
  20. package/dist/leader/wire.js +1 -1
  21. package/dist/main.js +26 -26
  22. package/dist/memory-scope.d.ts +18 -0
  23. package/dist/memory-scope.js +38 -0
  24. package/dist/observability/cost-taxonomy.d.ts +34 -0
  25. package/dist/observability/cost-taxonomy.js +26 -0
  26. package/dist/observability/prompt-manifest.d.ts +75 -0
  27. package/dist/observability/prompt-manifest.js +82 -0
  28. package/dist/plugins/web-search.d.ts +4 -3
  29. package/dist/question.d.ts +2 -1
  30. package/dist/question.js +3 -2
  31. package/dist/run-local.js +2 -2
  32. package/dist/runtime-caps-resolver.d.ts +7 -7
  33. package/dist/runtime-caps-resolver.js +3 -3
  34. package/dist/security.d.ts +2 -16
  35. package/dist/security.js +3 -37
  36. package/dist/tool-approval.d.ts +2 -1
  37. package/dist/tool-approval.js +3 -2
  38. package/package.json +1 -1
@@ -30,7 +30,7 @@ import { projectEvents, runSummary, mapTraceEvent, turnEndEventData, contextUsag
30
30
  import { createLedgerSink } from "../trace/ledger-sink.js";
31
31
  import { cacheFamilyOfMirror } from "../budget.js";
32
32
  import { projectArtifacts } from "../trace/artifacts.js";
33
- import { composeSupervisorCost, infraCost, infraUsageFromEvents, hasInfraPricing } from "../finance/cost-taxonomy.js";
33
+ import { composeSupervisorCost, infraCost, infraUsageFromEvents, hasInfraPricing } from "../observability/cost-taxonomy.js";
34
34
  import { redactSecrets, redactDeep, redactedPreview } from "../trace/redact.js";
35
35
  import { usageSummary, usageSeries, usageBreakdown } from "../usage-analytics.js";
36
36
  import { IdempotencyCache } from "./idempotency.js";
@@ -634,7 +634,7 @@ export function createHttpServer(deps) {
634
634
  }
635
635
  if (deps.drainState?.draining && req.method === "POST" && isBillableSubmitPath(url)) {
636
636
  res.setHeader("retry-after", "15");
637
- sendJson(res, 503, { error: "draining", errorCode: "draining", message: "this instance is draining for shutdown/upgrade — retry against the replacement instance" });
637
+ sendError(res, 503, "draining", "draining", { message: "this instance is draining for shutdown/upgrade — retry against the replacement instance" });
638
638
  return;
639
639
  }
640
640
  if (deps.modelReady && !deps.modelReady() && req.method === "POST" && isBillableSubmitPath(url)) {
@@ -655,7 +655,7 @@ export function createHttpServer(deps) {
655
655
  const name = url.slice("/v1/capabilities/scenarios/".length).split("?")[0];
656
656
  const detail = deps.scenarioDetails?.[name];
657
657
  if (!detail) {
658
- sendJson(res, 404, { error: "scenario not found", errorCode: "scenario_not_found" });
658
+ sendError(res, 404, "scenario_not_found", "scenario not found");
659
659
  return;
660
660
  }
661
661
  sendJson(res, 200, detail);
@@ -1718,7 +1718,7 @@ export function createHttpServer(deps) {
1718
1718
  }
1719
1719
  catch (e) {
1720
1720
  if (e instanceof CheckpointError && e.code === "steering.invalid_content") {
1721
- sendJson(res, 422, { errorCode: "steering.invalid_content", error: e.message });
1721
+ sendError(res, 422, "steering.invalid_content", e.message);
1722
1722
  return;
1723
1723
  }
1724
1724
  throw e;
@@ -1747,7 +1747,7 @@ export function createHttpServer(deps) {
1747
1747
  return (await cs.setPendingSteer(token, scope, { text, trusted })) ? "parked" : "no-checkpoint";
1748
1748
  };
1749
1749
  const sendParked = () => sendJson(res, 202, { taskId, status: "suspended", delivery: "queued", messageId, ...(priority ? { priority } : {}), note: "steer parked on the checkpoint — injected when the run resumes" });
1750
- const sendNotRunning = (error) => sendJson(res, 409, { errorCode: "steering.not_running", error });
1750
+ const sendNotRunning = (error) => sendError(res, 409, "steering.not_running", error);
1751
1751
  const live = steerableRuns.get(taskId);
1752
1752
  if (live) {
1753
1753
  try {
@@ -1758,7 +1758,7 @@ export function createHttpServer(deps) {
1758
1758
  catch (e) {
1759
1759
  const code = e.code;
1760
1760
  if (code === "steering.invalid_content") {
1761
- sendJson(res, 422, { errorCode: "steering.invalid_content", error: e instanceof Error ? e.message : "invalid steering content" });
1761
+ sendError(res, 422, "steering.invalid_content", e instanceof Error ? e.message : "invalid steering content");
1762
1762
  return;
1763
1763
  }
1764
1764
  if (code !== "steering.not_running")
@@ -1896,7 +1896,7 @@ export function createHttpServer(deps) {
1896
1896
  const compactInstructions = compactBody.instructions;
1897
1897
  const live = steerableRuns.get(taskId);
1898
1898
  if (!live) {
1899
- sendJson(res, 409, { errorCode: "compact.not_running", error: run.status === "running" ? "run is active on another replica — manual compact is replica-local" : `run is ${run.status} — not accepting compaction` });
1899
+ sendError(res, 409, "compact.not_running", run.status === "running" ? "run is active on another replica — manual compact is replica-local" : `run is ${run.status} — not accepting compaction`);
1900
1900
  return;
1901
1901
  }
1902
1902
  void live.compact(compactInstructions !== undefined ? { instructions: compactInstructions } : undefined).then((outcome) => {
@@ -1953,7 +1953,7 @@ export function createHttpServer(deps) {
1953
1953
  return;
1954
1954
  const live = steerableRuns.get(taskId);
1955
1955
  if (!live) {
1956
- sendJson(res, 409, { errorCode: "detach.not_running", error: run.status === "running" ? "run is active on another replica — detach is replica-local" : `run is ${run.status} — no running tool call to detach` });
1956
+ sendError(res, 409, "detach.not_running", run.status === "running" ? "run is active on another replica — detach is replica-local" : `run is ${run.status} — no running tool call to detach`);
1957
1957
  return;
1958
1958
  }
1959
1959
  live.detach(body.toolCallId);
@@ -1992,10 +1992,7 @@ export function createHttpServer(deps) {
1992
1992
  return;
1993
1993
  }
1994
1994
  if (body.content.length > STEER_IN_MAX_REQUEST_CHARS) {
1995
- sendJson(res, 413, {
1996
- errorCode: "steer.content_too_large",
1997
- error: `content exceeds ${STEER_IN_MAX_REQUEST_CHARS} characters (got ${body.content.length}); note only the first ${STEER_IN_MAX_CHARS} are delivered anyway`,
1998
- });
1995
+ sendError(res, 413, "steer.content_too_large", `content exceeds ${STEER_IN_MAX_REQUEST_CHARS} characters (got ${body.content.length}); note only the first ${STEER_IN_MAX_CHARS} are delivered anyway`);
1999
1996
  return;
2000
1997
  }
2001
1998
  const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
@@ -2007,11 +2004,11 @@ export function createHttpServer(deps) {
2007
2004
  }
2008
2005
  if (wfRun && !runSessionAcceptOk(req, res, { sessionId: wfRun.originatingSessionId ?? null }, "workflow.agent-steer", "workflow not found"))
2009
2006
  return;
2010
- const sendNotRunningWf = (error) => sendJson(res, 409, { errorCode: "steering.not_running", error });
2007
+ const sendNotRunningWf = (error) => sendError(res, 409, "steering.not_running", error);
2011
2008
  const redacted = redactSteerIn(body.content, label);
2012
2009
  const resolution = deps.workflowAgentRegistry?.resolve(wfRunId, label);
2013
2010
  if (resolution && resolution.count > 1) {
2014
- sendJson(res, 409, { errorCode: "steering.ambiguous_label", error: `${resolution.count} live agents share label '${label}' in this run — steer target is ambiguous` });
2011
+ sendError(res, 409, "steering.ambiguous_label", `${resolution.count} live agents share label '${label}' in this run — steer target is ambiguous`);
2015
2012
  return;
2016
2013
  }
2017
2014
  const handle = resolution?.handle;
@@ -2195,27 +2192,23 @@ export function createHttpServer(deps) {
2195
2192
  const stillParked = details?.error === "parked_pending_approval" || details?.status === "parked";
2196
2193
  if (stopVerb && details?.error !== undefined && (details.status === "running" || stillParked || parkResumeWon || parkArbiterUnreachable)) {
2197
2194
  const notLocal = details.error === "not_local";
2198
- sendJson(res, 409, {
2199
- errorCode: notLocal
2200
- ? "stop.not_local"
2201
- : parkArbiterUnreachable
2202
- ? "stop.park_arbiter_unreachable"
2203
- : parkResumeWon
2204
- ? "stop.park_resume_won"
2205
- : stillParked
2206
- ? "stop.parked"
2207
- : "stop.not_landed",
2208
- error: notLocal
2209
- ? "this task is not attached to this instance cannot stop it here (its durable record is still marked running; if its host process is gone, the retention sweep settles it as interrupted)"
2210
- : parkArbiterUnreachable
2211
- ? "the approval arbitration store was unreachable this task's row stays parked (not stopped); retry the stop"
2212
- : parkResumeWon
2213
- ? "the pending tool-approval was resolved (resumed) concurrently with this stop attemptthe resume won the race, so nothing was stopped; re-check the task's current status"
2214
- : stillParked
2215
- ? "this task is durably parked awaiting a tool-approval decision, not actively running — there is no live process to interrupt; resolve (or let expire) the pending approval instead"
2216
- : `stop did not land (${details.error}) — the process may still be running`,
2217
- taskId: runId, target, content: out.content, output: out.details,
2218
- });
2195
+ sendError(res, 409, notLocal
2196
+ ? "stop.not_local"
2197
+ : parkArbiterUnreachable
2198
+ ? "stop.park_arbiter_unreachable"
2199
+ : parkResumeWon
2200
+ ? "stop.park_resume_won"
2201
+ : stillParked
2202
+ ? "stop.parked"
2203
+ : "stop.not_landed", notLocal
2204
+ ? "this task is not attached to this instance — cannot stop it here (its durable record is still marked running; if its host process is gone, the retention sweep settles it as interrupted)"
2205
+ : parkArbiterUnreachable
2206
+ ? "the approval arbitration store was unreachable this task's row stays parked (not stopped); retry the stop"
2207
+ : parkResumeWon
2208
+ ? "the pending tool-approval was resolved (resumed) concurrently with this stop attempt the resume won the race, so nothing was stopped; re-check the task's current status"
2209
+ : stillParked
2210
+ ? "this task is durably parked awaiting a tool-approval decision, not actively running there is no live process to interrupt; resolve (or let expire) the pending approval instead"
2211
+ : `stop did not land (${details.error}) — the process may still be running`, { taskId: runId, target, content: out.content, output: out.details });
2219
2212
  return;
2220
2213
  }
2221
2214
  const g14 = (() => {
@@ -2264,10 +2257,7 @@ export function createHttpServer(deps) {
2264
2257
  return;
2265
2258
  }
2266
2259
  if (body.content.length > STEER_IN_MAX_REQUEST_CHARS) {
2267
- sendJson(res, 413, {
2268
- errorCode: "steer.content_too_large",
2269
- error: `content exceeds ${STEER_IN_MAX_REQUEST_CHARS} characters (got ${body.content.length}); note only the first ${STEER_IN_MAX_CHARS} are delivered anyway`,
2270
- });
2260
+ sendError(res, 413, "steer.content_too_large", `content exceeds ${STEER_IN_MAX_REQUEST_CHARS} characters (got ${body.content.length}); note only the first ${STEER_IN_MAX_CHARS} are delivered anyway`);
2271
2261
  return;
2272
2262
  }
2273
2263
  const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
@@ -2278,7 +2268,7 @@ export function createHttpServer(deps) {
2278
2268
  }
2279
2269
  if (!runSessionAcceptOk(req, res, run, `subagent.${verb}`))
2280
2270
  return;
2281
- const send409 = (errorCode, error) => sendJson(res, 409, { errorCode, error });
2271
+ const send409 = (errorCode, error) => sendError(res, 409, errorCode, error);
2282
2272
  const redacted = redactSteerIn(body.content, target);
2283
2273
  const resolution = deps.subagentSteerRegistry?.resolve(runId, target);
2284
2274
  if (resolution && resolution.count > 1) {
@@ -2964,13 +2954,13 @@ export function createHttpServer(deps) {
2964
2954
  return;
2965
2955
  }
2966
2956
  if (typeof body.reason === "string" && body.reason.length > MAX_APPROVAL_REASON_CHARS) {
2967
- sendJson(res, 413, { error: `reason too large (max ${MAX_APPROVAL_REASON_CHARS} chars)`, errorCode: "reason_too_large" });
2957
+ sendError(res, 413, "reason_too_large", `reason too large (max ${MAX_APPROVAL_REASON_CHARS} chars)`);
2968
2958
  return;
2969
2959
  }
2970
2960
  const run = await deps.runStore.getRun(taskId);
2971
2961
  if (!run) {
2972
2962
  if (deps.config.directDoorActive) {
2973
- sendJson(res, 401, { error: "principal proof required", errorCode: "principal_unverified" });
2963
+ sendError(res, 401, "principal_unverified", "principal proof required");
2974
2964
  return;
2975
2965
  }
2976
2966
  sendJson(res, 404, { error: "task not found" });
@@ -2981,7 +2971,7 @@ export function createHttpServer(deps) {
2981
2971
  const hdr = (n) => { const h = req.headers[n]; return Array.isArray(h) ? h[0] : h; };
2982
2972
  const proof = verifyDirectDoorProof({ jwt: hdr("x-approval-principal-token"), mac: hdr("x-approval-mac"), kid: hdr("x-approval-mac-kid") }, { sessionId: run.sessionId, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config, { actionBinding: false });
2983
2973
  if (!proof.ok) {
2984
- sendJson(res, proof.status, { error: proof.error, errorCode: proof.errorCode });
2974
+ sendError(res, proof.status, proof.errorCode, proof.error);
2985
2975
  return;
2986
2976
  }
2987
2977
  deciderPrincipal = proof.principal;
@@ -3019,7 +3009,7 @@ export function createHttpServer(deps) {
3019
3009
  return;
3020
3010
  }
3021
3011
  if (typeof body.reason === "string" && body.reason.length > MAX_APPROVAL_REASON_CHARS) {
3022
- sendJson(res, 413, { error: `reason too large (max ${MAX_APPROVAL_REASON_CHARS} chars)`, errorCode: "reason_too_large" });
3012
+ sendError(res, 413, "reason_too_large", `reason too large (max ${MAX_APPROVAL_REASON_CHARS} chars)`);
3023
3013
  return;
3024
3014
  }
3025
3015
  let remember = false;
@@ -3033,7 +3023,7 @@ export function createHttpServer(deps) {
3033
3023
  return;
3034
3024
  }
3035
3025
  if (deps.config.directDoorActive) {
3036
- sendJson(res, 400, { error: "remember is not supported on a direct-door worker (not covered by the decision proof)", errorCode: "remember_not_in_proof" });
3026
+ sendError(res, 400, "remember_not_in_proof", "remember is not supported on a direct-door worker (not covered by the decision proof)");
3037
3027
  return;
3038
3028
  }
3039
3029
  if (!deps.approvalExemptionStore) {
@@ -3041,7 +3031,7 @@ export function createHttpServer(deps) {
3041
3031
  return;
3042
3032
  }
3043
3033
  if (!body.checkpointToken && !body.boundCallId) {
3044
- sendJson(res, 400, { error: "remember requires the decision binding (echo checkpointToken and/or boundCallId from the pending approval)", errorCode: "remember_requires_binding" });
3034
+ sendError(res, 400, "remember_requires_binding", "remember requires the decision binding (echo checkpointToken and/or boundCallId from the pending approval)");
3045
3035
  return;
3046
3036
  }
3047
3037
  remember = true;
@@ -3061,10 +3051,7 @@ export function createHttpServer(deps) {
3061
3051
  }
3062
3052
  }
3063
3053
  if (deps.config.directDoorActive && body.updatedInput !== undefined) {
3064
- sendJson(res, 400, {
3065
- error: "updatedInput is not supported on a direct-door worker (not covered by the decision proof)",
3066
- errorCode: "updated_input_not_in_proof",
3067
- });
3054
+ sendError(res, 400, "updated_input_not_in_proof", "updatedInput is not supported on a direct-door worker (not covered by the decision proof)");
3068
3055
  return;
3069
3056
  }
3070
3057
  const binding = {
@@ -3081,7 +3068,7 @@ export function createHttpServer(deps) {
3081
3068
  };
3082
3069
  const proof = verifyDirectDoorProof({ jwt: hdr("x-approval-principal-token"), mac: hdr("x-approval-mac"), kid: hdr("x-approval-mac-kid") }, { sessionId, boundCallId: binding.boundCallId, boundInputHash: binding.boundInputHash, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config);
3083
3070
  if (!proof.ok) {
3084
- sendJson(res, proof.status, { error: proof.error, errorCode: proof.errorCode });
3071
+ sendError(res, proof.status, proof.errorCode, proof.error);
3085
3072
  return;
3086
3073
  }
3087
3074
  deciderPrincipal = proof.principal;
@@ -3133,7 +3120,7 @@ export function createHttpServer(deps) {
3133
3120
  if (deps.approvalStore && url.startsWith("/v1/approvals")) {
3134
3121
  const store = deps.approvalStore;
3135
3122
  if (deps.config.directApprovalDoor) {
3136
- sendJson(res, 503, { error: "approvals are served by the direct door on this worker", errorCode: "approval_door_unconfigured" });
3123
+ sendError(res, 503, "approval_door_unconfigured", "approvals are served by the direct door on this worker");
3137
3124
  return;
3138
3125
  }
3139
3126
  if (req.method === "GET" && url === "/v1/approvals") {
@@ -3291,11 +3278,11 @@ export function createHttpServer(deps) {
3291
3278
  scope = decodeURIComponent(url.slice("/v1/memory/sync/".length));
3292
3279
  }
3293
3280
  catch {
3294
- sendJson(res, 400, { error: "malformed percent-encoding in :scope", code: "memory_sync_invalid_scope", errorCode: "memory_sync_invalid_scope" });
3281
+ sendError(res, 400, "memory_sync_invalid_scope", "malformed percent-encoding in :scope", { code: "memory_sync_invalid_scope" });
3295
3282
  return;
3296
3283
  }
3297
3284
  if (scope.length === 0) {
3298
- sendJson(res, 400, { error: "missing :scope", code: "memory_sync_invalid_scope", errorCode: "memory_sync_invalid_scope" });
3285
+ sendError(res, 400, "memory_sync_invalid_scope", "missing :scope", { code: "memory_sync_invalid_scope" });
3299
3286
  return;
3300
3287
  }
3301
3288
  if (scope !== formatUserScope(principal) && !explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
@@ -3305,7 +3292,7 @@ export function createHttpServer(deps) {
3305
3292
  const body = await readJson(req);
3306
3293
  const parsed = parseMemorySyncRequest(body, scope);
3307
3294
  if (!parsed.ok) {
3308
- sendJson(res, 422, { error: parsed.error, code: "memory_sync_invalid_body", errorCode: "memory_sync_invalid_body" });
3295
+ sendError(res, 422, "memory_sync_invalid_body", parsed.error, { code: "memory_sync_invalid_body" });
3309
3296
  return;
3310
3297
  }
3311
3298
  try {
@@ -3931,7 +3918,7 @@ export function createHttpServer(deps) {
3931
3918
  }
3932
3919
  catch (e) {
3933
3920
  if (e instanceof SessionPolicyError) {
3934
- sendJson(res, e.code === "conflict" ? 409 : 403, { error: e.message, errorCode: e.code });
3921
+ sendError(res, e.code === "conflict" ? 409 : 403, e.code, e.message);
3935
3922
  return;
3936
3923
  }
3937
3924
  throw e;
@@ -4047,7 +4034,7 @@ export function createHttpServer(deps) {
4047
4034
  if (typeof fs.blobSizes === "function") {
4048
4035
  const known = (await fs.blobSizes([hash])).get(hash);
4049
4036
  if (known !== undefined && known > cap) {
4050
- 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 });
4037
+ sendError(res, 413, "workspace_file_too_large", "file exceeds the single-file read limit — download it via the archive endpoint", { code: "workspace_file_too_large", sizeBytes: known, limit: cap });
4051
4038
  return;
4052
4039
  }
4053
4040
  }
@@ -4057,7 +4044,7 @@ export function createHttpServer(deps) {
4057
4044
  return;
4058
4045
  }
4059
4046
  if (bytes.byteLength > cap) {
4060
- 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 });
4047
+ sendError(res, 413, "workspace_file_too_large", "file exceeds the single-file read limit — download it via the archive endpoint", { code: "workspace_file_too_large", sizeBytes: bytes.byteLength, limit: cap });
4061
4048
  return;
4062
4049
  }
4063
4050
  const ct = workspaceContentType(pathParam);
@@ -4260,11 +4247,7 @@ export function createHttpServer(deps) {
4260
4247
  }
4261
4248
  const body = await readRawBody(req, SYNC_BLOB_MAX_BYTES);
4262
4249
  if (deps.snapshotBlobSqlCapBytes !== undefined && body.byteLength > deps.snapshotBlobSqlCapBytes) {
4263
- sendJson(res, 413, {
4264
- code: "blob_too_large_for_sql",
4265
- errorCode: "blob_too_large_for_sql",
4266
- error: `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`,
4267
- });
4250
+ 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`, { code: "blob_too_large_for_sql" });
4268
4251
  return;
4269
4252
  }
4270
4253
  if (createHash("sha256").update(body).digest("hex") !== hash) {
@@ -4273,7 +4256,7 @@ export function createHttpServer(deps) {
4273
4256
  }
4274
4257
  const put = await fs.putBlob(hash, new Uint8Array(body));
4275
4258
  if (!put.ok) {
4276
- sendJson(res, 502, { error: "blob store write failed", code: put.error.code, errorCode: put.error.code });
4259
+ sendError(res, 502, put.error.code, "blob store write failed", { code: put.error.code });
4277
4260
  return;
4278
4261
  }
4279
4262
  res.writeHead(204).end();
@@ -4325,7 +4308,7 @@ export function createHttpServer(deps) {
4325
4308
  const dstIds = (await deps.sessionStorage.listEntryIds?.(sessionId)) ?? null;
4326
4309
  const relA = classifySyncRelationshipByIds(entryIds, dstIds);
4327
4310
  if ((relA.relation === "fork" || relA.relation === "stale") && resolution !== "overwrite-dst") {
4328
- sendJson(res, 409, { error: "sync conflict", errorCode: "conflict", relation: relA });
4311
+ sendError(res, 409, "conflict", "sync conflict", { relation: relA });
4329
4312
  return;
4330
4313
  }
4331
4314
  let payloadVerified = false;
@@ -4349,7 +4332,7 @@ export function createHttpServer(deps) {
4349
4332
  for (const snap of snapshots) {
4350
4333
  const r = await fss.importManifest(sessionId, snap.key, new Map(snap.manifest), getBlobA);
4351
4334
  if (!r.ok) {
4352
- sendJson(res, 422, { error: `snapshot import failed (${r.error.code})`, errorCode: r.error.code === "restore_failed" ? "restore_failed" : "missing_blob" });
4335
+ sendError(res, 422, r.error.code === "restore_failed" ? "restore_failed" : "missing_blob", `snapshot import failed (${r.error.code})`);
4353
4336
  return;
4354
4337
  }
4355
4338
  }
@@ -4371,7 +4354,7 @@ export function createHttpServer(deps) {
4371
4354
  }
4372
4355
  const activeTaskId = await deps.runStore?.getActiveTaskId(sessionId);
4373
4356
  if (activeTaskId) {
4374
- sendJson(res, 409, { error: "session has an active run; sync after it settles", errorCode: "session_active", activeTaskId });
4357
+ sendError(res, 409, "session_active", "session has an active run; sync after it settles", { activeTaskId });
4375
4358
  return;
4376
4359
  }
4377
4360
  const heldLease = importLeases.get(sessionId);
@@ -4387,12 +4370,8 @@ export function createHttpServer(deps) {
4387
4370
  }
4388
4371
  else {
4389
4372
  const retryAfterSec = staleMs > 0 ? Math.max(1, Math.ceil((staleMs - ageMs) / 1000)) : undefined;
4390
- sendJson(res, 409, {
4391
- error: "an import is already in flight for this session"
4392
- + (retryAfterSec !== undefined ? ` (a stalled holder is taken over after ${staleSec}s of inactivity; retry in ~${retryAfterSec}s)` : ""),
4393
- errorCode: "import_in_flight",
4394
- ...(retryAfterSec !== undefined ? { retryAfterSec } : {}),
4395
- });
4373
+ sendError(res, 409, "import_in_flight", "an import is already in flight for this session"
4374
+ + (retryAfterSec !== undefined ? ` (a stalled holder is taken over after ${staleSec}s of inactivity; retry in ~${retryAfterSec}s)` : ""), retryAfterSec !== undefined ? { retryAfterSec } : undefined);
4396
4375
  return;
4397
4376
  }
4398
4377
  }
@@ -4408,14 +4387,14 @@ export function createHttpServer(deps) {
4408
4387
  const present = await fss.hasBlobs(all);
4409
4388
  const missing = all.find((h) => !present.has(h));
4410
4389
  if (missing !== undefined) {
4411
- sendJson(res, 422, { error: "missing blob; PUT all referenced blobs before import", errorCode: "missing_blob" });
4390
+ sendError(res, 422, "missing_blob", "missing blob; PUT all referenced blobs before import");
4412
4391
  return;
4413
4392
  }
4414
4393
  }
4415
4394
  else {
4416
4395
  for (const hash of all) {
4417
4396
  if (!(await deps.fileSnapshotStore.getBlob(hash))) {
4418
- sendJson(res, 422, { error: "missing blob; PUT all referenced blobs before import", errorCode: "missing_blob" });
4397
+ sendError(res, 422, "missing_blob", "missing blob; PUT all referenced blobs before import");
4419
4398
  return;
4420
4399
  }
4421
4400
  }
@@ -4521,13 +4500,13 @@ export function createHttpServer(deps) {
4521
4500
  if (e instanceof PrecheckError) {
4522
4501
  await pending.handle.abort().catch(() => undefined);
4523
4502
  cleanupStaging(stagingId);
4524
- sendJson(res, e.status, { error: e.message, errorCode: "invalid_entries" });
4503
+ sendError(res, e.status, "invalid_entries", e.message);
4525
4504
  return;
4526
4505
  }
4527
4506
  if (e instanceof SessionError && e.code === "invalid_session") {
4528
4507
  await pending.handle.abort().catch(() => undefined);
4529
4508
  cleanupStaging(stagingId);
4530
- sendJson(res, 422, { error: "staged session log failed validation", errorCode: "invalid_entries" });
4509
+ sendError(res, 422, "invalid_entries", "staged session log failed validation");
4531
4510
  return;
4532
4511
  }
4533
4512
  await pending.handle.abort().catch(() => undefined);
@@ -4538,7 +4517,7 @@ export function createHttpServer(deps) {
4538
4517
  if (activeAtCommit) {
4539
4518
  await pending.handle.abort().catch(() => undefined);
4540
4519
  cleanupStaging(stagingId);
4541
- sendJson(res, 409, { error: "session started an active run while the import streamed; sync after it settles", errorCode: "session_active", activeTaskId: activeAtCommit });
4520
+ sendError(res, 409, "session_active", "session started an active run while the import streamed; sync after it settles", { activeTaskId: activeAtCommit });
4542
4521
  return;
4543
4522
  }
4544
4523
  try {
@@ -4548,7 +4527,7 @@ export function createHttpServer(deps) {
4548
4527
  for (const snap of pending.snapshots) {
4549
4528
  const r = await importManifest(sessionId, snap.key, new Map(snap.manifest), getBlob);
4550
4529
  if (!r.ok) {
4551
- sendJson(res, 422, { error: `snapshot import failed (${r.error.code})`, errorCode: r.error.code === "restore_failed" ? "restore_failed" : "missing_blob" });
4530
+ sendError(res, 422, r.error.code === "restore_failed" ? "restore_failed" : "missing_blob", `snapshot import failed (${r.error.code})`);
4552
4531
  cleanupStaging(stagingId);
4553
4532
  return;
4554
4533
  }
@@ -4570,12 +4549,12 @@ export function createHttpServer(deps) {
4570
4549
  if (e instanceof SyncConflictError) {
4571
4550
  await pending.handle.abort().catch(() => undefined);
4572
4551
  cleanupStaging(stagingId);
4573
- sendJson(res, 409, { error: "sync conflict", errorCode: "conflict", relation: e.relation });
4552
+ sendError(res, 409, "conflict", "sync conflict", { relation: e.relation });
4574
4553
  return;
4575
4554
  }
4576
4555
  if (e instanceof SessionPolicyError) {
4577
4556
  cleanupStaging(stagingId);
4578
- sendJson(res, e.code === "loosen_forbidden" ? 403 : 409, { error: e.message, errorCode: e.code });
4557
+ sendError(res, e.code === "loosen_forbidden" ? 403 : 409, e.code, e.message);
4579
4558
  return;
4580
4559
  }
4581
4560
  cleanupStaging(stagingId);
@@ -4640,7 +4619,7 @@ export function createHttpServer(deps) {
4640
4619
  }
4641
4620
  const allow = deps.config.attachmentMimeAllowlist;
4642
4621
  if (allow && !allow.some((a) => a === mime || (a.endsWith("/*") && mime.startsWith(a.slice(0, -1))))) {
4643
- 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 });
4622
+ sendError(res, 415, "attachment.mime_not_allowed", `mime '${mime}' is not in this deployment's allowlist`, { code: "attachment.mime_not_allowed", allowed: allow });
4644
4623
  return;
4645
4624
  }
4646
4625
  const max = deps.config.attachmentMaxBytes ?? 32 * 1024 * 1024;
@@ -4650,7 +4629,7 @@ export function createHttpServer(deps) {
4650
4629
  }
4651
4630
  catch (err) {
4652
4631
  if (err instanceof HttpError && err.status === 413) {
4653
- sendJson(res, 413, { code: "attachment.too_large", errorCode: "attachment.too_large", error: `attachment exceeds the per-file limit (${max} bytes)`, maxBytes: max });
4632
+ sendError(res, 413, "attachment.too_large", `attachment exceeds the per-file limit (${max} bytes)`, { code: "attachment.too_large", maxBytes: max });
4654
4633
  return;
4655
4634
  }
4656
4635
  throw err;
@@ -4772,7 +4751,7 @@ export function createHttpServer(deps) {
4772
4751
  catch (e) {
4773
4752
  const code = e.code;
4774
4753
  if (typeof code === "string" && code.startsWith("notify.")) {
4775
- sendJson(res, 400, { errorCode: code, error: e instanceof Error ? e.message : "invalid notification" });
4754
+ sendError(res, 400, code, e instanceof Error ? e.message : "invalid notification");
4776
4755
  return;
4777
4756
  }
4778
4757
  }
@@ -5902,7 +5881,7 @@ export function createHttpServer(deps) {
5902
5881
  return false;
5903
5882
  deps.metrics?.inc("fleet_lease_rejected_total");
5904
5883
  res.setHeader("retry-after", String(adm.retryAfterSec));
5905
- sendJson(res, 429, { error: "budget exhausted (quota lease)", code: "quota_exhausted", errorCode: "quota_exhausted", retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
5884
+ sendError(res, 429, "quota_exhausted", "budget exhausted (quota lease)", { code: "quota_exhausted", retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
5906
5885
  return true;
5907
5886
  }
5908
5887
  function rateLimited(req, res) {
@@ -6879,6 +6858,9 @@ function sendJson(res, status, body) {
6879
6858
  res.writeHead(status, { "content-type": "application/json" });
6880
6859
  res.end(JSON.stringify(body));
6881
6860
  }
6861
+ function sendError(res, status, errorCode, message, extra) {
6862
+ sendJson(res, status, { error: message, errorCode, ...extra });
6863
+ }
6882
6864
  function msg(e) {
6883
6865
  return e instanceof Error ? e.message : String(e);
6884
6866
  }
@@ -1,6 +1,6 @@
1
1
  import type { Model } from "@sema-agent/core";
2
2
  import { type SealedKeyPoison } from "./sealed-key.js";
3
- export declare function buildKeyResolver(modelApiKeyEnv: Record<string, string>, env?: NodeJS.ProcessEnv, modelApiKeys?: Record<string, string | SealedKeyPoison>): ((model: Model) => Promise<{
3
+ export declare function createKeyResolver(modelApiKeyEnv: Record<string, string>, env?: NodeJS.ProcessEnv, modelApiKeys?: Record<string, string | SealedKeyPoison>): ((model: Model) => Promise<{
4
4
  apiKey: string;
5
5
  } | undefined>) | undefined;
6
6
  //# sourceMappingURL=key-resolver.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { isSealedKeyPoison, SealedKeyPoisonedError } from "./sealed-key.js";
2
- export function buildKeyResolver(modelApiKeyEnv, env = process.env, modelApiKeys = {}) {
2
+ export function createKeyResolver(modelApiKeyEnv, env = process.env, modelApiKeys = {}) {
3
3
  if (Object.keys(modelApiKeyEnv).length === 0 && Object.keys(modelApiKeys).length === 0)
4
4
  return undefined;
5
5
  return async (model) => {
@@ -32,5 +32,5 @@ export interface MintedGrader extends GraderHandle {
32
32
  immutableOraclePaths: string[];
33
33
  destroy: () => Promise<void>;
34
34
  }
35
- export declare function buildGraderEnvFactory(_cfg: GraderEnvFactoryCfg): () => Promise<MintedGrader>;
35
+ export declare function createGraderEnvFactory(_cfg: GraderEnvFactoryCfg): () => Promise<MintedGrader>;
36
36
  //# sourceMappingURL=grader-env-factory.d.ts.map
@@ -63,9 +63,9 @@ export function changedFilesOutOfAllowlist(changedFiles, allowlist) {
63
63
  export function changedFilesWithinAllowlist(changedFiles, allowlist) {
64
64
  return changedFilesOutOfAllowlist(changedFiles, allowlist).length === 0;
65
65
  }
66
- export function buildGraderEnvFactory(_cfg) {
66
+ export function createGraderEnvFactory(_cfg) {
67
67
  return async () => {
68
- throw new Error("buildGraderEnvFactory: the standalone control-plane grader is the auto-accept slice (LEADER_GRADER_FACTORY) — " +
68
+ throw new Error("createGraderEnvFactory: the standalone control-plane grader is the auto-accept slice (LEADER_GRADER_FACTORY) — " +
69
69
  "NOT wired in v1 (which reuses the integration sandbox for R1 and re-seeds it for R2). " +
70
70
  "See docs/LEADER-REPAIRLOOP-INTEGRATION.md §3.2/§7.");
71
71
  };
@@ -61,6 +61,6 @@ export declare function leaderResourceConfig(env?: NodeJS.ProcessEnv): {
61
61
  maxSuspends: number;
62
62
  };
63
63
  };
64
- export declare function buildLeaderRunner(cfg: LeaderWireConfig): (body: LeaderRequestBody) => Promise<LeaderResult>;
64
+ export declare function createLeaderRunner(cfg: LeaderWireConfig): (body: LeaderRequestBody) => Promise<LeaderResult>;
65
65
  export {};
66
66
  //# sourceMappingURL=wire.d.ts.map
@@ -63,7 +63,7 @@ export function leaderResourceConfig(env = process.env) {
63
63
  : {}),
64
64
  };
65
65
  }
66
- export function buildLeaderRunner(cfg) {
66
+ export function createLeaderRunner(cfg) {
67
67
  const W = cfg.workspace ?? "/home/user";
68
68
  const repo = `${W}/repo`;
69
69
  const ident = cfg.git ?? { name: "leader", email: "leader@local" };