@sema-agent/server 7.79.3 → 7.80.1

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 (89) hide show
  1. package/dist/boot/stage-07-capability-layer.js +2 -1
  2. package/dist/capabilities/skills.d.ts +10 -0
  3. package/dist/capabilities/skills.js +3 -1
  4. package/dist/http/admission.js +3 -1
  5. package/dist/http/dispatch.js +28 -8
  6. package/dist/http/resume-legs.js +4 -3
  7. package/dist/http/route-ctx.d.ts +101 -4
  8. package/dist/http/route-ctx.js +4 -0
  9. package/dist/http/route-table.d.ts +71 -43
  10. package/dist/http/route-table.js +111 -187
  11. package/dist/http/routes/a2a-serve.d.ts +18 -3
  12. package/dist/http/routes/a2a-serve.js +25 -23
  13. package/dist/http/routes/admin-config-refresh.d.ts +9 -2
  14. package/dist/http/routes/admin-config-refresh.js +32 -31
  15. package/dist/http/routes/admin-drain.d.ts +9 -2
  16. package/dist/http/routes/admin-drain.js +32 -31
  17. package/dist/http/routes/admin-memory-optout.d.ts +28 -2
  18. package/dist/http/routes/admin-memory-optout.js +27 -40
  19. package/dist/http/routes/adoption.d.ts +15 -2
  20. package/dist/http/routes/adoption.js +52 -52
  21. package/dist/http/routes/agents-roster.d.ts +9 -2
  22. package/dist/http/routes/agents-roster.js +82 -80
  23. package/dist/http/routes/approvals-assistant.d.ts +58 -5
  24. package/dist/http/routes/approvals-assistant.js +38 -39
  25. package/dist/http/routes/attachments.d.ts +22 -2
  26. package/dist/http/routes/attachments.js +81 -72
  27. package/dist/http/routes/capabilities.d.ts +21 -2
  28. package/dist/http/routes/capabilities.js +174 -170
  29. package/dist/http/routes/config-catalog.d.ts +9 -2
  30. package/dist/http/routes/config-catalog.js +28 -31
  31. package/dist/http/routes/devices.d.ts +24 -2
  32. package/dist/http/routes/devices.js +19 -21
  33. package/dist/http/routes/diagnostics.d.ts +9 -2
  34. package/dist/http/routes/diagnostics.js +41 -43
  35. package/dist/http/routes/fleet.d.ts +11 -2
  36. package/dist/http/routes/fleet.js +23 -22
  37. package/dist/http/routes/images.d.ts +63 -9
  38. package/dist/http/routes/images.js +53 -46
  39. package/dist/http/routes/leader.d.ts +20 -2
  40. package/dist/http/routes/leader.js +43 -40
  41. package/dist/http/routes/memory-bundle.d.ts +15 -2
  42. package/dist/http/routes/memory-bundle.js +38 -38
  43. package/dist/http/routes/memory-compliance.d.ts +16 -4
  44. package/dist/http/routes/memory-compliance.js +44 -45
  45. package/dist/http/routes/memory-consolidation.d.ts +14 -2
  46. package/dist/http/routes/memory-consolidation.js +66 -66
  47. package/dist/http/routes/memory-origin.d.ts +23 -4
  48. package/dist/http/routes/memory-origin.js +65 -65
  49. package/dist/http/routes/memory-policy.d.ts +21 -2
  50. package/dist/http/routes/memory-policy.js +119 -115
  51. package/dist/http/routes/notify-wake.d.ts +21 -11
  52. package/dist/http/routes/notify-wake.js +118 -116
  53. package/dist/http/routes/observability.d.ts +16 -2
  54. package/dist/http/routes/observability.js +54 -51
  55. package/dist/http/routes/retention-ops.d.ts +19 -2
  56. package/dist/http/routes/retention-ops.js +20 -22
  57. package/dist/http/routes/rules.d.ts +36 -2
  58. package/dist/http/routes/rules.js +22 -24
  59. package/dist/http/routes/run-memory-capture.d.ts +10 -3
  60. package/dist/http/routes/run-memory-capture.js +84 -84
  61. package/dist/http/routes/runs.d.ts +102 -22
  62. package/dist/http/routes/runs.js +79 -96
  63. package/dist/http/routes/session-sync.d.ts +41 -2
  64. package/dist/http/routes/session-sync.js +26 -24
  65. package/dist/http/routes/sessions-list.d.ts +16 -2
  66. package/dist/http/routes/sessions-list.js +78 -75
  67. package/dist/http/routes/sessions.d.ts +77 -2
  68. package/dist/http/routes/sessions.js +50 -47
  69. package/dist/http/routes/shared-memory.d.ts +19 -2
  70. package/dist/http/routes/shared-memory.js +65 -63
  71. package/dist/http/routes/side-query.d.ts +11 -2
  72. package/dist/http/routes/side-query.js +81 -79
  73. package/dist/http/routes/tasks.d.ts +17 -2
  74. package/dist/http/routes/tasks.js +18 -11
  75. package/dist/http/routes/trace-usage.d.ts +40 -2
  76. package/dist/http/routes/trace-usage.js +46 -35
  77. package/dist/http/routes/workflows.d.ts +35 -3
  78. package/dist/http/routes/workflows.js +29 -29
  79. package/dist/http/wire-types.d.ts +15 -9
  80. package/dist/parked-decide.d.ts +9 -0
  81. package/dist/parked-decide.js +3 -3
  82. package/dist/run-local.js +2 -1
  83. package/dist/spec-fields.js +33 -30
  84. package/dist/task-cwd.d.ts +21 -0
  85. package/dist/task-cwd.js +3 -0
  86. package/dist/trace/core-keyset-guard.d.ts +6 -1
  87. package/dist/trace/project.d.ts +20 -0
  88. package/dist/trace/project.js +15 -1
  89. package/package.json +2 -2
@@ -7,6 +7,7 @@ import { recordFailOpen } from "../../observability/fail-open.js";
7
7
  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";
8
8
  import { sendJson, sendError, sseHeaders } from "../send.js";
9
9
  import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "../principal-gate.js";
10
+ import { assertNever } from "../route-ctx.js";
10
11
  const MCP_STATUS_TTL_MS = 5_000;
11
12
  const MCP_MATERIALIZE_TIMEOUT_MS = 10_000;
12
13
  const A2A_STATUS_TTL_MS = 5_000;
@@ -61,26 +62,20 @@ async function readLastLegMcp(deps, sessionId, principal) {
61
62
  }
62
63
  return undefined;
63
64
  }
64
- export async function handleSessions(req, res, url, ctx) {
65
- const miss = { fell: false };
66
- await handleSessionsBody(req, res, url, ctx, miss);
67
- return !miss.fell;
68
- }
69
- async function handleSessionsBody(req, res, url, ctx, miss) {
65
+ export async function handleSessions(req, res, match, ctx) {
70
66
  const { deps } = ctx;
71
67
  const { readJson, safeDecode, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite } = ctx.helpers;
72
68
  const sse = ctx.local.sessions;
73
69
  const { mcpStatusCache, a2aStatusCache } = ctx.local.sessions;
74
70
  {
75
- const im = /^\/v1\/sessions\/([^/]+)\/init$/.exec(url);
76
- if (im && req.method === "GET") {
71
+ if (match.decl.id === "session-init") {
77
72
  const principal = gatedPrincipal(req, deps.config);
78
73
  if (deps.config.requirePrincipal && !principal) {
79
74
  sendPrincipalRefusal(res, req, deps.config);
80
75
  return;
81
76
  }
82
77
  sendJson(res, 200, {
83
- sessionId: im[1],
78
+ sessionId: match.params.sessionId,
84
79
  model: { provider: deps.config.model.provider, modelId: deps.config.model.id },
85
80
  ...(deps.config.autonomy ? { mode: deps.config.autonomy } : {}),
86
81
  });
@@ -88,8 +83,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
88
83
  }
89
84
  }
90
85
  {
91
- const stm = /^\/v1\/sessions\/([^/]+)\/settings$/.exec(url);
92
- if (stm && req.method === "GET") {
86
+ if (match.decl.id === "session-settings") {
93
87
  const principal = gatedPrincipal(req, deps.config);
94
88
  if (deps.config.requirePrincipal && !principal) {
95
89
  sendPrincipalRefusal(res, req, deps.config);
@@ -109,14 +103,13 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
109
103
  }
110
104
  }
111
105
  {
112
- const mm = /^\/v1\/sessions\/([^/]+)\/mcp$/.exec(url);
113
- if (mm && req.method === "GET") {
106
+ if (match.decl.id === "session-mcp") {
114
107
  const principal = gatedPrincipal(req, deps.config);
115
108
  if (deps.config.requirePrincipal && !principal) {
116
109
  sendPrincipalRefusal(res, req, deps.config);
117
110
  return;
118
111
  }
119
- const lastLegMcp = await lastLegMcpFor(deps, safeDecode(mm[1]), principal);
112
+ const lastLegMcp = await lastLegMcpFor(deps, safeDecode(match.params.sessionId), principal);
120
113
  const specs = mcpForScenario(deps.config.mcpServers, deps.config.defaultScenario);
121
114
  const cacheKey = `${deps.config.defaultScenario}::${principal ?? ""}`;
122
115
  if (!specs || specs.length === 0) {
@@ -160,8 +153,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
160
153
  }
161
154
  }
162
155
  {
163
- const am = /^\/v1\/sessions\/([^/]+)\/a2a$/.exec(url);
164
- if (am && req.method === "GET") {
156
+ if (match.decl.id === "session-a2a") {
165
157
  if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
166
158
  sendPrincipalRefusal(res, req, deps.config);
167
159
  return;
@@ -220,15 +212,14 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
220
212
  }
221
213
  }
222
214
  {
223
- const hm = req.method === "GET" ? /^\/v1\/sessions\/([^/]+)\/head$/.exec(url) : null;
224
- if (hm) {
215
+ if (match.decl.id === "session-head") {
225
216
  const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
226
217
  const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
227
218
  if (!getLeafId || !ownerOf) {
228
219
  sendError(res, 501, "capability.session_store_required", "session head probe requires a durable session store (DB_BACKEND=mysql|pg|local)");
229
220
  return;
230
221
  }
231
- const headSession = decodeURIComponent(hm[1]);
222
+ const headSession = decodeURIComponent(match.params.sessionId);
232
223
  const getHead = deps.sessionStorage?.getHead?.bind(deps.sessionStorage);
233
224
  let owner;
234
225
  let leafId;
@@ -264,15 +255,14 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
264
255
  }
265
256
  }
266
257
  {
267
- const em = req.method === "GET" ? /^\/v1\/sessions\/([^/]+)\/events$/.exec(url) : null;
268
- if (em) {
258
+ if (match.decl.id === "session-events") {
269
259
  const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
270
260
  const watch = deps.sessionWatch;
271
261
  if (!deps.sessionStorage?.getLeafId || !ownerOf || !watch) {
272
262
  sendError(res, 501, "capability.session_store_required", "session events require a durable session store (DB_BACKEND=mysql|pg|local)");
273
263
  return;
274
264
  }
275
- const evSession = decodeURIComponent(em[1]);
265
+ const evSession = decodeURIComponent(match.params.sessionId);
276
266
  if (sse.sseConnections >= (deps.sessionEventsMaxConnections ?? 256)) {
277
267
  res.setHeader("retry-after", "5");
278
268
  sendError(res, 503, "state.sse_connection_cap", "session-events connection cap reached — fall back to /head polling");
@@ -333,7 +323,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
333
323
  cleanup();
334
324
  return;
335
325
  }
336
- const q = new URL(req.url ?? url, "http://x").searchParams;
326
+ const q = new URLSearchParams(match.search);
337
327
  const lastLeafId = q.get("lastLeafId");
338
328
  let first = true;
339
329
  let lastAccepted;
@@ -453,9 +443,8 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
453
443
  }
454
444
  }
455
445
  {
456
- const em = req.method === "POST" ? /^\/v1\/sessions\/([^/]+)\/memory\/erase$/.exec(url) : null;
457
- if (em) {
458
- const sessionId = safeDecode(em[1]);
446
+ if (match.decl.id === "session-memory-erase") {
447
+ const sessionId = safeDecode(match.params.sessionId);
459
448
  if (sessionId === null) {
460
449
  sendError(res, 400, "request.path_malformed", "session id segment is not valid percent-encoding");
461
450
  return;
@@ -513,9 +502,8 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
513
502
  }
514
503
  }
515
504
  {
516
- const msm = req.method === "GET" ? /^\/v1\/sessions\/([^/]+)\/memory-status$/.exec(url) : null;
517
- if (msm) {
518
- const sessionId = safeDecode(msm[1]);
505
+ if (match.decl.id === "session-memory-status") {
506
+ const sessionId = safeDecode(match.params.sessionId);
519
507
  if (sessionId === null) {
520
508
  sendError(res, 400, "request.path_malformed", "session id segment is not valid percent-encoding");
521
509
  return;
@@ -570,17 +558,20 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
570
558
  return;
571
559
  }
572
560
  }
573
- if (deps.sessionAudit && req.method === "GET") {
574
- const sm = /^\/v1\/sessions\/([^/]+)$/.exec(url);
575
- if (sm) {
576
- const audit = await deps.sessionAudit(sm[1]);
561
+ if (match.decl.id === "session-detail") {
562
+ {
563
+ if (!deps.sessionAudit) {
564
+ sendError(res, 404, "not_found.route", "not found");
565
+ return;
566
+ }
567
+ const audit = await deps.sessionAudit(match.params.sessionId);
577
568
  if (!audit) {
578
569
  sendError(res, 404, "not_found.session", "session not found");
579
570
  return;
580
571
  }
581
572
  if (!runOwnerOk(req, res, audit.owner))
582
573
  return;
583
- const q = new URL(req.url ?? url, "http://x").searchParams;
574
+ const q = new URLSearchParams(match.search);
584
575
  const qi = (k) => {
585
576
  const v = q.get(k);
586
577
  if (v === null)
@@ -619,8 +610,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
619
610
  }
620
611
  }
621
612
  {
622
- const fm = req.method === "POST" ? /^\/v1\/sessions\/([^/]+)\/fork$/.exec(url) : null;
623
- if (fm) {
613
+ if (match.decl.id === "session-fork") {
624
614
  const fork = deps.sessionStorage?.fork?.bind(deps.sessionStorage);
625
615
  const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
626
616
  if (!fork || !ownerOf) {
@@ -632,7 +622,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
632
622
  sendError(res, 401, "auth.unauthorized", "unauthorized");
633
623
  return;
634
624
  }
635
- const sourceId = safeDecode(fm[1]);
625
+ const sourceId = safeDecode(match.params.sessionId);
636
626
  if (sourceId === null || !isUuidV7(sourceId)) {
637
627
  sendError(res, 400, "request.id_invalid", "invalid session id (must be a uuidv7)");
638
628
  return;
@@ -657,8 +647,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
657
647
  }
658
648
  }
659
649
  {
660
- const dm = req.method === "DELETE" ? /^\/v1\/sessions\/([^/]+)$/.exec(url) : null;
661
- if (dm) {
650
+ if (match.decl.id === "session-delete") {
662
651
  const purge = deps.purgeSession;
663
652
  const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
664
653
  if (!purge || !ownerOf) {
@@ -670,7 +659,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
670
659
  sendError(res, 401, "auth.unauthorized", "unauthorized");
671
660
  return;
672
661
  }
673
- const sessionId = safeDecode(dm[1]);
662
+ const sessionId = safeDecode(match.params.sessionId);
674
663
  if (sessionId === null || !isUuidV7(sessionId)) {
675
664
  sendError(res, 400, "request.id_invalid", "invalid session id (must be a uuidv7)");
676
665
  return;
@@ -699,8 +688,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
699
688
  }
700
689
  }
701
690
  {
702
- const pm = req.method === "GET" || req.method === "PUT" ? /^\/v1\/sessions\/([^/]+)\/policy$/.exec(url) : null;
703
- if (pm) {
691
+ if (match.decl.id === "session-policy-get" || match.decl.id === "session-policy-put") {
704
692
  const store = deps.sessionPolicyStore;
705
693
  const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
706
694
  if (!store || !ownerOf) {
@@ -712,7 +700,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
712
700
  sendError(res, 401, "auth.unauthorized", "unauthorized");
713
701
  return;
714
702
  }
715
- const sessionId = safeDecode(pm[1]);
703
+ const sessionId = safeDecode(match.params.sessionId);
716
704
  if (sessionId === null || !isUuidV7(sessionId)) {
717
705
  sendError(res, 400, "request.id_invalid", "invalid session id (must be a uuidv7)");
718
706
  return;
@@ -732,7 +720,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
732
720
  return;
733
721
  }
734
722
  const target = owner;
735
- if (req.method === "GET") {
723
+ if (match.decl.id === "session-policy-get") {
736
724
  sendJson(res, 200, { rules: (await store.getRules(sessionId, target)) ?? { rev: 0 } });
737
725
  return;
738
726
  }
@@ -778,12 +766,27 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
778
766
  }
779
767
  }
780
768
  {
781
- const wm = req.method === "GET" ? /^\/v1\/sessions\/([^/]+)\/workspace(?:\/([^/]+)(?:\/(tree|file|archive))?)?$/.exec(url) : null;
782
- if (wm) {
769
+ if (match.decl.id === "session-workspace") {
783
770
  sendError(res, 501, "capability.workspace_retired", "workspace browse is retired: the whole-tree snapshot epoch it projected was replaced by per-edited-file rewind history (which holds only files the agent edited, not the workspace). There is no whole-tree manifest to browse anymore.");
784
771
  return;
785
772
  }
786
773
  }
787
- miss.fell = true;
774
+ return assertNever(match.decl.id);
788
775
  }
776
+ export const SESSIONS_ROUTES = [
777
+ { id: "session-init", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/init$/, label: "/v1/sessions/:id/$verb", methods: ["GET"] },
778
+ { id: "session-settings", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/settings$/, label: "/v1/sessions/:id/$verb", methods: ["GET"] },
779
+ { id: "session-mcp", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/mcp$/, label: "/v1/sessions/:id/$verb", methods: ["GET"] },
780
+ { id: "session-a2a", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/a2a$/, label: "/v1/sessions/:id/$verb", methods: ["GET"] },
781
+ { id: "session-head", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/head$/, label: "/v1/sessions/:id/$verb", methods: ["GET"] },
782
+ { id: "session-events", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/events$/, label: "/v1/sessions/:id/$verb", methods: ["GET"] },
783
+ { id: "session-fork", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/fork$/, label: "/v1/sessions/:id/$verb", methods: ["POST"] },
784
+ { id: "session-policy-get", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/policy$/, label: "/v1/sessions/:id/$verb", methods: ["GET"] },
785
+ { id: "session-policy-put", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/policy$/, label: "/v1/sessions/:id/$verb", methods: ["PUT"] },
786
+ { id: "session-memory-erase", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/memory\/erase$/, label: "/v1/sessions/:id/memory/erase", methods: ["POST"] },
787
+ { id: "session-memory-status", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/memory-status$/, label: "/v1/sessions/:id/memory-status", methods: ["GET"] },
788
+ { id: "session-workspace", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/workspace(?:\/(?<key>[^/]+)(?:\/(?<sub>tree|file|archive))?)?$/, label: "/v1/sessions/:id/workspace", methods: ["GET"] },
789
+ { id: "session-detail", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)$/, label: "/v1/sessions/:id", methods: ["GET"] },
790
+ { id: "session-delete", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)$/, label: "/v1/sessions/:id", methods: ["DELETE"] },
791
+ ];
789
792
  //# sourceMappingURL=sessions.js.map
@@ -26,6 +26,23 @@
26
26
  * 「库里没有这份文件」对他不是秘密,把它和「你不是成员」合并反而会让成员无法区分打错路径和权限问题。
27
27
  */
28
28
  import type { IncomingMessage, ServerResponse } from "node:http";
29
- import type { RouteCtx } from "../route-ctx.js";
30
- export declare function handleSharedMemory(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
29
+ import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
30
+ export declare function handleSharedMemory(req: IncomingMessage, res: ServerResponse, match: RouteMatch<SharedMemoryRouteId>, ctx: RouteCtx): Promise<void>;
31
+ export declare const SHARED_MEMORY_ROUTES: readonly [{
32
+ readonly id: "shared-memory-stores";
33
+ readonly path: "/v1/shared-memory/stores";
34
+ readonly methods: readonly ["GET"];
35
+ }, {
36
+ readonly id: "shared-memory-store-documents";
37
+ readonly pattern: RegExp;
38
+ readonly label: "/v1/shared-memory/stores/:store/documents";
39
+ readonly methods: readonly ["GET"];
40
+ }, {
41
+ readonly id: "shared-memory-store-document";
42
+ readonly pattern: RegExp;
43
+ readonly label: "/v1/shared-memory/stores/:store/document";
44
+ readonly methods: readonly ["GET"];
45
+ }];
46
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
47
+ export type SharedMemoryRouteId = RouteIdsOf<typeof SHARED_MEMORY_ROUTES>;
31
48
  //# sourceMappingURL=shared-memory.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { SHARED_MEMORY_READ_CAP_BYTES, SharedMemoryStoreError } from "@sema-agent/core";
2
2
  import { sendJson, sendError } from "../send.js";
3
3
  import { gatedPrincipal } from "../principal-gate.js";
4
- const PREFIX = "/v1/shared-memory/";
4
+ import { assertNever } from "../route-ctx.js";
5
5
  const RETRY_AFTER_SEC = 10;
6
6
  const HTTP_LIST_LIMIT = 100;
7
7
  const FORBIDDEN_PATH_CHARS = /[\p{Cc}\p{Cf}\p{Co}\p{Cn}\p{Default_Ignorable_Code_Point}\u2028\u2029\\]/u;
@@ -75,76 +75,78 @@ function resolveStore(res, snapshot, storeId) {
75
75
  }
76
76
  return found.reader;
77
77
  }
78
- export async function handleSharedMemory(req, res, url, ctx) {
79
- const miss = { fell: false };
80
- await handleSharedMemoryBody(req, res, url, ctx, miss);
81
- return !miss.fell;
82
- }
83
- async function handleSharedMemoryBody(req, res, url, ctx, miss) {
84
- if (url === "/v1/shared-memory/stores" && req.method === "GET") {
85
- const snapshot = await bindSnapshot(req, res, ctx);
86
- if (!snapshot)
87
- return;
88
- if (snapshot.state !== "connected") {
89
- sendJson(res, 200, { state: "connecting", stores: [] });
78
+ export async function handleSharedMemory(req, res, match, ctx) {
79
+ switch (match.decl.id) {
80
+ case "shared-memory-stores": {
81
+ const snapshot = await bindSnapshot(req, res, ctx);
82
+ if (!snapshot)
83
+ return;
84
+ if (snapshot.state !== "connected") {
85
+ sendJson(res, 200, { state: "connecting", stores: [] });
86
+ return;
87
+ }
88
+ sendJson(res, 200, { state: "connected", stores: snapshot.stores.map((s) => ({ id: s.info.id, description: s.info.description, writable: s.info.writable })) });
90
89
  return;
91
90
  }
92
- sendJson(res, 200, { state: "connected", stores: snapshot.stores.map((s) => ({ id: s.info.id, description: s.info.description, writable: s.info.writable })) });
93
- return;
94
- }
95
- const rest = url.startsWith(PREFIX) ? url.slice(PREFIX.length) : undefined;
96
- const segments = rest === undefined ? [] : rest.split("/");
97
- if (segments.length === 3 && segments[0] === "stores" && req.method === "GET" && (segments[2] === "documents" || segments[2] === "document")) {
98
- const storeId = decodeURIComponent(segments[1]);
99
- const query = new URL(req.url ?? "", "http://x").searchParams;
100
- const snapshot = await bindSnapshot(req, res, ctx);
101
- if (!snapshot)
102
- return;
103
- const reader = resolveStore(res, snapshot, storeId);
104
- if (!reader)
105
- return;
106
- if (segments[2] === "documents") {
107
- const prefix = query.get("path_prefix") ?? undefined;
108
- const cursor = query.get("cursor") ?? undefined;
109
- const page = await reader.list(prefix, { ...(cursor !== undefined ? { cursor } : {}), limit: HTTP_LIST_LIMIT }).catch((err) => {
110
- if (foldReaderError(res, err))
111
- return undefined;
91
+ case "shared-memory-store-documents":
92
+ case "shared-memory-store-document": {
93
+ const storeId = decodeURIComponent(match.params.store);
94
+ const query = new URLSearchParams(match.search);
95
+ const snapshot = await bindSnapshot(req, res, ctx);
96
+ if (!snapshot)
97
+ return;
98
+ const reader = resolveStore(res, snapshot, storeId);
99
+ if (!reader)
100
+ return;
101
+ if (match.decl.id === "shared-memory-store-documents") {
102
+ const prefix = query.get("path_prefix") ?? undefined;
103
+ const cursor = query.get("cursor") ?? undefined;
104
+ const page = await reader.list(prefix, { ...(cursor !== undefined ? { cursor } : {}), limit: HTTP_LIST_LIMIT }).catch((err) => {
105
+ if (foldReaderError(res, err))
106
+ return undefined;
107
+ throw err;
108
+ });
109
+ if (page === undefined)
110
+ return;
111
+ const entries = Array.isArray(page) ? page : page.entries;
112
+ const nextCursor = Array.isArray(page) ? undefined : page.nextCursor;
113
+ sendJson(res, 200, { store: storeId, entries, ...(nextCursor !== undefined ? { nextCursor } : {}) });
114
+ return;
115
+ }
116
+ const path = query.get("path");
117
+ if (path === null || path === "" || !isDocumentPath(path)) {
118
+ sendError(res, 400, "request.query_invalid", "missing or malformed ?path= (absolute, .md/.txt/.json/.jsonl)");
119
+ return;
120
+ }
121
+ let folded = false;
122
+ const doc = await reader.read(path, {}).catch((err) => {
123
+ if (foldReaderError(res, err)) {
124
+ folded = true;
125
+ return null;
126
+ }
112
127
  throw err;
113
128
  });
114
- if (page === undefined)
129
+ if (folded)
130
+ return;
131
+ if (doc === null) {
132
+ sendError(res, 404, "not_found.shared_memory_document", "not found");
115
133
  return;
116
- const entries = Array.isArray(page) ? page : page.entries;
117
- const nextCursor = Array.isArray(page) ? undefined : page.nextCursor;
118
- sendJson(res, 200, { store: storeId, entries, ...(nextCursor !== undefined ? { nextCursor } : {}) });
119
- return;
120
- }
121
- const path = query.get("path");
122
- if (path === null || path === "" || !isDocumentPath(path)) {
123
- sendError(res, 400, "request.query_invalid", "missing or malformed ?path= (absolute, .md/.txt/.json/.jsonl)");
124
- return;
125
- }
126
- let folded = false;
127
- const doc = await reader.read(path, {}).catch((err) => {
128
- if (foldReaderError(res, err)) {
129
- folded = true;
130
- return null;
131
134
  }
132
- throw err;
133
- });
134
- if (folded)
135
- return;
136
- if (doc === null) {
137
- sendError(res, 404, "not_found.shared_memory_document", "not found");
138
- return;
139
- }
140
- const bytes = Buffer.byteLength(doc.content, "utf8");
141
- if (bytes > SHARED_MEMORY_READ_CAP_BYTES) {
142
- sendError(res, 413, "request.payload_too_large", `document is ${bytes} bytes, over the ${SHARED_MEMORY_READ_CAP_BYTES}-byte shared-store read cap`, { bytes, capBytes: SHARED_MEMORY_READ_CAP_BYTES });
135
+ const bytes = Buffer.byteLength(doc.content, "utf8");
136
+ if (bytes > SHARED_MEMORY_READ_CAP_BYTES) {
137
+ sendError(res, 413, "request.payload_too_large", `document is ${bytes} bytes, over the ${SHARED_MEMORY_READ_CAP_BYTES}-byte shared-store read cap`, { bytes, capBytes: SHARED_MEMORY_READ_CAP_BYTES });
138
+ return;
139
+ }
140
+ sendJson(res, 200, { store: storeId, path, content: doc.content, bytes, ...(doc.updatedAt !== undefined ? { updatedAt: doc.updatedAt } : {}) });
143
141
  return;
144
142
  }
145
- sendJson(res, 200, { store: storeId, path, content: doc.content, bytes, ...(doc.updatedAt !== undefined ? { updatedAt: doc.updatedAt } : {}) });
146
- return;
143
+ default:
144
+ return assertNever(match.decl.id);
147
145
  }
148
- miss.fell = true;
149
146
  }
147
+ export const SHARED_MEMORY_ROUTES = [
148
+ { id: "shared-memory-stores", path: "/v1/shared-memory/stores", methods: ["GET"] },
149
+ { id: "shared-memory-store-documents", pattern: /^\/v1\/shared-memory\/stores\/(?<store>[^/]+)\/documents$/, label: "/v1/shared-memory/stores/:store/documents", methods: ["GET"] },
150
+ { id: "shared-memory-store-document", pattern: /^\/v1\/shared-memory\/stores\/(?<store>[^/]+)\/document$/, label: "/v1/shared-memory/stores/:store/document", methods: ["GET"] },
151
+ ];
150
152
  //# sourceMappingURL=shared-memory.js.map
@@ -7,10 +7,19 @@
7
7
  * 已含本路径,故 no-token 503 / draining / model-ready 三闸在进本域前就已生效。本域零可变状态。
8
8
  */
9
9
  import type { IncomingMessage, ServerResponse } from "node:http";
10
- import type { RouteCtx } from "../route-ctx.js";
10
+ import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
11
11
  /** 表内=core 的前置校验形(客户端可改请求)⇒ 400;表外一律 500。 */
12
12
  export declare function isCorePreflightValidationError(message: string): boolean;
13
13
  /** 供跨仓锚门读:门要拿这张表去装机的 core 里逐条对账(见上方 doc)。 */
14
14
  export declare const CORE_PREFLIGHT_VALIDATION_ANCHORS: readonly string[];
15
- export declare function handleSideQuery(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
15
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。 */
16
+ export declare function handleSideQuery(req: IncomingMessage, res: ServerResponse, match: RouteMatch<SideQueryRouteId>, ctx: RouteCtx): Promise<void>;
17
+ export declare const SIDE_QUERY_ROUTES: readonly [{
18
+ readonly id: "side-query";
19
+ readonly path: "/v1/side-query";
20
+ readonly billable: true;
21
+ readonly methods: readonly ["POST"];
22
+ }];
23
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
24
+ export type SideQueryRouteId = RouteIdsOf<typeof SIDE_QUERY_ROUTES>;
16
25
  //# sourceMappingURL=side-query.d.ts.map