@wingman-ai/gateway 0.2.4 → 0.3.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 (95) hide show
  1. package/.wingman/agents/coding/agent.md +5 -0
  2. package/.wingman/agents/coding-v2/agent.md +58 -0
  3. package/.wingman/agents/game-dev/agent.md +94 -0
  4. package/.wingman/agents/game-dev/art-generation.md +37 -0
  5. package/.wingman/agents/game-dev/asset-refinement.md +17 -0
  6. package/.wingman/agents/game-dev/planning-idea.md +17 -0
  7. package/.wingman/agents/game-dev/ui-specialist.md +17 -0
  8. package/.wingman/agents/main/agent.md +2 -0
  9. package/README.md +1 -0
  10. package/dist/agent/config/agentConfig.d.ts +4 -0
  11. package/dist/agent/config/mcpClientManager.cjs +44 -10
  12. package/dist/agent/config/mcpClientManager.d.ts +6 -2
  13. package/dist/agent/config/mcpClientManager.js +44 -10
  14. package/dist/agent/config/toolRegistry.cjs +3 -1
  15. package/dist/agent/config/toolRegistry.js +3 -1
  16. package/dist/agent/tests/mcpClientManager.test.cjs +124 -0
  17. package/dist/agent/tests/mcpClientManager.test.d.ts +1 -0
  18. package/dist/agent/tests/mcpClientManager.test.js +118 -0
  19. package/dist/agent/tools/command_execute.cjs +1 -1
  20. package/dist/agent/tools/command_execute.js +1 -1
  21. package/dist/cli/config/schema.d.ts +2 -0
  22. package/dist/cli/core/agentInvoker.cjs +55 -66
  23. package/dist/cli/core/agentInvoker.d.ts +10 -13
  24. package/dist/cli/core/agentInvoker.js +42 -62
  25. package/dist/cli/core/imagePersistence.cjs +125 -0
  26. package/dist/cli/core/imagePersistence.d.ts +24 -0
  27. package/dist/cli/core/imagePersistence.js +85 -0
  28. package/dist/cli/core/sessionManager.cjs +297 -40
  29. package/dist/cli/core/sessionManager.d.ts +9 -0
  30. package/dist/cli/core/sessionManager.js +297 -40
  31. package/dist/debug/terminalProbe.cjs +57 -0
  32. package/dist/debug/terminalProbe.d.ts +10 -0
  33. package/dist/debug/terminalProbe.js +20 -0
  34. package/dist/debug/terminalProbeAuth.cjs +140 -0
  35. package/dist/debug/terminalProbeAuth.d.ts +20 -0
  36. package/dist/debug/terminalProbeAuth.js +97 -0
  37. package/dist/gateway/http/fs.cjs +19 -0
  38. package/dist/gateway/http/fs.js +19 -0
  39. package/dist/gateway/http/sessions.cjs +25 -5
  40. package/dist/gateway/http/sessions.js +25 -5
  41. package/dist/gateway/server.cjs +112 -11
  42. package/dist/gateway/server.d.ts +2 -0
  43. package/dist/gateway/server.js +112 -11
  44. package/dist/providers/codex.cjs +230 -37
  45. package/dist/providers/codex.d.ts +2 -0
  46. package/dist/providers/codex.js +231 -38
  47. package/dist/tests/agentInvokerSummarization.test.cjs +56 -37
  48. package/dist/tests/agentInvokerSummarization.test.js +58 -39
  49. package/dist/tests/agentInvokerWorkdir.test.cjs +50 -0
  50. package/dist/tests/agentInvokerWorkdir.test.js +52 -2
  51. package/dist/tests/cli-init.test.cjs +36 -0
  52. package/dist/tests/cli-init.test.js +36 -0
  53. package/dist/tests/codex-provider.test.cjs +173 -0
  54. package/dist/tests/codex-provider.test.js +174 -1
  55. package/dist/tests/falRuntime.test.cjs +78 -0
  56. package/dist/tests/falRuntime.test.d.ts +1 -0
  57. package/dist/tests/falRuntime.test.js +72 -0
  58. package/dist/tests/falSummary.test.cjs +51 -0
  59. package/dist/tests/falSummary.test.d.ts +1 -0
  60. package/dist/tests/falSummary.test.js +45 -0
  61. package/dist/tests/gateway.test.cjs +109 -1
  62. package/dist/tests/gateway.test.js +109 -1
  63. package/dist/tests/imagePersistence.test.cjs +143 -0
  64. package/dist/tests/imagePersistence.test.d.ts +1 -0
  65. package/dist/tests/imagePersistence.test.js +137 -0
  66. package/dist/tests/sessionMessageAttachments.test.cjs +30 -0
  67. package/dist/tests/sessionMessageAttachments.test.js +30 -0
  68. package/dist/tests/sessionStateMessages.test.cjs +126 -0
  69. package/dist/tests/sessionStateMessages.test.js +126 -0
  70. package/dist/tests/sessions-api.test.cjs +117 -3
  71. package/dist/tests/sessions-api.test.js +118 -4
  72. package/dist/tests/terminalProbe.test.cjs +45 -0
  73. package/dist/tests/terminalProbe.test.d.ts +1 -0
  74. package/dist/tests/terminalProbe.test.js +39 -0
  75. package/dist/tests/terminalProbeAuth.test.cjs +85 -0
  76. package/dist/tests/terminalProbeAuth.test.d.ts +1 -0
  77. package/dist/tests/terminalProbeAuth.test.js +79 -0
  78. package/dist/tools/fal/runtime.cjs +103 -0
  79. package/dist/tools/fal/runtime.d.ts +10 -0
  80. package/dist/tools/fal/runtime.js +60 -0
  81. package/dist/tools/fal/summary.cjs +78 -0
  82. package/dist/tools/fal/summary.d.ts +22 -0
  83. package/dist/tools/fal/summary.js +41 -0
  84. package/dist/tools/mcp-fal-ai.cjs +1041 -0
  85. package/dist/tools/mcp-fal-ai.d.ts +1 -0
  86. package/dist/tools/mcp-fal-ai.js +1025 -0
  87. package/dist/types/mcp.cjs +2 -0
  88. package/dist/types/mcp.d.ts +8 -0
  89. package/dist/types/mcp.js +3 -1
  90. package/dist/webui/assets/index-0nUBsUUq.js +278 -0
  91. package/dist/webui/assets/index-kk7OrD-G.css +11 -0
  92. package/dist/webui/index.html +2 -2
  93. package/package.json +16 -13
  94. package/dist/webui/assets/index-DVWQluit.css +0 -11
  95. package/dist/webui/assets/index-Dlyzwalc.js +0 -270
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __webpack_exports__ = {};
3
+ const external_vitest_namespaceObject = require("vitest");
4
+ const summary_cjs_namespaceObject = require("../tools/fal/summary.cjs");
5
+ (0, external_vitest_namespaceObject.describe)("fal summary", ()=>{
6
+ (0, external_vitest_namespaceObject.it)("formats summary text with status, model, and files", ()=>{
7
+ const summary = (0, summary_cjs_namespaceObject.buildFalGenerationSummary)({
8
+ toolName: "generate_image_or_texture",
9
+ jobId: "job-123",
10
+ status: "completed",
11
+ modelId: "fal-ai/nano-banana-pro",
12
+ reviewState: "accepted",
13
+ media: [
14
+ {
15
+ modality: "image",
16
+ path: "/repo/apps/wingman/generated/images/asset.png",
17
+ mimeType: "image/png"
18
+ }
19
+ ],
20
+ cwd: "/repo/apps/wingman"
21
+ });
22
+ (0, external_vitest_namespaceObject.expect)(summary).toContain("FAL job `job-123`");
23
+ (0, external_vitest_namespaceObject.expect)(summary).toContain("**completed**");
24
+ (0, external_vitest_namespaceObject.expect)(summary).toContain("- Model: `fal-ai/nano-banana-pro`");
25
+ (0, external_vitest_namespaceObject.expect)(summary).toContain("- Assets: 1");
26
+ (0, external_vitest_namespaceObject.expect)(summary).toContain("[image] ./generated/images/asset.png image/png");
27
+ });
28
+ (0, external_vitest_namespaceObject.it)("includes review instruction when pending", ()=>{
29
+ const summary = (0, summary_cjs_namespaceObject.buildFalGenerationSummary)({
30
+ toolName: "generate_video_from_image",
31
+ jobId: "job-456",
32
+ status: "awaiting_review",
33
+ modelId: "fal-ai/kling-video/o3/standard/image-to-video",
34
+ reviewState: "pending",
35
+ media: []
36
+ });
37
+ (0, external_vitest_namespaceObject.expect)(summary).toContain("Review required");
38
+ (0, external_vitest_namespaceObject.expect)(summary).toContain("fal_generation_status");
39
+ });
40
+ (0, external_vitest_namespaceObject.it)("formats home paths with tilde when outside cwd", ()=>{
41
+ const formatted = (0, summary_cjs_namespaceObject.formatFalPath)("/Users/demo/Projects/output.mp4", {
42
+ cwd: "/repo/apps/wingman",
43
+ homeDir: "/Users/demo"
44
+ });
45
+ (0, external_vitest_namespaceObject.expect)(formatted).toBe("~/Projects/output.mp4");
46
+ });
47
+ });
48
+ for(var __rspack_i in __webpack_exports__)exports[__rspack_i] = __webpack_exports__[__rspack_i];
49
+ Object.defineProperty(exports, '__esModule', {
50
+ value: true
51
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildFalGenerationSummary, formatFalPath } from "../tools/fal/summary.js";
3
+ describe("fal summary", ()=>{
4
+ it("formats summary text with status, model, and files", ()=>{
5
+ const summary = buildFalGenerationSummary({
6
+ toolName: "generate_image_or_texture",
7
+ jobId: "job-123",
8
+ status: "completed",
9
+ modelId: "fal-ai/nano-banana-pro",
10
+ reviewState: "accepted",
11
+ media: [
12
+ {
13
+ modality: "image",
14
+ path: "/repo/apps/wingman/generated/images/asset.png",
15
+ mimeType: "image/png"
16
+ }
17
+ ],
18
+ cwd: "/repo/apps/wingman"
19
+ });
20
+ expect(summary).toContain("FAL job `job-123`");
21
+ expect(summary).toContain("**completed**");
22
+ expect(summary).toContain("- Model: `fal-ai/nano-banana-pro`");
23
+ expect(summary).toContain("- Assets: 1");
24
+ expect(summary).toContain("[image] ./generated/images/asset.png image/png");
25
+ });
26
+ it("includes review instruction when pending", ()=>{
27
+ const summary = buildFalGenerationSummary({
28
+ toolName: "generate_video_from_image",
29
+ jobId: "job-456",
30
+ status: "awaiting_review",
31
+ modelId: "fal-ai/kling-video/o3/standard/image-to-video",
32
+ reviewState: "pending",
33
+ media: []
34
+ });
35
+ expect(summary).toContain("Review required");
36
+ expect(summary).toContain("fal_generation_status");
37
+ });
38
+ it("formats home paths with tilde when outside cwd", ()=>{
39
+ const formatted = formatFalPath("/Users/demo/Projects/output.mp4", {
40
+ cwd: "/repo/apps/wingman",
41
+ homeDir: "/Users/demo"
42
+ });
43
+ expect(formatted).toBe("~/Projects/output.mp4");
44
+ });
45
+ });
@@ -1,5 +1,8 @@
1
1
  "use strict";
2
2
  var __webpack_exports__ = {};
3
+ const external_node_fs_namespaceObject = require("node:fs");
4
+ const external_node_os_namespaceObject = require("node:os");
5
+ const external_node_path_namespaceObject = require("node:path");
3
6
  const external_vitest_namespaceObject = require("vitest");
4
7
  const index_cjs_namespaceObject = require("../gateway/index.cjs");
5
8
  function _define_property(obj, key, value) {
@@ -38,6 +41,9 @@ external_vitest_namespaceObject.vi.mock("@/cli/core/agentInvoker.js", ()=>({
38
41
  cancelled: true
39
42
  };
40
43
  }
44
+ if ("return-no-event" === content) return {
45
+ streaming: true
46
+ };
41
47
  this.outputManager?.emitAgentComplete?.({
42
48
  streaming: true
43
49
  });
@@ -54,7 +60,9 @@ external_vitest_namespaceObject.vi.mock("@/cli/core/agentInvoker.js", ()=>({
54
60
  describeIfBun("Gateway", ()=>{
55
61
  let server;
56
62
  let port = 0;
63
+ let testWorkspace;
57
64
  (0, external_vitest_namespaceObject.beforeAll)(async ()=>{
65
+ testWorkspace = (0, external_node_fs_namespaceObject.mkdtempSync)((0, external_node_path_namespaceObject.join)((0, external_node_os_namespaceObject.tmpdir)(), "wingman-gateway-test-"));
58
66
  const instance = new index_cjs_namespaceObject.GatewayServer({
59
67
  port: 0,
60
68
  host: "localhost",
@@ -62,7 +70,10 @@ describeIfBun("Gateway", ()=>{
62
70
  auth: {
63
71
  mode: "none"
64
72
  },
65
- logLevel: "silent"
73
+ logLevel: "silent",
74
+ workspace: testWorkspace,
75
+ configDir: ".wingman-test-config",
76
+ stateDir: ".wingman-test-state"
66
77
  });
67
78
  await instance.start();
68
79
  server = instance;
@@ -72,6 +83,10 @@ describeIfBun("Gateway", ()=>{
72
83
  });
73
84
  (0, external_vitest_namespaceObject.afterAll)(async ()=>{
74
85
  if (server) await server.stop();
86
+ if (testWorkspace) (0, external_node_fs_namespaceObject.rmSync)(testWorkspace, {
87
+ recursive: true,
88
+ force: true
89
+ });
75
90
  });
76
91
  const connectClient = (instanceId, clientType = "test")=>new Promise((resolve, reject)=>{
77
92
  const ws = new WebSocket(`ws://localhost:${port}/ws`);
@@ -117,6 +132,24 @@ describeIfBun("Gateway", ()=>{
117
132
  };
118
133
  ws.addEventListener("message", handler);
119
134
  });
135
+ const collectMessages = (ws, predicate, durationMs = 600)=>new Promise((resolve)=>{
136
+ const matches = [];
137
+ const handler = (event)=>{
138
+ let msg;
139
+ try {
140
+ msg = JSON.parse(event.data);
141
+ } catch {
142
+ return;
143
+ }
144
+ if (!predicate(msg)) return;
145
+ matches.push(msg);
146
+ };
147
+ ws.addEventListener("message", handler);
148
+ setTimeout(()=>{
149
+ ws.removeEventListener("message", handler);
150
+ resolve(matches);
151
+ }, durationMs);
152
+ });
120
153
  (0, external_vitest_namespaceObject.it)("should start the gateway server", async ()=>{
121
154
  const response = await fetch(`http://localhost:${port}/health`);
122
155
  (0, external_vitest_namespaceObject.expect)(response.ok).toBe(true);
@@ -354,6 +387,49 @@ describeIfBun("Gateway", ()=>{
354
387
  (0, external_vitest_namespaceObject.expect)(errorMsg.payload?.agentId).toBe("main");
355
388
  requester.close();
356
389
  });
390
+ (0, external_vitest_namespaceObject.it)("should emit agent-complete when invocation returns without terminal output events", async ()=>{
391
+ const requester = await connectClient("session-complete-fallback-requester");
392
+ const requestId = "req-complete-fallback";
393
+ const sessionId = "session-complete-fallback";
394
+ requester.send(JSON.stringify({
395
+ type: "req:agent",
396
+ id: requestId,
397
+ payload: {
398
+ agentId: "main",
399
+ sessionKey: sessionId,
400
+ content: "return-no-event"
401
+ },
402
+ timestamp: Date.now()
403
+ }));
404
+ const completeMsg = await waitForMessage(requester, (msg)=>"event:agent" === msg.type && msg.id === requestId && msg.payload?.type === "agent-complete");
405
+ (0, external_vitest_namespaceObject.expect)(completeMsg.payload?.sessionId).toBe(sessionId);
406
+ (0, external_vitest_namespaceObject.expect)(completeMsg.payload?.agentId).toBe("main");
407
+ (0, external_vitest_namespaceObject.expect)(completeMsg.payload?.result).toEqual({
408
+ streaming: true
409
+ });
410
+ requester.close();
411
+ });
412
+ (0, external_vitest_namespaceObject.it)("should emit a single agent-complete terminal event per request", async ()=>{
413
+ const requester = await connectClient("session-single-complete-requester");
414
+ const requestId = "req-single-complete";
415
+ const sessionId = "session-single-complete";
416
+ const completionEventsPromise = collectMessages(requester, (msg)=>"event:agent" === msg.type && msg.id === requestId && msg.payload?.type === "agent-complete", 1000);
417
+ requester.send(JSON.stringify({
418
+ type: "req:agent",
419
+ id: requestId,
420
+ payload: {
421
+ agentId: "main",
422
+ sessionKey: sessionId,
423
+ content: "single-complete"
424
+ },
425
+ timestamp: Date.now()
426
+ }));
427
+ const completionEvents = await completionEventsPromise;
428
+ (0, external_vitest_namespaceObject.expect)(completionEvents).toHaveLength(1);
429
+ (0, external_vitest_namespaceObject.expect)(completionEvents[0].payload?.sessionId).toBe(sessionId);
430
+ (0, external_vitest_namespaceObject.expect)(completionEvents[0].payload?.agentId).toBe("main");
431
+ requester.close();
432
+ });
357
433
  (0, external_vitest_namespaceObject.it)("should cancel an in-flight agent request", async ()=>{
358
434
  const requester = await connectClient("session-cancel-requester");
359
435
  const requestId = "req-cancel-test";
@@ -460,6 +536,9 @@ describeIfBun("Gateway", ()=>{
460
536
  }));
461
537
  const cancelAck = await waitForMessage(requester, (msg)=>"ack" === msg.type && msg.payload?.action === "req:agent:cancel" && msg.payload?.requestId === secondRequestId && msg.payload?.status === "cancelled_queued", 10000);
462
538
  (0, external_vitest_namespaceObject.expect)(cancelAck.payload?.status).toBe("cancelled_queued");
539
+ const cancelEvent = await waitForMessage(requester, (msg)=>"event:agent" === msg.type && msg.id === secondRequestId && msg.payload?.type === "agent-error" && /cancel/i.test(String(msg.payload?.error || "")), 10000);
540
+ (0, external_vitest_namespaceObject.expect)(cancelEvent.payload?.sessionId).toBe(sessionId);
541
+ (0, external_vitest_namespaceObject.expect)(cancelEvent.payload?.agentId).toBe("main");
463
542
  await waitForMessage(requester, (msg)=>"event:agent" === msg.type && msg.id === firstRequestId && msg.payload?.type === "agent-complete", 10000);
464
543
  const queuedRequests = server.queuedSessionRequests;
465
544
  const isStillQueued = [
@@ -496,6 +575,35 @@ describeIfBun("Gateway", ()=>{
496
575
  (0, external_vitest_namespaceObject.expect)(updated?.messageCount).toBe(0);
497
576
  (0, external_vitest_namespaceObject.expect)(updated?.lastMessagePreview).toBeNull();
498
577
  });
578
+ (0, external_vitest_namespaceObject.it)("persists failed first-turn messages so the thread survives reload", async ()=>{
579
+ const requester = await connectClient("persist-failed-turn-requester");
580
+ const sessionId = `session-persist-failed-${Date.now()}`;
581
+ const requestId = `req-persist-failed-${Date.now()}`;
582
+ requester.send(JSON.stringify({
583
+ type: "req:agent",
584
+ id: requestId,
585
+ payload: {
586
+ agentId: "main",
587
+ sessionKey: sessionId,
588
+ content: "throw-no-event"
589
+ },
590
+ timestamp: Date.now()
591
+ }));
592
+ await waitForMessage(requester, (msg)=>"event:agent" === msg.type && msg.id === requestId && msg.payload?.type === "agent-error", 10000);
593
+ const sessionsRes = await fetch(`http://localhost:${port}/api/sessions?limit=100`);
594
+ (0, external_vitest_namespaceObject.expect)(sessionsRes.ok).toBe(true);
595
+ const sessions = await sessionsRes.json();
596
+ const created = sessions.find((session)=>session.id === sessionId);
597
+ (0, external_vitest_namespaceObject.expect)(created).toBeTruthy();
598
+ (0, external_vitest_namespaceObject.expect)(created?.messageCount).toBe(1);
599
+ const messagesRes = await fetch(`http://localhost:${port}/api/sessions/${encodeURIComponent(sessionId)}/messages?agentId=main`);
600
+ (0, external_vitest_namespaceObject.expect)(messagesRes.ok).toBe(true);
601
+ const messages = await messagesRes.json();
602
+ (0, external_vitest_namespaceObject.expect)(messages.some((message)=>"user" === message.role)).toBe(true);
603
+ (0, external_vitest_namespaceObject.expect)(messages.some((message)=>"user" === message.role && message.content.includes("throw-no-event"))).toBe(true);
604
+ (0, external_vitest_namespaceObject.expect)(messages.some((message)=>"assistant" === message.role && message.content.includes("Synthetic invocation failure"))).toBe(true);
605
+ requester.close();
606
+ });
499
607
  });
500
608
  for(var __rspack_i in __webpack_exports__)exports[__rspack_i] = __webpack_exports__[__rspack_i];
501
609
  Object.defineProperty(exports, '__esModule', {
@@ -1,3 +1,6 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
2
5
  import { GatewayClient, GatewayServer } from "../gateway/index.js";
3
6
  function _define_property(obj, key, value) {
@@ -36,6 +39,9 @@ vi.mock("@/cli/core/agentInvoker.js", ()=>({
36
39
  cancelled: true
37
40
  };
38
41
  }
42
+ if ("return-no-event" === content) return {
43
+ streaming: true
44
+ };
39
45
  this.outputManager?.emitAgentComplete?.({
40
46
  streaming: true
41
47
  });
@@ -52,7 +58,9 @@ vi.mock("@/cli/core/agentInvoker.js", ()=>({
52
58
  describeIfBun("Gateway", ()=>{
53
59
  let server;
54
60
  let port = 0;
61
+ let testWorkspace;
55
62
  beforeAll(async ()=>{
63
+ testWorkspace = mkdtempSync(join(tmpdir(), "wingman-gateway-test-"));
56
64
  const instance = new GatewayServer({
57
65
  port: 0,
58
66
  host: "localhost",
@@ -60,7 +68,10 @@ describeIfBun("Gateway", ()=>{
60
68
  auth: {
61
69
  mode: "none"
62
70
  },
63
- logLevel: "silent"
71
+ logLevel: "silent",
72
+ workspace: testWorkspace,
73
+ configDir: ".wingman-test-config",
74
+ stateDir: ".wingman-test-state"
64
75
  });
65
76
  await instance.start();
66
77
  server = instance;
@@ -70,6 +81,10 @@ describeIfBun("Gateway", ()=>{
70
81
  });
71
82
  afterAll(async ()=>{
72
83
  if (server) await server.stop();
84
+ if (testWorkspace) rmSync(testWorkspace, {
85
+ recursive: true,
86
+ force: true
87
+ });
73
88
  });
74
89
  const connectClient = (instanceId, clientType = "test")=>new Promise((resolve, reject)=>{
75
90
  const ws = new WebSocket(`ws://localhost:${port}/ws`);
@@ -115,6 +130,24 @@ describeIfBun("Gateway", ()=>{
115
130
  };
116
131
  ws.addEventListener("message", handler);
117
132
  });
133
+ const collectMessages = (ws, predicate, durationMs = 600)=>new Promise((resolve)=>{
134
+ const matches = [];
135
+ const handler = (event)=>{
136
+ let msg;
137
+ try {
138
+ msg = JSON.parse(event.data);
139
+ } catch {
140
+ return;
141
+ }
142
+ if (!predicate(msg)) return;
143
+ matches.push(msg);
144
+ };
145
+ ws.addEventListener("message", handler);
146
+ setTimeout(()=>{
147
+ ws.removeEventListener("message", handler);
148
+ resolve(matches);
149
+ }, durationMs);
150
+ });
118
151
  it("should start the gateway server", async ()=>{
119
152
  const response = await fetch(`http://localhost:${port}/health`);
120
153
  expect(response.ok).toBe(true);
@@ -352,6 +385,49 @@ describeIfBun("Gateway", ()=>{
352
385
  expect(errorMsg.payload?.agentId).toBe("main");
353
386
  requester.close();
354
387
  });
388
+ it("should emit agent-complete when invocation returns without terminal output events", async ()=>{
389
+ const requester = await connectClient("session-complete-fallback-requester");
390
+ const requestId = "req-complete-fallback";
391
+ const sessionId = "session-complete-fallback";
392
+ requester.send(JSON.stringify({
393
+ type: "req:agent",
394
+ id: requestId,
395
+ payload: {
396
+ agentId: "main",
397
+ sessionKey: sessionId,
398
+ content: "return-no-event"
399
+ },
400
+ timestamp: Date.now()
401
+ }));
402
+ const completeMsg = await waitForMessage(requester, (msg)=>"event:agent" === msg.type && msg.id === requestId && msg.payload?.type === "agent-complete");
403
+ expect(completeMsg.payload?.sessionId).toBe(sessionId);
404
+ expect(completeMsg.payload?.agentId).toBe("main");
405
+ expect(completeMsg.payload?.result).toEqual({
406
+ streaming: true
407
+ });
408
+ requester.close();
409
+ });
410
+ it("should emit a single agent-complete terminal event per request", async ()=>{
411
+ const requester = await connectClient("session-single-complete-requester");
412
+ const requestId = "req-single-complete";
413
+ const sessionId = "session-single-complete";
414
+ const completionEventsPromise = collectMessages(requester, (msg)=>"event:agent" === msg.type && msg.id === requestId && msg.payload?.type === "agent-complete", 1000);
415
+ requester.send(JSON.stringify({
416
+ type: "req:agent",
417
+ id: requestId,
418
+ payload: {
419
+ agentId: "main",
420
+ sessionKey: sessionId,
421
+ content: "single-complete"
422
+ },
423
+ timestamp: Date.now()
424
+ }));
425
+ const completionEvents = await completionEventsPromise;
426
+ expect(completionEvents).toHaveLength(1);
427
+ expect(completionEvents[0].payload?.sessionId).toBe(sessionId);
428
+ expect(completionEvents[0].payload?.agentId).toBe("main");
429
+ requester.close();
430
+ });
355
431
  it("should cancel an in-flight agent request", async ()=>{
356
432
  const requester = await connectClient("session-cancel-requester");
357
433
  const requestId = "req-cancel-test";
@@ -458,6 +534,9 @@ describeIfBun("Gateway", ()=>{
458
534
  }));
459
535
  const cancelAck = await waitForMessage(requester, (msg)=>"ack" === msg.type && msg.payload?.action === "req:agent:cancel" && msg.payload?.requestId === secondRequestId && msg.payload?.status === "cancelled_queued", 10000);
460
536
  expect(cancelAck.payload?.status).toBe("cancelled_queued");
537
+ const cancelEvent = await waitForMessage(requester, (msg)=>"event:agent" === msg.type && msg.id === secondRequestId && msg.payload?.type === "agent-error" && /cancel/i.test(String(msg.payload?.error || "")), 10000);
538
+ expect(cancelEvent.payload?.sessionId).toBe(sessionId);
539
+ expect(cancelEvent.payload?.agentId).toBe("main");
461
540
  await waitForMessage(requester, (msg)=>"event:agent" === msg.type && msg.id === firstRequestId && msg.payload?.type === "agent-complete", 10000);
462
541
  const queuedRequests = server.queuedSessionRequests;
463
542
  const isStillQueued = [
@@ -494,4 +573,33 @@ describeIfBun("Gateway", ()=>{
494
573
  expect(updated?.messageCount).toBe(0);
495
574
  expect(updated?.lastMessagePreview).toBeNull();
496
575
  });
576
+ it("persists failed first-turn messages so the thread survives reload", async ()=>{
577
+ const requester = await connectClient("persist-failed-turn-requester");
578
+ const sessionId = `session-persist-failed-${Date.now()}`;
579
+ const requestId = `req-persist-failed-${Date.now()}`;
580
+ requester.send(JSON.stringify({
581
+ type: "req:agent",
582
+ id: requestId,
583
+ payload: {
584
+ agentId: "main",
585
+ sessionKey: sessionId,
586
+ content: "throw-no-event"
587
+ },
588
+ timestamp: Date.now()
589
+ }));
590
+ await waitForMessage(requester, (msg)=>"event:agent" === msg.type && msg.id === requestId && msg.payload?.type === "agent-error", 10000);
591
+ const sessionsRes = await fetch(`http://localhost:${port}/api/sessions?limit=100`);
592
+ expect(sessionsRes.ok).toBe(true);
593
+ const sessions = await sessionsRes.json();
594
+ const created = sessions.find((session)=>session.id === sessionId);
595
+ expect(created).toBeTruthy();
596
+ expect(created?.messageCount).toBe(1);
597
+ const messagesRes = await fetch(`http://localhost:${port}/api/sessions/${encodeURIComponent(sessionId)}/messages?agentId=main`);
598
+ expect(messagesRes.ok).toBe(true);
599
+ const messages = await messagesRes.json();
600
+ expect(messages.some((message)=>"user" === message.role)).toBe(true);
601
+ expect(messages.some((message)=>"user" === message.role && message.content.includes("throw-no-event"))).toBe(true);
602
+ expect(messages.some((message)=>"assistant" === message.role && message.content.includes("Synthetic invocation failure"))).toBe(true);
603
+ requester.close();
604
+ });
497
605
  });
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ var __webpack_exports__ = {};
3
+ const external_node_fs_namespaceObject = require("node:fs");
4
+ const external_node_os_namespaceObject = require("node:os");
5
+ const external_node_path_namespaceObject = require("node:path");
6
+ const external_vitest_namespaceObject = require("vitest");
7
+ const imagePersistence_cjs_namespaceObject = require("../cli/core/imagePersistence.cjs");
8
+ const PNG_DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO3X6S0AAAAASUVORK5CYII=";
9
+ (0, external_vitest_namespaceObject.describe)("imagePersistence", ()=>{
10
+ const tempDirs = [];
11
+ (0, external_vitest_namespaceObject.afterEach)(()=>{
12
+ for (const dir of tempDirs)(0, external_node_fs_namespaceObject.rmSync)(dir, {
13
+ recursive: true,
14
+ force: true
15
+ });
16
+ tempDirs.length = 0;
17
+ });
18
+ (0, external_vitest_namespaceObject.it)("persists assistant image data URLs to disk", ()=>{
19
+ const tempDir = (0, external_node_fs_namespaceObject.mkdtempSync)((0, external_node_path_namespaceObject.join)((0, external_node_os_namespaceObject.tmpdir)(), "wingman-image-store-"));
20
+ tempDirs.push(tempDir);
21
+ const dbPath = (0, external_node_path_namespaceObject.join)(tempDir, "wingman.db");
22
+ const messages = [
23
+ {
24
+ role: "assistant",
25
+ attachments: [
26
+ {
27
+ kind: "image",
28
+ dataUrl: PNG_DATA_URL
29
+ }
30
+ ]
31
+ }
32
+ ];
33
+ (0, imagePersistence_cjs_namespaceObject.persistAssistantImagesToDisk)({
34
+ dbPath,
35
+ sessionId: "session-123",
36
+ messages
37
+ });
38
+ const attachment = messages[0].attachments?.[0];
39
+ (0, external_vitest_namespaceObject.expect)(typeof attachment?.path).toBe("string");
40
+ (0, external_vitest_namespaceObject.expect)(attachment?.mimeType).toBe("image/png");
41
+ (0, external_vitest_namespaceObject.expect)(typeof attachment?.size).toBe("number");
42
+ (0, external_vitest_namespaceObject.expect)(attachment?.size).toBeGreaterThan(0);
43
+ (0, external_vitest_namespaceObject.expect)((0, external_node_fs_namespaceObject.existsSync)(attachment.path)).toBe(true);
44
+ (0, external_vitest_namespaceObject.expect)((0, external_node_fs_namespaceObject.readFileSync)(attachment.path).length).toBe(attachment?.size);
45
+ });
46
+ (0, external_vitest_namespaceObject.it)("does not persist non-assistant images", ()=>{
47
+ const tempDir = (0, external_node_fs_namespaceObject.mkdtempSync)((0, external_node_path_namespaceObject.join)((0, external_node_os_namespaceObject.tmpdir)(), "wingman-image-store-"));
48
+ tempDirs.push(tempDir);
49
+ const dbPath = (0, external_node_path_namespaceObject.join)(tempDir, "wingman.db");
50
+ const messages = [
51
+ {
52
+ role: "user",
53
+ attachments: [
54
+ {
55
+ kind: "image",
56
+ dataUrl: PNG_DATA_URL
57
+ }
58
+ ]
59
+ }
60
+ ];
61
+ (0, imagePersistence_cjs_namespaceObject.persistAssistantImagesToDisk)({
62
+ dbPath,
63
+ sessionId: "session-123",
64
+ messages
65
+ });
66
+ (0, external_vitest_namespaceObject.expect)(messages[0].attachments?.[0].path).toBeUndefined();
67
+ });
68
+ (0, external_vitest_namespaceObject.it)("keeps remote image URLs untouched", ()=>{
69
+ const tempDir = (0, external_node_fs_namespaceObject.mkdtempSync)((0, external_node_path_namespaceObject.join)((0, external_node_os_namespaceObject.tmpdir)(), "wingman-image-store-"));
70
+ tempDirs.push(tempDir);
71
+ const dbPath = (0, external_node_path_namespaceObject.join)(tempDir, "wingman.db");
72
+ const messages = [
73
+ {
74
+ role: "assistant",
75
+ attachments: [
76
+ {
77
+ kind: "image",
78
+ dataUrl: "https://example.com/image.png"
79
+ }
80
+ ]
81
+ }
82
+ ];
83
+ (0, imagePersistence_cjs_namespaceObject.persistAssistantImagesToDisk)({
84
+ dbPath,
85
+ sessionId: "session-123",
86
+ messages
87
+ });
88
+ (0, external_vitest_namespaceObject.expect)(messages[0].attachments?.[0].path).toBeUndefined();
89
+ });
90
+ (0, external_vitest_namespaceObject.it)("derives deterministic file paths for repeated image payloads", ()=>{
91
+ const tempDir = (0, external_node_fs_namespaceObject.mkdtempSync)((0, external_node_path_namespaceObject.join)((0, external_node_os_namespaceObject.tmpdir)(), "wingman-image-store-"));
92
+ tempDirs.push(tempDir);
93
+ const dbPath = (0, external_node_path_namespaceObject.join)(tempDir, "wingman.db");
94
+ const messages = [
95
+ {
96
+ role: "assistant",
97
+ attachments: [
98
+ {
99
+ kind: "image",
100
+ dataUrl: PNG_DATA_URL
101
+ }
102
+ ]
103
+ }
104
+ ];
105
+ (0, imagePersistence_cjs_namespaceObject.persistAssistantImagesToDisk)({
106
+ dbPath,
107
+ sessionId: "session-123",
108
+ messages
109
+ });
110
+ const firstPath = messages[0].attachments?.[0].path;
111
+ const nextMessages = [
112
+ {
113
+ role: "assistant",
114
+ attachments: [
115
+ {
116
+ kind: "image",
117
+ dataUrl: PNG_DATA_URL
118
+ }
119
+ ]
120
+ }
121
+ ];
122
+ (0, imagePersistence_cjs_namespaceObject.persistAssistantImagesToDisk)({
123
+ dbPath,
124
+ sessionId: "session-123",
125
+ messages: nextMessages
126
+ });
127
+ const secondPath = nextMessages[0].attachments?.[0].path;
128
+ (0, external_vitest_namespaceObject.expect)(firstPath).toBeTruthy();
129
+ (0, external_vitest_namespaceObject.expect)(secondPath).toBe(firstPath);
130
+ });
131
+ (0, external_vitest_namespaceObject.it)("parses base64 data URLs and resolves extensions", ()=>{
132
+ const parsed = (0, imagePersistence_cjs_namespaceObject.parseBase64DataUrl)(PNG_DATA_URL);
133
+ (0, external_vitest_namespaceObject.expect)(parsed?.mimeType).toBe("image/png");
134
+ (0, external_vitest_namespaceObject.expect)(typeof parsed?.data).toBe("string");
135
+ (0, external_vitest_namespaceObject.expect)((0, imagePersistence_cjs_namespaceObject.resolveImageExtension)("image/jpeg")).toBe("jpg");
136
+ (0, external_vitest_namespaceObject.expect)((0, imagePersistence_cjs_namespaceObject.resolveImageExtension)("image/svg+xml")).toBe("svg");
137
+ (0, external_vitest_namespaceObject.expect)((0, imagePersistence_cjs_namespaceObject.resolveImageExtension)("image/custom+format")).toBe("customformat");
138
+ });
139
+ });
140
+ for(var __rspack_i in __webpack_exports__)exports[__rspack_i] = __webpack_exports__[__rspack_i];
141
+ Object.defineProperty(exports, '__esModule', {
142
+ value: true
143
+ });
@@ -0,0 +1 @@
1
+ export {};