@sema-agent/server 7.103.2 → 7.105.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 (204) hide show
  1. package/MIGRATION.md +378 -2
  2. package/README.md +6 -1
  3. package/README.zh-CN.md +5 -1
  4. package/USAGE.md +320 -16
  5. package/deploy/sema-up/README.md +57 -7
  6. package/deploy/sema-up/cluster-up.sh +18 -4
  7. package/deploy/sema-up/kube-up.sh +16 -0
  8. package/deploy/sema-up/lib/dataroot.sh +142 -0
  9. package/deploy/sema-up/lib/flagshape.sh +69 -0
  10. package/deploy/sema-up/lib/lanes.py +14 -4
  11. package/deploy/sema-up/lib/shell-access.sh +41 -0
  12. package/deploy/sema-up/preflight.sh +14 -1
  13. package/deploy/sema-up/sema-up.sh +134 -37
  14. package/deploy/sema-up/smoke.sh +5 -0
  15. package/dist/auto-mode-face.js +3 -4
  16. package/dist/boot/budget-tracing.js +1 -1
  17. package/dist/boot/config-center.d.ts +6 -2
  18. package/dist/boot/config-center.js +5 -59
  19. package/dist/boot/execution-env.d.ts +11 -0
  20. package/dist/boot/execution-env.js +14 -0
  21. package/dist/boot/execution-lane.d.ts +25 -0
  22. package/dist/boot/execution-lane.js +95 -0
  23. package/dist/boot/memory-consolidation.d.ts +185 -9
  24. package/dist/boot/memory-consolidation.js +192 -32
  25. package/dist/boot/resolve-spec.d.ts +2 -0
  26. package/dist/boot/resolve-spec.js +28 -24
  27. package/dist/boot/runner-deps.d.ts +6 -2
  28. package/dist/boot/runner-deps.js +6 -1
  29. package/dist/boot/session-faces.d.ts +5 -0
  30. package/dist/boot/session-faces.js +18 -2
  31. package/dist/boot/session-incarnation-seat.d.ts +41 -0
  32. package/dist/boot/session-incarnation-seat.js +69 -0
  33. package/dist/boot/stage-01-config.js +2 -2
  34. package/dist/boot/stage-02-registry-pull.js +3 -1
  35. package/dist/boot/stage-04-stores.d.ts +2 -0
  36. package/dist/boot/stage-04-stores.js +3 -1
  37. package/dist/boot/stage-05-execution-env.d.ts +4 -0
  38. package/dist/boot/stage-05-execution-env.js +2 -2
  39. package/dist/boot/stage-06-runners.d.ts +4 -1
  40. package/dist/boot/stage-06-runners.js +7 -14
  41. package/dist/boot/stage-07-capability-layer.d.ts +4 -1
  42. package/dist/boot/stage-08-reapers.d.ts +4 -1
  43. package/dist/boot/stage-09-leader.d.ts +6 -1
  44. package/dist/boot/stage-09-leader.js +2 -1
  45. package/dist/boot/stage-10-http-server.d.ts +6 -1
  46. package/dist/boot/stage-10-http-server.js +4 -1
  47. package/dist/brain.d.ts +1 -1
  48. package/dist/config-catalog.d.ts +16 -1
  49. package/dist/config-catalog.js +9 -9
  50. package/dist/config-center/restart-signal.d.ts +4 -4
  51. package/dist/config-invariants.d.ts +7 -1
  52. package/dist/config-invariants.js +37 -6
  53. package/dist/config-provider.d.ts +42 -10
  54. package/dist/config-provider.js +18 -16
  55. package/dist/config-types.d.ts +37 -16
  56. package/dist/config-types.js +1 -0
  57. package/dist/config.d.ts +28 -30
  58. package/dist/config.js +95 -77
  59. package/dist/env-facts.d.ts +13 -3
  60. package/dist/env-facts.js +15 -3
  61. package/dist/execution-lane-caps.d.ts +15 -0
  62. package/dist/execution-lane-caps.js +8 -0
  63. package/dist/file-history-capture.d.ts +42 -35
  64. package/dist/file-history-capture.js +15 -10
  65. package/dist/file-history-disclosure.d.ts +7 -31
  66. package/dist/file-history-disclosure.js +12 -0
  67. package/dist/fleet/fleet-bus.d.ts +5 -1
  68. package/dist/http/admission.d.ts +12 -5
  69. package/dist/http/admission.js +5 -1
  70. package/dist/http/dispatch.d.ts +18 -0
  71. package/dist/http/dispatch.js +19 -6
  72. package/dist/http/principal-gate.d.ts +22 -0
  73. package/dist/http/principal-gate.js +20 -0
  74. package/dist/http/resume-legs.d.ts +1 -1
  75. package/dist/http/resume-legs.js +9 -5
  76. package/dist/http/route-table.d.ts +15 -4
  77. package/dist/http/route-table.js +6 -5
  78. package/dist/http/routes/agents-roster.d.ts +7 -0
  79. package/dist/http/routes/agents-roster.js +6 -2
  80. package/dist/http/routes/approvals-assistant.d.ts +48 -19
  81. package/dist/http/routes/approvals-assistant.js +117 -50
  82. package/dist/http/routes/capabilities.js +3 -2
  83. package/dist/http/routes/config-catalog.js +1 -0
  84. package/dist/http/routes/fleet.d.ts +3 -2
  85. package/dist/http/routes/images.js +0 -1
  86. package/dist/http/routes/memory-consolidation.d.ts +3 -2
  87. package/dist/http/routes/memory-policy.js +3 -3
  88. package/dist/http/routes/rules.js +2 -2
  89. package/dist/http/routes/runs.js +29 -10
  90. package/dist/http/routes/session-sync.js +14 -15
  91. package/dist/http/routes/sessions-list.js +3 -2
  92. package/dist/http/routes/sessions.js +49 -25
  93. package/dist/http/routes/shared-memory.js +2 -2
  94. package/dist/http/routes/side-query.js +8 -1
  95. package/dist/http/routes/tasks.js +22 -19
  96. package/dist/http/routes/trace-usage.d.ts +2 -0
  97. package/dist/http/routes/trace-usage.js +8 -13
  98. package/dist/http/send.d.ts +12 -0
  99. package/dist/http/send.js +12 -1
  100. package/dist/http/server.d.ts +30 -8
  101. package/dist/http/server.js +11 -5
  102. package/dist/http/sse-lifecycle.d.ts +1 -1
  103. package/dist/http/sse-log.d.ts +12 -9
  104. package/dist/http/sse-log.js +1 -8
  105. package/dist/http/sync-submit-body.d.ts +43 -0
  106. package/dist/http/sync-submit-body.js +4 -0
  107. package/dist/http/wire-types.d.ts +23 -5
  108. package/dist/index.d.ts +1 -0
  109. package/dist/index.js +1 -0
  110. package/dist/leader/merge.d.ts +8 -3
  111. package/dist/memory-incident-sink.js +2 -1
  112. package/dist/model-select.d.ts +9 -0
  113. package/dist/model-select.js +5 -1
  114. package/dist/observability/corrupt-read-seat.d.ts +151 -16
  115. package/dist/observability/corrupt-read-seat.js +158 -15
  116. package/dist/observability/fail-open.d.ts +17 -1
  117. package/dist/observability/fail-open.js +17 -1
  118. package/dist/observability/metrics.js +1 -1
  119. package/dist/observability/run-terminal.d.ts +60 -10
  120. package/dist/observability/run-terminal.js +119 -9
  121. package/dist/parked-decide.d.ts +8 -3
  122. package/dist/parked-decide.js +56 -20
  123. package/dist/parked-revive-surface.d.ts +14 -7
  124. package/dist/parked-revive-surface.js +5 -1
  125. package/dist/plugins/approval-ask-store-file.d.ts +4 -1
  126. package/dist/plugins/approval-ask-store-file.js +9 -12
  127. package/dist/plugins/approval-ask-store-sql.d.ts +4 -0
  128. package/dist/plugins/approval-ask-store-sql.js +4 -1
  129. package/dist/plugins/approval-exemption-store.d.ts +21 -1
  130. package/dist/plugins/approval-exemption-store.js +36 -7
  131. package/dist/plugins/approval-nonce-store.d.ts +16 -2
  132. package/dist/plugins/approval-nonce-store.js +17 -7
  133. package/dist/plugins/caching-session-store.d.ts +5 -0
  134. package/dist/plugins/caching-session-store.js +3 -0
  135. package/dist/plugins/file-resume-anchor-store.d.ts +7 -2
  136. package/dist/plugins/file-resume-anchor-store.js +19 -5
  137. package/dist/plugins/file-run-store.d.ts +21 -8
  138. package/dist/plugins/file-run-store.js +60 -38
  139. package/dist/plugins/image-bake-store-sql.d.ts +0 -2
  140. package/dist/plugins/image-bake-store-sql.js +0 -5
  141. package/dist/plugins/local-session-store.d.ts +22 -0
  142. package/dist/plugins/local-session-store.js +74 -11
  143. package/dist/plugins/memory-run-store.d.ts +7 -5
  144. package/dist/plugins/memory-run-store.js +11 -4
  145. package/dist/plugins/one-time-migrations.d.ts +38 -1
  146. package/dist/plugins/one-time-migrations.js +143 -15
  147. package/dist/plugins/permission-rule-store-file.d.ts +9 -2
  148. package/dist/plugins/permission-rule-store-file.js +37 -51
  149. package/dist/plugins/pg-session-storage.d.ts +3 -0
  150. package/dist/plugins/pg-session-storage.js +19 -10
  151. package/dist/plugins/retention-store-sql.js +1 -0
  152. package/dist/plugins/run-store-sql.d.ts +15 -6
  153. package/dist/plugins/run-store-sql.js +15 -13
  154. package/dist/plugins/schema-indexes.js +1 -0
  155. package/dist/plugins/send-file-ledger.d.ts +5 -1
  156. package/dist/plugins/send-file-ledger.js +11 -6
  157. package/dist/plugins/session-event-payload.d.ts +14 -4
  158. package/dist/plugins/session-event-payload.js +5 -2
  159. package/dist/plugins/session-policy-store-sql.js +18 -5
  160. package/dist/plugins/store-backend.d.ts +13 -7
  161. package/dist/plugins/store-backend.js +10 -6
  162. package/dist/plugins/store-contracts.d.ts +7 -2
  163. package/dist/plugins/tidb-pool.js +12 -5
  164. package/dist/plugins/tidb-session-store.d.ts +6 -1
  165. package/dist/plugins/tidb-session-store.js +17 -10
  166. package/dist/project-identity.d.ts +4 -6
  167. package/dist/read-face-posture.d.ts +21 -0
  168. package/dist/read-face-posture.js +9 -0
  169. package/dist/request-key-closure.js +1 -1
  170. package/dist/request-value-closure.d.ts +72 -3
  171. package/dist/request-value-closure.js +12 -4
  172. package/dist/roster-bound-http-status.d.ts +6 -0
  173. package/dist/roster-bound-http-status.js +6 -0
  174. package/dist/rules-consent.js +0 -11
  175. package/dist/run-local.js +22 -27
  176. package/dist/runs.d.ts +22 -2
  177. package/dist/runs.js +29 -7
  178. package/dist/sealed-key.d.ts +31 -4
  179. package/dist/sealed-key.js +9 -0
  180. package/dist/security.d.ts +5 -1
  181. package/dist/session-children-retire.d.ts +92 -0
  182. package/dist/session-children-retire.js +85 -0
  183. package/dist/session-error-wire.d.ts +58 -0
  184. package/dist/session-error-wire.js +38 -0
  185. package/dist/session-incarnation.d.ts +61 -0
  186. package/dist/session-incarnation.js +43 -0
  187. package/dist/session-policy-wire.d.ts +98 -12
  188. package/dist/session-policy-wire.js +71 -8
  189. package/dist/session-records-skipped-notice.d.ts +20 -0
  190. package/dist/session-records-skipped-notice.js +27 -0
  191. package/dist/tool-approval.d.ts +65 -0
  192. package/dist/tool-approval.js +36 -9
  193. package/dist/trace/core-keyset-guard.d.ts +8 -7
  194. package/dist/trace/engine-notice-wire.d.ts +3 -1
  195. package/dist/trace/engine-notice-wire.js +2 -0
  196. package/dist/trace/project.d.ts +11 -6
  197. package/dist/trace/project.js +12 -3
  198. package/dist/trace/redact.d.ts +7 -0
  199. package/dist/trace/redact.js +153 -61
  200. package/dist/trace/wire-projection-faces.d.ts +10 -3
  201. package/dist/trace/wire-projection-faces.js +8 -2
  202. package/package.json +2 -2
  203. package/dist/plugins/memory-session-policy-store.d.ts +0 -46
  204. package/dist/plugins/memory-session-policy-store.js +0 -52
@@ -1,6 +1,5 @@
1
1
  import { persistedPlaneOf } from "../../terminal.js";
2
- import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventData, redactTerminalResult } from "../../observability/run-terminal.js";
3
- import { isModelTextLedgerEventType } from "../../trace/ledger-events.js";
2
+ import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventForRead, redactTerminalResult } from "../../observability/run-terminal.js";
4
3
  import { z } from "zod";
5
4
  import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS } from "@sema-agent/core";
6
5
  import { isTerminalRunStatus, isParkedRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
@@ -16,7 +15,7 @@ import { scopedIdempotencyKey } from "../idempotency.js";
16
15
  import { streamSseLog } from "../sse-log.js";
17
16
  import { bindSseLifecycle } from "../sse-lifecycle.js";
18
17
  import { buildApprovalPreamble, buildApprovalPreambleSseFrames } from "../../approval-card.js";
19
- import { ASK_DECISION_CONSUMER_ABSENT, outcomeOfDecidedRow, resolveStreamApprovalGate } from "../../tool-approval.js";
18
+ import { ASK_DECISION_CONSUMER_ABSENT, SETTLED_BY_DENY_ONLY, SETTLED_BY_MUST_BE_STRING, SETTLED_BY_TOO_WIDE, judgeSettledByBody, outcomeOfDecidedRow, resolveStreamApprovalGate, respondSettledBy } from "../../tool-approval.js";
20
19
  import { MAX_DECISION_NOTE_CHARS } from "../../approval-ask-machine.js";
21
20
  import { normalizeRunEventType, screenedStoredEventData } from "../../trace/project.js";
22
21
  import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
@@ -45,6 +44,7 @@ const AskDecisionBodySchema = z
45
44
  .refine((k) => k.trim() === k, { message: "idempotency key must not have leading/trailing whitespace" })
46
45
  .optional(),
47
46
  actor: z.object({ label: z.string().max(ASK_ACTOR_LABEL_MAX) }).strict().optional(),
47
+ settledBy: z.unknown().optional(),
48
48
  })
49
49
  .strict();
50
50
  const ASK_DECISION_CONFIRM_TIMEOUT_MS = 2_000;
@@ -125,14 +125,9 @@ async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
125
125
  return run ? { status: run.status, updatedAt: run.updatedAt } : undefined;
126
126
  },
127
127
  getEvents: (id, after) => runStore.getEvents(id, after),
128
- retainedFrom: (id) => runStore.retainedFrom(id),
129
128
  formatEvent: (ev) => {
130
129
  const type = normalizeRunEventType(ev.type);
131
- const redacted = type === "done" && ev.data && typeof ev.data === "object" && ev.data.result && typeof ev.data.result === "object"
132
- ? { ...ev.data, result: memoRedactTerminalResult("ev.done.result", ev.data.result) }
133
- : type === "failed" ? memoRedactedRead("ev.failed", ev.data, (d) => redactLedgerEventData(type, d))
134
- : isModelTextLedgerEventType(type) ? memoRedactedRead("ev.model-text", ev.data, (d) => redactLedgerEventData(type, d))
135
- : redactLedgerEventData(type, ev.data);
130
+ const redacted = redactLedgerEventForRead(type, ev.data);
136
131
  const data = screenedStoredEventData(type, redacted);
137
132
  return { id: ev.seq, event: type, data: { type, ...(data ?? {}) } };
138
133
  },
@@ -1287,10 +1282,33 @@ export async function handleRunVerbs(req, res, match, ctx) {
1287
1282
  }
1288
1283
  const parsedBody = AskDecisionBodySchema.safeParse(rawBody);
1289
1284
  if (!parsedBody.success) {
1290
- sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label");
1285
+ sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / settledBy");
1291
1286
  return;
1292
1287
  }
1293
1288
  const body = parsedBody.data;
1289
+ const settledByVerdict = judgeSettledByBody(body.settledBy, body.decision, deps.config.directDoorActive);
1290
+ switch (settledByVerdict.kind) {
1291
+ case "not_in_proof":
1292
+ sendError(res, 400, "settled_by_not_in_proof", "settledBy is not supported on a direct-door worker (not covered by the decision proof)");
1293
+ return;
1294
+ case "deny_only":
1295
+ sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
1296
+ return;
1297
+ case "not_string":
1298
+ sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
1299
+ return;
1300
+ case "too_wide":
1301
+ sendError(res, 400, "request.field_invalid", SETTLED_BY_TOO_WIDE);
1302
+ return;
1303
+ case "absent":
1304
+ case "accepted":
1305
+ break;
1306
+ default: {
1307
+ const unreachable = settledByVerdict;
1308
+ throw new Error(`unhandled settledBy verdict: ${JSON.stringify(unreachable)}`);
1309
+ }
1310
+ }
1311
+ const settledBy = settledByVerdict.kind === "accepted" ? settledByVerdict.settledBy : undefined;
1294
1312
  const hasUpdatedInput = typeof rawBody === "object" && rawBody !== null && "updatedInput" in rawBody;
1295
1313
  const taskId = match.params.taskId;
1296
1314
  const askId = match.params.askId;
@@ -1424,6 +1442,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1424
1442
  decisionActor: actor,
1425
1443
  decisionNote: body.note ?? null,
1426
1444
  idempotencyKey: body.idempotencyKey ?? null,
1445
+ ...(settledBy !== undefined ? { settledBy: respondSettledBy({ settledBy }) } : {}),
1427
1446
  ...(body.decision === "approve" && hasUpdatedInput ? { updatedInput: body.updatedInput } : {}),
1428
1447
  });
1429
1448
  }
@@ -1,19 +1,19 @@
1
1
  import { once } from "node:events";
2
2
  import { randomBytes, createHash } from "node:crypto";
3
- import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator, SessionError, validateFileHistoryExport } from "@sema-agent/core";
3
+ import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator, validateFileHistoryExport } from "@sema-agent/core";
4
4
  import { StringDecoder } from "node:string_decoder";
5
5
  import { exportSessionManifest, fileHistoryImportFace, replayFileHistory, replayPolicyRecords, overwriteWipeIncapable, wipeSessionAttendants } from "../../session-sync.js";
6
6
  import { classifySyncRelationshipByIds, SyncConflictError } from "../../session-sync-kernel.js";
7
7
  import { isUuidShape } from "../../security.js";
8
- import { sendJson, sendError, sendSessionPolicyError, msg } from "../send.js";
8
+ import { sendJson, sendError, sendSessionError, sendSessionPolicyError, msg } from "../send.js";
9
9
  import { buildDisplaySafeUrl } from "../../trace/redact.js";
10
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
10
+ import { gatedPrincipal, explicitOperatorOk, sendUnauthorized } from "../principal-gate.js";
11
11
  const SYNC_BLOB_MAX_BYTES = 64 * 1024 * 1024;
12
12
  const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
13
13
  const SYNC_IMPORT_BATCH = 500;
14
14
  import { SHA256_HEX_RE } from "../../digest-form.js";
15
15
  import { assertNever } from "../route-ctx.js";
16
- import { readSessionRulesRecords, describeSessionRulesRecordProblem } from "../../session-policy-wire.js";
16
+ import { readSessionRulesRecords, describeSessionRulesRecordProblem, legacyToolNameProblemOf } from "../../session-policy-wire.js";
17
17
  export function createSessionSyncLocal() {
18
18
  const pendingStagings = new Map();
19
19
  const importLeases = new Map();
@@ -43,12 +43,12 @@ export async function handleSessionSync(req, res, match, ctx) {
43
43
  }
44
44
  const historyStore = deps.fileHistoryStore;
45
45
  if (historyStore === undefined) {
46
- sendError(res, 501, "capability.snapshot_store_required", "session sync is not available on this deployment: file-history capture is turned off (FILE_HISTORY_CAPTURE=false), so there is no file history to export or import");
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");
47
47
  return;
48
48
  }
49
49
  const scope = sessionOwnerScopeForWrite(req);
50
50
  if (!scope.fleetWide && scope.gateOwner === null) {
51
- sendError(res, 401, "auth.unauthorized", "unauthorized");
51
+ sendUnauthorized(res, "identity");
52
52
  return;
53
53
  }
54
54
  const sessionId = safeDecode(match.params.sessionId);
@@ -235,6 +235,11 @@ export async function handleSessionSync(req, res, match, ctx) {
235
235
  }
236
236
  const policyRead = readSessionRulesRecords(body.policy);
237
237
  if (policyRead.problem !== undefined) {
238
+ const legacy = legacyToolNameProblemOf(policyRead.problem);
239
+ if (legacy !== undefined) {
240
+ sendError(res, 400, "rules.legacy_tool_name", `policy: ${describeSessionRulesRecordProblem(policyRead.problem)}`, { field: legacy.field, name: legacy.name });
241
+ return;
242
+ }
238
243
  sendError(res, 400, "request.field_invalid", `policy: ${describeSessionRulesRecordProblem(policyRead.problem)}`);
239
244
  return;
240
245
  }
@@ -459,20 +464,14 @@ export async function handleSessionSync(req, res, match, ctx) {
459
464
  }
460
465
  }
461
466
  catch (e) {
467
+ await pending.handle.abort().catch(() => undefined);
468
+ cleanupStaging(stagingId);
462
469
  if (e instanceof PrecheckError) {
463
- await pending.handle.abort().catch(() => undefined);
464
- cleanupStaging(stagingId);
465
470
  sendError(res, e.status, "invalid_entries", e.message);
466
471
  return;
467
472
  }
468
- if (e instanceof SessionError && e.code === "invalid_session") {
469
- await pending.handle.abort().catch(() => undefined);
470
- cleanupStaging(stagingId);
471
- sendError(res, 422, "invalid_entries", "staged session log failed validation");
473
+ if (sendSessionError(res, e, "import"))
472
474
  return;
473
- }
474
- await pending.handle.abort().catch(() => undefined);
475
- cleanupStaging(stagingId);
476
475
  throw e;
477
476
  }
478
477
  const activeAtCommit = await deps.runStore?.getActiveTaskId(sessionId);
@@ -1,5 +1,6 @@
1
1
  import { isUuidV7, legacyUnownedAmnestyOpen } from "../../security.js";
2
2
  import { sendJson, sendError } from "../send.js";
3
+ import { sendUnauthorized } from "../principal-gate.js";
3
4
  import { filterFingerprint } from "../cursor-fingerprint.js";
4
5
  import { sessionTempoOfRunStatus } from "../../plugins/store-contracts.js";
5
6
  import { assertNever } from "../route-ctx.js";
@@ -10,7 +11,7 @@ export async function handleSessionsList(req, res, match, ctx) {
10
11
  case "sessions-list": {
11
12
  const { fleetWide, gateOwner } = sessionOwnerScope(req);
12
13
  if (!fleetWide && gateOwner === null) {
13
- sendError(res, 401, "auth.unauthorized", "unauthorized");
14
+ sendUnauthorized(res, "identity");
14
15
  return;
15
16
  }
16
17
  const lister = deps.sessionStorage?.listSessions?.bind(deps.sessionStorage) ?? deps.runStore?.listSessions?.bind(deps.runStore);
@@ -25,7 +26,7 @@ export async function handleSessionsList(req, res, match, ctx) {
25
26
  case "sendfile-links": {
26
27
  const { fleetWide, gateOwner } = sessionOwnerScope(req);
27
28
  if (!fleetWide && gateOwner === null) {
28
- sendError(res, 401, "auth.unauthorized", "unauthorized");
29
+ sendUnauthorized(res, "identity");
29
30
  return;
30
31
  }
31
32
  if (!deps.sendFileLedger) {
@@ -6,10 +6,11 @@ import { redactSecrets } from "../../trace/redact.js";
6
6
  import { recordFailOpen } from "../../observability/fail-open.js";
7
7
  import { targetOutsideStoreOf } from "../../memory-store-refusal.js";
8
8
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
9
- import { readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
10
- import { sendJson, sendError, sendSessionPolicyError, sseHeaders } from "../send.js";
11
- import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "../principal-gate.js";
9
+ import { readWritableSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
10
+ import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } from "../send.js";
11
+ import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
12
12
  import { assertNever } from "../route-ctx.js";
13
+ import { rosterAgentId } from "./agents-roster.js";
13
14
  const MCP_STATUS_TTL_MS = 5_000;
14
15
  const MCP_MATERIALIZE_TIMEOUT_MS = 10_000;
15
16
  const A2A_STATUS_TTL_MS = 5_000;
@@ -67,6 +68,34 @@ async function readLastLegMcp(deps, sessionId, principal) {
67
68
  export async function handleSessions(req, res, match, ctx) {
68
69
  const { deps } = ctx;
69
70
  const { readJson, safeDecode, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite } = ctx.helpers;
71
+ const readOwnedSessionView = async (sessionAudit, sessionId, notFoundForm) => {
72
+ const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
73
+ if (ownerOf !== undefined) {
74
+ const owner = await ownerOf(sessionId);
75
+ if (owner === undefined) {
76
+ sendError(res, 404, "not_found.session", "session not found");
77
+ return undefined;
78
+ }
79
+ if (!runOwnerOk(req, res, owner, notFoundForm))
80
+ return undefined;
81
+ }
82
+ let audit;
83
+ try {
84
+ audit = await sessionAudit(sessionId);
85
+ }
86
+ catch (e) {
87
+ if (ownerOf !== undefined)
88
+ throw e;
89
+ throw new Error(`session view read failed before ownership could be established: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
90
+ }
91
+ if (!audit) {
92
+ sendError(res, 404, "not_found.session", "session not found");
93
+ return undefined;
94
+ }
95
+ if (!runOwnerOk(req, res, audit.owner, notFoundForm))
96
+ return undefined;
97
+ return audit;
98
+ };
70
99
  const sse = ctx.local.sessions;
71
100
  const { mcpStatusCache, a2aStatusCache } = ctx.local.sessions;
72
101
  {
@@ -441,6 +470,8 @@ export async function handleSessions(req, res, match, ctx) {
441
470
  }
442
471
  catch (e) {
443
472
  cleanup();
473
+ if (!res.headersSent && sendSessionError(res, e, "open"))
474
+ return;
444
475
  if (!res.headersSent)
445
476
  sendError(res, 500, "internal.error", `session events failed: ${e instanceof Error ? e.message : "unknown"}`);
446
477
  else
@@ -467,12 +498,7 @@ export async function handleSessions(req, res, match, ctx) {
467
498
  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)");
468
499
  return;
469
500
  }
470
- const audit = await deps.sessionAudit(sessionId);
471
- if (!audit) {
472
- sendError(res, 404, "not_found.session", "session not found");
473
- return;
474
- }
475
- if (!runOwnerOk(req, res, audit.owner, "session"))
501
+ if ((await readOwnedSessionView(deps.sessionAudit, sessionId, "session")) === undefined)
476
502
  return;
477
503
  const faces = deps.memoryCompliance;
478
504
  if (faces === undefined) {
@@ -532,12 +558,7 @@ export async function handleSessions(req, res, match, ctx) {
532
558
  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)");
533
559
  return;
534
560
  }
535
- const audit = await deps.sessionAudit(sessionId);
536
- if (!audit) {
537
- sendError(res, 404, "not_found.session", "session not found");
538
- return;
539
- }
540
- if (!runOwnerOk(req, res, audit.owner, "session"))
561
+ if ((await readOwnedSessionView(deps.sessionAudit, sessionId, "session")) === undefined)
541
562
  return;
542
563
  const face = deps.sessionMemoryStatus;
543
564
  if (face === undefined) {
@@ -581,12 +602,8 @@ export async function handleSessions(req, res, match, ctx) {
581
602
  sendError(res, 404, "not_found.route", "not found");
582
603
  return;
583
604
  }
584
- const audit = await deps.sessionAudit(match.params.sessionId);
585
- if (!audit) {
586
- sendError(res, 404, "not_found.session", "session not found");
587
- return;
588
- }
589
- if (!runOwnerOk(req, res, audit.owner))
605
+ const audit = await readOwnedSessionView(deps.sessionAudit, match.params.sessionId, "run");
606
+ if (audit === undefined)
590
607
  return;
591
608
  const q = new URLSearchParams(match.search);
592
609
  const qi = (k) => {
@@ -636,7 +653,7 @@ export async function handleSessions(req, res, match, ctx) {
636
653
  }
637
654
  const scope = sessionOwnerScopeForWrite(req);
638
655
  if (!scope.fleetWide && scope.gateOwner === null) {
639
- sendError(res, 401, "auth.unauthorized", "unauthorized");
656
+ sendUnauthorized(res, "identity");
640
657
  return;
641
658
  }
642
659
  const sourceId = safeDecode(match.params.sessionId);
@@ -673,7 +690,7 @@ export async function handleSessions(req, res, match, ctx) {
673
690
  }
674
691
  const scope = sessionOwnerScopeForWrite(req);
675
692
  if (!scope.fleetWide && scope.gateOwner === null) {
676
- sendError(res, 401, "auth.unauthorized", "unauthorized");
693
+ sendUnauthorized(res, "identity");
677
694
  return;
678
695
  }
679
696
  const sessionId = safeDecode(match.params.sessionId);
@@ -700,6 +717,10 @@ export async function handleSessions(req, res, match, ctx) {
700
717
  sendError(res, 409, "conflict.session_active_run", "session has an active run; cancel it before deleting", { activeTaskId: result.active });
701
718
  return;
702
719
  }
720
+ if ("childrenAlive" in result) {
721
+ sendError(res, 409, "conflict.session_children_alive", "this session still has background agents running where this replica cannot stop them (on another replica, for example), so the delete was refused before anything was deleted — `remaining` lists their ids (the same ids `GET /v1/agents/roster` shows); retry the delete once they finish, or stop them where they run", { remaining: result.childrenAlive.map((handle) => rosterAgentId({ handle })) });
722
+ return;
723
+ }
703
724
  sendJson(res, 200, result);
704
725
  return;
705
726
  }
@@ -714,7 +735,7 @@ export async function handleSessions(req, res, match, ctx) {
714
735
  }
715
736
  const principal = gatedPrincipal(req, deps.config);
716
737
  if (deps.config.requirePrincipal && principal === undefined) {
717
- sendError(res, 401, "auth.unauthorized", "unauthorized");
738
+ sendUnauthorized(res, "identity");
718
739
  return;
719
740
  }
720
741
  const sessionId = safeDecode(match.params.sessionId);
@@ -756,7 +777,7 @@ export async function handleSessions(req, res, match, ctx) {
756
777
  sendError(res, 400, "request.invalid_json", "invalid JSON body");
757
778
  return;
758
779
  }
759
- const read = readSessionPermissionRules(body.rules);
780
+ const read = readWritableSessionPermissionRules(body.rules);
760
781
  if (read.problem !== undefined) {
761
782
  switch (read.problem.kind) {
762
783
  case "not-an-object":
@@ -765,6 +786,9 @@ export async function handleSessions(req, res, match, ctx) {
765
786
  case "field-not-string-array":
766
787
  sendError(res, 400, "request.field_invalid", describeSessionPermissionRulesProblem(read.problem));
767
788
  return;
789
+ case "legacy-tool-name":
790
+ sendError(res, 400, "rules.legacy_tool_name", describeSessionPermissionRulesProblem(read.problem), { field: read.problem.field, name: read.problem.name });
791
+ return;
768
792
  default: {
769
793
  const unhandled = read.problem;
770
794
  throw new Error(`unhandled SessionPermissionRules problem: ${JSON.stringify(unhandled)}`);
@@ -1,6 +1,6 @@
1
1
  import { SHARED_MEMORY_READ_CAP_BYTES, SharedMemoryStoreError } from "@sema-agent/core";
2
2
  import { sendJson, sendError } from "../send.js";
3
- import { gatedPrincipal } from "../principal-gate.js";
3
+ import { gatedPrincipal, sendUnauthorized } from "../principal-gate.js";
4
4
  import { assertNever } from "../route-ctx.js";
5
5
  const RETRY_AFTER_SEC = 10;
6
6
  const HTTP_LIST_LIMIT = 100;
@@ -29,7 +29,7 @@ async function bindSnapshot(req, res, ctx) {
29
29
  const { deps } = ctx;
30
30
  const principal = gatedPrincipal(req, deps.config);
31
31
  if (!principal && deps.config.requirePrincipal === true) {
32
- sendError(res, 401, "auth.unauthorized", "unauthorized");
32
+ sendUnauthorized(res, "identity");
33
33
  return undefined;
34
34
  }
35
35
  const provider = deps.sharedMemoryStore;
@@ -3,6 +3,7 @@ import { HttpError } from "../../security.js";
3
3
  import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../../model-select.js";
4
4
  import { modelUnavailableMessage } from "../../model-entry-refusal.js";
5
5
  import { redactSecrets } from "../../trace/redact.js";
6
+ import { admitProviderEcho } from "../../observability/run-terminal.js";
6
7
  import { sendJson, sendError, httpErrorCode, msg } from "../send.js";
7
8
  import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
8
9
  import { assertNever } from "../route-ctx.js";
@@ -28,6 +29,12 @@ function isModelRole(value) {
28
29
  function passthroughModelRole(value) {
29
30
  return value;
30
31
  }
32
+ function shapeSideQueryEcho(result) {
33
+ if (result.responseModel === undefined || admitProviderEcho(result.responseModel, "side-query.responseModel"))
34
+ return result;
35
+ const { responseModel: _outOfShape, ...admitted } = result;
36
+ return admitted;
37
+ }
31
38
  export async function handleSideQuery(req, res, match, ctx) {
32
39
  const { deps } = ctx;
33
40
  const { readJson, rateLimited, quotaExceeded, leaseDenied } = ctx.helpers;
@@ -100,7 +107,7 @@ export async function handleSideQuery(req, res, match, ctx) {
100
107
  deps.sideQueryAccounting?.(principal, { model: modelId, usage: rr.usage });
101
108
  deps.metrics?.inc?.("side_query_total", { result: "ok" });
102
109
  if (!res.writableEnded)
103
- sendJson(res, 200, result);
110
+ sendJson(res, 200, shapeSideQueryEcho(result));
104
111
  }
105
112
  catch (e) {
106
113
  deps.metrics?.inc?.("side_query_total", { result: "error" });
@@ -1,6 +1,6 @@
1
1
  import { redactErrorMessage, redactHead, redactTerminalResult } from "../../observability/run-terminal.js";
2
2
  import { runWithVerification, runCascade, taskNotificationDedupKey, uuidv7 } from "@sema-agent/core";
3
- import { graftParkToolCallId, markChildrenStoppedByUserOnAbort, resumeAtHttpStatus, HEARTBEAT_MS } from "../../runs.js";
3
+ import { graftParkToolCallId, markChildrenStoppedByUserOnAbort, syncRefusalHttpStatus, HEARTBEAT_MS } from "../../runs.js";
4
4
  import { sealResultForExit, outwardPlane } from "../../terminal.js";
5
5
  import { withPrincipal } from "../../observability/principal-context.js";
6
6
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../../fleet/fleet-bus.js";
@@ -24,6 +24,7 @@ import { buildActiveRunConflict, toDoneFrameResult } from "../active-run-conflic
24
24
  import { commitPlacement, devicePlacementErrorBody } from "../device-placement.js";
25
25
  import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
26
26
  import { failOpenTagForDroppedFrame, recordFailOpen } from "../../observability/fail-open.js";
27
+ import { buildSyncSubmitBody } from "../sync-submit-body.js";
27
28
  import { assertNever } from "../route-ctx.js";
28
29
  const DISCONNECT_PARK_GRACE_MS = 10_000;
29
30
  function sseData(res, payload) {
@@ -31,6 +32,9 @@ function sseData(res, payload) {
31
32
  const idLine = typeof anchor === "string" && anchor.length > 0 ? `id: ${anchor}\n` : "";
32
33
  res.write(`${idLine}data: ${JSON.stringify(payload)}\n\n`);
33
34
  }
35
+ async function mintSubmitExitResult(result, checkpoints) {
36
+ return redactTerminalResult(sealResultForExit(await graftParkToolCallId(result, checkpoints)));
37
+ }
34
38
  function sendSubmitResult(res, resp) {
35
39
  const raw = resp.body;
36
40
  if (typeof raw === "object" && raw !== null && Object.hasOwn(raw, "retryAfterSec") && "retryAfterSec" in raw) {
@@ -421,7 +425,7 @@ export async function handleTasks(req, res, match, ctx) {
421
425
  userMsgEntryId = ev.entryId;
422
426
  const cancelVerbAtDone = ev.type === "done" && durableTaskId !== undefined && cancelledViaVerb.has(durableTaskId);
423
427
  if (ev.type === "done") {
424
- finalResult = redactTerminalResult(sealResultForExit(await graftParkToolCallId(ev.result, deps.checkpointStore)));
428
+ finalResult = await mintSubmitExitResult(ev.result, deps.checkpointStore);
425
429
  if (durableTaskId && finalResult)
426
430
  finalResult = { ...finalResult, taskId: durableTaskId };
427
431
  if (cancelVerbAtDone && finalResult !== undefined) {
@@ -652,10 +656,10 @@ export async function handleTasks(req, res, match, ctx) {
652
656
  : withApproval());
653
657
  if (durableTaskId && deps.runStore) {
654
658
  const finalPlane = finalResult ? outwardPlane(finalResult.terminal) : undefined;
655
- if (finalPlane?.status === "suspended" && deps.checkpointStore)
656
- await deps.runStore.setSuspended(durableTaskId);
657
- else if (finalPlane?.status === "needs_review" && deps.checkpointStore)
658
- await deps.runStore.setNeedsReview(durableTaskId);
659
+ if (finalResult && finalPlane?.status === "suspended" && deps.checkpointStore)
660
+ await deps.runStore.setSuspended(durableTaskId, finalResult);
661
+ else if (finalResult && finalPlane?.status === "needs_review" && deps.checkpointStore)
662
+ await deps.runStore.setNeedsReview(durableTaskId, finalResult);
659
663
  else if (finalResult && finalPlane)
660
664
  await deps.runStore.setTerminal(durableTaskId, finalPlane.status, finalResult, finalPlane.errorMessage ?? null);
661
665
  else {
@@ -667,7 +671,7 @@ export async function handleTasks(req, res, match, ctx) {
667
671
  await ledgerSink.flush().catch(() => undefined);
668
672
  await ledgerSink.appendParked("suspended", {}).catch(() => undefined);
669
673
  }
670
- await deps.runStore.setSuspended(durableTaskId);
674
+ await deps.runStore.setSuspended(durableTaskId, null);
671
675
  settleFleet("suspended");
672
676
  }
673
677
  else {
@@ -710,7 +714,7 @@ export async function handleTasks(req, res, match, ctx) {
710
714
  await ledgerSink.flush().catch(() => undefined);
711
715
  await ledgerSink.appendParked("suspended", {}).catch(() => undefined);
712
716
  }
713
- await deps.runStore.setSuspended(durableTaskId);
717
+ await deps.runStore.setSuspended(durableTaskId, null);
714
718
  settleFleet("suspended");
715
719
  }
716
720
  else if (finalResult) {
@@ -859,7 +863,7 @@ export async function handleTasks(req, res, match, ctx) {
859
863
  engineResult: result,
860
864
  });
861
865
  }
862
- const resumeAtStatus = resumeAtHttpStatus(result);
866
+ const resumeAtStatus = syncRefusalHttpStatus(result);
863
867
  if (resumeAtStatus) {
864
868
  const resumeAtPlane = outwardPlane(result.terminal);
865
869
  if (durableTaskId && deps.runStore)
@@ -869,22 +873,21 @@ export async function handleTasks(req, res, match, ctx) {
869
873
  : undefined;
870
874
  return { status: resumeAtStatus, body: { errorCode: resumeAtPlane.errorCode, error: redactErrorMessage(resumeAtPlane.errorMessage) ?? "resume-at failed", ...(retryAfterSec !== undefined ? { retryAfterSec } : {}) } };
871
875
  }
876
+ const exitResult = await mintSubmitExitResult(result, deps.checkpointStore);
872
877
  const syncPlane = outwardPlane(result.terminal);
873
- if (syncPlane.status === "suspended" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
874
- if (durableTaskId)
875
- await deps.runStore.setSuspended(durableTaskId);
876
- return { status: 200, body: withSyncSubmitKeys({ taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "suspended" }) };
878
+ if (syncPlane.status === "suspended" && deps.checkpointStore && deps.runStore && durableTaskId && prepared.spec.sessionId) {
879
+ await deps.runStore.setSuspended(durableTaskId, exitResult);
880
+ return { status: 200, body: withSyncSubmitKeys(buildSyncSubmitBody(exitResult, { taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "suspended" })) };
877
881
  }
878
- if (syncPlane.status === "needs_review" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
879
- if (durableTaskId)
880
- await deps.runStore.setNeedsReview(durableTaskId);
881
- return { status: 200, body: withSyncSubmitKeys({ taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "needs_review" }) };
882
+ if (syncPlane.status === "needs_review" && deps.checkpointStore && deps.runStore && durableTaskId && prepared.spec.sessionId) {
883
+ await deps.runStore.setNeedsReview(durableTaskId, exitResult);
884
+ return { status: 200, body: withSyncSubmitKeys(buildSyncSubmitBody(exitResult, { taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "needs_review" })) };
882
885
  }
883
886
  if (durableTaskId && deps.runStore)
884
- await deps.runStore.setTerminal(durableTaskId, syncPlane.status, sealResultForExit(result), syncPlane.errorMessage ?? null);
887
+ await deps.runStore.setTerminal(durableTaskId, syncPlane.status, exitResult, syncPlane.errorMessage ?? null);
885
888
  finalizeTaskResult(result, principal, prepared.spec.objective, prepared.spec.sessionId);
886
889
  noteRunOutcome(result);
887
- return { status: 200, body: withSyncSubmitKeys(redactTerminalResult(sealResultForExit(result))) };
890
+ return { status: 200, body: withSyncSubmitKeys(buildSyncSubmitBody(exitResult)) };
888
891
  };
889
892
  const resp = await idemCache.run(idemKey, async () => withRunReceipt(await runSyncSubmitLeg()), (r) => r.status < 400);
890
893
  sendSubmitResult(res, resp);
@@ -6,6 +6,8 @@
6
6
  *
7
7
  * ⚠️ 三块都跑在全局 service-credential 门**之前**(与 /metrics 同款待遇)——只读 traceToken 单凭据的调用方
8
8
  * (registry 中继的姿势)在全局门会被 401,挂门后=门内 traceToken 支持成死代码(1.148 a2 实机抓获)。
9
+ * S-672(7.105.0):门前不等于不设防 —— 本域坐在读路谓词段,先过 `dispatch.ts` 的 `readCredentialOk`
10
+ * (部署配了 service 凭据时要「service token / traceToken / 已验 principal」之一),块内这几道门在它之后。
9
11
  * 因此本域在 `handle()` 里的位置就是契约的一部分:必须留在鉴权门上方,顺序不可与其他域对调。本域零可变状态。
10
12
  */
11
13
  import type { IncomingMessage, ServerResponse } from "node:http";
@@ -1,11 +1,12 @@
1
1
  import { projectEvents, taskSummary, mapTraceEvent } from "../../trace/project.js";
2
+ import { redactLedgerEventForRead } from "../../observability/run-terminal.js";
2
3
  import { projectArtifacts } from "../../trace/artifacts.js";
3
4
  import { usageSummary, usageSeries, usageBreakdown } from "../../usage-analytics.js";
4
5
  import { sleep } from "../sse-log.js";
5
6
  import { bindSseLifecycle } from "../sse-lifecycle.js";
6
7
  import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
7
8
  import { filterFingerprint } from "../cursor-fingerprint.js";
8
- import { gatedPrincipal } from "../principal-gate.js";
9
+ import { gatedPrincipal, sendUnauthorized } from "../principal-gate.js";
9
10
  import { assertNever } from "../route-ctx.js";
10
11
  const TOOL_RESULT_LIMIT_CAP = 262_144;
11
12
  const TOOL_RESULT_OFFSET_CAP = 2_147_483_646;
@@ -14,7 +15,7 @@ export async function handleTraceUsage(req, res, match, ctx) {
14
15
  const { isFleetWide, safeDecode } = ctx.helpers;
15
16
  if (match.decl.id === "tasks-source-summary") {
16
17
  if (!isFleetWide(req)) {
17
- sendError(res, 401, "auth.unauthorized", "unauthorized");
18
+ sendUnauthorized(res, "operator");
18
19
  return;
19
20
  }
20
21
  if (!deps.runStore) {
@@ -34,7 +35,7 @@ export async function handleTraceUsage(req, res, match, ctx) {
34
35
  const fleetWide = isFleetWide(req);
35
36
  const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
36
37
  if (!fleetWide && gateOwner === null) {
37
- sendError(res, 401, "auth.unauthorized", "unauthorized");
38
+ sendUnauthorized(res, "identity");
38
39
  return;
39
40
  }
40
41
  if (!deps.runStore) {
@@ -84,7 +85,7 @@ export async function handleTraceUsage(req, res, match, ctx) {
84
85
  const fleetWide = isFleetWide(req);
85
86
  const jwtOwner = fleetWide ? undefined : gatedPrincipal(req, deps.config);
86
87
  if (!fleetWide && !jwtOwner) {
87
- sendError(res, 401, "auth.unauthorized", "unauthorized");
88
+ sendUnauthorized(res, "identity");
88
89
  return;
89
90
  }
90
91
  const q = new URLSearchParams(match.search);
@@ -220,6 +221,7 @@ async function handleToolResult(res, store, runStore, taskId, ownedRun, ref, que
220
221
  return notFound();
221
222
  sendJson(res, 200, { content: slice.content, offset: slice.offset, totalChars: slice.totalChars });
222
223
  }
224
+ const readFaceEvent = (ev) => ({ ...ev, data: redactLedgerEventForRead(ev.type, ev.data) });
223
225
  async function handleTaskTurns(res, runStore, taskId, query) {
224
226
  const run = await runStore.getRun(taskId);
225
227
  if (!run) {
@@ -229,7 +231,7 @@ async function handleTaskTurns(res, runStore, taskId, query) {
229
231
  const limit = Math.min(100, Math.max(1, Number(query.get("limit") ?? 20) || 20));
230
232
  const cursorSeq = Number(query.get("cursor") ?? 0) || 0;
231
233
  const [events, retainedFrom] = await Promise.all([runStore.getEvents(taskId, 0), runStore.retainedFrom(taskId)]);
232
- const all = projectEvents(events);
234
+ const all = projectEvents(events.map(readFaceEvent));
233
235
  const eligible = cursorSeq > 0 ? all.filter((t) => t.seq < cursorSeq) : all;
234
236
  const turns = eligible.slice(Math.max(0, eligible.length - limit));
235
237
  const oldest = turns[0];
@@ -244,13 +246,6 @@ async function streamTaskTrace(req, res, runStore, taskId, staleMs, search) {
244
246
  }
245
247
  const lastId = Number(req.headers["last-event-id"] ?? new URLSearchParams(search).get("from") ?? 0);
246
248
  let from = Number.isFinite(lastId) ? lastId : 0;
247
- if (from > 0) {
248
- const retainedFrom = await runStore.retainedFrom(taskId);
249
- if (retainedFrom > from + 1) {
250
- sendError(res, 416, "limit.retention_evicted", "resume point evicted past retention", { retainedFrom });
251
- return;
252
- }
253
- }
254
249
  sseHeaders(res);
255
250
  res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", version: 1, mode: "delta", resumeFrom: from })}\n\n`);
256
251
  let closed = false;
@@ -261,7 +256,7 @@ async function streamTaskTrace(req, res, runStore, taskId, staleMs, search) {
261
256
  let lastBeat = Date.now();
262
257
  const write = (events) => {
263
258
  for (const ev of events) {
264
- const out = mapTraceEvent(ev.type, ev.seq, (ev.data ?? {}));
259
+ const out = mapTraceEvent(ev.type, ev.seq, (readFaceEvent(ev).data ?? {}));
265
260
  if (out)
266
261
  res.write(`id: ${ev.seq}\nevent: ${out.event}\ndata: ${JSON.stringify(out.data)}\n\n`);
267
262
  from = ev.seq;
@@ -8,6 +8,7 @@
8
8
  * now imports them from here, so all ~500 existing `sendJson(...)` call sites are unchanged text.
9
9
  */
10
10
  import type { ServerResponse } from "node:http";
11
+ import { type SessionErrorFace } from "../session-error-wire.js";
11
12
  /**
12
13
  * [ref](黑板 [ref]②)—— 一条响应「是 SSE 流」的**在场标记**。
13
14
  *
@@ -71,5 +72,16 @@ export declare function httpErrorCode(status: number, code?: string): string;
71
72
  * `500 corrupt` 并点名那一列)。一只腿之后,新站点只会忘记调它,不会再各自发明一份映射。
72
73
  */
73
74
  export declare function sendSessionPolicyError(res: ServerResponse, e: unknown): boolean;
75
+ /**
76
+ * S-612 —— core `SessionError` → 应答,经 {@link sessionErrorHttp} 那**一只**映射(`src/session-error-wire.ts`)。
77
+ * 回 `true` = 已应答,调用点 `return`;`false` = 不是 `SessionError`,或这个词在这一面上是登记过的留白(站点的既有
78
+ * 处置照旧 —— 通常是继续上抛到全局兜底)。
79
+ *
80
+ * 句子:属主给了固定句就发它(import 面 `invalid_session` 那一句),否则发 core 的原句过既有脱敏(`redactErrorMessage`:
81
+ * 不抛、有上限)。core 的 `corrupt_session` 原句只点名会话 id 与跳过计数(「Session <id> is unreadable: its log has no
82
+ * meta header (N of M records skipped as corrupt)」),SQL 两店的原句点名 `<表>.<列>` 与行号 —— 两者都不含盘上路径
83
+ * (路径只进运维日志:`session_repo_corrupt_read` 行)。
84
+ */
85
+ export declare function sendSessionError(res: ServerResponse, e: unknown, face: SessionErrorFace): boolean;
74
86
  export declare function msg(e: unknown): string;
75
87
  //# sourceMappingURL=send.d.ts.map
package/dist/http/send.js CHANGED
@@ -1,5 +1,7 @@
1
- import { SessionPolicyError } from "@sema-agent/core";
1
+ import { SessionError, SessionPolicyError } from "@sema-agent/core";
2
2
  import { sessionPolicyErrorHttp } from "../session-policy-wire.js";
3
+ import { sessionErrorHttp } from "../session-error-wire.js";
4
+ import { redactErrorMessage } from "../observability/run-terminal.js";
3
5
  export const SSE_RESPONSE_MARK = Symbol("sema.http.sse-response");
4
6
  export function isSseResponse(res) {
5
7
  return res[SSE_RESPONSE_MARK] === true;
@@ -44,6 +46,15 @@ export function sendSessionPolicyError(res, e) {
44
46
  sendError(res, h.status, h.errorCode, e.message);
45
47
  return true;
46
48
  }
49
+ export function sendSessionError(res, e, face) {
50
+ if (!(e instanceof SessionError))
51
+ return false;
52
+ const wire = sessionErrorHttp(e.code, face);
53
+ if (wire === undefined)
54
+ return false;
55
+ sendError(res, wire.status, wire.errorCode, wire.sentence ?? redactErrorMessage(e.message) ?? "session store error");
56
+ return true;
57
+ }
47
58
  export function msg(e) {
48
59
  return e instanceof Error ? e.message : String(e);
49
60
  }