@desplega.ai/agent-swarm 1.83.1 → 1.83.2

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 (55) hide show
  1. package/openapi.json +139 -8
  2. package/package.json +1 -1
  3. package/src/artifact-sdk/server.ts +23 -1
  4. package/src/be/budget-admission.ts +28 -4
  5. package/src/be/budget-refusal-notify.ts +19 -3
  6. package/src/be/db-queries/oauth.ts +43 -0
  7. package/src/be/db.ts +35 -2
  8. package/src/be/migrations/074_user_budget_scope.sql +85 -0
  9. package/src/commands/resume-session.ts +118 -0
  10. package/src/commands/runner.ts +137 -67
  11. package/src/http/core.ts +4 -1
  12. package/src/http/index.ts +16 -0
  13. package/src/http/integrations.ts +26 -0
  14. package/src/http/mcp-user.ts +111 -0
  15. package/src/http/poll.ts +19 -5
  16. package/src/http/schedules.ts +1 -1
  17. package/src/http/users.ts +107 -2
  18. package/src/jira/client.ts +3 -5
  19. package/src/jira/oauth.ts +1 -0
  20. package/src/jira/sync.ts +2 -2
  21. package/src/oauth/ensure-token.ts +1 -0
  22. package/src/oauth/wrapper.ts +38 -7
  23. package/src/providers/claude-adapter.ts +7 -2
  24. package/src/providers/claude-managed-adapter.ts +1 -1
  25. package/src/providers/codex-adapter.ts +30 -0
  26. package/src/providers/opencode-adapter.ts +149 -14
  27. package/src/providers/pi-mono-adapter.ts +41 -1
  28. package/src/providers/types.ts +1 -1
  29. package/src/server-user.ts +117 -0
  30. package/src/tests/artifact-sdk.test.ts +23 -19
  31. package/src/tests/budget-user-scope.test.ts +376 -0
  32. package/src/tests/claude-managed-adapter.test.ts +6 -0
  33. package/src/tests/codex-adapter.test.ts +192 -0
  34. package/src/tests/codex-rate-limit-parse.test.ts +256 -0
  35. package/src/tests/db-queries-oauth.test.ts +43 -0
  36. package/src/tests/ensure-token.test.ts +93 -0
  37. package/src/tests/error-tracker.test.ts +52 -0
  38. package/src/tests/fetch-resolved-env.test.ts +33 -20
  39. package/src/tests/http-users.test.ts +29 -1
  40. package/src/tests/mcp-user-route.test.ts +325 -0
  41. package/src/tests/opencode-adapter.test.ts +75 -0
  42. package/src/tests/pi-mono-adapter.test.ts +21 -1
  43. package/src/tests/rate-limit-event.test.ts +69 -6
  44. package/src/tests/resume-session.test.ts +93 -0
  45. package/src/tests/task-tools-ctx.test.ts +100 -0
  46. package/src/tests/task-tools-ownership.test.ts +167 -0
  47. package/src/tests/user-token-routes.test.ts +221 -0
  48. package/src/tools/cancel-task.ts +137 -83
  49. package/src/tools/get-task-details.ts +73 -59
  50. package/src/tools/get-tasks.ts +134 -126
  51. package/src/tools/send-task.ts +312 -312
  52. package/src/tools/task-action.ts +464 -367
  53. package/src/tools/task-tool-ctx.ts +43 -0
  54. package/src/types.ts +6 -2
  55. package/src/utils/error-tracker.ts +122 -9
@@ -100,6 +100,74 @@ function isAssistantMessage(msg: unknown): msg is AssistantMessage {
100
100
  }
101
101
 
102
102
  const DOCKER_PLUGIN_PATH = "/home/worker/.config/opencode/plugins/agent-swarm.ts";
103
+ const MODEL_CACHE_REFRESH_TIMEOUT_MS = 15_000;
104
+
105
+ function isOpenRouterModel(model: string | undefined): boolean {
106
+ return Boolean(model?.toLowerCase().startsWith("openrouter/"));
107
+ }
108
+
109
+ function isModelNotFoundError(message: string): boolean {
110
+ return /model not found:/i.test(message);
111
+ }
112
+
113
+ async function readSpawnOutput(stream: ReadableStream<Uint8Array> | null): Promise<string> {
114
+ if (!stream) return "";
115
+ return await new Response(stream).text();
116
+ }
117
+
118
+ function formatUnknownError(err: unknown): string {
119
+ return err instanceof Error ? err.message : String(err);
120
+ }
121
+
122
+ async function refreshOpenRouterModelCache(
123
+ opencodeConfig: Config & { plugin?: string[] },
124
+ configFilePath: string,
125
+ dataHomePath: string,
126
+ ): Promise<void> {
127
+ const binary = process.env.OPENCODE_BINARY || "opencode";
128
+ const proc = Bun.spawn([binary, "models", "--refresh", "openrouter"], {
129
+ stdout: "pipe",
130
+ stderr: "pipe",
131
+ env: {
132
+ ...process.env,
133
+ OPENCODE_CONFIG: configFilePath,
134
+ OPENCODE_CONFIG_CONTENT: JSON.stringify(opencodeConfig),
135
+ OPENCODE_DATA_HOME: dataHomePath,
136
+ },
137
+ });
138
+
139
+ let timedOut = false;
140
+ const timeout = setTimeout(() => {
141
+ timedOut = true;
142
+ proc.kill();
143
+ }, MODEL_CACHE_REFRESH_TIMEOUT_MS);
144
+
145
+ const [stdout, stderr, exitCode] = await Promise.all([
146
+ readSpawnOutput(proc.stdout),
147
+ readSpawnOutput(proc.stderr),
148
+ proc.exited,
149
+ ]).finally(() => clearTimeout(timeout));
150
+
151
+ if (timedOut) {
152
+ throw new Error(
153
+ `opencode models --refresh openrouter timed out after ${MODEL_CACHE_REFRESH_TIMEOUT_MS}ms`,
154
+ );
155
+ }
156
+ if (exitCode !== 0) {
157
+ const detail = scrubSecrets([stderr.trim(), stdout.trim()].filter(Boolean).join("\n"));
158
+ throw new Error(
159
+ `opencode models --refresh openrouter exited with code ${exitCode}${detail ? `: ${detail}` : ""}`,
160
+ );
161
+ }
162
+ }
163
+
164
+ let refreshOpenRouterModelCacheImpl = refreshOpenRouterModelCache;
165
+
166
+ export function _setOpenRouterModelCacheRefreshForTests(
167
+ fn: typeof refreshOpenRouterModelCache | null,
168
+ ): void {
169
+ refreshOpenRouterModelCacheImpl = fn ?? refreshOpenRouterModelCache;
170
+ }
103
171
 
104
172
  function resolvePluginPath(): string {
105
173
  const override = process.env.OPENCODE_SWARM_PLUGIN_PATH;
@@ -141,6 +209,8 @@ export class OpencodeSession implements ProviderSession {
141
209
  private agentFilePath: string;
142
210
  private configFilePath: string;
143
211
  private dataHomePath: string;
212
+ private retryAfterModelRefresh?: () => Promise<boolean>;
213
+ private modelRefreshRecoveryInFlight = false;
144
214
 
145
215
  // Track which tool callIDs have already emitted tool_start, so transitions
146
216
  // through pending → running → completed don't fire duplicate events.
@@ -155,6 +225,7 @@ export class OpencodeSession implements ProviderSession {
155
225
  agentFilePath: string,
156
226
  configFilePath: string,
157
227
  dataHomePath: string,
228
+ retryAfterModelRefresh?: () => Promise<boolean>,
158
229
  ) {
159
230
  this._sessionId = sessionId;
160
231
  this.server = server;
@@ -164,6 +235,7 @@ export class OpencodeSession implements ProviderSession {
164
235
  this.agentFilePath = agentFilePath;
165
236
  this.configFilePath = configFilePath;
166
237
  this.dataHomePath = dataHomePath;
238
+ this.retryAfterModelRefresh = retryAfterModelRefresh;
167
239
  this.completionPromise = new Promise<ProviderResult>((resolve, reject) => {
168
240
  this.completionResolve = resolve;
169
241
  this.completionReject = reject;
@@ -211,6 +283,33 @@ export class OpencodeSession implements ProviderSession {
211
283
  for (const l of this.listeners) l(event);
212
284
  }
213
285
 
286
+ emitModelCacheRefreshProgress(): void {
287
+ this.emit({
288
+ type: "progress",
289
+ message: "opencode model cache is stale; refreshing OpenRouter models and retrying once",
290
+ });
291
+ }
292
+
293
+ emitModelCacheRefreshFailure(message: string, err: unknown): void {
294
+ this.emitError(`${message}; OpenRouter model cache refresh failed: ${formatUnknownError(err)}`);
295
+ }
296
+
297
+ private recoverFromModelNotFound(message: string): void {
298
+ if (!this.retryAfterModelRefresh || this.modelRefreshRecoveryInFlight) {
299
+ this.emitError(message);
300
+ return;
301
+ }
302
+ this.modelRefreshRecoveryInFlight = true;
303
+ this.emitModelCacheRefreshProgress();
304
+ this.retryAfterModelRefresh()
305
+ .then((retried) => {
306
+ if (!retried) this.emitError(message);
307
+ })
308
+ .catch((err: unknown) => {
309
+ this.emitModelCacheRefreshFailure(message, err);
310
+ });
311
+ }
312
+
214
313
  /** Best-effort cleanup of per-task isolation files and directories. */
215
314
  private async cleanupFiles(): Promise<void> {
216
315
  try {
@@ -361,6 +460,10 @@ export class OpencodeSession implements ProviderSession {
361
460
  ev.properties.error && "message" in ev.properties.error
362
461
  ? String((ev.properties.error as { message?: string }).message ?? "unknown error")
363
462
  : "opencode session error";
463
+ if (isModelNotFoundError(errMsg)) {
464
+ this.recoverFromModelNotFound(errMsg);
465
+ break;
466
+ }
364
467
  this.emitError(errMsg);
365
468
  break;
366
469
  }
@@ -562,6 +665,30 @@ export class OpencodeAdapter implements ProviderAdapter {
562
665
  const opencodeSession = createResult.data;
563
666
  const sessionId = opencodeSession.id;
564
667
 
668
+ let promptRefreshAttempted = false;
669
+ let promptRefreshPromise: Promise<boolean> | undefined;
670
+ const sendPrompt = async () => {
671
+ await client.session.prompt({
672
+ path: { id: sessionId },
673
+ query: { directory: config.cwd },
674
+ body: {
675
+ agent: agentName,
676
+ parts: [{ type: "text", text: config.prompt }],
677
+ },
678
+ });
679
+ };
680
+ const refreshOpenRouterAndRetryPrompt = async (): Promise<boolean> => {
681
+ if (promptRefreshPromise) return await promptRefreshPromise;
682
+ if (promptRefreshAttempted || !isOpenRouterModel(config.model)) return false;
683
+ promptRefreshAttempted = true;
684
+ promptRefreshPromise = (async () => {
685
+ await refreshOpenRouterModelCacheImpl(opencodeConfig, configFilePath, dataHomePath);
686
+ await sendPrompt();
687
+ return true;
688
+ })();
689
+ return await promptRefreshPromise;
690
+ };
691
+
565
692
  const session = new OpencodeSession(
566
693
  sessionId,
567
694
  server,
@@ -571,6 +698,7 @@ export class OpencodeAdapter implements ProviderAdapter {
571
698
  agentFilePath,
572
699
  configFilePath,
573
700
  dataHomePath,
701
+ isOpenRouterModel(config.model) ? refreshOpenRouterAndRetryPrompt : undefined,
574
702
  );
575
703
 
576
704
  // Emit session_init synchronously; the session buffers events until the
@@ -594,21 +722,28 @@ export class OpencodeAdapter implements ProviderAdapter {
594
722
  });
595
723
 
596
724
  // Fire-and-forget: send the prompt using the per-task agent
597
- client.session
598
- .prompt({
599
- path: { id: sessionId },
600
- query: { directory: config.cwd },
601
- body: {
602
- agent: agentName,
603
- parts: [{ type: "text", text: config.prompt }],
604
- },
605
- })
606
- .catch((err: unknown) => {
607
- session.handleOpencodeEvent({
608
- type: "session.error",
609
- properties: { sessionID: sessionId, error: { message: String(err) } as never },
610
- });
725
+ sendPrompt().catch((err: unknown) => {
726
+ const message = formatUnknownError(err);
727
+ if (isModelNotFoundError(message) && isOpenRouterModel(config.model)) {
728
+ session.emitModelCacheRefreshProgress();
729
+ refreshOpenRouterAndRetryPrompt()
730
+ .then((retried) => {
731
+ if (retried) return;
732
+ session.handleOpencodeEvent({
733
+ type: "session.error",
734
+ properties: { sessionID: sessionId, error: { message } as never },
735
+ });
736
+ })
737
+ .catch((retryErr: unknown) => {
738
+ session.emitModelCacheRefreshFailure(message, retryErr);
739
+ });
740
+ return;
741
+ }
742
+ session.handleOpencodeEvent({
743
+ type: "session.error",
744
+ properties: { sessionID: sessionId, error: { message } as never },
611
745
  });
746
+ });
612
747
 
613
748
  return session;
614
749
  }
@@ -17,9 +17,11 @@ import type {
17
17
  } from "@earendil-works/pi-coding-agent";
18
18
  import {
19
19
  type AgentSession,
20
+ AuthStorage,
20
21
  createAgentSession,
21
22
  DefaultResourceLoader,
22
23
  getAgentDir,
24
+ ModelRegistry,
23
25
  SessionManager,
24
26
  } from "@earendil-works/pi-coding-agent";
25
27
  import { type TSchema, Type } from "typebox";
@@ -180,6 +182,39 @@ function envHasAnthropicCred(env: Record<string, string | undefined>): boolean {
180
182
  return !!(env.ANTHROPIC_API_KEY || env.ANTHROPIC_OAUTH_TOKEN);
181
183
  }
182
184
 
185
+ const PI_RUNTIME_API_KEYS = [
186
+ ["OPENROUTER_API_KEY", "openrouter"],
187
+ ["ANTHROPIC_API_KEY", "anthropic"],
188
+ ["OPENAI_API_KEY", "openai"],
189
+ ["GOOGLE_API_KEY", "google"],
190
+ ] as const;
191
+
192
+ /**
193
+ * Build pi-coding-agent auth services from the runner's per-task resolved env.
194
+ *
195
+ * The runner intentionally does not copy rotated credential-pool selections
196
+ * into `process.env` because that would freeze rotation globally. pi-mono runs
197
+ * in-process, so pass selected keys through pi's runtime auth override instead
198
+ * of relying on environment lookup.
199
+ */
200
+ export function createPiRuntimeAuth(env: Record<string, string | undefined> = process.env): {
201
+ authStorage: AuthStorage;
202
+ modelRegistry: ModelRegistry;
203
+ } {
204
+ const authStorage = AuthStorage.create();
205
+ for (const [envKey, provider] of PI_RUNTIME_API_KEYS) {
206
+ const apiKey = env[envKey];
207
+ if (apiKey) {
208
+ authStorage.setRuntimeApiKey(provider, apiKey);
209
+ }
210
+ }
211
+
212
+ return {
213
+ authStorage,
214
+ modelRegistry: ModelRegistry.create(authStorage),
215
+ };
216
+ }
217
+
183
218
  /**
184
219
  * Resolve a model string to a pi-ai Model object.
185
220
  *
@@ -672,8 +707,11 @@ export class PiMonoAdapter implements ProviderAdapter {
672
707
  }
673
708
  }
674
709
 
710
+ const sessionEnv = config.env ?? process.env;
711
+
675
712
  // 3. Resolve model
676
- const model = resolveModel(config.model);
713
+ const model = resolveModel(config.model, sessionEnv);
714
+ const { authStorage, modelRegistry } = createPiRuntimeAuth(sessionEnv);
677
715
 
678
716
  // 4. Create swarm hooks extension
679
717
  const swarmExtension = createSwarmHooksExtension({
@@ -698,6 +736,8 @@ export class PiMonoAdapter implements ProviderAdapter {
698
736
  model,
699
737
  customTools,
700
738
  resourceLoader,
739
+ authStorage,
740
+ modelRegistry,
701
741
  };
702
742
 
703
743
  // 7. Create the session
@@ -125,7 +125,7 @@ export interface ProviderResult {
125
125
  * ISO timestamp of the rate limit reset time, parsed from a structured
126
126
  * `rate_limit_event` line in the Claude CLI stream. Only set by the Claude
127
127
  * adapter when a `status: "rejected"` event is present. Already clamped to
128
- * [now+60s, now+6h] at the source. The runner uses this as tier-1 of the
128
+ * [now+60s, now+7d] at the source. The runner uses this as tier-1 of the
129
129
  * three-tier cooldown resolver.
130
130
  */
131
131
  rateLimitResetAt?: string;
@@ -0,0 +1,117 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import pkg from "../package.json";
4
+ import {
5
+ cancelTaskHandler,
6
+ cancelTaskInputSchema,
7
+ cancelTaskOutputSchema,
8
+ } from "./tools/cancel-task";
9
+ import {
10
+ getTaskDetailsHandler,
11
+ getTaskDetailsInputSchema,
12
+ getTaskDetailsOutputSchema,
13
+ } from "./tools/get-task-details";
14
+ import { getTasksHandler, getTasksInputSchema, getTasksOutputSchema } from "./tools/get-tasks";
15
+ import { sendTaskHandler, sendTaskOutputSchema } from "./tools/send-task";
16
+ import {
17
+ taskActionHandler,
18
+ taskActionInputSchema,
19
+ taskActionOutputSchema,
20
+ } from "./tools/task-action";
21
+ import { userCtx } from "./tools/task-tool-ctx";
22
+ import { createToolRegistrar } from "./tools/utils";
23
+ import type { User } from "./types";
24
+
25
+ const userSendTaskInputSchema = z.object({
26
+ task: z.string().min(1).describe("The task description to send."),
27
+ taskType: z.string().max(50).optional().describe("Task type (e.g., 'bug', 'feature', 'review')."),
28
+ tags: z.array(z.string()).optional().describe("Tags for filtering (e.g., ['urgent'])."),
29
+ priority: z.number().int().min(0).max(100).optional().describe("Priority 0-100 (default: 50)."),
30
+ model: z
31
+ .enum(["haiku", "sonnet", "opus"])
32
+ .optional()
33
+ .describe("Model to use for this task ('haiku', 'sonnet', or 'opus')."),
34
+ });
35
+
36
+ export function createUserServer(user: User): McpServer {
37
+ const server = new McpServer(
38
+ {
39
+ name: `${pkg.name}-user`,
40
+ version: pkg.version,
41
+ description: "End-user task MCP surface for Agent Swarm.",
42
+ },
43
+ {
44
+ capabilities: {
45
+ logging: {},
46
+ },
47
+ },
48
+ );
49
+
50
+ const registerTool = createToolRegistrar(server);
51
+
52
+ registerTool(
53
+ "send-task",
54
+ {
55
+ title: "Send a task",
56
+ annotations: { destructiveHint: false },
57
+ description: "Creates an unassigned task requested by the authenticated user.",
58
+ inputSchema: userSendTaskInputSchema,
59
+ outputSchema: sendTaskOutputSchema,
60
+ },
61
+ async (args, info, _meta) =>
62
+ sendTaskHandler(userCtx(user, info.sessionId), {
63
+ offerMode: false,
64
+ allowDuplicate: false,
65
+ ...args,
66
+ }),
67
+ );
68
+
69
+ registerTool(
70
+ "get-tasks",
71
+ {
72
+ title: "Get tasks",
73
+ description: "Returns tasks requested by the authenticated user.",
74
+ annotations: { readOnlyHint: true },
75
+ inputSchema: getTasksInputSchema,
76
+ outputSchema: getTasksOutputSchema,
77
+ },
78
+ async (args, info, _meta) => getTasksHandler(userCtx(user, info.sessionId), args),
79
+ );
80
+
81
+ registerTool(
82
+ "get-task-details",
83
+ {
84
+ title: "Get task details",
85
+ description: "Returns detailed information about one of your tasks.",
86
+ annotations: { readOnlyHint: true },
87
+ inputSchema: getTaskDetailsInputSchema,
88
+ outputSchema: getTaskDetailsOutputSchema,
89
+ },
90
+ async (args, info, _meta) => getTaskDetailsHandler(userCtx(user, info.sessionId), args),
91
+ );
92
+
93
+ registerTool(
94
+ "cancel-task",
95
+ {
96
+ title: "Cancel Task",
97
+ description: "Cancel one of your pending or in-progress tasks.",
98
+ annotations: { destructiveHint: true },
99
+ inputSchema: cancelTaskInputSchema,
100
+ outputSchema: cancelTaskOutputSchema,
101
+ },
102
+ async (args, info, _meta) => cancelTaskHandler(userCtx(user, info.sessionId), args),
103
+ );
104
+
105
+ registerTool(
106
+ "task-action",
107
+ {
108
+ title: "Task Pool Action",
109
+ description: "Move one of your tasks to or from backlog.",
110
+ inputSchema: taskActionInputSchema,
111
+ outputSchema: taskActionOutputSchema,
112
+ },
113
+ async (args, info, _meta) => taskActionHandler(userCtx(user, info.sessionId), args),
114
+ );
115
+
116
+ return server;
117
+ }
@@ -2,7 +2,11 @@ import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
2
2
  import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { BROWSER_SDK_JS } from "../artifact-sdk/browser-sdk";
4
4
  import { getAvailablePort } from "../artifact-sdk/port";
5
- import { createArtifactServer } from "../artifact-sdk/server";
5
+ import {
6
+ createArtifactServer,
7
+ createBunHonoFetchHandler,
8
+ createBunResponse,
9
+ } from "../artifact-sdk/server";
6
10
  import { getBasePrompt } from "../prompts/base-prompt";
7
11
 
8
12
  // ─── Port allocation tests ──────────────────────────────────────────────
@@ -25,7 +29,7 @@ describe("getAvailablePort", () => {
25
29
  // Try to start a Bun server on the port — should succeed
26
30
  const server = Bun.serve({
27
31
  port,
28
- fetch: () => new Response("ok"),
32
+ fetch: () => createBunResponse("ok"),
29
33
  });
30
34
  expect(server.port).toBe(port);
31
35
  server.stop();
@@ -172,7 +176,7 @@ describe("createArtifactServer", () => {
172
176
  });
173
177
  honoApp.route("/", app);
174
178
 
175
- const server = Bun.serve({ port, fetch: honoApp.fetch });
179
+ const server = Bun.serve({ port, fetch: createBunHonoFetchHandler(honoApp) });
176
180
 
177
181
  try {
178
182
  // Test content serving
@@ -211,7 +215,7 @@ describe("createArtifactServer", () => {
211
215
  const app = new Hono();
212
216
  app.use("/*", serveStatic({ root: testDir }));
213
217
 
214
- const server = Bun.serve({ port, fetch: app.fetch });
218
+ const server = Bun.serve({ port, fetch: createBunHonoFetchHandler(app) });
215
219
 
216
220
  try {
217
221
  const res = await fetch(`http://localhost:${port}/index.html`);
@@ -244,7 +248,7 @@ describe("createArtifactServer", () => {
244
248
  }
245
249
  });
246
250
 
247
- const server = Bun.serve({ port, fetch: app.fetch });
251
+ const server = Bun.serve({ port, fetch: createBunHonoFetchHandler(app) });
248
252
 
249
253
  try {
250
254
  const res = await fetch(`http://localhost:${port}/@swarm/api/agents`);
@@ -272,7 +276,7 @@ describe("createArtifactServer", () => {
272
276
  });
273
277
  });
274
278
 
275
- const server = Bun.serve({ port, fetch: app.fetch });
279
+ const server = Bun.serve({ port, fetch: createBunHonoFetchHandler(app) });
276
280
 
277
281
  try {
278
282
  const res = await fetch(`http://localhost:${port}/@swarm/api/tasks`, {
@@ -303,7 +307,7 @@ describe("createArtifactServer", () => {
303
307
  for (const [key, value] of req.headers.entries()) {
304
308
  capturedHeaders[key.toLowerCase()] = value;
305
309
  }
306
- return new Response(JSON.stringify({ success: true }), {
310
+ return createBunResponse(JSON.stringify({ success: true }), {
307
311
  headers: { "Content-Type": "application/json" },
308
312
  });
309
313
  },
@@ -336,7 +340,7 @@ describe("createArtifactServer", () => {
336
340
  }
337
341
  });
338
342
 
339
- const proxy = Bun.serve({ port: proxyPort, fetch: app.fetch });
343
+ const proxy = Bun.serve({ port: proxyPort, fetch: createBunHonoFetchHandler(app) });
340
344
 
341
345
  try {
342
346
  // Test GET request headers
@@ -369,7 +373,7 @@ describe("createArtifactServer", () => {
369
373
  const mockMcp = Bun.serve({
370
374
  port: mockMcpPort,
371
375
  fetch: () =>
372
- new Response(JSON.stringify({ ok: true }), {
376
+ createBunResponse(JSON.stringify({ ok: true }), {
373
377
  headers: { "Content-Type": "application/json" },
374
378
  }),
375
379
  });
@@ -393,7 +397,7 @@ describe("createArtifactServer", () => {
393
397
  }
394
398
  });
395
399
 
396
- const proxy = Bun.serve({ port: proxyPort, fetch: app.fetch });
400
+ const proxy = Bun.serve({ port: proxyPort, fetch: createBunHonoFetchHandler(app) });
397
401
 
398
402
  try {
399
403
  const res = await fetch(`http://localhost:${proxyPort}/@swarm/api/agents`);
@@ -415,7 +419,7 @@ describe("createArtifactServer", () => {
415
419
  port: mockMcpPort,
416
420
  fetch: (req) => {
417
421
  capturedPath = new URL(req.url).pathname;
418
- return new Response(JSON.stringify({ ok: true }));
422
+ return createBunResponse(JSON.stringify({ ok: true }));
419
423
  },
420
424
  });
421
425
 
@@ -431,7 +435,7 @@ describe("createArtifactServer", () => {
431
435
  }
432
436
  });
433
437
 
434
- const proxy = Bun.serve({ port: proxyPort, fetch: app.fetch });
438
+ const proxy = Bun.serve({ port: proxyPort, fetch: createBunHonoFetchHandler(app) });
435
439
 
436
440
  try {
437
441
  await fetch(`http://localhost:${proxyPort}/@swarm/api/tasks/123/progress`);
@@ -535,7 +539,7 @@ describe("artifact CLI command", () => {
535
539
  fetch: (req) => {
536
540
  const url = new URL(req.url);
537
541
  if (url.pathname === "/api/services") {
538
- return new Response(
542
+ return createBunResponse(
539
543
  JSON.stringify({
540
544
  services: [
541
545
  {
@@ -561,7 +565,7 @@ describe("artifact CLI command", () => {
561
565
  }),
562
566
  );
563
567
  }
564
- return new Response("Not found", { status: 404 });
568
+ return createBunResponse("Not found", { status: 404 });
565
569
  },
566
570
  });
567
571
 
@@ -595,7 +599,7 @@ describe("artifact CLI command", () => {
595
599
  const mockPort = await getAvailablePort();
596
600
  const mockServer = Bun.serve({
597
601
  port: mockPort,
598
- fetch: () => new Response(JSON.stringify({ services: [] })),
602
+ fetch: () => createBunResponse(JSON.stringify({ services: [] })),
599
603
  });
600
604
 
601
605
  const origEnv = { ...process.env };
@@ -627,7 +631,7 @@ describe("artifact CLI command", () => {
627
631
  const mockServer = Bun.serve({
628
632
  port: mockPort,
629
633
  fetch: () =>
630
- new Response(
634
+ createBunResponse(
631
635
  JSON.stringify({
632
636
  services: [
633
637
  {
@@ -685,7 +689,7 @@ describe("artifact CLI command", () => {
685
689
  fetch: (req) => {
686
690
  const url = new URL(req.url);
687
691
  if (req.method === "GET" && url.pathname === "/api/services") {
688
- return new Response(
692
+ return createBunResponse(
689
693
  JSON.stringify({
690
694
  services: [
691
695
  {
@@ -699,9 +703,9 @@ describe("artifact CLI command", () => {
699
703
  }
700
704
  if (req.method === "DELETE" && url.pathname.startsWith("/api/services/")) {
701
705
  deletedServiceId = url.pathname.split("/").pop() || "";
702
- return new Response(JSON.stringify({ success: true }));
706
+ return createBunResponse(JSON.stringify({ success: true }));
703
707
  }
704
- return new Response("Not found", { status: 404 });
708
+ return createBunResponse("Not found", { status: 404 });
705
709
  },
706
710
  });
707
711