agent-world 0.12.3 → 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 +105 -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 +9 -2
  5. package/dist/cli/hitl.js +61 -20
  6. package/dist/cli/index.js +250 -96
  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 +38 -2
  10. package/dist/core/activity-tracker.d.ts.map +1 -1
  11. package/dist/core/activity-tracker.js +62 -9
  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 +28 -25
  22. package/dist/core/create-agent-tool.d.ts.map +1 -1
  23. package/dist/core/create-agent-tool.js +264 -141
  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 +214 -38
  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 -61
  40. package/dist/core/events/persistence.js.map +1 -1
  41. package/dist/core/events/publishers.d.ts +13 -16
  42. package/dist/core/events/publishers.d.ts.map +1 -1
  43. package/dist/core/events/publishers.js +54 -55
  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 +68 -147
  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 +73 -0
  81. package/dist/core/hitl-tool.d.ts.map +1 -0
  82. package/dist/core/hitl-tool.js +284 -0
  83. package/dist/core/hitl-tool.js.map +1 -0
  84. package/dist/core/hitl.d.ts +85 -8
  85. package/dist/core/hitl.d.ts.map +1 -1
  86. package/dist/core/hitl.js +375 -61
  87. package/dist/core/hitl.js.map +1 -1
  88. package/dist/core/index.d.ts +12 -7
  89. package/dist/core/index.d.ts.map +1 -1
  90. package/dist/core/index.js +11 -6
  91. package/dist/core/index.js.map +1 -1
  92. package/dist/core/llm-manager.d.ts +17 -0
  93. package/dist/core/llm-manager.d.ts.map +1 -1
  94. package/dist/core/llm-manager.js +335 -43
  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 +41 -52
  105. package/dist/core/managers.d.ts.map +1 -1
  106. package/dist/core/managers.js +422 -533
  107. package/dist/core/managers.js.map +1 -1
  108. package/dist/core/mcp-server-registry.d.ts +19 -2
  109. package/dist/core/mcp-server-registry.d.ts.map +1 -1
  110. package/dist/core/mcp-server-registry.js +168 -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 +9 -2
  228. package/dist/core/tool-utils.d.ts.map +1 -1
  229. package/dist/core/tool-utils.js +122 -28
  230. package/dist/core/tool-utils.js.map +1 -1
  231. package/dist/core/types.d.ts +69 -36
  232. package/dist/core/types.d.ts.map +1 -1
  233. package/dist/core/types.js +3 -2
  234. package/dist/core/types.js.map +1 -1
  235. package/dist/core/utils.d.ts +16 -0
  236. package/dist/core/utils.d.ts.map +1 -1
  237. package/dist/core/utils.js +99 -24
  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 +288 -58
  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 +13 -2
  257. package/dist/server/sse-handler.js +194 -26
  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-BO20H4xt.js +0 -96
  263. package/dist/public/assets/index-ETY7W5_S.css +0 -1
@@ -5,12 +5,29 @@
5
5
  * Supports world/agent/chat management with optimized serialization and error handling.
6
6
  *
7
7
  * Changes:
8
- * - 2026-02-19: Added chat branch endpoint `POST /worlds/:worldName/chats/:chatId/branch/:messageId` for web branching UX.
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.
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, branchChatFromMessage, editUserMessage, stopMessageProcessing, submitWorldOptionResponse, 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,16 +896,29 @@ 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);
695
- // Clean up subscription when the HTTP response finishes to prevent stale world
902
+ await sseHandler.ready;
903
+ // Clean up subscription when the HTTP response closes/finishes to prevent stale world
696
904
  // instances from accumulating in activeSubscribedWorlds.
697
- res.on('finish', () => {
698
- subscription?.unsubscribe();
699
- });
905
+ let subscriptionCleanedUp = false;
906
+ const cleanupSubscription = () => {
907
+ if (subscriptionCleanedUp) {
908
+ return;
909
+ }
910
+ subscriptionCleanedUp = true;
911
+ void subscription.unsubscribe();
912
+ };
913
+ res.on('finish', cleanupSubscription);
914
+ res.on('close', cleanupSubscription);
700
915
  try {
701
- // Publish message - events will be automatically streamed
702
- 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
+ }
703
922
  }
704
923
  catch (error) {
705
924
  sseHandler.sendSSE({
@@ -791,6 +1010,10 @@ router.put('/worlds/:worldName/messages/:messageId', validateWorld, async (req,
791
1010
  });
792
1011
  return;
793
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 });
794
1017
  const subscription = await subscribeWorld(worldCtx.id, { isOpen: true });
795
1018
  if (!subscription?.world) {
796
1019
  res.setHeader('Content-Type', 'text/event-stream');
@@ -800,7 +1023,17 @@ router.put('/worlds/:worldName/messages/:messageId', validateWorld, async (req,
800
1023
  res.end();
801
1024
  return;
802
1025
  }
1026
+ await attachOptionalOpikTracer(subscription.world, { source: 'server' });
803
1027
  const sseHandler = createSSEHandler(req, res, subscription.world, 'edit', chatId);
1028
+ await sseHandler.ready;
1029
+ let subscriptionCleanedUp = false;
1030
+ const cleanupSubscription = () => {
1031
+ if (subscriptionCleanedUp) {
1032
+ return;
1033
+ }
1034
+ subscriptionCleanedUp = true;
1035
+ void subscription.unsubscribe();
1036
+ };
804
1037
  const finalizeWithError = (message, data) => {
805
1038
  sseHandler.sendSSE({
806
1039
  type: 'error',
@@ -809,13 +1042,12 @@ router.put('/worlds/:worldName/messages/:messageId', validateWorld, async (req,
809
1042
  });
810
1043
  setTimeout(() => {
811
1044
  sseHandler.endResponse();
812
- subscription?.unsubscribe();
1045
+ cleanupSubscription();
813
1046
  }, 500);
814
1047
  };
815
- // Clean up subscription when the HTTP response finishes.
816
- res.on('finish', () => {
817
- subscription?.unsubscribe();
818
- });
1048
+ // Clean up subscription when the HTTP response closes/finishes.
1049
+ res.on('finish', cleanupSubscription);
1050
+ res.on('close', cleanupSubscription);
819
1051
  // Pass subscription.world so editUserMessage emits on the same eventEmitter
820
1052
  // that the SSE handler is listening on, avoiding stale-world mismatch.
821
1053
  const result = await editUserMessage(worldCtx.id, messageId, newContent, chatId, subscription.world);
@@ -921,12 +1153,33 @@ router.post('/worlds/:worldName/hitl/respond', validateWorld, async (req, res) =
921
1153
  return;
922
1154
  }
923
1155
  const worldCtx = req.worldCtx;
924
- const { requestId, optionId } = validation.data;
925
- const result = submitWorldOptionResponse({
1156
+ const { requestId, optionId, chatId } = validation.data;
1157
+ const result = submitWorldHitlResponse({
926
1158
  worldId: worldCtx.id,
927
1159
  requestId,
928
- optionId
1160
+ optionId,
1161
+ ...(chatId !== undefined ? { chatId } : {}),
929
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
+ }
930
1183
  res.json(result);
931
1184
  }
932
1185
  catch (error) {
@@ -1016,33 +1269,6 @@ router.post('/worlds/:worldName/chats', validateWorld, async (req, res) => {
1016
1269
  sendError(res, 500, 'Failed to create new chat', 'NEW_CHAT_ERROR');
1017
1270
  }
1018
1271
  });
1019
- router.post('/worlds/:worldName/chats/:chatId/branch/:messageId', validateWorld, async (req, res) => {
1020
- try {
1021
- const { chatId, messageId } = req.params;
1022
- const worldCtx = req.worldCtx;
1023
- if (!(await chatExists(worldCtx, chatId))) {
1024
- sendError(res, 404, 'Chat not found', 'CHAT_NOT_FOUND');
1025
- return;
1026
- }
1027
- const result = await branchChatFromMessage(worldCtx.id, chatId, messageId);
1028
- res.json({
1029
- success: true,
1030
- world: serializeWorld(result.world),
1031
- chatId: result.newChatId,
1032
- copiedMessageCount: result.copiedMessageCount,
1033
- });
1034
- }
1035
- catch (error) {
1036
- const errorMessage = error instanceof Error ? error.message : String(error);
1037
- const isNotFound = errorMessage.includes('not found');
1038
- const isClientValidationError = errorMessage.includes('required') ||
1039
- errorMessage.includes('Can only branch from assistant messages.') ||
1040
- errorMessage.includes('Message not found in source chat');
1041
- const status = isNotFound ? 404 : (isClientValidationError ? 400 : 500);
1042
- const code = isNotFound ? 'CHAT_BRANCH_NOT_FOUND' : 'CHAT_BRANCH_ERROR';
1043
- sendError(res, status, errorMessage || 'Failed to branch chat', code);
1044
- }
1045
- });
1046
1272
  router.post('/worlds/:worldName/setChat/:chatId', validateWorld, async (req, res) => {
1047
1273
  try {
1048
1274
  const { chatId } = req.params;
@@ -1052,18 +1278,22 @@ router.post('/worlds/:worldName/setChat/:chatId', validateWorld, async (req, res
1052
1278
  sendError(res, 404, 'World not found', 'WORLD_NOT_FOUND');
1053
1279
  return;
1054
1280
  }
1055
- const updatedWorld = await worldCtx.setChat(chatId);
1056
- if (!updatedWorld) {
1281
+ const activated = await activateChatWithSnapshot(worldCtx.id, chatId);
1282
+ if (!activated) {
1057
1283
  res.json({
1058
1284
  world: serializeWorld(currentWorld),
1059
1285
  chatId: currentWorld.currentChatId,
1286
+ hitlPrompts: [],
1060
1287
  success: false
1061
1288
  });
1062
1289
  return;
1063
1290
  }
1291
+ const updatedWorld = activated.world;
1292
+ const pendingHitlPrompts = activated.hitlPrompts;
1064
1293
  res.json({
1065
1294
  world: serializeWorld(updatedWorld),
1066
- chatId: updatedWorld.currentChatId,
1295
+ chatId: activated.chatId,
1296
+ hitlPrompts: pendingHitlPrompts,
1067
1297
  success: true
1068
1298
  });
1069
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
+ }