@sema-agent/server 7.79.3 → 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 (74) hide show
  1. package/dist/http/dispatch.js +28 -8
  2. package/dist/http/route-ctx.d.ts +101 -4
  3. package/dist/http/route-ctx.js +4 -0
  4. package/dist/http/route-table.d.ts +71 -43
  5. package/dist/http/route-table.js +111 -187
  6. package/dist/http/routes/a2a-serve.d.ts +18 -3
  7. package/dist/http/routes/a2a-serve.js +25 -23
  8. package/dist/http/routes/admin-config-refresh.d.ts +9 -2
  9. package/dist/http/routes/admin-config-refresh.js +32 -31
  10. package/dist/http/routes/admin-drain.d.ts +9 -2
  11. package/dist/http/routes/admin-drain.js +32 -31
  12. package/dist/http/routes/admin-memory-optout.d.ts +28 -2
  13. package/dist/http/routes/admin-memory-optout.js +26 -39
  14. package/dist/http/routes/adoption.d.ts +15 -2
  15. package/dist/http/routes/adoption.js +52 -52
  16. package/dist/http/routes/agents-roster.d.ts +9 -2
  17. package/dist/http/routes/agents-roster.js +82 -80
  18. package/dist/http/routes/approvals-assistant.d.ts +58 -5
  19. package/dist/http/routes/approvals-assistant.js +37 -38
  20. package/dist/http/routes/attachments.d.ts +22 -2
  21. package/dist/http/routes/attachments.js +81 -72
  22. package/dist/http/routes/capabilities.d.ts +21 -2
  23. package/dist/http/routes/capabilities.js +174 -170
  24. package/dist/http/routes/config-catalog.d.ts +9 -2
  25. package/dist/http/routes/config-catalog.js +28 -31
  26. package/dist/http/routes/devices.d.ts +24 -2
  27. package/dist/http/routes/devices.js +19 -21
  28. package/dist/http/routes/diagnostics.d.ts +9 -2
  29. package/dist/http/routes/diagnostics.js +41 -43
  30. package/dist/http/routes/fleet.d.ts +11 -2
  31. package/dist/http/routes/fleet.js +23 -22
  32. package/dist/http/routes/images.d.ts +63 -9
  33. package/dist/http/routes/images.js +53 -46
  34. package/dist/http/routes/leader.d.ts +20 -2
  35. package/dist/http/routes/leader.js +43 -40
  36. package/dist/http/routes/memory-bundle.d.ts +15 -2
  37. package/dist/http/routes/memory-bundle.js +38 -38
  38. package/dist/http/routes/memory-compliance.d.ts +16 -4
  39. package/dist/http/routes/memory-compliance.js +44 -45
  40. package/dist/http/routes/memory-consolidation.d.ts +14 -2
  41. package/dist/http/routes/memory-consolidation.js +66 -66
  42. package/dist/http/routes/memory-origin.d.ts +23 -4
  43. package/dist/http/routes/memory-origin.js +65 -65
  44. package/dist/http/routes/memory-policy.d.ts +21 -2
  45. package/dist/http/routes/memory-policy.js +119 -115
  46. package/dist/http/routes/notify-wake.d.ts +21 -11
  47. package/dist/http/routes/notify-wake.js +118 -116
  48. package/dist/http/routes/observability.d.ts +16 -2
  49. package/dist/http/routes/observability.js +54 -51
  50. package/dist/http/routes/retention-ops.d.ts +19 -2
  51. package/dist/http/routes/retention-ops.js +20 -22
  52. package/dist/http/routes/rules.d.ts +36 -2
  53. package/dist/http/routes/rules.js +22 -24
  54. package/dist/http/routes/run-memory-capture.d.ts +10 -3
  55. package/dist/http/routes/run-memory-capture.js +84 -84
  56. package/dist/http/routes/runs.d.ts +102 -22
  57. package/dist/http/routes/runs.js +71 -88
  58. package/dist/http/routes/session-sync.d.ts +41 -2
  59. package/dist/http/routes/session-sync.js +25 -23
  60. package/dist/http/routes/sessions-list.d.ts +16 -2
  61. package/dist/http/routes/sessions-list.js +78 -75
  62. package/dist/http/routes/sessions.d.ts +77 -2
  63. package/dist/http/routes/sessions.js +50 -47
  64. package/dist/http/routes/shared-memory.d.ts +19 -2
  65. package/dist/http/routes/shared-memory.js +65 -63
  66. package/dist/http/routes/side-query.d.ts +11 -2
  67. package/dist/http/routes/side-query.js +81 -79
  68. package/dist/http/routes/tasks.d.ts +17 -2
  69. package/dist/http/routes/tasks.js +18 -11
  70. package/dist/http/routes/trace-usage.d.ts +40 -2
  71. package/dist/http/routes/trace-usage.js +46 -35
  72. package/dist/http/routes/workflows.d.ts +35 -3
  73. package/dist/http/routes/workflows.js +29 -29
  74. package/package.json +1 -1
@@ -1,91 +1,90 @@
1
1
  import { isUuidV7, legacyUnownedAmnestyOpen } from "../../security.js";
2
2
  import { sendJson, sendError } from "../send.js";
3
3
  import { filterFingerprint } from "../cursor-fingerprint.js";
4
- export async function handleSessionsList(req, res, url, ctx) {
5
- const miss = { fell: false };
6
- await handleSessionsListBody(req, res, url, ctx, miss);
7
- return !miss.fell;
8
- }
9
- async function handleSessionsListBody(req, res, url, ctx, miss) {
4
+ import { assertNever } from "../route-ctx.js";
5
+ export async function handleSessionsList(req, res, match, ctx) {
10
6
  const { deps } = ctx;
11
7
  const { isFleetWide, sessionOwnerScope } = ctx.helpers;
12
- if (req.method === "GET" && url === "/v1/sessions") {
13
- const { fleetWide, gateOwner } = sessionOwnerScope(req);
14
- if (!fleetWide && gateOwner === null) {
15
- sendError(res, 401, "auth.unauthorized", "unauthorized");
16
- return;
17
- }
18
- const lister = deps.sessionStorage?.listSessions?.bind(deps.sessionStorage) ?? deps.runStore?.listSessions?.bind(deps.runStore);
19
- if (!lister) {
20
- sendError(res, 501, "capability.session_store_required", "session list requires a durable session store (DB_BACKEND=mysql|pg|local)");
21
- return;
22
- }
23
- const includeUnowned = !fleetWide && legacyUnownedAmnestyOpen(deps.config.requirePrincipal, gateOwner);
24
- await handleSessionList(res, lister, new URL(req.url ?? "", "http://x").searchParams, gateOwner, includeUnowned);
25
- return;
26
- }
27
- if (req.method === "GET" && url === "/v1/sendfile-links") {
28
- const { fleetWide, gateOwner } = sessionOwnerScope(req);
29
- if (!fleetWide && gateOwner === null) {
30
- sendError(res, 401, "auth.unauthorized", "unauthorized");
31
- return;
32
- }
33
- if (!deps.sendFileLedger) {
34
- sendError(res, 501, "capability.store_required", "sendfile ledger requires a store backend (DB_BACKEND=mysql|pg|local) and the SendUserFile issuer configured");
35
- return;
36
- }
37
- const query = new URL(req.url ?? "", "http://x").searchParams;
38
- const limitRaw = query.get("limit");
39
- if (limitRaw !== null && !(/^[0-9]+$/.test(limitRaw) && Number(limitRaw) >= 1 && Number(limitRaw) <= 100)) {
40
- sendError(res, 400, "request.query_invalid", "limit must be a decimal integer in 1..100");
41
- return;
42
- }
43
- const limit = limitRaw === null ? 50 : Number(limitRaw);
44
- const beforeRaw = query.get("before");
45
- if (beforeRaw !== null && !isUuidV7(beforeRaw)) {
46
- sendError(res, 400, "request.id_invalid", "before must be a uuidv7 link id (the `id`/`nextBefore` of a previous page)");
8
+ switch (match.decl.id) {
9
+ case "sessions-list": {
10
+ const { fleetWide, gateOwner } = sessionOwnerScope(req);
11
+ if (!fleetWide && gateOwner === null) {
12
+ sendError(res, 401, "auth.unauthorized", "unauthorized");
13
+ return;
14
+ }
15
+ const lister = deps.sessionStorage?.listSessions?.bind(deps.sessionStorage) ?? deps.runStore?.listSessions?.bind(deps.runStore);
16
+ if (!lister) {
17
+ sendError(res, 501, "capability.session_store_required", "session list requires a durable session store (DB_BACKEND=mysql|pg|local)");
18
+ return;
19
+ }
20
+ const includeUnowned = !fleetWide && legacyUnownedAmnestyOpen(deps.config.requirePrincipal, gateOwner);
21
+ await handleSessionList(res, lister, new URLSearchParams(match.search), gateOwner, includeUnowned);
47
22
  return;
48
23
  }
49
- const before = beforeRaw ?? undefined;
50
- let scope;
51
- if (gateOwner !== null) {
52
- scope = gateOwner;
53
- }
54
- else {
55
- const scopeRaw = query.get("scope");
56
- if (scopeRaw === null) {
57
- scope = "_";
24
+ case "sendfile-links": {
25
+ const { fleetWide, gateOwner } = sessionOwnerScope(req);
26
+ if (!fleetWide && gateOwner === null) {
27
+ sendError(res, 401, "auth.unauthorized", "unauthorized");
28
+ return;
29
+ }
30
+ if (!deps.sendFileLedger) {
31
+ sendError(res, 501, "capability.store_required", "sendfile ledger requires a store backend (DB_BACKEND=mysql|pg|local) and the SendUserFile issuer configured");
32
+ return;
33
+ }
34
+ const query = new URLSearchParams(match.search);
35
+ const limitRaw = query.get("limit");
36
+ if (limitRaw !== null && !(/^[0-9]+$/.test(limitRaw) && Number(limitRaw) >= 1 && Number(limitRaw) <= 100)) {
37
+ sendError(res, 400, "request.query_invalid", "limit must be a decimal integer in 1..100");
38
+ return;
39
+ }
40
+ const limit = limitRaw === null ? 50 : Number(limitRaw);
41
+ const beforeRaw = query.get("before");
42
+ if (beforeRaw !== null && !isUuidV7(beforeRaw)) {
43
+ sendError(res, 400, "request.id_invalid", "before must be a uuidv7 link id (the `id`/`nextBefore` of a previous page)");
44
+ return;
45
+ }
46
+ const before = beforeRaw ?? undefined;
47
+ let scope;
48
+ if (gateOwner !== null) {
49
+ scope = gateOwner;
58
50
  }
59
51
  else {
60
- const s = scopeRaw.trim();
61
- if (s === "" || s.length > 190) {
62
- sendError(res, 400, "request.query_invalid", "scope must be a non-blank tenant principal of at most 190 characters (the sendfile_link.scope column width)");
63
- return;
52
+ const scopeRaw = query.get("scope");
53
+ if (scopeRaw === null) {
54
+ scope = "_";
55
+ }
56
+ else {
57
+ const s = scopeRaw.trim();
58
+ if (s === "" || s.length > 190) {
59
+ sendError(res, 400, "request.query_invalid", "scope must be a non-blank tenant principal of at most 190 characters (the sendfile_link.scope column width)");
60
+ return;
61
+ }
62
+ scope = s;
64
63
  }
65
- scope = s;
66
64
  }
65
+ const rows = await deps.sendFileLedger.listByScope(scope, limit + 1, before);
66
+ const hasMore = rows.length > limit;
67
+ const page = hasMore ? rows.slice(0, limit) : rows;
68
+ sendJson(res, 200, {
69
+ links: page.map((r) => ({
70
+ id: r.id,
71
+ filename: r.filename,
72
+ size: r.size,
73
+ ttlSec: r.ttlSec,
74
+ track: r.track,
75
+ bucket: r.bucket,
76
+ key: r.objectKey,
77
+ createdAt: new Date(r.createdAt).toISOString(),
78
+ ...(r.taskId !== undefined ? { taskId: r.taskId } : {}),
79
+ ...(r.sessionId !== undefined ? { sessionId: r.sessionId } : {}),
80
+ })),
81
+ ...(hasMore ? { nextBefore: page[page.length - 1].id } : {}),
82
+ });
83
+ return;
67
84
  }
68
- const rows = await deps.sendFileLedger.listByScope(scope, limit + 1, before);
69
- const hasMore = rows.length > limit;
70
- const page = hasMore ? rows.slice(0, limit) : rows;
71
- sendJson(res, 200, {
72
- links: page.map((r) => ({
73
- id: r.id,
74
- filename: r.filename,
75
- size: r.size,
76
- ttlSec: r.ttlSec,
77
- track: r.track,
78
- bucket: r.bucket,
79
- key: r.objectKey,
80
- createdAt: new Date(r.createdAt).toISOString(),
81
- ...(r.taskId !== undefined ? { taskId: r.taskId } : {}),
82
- ...(r.sessionId !== undefined ? { sessionId: r.sessionId } : {}),
83
- })),
84
- ...(hasMore ? { nextBefore: page[page.length - 1].id } : {}),
85
- });
86
- return;
85
+ default:
86
+ return assertNever(match.decl.id);
87
87
  }
88
- miss.fell = true;
89
88
  }
90
89
  function encodeSessionCursor(c, f) {
91
90
  return Buffer.from(JSON.stringify({ ...c, f })).toString("base64url");
@@ -123,4 +122,8 @@ async function handleSessionList(res, lister, query, forceOwner, includeUnowned
123
122
  const nextCursor = hasMore && last ? encodeSessionCursor({ lastActivityAt: last.lastActivityAt, sessionId: last.sessionId }, f) : undefined;
124
123
  sendJson(res, 200, { sessions: page, ...(nextCursor ? { nextCursor } : {}) });
125
124
  }
125
+ export const SESSIONS_LIST_ROUTES = [
126
+ { id: "sessions-list", path: "/v1/sessions", methods: ["GET"] },
127
+ { id: "sendfile-links", path: "/v1/sendfile-links", methods: ["GET"] },
128
+ ];
126
129
  //# sourceMappingURL=sessions-list.js.map
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import type { IncomingMessage, ServerResponse } from "node:http";
15
15
  import { type SessionMemoryStatus } from "@sema-agent/core";
16
- import type { RouteCtx } from "../route-ctx.js";
16
+ import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
17
17
  /** 单域独占状态。每个 server 实例一份——模块级会把连接坑/缓存跨实例串味。 */
18
18
  export interface SessionsLocal {
19
19
  sseConnections: number;
@@ -46,5 +46,80 @@ export interface SessionsLocal {
46
46
  }>;
47
47
  }
48
48
  export declare function createSessionsLocal(): SessionsLocal;
49
- export declare function handleSessions(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
49
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(去缩进两格;sessions 另有一处见文件头说明)。
50
+ * 裸 `return;` 的语义原样 =「本域已应答」;走到函数尾才是「没匹配上」,置 `miss.fell` 交回装配器继续下一个域。 */
51
+ export declare function handleSessions(req: IncomingMessage, res: ServerResponse, match: RouteMatch<SessionsRouteId>, ctx: RouteCtx): Promise<void>;
52
+ export declare const SESSIONS_ROUTES: readonly [{
53
+ readonly id: "session-init";
54
+ readonly pattern: RegExp;
55
+ readonly label: "/v1/sessions/:id/$verb";
56
+ readonly methods: readonly ["GET"];
57
+ }, {
58
+ readonly id: "session-settings";
59
+ readonly pattern: RegExp;
60
+ readonly label: "/v1/sessions/:id/$verb";
61
+ readonly methods: readonly ["GET"];
62
+ }, {
63
+ readonly id: "session-mcp";
64
+ readonly pattern: RegExp;
65
+ readonly label: "/v1/sessions/:id/$verb";
66
+ readonly methods: readonly ["GET"];
67
+ }, {
68
+ readonly id: "session-a2a";
69
+ readonly pattern: RegExp;
70
+ readonly label: "/v1/sessions/:id/$verb";
71
+ readonly methods: readonly ["GET"];
72
+ }, {
73
+ readonly id: "session-head";
74
+ readonly pattern: RegExp;
75
+ readonly label: "/v1/sessions/:id/$verb";
76
+ readonly methods: readonly ["GET"];
77
+ }, {
78
+ readonly id: "session-events";
79
+ readonly pattern: RegExp;
80
+ readonly label: "/v1/sessions/:id/$verb";
81
+ readonly methods: readonly ["GET"];
82
+ }, {
83
+ readonly id: "session-fork";
84
+ readonly pattern: RegExp;
85
+ readonly label: "/v1/sessions/:id/$verb";
86
+ readonly methods: readonly ["POST"];
87
+ }, {
88
+ readonly id: "session-policy-get";
89
+ readonly pattern: RegExp;
90
+ readonly label: "/v1/sessions/:id/$verb";
91
+ readonly methods: readonly ["GET"];
92
+ }, {
93
+ readonly id: "session-policy-put";
94
+ readonly pattern: RegExp;
95
+ readonly label: "/v1/sessions/:id/$verb";
96
+ readonly methods: readonly ["PUT"];
97
+ }, {
98
+ readonly id: "session-memory-erase";
99
+ readonly pattern: RegExp;
100
+ readonly label: "/v1/sessions/:id/memory/erase";
101
+ readonly methods: readonly ["POST"];
102
+ }, {
103
+ readonly id: "session-memory-status";
104
+ readonly pattern: RegExp;
105
+ readonly label: "/v1/sessions/:id/memory-status";
106
+ readonly methods: readonly ["GET"];
107
+ }, {
108
+ readonly id: "session-workspace";
109
+ readonly pattern: RegExp;
110
+ readonly label: "/v1/sessions/:id/workspace";
111
+ readonly methods: readonly ["GET"];
112
+ }, {
113
+ readonly id: "session-detail";
114
+ readonly pattern: RegExp;
115
+ readonly label: "/v1/sessions/:id";
116
+ readonly methods: readonly ["GET"];
117
+ }, {
118
+ readonly id: "session-delete";
119
+ readonly pattern: RegExp;
120
+ readonly label: "/v1/sessions/:id";
121
+ readonly methods: readonly ["DELETE"];
122
+ }];
123
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
124
+ export type SessionsRouteId = RouteIdsOf<typeof SESSIONS_ROUTES>;
50
125
  //# sourceMappingURL=sessions.d.ts.map
@@ -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