@desplega.ai/agent-swarm 1.79.4 → 1.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 (130) hide show
  1. package/openapi.json +496 -32
  2. package/package.json +14 -6
  3. package/src/artifact-sdk/server.ts +2 -1
  4. package/src/be/db.ts +102 -31
  5. package/src/be/migrations/063_cost_context_schema_relax.sql +133 -0
  6. package/src/be/migrations/064_scripts.sql +39 -0
  7. package/src/be/migrations/065_script_embeddings.sql +7 -0
  8. package/src/be/pricing-normalize.ts +81 -0
  9. package/src/be/scripts/db.ts +391 -0
  10. package/src/be/scripts/embeddings.ts +231 -0
  11. package/src/be/scripts/maintenance.ts +9 -0
  12. package/src/be/scripts/typecheck.ts +193 -0
  13. package/src/be/seed-pricing.ts +293 -0
  14. package/src/cli.tsx +22 -5
  15. package/src/commands/artifact.ts +3 -2
  16. package/src/commands/claude-managed-setup.ts +21 -4
  17. package/src/commands/codex-login.ts +5 -3
  18. package/src/commands/onboard.tsx +2 -1
  19. package/src/commands/runner.ts +663 -246
  20. package/src/commands/setup.tsx +5 -3
  21. package/src/hooks/hook.ts +4 -3
  22. package/src/http/context.ts +6 -2
  23. package/src/http/index.ts +126 -68
  24. package/src/http/memory.ts +28 -0
  25. package/src/http/openapi.ts +1 -0
  26. package/src/http/page-proxy.ts +2 -1
  27. package/src/http/route-def.ts +1 -0
  28. package/src/http/schedules.ts +37 -0
  29. package/src/http/scripts.ts +381 -0
  30. package/src/http/session-data.ts +74 -23
  31. package/src/linear/outbound.ts +9 -2
  32. package/src/otel-impl.ts +200 -0
  33. package/src/otel.ts +132 -0
  34. package/src/providers/claude-adapter.ts +52 -6
  35. package/src/providers/claude-managed-adapter.ts +43 -17
  36. package/src/providers/claude-managed-pricing.ts +34 -0
  37. package/src/providers/codex-adapter.ts +38 -27
  38. package/src/providers/codex-models.ts +22 -3
  39. package/src/providers/devin-adapter.ts +11 -0
  40. package/src/providers/opencode-adapter.ts +31 -7
  41. package/src/providers/pi-mono-adapter.ts +39 -7
  42. package/src/providers/pricing-sources.md +52 -0
  43. package/src/providers/swarm-events-shared.ts +8 -4
  44. package/src/providers/types.ts +33 -10
  45. package/src/scripts-runtime/ctx.ts +23 -0
  46. package/src/scripts-runtime/eval-harness.ts +39 -0
  47. package/src/scripts-runtime/executors/native.ts +229 -0
  48. package/src/scripts-runtime/executors/registry.ts +16 -0
  49. package/src/scripts-runtime/executors/types.ts +63 -0
  50. package/src/scripts-runtime/extract-signature.ts +81 -0
  51. package/src/scripts-runtime/import-allowlist.ts +109 -0
  52. package/src/scripts-runtime/loader.ts +96 -0
  53. package/src/scripts-runtime/redacted.ts +48 -0
  54. package/src/scripts-runtime/sdk-allowlist.ts +29 -0
  55. package/src/scripts-runtime/stdlib/fetch.ts +46 -0
  56. package/src/scripts-runtime/stdlib/glob.ts +8 -0
  57. package/src/scripts-runtime/stdlib/grep.ts +34 -0
  58. package/src/scripts-runtime/stdlib/index.ts +16 -0
  59. package/src/scripts-runtime/stdlib/table.ts +17 -0
  60. package/src/scripts-runtime/swarm-config.ts +35 -0
  61. package/src/scripts-runtime/swarm-sdk.ts +197 -0
  62. package/src/scripts-runtime/types/stdlib.d.ts +104 -0
  63. package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
  64. package/src/server.ts +18 -0
  65. package/src/tests/api-key.test.ts +33 -0
  66. package/src/tests/claude-managed-adapter.test.ts +17 -3
  67. package/src/tests/claude-managed-setup.test.ts +10 -1
  68. package/src/tests/codex-adapter.test.ts +20 -19
  69. package/src/tests/codex-login.test.ts +1 -1
  70. package/src/tests/context-snapshot.test.ts +2 -2
  71. package/src/tests/context-window.test.ts +65 -1
  72. package/src/tests/devin-adapter.test.ts +2 -0
  73. package/src/tests/http/context-routes.test.ts +161 -0
  74. package/src/tests/linear-outbound-sync.test.ts +109 -0
  75. package/src/tests/mcp-tools.test.ts +69 -0
  76. package/src/tests/migration-063-schema-relax.test.ts +109 -0
  77. package/src/tests/opencode-adapter.test.ts +146 -1
  78. package/src/tests/otel-impl-secret-scrubbing.test.ts +33 -0
  79. package/src/tests/pages-view-count.test.ts +30 -5
  80. package/src/tests/providers/codex-cost.test.ts +18 -0
  81. package/src/tests/providers/opencode-cost.test.ts +74 -0
  82. package/src/tests/providers/pi-cost.test.ts +128 -0
  83. package/src/tests/redacted.test.ts +29 -0
  84. package/src/tests/runner-tool-spans.test.ts +268 -0
  85. package/src/tests/script-executor-conformance.test.ts +142 -0
  86. package/src/tests/script-executor-registry.test.ts +17 -0
  87. package/src/tests/scripts-db.test.ts +329 -0
  88. package/src/tests/scripts-embeddings.test.ts +291 -0
  89. package/src/tests/scripts-extract-signature.test.ts +47 -0
  90. package/src/tests/scripts-http.test.ts +350 -0
  91. package/src/tests/scripts-import-allowlist.test.ts +55 -0
  92. package/src/tests/scripts-mcp-e2e.test.ts +269 -0
  93. package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
  94. package/src/tests/scripts-runtime.test.ts +289 -0
  95. package/src/tests/sdk-allowlist.test.ts +59 -0
  96. package/src/tests/secret-scrubber.test.ts +54 -1
  97. package/src/tests/session-costs-codex-recompute.test.ts +35 -22
  98. package/src/tests/session-costs-model-key-normalize.test.ts +271 -0
  99. package/src/tests/session-costs-recompute-all-providers.test.ts +170 -0
  100. package/src/tests/store-progress-cost.test.ts +6 -1
  101. package/src/tests/swarm-config.test.ts +38 -0
  102. package/src/tests/tool-annotations.test.ts +2 -2
  103. package/src/tests/tool-call-progress.test.ts +30 -0
  104. package/src/tests/workflow-e2e.test.ts +218 -0
  105. package/src/tests/workflow-executors.test.ts +32 -2
  106. package/src/tests/workflow-input-redaction.test.ts +232 -0
  107. package/src/tests/workflow-swarm-script.test.ts +273 -0
  108. package/src/tools/memory-rate.ts +2 -1
  109. package/src/tools/script-common.ts +88 -0
  110. package/src/tools/script-delete.ts +35 -0
  111. package/src/tools/script-query-types.ts +37 -0
  112. package/src/tools/script-run.ts +43 -0
  113. package/src/tools/script-search.ts +32 -0
  114. package/src/tools/script-upsert.ts +43 -0
  115. package/src/tools/store-progress.ts +16 -60
  116. package/src/tools/tool-config.ts +7 -0
  117. package/src/tools/utils.ts +65 -12
  118. package/src/types.ts +122 -10
  119. package/src/utils/api-key.ts +28 -0
  120. package/src/utils/context-window.ts +104 -4
  121. package/src/utils/page-session.ts +8 -6
  122. package/src/utils/secret-scrubber.ts +29 -1
  123. package/src/workflows/engine.ts +12 -4
  124. package/src/workflows/executors/index.ts +1 -0
  125. package/src/workflows/executors/registry.ts +2 -0
  126. package/src/workflows/executors/script.ts +12 -1
  127. package/src/workflows/executors/swarm-script.ts +170 -0
  128. package/src/workflows/input.ts +65 -0
  129. package/src/workflows/recovery.ts +31 -3
  130. package/src/workflows/resume.ts +43 -5
@@ -2,6 +2,7 @@
2
2
  import { Spinner, TextInput } from "@inkjs/ui";
3
3
  import { Box, Text, useApp } from "ink";
4
4
  import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { getApiKey } from "../utils/api-key.ts";
5
6
  import {
6
7
  createDefaultMcpJson,
7
8
  createDefaultSettingsLocal,
@@ -47,7 +48,7 @@ export function Setup({ dryRun = false, restore = false, yes = false }: SetupPro
47
48
  const { exit } = useApp();
48
49
  const [state, setState] = useState<SetupState>({
49
50
  step: restore ? "restoring" : "check_dirs",
50
- token: yes ? process.env.API_KEY || "" : "",
51
+ token: yes ? getApiKey() : "",
51
52
  agentId: yes ? process.env.AGENT_ID || "" : "",
52
53
  existingToken: "",
53
54
  existingAgentId: "",
@@ -258,14 +259,15 @@ export function Setup({ dryRun = false, restore = false, yes = false }: SetupPro
258
259
 
259
260
  // In non-interactive mode (yes=true), skip prompts and go directly to updating
260
261
  if (yes) {
261
- const token = process.env.API_KEY;
262
+ const token = getApiKey();
262
263
  const agentId = process.env.AGENT_ID;
263
264
 
264
265
  if (!token) {
265
266
  setState((s) => ({
266
267
  ...s,
267
268
  step: "error",
268
- error: "API_KEY environment variable is required in non-interactive mode (-y/--yes)",
269
+ error:
270
+ "AGENT_SWARM_API_KEY (or legacy API_KEY) environment variable is required in non-interactive mode (-y/--yes)",
269
271
  }));
270
272
  return;
271
273
  }
package/src/hooks/hook.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  type RetrievalRow,
11
11
  } from "../be/memory/raters/llm";
12
12
  import type { Agent } from "../types";
13
+ import { getApiKey } from "../utils/api-key";
13
14
  import { summarizeSession as runSummarize } from "../utils/internal-ai";
14
15
  import { checkToolLoop, clearToolHistory } from "./tool-loop-detection";
15
16
 
@@ -150,7 +151,7 @@ async function fetchTaskDetails(
150
151
  taskId: string,
151
152
  ): Promise<{ id: string; task: string; progress?: string } | null> {
152
153
  const apiUrl = process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
153
- const apiKey = process.env.API_KEY || "";
154
+ const apiKey = getApiKey();
154
155
  const headers: Record<string, string> = {};
155
156
  if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
156
157
 
@@ -301,7 +302,7 @@ export async function runStopHookSessionSummary(
301
302
  const { taskContext, taskId } = await resolveStopHookTaskContext(env);
302
303
 
303
304
  const apiUrl = env.MCP_BASE_URL || `http://localhost:${env.PORT || "3013"}`;
304
- const apiKey = env.API_KEY || "";
305
+ const apiKey = getApiKey(env);
305
306
 
306
307
  // Memory-rater v1.5 step-4: piggyback per-memory ratings on the
307
308
  // existing summary call when MEMORY_RATERS includes `llm`.
@@ -1152,7 +1153,7 @@ ${hasAgentIdHeader() ? `You have a pre-defined agent ID via header: ${mcpConfig?
1152
1153
  try {
1153
1154
  const apiUrl =
1154
1155
  process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
1155
- const apiKey = process.env.API_KEY || "";
1156
+ const apiKey = getApiKey();
1156
1157
  const fileContent = await Bun.file(editedPath).text();
1157
1158
  const isShared = editedPath.startsWith("/workspace/shared/");
1158
1159
  const fileName = editedPath.split("/").pop() ?? "unnamed";
@@ -6,7 +6,7 @@ import {
6
6
  getContextSummaryByTaskId,
7
7
  getTaskById,
8
8
  } from "../be/db";
9
- import { ContextSnapshotEventTypeSchema } from "../types";
9
+ import { ContextFormulaSchema, ContextSnapshotEventTypeSchema } from "../types";
10
10
  import { route } from "./route-def";
11
11
  import { json, jsonError } from "./utils";
12
12
 
@@ -25,10 +25,13 @@ const postContext = route({
25
25
  contextUsedTokens: z.number().int().min(0).optional(),
26
26
  contextTotalTokens: z.number().int().min(0).optional(),
27
27
  contextPercent: z.number().min(0).max(100).optional(),
28
- compactTrigger: z.enum(["auto", "manual"]).optional(),
28
+ compactTrigger: z.enum(["auto", "manual", "auto-inferred"]).optional(),
29
29
  preCompactTokens: z.number().int().min(0).optional(),
30
30
  cumulativeInputTokens: z.number().int().min(0).optional(),
31
31
  cumulativeOutputTokens: z.number().int().min(0).optional(),
32
+ // Migration 063: adapters tag the formula they used so cross-provider
33
+ // comparisons can tell apples from oranges.
34
+ contextFormula: ContextFormulaSchema.optional(),
32
35
  }),
33
36
  responses: {
34
37
  200: { description: "Snapshot recorded" },
@@ -91,6 +94,7 @@ export async function handleContext(
91
94
  preCompactTokens: parsed.body.preCompactTokens,
92
95
  cumulativeInputTokens: parsed.body.cumulativeInputTokens,
93
96
  cumulativeOutputTokens: parsed.body.cumulativeOutputTokens,
97
+ contextFormula: parsed.body.contextFormula,
94
98
  });
95
99
 
96
100
  json(res, { ok: true, snapshotId: snapshot.id });
package/src/http/index.ts CHANGED
@@ -14,8 +14,10 @@ import { initGitLab } from "../gitlab";
14
14
  import { stopHeartbeat } from "../heartbeat";
15
15
  import { initJira } from "../jira";
16
16
  import { initLinear } from "../linear";
17
+ import { initOtel, isPollTracingEnabled, startSpan, withRemoteContext } from "../otel";
17
18
  import { startSlackApp, stopSlackApp } from "../slack";
18
19
  import { initTelemetry, telemetry } from "../telemetry";
20
+ import { getApiKey } from "../utils/api-key";
19
21
  import { initWorkflows } from "../workflows";
20
22
  import { handleActiveSessions } from "./active-sessions";
21
23
  import { handleAgentRegister, handleAgentsRest } from "./agents";
@@ -44,6 +46,7 @@ import { handlePricing } from "./pricing";
44
46
  import { handlePromptTemplates } from "./prompt-templates";
45
47
  import { handleRepos } from "./repos";
46
48
  import { handleSchedules } from "./schedules";
49
+ import { handleScripts } from "./scripts";
47
50
  import { handleSessionData } from "./session-data";
48
51
  import { handleSessions } from "./sessions";
49
52
  import { handleSkills } from "./skills";
@@ -68,7 +71,7 @@ process.on("unhandledRejection", (reason) => {
68
71
  });
69
72
 
70
73
  const port = parseInt(process.env.PORT || process.argv[2] || "3013", 10);
71
- const apiKey = process.env.API_KEY || "";
74
+ const apiKey = getApiKey();
72
75
 
73
76
  // Use globalThis to persist state across hot reloads
74
77
  const globalState = globalThis as typeof globalThis & {
@@ -89,6 +92,7 @@ const transports: Record<string, StreamableHTTPServerTransport> = globalState.__
89
92
  const httpServer = createHttpServer(async (req, res) => {
90
93
  const startTime = performance.now();
91
94
  let statusCode = 200;
95
+ let spanEnded = false;
92
96
 
93
97
  // Wrap writeHead to capture status code
94
98
  const originalWriteHead = res.writeHead.bind(res);
@@ -113,76 +117,117 @@ const httpServer = createHttpServer(async (req, res) => {
113
117
  console.error(`[HTTP] ❌ ${req.method} ${req.url} → Error: ${err.message}`);
114
118
  });
115
119
 
116
- setCorsHeaders(req, res);
117
-
118
- // ── Core routes (OPTIONS, health, auth, /me, /cancelled-tasks, /ping, /close) ──
119
- if (await handleCore(req, res, req.headers["x-agent-id"] as string | undefined, apiKey)) return;
120
-
121
- const pathSegments = getPathSegments(req.url || "");
122
- const queryParams = parseQueryParams(req.url || "");
123
- const myAgentId = req.headers["x-agent-id"] as string | undefined;
124
-
125
- // ── Route handlers (order matters — first match wins) ──
126
- const handlers: (() => Promise<boolean>)[] = [
127
- () => handleAgentRegister(req, res, pathSegments, myAgentId),
128
- () => handlePoll(req, res, pathSegments, queryParams, myAgentId),
129
- () => handleSessionData(req, res, pathSegments, queryParams, myAgentId),
130
- () => handleEcosystem(req, res, pathSegments, myAgentId),
131
- () => handleTrackers(req, res, pathSegments),
132
- () => handleWebhooks(req, res, pathSegments),
133
- () => handleAgentsRest(req, res, pathSegments, queryParams, myAgentId),
134
- () => handleBudgets(req, res, pathSegments, queryParams, myAgentId),
135
- () => handleContext(req, res, pathSegments, queryParams, myAgentId),
136
- () => handleTasks(req, res, pathSegments, queryParams, myAgentId),
137
- () => handleStats(req, res, pathSegments, queryParams),
138
- () => handleStatus(req, res, pathSegments, queryParams),
139
- () => handleActiveSessions(req, res, pathSegments, queryParams, myAgentId),
140
- () => handlePricing(req, res, pathSegments, queryParams, myAgentId),
141
- () => handleSchedules(req, res, pathSegments, queryParams, myAgentId),
142
- () => handleWorkflows(req, res, pathSegments, queryParams, myAgentId),
143
- () => handleWorkflowEvents(req, res, pathSegments, queryParams),
144
- () => handleApprovalRequests(req, res, pathSegments, queryParams),
145
- () => handleConfig(req, res, pathSegments, queryParams),
146
- () => handleKv(req, res, pathSegments, queryParams),
147
- () => handleIntegrations(req, res, pathSegments),
148
- () => handlePromptTemplates(req, res, pathSegments, queryParams),
149
- () => handleDbQuery(req, res, pathSegments, queryParams),
150
- () => handleRepos(req, res, pathSegments, queryParams),
151
- () => handleSkills(req, res, pathSegments, queryParams, myAgentId),
152
- () => handleMcpServers(req, res, pathSegments, queryParams),
153
- () => handleMcpOAuth(req, res, pathSegments, queryParams),
154
- () => handleMemory(req, res, pathSegments, myAgentId),
155
- () => handlePagesPublic(req, res, pathSegments, queryParams),
156
- () => handlePageProxy(req, res),
157
- () => handlePages(req, res, pathSegments, queryParams, myAgentId),
158
- () => handleApiKeys(req, res, pathSegments, queryParams),
159
- () => handleHeartbeat(req, res, pathSegments),
160
- () => handleEvents(req, res, pathSegments, queryParams, myAgentId),
161
- () => handleUsers(req, res, pathSegments, queryParams),
162
- () => handleSessions(req, res, pathSegments, queryParams),
163
- () => handleInboxState(req, res, pathSegments, queryParams),
164
- () => handleTaskTemplates(req, res, pathSegments, queryParams),
165
- () => handleMcp(req, res, transports),
166
- ];
167
-
168
- try {
169
- for (const handler of handlers) {
170
- if (await handler()) return;
120
+ await withRemoteContext(req.headers as Record<string, unknown>, async () => {
121
+ const reqPath = req.url?.split("?")[0] ?? "";
122
+ const skipSpan = reqPath === "/api/poll" && !isPollTracingEnabled();
123
+ const span = skipSpan
124
+ ? null
125
+ : startSpan("http.server", {
126
+ "http.request.method": req.method ?? "",
127
+ "url.path": reqPath,
128
+ "agent.id": req.headers["x-agent-id"] as string | undefined,
129
+ "agentswarm.component": "api",
130
+ });
131
+
132
+ if (span) {
133
+ res.on("finish", () => {
134
+ if (spanEnded) return;
135
+ spanEnded = true;
136
+ span.setAttributes({
137
+ "http.response.status_code": statusCode,
138
+ "agentswarm.http.duration_ms": Math.round((performance.now() - startTime) * 10) / 10,
139
+ });
140
+ if (statusCode >= 500) {
141
+ span.setStatus({ code: 2, message: `HTTP ${statusCode}` });
142
+ }
143
+ span.end();
144
+ });
145
+
146
+ res.on("error", (err) => {
147
+ if (spanEnded) return;
148
+ spanEnded = true;
149
+ span.recordException(err);
150
+ span.setStatus({ code: 2, message: err.message });
151
+ span.end();
152
+ });
171
153
  }
172
154
 
173
- // ── 404 ──
174
- res.writeHead(404);
175
- res.end("Not Found");
176
- } catch (err) {
177
- const message = err instanceof Error ? err.message : String(err);
178
- console.error(`[HTTP] ${req.method} ${req.url} ${message}`);
179
- if (!res.headersSent) {
180
- res.writeHead(500, { "Content-Type": "application/json" });
181
- res.end(JSON.stringify({ error: message }));
182
- } else if (!res.writableEnded) {
183
- res.end();
155
+ setCorsHeaders(req, res);
156
+
157
+ // ── Core routes (OPTIONS, health, auth, /me, /cancelled-tasks, /ping, /close) ──
158
+ if (await handleCore(req, res, req.headers["x-agent-id"] as string | undefined, apiKey)) return;
159
+
160
+ const pathSegments = getPathSegments(req.url || "");
161
+ const queryParams = parseQueryParams(req.url || "");
162
+ const myAgentId = req.headers["x-agent-id"] as string | undefined;
163
+
164
+ // ── Route handlers (order matters — first match wins) ──
165
+ const handlers: (() => Promise<boolean>)[] = [
166
+ () => handleAgentRegister(req, res, pathSegments, myAgentId),
167
+ () => handlePoll(req, res, pathSegments, queryParams, myAgentId),
168
+ () => handleSessionData(req, res, pathSegments, queryParams, myAgentId),
169
+ () => handleEcosystem(req, res, pathSegments, myAgentId),
170
+ () => handleTrackers(req, res, pathSegments),
171
+ () => handleWebhooks(req, res, pathSegments),
172
+ () => handleAgentsRest(req, res, pathSegments, queryParams, myAgentId),
173
+ () => handleBudgets(req, res, pathSegments, queryParams, myAgentId),
174
+ () => handleContext(req, res, pathSegments, queryParams, myAgentId),
175
+ () => handleTasks(req, res, pathSegments, queryParams, myAgentId),
176
+ () => handleStats(req, res, pathSegments, queryParams),
177
+ () => handleStatus(req, res, pathSegments, queryParams),
178
+ () => handleActiveSessions(req, res, pathSegments, queryParams, myAgentId),
179
+ () => handlePricing(req, res, pathSegments, queryParams, myAgentId),
180
+ () => handleSchedules(req, res, pathSegments, queryParams, myAgentId),
181
+ () => handleWorkflows(req, res, pathSegments, queryParams, myAgentId),
182
+ () => handleWorkflowEvents(req, res, pathSegments, queryParams),
183
+ () => handleApprovalRequests(req, res, pathSegments, queryParams),
184
+ () => handleConfig(req, res, pathSegments, queryParams),
185
+ () => handleKv(req, res, pathSegments, queryParams),
186
+ () => handleIntegrations(req, res, pathSegments),
187
+ () => handlePromptTemplates(req, res, pathSegments, queryParams),
188
+ () => handleDbQuery(req, res, pathSegments, queryParams),
189
+ () => handleRepos(req, res, pathSegments, queryParams),
190
+ () => handleSkills(req, res, pathSegments, queryParams, myAgentId),
191
+ () => handleScripts(req, res, pathSegments, queryParams, myAgentId),
192
+ () => handleMcpServers(req, res, pathSegments, queryParams),
193
+ () => handleMcpOAuth(req, res, pathSegments, queryParams),
194
+ () => handleMemory(req, res, pathSegments, myAgentId),
195
+ () => handlePagesPublic(req, res, pathSegments, queryParams),
196
+ () => handlePageProxy(req, res),
197
+ () => handlePages(req, res, pathSegments, queryParams, myAgentId),
198
+ () => handleApiKeys(req, res, pathSegments, queryParams),
199
+ () => handleHeartbeat(req, res, pathSegments),
200
+ () => handleEvents(req, res, pathSegments, queryParams, myAgentId),
201
+ () => handleUsers(req, res, pathSegments, queryParams),
202
+ () => handleSessions(req, res, pathSegments, queryParams),
203
+ () => handleInboxState(req, res, pathSegments, queryParams),
204
+ () => handleTaskTemplates(req, res, pathSegments, queryParams),
205
+ () => handleMcp(req, res, transports),
206
+ ];
207
+
208
+ try {
209
+ for (const handler of handlers) {
210
+ if (await handler()) return;
211
+ }
212
+
213
+ // ── 404 ──
214
+ res.writeHead(404);
215
+ res.end("Not Found");
216
+ } catch (err) {
217
+ if (span) {
218
+ span.recordException(err);
219
+ span.setStatus({ code: 2, message: err instanceof Error ? err.message : String(err) });
220
+ }
221
+ const message = err instanceof Error ? err.message : String(err);
222
+ console.error(`[HTTP] ❌ ${req.method} ${req.url} → ${message}`);
223
+ if (!res.headersSent) {
224
+ res.writeHead(500, { "Content-Type": "application/json" });
225
+ res.end(JSON.stringify({ error: message }));
226
+ } else if (!res.writableEnded) {
227
+ res.end();
228
+ }
184
229
  }
185
- }
230
+ });
186
231
  });
187
232
 
188
233
  // Store references in globalThis for hot reload persistence
@@ -250,9 +295,22 @@ try {
250
295
  throw err;
251
296
  }
252
297
 
298
+ // Phase 2 of the cost-tracking plan: project the vendored models.dev snapshot
299
+ // into pricing rows at boot. Lazy `getDb()` would also work, but doing it
300
+ // here surfaces the count in the boot log and makes the API ready to recompute
301
+ // USD before the first POST /api/session-costs lands.
302
+ try {
303
+ const { seedPricingFromModelsDev } = await import("../be/seed-pricing");
304
+ seedPricingFromModelsDev();
305
+ } catch (err) {
306
+ console.error("[startup] Failed to seed pricing rows:", err);
307
+ }
308
+
253
309
  // business-use initialization (no-op if envs not set)
254
310
  initialize();
255
311
 
312
+ await initOtel("api");
313
+
256
314
  httpServer
257
315
  .listen(port, async () => {
258
316
  console.log(`MCP HTTP server running on http://localhost:${port}/mcp`);
@@ -124,6 +124,20 @@ const deleteMemoryById = route({
124
124
  },
125
125
  });
126
126
 
127
+ const getMemoryById = route({
128
+ method: "get",
129
+ path: "/api/memory/{id}",
130
+ pattern: ["api", "memory", null],
131
+ summary: "Get a single memory by ID",
132
+ tags: ["Memory"],
133
+ auth: { apiKey: true, agentId: true },
134
+ params: z.object({ id: z.string().uuid() }),
135
+ responses: {
136
+ 200: { description: "Memory details" },
137
+ 404: { description: "Memory not found" },
138
+ },
139
+ });
140
+
127
141
  // Memory rater v1.5 — worker-facing rating endpoints. Plan:
128
142
  // thoughts/taras/plans/2026-05-05-memory-rater-v1.5/step-3.md
129
143
  //
@@ -618,5 +632,19 @@ export async function handleMemory(
618
632
  return true;
619
633
  }
620
634
 
635
+ if (getMemoryById.match(req.method, pathSegments)) {
636
+ const parsed = await getMemoryById.parse(req, res, pathSegments, new URLSearchParams());
637
+ if (!parsed) return true;
638
+
639
+ const memory = getMemoryStore().get(parsed.params.id);
640
+ if (!memory) {
641
+ jsonError(res, "Memory not found", 404);
642
+ return true;
643
+ }
644
+
645
+ json(res, { memory });
646
+ return true;
647
+ }
648
+
621
649
  return false;
622
650
  }
@@ -64,6 +64,7 @@ export function generateOpenApiSpec(opts: OpenApiOptions): string {
64
64
  registry.registerPath({
65
65
  method: routeDef.method,
66
66
  path: routeDef.path,
67
+ operationId: routeDef.operationId,
67
68
  summary: routeDef.summary,
68
69
  description: routeDef.description,
69
70
  tags: routeDef.tags,
@@ -1,5 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { getPage } from "../be/db";
3
+ import { getApiKey } from "../utils/api-key";
3
4
  import { extractAndVerifyCookie } from "../utils/page-session";
4
5
  import { route } from "./route-def";
5
6
  import { jsonError } from "./utils";
@@ -147,7 +148,7 @@ export async function handlePageProxy(req: IncomingMessage, res: ServerResponse)
147
148
  const baseUrl = `http://127.0.0.1:${port}`;
148
149
  const targetUrl = `${baseUrl}${rewrittenPath}${queryPart}`;
149
150
 
150
- const apiKey = process.env.API_KEY ?? "";
151
+ const apiKey = getApiKey();
151
152
  // `X-Page-Id` is the trust anchor for page-scoped KV: only the page-proxy
152
153
  // ever sets it (any external `X-Page-Id` header is dropped because we don't
153
154
  // forward the original headers). The KV handler treats this as the highest-
@@ -20,6 +20,7 @@ export interface RouteDef<
20
20
  path: string; // OpenAPI-style: "/api/tasks/{id}"
21
21
  pattern: readonly (string | null)[]; // matchRoute-style: ["api", "tasks", null]
22
22
  exact?: boolean; // default true
23
+ operationId?: string;
23
24
  summary: string;
24
25
  description?: string;
25
26
  tags: string[];
@@ -8,6 +8,7 @@ import {
8
8
  getDb,
9
9
  getScheduledTaskById,
10
10
  getScheduledTaskByName,
11
+ getScheduledTasks,
11
12
  updateScheduledTask,
12
13
  } from "../be/db";
13
14
  import { calculateNextRun } from "../scheduler/scheduler";
@@ -64,6 +65,29 @@ const runScheduleNow = route({
64
65
  },
65
66
  });
66
67
 
68
+ const listSchedules = route({
69
+ method: "get",
70
+ path: "/api/schedules",
71
+ pattern: ["api", "schedules"],
72
+ summary: "List schedules",
73
+ tags: ["Schedules"],
74
+ query: z.object({
75
+ enabled: z
76
+ .enum(["true", "false"])
77
+ .optional()
78
+ .transform((v) => (v === undefined ? undefined : v === "true")),
79
+ name: z.string().optional(),
80
+ scheduleType: z.enum(["recurring", "one_time"]).optional(),
81
+ hideCompleted: z
82
+ .enum(["true", "false"])
83
+ .optional()
84
+ .transform((v) => (v === undefined ? undefined : v === "true")),
85
+ }),
86
+ responses: {
87
+ 200: { description: "List of schedules" },
88
+ },
89
+ });
90
+
67
91
  const getSchedule = route({
68
92
  method: "get",
69
93
  path: "/api/schedules/{id}",
@@ -129,6 +153,19 @@ export async function handleSchedules(
129
153
  queryParams: URLSearchParams,
130
154
  _myAgentId: string | undefined,
131
155
  ): Promise<boolean> {
156
+ if (listSchedules.match(req.method, pathSegments)) {
157
+ const parsed = await listSchedules.parse(req, res, pathSegments, queryParams);
158
+ if (!parsed) return true;
159
+ const schedules = getScheduledTasks({
160
+ enabled: parsed.query.enabled,
161
+ name: parsed.query.name,
162
+ scheduleType: parsed.query.scheduleType,
163
+ hideCompleted: parsed.query.hideCompleted,
164
+ });
165
+ json(res, { schedules, count: schedules.length });
166
+ return true;
167
+ }
168
+
132
169
  if (createSchedule.match(req.method, pathSegments)) {
133
170
  const parsed = await createSchedule.parse(req, res, pathSegments, queryParams);
134
171
  if (!parsed) return true;