@sema-agent/server 7.79.2 → 7.80.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 (104) hide show
  1. package/dist/boot/late-binds.d.ts +35 -0
  2. package/dist/boot/late-binds.js +20 -0
  3. package/dist/boot/stage-01-config.d.ts +7 -0
  4. package/dist/boot/stage-01-config.js +60 -0
  5. package/dist/boot/stage-02-registry-pull.d.ts +10 -0
  6. package/dist/boot/stage-02-registry-pull.js +11 -0
  7. package/dist/boot/stage-03-store-backend.d.ts +32 -0
  8. package/dist/boot/stage-03-store-backend.js +11 -0
  9. package/dist/boot/stage-04-stores.d.ts +69 -0
  10. package/dist/boot/stage-04-stores.js +49 -0
  11. package/dist/boot/stage-05-execution-env.d.ts +82 -0
  12. package/dist/boot/stage-05-execution-env.js +20 -0
  13. package/dist/boot/stage-06-runners.d.ts +143 -0
  14. package/dist/boot/stage-06-runners.js +207 -0
  15. package/dist/boot/stage-07-capability-layer.d.ts +157 -0
  16. package/dist/boot/stage-07-capability-layer.js +162 -0
  17. package/dist/boot/stage-08-reapers.d.ts +172 -0
  18. package/dist/boot/stage-08-reapers.js +78 -0
  19. package/dist/boot/stage-09-leader.d.ts +204 -0
  20. package/dist/boot/stage-09-leader.js +47 -0
  21. package/dist/boot/stage-10-http-server.d.ts +204 -0
  22. package/dist/boot/stage-10-http-server.js +289 -0
  23. package/dist/config-invariants.d.ts +9 -0
  24. package/dist/config-invariants.js +7 -1
  25. package/dist/config-types.d.ts +1 -1
  26. package/dist/http/dispatch.js +28 -8
  27. package/dist/http/route-ctx.d.ts +101 -4
  28. package/dist/http/route-ctx.js +4 -0
  29. package/dist/http/route-table.d.ts +71 -43
  30. package/dist/http/route-table.js +111 -187
  31. package/dist/http/routes/a2a-serve.d.ts +18 -3
  32. package/dist/http/routes/a2a-serve.js +25 -23
  33. package/dist/http/routes/admin-config-refresh.d.ts +9 -2
  34. package/dist/http/routes/admin-config-refresh.js +32 -31
  35. package/dist/http/routes/admin-drain.d.ts +9 -2
  36. package/dist/http/routes/admin-drain.js +32 -31
  37. package/dist/http/routes/admin-memory-optout.d.ts +28 -2
  38. package/dist/http/routes/admin-memory-optout.js +26 -39
  39. package/dist/http/routes/adoption.d.ts +15 -2
  40. package/dist/http/routes/adoption.js +52 -52
  41. package/dist/http/routes/agents-roster.d.ts +9 -2
  42. package/dist/http/routes/agents-roster.js +82 -80
  43. package/dist/http/routes/approvals-assistant.d.ts +58 -5
  44. package/dist/http/routes/approvals-assistant.js +37 -38
  45. package/dist/http/routes/attachments.d.ts +22 -2
  46. package/dist/http/routes/attachments.js +81 -72
  47. package/dist/http/routes/capabilities.d.ts +21 -2
  48. package/dist/http/routes/capabilities.js +174 -170
  49. package/dist/http/routes/config-catalog.d.ts +9 -2
  50. package/dist/http/routes/config-catalog.js +28 -31
  51. package/dist/http/routes/devices.d.ts +24 -2
  52. package/dist/http/routes/devices.js +19 -21
  53. package/dist/http/routes/diagnostics.d.ts +9 -2
  54. package/dist/http/routes/diagnostics.js +41 -43
  55. package/dist/http/routes/fleet.d.ts +11 -2
  56. package/dist/http/routes/fleet.js +23 -22
  57. package/dist/http/routes/images.d.ts +63 -9
  58. package/dist/http/routes/images.js +53 -46
  59. package/dist/http/routes/leader.d.ts +20 -2
  60. package/dist/http/routes/leader.js +43 -40
  61. package/dist/http/routes/memory-bundle.d.ts +15 -2
  62. package/dist/http/routes/memory-bundle.js +38 -38
  63. package/dist/http/routes/memory-compliance.d.ts +16 -4
  64. package/dist/http/routes/memory-compliance.js +44 -45
  65. package/dist/http/routes/memory-consolidation.d.ts +14 -2
  66. package/dist/http/routes/memory-consolidation.js +66 -66
  67. package/dist/http/routes/memory-origin.d.ts +23 -4
  68. package/dist/http/routes/memory-origin.js +65 -65
  69. package/dist/http/routes/memory-policy.d.ts +21 -2
  70. package/dist/http/routes/memory-policy.js +119 -115
  71. package/dist/http/routes/notify-wake.d.ts +21 -11
  72. package/dist/http/routes/notify-wake.js +118 -116
  73. package/dist/http/routes/observability.d.ts +16 -2
  74. package/dist/http/routes/observability.js +54 -51
  75. package/dist/http/routes/retention-ops.d.ts +19 -2
  76. package/dist/http/routes/retention-ops.js +20 -22
  77. package/dist/http/routes/rules.d.ts +36 -2
  78. package/dist/http/routes/rules.js +22 -24
  79. package/dist/http/routes/run-memory-capture.d.ts +10 -3
  80. package/dist/http/routes/run-memory-capture.js +84 -84
  81. package/dist/http/routes/runs.d.ts +102 -22
  82. package/dist/http/routes/runs.js +71 -88
  83. package/dist/http/routes/session-sync.d.ts +41 -2
  84. package/dist/http/routes/session-sync.js +25 -23
  85. package/dist/http/routes/sessions-list.d.ts +16 -2
  86. package/dist/http/routes/sessions-list.js +78 -75
  87. package/dist/http/routes/sessions.d.ts +77 -2
  88. package/dist/http/routes/sessions.js +50 -47
  89. package/dist/http/routes/shared-memory.d.ts +19 -2
  90. package/dist/http/routes/shared-memory.js +65 -63
  91. package/dist/http/routes/side-query.d.ts +11 -2
  92. package/dist/http/routes/side-query.js +81 -79
  93. package/dist/http/routes/tasks.d.ts +17 -2
  94. package/dist/http/routes/tasks.js +18 -11
  95. package/dist/http/routes/trace-usage.d.ts +40 -2
  96. package/dist/http/routes/trace-usage.js +46 -35
  97. package/dist/http/routes/workflows.d.ts +35 -3
  98. package/dist/http/routes/workflows.js +29 -29
  99. package/dist/main.js +20 -870
  100. package/dist/service-version.d.ts +5 -0
  101. package/dist/service-version.js +12 -0
  102. package/dist/trace/redact.d.ts +7 -6
  103. package/dist/trace/redact.js +419 -229
  104. package/package.json +1 -1
@@ -25,19 +25,10 @@ import { mintCancelledResult } from "../../run-cancel-context.js";
25
25
  import { buildCrossSliceUsage, readResourceWindow } from "../../resource-window.js";
26
26
  import { headerStr, gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
27
27
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
28
+ import { assertNever } from "../route-ctx.js";
28
29
  import { mintAndDriveDurableRun } from "../durable-run-leg.js";
29
30
  import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
30
31
  import { withPrincipal } from "../../observability/principal-context.js";
31
- export const RUN_ID_RE = /^\/v1\/runs\/([^/]+)(\/events)?$/;
32
- export const RUN_CANCEL_RE = /^\/v1\/runs\/([^/]+)\/cancel$/;
33
- export const RUN_STEER_RE = /^\/v1\/runs\/([^/]+)\/steer$/;
34
- export const RUN_DETACH_RE = /^\/v1\/runs\/([^/]+)\/detach$/;
35
- export const RUN_COMPACT_RE = /^\/v1\/runs\/([^/]+)\/compact$/;
36
- export const RUN_INTERRUPT_RE = /^\/v1\/runs\/([^/]+)\/interrupt$/;
37
- export const ELICIT_RESPOND_RE = /^\/v1\/elicitations\/([^/]+)\/respond$/;
38
- export const QUESTION_RESPOND_RE = /^\/v1\/questions\/([^/]+)\/respond$/;
39
- export const TOOL_APPROVAL_RESPOND_RE = /^\/v1\/tool-approvals\/([^/]+)\/respond$/;
40
- export const TASK_ASK_DECISION_RE = /^\/v1\/tasks\/([^/]+)\/asks\/([^/]+)\/decision$/;
41
32
  const ASK_PARKING_RETRY_AFTER_SEC = 2;
42
33
  const ASK_ACTOR_LABEL_MAX = 256;
43
34
  const AskDecisionActorSchema = ActorAssertionWire.strip();
@@ -73,13 +64,7 @@ export function projectDecisionActor(raw) {
73
64
  const parsed = AskDecisionActorSchema.safeParse(raw);
74
65
  return parsed.success ? parsed.data : undefined;
75
66
  }
76
- export const RUN_SUBAGENT_STEER_RE = /^\/v1\/runs\/([^/]+)\/subagents\/([^/]+)\/steer$/;
77
- export const RUN_SUBAGENT_RESUME_RE = /^\/v1\/runs\/([^/]+)\/subagents\/([^/]+)\/resume$/;
78
- export const RUN_SUBAGENT_OUTPUT_RE = /^\/v1\/runs\/([^/]+)\/subagents\/([^/]+)\/output$/;
79
- export const RUN_SUBAGENT_STREAM_RE = /^\/v1\/runs\/([^/]+)\/subagents\/([^/]+)\/stream$/;
80
- export const RUN_TASK_OUTPUT_RE = /^\/v1\/runs\/([^/]+)\/tasks\/([^/]+)\/output$/;
81
- export const RUN_TASK_STOP_RE = /^\/v1\/runs\/([^/]+)\/tasks\/([^/]+)\/stop$/;
82
- async function subagentReadGate(req, res, deps, runId) {
67
+ async function subagentReadGate(req, res, deps, runId, search) {
83
68
  const principal = gatedPrincipal(req, deps.config);
84
69
  if (deps.config.requirePrincipal && principal === undefined) {
85
70
  sendPrincipalRefusal(res, req, deps.config);
@@ -91,7 +76,7 @@ async function subagentReadGate(req, res, deps, runId) {
91
76
  sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE);
92
77
  return null;
93
78
  }
94
- const callerSession = new URL(req.url ?? "", "http://x").searchParams.get("session");
79
+ const callerSession = new URLSearchParams(search).get("session");
95
80
  if (!trusted && run.sessionId && callerSession !== run.sessionId) {
96
81
  sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE);
97
82
  return null;
@@ -149,23 +134,13 @@ async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
149
134
  staleFrame: () => ({ event: "failed", data: { type: "failed", errorMessage: "run stalled (instance lost?)" } }),
150
135
  }, taskId, staleMs);
151
136
  }
152
- export async function handleRuns(req, res, url, ctx) {
153
- const miss = { fell: false };
154
- await handleRunsBody(req, res, url, ctx, miss);
155
- return !miss.fell;
156
- }
157
- export async function handleRunVerbs(req, res, url, ctx) {
158
- const miss = { fell: false };
159
- await handleRunVerbsBody(req, res, url, ctx, miss);
160
- return !miss.fell;
161
- }
162
- async function handleRunsBody(req, res, url, ctx, miss) {
137
+ export async function handleRuns(req, res, match, ctx) {
163
138
  const { deps } = ctx;
164
139
  const { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints } = ctx.registry;
165
140
  const { readJson, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied, runOwnerOk, runSessionAcceptOk } = ctx.helpers;
166
141
  const { prepareSpec } = ctx.legs;
167
142
  const source = ctx.req.source;
168
- if (req.method === "POST" && url === "/v1/runs") {
143
+ if (match.decl.id === "runs-submit") {
169
144
  if (!deps.runStore) {
170
145
  sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg|local)");
171
146
  return;
@@ -189,13 +164,12 @@ async function handleRunsBody(req, res, url, ctx, miss) {
189
164
  sendJson(res, resp.status, resp.body);
190
165
  return;
191
166
  }
192
- const runMatch = req.method === "GET" ? RUN_ID_RE.exec(url) : null;
193
- if (runMatch) {
167
+ if (match.decl.id === "run-detail" || match.decl.id === "run-events") {
194
168
  if (!deps.runStore) {
195
169
  sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg|local)");
196
170
  return;
197
171
  }
198
- const taskId = runMatch[1];
172
+ const taskId = match.params.taskId;
199
173
  if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
200
174
  sendPrincipalRefusal(res, req, deps.config);
201
175
  return;
@@ -207,9 +181,9 @@ async function handleRunsBody(req, res, url, ctx, miss) {
207
181
  }
208
182
  if (!runOwnerOk(req, res, run.owner))
209
183
  return;
210
- if (!runSessionAcceptOk(req, res, run, runMatch[2] ? "run.events" : "run.poll"))
184
+ if (!runSessionAcceptOk(req, res, run, match.decl.id === "run-events" ? "run.events" : "run.poll"))
211
185
  return;
212
- if (runMatch[2]) {
186
+ if (match.decl.id === "run-events") {
213
187
  await streamRunEvents(req, res, deps, deps.runStore, taskId, deps.config.runStaleSec * 1000);
214
188
  }
215
189
  else {
@@ -272,8 +246,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
272
246
  }
273
247
  return;
274
248
  }
275
- const cancelMatch = req.method === "POST" ? RUN_CANCEL_RE.exec(url) : null;
276
- if (cancelMatch) {
249
+ if (match.decl.id === "run-cancel") {
277
250
  if (!deps.runStore) {
278
251
  sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg|local)");
279
252
  return;
@@ -282,7 +255,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
282
255
  sendPrincipalRefusal(res, req, deps.config);
283
256
  return;
284
257
  }
285
- const taskId = cancelMatch[1];
258
+ const taskId = match.params.taskId;
286
259
  const run = await deps.runStore.getRun(taskId);
287
260
  if (!run) {
288
261
  sendError(res, 404, "not_found.run", runNotFoundMessage(taskId));
@@ -397,8 +370,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
397
370
  }
398
371
  return;
399
372
  }
400
- const steerMatch = req.method === "POST" ? RUN_STEER_RE.exec(url) : null;
401
- if (steerMatch) {
373
+ if (match.decl.id === "run-steer") {
402
374
  if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
403
375
  return;
404
376
  if (!deps.runStore) {
@@ -410,7 +382,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
410
382
  sendPrincipalRefusal(res, req, deps.config);
411
383
  return;
412
384
  }
413
- const taskId = steerMatch[1];
385
+ const taskId = match.params.taskId;
414
386
  let body;
415
387
  try {
416
388
  body = ((await readJson(req)) ?? {});
@@ -676,8 +648,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
676
648
  sendNotRunning(`run is ${run.status} — not accepting steers`);
677
649
  return;
678
650
  }
679
- const interruptMatch = req.method === "POST" ? RUN_INTERRUPT_RE.exec(url) : null;
680
- if (interruptMatch) {
651
+ if (match.decl.id === "run-interrupt") {
681
652
  if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
682
653
  return;
683
654
  if (!deps.runStore) {
@@ -689,7 +660,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
689
660
  sendPrincipalRefusal(res, req, deps.config);
690
661
  return;
691
662
  }
692
- const taskId = interruptMatch[1];
663
+ const taskId = match.params.taskId;
693
664
  let interruptRaw;
694
665
  try {
695
666
  interruptRaw = await readJson(req);
@@ -822,8 +793,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
822
793
  sendJson(res, 202, { taskId, status: run.status, delivery: "queued-immediate", messageId: interruptMessageId, note: "interrupt accepted — the cut is best-effort: a task.turn_interrupted event on the run stream is the proof a turn was actually cut; with nothing in flight the text lands at the next turn boundary" });
823
794
  return;
824
795
  }
825
- const compactMatch = req.method === "POST" ? RUN_COMPACT_RE.exec(url) : null;
826
- if (compactMatch) {
796
+ if (match.decl.id === "run-compact") {
827
797
  if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
828
798
  return;
829
799
  if (!deps.runStore) {
@@ -835,7 +805,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
835
805
  sendPrincipalRefusal(res, req, deps.config);
836
806
  return;
837
807
  }
838
- const taskId = compactMatch[1];
808
+ const taskId = match.params.taskId;
839
809
  const run = await deps.runStore.getRun(taskId);
840
810
  if (!run) {
841
811
  sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE);
@@ -889,8 +859,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
889
859
  sendJson(res, 202, { taskId, status: "running", delivery: "accepted", note: "compaction will run at the next turn boundary; a compacted{trigger:'manual'} event rides the run stream if anything is summarized" });
890
860
  return;
891
861
  }
892
- const detachMatch = req.method === "POST" ? RUN_DETACH_RE.exec(url) : null;
893
- if (detachMatch) {
862
+ if (match.decl.id === "run-detach") {
894
863
  if (rateLimited(req, res))
895
864
  return;
896
865
  if (!deps.runStore) {
@@ -902,7 +871,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
902
871
  sendPrincipalRefusal(res, req, deps.config);
903
872
  return;
904
873
  }
905
- const taskId = detachMatch[1];
874
+ const taskId = match.params.taskId;
906
875
  let body;
907
876
  try {
908
877
  body = ((await readJson(req)) ?? {});
@@ -940,24 +909,23 @@ async function handleRunsBody(req, res, url, ctx, miss) {
940
909
  sendJson(res, 202, { taskId, toolCallId: body.toolCallId, delivery: "requested", note: "if the tool call is running in a detach-capable env it settles early with 'moved to background; task_id=b*'; otherwise the request is a no-op (fail-safe)" });
941
910
  return;
942
911
  }
943
- miss.fell = true;
912
+ return assertNever(match.decl.id);
944
913
  }
945
- async function handleRunVerbsBody(req, res, url, ctx, miss) {
914
+ export async function handleRunVerbs(req, res, match, ctx) {
946
915
  const { deps } = ctx;
947
916
  const { readJson, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, safeDecode, runSessionAcceptOk } = ctx.helpers;
948
- const subOutputMatch = req.method === "GET" ? RUN_SUBAGENT_OUTPUT_RE.exec(url) : null;
949
- if (subOutputMatch) {
917
+ if (match.decl.id === "run-subagent-output") {
950
918
  if (!deps.runStore || !deps.subagentTaskOutput) {
951
919
  sendError(res, 501, "capability.run_store_required", "subagent output reads require the run store");
952
920
  return;
953
921
  }
954
- const runId = safeDecode(subOutputMatch[1]);
955
- const target = safeDecode(subOutputMatch[2]);
922
+ const runId = safeDecode(match.params.runId);
923
+ const target = safeDecode(match.params.sub);
956
924
  if (runId === null || target === null) {
957
925
  sendError(res, 400, "request.path_malformed", "malformed subagent path (invalid percent-encoding)");
958
926
  return;
959
927
  }
960
- const gated = await subagentReadGate(req, res, deps, runId);
928
+ const gated = await subagentReadGate(req, res, deps, runId, match.search);
961
929
  if (!gated)
962
930
  return;
963
931
  const { run } = gated;
@@ -971,19 +939,18 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
971
939
  sendJson(res, 200, { taskId: runId, target, content: out.content, output: out.details });
972
940
  return;
973
941
  }
974
- const subStreamMatch = req.method === "GET" ? RUN_SUBAGENT_STREAM_RE.exec(url) : null;
975
- if (subStreamMatch) {
942
+ if (match.decl.id === "run-subagent-stream") {
976
943
  if (!deps.runStore || !deps.subagentTaskOutput) {
977
944
  sendError(res, 501, "capability.run_store_required", "subagent streams require the run store");
978
945
  return;
979
946
  }
980
- const runId = safeDecode(subStreamMatch[1]);
981
- const target = safeDecode(subStreamMatch[2]);
947
+ const runId = safeDecode(match.params.runId);
948
+ const target = safeDecode(match.params.sub);
982
949
  if (runId === null || target === null) {
983
950
  sendError(res, 400, "request.path_malformed", "malformed subagent path (invalid percent-encoding)");
984
951
  return;
985
952
  }
986
- const gatedTail = await subagentReadGate(req, res, deps, runId);
953
+ const gatedTail = await subagentReadGate(req, res, deps, runId, match.search);
987
954
  if (!gatedTail)
988
955
  return;
989
956
  const { run } = gatedTail;
@@ -1034,26 +1001,25 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1034
1001
  }
1035
1002
  return;
1036
1003
  }
1037
- const taskVerbMatch = req.method === "GET" ? RUN_TASK_OUTPUT_RE.exec(url) : req.method === "POST" ? RUN_TASK_STOP_RE.exec(url) : null;
1038
- if (taskVerbMatch) {
1039
- const stopVerb = req.method === "POST";
1004
+ if (match.decl.id === "run-task-output" || match.decl.id === "run-task-stop") {
1005
+ const stopVerb = match.decl.id === "run-task-stop";
1040
1006
  if (!deps.runStore || !deps.taskHandleOutput || !deps.taskHandleStop) {
1041
1007
  sendError(res, 501, "capability.run_store_required", "task-handle verbs require the run store + task-registry seams");
1042
1008
  return;
1043
1009
  }
1044
1010
  if (stopVerb && rateLimited(req, res))
1045
1011
  return;
1046
- const runId = safeDecode(taskVerbMatch[1]);
1047
- const target = safeDecode(taskVerbMatch[2]);
1012
+ const runId = safeDecode(match.params.runId);
1013
+ const target = safeDecode(match.params.tid);
1048
1014
  if (runId === null || target === null) {
1049
1015
  sendError(res, 400, "request.path_malformed", "malformed task path (invalid percent-encoding)");
1050
1016
  return;
1051
1017
  }
1052
- const gatedB = await subagentReadGate(req, res, deps, runId);
1018
+ const gatedB = await subagentReadGate(req, res, deps, runId, match.search);
1053
1019
  if (!gatedB)
1054
1020
  return;
1055
1021
  const { run } = gatedB;
1056
- const q = new URL(req.url ?? "", "http://x").searchParams;
1022
+ const q = new URLSearchParams(match.search);
1057
1023
  const access = { owner: runId, scope: run.owner ?? "default", ...(run.sessionId ? { sessionId: run.sessionId } : {}) };
1058
1024
  if (q.get("filter") !== null) {
1059
1025
  sendError(res, 400, "request.param_unsupported", "filter is not accepted on the wire — fetch the output and filter client-side (note: the wire serves the clipped projection)");
@@ -1106,9 +1072,8 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1106
1072
  sendJson(res, 200, { taskId: runId, target, content: out.content, output: out.details, ...g14 });
1107
1073
  return;
1108
1074
  }
1109
- const subVerbMatch = req.method === "POST" ? (RUN_SUBAGENT_STEER_RE.exec(url) ?? RUN_SUBAGENT_RESUME_RE.exec(url)) : null;
1110
- if (subVerbMatch) {
1111
- const verb = url.endsWith("/resume") ? "resume" : "steer";
1075
+ if (match.decl.id === "run-subagent-steer" || match.decl.id === "run-subagent-resume") {
1076
+ const verb = match.decl.id === "run-subagent-resume" ? "resume" : "steer";
1112
1077
  if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
1113
1078
  return;
1114
1079
  if (!deps.runStore) {
@@ -1120,8 +1085,8 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1120
1085
  sendPrincipalRefusal(res, req, deps.config);
1121
1086
  return;
1122
1087
  }
1123
- const runId = safeDecode(subVerbMatch[1]);
1124
- const target = safeDecode(subVerbMatch[2]);
1088
+ const runId = safeDecode(match.params.runId);
1089
+ const target = safeDecode(match.params.sub);
1125
1090
  if (runId === null || target === null) {
1126
1091
  sendError(res, 400, "request.path_malformed", "malformed subagent path (invalid percent-encoding)");
1127
1092
  return;
@@ -1204,8 +1169,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1204
1169
  : `run is ${run.status} — its sub-agents are no longer addressable`);
1205
1170
  return;
1206
1171
  }
1207
- const elicitMatch = req.method === "POST" ? ELICIT_RESPOND_RE.exec(url) : null;
1208
- if (elicitMatch) {
1172
+ if (match.decl.id === "elicitation-respond") {
1209
1173
  if (rateLimited(req, res))
1210
1174
  return;
1211
1175
  if (!deps.elicitation) {
@@ -1217,7 +1181,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1217
1181
  sendPrincipalRefusal(res, req, deps.config);
1218
1182
  return;
1219
1183
  }
1220
- const id = elicitMatch[1];
1184
+ const id = match.params.id;
1221
1185
  let body;
1222
1186
  try {
1223
1187
  body = await readJson(req);
@@ -1230,8 +1194,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1230
1194
  sendJson(res, status, respBody);
1231
1195
  return;
1232
1196
  }
1233
- const questionMatch = req.method === "POST" ? QUESTION_RESPOND_RE.exec(url) : null;
1234
- if (questionMatch) {
1197
+ if (match.decl.id === "question-respond") {
1235
1198
  if (rateLimited(req, res))
1236
1199
  return;
1237
1200
  if (!deps.question) {
@@ -1243,7 +1206,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1243
1206
  sendPrincipalRefusal(res, req, deps.config);
1244
1207
  return;
1245
1208
  }
1246
- const id = questionMatch[1];
1209
+ const id = match.params.id;
1247
1210
  let body;
1248
1211
  try {
1249
1212
  body = await readJson(req);
@@ -1256,8 +1219,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1256
1219
  sendJson(res, status, respBody);
1257
1220
  return;
1258
1221
  }
1259
- const approvalMatch = req.method === "POST" ? TOOL_APPROVAL_RESPOND_RE.exec(url) : null;
1260
- if (approvalMatch) {
1222
+ if (match.decl.id === "tool-approval-respond") {
1261
1223
  if (rateLimited(req, res))
1262
1224
  return;
1263
1225
  if (!deps.toolApproval) {
@@ -1269,7 +1231,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1269
1231
  sendPrincipalRefusal(res, req, deps.config);
1270
1232
  return;
1271
1233
  }
1272
- const id = approvalMatch[1];
1234
+ const id = match.params.id;
1273
1235
  let body;
1274
1236
  try {
1275
1237
  body = await readJson(req);
@@ -1286,8 +1248,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1286
1248
  sendJson(res, status, respBody);
1287
1249
  return;
1288
1250
  }
1289
- const askDecisionMatch = req.method === "POST" ? TASK_ASK_DECISION_RE.exec(url) : null;
1290
- if (askDecisionMatch) {
1251
+ if (match.decl.id === "task-ask-decision") {
1291
1252
  if (rateLimited(req, res))
1292
1253
  return;
1293
1254
  const gate = resolveStreamApprovalGate({
@@ -1321,8 +1282,8 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1321
1282
  }
1322
1283
  const body = parsedBody.data;
1323
1284
  const hasUpdatedInput = typeof rawBody === "object" && rawBody !== null && "updatedInput" in rawBody;
1324
- const taskId = askDecisionMatch[1];
1325
- const askId = askDecisionMatch[2];
1285
+ const taskId = match.params.taskId;
1286
+ const askId = match.params.askId;
1326
1287
  const askNotFound = () => sendError(res, 404, "not_found.ask", "no such approval ask under this task (never existed, belongs to another task, or is not yours)");
1327
1288
  const row0 = await askStore.getAsk(askId);
1328
1289
  if (!row0 || row0.taskId !== taskId) {
@@ -1531,7 +1492,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1531
1492
  });
1532
1493
  return;
1533
1494
  }
1534
- miss.fell = true;
1495
+ return assertNever(match.decl.id);
1535
1496
  }
1536
1497
  export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1537
1498
  const { deps } = ctx;
@@ -1571,4 +1532,26 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1571
1532
  }), (r) => r.status === 202);
1572
1533
  return resp;
1573
1534
  }
1535
+ export const RUNS_ROUTES = [
1536
+ { id: "runs-submit", path: "/v1/runs", billable: true, methods: ["POST"] },
1537
+ { id: "run-steer", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/steer$/, label: "/v1/runs/:id/steer", methods: ["POST"] },
1538
+ { id: "run-interrupt", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/interrupt$/, label: "/v1/runs/:id/interrupt", methods: ["POST"] },
1539
+ { id: "run-detach", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/detach$/, label: "/v1/runs/:id/detach", methods: ["POST"] },
1540
+ { id: "run-cancel", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/cancel$/, label: "/v1/runs/:id/cancel", methods: ["POST"] },
1541
+ { id: "run-compact", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/compact$/, label: "/v1/runs/:id/compact", methods: ["POST"] },
1542
+ { id: "run-events", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/events$/, label: "/v1/runs/:id/events", methods: ["GET"] },
1543
+ { id: "run-detail", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)$/, label: "/v1/runs/:id", methods: ["GET"] },
1544
+ ];
1545
+ export const RUN_VERBS_ROUTES = [
1546
+ { id: "run-subagent-output", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/subagents\/(?<sub>[^/]+)\/output$/, label: "/v1/runs/:id/subagents/:sub/*", methods: ["GET"] },
1547
+ { id: "run-subagent-stream", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/subagents\/(?<sub>[^/]+)\/stream$/, label: "/v1/runs/:id/subagents/:sub/*", methods: ["GET"] },
1548
+ { id: "run-subagent-steer", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/subagents\/(?<sub>[^/]+)\/steer$/, label: "/v1/runs/:id/subagents/:sub/*", methods: ["POST"] },
1549
+ { id: "run-subagent-resume", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/subagents\/(?<sub>[^/]+)\/resume$/, label: "/v1/runs/:id/subagents/:sub/*", billable: true, methods: ["POST"] },
1550
+ { id: "run-task-output", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/tasks\/(?<tid>[^/]+)\/output$/, label: "/v1/runs/:id/tasks/:tid/*", methods: ["GET"] },
1551
+ { id: "run-task-stop", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/tasks\/(?<tid>[^/]+)\/stop$/, label: "/v1/runs/:id/tasks/:tid/*", methods: ["POST"] },
1552
+ { id: "elicitation-respond", pattern: /^\/v1\/elicitations\/(?<id>[^/]+)\/respond$/, label: "/v1/elicitations/:id/respond", methods: ["POST"] },
1553
+ { id: "question-respond", pattern: /^\/v1\/questions\/(?<id>[^/]+)\/respond$/, label: "/v1/questions/:id/respond", methods: ["POST"] },
1554
+ { id: "tool-approval-respond", pattern: /^\/v1\/tool-approvals\/(?<id>[^/]+)\/respond$/, label: "/v1/tool-approvals/:id/respond", methods: ["POST"] },
1555
+ { id: "task-ask-decision", pattern: /^\/v1\/tasks\/(?<taskId>[^/]+)\/asks\/(?<askId>[^/]+)\/decision$/, label: "/v1/tasks/:id/asks/:askId/decision", methods: ["POST"] },
1556
+ ];
1574
1557
  //# sourceMappingURL=runs.js.map
@@ -10,7 +10,7 @@
10
10
  import type { IncomingMessage, ServerResponse } from "node:http";
11
11
  import { type SessionRulesRecord, type FileHistoryExport } from "@sema-agent/core";
12
12
  import { type StagingHandle } from "../../session-sync-kernel.js";
13
- import type { RouteCtx } from "../route-ctx.js";
13
+ import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
14
14
  export type PendingStaging = {
15
15
  handle: StagingHandle;
16
16
  realSessionId: string;
@@ -45,5 +45,44 @@ export interface SessionSyncLocal {
45
45
  cleanupStaging(stagingId: string): void;
46
46
  }
47
47
  export declare function createSessionSyncLocal(): SessionSyncLocal;
48
- export declare function handleSessionSync(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
48
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。 */
49
+ export declare function handleSessionSync(req: IncomingMessage, res: ServerResponse, match: RouteMatch<SessionSyncRouteId>, ctx: RouteCtx): Promise<void>;
50
+ export declare const SESSION_SYNC_ROUTES: readonly [{
51
+ readonly id: "session-sync-manifest";
52
+ readonly pattern: RegExp;
53
+ readonly label: "/v1/sessions/:id/sync/$verb";
54
+ readonly methods: readonly ["GET"];
55
+ }, {
56
+ readonly id: "session-sync-entries";
57
+ readonly pattern: RegExp;
58
+ readonly label: "/v1/sessions/:id/sync/$verb";
59
+ readonly methods: readonly ["GET"];
60
+ }, {
61
+ readonly id: "session-sync-plan";
62
+ readonly pattern: RegExp;
63
+ readonly label: "/v1/sessions/:id/sync/$verb";
64
+ readonly methods: readonly ["POST"];
65
+ }, {
66
+ readonly id: "session-sync-import";
67
+ readonly pattern: RegExp;
68
+ readonly label: "/v1/sessions/:id/sync/import";
69
+ readonly methods: readonly ["POST"];
70
+ }, {
71
+ readonly id: "session-sync-import-entries";
72
+ readonly pattern: RegExp;
73
+ readonly label: "/v1/sessions/:id/sync/import";
74
+ readonly methods: readonly ["POST"];
75
+ }, {
76
+ readonly id: "session-sync-blob-put";
77
+ readonly pattern: RegExp;
78
+ readonly label: "/v1/sessions/:id/sync/blobs";
79
+ readonly methods: readonly ["PUT"];
80
+ }, {
81
+ readonly id: "session-sync-history-blob";
82
+ readonly pattern: RegExp;
83
+ readonly label: "/v1/sessions/:id/sync/history/blobs/:hash";
84
+ readonly methods: readonly ["GET"];
85
+ }];
86
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
87
+ export type SessionSyncRouteId = RouteIdsOf<typeof SESSION_SYNC_ROUTES>;
49
88
  //# sourceMappingURL=session-sync.d.ts.map
@@ -11,6 +11,7 @@ const SYNC_BLOB_MAX_BYTES = 64 * 1024 * 1024;
11
11
  const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
12
12
  const SYNC_IMPORT_BATCH = 500;
13
13
  import { SHA256_HEX_RE } from "../../digest-form.js";
14
+ import { assertNever } from "../route-ctx.js";
14
15
  export function createSessionSyncLocal() {
15
16
  const pendingStagings = new Map();
16
17
  const importLeases = new Map();
@@ -27,18 +28,12 @@ export function createSessionSyncLocal() {
27
28
  };
28
29
  return { pendingStagings, importLeases, touchImportLease, cleanupStaging };
29
30
  }
30
- export async function handleSessionSync(req, res, url, ctx) {
31
- const miss = { fell: false };
32
- await handleSessionSyncBody(req, res, url, ctx, miss);
33
- return !miss.fell;
34
- }
35
- async function handleSessionSyncBody(req, res, url, ctx, miss) {
31
+ export async function handleSessionSync(req, res, match, ctx) {
36
32
  const { deps } = ctx;
37
33
  const { readJson, readRawBody, safeDecode, sessionOwnerScopeForWrite, rateLimited } = ctx.helpers;
38
34
  const { pendingStagings, importLeases, touchImportLease, cleanupStaging } = ctx.local.sessionSync;
39
35
  {
40
- const sm = /^\/v1\/sessions\/([^/]+)\/sync\/(manifest|entries|import|plan|blobs(?:\/([^/]+))?|history\/blobs\/([^/]+)|import\/([^/]+)\/entries)$/.exec(url);
41
- if (sm) {
36
+ {
42
37
  const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
43
38
  if (!deps.backend || !ownerOf) {
44
39
  sendError(res, 501, "capability.session_store_required", "session sync requires a durable session store");
@@ -49,7 +44,7 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
49
44
  sendError(res, 401, "auth.unauthorized", "unauthorized");
50
45
  return;
51
46
  }
52
- const sessionId = safeDecode(sm[1]);
47
+ const sessionId = safeDecode(match.params.sessionId);
53
48
  if (sessionId === null || !isUuidShape(sessionId)) {
54
49
  sendError(res, 400, "request.id_invalid", "invalid session id (must be a canonical uuid)");
55
50
  return;
@@ -74,7 +69,7 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
74
69
  }
75
70
  return true;
76
71
  };
77
- if (sm[2] === "manifest" && req.method === "GET") {
72
+ if (match.decl.id === "session-sync-manifest") {
78
73
  if (!pullGateOk())
79
74
  return;
80
75
  const out = await exportSessionManifest(sessionId, deps.backend);
@@ -85,10 +80,10 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
85
80
  sendJson(res, 200, { manifest: out.manifest });
86
81
  return;
87
82
  }
88
- if (sm[2] === "entries" && req.method === "GET") {
83
+ if (match.decl.id === "session-sync-entries") {
89
84
  if (!pullGateOk())
90
85
  return;
91
- const afterSeqRaw = new URL(req.url ?? "", "http://x").searchParams.get("afterSeq");
86
+ const afterSeqRaw = new URLSearchParams(match.search).get("afterSeq");
92
87
  let afterSeq;
93
88
  if (afterSeqRaw !== null) {
94
89
  const n = Number(afterSeqRaw);
@@ -127,10 +122,10 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
127
122
  }
128
123
  return;
129
124
  }
130
- if (sm[4] !== undefined && req.method === "GET") {
125
+ if (match.decl.id === "session-sync-history-blob") {
131
126
  if (!pullGateOk())
132
127
  return;
133
- const hash = safeDecode(sm[4]);
128
+ const hash = safeDecode(match.params.hash);
134
129
  if (hash === null) {
135
130
  sendError(res, 400, "request.path_malformed", "invalid blob hash");
136
131
  return;
@@ -159,12 +154,12 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
159
154
  res.end(Buffer.from(bytes));
160
155
  return;
161
156
  }
162
- if (sm[3] !== undefined && req.method === "PUT") {
157
+ if (match.decl.id === "session-sync-blob-put") {
163
158
  if (rateLimited(req, res))
164
159
  return;
165
160
  if (!pushGateOk())
166
161
  return;
167
- const hash = safeDecode(sm[3]);
162
+ const hash = safeDecode(match.params.hash);
168
163
  if (hash === null || !SHA256_HEX_RE.test(hash)) {
169
164
  sendError(res, 400, "request.id_invalid", "blob hash must be 64 lowercase-hex chars");
170
165
  return;
@@ -191,7 +186,7 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
191
186
  res.writeHead(204).end();
192
187
  return;
193
188
  }
194
- if (sm[2] === "import" && req.method === "POST") {
189
+ if (match.decl.id === "session-sync-import") {
195
190
  if (!pushGateOk())
196
191
  return;
197
192
  if (deps.sessionMirrorRuling && !scope.fleetWide && scope.gateOwner !== null) {
@@ -353,8 +348,8 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
353
348
  sendJson(res, 200, { stagingId, relation: relA.relation });
354
349
  return;
355
350
  }
356
- if (sm[5] !== undefined && req.method === "POST") {
357
- const stagingId = safeDecode(sm[5]);
351
+ if (match.decl.id === "session-sync-import-entries") {
352
+ const stagingId = safeDecode(match.params.stagingId);
358
353
  if (stagingId === null) {
359
354
  sendError(res, 400, "request.id_invalid", "invalid staging id");
360
355
  return;
@@ -507,7 +502,7 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
507
502
  }
508
503
  return;
509
504
  }
510
- if (sm[2] === "plan" && req.method === "POST") {
505
+ if (match.decl.id === "session-sync-plan") {
511
506
  if (!pushGateOk())
512
507
  return;
513
508
  let body;
@@ -527,10 +522,17 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
527
522
  sendJson(res, 200, relation);
528
523
  return;
529
524
  }
530
- sendError(res, 404, "not_found.route", "not found");
531
- return;
525
+ return assertNever(match.decl.id);
532
526
  }
533
527
  }
534
- miss.fell = true;
535
528
  }
529
+ export const SESSION_SYNC_ROUTES = [
530
+ { id: "session-sync-manifest", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/sync\/manifest$/, label: "/v1/sessions/:id/sync/$verb", methods: ["GET"] },
531
+ { id: "session-sync-entries", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/sync\/entries$/, label: "/v1/sessions/:id/sync/$verb", methods: ["GET"] },
532
+ { id: "session-sync-plan", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/sync\/plan$/, label: "/v1/sessions/:id/sync/$verb", methods: ["POST"] },
533
+ { id: "session-sync-import", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/sync\/import$/, label: "/v1/sessions/:id/sync/import", methods: ["POST"] },
534
+ { id: "session-sync-import-entries", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/sync\/import\/(?<stagingId>[^/]+)\/entries$/, label: "/v1/sessions/:id/sync/import", methods: ["POST"] },
535
+ { id: "session-sync-blob-put", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/sync\/blobs\/(?<hash>[^/]+)$/, label: "/v1/sessions/:id/sync/blobs", methods: ["PUT"] },
536
+ { id: "session-sync-history-blob", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/sync\/history\/blobs\/(?<hash>[^/]+)$/, label: "/v1/sessions/:id/sync/history/blobs/:hash", methods: ["GET"] },
537
+ ];
536
538
  //# sourceMappingURL=session-sync.js.map
@@ -5,6 +5,20 @@
5
5
  * owner-scope 与 trace 列表同一权威模型(system/traceToken=fleet-wide,principal=钉住本人)。本域零可变状态。
6
6
  */
7
7
  import type { IncomingMessage, ServerResponse } from "node:http";
8
- import type { RouteCtx } from "../route-ctx.js";
9
- export declare function handleSessionsList(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
8
+ import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
9
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
10
+ * 的语义原样 =「本域已应答」;走到函数尾才是「没匹配上」,置 `miss.fell` 交回装配器继续下一个域。
11
+ * 跨域的东西(deps / 助手 / 单域状态)一律在首行解构出来,于是下面每一行都逐字不变。 */
12
+ export declare function handleSessionsList(req: IncomingMessage, res: ServerResponse, match: RouteMatch<SessionsListRouteId>, ctx: RouteCtx): Promise<void>;
13
+ export declare const SESSIONS_LIST_ROUTES: readonly [{
14
+ readonly id: "sessions-list";
15
+ readonly path: "/v1/sessions";
16
+ readonly methods: readonly ["GET"];
17
+ }, {
18
+ readonly id: "sendfile-links";
19
+ readonly path: "/v1/sendfile-links";
20
+ readonly methods: readonly ["GET"];
21
+ }];
22
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
23
+ export type SessionsListRouteId = RouteIdsOf<typeof SESSIONS_LIST_ROUTES>;
10
24
  //# sourceMappingURL=sessions-list.d.ts.map