agent-world 0.13.0 → 0.15.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 (263) hide show
  1. package/README.md +90 -17
  2. package/dist/cli/commands.d.ts +7 -1
  3. package/dist/cli/commands.js +27 -10
  4. package/dist/cli/hitl.d.ts +4 -1
  5. package/dist/cli/hitl.js +55 -20
  6. package/dist/cli/index.js +249 -97
  7. package/dist/cli/system-events.d.ts +27 -0
  8. package/dist/cli/system-events.js +63 -0
  9. package/dist/core/activity-tracker.d.ts +26 -0
  10. package/dist/core/activity-tracker.d.ts.map +1 -1
  11. package/dist/core/activity-tracker.js +21 -4
  12. package/dist/core/activity-tracker.js.map +1 -1
  13. package/dist/core/anthropic-direct.d.ts +2 -0
  14. package/dist/core/anthropic-direct.d.ts.map +1 -1
  15. package/dist/core/anthropic-direct.js +43 -1
  16. package/dist/core/anthropic-direct.js.map +1 -1
  17. package/dist/core/chat-constants.d.ts +12 -0
  18. package/dist/core/chat-constants.d.ts.map +1 -1
  19. package/dist/core/chat-constants.js +5 -0
  20. package/dist/core/chat-constants.js.map +1 -1
  21. package/dist/core/create-agent-tool.d.ts +5 -0
  22. package/dist/core/create-agent-tool.d.ts.map +1 -1
  23. package/dist/core/create-agent-tool.js +57 -34
  24. package/dist/core/create-agent-tool.js.map +1 -1
  25. package/dist/core/events/index.d.ts +5 -2
  26. package/dist/core/events/index.d.ts.map +1 -1
  27. package/dist/core/events/index.js +5 -2
  28. package/dist/core/events/index.js.map +1 -1
  29. package/dist/core/events/memory-manager.d.ts +26 -1
  30. package/dist/core/events/memory-manager.d.ts.map +1 -1
  31. package/dist/core/events/memory-manager.js +877 -72
  32. package/dist/core/events/memory-manager.js.map +1 -1
  33. package/dist/core/events/orchestrator.d.ts +8 -0
  34. package/dist/core/events/orchestrator.d.ts.map +1 -1
  35. package/dist/core/events/orchestrator.js +203 -36
  36. package/dist/core/events/orchestrator.js.map +1 -1
  37. package/dist/core/events/persistence.d.ts +21 -14
  38. package/dist/core/events/persistence.d.ts.map +1 -1
  39. package/dist/core/events/persistence.js +100 -35
  40. package/dist/core/events/persistence.js.map +1 -1
  41. package/dist/core/events/publishers.d.ts +13 -7
  42. package/dist/core/events/publishers.d.ts.map +1 -1
  43. package/dist/core/events/publishers.js +53 -37
  44. package/dist/core/events/publishers.js.map +1 -1
  45. package/dist/core/events/subscribers.d.ts +17 -14
  46. package/dist/core/events/subscribers.d.ts.map +1 -1
  47. package/dist/core/events/subscribers.js +61 -148
  48. package/dist/core/events/subscribers.js.map +1 -1
  49. package/dist/core/events/title-scheduler.d.ts +27 -0
  50. package/dist/core/events/title-scheduler.d.ts.map +1 -0
  51. package/dist/core/events/title-scheduler.js +135 -0
  52. package/dist/core/events/title-scheduler.js.map +1 -0
  53. package/dist/core/events/tool-bridge-logging.d.ts +4 -1
  54. package/dist/core/events/tool-bridge-logging.d.ts.map +1 -1
  55. package/dist/core/events/tool-bridge-logging.js +112 -13
  56. package/dist/core/events/tool-bridge-logging.js.map +1 -1
  57. package/dist/core/events-metadata.d.ts.map +1 -1
  58. package/dist/core/events-metadata.js +8 -4
  59. package/dist/core/events-metadata.js.map +1 -1
  60. package/dist/core/export.d.ts +1 -1
  61. package/dist/core/export.d.ts.map +1 -1
  62. package/dist/core/export.js +2 -15
  63. package/dist/core/export.js.map +1 -1
  64. package/dist/core/feature-path-logging.d.ts +50 -0
  65. package/dist/core/feature-path-logging.d.ts.map +1 -0
  66. package/dist/core/feature-path-logging.js +130 -0
  67. package/dist/core/feature-path-logging.js.map +1 -0
  68. package/dist/core/file-tools.d.ts +57 -1
  69. package/dist/core/file-tools.d.ts.map +1 -1
  70. package/dist/core/file-tools.js +329 -29
  71. package/dist/core/file-tools.js.map +1 -1
  72. package/dist/core/google-direct.d.ts +6 -1
  73. package/dist/core/google-direct.d.ts.map +1 -1
  74. package/dist/core/google-direct.js +76 -7
  75. package/dist/core/google-direct.js.map +1 -1
  76. package/dist/core/heartbeat.d.ts +34 -0
  77. package/dist/core/heartbeat.d.ts.map +1 -0
  78. package/dist/core/heartbeat.js +153 -0
  79. package/dist/core/heartbeat.js.map +1 -0
  80. package/dist/core/hitl-tool.d.ts +6 -12
  81. package/dist/core/hitl-tool.d.ts.map +1 -1
  82. package/dist/core/hitl-tool.js +66 -88
  83. package/dist/core/hitl-tool.js.map +1 -1
  84. package/dist/core/hitl.d.ts +61 -4
  85. package/dist/core/hitl.d.ts.map +1 -1
  86. package/dist/core/hitl.js +324 -60
  87. package/dist/core/hitl.js.map +1 -1
  88. package/dist/core/index.d.ts +11 -7
  89. package/dist/core/index.d.ts.map +1 -1
  90. package/dist/core/index.js +10 -6
  91. package/dist/core/index.js.map +1 -1
  92. package/dist/core/llm-manager.d.ts +15 -0
  93. package/dist/core/llm-manager.d.ts.map +1 -1
  94. package/dist/core/llm-manager.js +325 -40
  95. package/dist/core/llm-manager.js.map +1 -1
  96. package/dist/core/load-skill-tool.d.ts +36 -3
  97. package/dist/core/load-skill-tool.d.ts.map +1 -1
  98. package/dist/core/load-skill-tool.js +807 -93
  99. package/dist/core/load-skill-tool.js.map +1 -1
  100. package/dist/core/logger.d.ts +14 -0
  101. package/dist/core/logger.d.ts.map +1 -1
  102. package/dist/core/logger.js +15 -0
  103. package/dist/core/logger.js.map +1 -1
  104. package/dist/core/managers.d.ts +18 -50
  105. package/dist/core/managers.d.ts.map +1 -1
  106. package/dist/core/managers.js +340 -502
  107. package/dist/core/managers.js.map +1 -1
  108. package/dist/core/mcp-server-registry.d.ts +16 -1
  109. package/dist/core/mcp-server-registry.d.ts.map +1 -1
  110. package/dist/core/mcp-server-registry.js +162 -12
  111. package/dist/core/mcp-server-registry.js.map +1 -1
  112. package/dist/core/message-cutoff.d.ts +29 -0
  113. package/dist/core/message-cutoff.d.ts.map +1 -0
  114. package/dist/core/message-cutoff.js +63 -0
  115. package/dist/core/message-cutoff.js.map +1 -0
  116. package/dist/core/message-edit-manager.d.ts +54 -0
  117. package/dist/core/message-edit-manager.d.ts.map +1 -0
  118. package/dist/core/message-edit-manager.js +602 -0
  119. package/dist/core/message-edit-manager.js.map +1 -0
  120. package/dist/core/message-prep.d.ts +2 -0
  121. package/dist/core/message-prep.d.ts.map +1 -1
  122. package/dist/core/message-prep.js +39 -12
  123. package/dist/core/message-prep.js.map +1 -1
  124. package/dist/core/message-processing-control.d.ts +1 -0
  125. package/dist/core/message-processing-control.d.ts.map +1 -1
  126. package/dist/core/message-processing-control.js +23 -6
  127. package/dist/core/message-processing-control.js.map +1 -1
  128. package/dist/core/openai-direct.d.ts +9 -3
  129. package/dist/core/openai-direct.d.ts.map +1 -1
  130. package/dist/core/openai-direct.js +267 -33
  131. package/dist/core/openai-direct.js.map +1 -1
  132. package/dist/core/optional-tracers/opik-runtime.d.ts +32 -0
  133. package/dist/core/optional-tracers/opik-runtime.d.ts.map +1 -0
  134. package/dist/core/optional-tracers/opik-runtime.js +141 -0
  135. package/dist/core/optional-tracers/opik-runtime.js.map +1 -0
  136. package/dist/core/queue-manager.d.ts +84 -0
  137. package/dist/core/queue-manager.d.ts.map +1 -0
  138. package/dist/core/queue-manager.js +814 -0
  139. package/dist/core/queue-manager.js.map +1 -0
  140. package/dist/core/reasoning-controls.d.ts +30 -0
  141. package/dist/core/reasoning-controls.d.ts.map +1 -0
  142. package/dist/core/reasoning-controls.js +118 -0
  143. package/dist/core/reasoning-controls.js.map +1 -0
  144. package/dist/core/reliability-config.d.ts +82 -0
  145. package/dist/core/reliability-config.d.ts.map +1 -0
  146. package/dist/core/reliability-config.js +106 -0
  147. package/dist/core/reliability-config.js.map +1 -0
  148. package/dist/core/reliability-runtime.d.ts +53 -0
  149. package/dist/core/reliability-runtime.d.ts.map +1 -0
  150. package/dist/core/reliability-runtime.js +92 -0
  151. package/dist/core/reliability-runtime.js.map +1 -0
  152. package/dist/core/security/guardrails.d.ts +21 -0
  153. package/dist/core/security/guardrails.d.ts.map +1 -0
  154. package/dist/core/security/guardrails.js +111 -0
  155. package/dist/core/security/guardrails.js.map +1 -0
  156. package/dist/core/send-message-tool.d.ts +79 -0
  157. package/dist/core/send-message-tool.d.ts.map +1 -0
  158. package/dist/core/send-message-tool.js +222 -0
  159. package/dist/core/send-message-tool.js.map +1 -0
  160. package/dist/core/shell-cmd-tool.d.ts +82 -1
  161. package/dist/core/shell-cmd-tool.d.ts.map +1 -1
  162. package/dist/core/shell-cmd-tool.js +854 -42
  163. package/dist/core/shell-cmd-tool.js.map +1 -1
  164. package/dist/core/skill-registry.d.ts +2 -0
  165. package/dist/core/skill-registry.d.ts.map +1 -1
  166. package/dist/core/skill-registry.js +52 -2
  167. package/dist/core/skill-registry.js.map +1 -1
  168. package/dist/core/storage/eventStorage/fileEventStorage.d.ts +5 -0
  169. package/dist/core/storage/eventStorage/fileEventStorage.d.ts.map +1 -1
  170. package/dist/core/storage/eventStorage/fileEventStorage.js +61 -0
  171. package/dist/core/storage/eventStorage/fileEventStorage.js.map +1 -1
  172. package/dist/core/storage/eventStorage/memoryEventStorage.d.ts +5 -0
  173. package/dist/core/storage/eventStorage/memoryEventStorage.d.ts.map +1 -1
  174. package/dist/core/storage/eventStorage/memoryEventStorage.js +34 -0
  175. package/dist/core/storage/eventStorage/memoryEventStorage.js.map +1 -1
  176. package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts +1 -0
  177. package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts.map +1 -1
  178. package/dist/core/storage/eventStorage/sqliteEventStorage.js +19 -2
  179. package/dist/core/storage/eventStorage/sqliteEventStorage.js.map +1 -1
  180. package/dist/core/storage/eventStorage/types.d.ts +6 -0
  181. package/dist/core/storage/eventStorage/types.d.ts.map +1 -1
  182. package/dist/core/storage/eventStorage/types.js +1 -0
  183. package/dist/core/storage/eventStorage/types.js.map +1 -1
  184. package/dist/core/storage/eventStorage/validation.d.ts.map +1 -1
  185. package/dist/core/storage/eventStorage/validation.js +2 -1
  186. package/dist/core/storage/eventStorage/validation.js.map +1 -1
  187. package/dist/core/storage/github-world-import.d.ts +84 -0
  188. package/dist/core/storage/github-world-import.d.ts.map +1 -0
  189. package/dist/core/storage/github-world-import.js +365 -0
  190. package/dist/core/storage/github-world-import.js.map +1 -0
  191. package/dist/core/storage/memory-storage.d.ts +19 -8
  192. package/dist/core/storage/memory-storage.d.ts.map +1 -1
  193. package/dist/core/storage/memory-storage.js +147 -49
  194. package/dist/core/storage/memory-storage.js.map +1 -1
  195. package/dist/core/storage/queue-storage.d.ts +1 -0
  196. package/dist/core/storage/queue-storage.d.ts.map +1 -1
  197. package/dist/core/storage/queue-storage.js +3 -2
  198. package/dist/core/storage/queue-storage.js.map +1 -1
  199. package/dist/core/storage/sqlite-storage.d.ts +14 -9
  200. package/dist/core/storage/sqlite-storage.d.ts.map +1 -1
  201. package/dist/core/storage/sqlite-storage.js +131 -154
  202. package/dist/core/storage/sqlite-storage.js.map +1 -1
  203. package/dist/core/storage/storage-factory.d.ts +3 -0
  204. package/dist/core/storage/storage-factory.d.ts.map +1 -1
  205. package/dist/core/storage/storage-factory.js +175 -89
  206. package/dist/core/storage/storage-factory.js.map +1 -1
  207. package/dist/core/storage/world-storage.d.ts +1 -1
  208. package/dist/core/storage/world-storage.d.ts.map +1 -1
  209. package/dist/core/storage/world-storage.js +5 -1
  210. package/dist/core/storage/world-storage.js.map +1 -1
  211. package/dist/core/storage-init.d.ts +11 -0
  212. package/dist/core/storage-init.d.ts.map +1 -0
  213. package/dist/core/storage-init.js +122 -0
  214. package/dist/core/storage-init.js.map +1 -0
  215. package/dist/core/subscription.d.ts +8 -1
  216. package/dist/core/subscription.d.ts.map +1 -1
  217. package/dist/core/subscription.js +130 -23
  218. package/dist/core/subscription.js.map +1 -1
  219. package/dist/core/tool-approval.d.ts +45 -0
  220. package/dist/core/tool-approval.d.ts.map +1 -0
  221. package/dist/core/tool-approval.js +223 -0
  222. package/dist/core/tool-approval.js.map +1 -0
  223. package/dist/core/tool-execution-envelope.d.ts +87 -0
  224. package/dist/core/tool-execution-envelope.d.ts.map +1 -0
  225. package/dist/core/tool-execution-envelope.js +168 -0
  226. package/dist/core/tool-execution-envelope.js.map +1 -0
  227. package/dist/core/tool-utils.d.ts +7 -2
  228. package/dist/core/tool-utils.d.ts.map +1 -1
  229. package/dist/core/tool-utils.js +81 -17
  230. package/dist/core/tool-utils.js.map +1 -1
  231. package/dist/core/types.d.ts +67 -19
  232. package/dist/core/types.d.ts.map +1 -1
  233. package/dist/core/types.js +3 -0
  234. package/dist/core/types.js.map +1 -1
  235. package/dist/core/utils.d.ts +7 -0
  236. package/dist/core/utils.d.ts.map +1 -1
  237. package/dist/core/utils.js +71 -21
  238. package/dist/core/utils.js.map +1 -1
  239. package/dist/core/web-fetch-tool.d.ts +72 -0
  240. package/dist/core/web-fetch-tool.d.ts.map +1 -0
  241. package/dist/core/web-fetch-tool.js +491 -0
  242. package/dist/core/web-fetch-tool.js.map +1 -0
  243. package/dist/core/world-registry.d.ts +84 -0
  244. package/dist/core/world-registry.d.ts.map +1 -0
  245. package/dist/core/world-registry.js +247 -0
  246. package/dist/core/world-registry.js.map +1 -0
  247. package/dist/public/assets/index-Be-1xtV-.js +104 -0
  248. package/dist/public/assets/index-tsDdiXDU.css +1 -0
  249. package/dist/public/index.html +2 -2
  250. package/dist/public/mcp-sandbox-proxy.html +148 -0
  251. package/dist/server/api.js +260 -18
  252. package/dist/server/error-response.d.ts +27 -0
  253. package/dist/server/error-response.js +77 -0
  254. package/dist/server/index.d.ts +2 -1
  255. package/dist/server/index.js +6 -2
  256. package/dist/server/sse-handler.d.ts +11 -1
  257. package/dist/server/sse-handler.js +194 -34
  258. package/migrations/0015_add_message_queue.sql +36 -0
  259. package/migrations/0016_add_world_heartbeat.sql +13 -0
  260. package/migrations/0017_add_title_provenance.sql +7 -0
  261. package/package.json +31 -10
  262. package/dist/public/assets/index-BW41BxMy.css +0 -1
  263. package/dist/public/assets/index-kO6UJFwK.js +0 -96
@@ -5,12 +5,29 @@
5
5
  * Supports world/agent/chat management with optimized serialization and error handling.
6
6
  *
7
7
  * Changes:
8
+ * - 2026-03-12: World status now keeps the current chat in `queuedChatIds` while a durable `queued`/`sending`
9
+ * message_queue row still exists, preventing false-idle status during post-response queue cleanup.
10
+ * - 2026-03-12: Trailing comma cleanup in WorldUpdateSchema; tool_permission is stored in world.variables env key — no dedicated API schema field needed.
11
+ * - 2026-03-06: Removed runtime `world.currentChatId` fallback from message send routes; chat-scoped sends now require explicit `chatId`.
12
+ * - 2026-03-06: Added `/tool-artifact` for stable, restorable adopted-tool preview URLs limited to approved world working directories and registered skill roots.
13
+ * - 2026-03-06: Hardened `POST /worlds/:worldName/hitl/respond` for restart-safe validation: when the runtime pending map lacks the request entry but it exists in persisted messages, trigger `activateChatWithSnapshot` to seed the runtime map and return an actionable error.
14
+ * - 2026-03-10: Queue-backed message ingress is now user-only; non-user API senders use explicit immediate dispatch instead of the mixed queue helper.
15
+ * - 2026-03-04: Added queue metadata fields to non-streaming `/messages` success responses (`queueMessageId`, `queueStatus`, `queueRetryCount`) to expose queue terminal/error state.
16
+ * - 2026-02-27: Hardened non-streaming `/messages` event collection with chat-scoped filtering to prevent cross-chat response contamination.
17
+ * - 2026-02-24: Added `hitlPrompts` payload to `POST /worlds/:worldName/setChat/:chatId` responses so web chat switches can render replayed pending HITL prompts.
18
+ * - 2026-03-11: Added restoreChat(suppressAutoResume: true) before subscribeWorld in streaming edit path
19
+ * to sync agent memory from storage, matching Electron editMessageInChat flow and fixing stale-runtime
20
+ * cause of edit-success hang after world delete+recreate in e2e tests.
21
+ * - 2026-03-11: Added restoreChat before subscribeWorld in handleStreamingChat and handleNonStreamingChat
22
+ * send-message paths to match Electron sendChatMessage pattern and prevent stale-agent hangs.
23
+ * - 2026-02-21: Removed temporary server-side folder-picker endpoint in favor of web File API based selection.
8
24
  * - 2026-02-20: Enforced options-only HITL response endpoint `POST /worlds/:worldName/hitl/respond` (`optionId` required).
9
25
  * - 2026-02-14: Added HITL option response endpoint `POST /worlds/:worldName/hitl/respond` for web/CLI approval submissions.
10
26
  * - 2026-02-13: Added core-managed message edit endpoint `PUT /worlds/:worldName/messages/:messageId`
11
27
  * - Delegates edit/remove/resubmit flow to `core.editUserMessage` for cross-client consistency
12
28
  * - Streams edit-resubmission follow-up events over SSE by default (`stream: true`)
13
29
  * - Keeps DELETE endpoint focused on removal-only behavior
30
+ * - 2026-02-21: Extended non-streaming timeout refresh to include shell assistant-stream SSE activity (`start`/`chunk`/`end` with `toolName='shell_cmd'`) in addition to legacy `tool-stream`.
14
31
  * - 2026-02-11: Extended non-streaming timeout on tool-stream events to prevent premature timeout during long-running tools
15
32
  * - Standardized world-scoped routes to use validateWorld middleware to load and attach worldCtx/world
16
33
  * - Removed ad-hoc world loading and undefined getWorldOrError usage; handlers now use (req as any).worldCtx and (req as any).world
@@ -47,12 +64,18 @@
47
64
  * - 2026-02-08: Removed legacy manual intervention endpoint and related server handling
48
65
  */
49
66
  import express from 'express';
67
+ import { promises as fs } from 'fs';
68
+ import * as path from 'path';
50
69
  import { z } from 'zod';
51
70
  import { createSSEHandler } from './sse-handler.js';
52
- import { createWorld, listWorlds, createCategoryLogger, publishMessage, enableStreaming, disableStreaming,
71
+ import { createWorld, listWorlds, createCategoryLogger, enqueueAndProcessUserTurn, dispatchImmediateChatMessage, enableStreaming, disableStreaming,
53
72
  // core managers (function-based)
54
- getWorld, updateWorld, deleteWorld, createAgent, getAgent, updateAgent, deleteAgent, listChats, newChat, restoreChat, deleteChat as deleteChatCore, clearAgentMemory, listAgents as listAgentsCore, getMemory as coreGetMemory, exportWorldToMarkdown, removeMessagesFrom, editUserMessage, stopMessageProcessing, submitWorldHitlResponse, EventType } from '../core/index.js';
73
+ getWorld, updateWorld, deleteWorld, createAgent, getAgent, updateAgent, deleteAgent, listChats, newChat, activateChatWithSnapshot, restoreChat, deleteChat as deleteChatCore, clearAgentMemory, listAgents as listAgentsCore, getMemory as coreGetMemory, exportWorldToMarkdown, removeMessagesFrom, editUserMessage, stopMessageProcessing, submitWorldHitlResponse, listPendingHitlPromptEventsFromMessages, getQueueMessages, getActiveProcessingChatIds, getActiveAgentNames, EventType } from '../core/index.js';
55
74
  import { subscribeWorld } from '../core/index.js';
75
+ // Opik integration: optional tracer attach for API-managed world subscriptions.
76
+ import { attachOptionalOpikTracer } from '../core/optional-tracers/opik-runtime.js';
77
+ import { getSkillSourcePath, getSkills } from '../core/skill-registry.js';
78
+ import { getDefaultWorkingDirectory, getEnvValueFromText, toKebabCase } from '../core/utils.js';
56
79
  import { listMCPServers, restartMCPServer, getMCPSystemHealth, getMCPRegistryStats } from '../core/mcp-server-registry.js';
57
80
  // Function-specific loggers for granular debugging control
58
81
  const loggerWorld = createCategoryLogger('api.world');
@@ -62,6 +85,21 @@ const loggerStream = createCategoryLogger('api.stream');
62
85
  const loggerValidation = createCategoryLogger('api.validation');
63
86
  const loggerMcp = createCategoryLogger('api.mcp');
64
87
  const loggerExport = createCategoryLogger('api.export');
88
+ function isUserSender(sender) {
89
+ const normalized = String(sender || '').trim().toLowerCase();
90
+ return normalized === 'human' || normalized === 'world' || normalized.startsWith('user');
91
+ }
92
+ async function hasPendingCurrentChatQueueMessage(world) {
93
+ const currentChatId = String(world.currentChatId || '').trim();
94
+ if (!currentChatId) {
95
+ return false;
96
+ }
97
+ const queuedMessages = await getQueueMessages(world.id, currentChatId);
98
+ return queuedMessages.some((entry) => {
99
+ const status = String(entry?.status || '').trim().toLowerCase();
100
+ return status === 'queued' || status === 'sending';
101
+ });
102
+ }
65
103
  const DEFAULT_WORLD_NAME = 'Default World';
66
104
  // World context factory - eliminates repetitive worldId passing
67
105
  function createWorldContext(worldId) {
@@ -97,6 +135,8 @@ function serializeWorld(world) {
97
135
  currentChatId: world.currentChatId || null,
98
136
  mcpConfig: world.mcpConfig || null,
99
137
  variables: typeof world.variables === 'string' ? world.variables : '',
138
+ uiMode: world.uiMode || 'chat',
139
+ dashboardZones: world.dashboardZones || [],
100
140
  agents: Array.from(world.agents.values()).map(serializeAgent),
101
141
  chats: Array.from(world.chats.values()).map(serializeChat)
102
142
  };
@@ -132,8 +172,77 @@ function sendError(res, status, message, code, details) {
132
172
  error.details = details;
133
173
  res.status(status).json(error);
134
174
  }
135
- function toKebabCase(name) {
136
- return name.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
175
+ function isPathWithinRoot(rootPath, candidatePath) {
176
+ const normalizedRoot = normalizeResolvedPath(rootPath);
177
+ const normalizedCandidate = normalizeResolvedPath(candidatePath);
178
+ return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}/`);
179
+ }
180
+ function normalizeResolvedPath(targetPath) {
181
+ return path.resolve(targetPath).replace(/\\/g, '/').replace(/\/+/g, '/').replace(/\/$/, '') || '/';
182
+ }
183
+ async function getRegisteredSkillRoots() {
184
+ const skillRoots = await Promise.all(getSkills()
185
+ .map((skill) => getSkillSourcePath(skill.skill_id))
186
+ .filter((skillPath) => typeof skillPath === 'string' && skillPath.trim().length > 0)
187
+ .map(async (skillPath) => {
188
+ try {
189
+ return normalizeResolvedPath(path.dirname(await fs.realpath(path.resolve(skillPath))));
190
+ }
191
+ catch {
192
+ return null;
193
+ }
194
+ }));
195
+ return [...new Set(skillRoots.filter((skillRoot) => typeof skillRoot === 'string' && skillRoot.length > 0))];
196
+ }
197
+ async function resolveWorldArtifactRoot(worldId) {
198
+ if (!worldId) {
199
+ return null;
200
+ }
201
+ const world = await getWorld(toKebabCase(worldId));
202
+ if (!world) {
203
+ return null;
204
+ }
205
+ const lexicalRoot = path.resolve(getEnvValueFromText(typeof world.variables === 'string' ? world.variables : '', 'working_directory') || getDefaultWorkingDirectory());
206
+ try {
207
+ return normalizeResolvedPath(await fs.realpath(lexicalRoot));
208
+ }
209
+ catch {
210
+ return null;
211
+ }
212
+ }
213
+ async function resolveToolArtifactPath(requestedPath, worldId) {
214
+ const normalizedPath = String(requestedPath || '').trim();
215
+ if (!normalizedPath) {
216
+ return null;
217
+ }
218
+ const worldRoot = await resolveWorldArtifactRoot(worldId);
219
+ const skillRoots = await getRegisteredSkillRoots();
220
+ const uniqueRoots = [...new Set([
221
+ ...(worldRoot ? [worldRoot] : []),
222
+ ...skillRoots,
223
+ ].map((root) => normalizeResolvedPath(root)))];
224
+ if (uniqueRoots.length === 0) {
225
+ return null;
226
+ }
227
+ const candidatePaths = path.isAbsolute(normalizedPath)
228
+ ? [normalizeResolvedPath(normalizedPath)]
229
+ : uniqueRoots.map((root) => normalizeResolvedPath(path.resolve(root, normalizedPath)));
230
+ for (const candidatePath of candidatePaths) {
231
+ try {
232
+ const realCandidatePath = normalizeResolvedPath(await fs.realpath(candidatePath));
233
+ if (!uniqueRoots.some((root) => isPathWithinRoot(root, realCandidatePath))) {
234
+ continue;
235
+ }
236
+ const stat = await fs.stat(realCandidatePath);
237
+ if (stat.isFile()) {
238
+ return realCandidatePath;
239
+ }
240
+ }
241
+ catch {
242
+ continue;
243
+ }
244
+ }
245
+ return null;
137
246
  }
138
247
  async function isAgentNameUnique(worldCtx, agentName, excludeAgent) {
139
248
  const normalizedAgentName = toKebabCase(agentName);
@@ -183,7 +292,7 @@ const WorldUpdateSchema = z.object({
183
292
  chatLLMProvider: z.enum(['openai', 'anthropic', 'azure', 'google', 'xai', 'openai-compatible', 'ollama']).nullable().optional(),
184
293
  chatLLMModel: z.string().nullable().optional(),
185
294
  mcpConfig: z.string().nullable().optional(),
186
- variables: z.string().nullable().optional()
295
+ variables: z.string().nullable().optional(),
187
296
  });
188
297
  const AgentCreateSchema = z.object({
189
298
  name: z.string().min(1).max(100),
@@ -201,7 +310,7 @@ const ChatMessageSchema = z.object({
201
310
  message: z.string().min(1),
202
311
  sender: z.string().default("human"),
203
312
  stream: z.boolean().optional().default(true),
204
- chatId: z.string().min(1).optional(),
313
+ chatId: z.string().min(1),
205
314
  messages: z.array(z.any()).optional()
206
315
  });
207
316
  const MessageEditSchema = z.object({
@@ -217,6 +326,10 @@ const HitlResponseSchema = z.object({
217
326
  optionId: z.string().min(1),
218
327
  chatId: z.string().nullable().optional()
219
328
  });
329
+ const ToolArtifactQuerySchema = z.object({
330
+ path: z.string().min(1),
331
+ worldId: z.string().min(1).optional(),
332
+ });
220
333
  const AgentUpdateSchema = z.object({
221
334
  name: z.string().min(1).max(100).optional(),
222
335
  type: z.string().optional(),
@@ -230,6 +343,19 @@ const AgentUpdateSchema = z.object({
230
343
  clearMemory: z.boolean().optional()
231
344
  });
232
345
  const router = express.Router();
346
+ router.get('/tool-artifact', async (req, res) => {
347
+ const validation = ToolArtifactQuerySchema.safeParse(req.query);
348
+ if (!validation.success) {
349
+ sendError(res, 400, 'Invalid tool artifact request', 'VALIDATION_ERROR', validation.error.issues);
350
+ return;
351
+ }
352
+ const resolvedPath = await resolveToolArtifactPath(validation.data.path, validation.data.worldId);
353
+ if (!resolvedPath) {
354
+ sendError(res, 404, 'Tool artifact not found', 'TOOL_ARTIFACT_NOT_FOUND');
355
+ return;
356
+ }
357
+ res.sendFile(resolvedPath);
358
+ });
233
359
  // World Routes
234
360
  router.get('/worlds', async (req, res) => {
235
361
  try {
@@ -268,6 +394,33 @@ router.get('/worlds/:worldName', validateWorld, async (req, res) => {
268
394
  sendError(res, 500, 'Internal server error', 'INTERNAL_ERROR');
269
395
  }
270
396
  });
397
+ router.get('/worlds/:worldName/status', validateWorld, async (req, res) => {
398
+ try {
399
+ const world = req.world;
400
+ const includeCurrentChatQueue = await hasPendingCurrentChatQueueMessage(world);
401
+ const activeChatIds = [...getActiveProcessingChatIds(world)];
402
+ const queuedChatIds = [
403
+ ...new Set([
404
+ ...(world._queuedChatIds ?? []),
405
+ ...(includeCurrentChatQueue && world.currentChatId ? [world.currentChatId] : []),
406
+ ]),
407
+ ];
408
+ const activeAgentNames = getActiveAgentNames(world);
409
+ res.json({
410
+ worldId: world.id,
411
+ isProcessing: world.isProcessing ?? false,
412
+ activeChatIds,
413
+ queuedChatIds,
414
+ activeAgentNames,
415
+ queueDepth: queuedChatIds.length,
416
+ sendingCount: activeChatIds.length,
417
+ });
418
+ }
419
+ catch (error) {
420
+ loggerWorld.error('Error getting world status', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName });
421
+ sendError(res, 500, 'Internal server error', 'INTERNAL_ERROR');
422
+ }
423
+ });
271
424
  router.post('/worlds', async (req, res) => {
272
425
  try {
273
426
  const validation = WorldCreateSchema.safeParse(req.body);
@@ -551,8 +704,28 @@ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
551
704
  disableStreaming();
552
705
  let subscription = null;
553
706
  let listeners = new Map();
707
+ const normalizedChatId = typeof chatId === 'string' ? chatId.trim() : '';
708
+ const scopedChatId = normalizedChatId.length > 0 ? normalizedChatId : null;
709
+ const isChatEventInScope = (eventChatId, includeUnscopedWhenScoped = false) => {
710
+ if (!scopedChatId) {
711
+ return true;
712
+ }
713
+ if (eventChatId === undefined || eventChatId === null) {
714
+ return includeUnscopedWhenScoped;
715
+ }
716
+ return String(eventChatId).trim() === scopedChatId;
717
+ };
554
718
  try {
719
+ // Sync agent memory from storage before subscribing, mirroring the Electron sendChatMessage pattern.
720
+ // Prevents stale-runtime agents (e.g. after agent add/delete with a live runtime) from silently
721
+ // dropping the message with no response-start event.
722
+ if (chatId) {
723
+ await restoreChat(worldName, chatId);
724
+ }
555
725
  let responseContent = '';
726
+ let queuedMessageId = null;
727
+ let queuedStatus = null;
728
+ let queuedRetryCount = null;
556
729
  let isComplete = false;
557
730
  let hasError = false;
558
731
  let errorMessage = '';
@@ -566,7 +739,7 @@ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
566
739
  reject(new Error(errorMessage));
567
740
  }
568
741
  }, 60000); // Longer timeout as fallback since we rely on events
569
- // Helper to reset the fallback timeout (called when tool-stream data arrives)
742
+ // Helper to reset the fallback timeout when long-running shell stream activity arrives.
570
743
  const resetTimeout = () => {
571
744
  clearTimeout(timeoutTimer);
572
745
  timeoutTimer = setTimeout(() => {
@@ -579,7 +752,7 @@ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
579
752
  }, 60000);
580
753
  };
581
754
  // Subscribe with minimal client (no forwarding callbacks)
582
- subscribeWorld(worldName, { isOpen: true }).then(sub => {
755
+ subscribeWorld(worldName, { isOpen: true }).then(async (sub) => {
583
756
  if (!sub) {
584
757
  hasError = true;
585
758
  errorMessage = 'Failed to subscribe to world';
@@ -588,8 +761,12 @@ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
588
761
  }
589
762
  subscription = sub;
590
763
  const world = subscription.world;
764
+ await attachOptionalOpikTracer(world, { source: 'server' });
591
765
  // Listen to world activity events to detect when all processing is complete
592
766
  const worldActivityListener = (eventData) => {
767
+ if (!isChatEventInScope(eventData?.chatId, true)) {
768
+ return;
769
+ }
593
770
  if (eventData.type === 'response-start') {
594
771
  awaitingIdle = true;
595
772
  loggerChat.debug('Non-streaming: world processing started', {
@@ -608,6 +785,9 @@ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
608
785
  };
609
786
  // Collect message events for response
610
787
  const messageListener = (eventData) => {
788
+ if (!isChatEventInScope(eventData?.chatId, false)) {
789
+ return;
790
+ }
611
791
  responseContent = JSON.stringify({ type: 'message', data: eventData });
612
792
  };
613
793
  // Listen to activity events for completion detection
@@ -616,16 +796,33 @@ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
616
796
  // Listen to message events for response content
617
797
  world.eventEmitter.on(EventType.MESSAGE, messageListener);
618
798
  listeners.set(EventType.MESSAGE, messageListener);
619
- // Listen to SSE events to extend timeout on tool-stream data
799
+ // Listen to SSE events to extend timeout on shell stream activity.
620
800
  const sseListener = (eventData) => {
621
- if (eventData.type === 'tool-stream') {
801
+ if (!isChatEventInScope(eventData?.chatId, false)) {
802
+ return;
803
+ }
804
+ const isLegacyToolStream = eventData.type === 'tool-stream';
805
+ const isShellAssistantStream = eventData.toolName === 'shell_cmd' &&
806
+ (eventData.type === 'start' || eventData.type === 'chunk' || eventData.type === 'end');
807
+ if (isLegacyToolStream || isShellAssistantStream) {
622
808
  resetTimeout();
623
809
  }
624
810
  };
625
811
  world.eventEmitter.on(EventType.SSE, sseListener);
626
812
  listeners.set(EventType.SSE, sseListener);
627
- // Publish message
628
- publishMessage(world, message, sender, chatId);
813
+ if (isUserSender(sender)) {
814
+ // Queue-backed user ingress: enqueue then trigger event-driven processing.
815
+ const queued = await enqueueAndProcessUserTurn(world.id, chatId, message, sender, world);
816
+ queuedMessageId = queued?.messageId || null;
817
+ queuedStatus = queued?.status || null;
818
+ queuedRetryCount = typeof queued?.retryCount === 'number' ? queued.retryCount : null;
819
+ }
820
+ else {
821
+ const dispatched = await dispatchImmediateChatMessage(world.id, chatId, message, sender, world);
822
+ queuedMessageId = dispatched?.messageId || null;
823
+ queuedStatus = null;
824
+ queuedRetryCount = null;
825
+ }
629
826
  }).catch(error => {
630
827
  hasError = true;
631
828
  errorMessage = `Failed to connect to world: ${error instanceof Error ? error.message : error}`;
@@ -642,7 +839,10 @@ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
642
839
  message: 'Message processed successfully',
643
840
  data: {
644
841
  content: responseContent || 'No response received',
645
- timestamp: new Date().toISOString()
842
+ timestamp: new Date().toISOString(),
843
+ queueMessageId: queuedMessageId,
844
+ queueStatus: queuedStatus,
845
+ queueRetryCount: queuedRetryCount
646
846
  }
647
847
  });
648
848
  }
@@ -680,6 +880,12 @@ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
680
880
  * @returns Promise that resolves when stream is complete
681
881
  */
682
882
  async function handleStreamingChat(req, res, worldName, message, sender, chatId) {
883
+ // Sync agent memory from storage before subscribing, mirroring the Electron sendChatMessage pattern.
884
+ // Prevents stale-runtime agents (e.g. after agent add/delete with a live runtime) from silently
885
+ // dropping the message with no response-start event.
886
+ if (chatId) {
887
+ await restoreChat(worldName, chatId);
888
+ }
683
889
  // Subscribe to world to get the world instance
684
890
  const subscription = await subscribeWorld(worldName, { isOpen: true });
685
891
  if (!subscription) {
@@ -690,8 +896,10 @@ async function handleStreamingChat(req, res, worldName, message, sender, chatId)
690
896
  return;
691
897
  }
692
898
  const world = subscription.world;
899
+ await attachOptionalOpikTracer(world, { source: 'server' });
693
900
  // Create SSE handler - automatically sets up headers, listeners, and cleanup
694
901
  const sseHandler = createSSEHandler(req, res, world, 'chat', chatId);
902
+ await sseHandler.ready;
695
903
  // Clean up subscription when the HTTP response closes/finishes to prevent stale world
696
904
  // instances from accumulating in activeSubscribedWorlds.
697
905
  let subscriptionCleanedUp = false;
@@ -705,8 +913,12 @@ async function handleStreamingChat(req, res, worldName, message, sender, chatId)
705
913
  res.on('finish', cleanupSubscription);
706
914
  res.on('close', cleanupSubscription);
707
915
  try {
708
- // Publish message - events will be automatically streamed
709
- publishMessage(world, message, sender, chatId);
916
+ if (isUserSender(sender)) {
917
+ await enqueueAndProcessUserTurn(world.id, chatId, message, sender, world);
918
+ }
919
+ else {
920
+ await dispatchImmediateChatMessage(world.id, chatId, message, sender, world);
921
+ }
710
922
  }
711
923
  catch (error) {
712
924
  sseHandler.sendSSE({
@@ -798,6 +1010,10 @@ router.put('/worlds/:worldName/messages/:messageId', validateWorld, async (req,
798
1010
  });
799
1011
  return;
800
1012
  }
1013
+ // Sync agent memory from storage before subscribing so a stale runtime
1014
+ // (e.g. after a world delete+recreate) picks up the fresh agent list.
1015
+ // Mirrors the Electron editMessageInChat flow: restoreChat → ensureWorldSubscribed.
1016
+ await restoreChat(worldCtx.id, chatId, { suppressAutoResume: true });
801
1017
  const subscription = await subscribeWorld(worldCtx.id, { isOpen: true });
802
1018
  if (!subscription?.world) {
803
1019
  res.setHeader('Content-Type', 'text/event-stream');
@@ -807,7 +1023,9 @@ router.put('/worlds/:worldName/messages/:messageId', validateWorld, async (req,
807
1023
  res.end();
808
1024
  return;
809
1025
  }
1026
+ await attachOptionalOpikTracer(subscription.world, { source: 'server' });
810
1027
  const sseHandler = createSSEHandler(req, res, subscription.world, 'edit', chatId);
1028
+ await sseHandler.ready;
811
1029
  let subscriptionCleanedUp = false;
812
1030
  const cleanupSubscription = () => {
813
1031
  if (subscriptionCleanedUp) {
@@ -942,6 +1160,26 @@ router.post('/worlds/:worldName/hitl/respond', validateWorld, async (req, res) =
942
1160
  optionId,
943
1161
  ...(chatId !== undefined ? { chatId } : {}),
944
1162
  });
1163
+ // Restart-safe fallback: if the runtime pending map lacks the entry (e.g. server
1164
+ // restarted before /setChat was called) but the request exists in persisted messages,
1165
+ // trigger chat activation to seed the pending map via the tool-call resume path.
1166
+ // The caller should retry /hitl/respond after receiving the HITL SSE event.
1167
+ if (!result.accepted && result.reason?.includes('No pending HITL request') && chatId) {
1168
+ const memory = await coreGetMemory(worldCtx.id, chatId);
1169
+ if (Array.isArray(memory)) {
1170
+ const surviving = listPendingHitlPromptEventsFromMessages(memory, chatId);
1171
+ const inMessages = surviving.some((item) => item.prompt.requestId === requestId);
1172
+ if (inMessages) {
1173
+ // Trigger restore/resume asynchronously to repopulate the pending map.
1174
+ void activateChatWithSnapshot(worldCtx.id, chatId).catch(() => undefined);
1175
+ res.json({
1176
+ accepted: false,
1177
+ reason: `HITL request '${requestId}' is pending in chat messages but the chat is not yet activated on this server. Call POST /worlds/${req.params.worldName}/setChat/${chatId} to activate, then retry this request.`,
1178
+ });
1179
+ return;
1180
+ }
1181
+ }
1182
+ }
945
1183
  res.json(result);
946
1184
  }
947
1185
  catch (error) {
@@ -1040,18 +1278,22 @@ router.post('/worlds/:worldName/setChat/:chatId', validateWorld, async (req, res
1040
1278
  sendError(res, 404, 'World not found', 'WORLD_NOT_FOUND');
1041
1279
  return;
1042
1280
  }
1043
- const updatedWorld = await worldCtx.setChat(chatId);
1044
- if (!updatedWorld) {
1281
+ const activated = await activateChatWithSnapshot(worldCtx.id, chatId);
1282
+ if (!activated) {
1045
1283
  res.json({
1046
1284
  world: serializeWorld(currentWorld),
1047
1285
  chatId: currentWorld.currentChatId,
1286
+ hitlPrompts: [],
1048
1287
  success: false
1049
1288
  });
1050
1289
  return;
1051
1290
  }
1291
+ const updatedWorld = activated.world;
1292
+ const pendingHitlPrompts = activated.hitlPrompts;
1052
1293
  res.json({
1053
1294
  world: serializeWorld(updatedWorld),
1054
- chatId: updatedWorld.currentChatId,
1295
+ chatId: activated.chatId,
1296
+ hitlPrompts: pendingHitlPrompts,
1055
1297
  success: true
1056
1298
  });
1057
1299
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Server Error Response Mapping
3
+ *
4
+ * Purpose:
5
+ * - Convert runtime/server errors into stable, user-meaningful HTTP responses.
6
+ *
7
+ * Features:
8
+ * - Specific mappings for oversized request payloads and invalid JSON bodies.
9
+ * - Readonly SQLite detection with actionable error messaging.
10
+ * - Safe fallback for unknown internal errors.
11
+ *
12
+ * Implementation Notes:
13
+ * - JSON parse mapping is intentionally strict to body-parser parse errors only,
14
+ * so unrelated `SyntaxError`s in route logic are not mislabeled as request-body issues.
15
+ *
16
+ * Changes:
17
+ * - 2026-02-26: Tightened invalid JSON detection to avoid misclassifying generic runtime SyntaxErrors.
18
+ * - 2026-02-26: Initial extraction from `server/index.ts` for reusable, testable global error mapping.
19
+ */
20
+ export type ErrorResponsePayload = {
21
+ error: string;
22
+ code: string;
23
+ };
24
+ export declare function getErrorResponse(error: unknown): {
25
+ status: number;
26
+ payload: ErrorResponsePayload;
27
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Server Error Response Mapping
3
+ *
4
+ * Purpose:
5
+ * - Convert runtime/server errors into stable, user-meaningful HTTP responses.
6
+ *
7
+ * Features:
8
+ * - Specific mappings for oversized request payloads and invalid JSON bodies.
9
+ * - Readonly SQLite detection with actionable error messaging.
10
+ * - Safe fallback for unknown internal errors.
11
+ *
12
+ * Implementation Notes:
13
+ * - JSON parse mapping is intentionally strict to body-parser parse errors only,
14
+ * so unrelated `SyntaxError`s in route logic are not mislabeled as request-body issues.
15
+ *
16
+ * Changes:
17
+ * - 2026-02-26: Tightened invalid JSON detection to avoid misclassifying generic runtime SyntaxErrors.
18
+ * - 2026-02-26: Initial extraction from `server/index.ts` for reusable, testable global error mapping.
19
+ */
20
+ function isEntityTooLargeError(error) {
21
+ if (!error || typeof error !== 'object')
22
+ return false;
23
+ const candidate = error;
24
+ return candidate.type === 'entity.too.large' || candidate.status === 413 || candidate.statusCode === 413;
25
+ }
26
+ function isJsonParseError(error) {
27
+ if (!error || typeof error !== 'object')
28
+ return false;
29
+ const candidate = error;
30
+ const isBodyParserParseType = candidate.type === 'entity.parse.failed';
31
+ const isBodyParserSyntaxError = error instanceof SyntaxError
32
+ && (candidate.status === 400 || candidate.statusCode === 400)
33
+ && 'body' in candidate;
34
+ return isBodyParserParseType || isBodyParserSyntaxError;
35
+ }
36
+ function isReadonlySqliteError(error) {
37
+ if (!error || typeof error !== 'object')
38
+ return false;
39
+ const candidate = error;
40
+ return candidate.code === 'SQLITE_READONLY' || String(candidate.message || '').includes('SQLITE_READONLY');
41
+ }
42
+ export function getErrorResponse(error) {
43
+ if (isEntityTooLargeError(error)) {
44
+ return {
45
+ status: 413,
46
+ payload: {
47
+ error: 'Request payload too large. Try submitting a smaller update payload.',
48
+ code: 'PAYLOAD_TOO_LARGE'
49
+ }
50
+ };
51
+ }
52
+ if (isJsonParseError(error)) {
53
+ return {
54
+ status: 400,
55
+ payload: {
56
+ error: 'Invalid JSON body. Please check request formatting.',
57
+ code: 'INVALID_JSON_BODY'
58
+ }
59
+ };
60
+ }
61
+ if (isReadonlySqliteError(error)) {
62
+ return {
63
+ status: 503,
64
+ payload: {
65
+ error: 'Database is read-only. Check database file permissions and retry.',
66
+ code: 'DATABASE_READONLY'
67
+ }
68
+ };
69
+ }
70
+ return {
71
+ status: 500,
72
+ payload: {
73
+ error: 'Server failed to process the request.',
74
+ code: 'INTERNAL_ERROR'
75
+ }
76
+ };
77
+ }
@@ -15,15 +15,16 @@
15
15
  * Endpoints: /health + API routes from ./api.ts
16
16
  *
17
17
  * Recent Changes:
18
+ * - 2026-02-26: Added specific global API error mapping for oversized JSON payloads, invalid JSON bodies, and readonly database errors to avoid generic INTERNAL_ERROR responses.
18
19
  * - 2026-02-08: Fixed auto-run detection to only trigger on direct execution
19
20
  * - 2026-02-08: Made browser auto-open configurable via AGENT_WORLD_AUTO_OPEN env var
20
21
  * - 2026-02-08: Prevented duplicate process signal handler registration using WeakSet
21
22
  * - 2026-02-08: Added shutdown guard to prevent race conditions during graceful shutdown
22
23
  */
23
24
  import { Server } from 'http';
25
+ export { getErrorResponse } from './error-response.js';
24
26
  type StartWebServerOptions = {
25
27
  openBrowser?: boolean;
26
28
  registerProcessHandlers?: boolean;
27
29
  };
28
30
  export declare function startWebServer(port?: number, host?: string, options?: StartWebServerOptions): Promise<Server>;
29
- export {};
@@ -15,6 +15,7 @@
15
15
  * Endpoints: /health + API routes from ./api.ts
16
16
  *
17
17
  * Recent Changes:
18
+ * - 2026-02-26: Added specific global API error mapping for oversized JSON payloads, invalid JSON bodies, and readonly database errors to avoid generic INTERNAL_ERROR responses.
18
19
  * - 2026-02-08: Fixed auto-run detection to only trigger on direct execution
19
20
  * - 2026-02-08: Made browser auto-open configurable via AGENT_WORLD_AUTO_OPEN env var
20
21
  * - 2026-02-08: Prevented duplicate process signal handler registration using WeakSet
@@ -32,11 +33,13 @@ import path from 'path';
32
33
  import { fileURLToPath, pathToFileURL } from 'url';
33
34
  import apiRouter from './api.js';
34
35
  import { initializeMCPRegistry, shutdownAllMCPServers } from '../core/mcp-server-registry.js';
36
+ import { getErrorResponse } from './error-response.js';
37
+ export { getErrorResponse } from './error-response.js';
35
38
  // ES modules setup
36
39
  const __filename = fileURLToPath(import.meta.url);
37
40
  const __dirname = path.dirname(__filename);
38
41
  // Configuration
39
- const PORT = Number(process.env.PORT) || 0;
42
+ const PORT = Number(process.env.PORT) || 3000;
40
43
  const HOST = process.env.HOST || '127.0.0.1';
41
44
  // Create server logger after logger auto-initialization
42
45
  const serverLogger = createCategoryLogger('server');
@@ -117,7 +120,8 @@ app.get('*', (req, res) => {
117
120
  // Error handling middleware
118
121
  app.use((error, req, res, next) => {
119
122
  console.error('Unhandled error:', error);
120
- res.status(500).json({ error: 'Internal server error', code: 'INTERNAL_ERROR' });
123
+ const { status, payload } = getErrorResponse(error);
124
+ res.status(status).json(payload);
121
125
  });
122
126
  // 404 handler
123
127
  app.use((req, res) => {
@@ -29,8 +29,14 @@
29
29
  * ```
30
30
  *
31
31
  * Created: 2025-11-10 - Extracted from api.ts for reusability
32
+ * Updated: 2026-03-01 - Skip synthesis for 'edit' context to prevent duplicate "From human" messages after message edits.
33
+ * Updated: 2026-03-11 - Exposed a readiness promise so chat/edit dispatch waits until SSE listeners are attached, preventing the web client from missing the initial user-message echo.
34
+ * Updated: 2026-03-11 - Replaced optional-call resolution of the readiness promise with an explicit helper to satisfy
35
+ * strict TypeScript control-flow analysis in the build config.
36
+ * Updated: 2026-02-27 - Scoped realtime log forwarding by world/chat to prevent cross-chat log leakage in chat-scoped streams.
37
+ * Updated: 2026-02-26 - Added realtime log-stream forwarding (`type: 'log'`) to SSE clients to align web error visibility with Electron.
32
38
  * Updated: 2026-02-20 - Removed stale legacy event-channel SSE forwarding from this handler.
33
- * Updated: 2026-02-20 - Keep `hitl-option-request` system events bypassing strict chat scope filtering so HITL prompts are always delivered.
39
+ * Updated: 2026-02-21 - Refresh fallback timeout on shell assistant-stream SSE activity (`start`/`chunk`/`end` + `toolName='shell_cmd'`) as well as legacy `tool-stream`.
34
40
  * Updated: 2026-02-11 - Extended fallback timeout on tool-stream events to prevent premature timeout
35
41
  * Updated: 2026-02-08 - Removed manual tool-intervention SSE commentary and kept generic tool_call forwarding
36
42
  * Updated: 2025-11-10 - Added tool event forwarding to SSE channel
@@ -38,6 +44,10 @@
38
44
  import { Request, Response } from 'express';
39
45
  import { World } from '../core/index.js';
40
46
  export interface SSEHandler {
47
+ /**
48
+ * Resolves once synthesis has finished and live listeners are attached.
49
+ */
50
+ ready: Promise<void>;
41
51
  /**
42
52
  * Send a Server-Sent Event to the client
43
53
  * @param data - Data object to send (will be JSON stringified)