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
@@ -8,25 +8,41 @@
8
8
  * - Automatic ID normalization to kebab-case for consistency
9
9
  * - Environment-aware storage operations through storage-factory
10
10
  * - Agent message management with automatic agentId assignment
11
- * - Message ID migration for user message edit feature
12
- * - User message editing with removal and resubmission
13
- * - Error logging for message edit operations
11
+ *
12
+ * Queue management, message editing, and storage initialization are handled by
13
+ * dedicated sub-modules (queue-manager.ts, message-edit-manager.ts, storage-init.ts)
14
+ * whose public APIs are re-exported here for backward compatibility.
14
15
  *
15
16
  * API: World (create/get/update/delete/list), Agent (create/get/update/delete/list/updateMemory/clearMemory),
16
- * Chat (newChat/listChats/deleteChat/restoreChat), Migration (migrateMessageIds),
17
- * MessageEdit (removeMessagesFrom/editUserMessage/logEditError/getEditErrors)
17
+ * Chat (newChat/listChats/deleteChat/restoreChat),
18
+ * re-exports: migrateMessageIds, removeMessagesFrom, editUserMessage, logEditError, getEditErrors,
19
+ * addToQueue, enqueueAndProcessUserTurn, dispatchImmediateChatMessage, getQueueMessages,
20
+ * pauseChatQueue, resumeChatQueue, stopChatQueue, clearChatQueue, retryQueueMessage,
21
+ * recoverQueueSendingMessages
18
22
  *
19
23
  * Implementation Details:
20
24
  * - Ensures all agent messages include agentId for proper export functionality
21
25
  * - Compatible with both SQLite and memory storage backends
22
26
  * - Automatic agent identification for message source tracking
23
- * - Idempotent message ID migration supporting both file and SQL storage
24
- * - Comprehensive error tracking for partial failures
25
- * - Error log persistence with 100-entry retention policy
26
27
  *
27
28
  * Recent Changes:
28
- * - 2026-02-20: Added `claimAgentCreationSlot` to allow `create_agent` tool to hold the TOCTOU slot before showing approval dialog, preventing duplicate-approval race conditions.
29
- * - 2026-02-20: Added `createAgent` option `allowWhileWorldProcessing` so approval-gated in-flight tool calls can create agents without disabling default processing guards.
29
+ * - 2026-03-11: Synchronized active runtime world metadata during `updateWorld` so live chats pick up
30
+ * updated `variables`/`working_directory` and other top-level config without requiring a runtime refresh.
31
+ * - 2026-03-10: Renamed queue-backed ingress to `enqueueAndProcessUserTurn` and split non-user immediate dispatch into `dispatchImmediateChatMessage`.
32
+ * - 2026-03-10: Removed restore-time user-last resend from persisted chat memory; queue-owned rows are now the only automatic resume authority.
33
+ * - 2026-03-10: Added `restoreChat(..., { suppressAutoResume: true })` support for edit/delete mutation flows so failed last-turn messages are not replayed before mutation.
34
+ * - 2026-03-09: God-module decomposition — extracted queue management (queue-manager.ts),
35
+ * message edit/migration logic (message-edit-manager.ts), and storage singleton +
36
+ * identifier resolution (storage-init.ts) into focused sub-modules. managers.ts reduced
37
+ * from 2928 → 1300 lines; all public exports preserved via re-exports.
38
+ * - 2026-03-09: Added `activateChatResources` helper to consolidate the resource-reactivation
39
+ * sequence (memory sync, skill approvals, HITL replay, queue resume) shared between the
40
+ * two branches of `restoreChat`.
41
+ * - 2026-03-09: Added SSE terminal-state guard in `triggerPendingLastMessageResume` to prevent
42
+ * infinite auto-resume loops when the last SSE event is already terminal.
43
+ * - 2026-03-06: Moved chat-selection runtime control flow onto explicit/persisted chat helpers.
44
+ * - 2026-03-06: Removed runtime `world.currentChatId` fallback from exported chat-memory helpers.
45
+ * - 2026-02-20: Added `claimAgentCreationSlot` / `allowWhileWorldProcessing` for approval-gated tool calls.
30
46
  * - 2026-02-16: Added `branchChatFromMessage` to create a new chat branched from an assistant message and copy source-chat history up to the target message.
31
47
  * - 2026-02-14: Updated `editUserMessage` to be fully core-managed for clear+resend behavior without client-side subscription refresh logic.
32
48
  * - Edit resubmission now prefers active subscribed world runtimes.
@@ -68,43 +84,123 @@
68
84
  * - logEditError/getEditErrors: Error persistence in edit-errors.json
69
85
  *
70
86
  * Note: Export functionality has been moved to core/export.ts
71
- */ // Core module imports
72
- import { createCategoryLogger, initializeLogger } from './logger.js';
87
+ */
88
+ // Core module imports
89
+ import { createCategoryLogger } from './logger.js';
73
90
  import { EventEmitter } from 'events';
74
- import { createStorageWithWrappers } from './storage/storage-factory.js';
75
91
  import * as utils from './utils.js';
76
- import { nanoid } from 'nanoid';
77
- import * as fs from 'fs';
78
- import * as path from 'path';
79
- import { getWorldDir } from './storage/world-storage.js';
80
- import { getDefaultRootPath } from './storage/storage-factory.js';
81
- import { NEW_CHAT_TITLE, isDefaultChatTitle } from './chat-constants.js';
82
- import { hasActiveChatMessageProcessing, stopMessageProcessing } from './message-processing-control.js';
83
- // Initialize logger and storage
92
+ import { NEW_CHAT_TITLE, isDefaultChatTitle, TITLE_PROVENANCE_MANUAL } from './chat-constants.js';
93
+ import { hasActiveChatMessageProcessing } from './message-processing-control.js';
94
+ import { replayPendingHitlRequests, listPendingHitlPromptEventsFromMessages } from './hitl.js';
95
+ import { clearChatSkillApprovals, reconstructSkillApprovalsFromMessages } from './load-skill-tool.js';
96
+ import { resumePendingToolCallsForChat } from './events/memory-manager.js';
97
+ import { stopWorldRuntimesByWorldId } from './world-registry.js';
98
+ import { storageWrappers, ensureInitialization, getResolvedWorldId, getResolvedAgentId, } from './storage-init.js';
99
+ import { triggerPendingQueueResume, autoPauseQueueForChat, clearQueuePauseForChat, } from './queue-manager.js';
100
+ import { syncRuntimeAgentMemoryFromStorage, migrateMessageIds, } from './message-edit-manager.js';
101
+ // Per-module state
84
102
  const logger = createCategoryLogger('core.managers');
85
- let storageWrappers = null;
86
- let moduleInitialization = null;
87
- async function initializeModules() {
88
- if (storageWrappers) {
89
- return; // Already initialized
103
+ const loggerRestore = createCategoryLogger('chat.restore');
104
+ const loggerRestoreResume = createCategoryLogger('chat.restore.resume');
105
+ const inFlightToolResumeKeys = new Set();
106
+ function triggerPendingToolCallResume(world, chatId, targetAssistantMessageId) {
107
+ if (!chatId) {
108
+ return;
90
109
  }
91
- try {
92
- initializeLogger();
93
- storageWrappers = await createStorageWithWrappers();
110
+ if (hasActiveChatMessageProcessing(world.id, chatId)) {
111
+ return;
94
112
  }
95
- catch (error) {
96
- // Log error but don't throw - allows tests to proceed with mocked storage
97
- logger.error('Failed to initialize storage', { error: error instanceof Error ? error.message : error });
98
- throw error;
113
+ const resumeKey = `${world.id}:${chatId}`;
114
+ if (inFlightToolResumeKeys.has(resumeKey)) {
115
+ return;
116
+ }
117
+ inFlightToolResumeKeys.add(resumeKey);
118
+ void (async () => {
119
+ try {
120
+ const resumedCount = await resumePendingToolCallsForChat(world, chatId, targetAssistantMessageId);
121
+ loggerRestoreResume.debug('Attempted pending tool-call resume after chat restore', {
122
+ worldId: world.id,
123
+ chatId,
124
+ targetAssistantMessageId: targetAssistantMessageId || null,
125
+ resumedCount,
126
+ });
127
+ if (resumedCount > 0) {
128
+ loggerRestoreResume.debug('Resumed pending persisted tool calls after chat restore', {
129
+ worldId: world.id,
130
+ chatId,
131
+ targetAssistantMessageId,
132
+ resumedCount,
133
+ });
134
+ }
135
+ }
136
+ catch (error) {
137
+ loggerRestoreResume.warn('Failed to resume pending persisted tool calls after chat restore', {
138
+ worldId: world.id,
139
+ chatId,
140
+ targetAssistantMessageId,
141
+ error: error instanceof Error ? error.message : String(error),
142
+ });
143
+ }
144
+ finally {
145
+ inFlightToolResumeKeys.delete(resumeKey);
146
+ }
147
+ })();
148
+ }
149
+ function hasPendingToolCallsOnLastAssistantMessage(lastMessage, chatMemory) {
150
+ if (lastMessage.role !== 'assistant' || !Array.isArray(lastMessage.tool_calls) || lastMessage.tool_calls.length === 0) {
151
+ return false;
152
+ }
153
+ const completedToolCallIds = new Set();
154
+ for (const message of chatMemory) {
155
+ if (message.role !== 'tool' || typeof message.tool_call_id !== 'string') {
156
+ continue;
157
+ }
158
+ const toolCallId = message.tool_call_id.trim();
159
+ if (toolCallId) {
160
+ completedToolCallIds.add(toolCallId);
161
+ }
162
+ }
163
+ for (const toolCall of lastMessage.tool_calls) {
164
+ const toolCallId = String(toolCall?.id || '').trim();
165
+ if (!toolCallId) {
166
+ continue;
167
+ }
168
+ if (!completedToolCallIds.has(toolCallId)) {
169
+ return true;
170
+ }
99
171
  }
172
+ return false;
100
173
  }
101
- function ensureInitialization() {
102
- if (!moduleInitialization) {
103
- moduleInitialization = initializeModules();
174
+ function triggerPendingToolCallResumeFromLastMessage(world, chatId) {
175
+ if (!chatId) {
176
+ return;
104
177
  }
105
- return moduleInitialization;
178
+ void (async () => {
179
+ try {
180
+ const chatMemory = await storageWrappers.getMemory(world.id, chatId);
181
+ if (!chatMemory || chatMemory.length === 0) {
182
+ return;
183
+ }
184
+ const lastMessage = chatMemory[chatMemory.length - 1];
185
+ if (!hasPendingToolCallsOnLastAssistantMessage(lastMessage, chatMemory)) {
186
+ return;
187
+ }
188
+ loggerRestoreResume.debug('Detected pending tool-call-last message during chat-restore inspection', {
189
+ worldId: world.id,
190
+ chatId,
191
+ messageId: lastMessage.messageId || null,
192
+ });
193
+ triggerPendingToolCallResume(world, chatId, lastMessage.messageId);
194
+ }
195
+ catch (error) {
196
+ loggerRestoreResume.warn('Failed to inspect last message for pending tool-call resume during chat restore', {
197
+ worldId: world.id,
198
+ chatId,
199
+ error: error instanceof Error ? error.message : String(error),
200
+ });
201
+ }
202
+ })();
106
203
  }
107
- const NEW_CHAT_CONFIG = { REUSABLE_CHAT_TITLE: NEW_CHAT_TITLE };
108
204
  // TOCTOU guard: prevents two concurrent createAgent calls from both passing the
109
205
  // `agentExists` check before either write lands. Maps worldId → Set<agentId>.
110
206
  const pendingAgentCreates = new Map();
@@ -142,137 +238,6 @@ export async function claimAgentCreationSlot(worldId, agentName) {
142
238
  },
143
239
  };
144
240
  }
145
- function extractGeneratedChatTitleFromSystemPayload(payload) {
146
- if (!payload || typeof payload !== 'object')
147
- return null;
148
- if (payload.eventType !== 'chat-title-updated')
149
- return null;
150
- const title = typeof payload.title === 'string' ? payload.title.trim() : '';
151
- return title || null;
152
- }
153
- async function resetAutoGeneratedChatTitleForEditResubmission(world, chatId) {
154
- const chat = world.chats.get(chatId) ?? await storageWrappers.loadChatData(world.id, chatId);
155
- if (!chat)
156
- return;
157
- const currentTitle = String(chat.name || '').trim();
158
- if (!currentTitle || isDefaultChatTitle(currentTitle)) {
159
- return;
160
- }
161
- const eventStorage = world.eventStorage;
162
- if (!eventStorage) {
163
- return;
164
- }
165
- let latestGeneratedTitle = null;
166
- try {
167
- const systemEvents = await eventStorage.getEventsByWorldAndChat(world.id, chatId, {
168
- types: ['system'],
169
- order: 'desc',
170
- limit: 25
171
- });
172
- for (const event of systemEvents) {
173
- const generatedTitle = extractGeneratedChatTitleFromSystemPayload(event?.payload);
174
- if (generatedTitle) {
175
- latestGeneratedTitle = generatedTitle;
176
- break;
177
- }
178
- }
179
- }
180
- catch (error) {
181
- logger.debug('Skipping auto-title reset because system events could not be queried', {
182
- worldId: world.id,
183
- chatId,
184
- error: error instanceof Error ? error.message : String(error)
185
- });
186
- return;
187
- }
188
- if (!latestGeneratedTitle || latestGeneratedTitle !== currentTitle) {
189
- return;
190
- }
191
- let resetSucceeded = false;
192
- if (typeof storageWrappers.updateChatNameIfCurrent === 'function') {
193
- resetSucceeded = await storageWrappers.updateChatNameIfCurrent(world.id, chatId, currentTitle, NEW_CHAT_TITLE);
194
- }
195
- else {
196
- const updated = await storageWrappers.updateChatData(world.id, chatId, { name: NEW_CHAT_TITLE });
197
- resetSucceeded = !!updated;
198
- }
199
- if (!resetSucceeded) {
200
- return;
201
- }
202
- const runtimeChat = world.chats.get(chatId);
203
- if (runtimeChat) {
204
- runtimeChat.name = NEW_CHAT_TITLE;
205
- }
206
- }
207
- async function syncRuntimeAgentMemoryFromStorage(world, worldId) {
208
- if (!world?.agents || world.agents.size === 0)
209
- return;
210
- for (const runtimeAgent of world.agents.values()) {
211
- const persistedAgent = await storageWrappers.loadAgent(worldId, runtimeAgent.id);
212
- runtimeAgent.memory = Array.isArray(persistedAgent?.memory)
213
- ? [...persistedAgent.memory]
214
- : [];
215
- }
216
- }
217
- /**
218
- * Resolve a world identifier to the persisted world ID.
219
- * Accepts either world ID or world name and supports historical rename drift.
220
- */
221
- async function resolveWorldIdentifier(worldIdOrName) {
222
- const normalizedInput = utils.toKebabCase(worldIdOrName);
223
- if (!normalizedInput)
224
- return null;
225
- // Fast path: direct normalized ID lookup
226
- const directWorld = await storageWrappers.loadWorld(normalizedInput);
227
- if (directWorld?.id) {
228
- return directWorld.id;
229
- }
230
- // Fallback: scan worlds and match by normalized ID or normalized name
231
- const worlds = await storageWrappers.listWorlds();
232
- const matched = worlds.find((world) => {
233
- const storedId = String(world.id || '');
234
- const storedName = String(world.name || '');
235
- return (storedId === worldIdOrName ||
236
- storedName === worldIdOrName ||
237
- utils.toKebabCase(storedId) === normalizedInput ||
238
- utils.toKebabCase(storedName) === normalizedInput);
239
- });
240
- return matched?.id || null;
241
- }
242
- async function getResolvedWorldId(worldIdOrName) {
243
- const resolved = await resolveWorldIdentifier(worldIdOrName);
244
- return resolved || utils.toKebabCase(worldIdOrName);
245
- }
246
- /**
247
- * Resolve an agent identifier to the persisted agent ID within a world.
248
- * Accepts either agent ID or agent name and supports historical rename drift.
249
- */
250
- async function resolveAgentIdentifier(worldIdOrName, agentIdOrName) {
251
- const resolvedWorldId = await getResolvedWorldId(worldIdOrName);
252
- const normalizedInput = utils.toKebabCase(agentIdOrName);
253
- if (!normalizedInput)
254
- return null;
255
- // Fast path: direct normalized ID lookup
256
- const directAgent = await storageWrappers.loadAgent(resolvedWorldId, normalizedInput);
257
- if (directAgent?.id) {
258
- return directAgent.id;
259
- }
260
- // Fallback: scan agents and match by normalized ID or normalized name
261
- const agents = await storageWrappers.listAgents(resolvedWorldId);
262
- const matched = agents.find((agent) => {
263
- const storedId = String(agent.id || '');
264
- const storedName = String(agent.name || '');
265
- return (storedId === agentIdOrName ||
266
- storedName === agentIdOrName ||
267
- utils.toKebabCase(storedId) === normalizedInput ||
268
- utils.toKebabCase(storedName) === normalizedInput);
269
- });
270
- return matched?.id || null;
271
- }
272
- async function getResolvedAgentId(worldIdOrName, agentIdOrName) {
273
- const resolved = await resolveAgentIdentifier(worldIdOrName, agentIdOrName);
274
- return resolved || utils.toKebabCase(agentIdOrName);
275
- }
276
241
  /**
277
242
  * Create new world with configuration and automatically create a new chat
278
243
  */
@@ -293,6 +258,9 @@ export async function createWorld(params) {
293
258
  chatLLMModel: params.chatLLMModel,
294
259
  mcpConfig: params.mcpConfig,
295
260
  variables: params.variables,
261
+ heartbeatEnabled: params.heartbeatEnabled === true,
262
+ heartbeatInterval: params.heartbeatInterval ? String(params.heartbeatInterval).trim() : null,
263
+ heartbeatPrompt: params.heartbeatPrompt ? String(params.heartbeatPrompt) : null,
296
264
  createdAt: new Date(),
297
265
  lastUpdated: new Date(),
298
266
  totalAgents: 0,
@@ -337,8 +305,59 @@ export async function updateWorld(worldId, updates) {
337
305
  lastUpdated: new Date()
338
306
  };
339
307
  await storageWrappers.saveWorld(updatedData);
308
+ const { getActiveSubscribedWorlds } = await import('./subscription.js');
309
+ const activeRuntimeWorlds = getActiveSubscribedWorlds(resolvedWorldId);
310
+ if (activeRuntimeWorlds.length > 0) {
311
+ const shouldSyncCurrentChatId = normalizedUpdates.currentChatId !== undefined;
312
+ for (const activeRuntimeWorld of activeRuntimeWorlds) {
313
+ syncActiveRuntimeWorldMetadata(activeRuntimeWorld, updatedData);
314
+ if (shouldSyncCurrentChatId) {
315
+ activeRuntimeWorld.currentChatId = updatedData.currentChatId ?? null;
316
+ }
317
+ }
318
+ return activeRuntimeWorlds[0] ?? null;
319
+ }
340
320
  return getWorld(resolvedWorldId);
341
321
  }
322
+ function syncActiveRuntimeWorldMetadata(targetWorld, persistedWorld) {
323
+ targetWorld.name = persistedWorld.name;
324
+ targetWorld.description = persistedWorld.description;
325
+ targetWorld.turnLimit = persistedWorld.turnLimit;
326
+ targetWorld.mainAgent = persistedWorld.mainAgent ?? null;
327
+ targetWorld.chatLLMProvider = persistedWorld.chatLLMProvider;
328
+ targetWorld.chatLLMModel = persistedWorld.chatLLMModel;
329
+ targetWorld.mcpConfig = persistedWorld.mcpConfig ?? null;
330
+ targetWorld.variables = persistedWorld.variables;
331
+ targetWorld.uiMode = persistedWorld.uiMode;
332
+ targetWorld.dashboardZones = persistedWorld.dashboardZones;
333
+ targetWorld.heartbeatEnabled = persistedWorld.heartbeatEnabled === true;
334
+ targetWorld.heartbeatInterval = persistedWorld.heartbeatInterval ?? null;
335
+ targetWorld.heartbeatPrompt = persistedWorld.heartbeatPrompt ?? null;
336
+ targetWorld.lastUpdated = persistedWorld.lastUpdated;
337
+ }
338
+ async function getPersistedCurrentChatId(worldId) {
339
+ const resolvedWorldId = await getResolvedWorldId(worldId);
340
+ const worldData = await storageWrappers.loadWorld(resolvedWorldId);
341
+ const chatId = String(worldData?.currentChatId || '').trim();
342
+ return chatId || null;
343
+ }
344
+ async function setPersistedCurrentChatId(worldId, chatId) {
345
+ const resolvedWorldId = await getResolvedWorldId(worldId);
346
+ const worldData = await storageWrappers.loadWorld(resolvedWorldId);
347
+ if (!worldData) {
348
+ return;
349
+ }
350
+ const normalizedChatId = String(chatId || '').trim() || null;
351
+ const existingChatId = String(worldData.currentChatId || '').trim() || null;
352
+ if (existingChatId === normalizedChatId) {
353
+ return;
354
+ }
355
+ await storageWrappers.saveWorld({
356
+ ...worldData,
357
+ currentChatId: normalizedChatId,
358
+ lastUpdated: new Date(),
359
+ });
360
+ }
342
361
  /**
343
362
  * Set the raw .env-style variables text for a world
344
363
  */
@@ -391,7 +410,11 @@ export async function deleteWorld(worldId) {
391
410
  if (worldData?._activityListenerCleanup) {
392
411
  worldData._activityListenerCleanup();
393
412
  }
394
- return await storageWrappers.deleteWorld(resolvedWorldId);
413
+ const result = await storageWrappers.deleteWorld(resolvedWorldId);
414
+ // Stop any active runtime so subsequent subscriptions get a fresh world
415
+ // instead of reusing state from the deleted incarnation.
416
+ await stopWorldRuntimesByWorldId(resolvedWorldId);
417
+ return result;
395
418
  }
396
419
  /**
397
420
  * Get all world IDs and basic information
@@ -455,6 +478,8 @@ export async function getWorld(worldId) {
455
478
  world._eventPersistenceCleanup = setupEventPersistence(world);
456
479
  world._activityListenerCleanup = setupWorldActivityListener(world);
457
480
  }
481
+ // Initialize queued chat set; per-chat seeding deferred to explicit queue operations
482
+ world._queuedChatIds = new Set();
458
483
  return world;
459
484
  }
460
485
  /**
@@ -705,7 +730,7 @@ async function createChat(worldId, params) {
705
730
  const chatData = {
706
731
  id: chatId,
707
732
  worldId,
708
- name: NEW_CHAT_CONFIG.REUSABLE_CHAT_TITLE,
733
+ name: NEW_CHAT_TITLE,
709
734
  description: params.description,
710
735
  createdAt: now,
711
736
  updatedAt: now,
@@ -730,7 +755,8 @@ export async function newChat(worldId) {
730
755
  if (existingChat) {
731
756
  const messages = await storageWrappers.getMemory(resolvedWorldId, existingChat.id);
732
757
  if (messages.length === 0) {
733
- return await updateWorld(resolvedWorldId, { currentChatId: existingChat.id });
758
+ await setPersistedCurrentChatId(resolvedWorldId, existingChat.id);
759
+ return await getWorld(resolvedWorldId);
734
760
  }
735
761
  // If chat has messages, fall through to create a new one
736
762
  }
@@ -738,7 +764,8 @@ export async function newChat(worldId) {
738
764
  name: NEW_CHAT_TITLE,
739
765
  captureChat: false
740
766
  });
741
- return await updateWorld(resolvedWorldId, { currentChatId: chatData.id });
767
+ await setPersistedCurrentChatId(resolvedWorldId, chatData.id);
768
+ return await getWorld(resolvedWorldId);
742
769
  }
743
770
  /**
744
771
  * Create a branched chat from a source chat up to (and including) the provided message.
@@ -795,13 +822,10 @@ export async function branchChatFromMessage(worldId, sourceChatId, messageId) {
795
822
  };
796
823
  const cutoffTimestamp = toEpochMillis(targetMessage?.createdAt);
797
824
  const updatedWorld = await newChat(resolvedWorldId);
798
- if (!updatedWorld || !updatedWorld.currentChatId) {
825
+ const newChatId = await getPersistedCurrentChatId(resolvedWorldId);
826
+ if (!updatedWorld || !newChatId) {
799
827
  throw new Error('Failed to create branch chat.');
800
828
  }
801
- const newChatId = String(updatedWorld.currentChatId || '').trim();
802
- if (!newChatId) {
803
- throw new Error('Failed to resolve new chat ID for branch.');
804
- }
805
829
  let copiedMessageCount = 0;
806
830
  const agents = await listAgents(resolvedWorldId);
807
831
  try {
@@ -868,6 +892,11 @@ export async function listChats(worldId) {
868
892
  export async function updateChat(worldId, chatId, updates) {
869
893
  await ensureInitialization();
870
894
  const resolvedWorldId = await getResolvedWorldId(worldId);
895
+ // When the caller explicitly sets a name, treat it as a manual title so the
896
+ // auto-title scheduler will not overwrite it.
897
+ if (typeof updates.name === 'string' && updates.name.trim().length > 0) {
898
+ updates = { ...updates, titleProvenance: TITLE_PROVENANCE_MANUAL };
899
+ }
871
900
  const chat = await storageWrappers.updateChatData(resolvedWorldId, chatId, updates);
872
901
  if (!chat) {
873
902
  return null;
@@ -888,12 +917,10 @@ export async function deleteChat(worldId, chatId) {
888
917
  // First, delete all agent memory items associated with this chat
889
918
  const deletedMemoryCount = await storageWrappers.deleteMemoryByChatId(resolvedWorldId, chatId);
890
919
  logger.debug('Deleted memory items for chat', { worldId, resolvedWorldId, chatId, deletedMemoryCount });
891
- // Get the world to check if this was the current chat
920
+ const persistedCurrentChatId = await getPersistedCurrentChatId(resolvedWorldId);
921
+ // Get the world to update in-memory chat cache only
892
922
  const world = await getWorld(resolvedWorldId);
893
- let shouldSetNewCurrentChat = false;
894
- if (world && world.currentChatId === chatId) {
895
- shouldSetNewCurrentChat = true;
896
- }
923
+ const shouldSetNewCurrentChat = persistedCurrentChatId === chatId;
897
924
  // Then delete the chat itself
898
925
  const chatDeleted = await storageWrappers.deleteChatData(resolvedWorldId, chatId);
899
926
  // Remove from world's in-memory chat map
@@ -906,7 +933,7 @@ export async function deleteChat(worldId, chatId) {
906
933
  if (remainingChats.length > 0) {
907
934
  // Set the most recently updated chat as current
908
935
  const latestChat = remainingChats.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())[0];
909
- await updateWorld(resolvedWorldId, { currentChatId: latestChat.id });
936
+ await setPersistedCurrentChatId(resolvedWorldId, latestChat.id);
910
937
  }
911
938
  else {
912
939
  // No chats left, create a new one
@@ -916,14 +943,64 @@ export async function deleteChat(worldId, chatId) {
916
943
  }
917
944
  return chatDeleted;
918
945
  }
919
- export async function restoreChat(worldId, chatId) {
946
+ async function activateChatResources(world, resolvedWorldId, chatId, options) {
947
+ await syncRuntimeAgentMemoryFromStorage(world, resolvedWorldId);
948
+ loggerRestore.debug('Restore chat memory sync complete', { worldId: world.id, chatId });
949
+ const memoryForApprovals = await storageWrappers.getMemory(resolvedWorldId, chatId);
950
+ clearChatSkillApprovals(resolvedWorldId, chatId);
951
+ reconstructSkillApprovalsFromMessages(resolvedWorldId, chatId, Array.isArray(memoryForApprovals) ? memoryForApprovals : []);
952
+ const { getActiveSubscribedWorld } = await import('./subscription.js');
953
+ const runtimeWorld = getActiveSubscribedWorld(resolvedWorldId, chatId) || world;
954
+ const replayedHitlPromptCount = replayPendingHitlRequests(runtimeWorld, chatId);
955
+ loggerRestore.debug('Restore chat pending HITL replay triggered', { worldId: world.id, chatId });
956
+ if (options?.suppressAutoResume === true) {
957
+ loggerRestore.debug('Restore chat auto-resume suppressed for mutation flow', {
958
+ worldId: world.id,
959
+ chatId,
960
+ });
961
+ return;
962
+ }
963
+ if (replayedHitlPromptCount > 0) {
964
+ loggerRestore.debug('Restore chat auto-resume skipped because chat is awaiting HITL input', {
965
+ worldId: world.id,
966
+ chatId,
967
+ replayedHitlPromptCount,
968
+ });
969
+ return;
970
+ }
971
+ triggerPendingToolCallResumeFromLastMessage(runtimeWorld, chatId);
972
+ clearQueuePauseForChat(runtimeWorld.id, chatId);
973
+ triggerPendingQueueResume(runtimeWorld, chatId, { recoverStaleSending: true });
974
+ }
975
+ export async function restoreChat(worldId, chatId, options) {
920
976
  await ensureInitialization();
921
977
  const resolvedWorldId = await getResolvedWorldId(worldId);
978
+ const restoreStartedAt = Date.now();
979
+ loggerRestore.debug('Restore chat started', {
980
+ worldId: resolvedWorldId,
981
+ requestedChatId: chatId,
982
+ });
922
983
  let world = await getWorld(resolvedWorldId);
984
+ const persistedCurrentChatId = await getPersistedCurrentChatId(resolvedWorldId);
923
985
  if (!world) {
986
+ loggerRestore.debug('Restore chat aborted: world missing', {
987
+ worldId: resolvedWorldId,
988
+ requestedChatId: chatId,
989
+ });
924
990
  return null;
925
991
  }
926
- if (world.currentChatId === chatId) {
992
+ if (persistedCurrentChatId === chatId) {
993
+ loggerRestore.debug('Restore chat detected already-current chat', {
994
+ worldId: world.id,
995
+ chatId,
996
+ action: 'sync-memory+resume',
997
+ });
998
+ await activateChatResources(world, resolvedWorldId, chatId, options);
999
+ loggerRestore.debug('Restore chat resume inspection triggered', {
1000
+ worldId: world.id,
1001
+ chatId,
1002
+ elapsedMs: Date.now() - restoreStartedAt,
1003
+ });
927
1004
  return world;
928
1005
  }
929
1006
  const runtimeChatExists = world.chats.has(chatId);
@@ -931,13 +1008,81 @@ export async function restoreChat(worldId, chatId) {
931
1008
  ? true
932
1009
  : !!(await storageWrappers.loadChatData(resolvedWorldId, chatId));
933
1010
  if (!persistedChatExists) {
1011
+ loggerRestore.debug('Restore chat aborted: chat missing', {
1012
+ worldId: resolvedWorldId,
1013
+ requestedChatId: chatId,
1014
+ runtimeChatExists,
1015
+ });
934
1016
  return null;
935
1017
  }
936
- world = await updateWorld(resolvedWorldId, {
937
- currentChatId: chatId
1018
+ // Auto-pause the old chat's queue when switching away (FR-8 safety)
1019
+ const previousChatId = persistedCurrentChatId;
1020
+ if (previousChatId && previousChatId !== chatId) {
1021
+ autoPauseQueueForChat(resolvedWorldId, previousChatId);
1022
+ loggerRestore.debug('Auto-paused queue for previous chat on switch', {
1023
+ worldId: resolvedWorldId,
1024
+ previousChatId,
1025
+ newChatId: chatId,
1026
+ });
1027
+ }
1028
+ loggerRestore.debug('Restore chat switching current chat', {
1029
+ worldId: resolvedWorldId,
1030
+ fromChatId: previousChatId,
1031
+ toChatId: chatId,
1032
+ });
1033
+ await setPersistedCurrentChatId(resolvedWorldId, chatId);
1034
+ world = await getWorld(resolvedWorldId);
1035
+ if (world) {
1036
+ await activateChatResources(world, resolvedWorldId, chatId, options);
1037
+ loggerRestore.debug('Restore chat resume inspection triggered', {
1038
+ worldId: world.id,
1039
+ chatId,
1040
+ elapsedMs: Date.now() - restoreStartedAt,
1041
+ });
1042
+ }
1043
+ else {
1044
+ loggerRestore.warn('Restore chat update-world returned null', {
1045
+ worldId: resolvedWorldId,
1046
+ requestedChatId: chatId,
1047
+ elapsedMs: Date.now() - restoreStartedAt,
1048
+ });
1049
+ }
1050
+ loggerRestore.debug('Restore chat completed', {
1051
+ worldId: resolvedWorldId,
1052
+ requestedChatId: chatId,
1053
+ restoredCurrentChatId: await getPersistedCurrentChatId(resolvedWorldId),
1054
+ elapsedMs: Date.now() - restoreStartedAt,
938
1055
  });
939
1056
  return world;
940
1057
  }
1058
+ export async function activateChatWithSnapshot(worldId, chatId) {
1059
+ const world = await restoreChat(worldId, chatId);
1060
+ if (!world) {
1061
+ return null;
1062
+ }
1063
+ const resolvedChatId = String(chatId || '').trim();
1064
+ if (!resolvedChatId) {
1065
+ return null;
1066
+ }
1067
+ const memory = await storageWrappers.getMemory(world.id, resolvedChatId);
1068
+ const safeMemory = Array.isArray(memory) ? memory : [];
1069
+ // Message-authoritative: pending HITL state is derived from persisted messages only.
1070
+ // Runtime pending map serves transport/notification purposes; the snapshot always uses
1071
+ // messages as the single source of truth (AD-1, AD-4).
1072
+ const hitlPrompts = listPendingHitlPromptEventsFromMessages(safeMemory, resolvedChatId);
1073
+ loggerRestore.debug('Activate chat snapshot assembled', {
1074
+ worldId: world.id,
1075
+ chatId: resolvedChatId,
1076
+ memoryCount: safeMemory.length,
1077
+ hitlPromptCount: hitlPrompts.length,
1078
+ });
1079
+ return {
1080
+ world,
1081
+ chatId: resolvedChatId,
1082
+ memory: safeMemory,
1083
+ hitlPrompts,
1084
+ };
1085
+ }
941
1086
  export async function getMemory(worldId, chatId) {
942
1087
  await ensureInitialization();
943
1088
  const resolvedWorldId = await getResolvedWorldId(worldId);
@@ -945,7 +1090,10 @@ export async function getMemory(worldId, chatId) {
945
1090
  if (!world) {
946
1091
  return null;
947
1092
  }
948
- const resolvedChatId = chatId || world.currentChatId;
1093
+ const resolvedChatId = String(chatId || '').trim();
1094
+ if (!resolvedChatId) {
1095
+ throw new Error('getMemory: chatId is required.');
1096
+ }
949
1097
  const memory = await storageWrappers.getMemory(resolvedWorldId, resolvedChatId);
950
1098
  // Auto-repair legacy memories so downstream clients can rely on messageId without UI fallbacks.
951
1099
  if (memory.some(message => !message.messageId)) {
@@ -958,316 +1106,6 @@ export async function getMemory(worldId, chatId) {
958
1106
  }
959
1107
  return memory;
960
1108
  }
961
- /**
962
- * Migrate messages to include messageId for user message edit feature
963
- * Automatically detects storage type and handles both file and SQL storage
964
- * Idempotent - safe to run multiple times
965
- *
966
- * @param worldId - World ID to migrate messages for
967
- * @returns Number of messages migrated
968
- */
969
- export async function migrateMessageIds(worldId) {
970
- await ensureInitialization();
971
- const resolvedWorldId = await getResolvedWorldId(worldId);
972
- let totalMigrated = 0;
973
- const world = await getWorld(resolvedWorldId);
974
- if (!world) {
975
- throw new Error(`World '${worldId}' not found`);
976
- }
977
- // Get all agents in the world
978
- const agents = await listAgents(resolvedWorldId);
979
- // Get all chats for the world
980
- const chats = await storageWrappers.listChats(resolvedWorldId);
981
- // Migrate messages for each chat
982
- for (const chat of chats) {
983
- const chatId = chat.id;
984
- // Get all memory for this chat
985
- const memory = await storageWrappers.getMemory(resolvedWorldId, chatId);
986
- if (!memory || memory.length === 0) {
987
- continue;
988
- }
989
- // Check which messages need messageId
990
- let needsMigration = false;
991
- const updatedMemory = [];
992
- for (const message of memory) {
993
- if (!message.messageId) {
994
- needsMigration = true;
995
- updatedMemory.push({
996
- ...message,
997
- messageId: nanoid(10)
998
- });
999
- totalMigrated++;
1000
- }
1001
- else {
1002
- updatedMemory.push(message);
1003
- }
1004
- }
1005
- // If any messages were updated, save the entire memory back
1006
- if (needsMigration) {
1007
- // For each agent, update their memory with the migrated messages
1008
- for (const agent of agents) {
1009
- const agentMessages = updatedMemory.filter(m => m.agentId === agent.id);
1010
- if (agentMessages.length > 0) {
1011
- await storageWrappers.saveAgentMemory(resolvedWorldId, agent.id, agentMessages);
1012
- }
1013
- }
1014
- }
1015
- }
1016
- logger.info(`Migrated ${totalMigrated} messages with messageId for world '${resolvedWorldId}'`);
1017
- return totalMigrated;
1018
- }
1019
- /**
1020
- * Remove a message and all subsequent messages from all agents in a world
1021
- * Used for user message editing feature
1022
- *
1023
- * @param worldId - World ID
1024
- * @param messageId - ID of the message to remove (and all after it)
1025
- * @param chatId - Chat ID to filter messages
1026
- * @returns RemovalResult with per-agent removal details
1027
- */
1028
- export async function removeMessagesFrom(worldId, messageId, chatId) {
1029
- await ensureInitialization();
1030
- const resolvedWorldId = await getResolvedWorldId(worldId);
1031
- const world = await getWorld(resolvedWorldId);
1032
- if (!world) {
1033
- throw new Error(`World '${worldId}' not found`);
1034
- }
1035
- // Get all agents
1036
- const agents = await listAgents(resolvedWorldId);
1037
- // Track results per agent
1038
- const processedAgents = [];
1039
- const failedAgents = [];
1040
- let messagesRemovedTotal = 0;
1041
- let foundTargetInAnyAgent = false;
1042
- let targetTimestampValue = null;
1043
- const loadedAgentsById = new Map();
1044
- const toTimestamp = (value) => {
1045
- if (value instanceof Date) {
1046
- return value.getTime();
1047
- }
1048
- if (value) {
1049
- const parsed = new Date(value).getTime();
1050
- if (Number.isFinite(parsed)) {
1051
- return parsed;
1052
- }
1053
- }
1054
- return Date.now();
1055
- };
1056
- // First pass: load each agent and discover the deletion cutoff timestamp
1057
- for (const agent of agents) {
1058
- try {
1059
- const fullAgent = await storageWrappers.loadAgent(resolvedWorldId, agent.id);
1060
- if (!fullAgent || !fullAgent.memory || fullAgent.memory.length === 0) {
1061
- continue;
1062
- }
1063
- loadedAgentsById.set(agent.id, fullAgent);
1064
- // Find the target message in this chat for global cutoff derivation
1065
- const targetMsg = fullAgent.memory.find(m => m.messageId === messageId && m.chatId === chatId);
1066
- if (targetMsg) {
1067
- foundTargetInAnyAgent = true;
1068
- const candidateTimestamp = toTimestamp(targetMsg.createdAt);
1069
- if (targetTimestampValue === null || candidateTimestamp < targetTimestampValue) {
1070
- targetTimestampValue = candidateTimestamp;
1071
- }
1072
- }
1073
- }
1074
- catch (error) {
1075
- const errorMsg = error instanceof Error ? error.message : String(error);
1076
- failedAgents.push({
1077
- agentId: agent.id,
1078
- error: errorMsg
1079
- });
1080
- }
1081
- }
1082
- if (!foundTargetInAnyAgent || targetTimestampValue === null) {
1083
- const notFoundFailures = agents.length > 0
1084
- ? [
1085
- ...failedAgents,
1086
- { agentId: 'all', error: `Message with ID '${messageId}' not found in chat '${chatId}'` }
1087
- ]
1088
- : failedAgents;
1089
- return {
1090
- success: false,
1091
- messageId,
1092
- totalAgents: agents.length,
1093
- processedAgents,
1094
- failedAgents: notFoundFailures,
1095
- messagesRemovedTotal,
1096
- requiresRetry: false,
1097
- resubmissionStatus: 'skipped',
1098
- newMessageId: undefined
1099
- };
1100
- }
1101
- // Second pass: apply cutoff to all agents in the target chat
1102
- for (const agent of agents) {
1103
- if (failedAgents.some(entry => entry.agentId === agent.id)) {
1104
- continue;
1105
- }
1106
- const fullAgent = loadedAgentsById.get(agent.id);
1107
- if (!fullAgent || !Array.isArray(fullAgent.memory) || fullAgent.memory.length === 0) {
1108
- processedAgents.push(agent.id);
1109
- continue;
1110
- }
1111
- try {
1112
- const messagesToKeep = fullAgent.memory.filter(m => {
1113
- if (m.chatId !== chatId) {
1114
- return true;
1115
- }
1116
- const msgTimestamp = toTimestamp(m.createdAt);
1117
- return msgTimestamp < targetTimestampValue;
1118
- });
1119
- const removedCount = fullAgent.memory.length - messagesToKeep.length;
1120
- if (removedCount === 0) {
1121
- processedAgents.push(agent.id);
1122
- continue;
1123
- }
1124
- await storageWrappers.saveAgentMemory(resolvedWorldId, agent.id, messagesToKeep);
1125
- messagesRemovedTotal += removedCount;
1126
- processedAgents.push(agent.id);
1127
- }
1128
- catch (error) {
1129
- const errorMsg = error instanceof Error ? error.message : String(error);
1130
- failedAgents.push({
1131
- agentId: agent.id,
1132
- error: errorMsg
1133
- });
1134
- }
1135
- }
1136
- logger.info('Message removal completed', {
1137
- messageId,
1138
- success: failedAgents.length === 0,
1139
- totalAgents: agents.length,
1140
- processedAgents: processedAgents.length,
1141
- failedAgents: failedAgents.length,
1142
- messagesRemovedTotal
1143
- });
1144
- return {
1145
- success: failedAgents.length === 0,
1146
- messageId,
1147
- totalAgents: agents.length,
1148
- processedAgents,
1149
- failedAgents,
1150
- messagesRemovedTotal,
1151
- requiresRetry: failedAgents.length > 0,
1152
- resubmissionStatus: 'skipped', // Will be updated by editUserMessage
1153
- newMessageId: undefined
1154
- };
1155
- }
1156
- /**
1157
- * Edit a user message by removing it and all subsequent messages, then resubmitting with new content
1158
- * Combines removal and resubmission in a single operation with comprehensive error tracking
1159
- *
1160
- * @param worldId - World ID
1161
- * @param messageId - ID of the message to edit
1162
- * @param newContent - New message content
1163
- * @param chatId - Chat ID for the message
1164
- * @returns RemovalResult with removal and resubmission details
1165
- */
1166
- export async function editUserMessage(worldId, messageId, newContent, chatId, targetWorld) {
1167
- await ensureInitialization();
1168
- const resolvedWorldId = await getResolvedWorldId(worldId);
1169
- const { getActiveSubscribedWorld } = await import('./subscription.js');
1170
- const activeSubscribedWorld = targetWorld || getActiveSubscribedWorld(resolvedWorldId);
1171
- const world = activeSubscribedWorld || await getWorld(resolvedWorldId);
1172
- if (!world) {
1173
- throw new Error(`World '${worldId}' not found`);
1174
- }
1175
- if (hasActiveChatMessageProcessing(resolvedWorldId, chatId)) {
1176
- stopMessageProcessing(resolvedWorldId, chatId);
1177
- }
1178
- // Step 1: Remove the message and all subsequent messages
1179
- const removalResult = await removeMessagesFrom(resolvedWorldId, messageId, chatId);
1180
- if (!removalResult.success) {
1181
- return removalResult;
1182
- }
1183
- await syncRuntimeAgentMemoryFromStorage(activeSubscribedWorld || world, resolvedWorldId);
1184
- // Step 2: Reset auto-generated chat title so post-resubmission title generation can run again.
1185
- await resetAutoGeneratedChatTitleForEditResubmission(world, chatId);
1186
- const worldForResubmission = activeSubscribedWorld || world;
1187
- if (!activeSubscribedWorld) {
1188
- const { subscribeAgentToMessages, subscribeWorldToMessages } = await import('./events/index.js');
1189
- for (const agent of worldForResubmission.agents.values()) {
1190
- subscribeAgentToMessages(worldForResubmission, agent);
1191
- }
1192
- subscribeWorldToMessages(worldForResubmission);
1193
- }
1194
- // Step 3: Attempt resubmission using publishMessage directly
1195
- try {
1196
- const { publishMessage } = await import('./events/index.js');
1197
- const messageEvent = publishMessage(worldForResubmission, newContent, 'human', chatId);
1198
- logger.info(`Resubmitted edited message to world '${resolvedWorldId}' with new messageId '${messageEvent.messageId}'`);
1199
- return {
1200
- ...removalResult,
1201
- resubmissionStatus: 'success',
1202
- newMessageId: messageEvent.messageId
1203
- };
1204
- }
1205
- catch (error) {
1206
- const errorMsg = error instanceof Error ? error.message : String(error);
1207
- logger.error(`Failed to resubmit message to world '${resolvedWorldId}': ${errorMsg}`);
1208
- return {
1209
- ...removalResult,
1210
- resubmissionStatus: 'failed',
1211
- resubmissionError: errorMsg
1212
- };
1213
- }
1214
- }
1215
- /**
1216
- * Log an error from a message edit operation for troubleshooting and retry
1217
- * Stores errors in data/worlds/{worldName}/edit-errors.json
1218
- * Keeps only the last 100 errors
1219
- *
1220
- * @param worldId - World ID
1221
- * @param errorLog - EditErrorLog to persist
1222
- */
1223
- export async function logEditError(worldId, errorLog) {
1224
- await ensureInitialization();
1225
- const resolvedWorldId = await getResolvedWorldId(worldId);
1226
- const rootPath = getDefaultRootPath();
1227
- const worldDir = getWorldDir(rootPath, resolvedWorldId);
1228
- const errorsFile = path.join(worldDir, 'edit-errors.json');
1229
- try {
1230
- // Read existing errors
1231
- let errors = [];
1232
- if (fs.existsSync(errorsFile)) {
1233
- const data = fs.readFileSync(errorsFile, 'utf-8');
1234
- errors = JSON.parse(data);
1235
- }
1236
- // Add new error
1237
- errors.push(errorLog);
1238
- // Keep only last 100 errors
1239
- if (errors.length > 100) {
1240
- errors = errors.slice(-100);
1241
- }
1242
- // Write back to file
1243
- fs.writeFileSync(errorsFile, JSON.stringify(errors, null, 2), 'utf-8');
1244
- logger.debug(`Logged edit error for world '${resolvedWorldId}'`);
1245
- }
1246
- catch (error) {
1247
- logger.error(`Failed to log edit error for world '${resolvedWorldId}': ${error instanceof Error ? error.message : error}`);
1248
- }
1249
- }
1250
- /**
1251
- * Get edit error logs for a world
1252
- *
1253
- * @param worldId - World ID
1254
- * @returns Array of EditErrorLog entries
1255
- */
1256
- export async function getEditErrors(worldId) {
1257
- await ensureInitialization();
1258
- const resolvedWorldId = await getResolvedWorldId(worldId);
1259
- const rootPath = getDefaultRootPath();
1260
- const worldDir = getWorldDir(rootPath, resolvedWorldId);
1261
- const errorsFile = path.join(worldDir, 'edit-errors.json');
1262
- try {
1263
- if (!fs.existsSync(errorsFile)) {
1264
- return [];
1265
- }
1266
- const data = fs.readFileSync(errorsFile, 'utf-8');
1267
- return JSON.parse(data);
1268
- }
1269
- catch (error) {
1270
- logger.error(`Failed to read edit errors for world '${resolvedWorldId}': ${error instanceof Error ? error.message : error}`);
1271
- return [];
1272
- }
1273
- }
1109
+ // Re-exports from extracted modules
1110
+ export { migrateMessageIds, removeMessagesFrom, editUserMessage, logEditError, getEditErrors } from './message-edit-manager.js';
1111
+ export { addToQueue, recoverQueueSendingMessages, enqueueAndProcessUserTurn, dispatchImmediateChatMessage, getQueueMessages, removeFromQueue, pauseChatQueue, resumeChatQueue, stopChatQueue, clearChatQueue, retryQueueMessage } from './queue-manager.js';