agent-world 0.11.1 → 0.12.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 (267) hide show
  1. package/README.md +17 -7
  2. package/dist/cli/commands.d.ts +109 -0
  3. package/dist/cli/commands.js +2024 -0
  4. package/dist/cli/display.d.ts +124 -0
  5. package/dist/cli/display.js +381 -0
  6. package/dist/cli/hitl.d.ts +33 -0
  7. package/dist/cli/hitl.js +81 -0
  8. package/dist/cli/index.d.ts +2 -0
  9. package/dist/cli/stream.d.ts +41 -0
  10. package/dist/cli/stream.js +222 -0
  11. package/dist/core/activity-tracker.d.ts +16 -0
  12. package/dist/core/activity-tracker.d.ts.map +1 -0
  13. package/dist/core/activity-tracker.js +91 -0
  14. package/dist/core/activity-tracker.js.map +1 -0
  15. package/dist/core/ai-commands.d.ts +16 -0
  16. package/dist/core/ai-commands.d.ts.map +1 -0
  17. package/dist/core/ai-commands.js +24 -0
  18. package/dist/core/ai-commands.js.map +1 -0
  19. package/dist/core/ai-sdk-patch.d.ts +24 -0
  20. package/dist/core/ai-sdk-patch.d.ts.map +1 -0
  21. package/dist/core/ai-sdk-patch.js +169 -0
  22. package/dist/core/ai-sdk-patch.js.map +1 -0
  23. package/dist/core/anthropic-direct.d.ts +52 -0
  24. package/dist/core/anthropic-direct.d.ts.map +1 -0
  25. package/dist/core/anthropic-direct.js +301 -0
  26. package/dist/core/anthropic-direct.js.map +1 -0
  27. package/dist/core/approval-cache.d.ts +104 -0
  28. package/dist/core/approval-cache.d.ts.map +1 -0
  29. package/dist/core/approval-cache.js +150 -0
  30. package/dist/core/approval-cache.js.map +1 -0
  31. package/dist/core/chat-constants.d.ts +20 -0
  32. package/dist/core/chat-constants.d.ts.map +1 -0
  33. package/dist/core/chat-constants.js +22 -0
  34. package/dist/core/chat-constants.js.map +1 -0
  35. package/dist/core/create-agent-tool.d.ts +66 -0
  36. package/dist/core/create-agent-tool.d.ts.map +1 -0
  37. package/dist/core/create-agent-tool.js +212 -0
  38. package/dist/core/create-agent-tool.js.map +1 -0
  39. package/dist/core/events/approval-checker.d.ts +61 -0
  40. package/dist/core/events/approval-checker.d.ts.map +1 -0
  41. package/dist/core/events/approval-checker.js +226 -0
  42. package/dist/core/events/approval-checker.js.map +1 -0
  43. package/dist/core/events/index.d.ts +25 -0
  44. package/dist/core/events/index.d.ts.map +1 -0
  45. package/dist/core/events/index.js +30 -0
  46. package/dist/core/events/index.js.map +1 -0
  47. package/dist/core/events/memory-manager.d.ts +73 -0
  48. package/dist/core/events/memory-manager.d.ts.map +1 -0
  49. package/dist/core/events/memory-manager.js +1218 -0
  50. package/dist/core/events/memory-manager.js.map +1 -0
  51. package/dist/core/events/mention-logic.d.ts +39 -0
  52. package/dist/core/events/mention-logic.d.ts.map +1 -0
  53. package/dist/core/events/mention-logic.js +163 -0
  54. package/dist/core/events/mention-logic.js.map +1 -0
  55. package/dist/core/events/orchestrator.d.ts +69 -0
  56. package/dist/core/events/orchestrator.d.ts.map +1 -0
  57. package/dist/core/events/orchestrator.js +883 -0
  58. package/dist/core/events/orchestrator.js.map +1 -0
  59. package/dist/core/events/persistence.d.ts +41 -0
  60. package/dist/core/events/persistence.d.ts.map +1 -0
  61. package/dist/core/events/persistence.js +296 -0
  62. package/dist/core/events/persistence.js.map +1 -0
  63. package/dist/core/events/publishers.d.ts +81 -0
  64. package/dist/core/events/publishers.d.ts.map +1 -0
  65. package/dist/core/events/publishers.js +272 -0
  66. package/dist/core/events/publishers.js.map +1 -0
  67. package/dist/core/events/subscribers.d.ts +45 -0
  68. package/dist/core/events/subscribers.d.ts.map +1 -0
  69. package/dist/core/events/subscribers.js +288 -0
  70. package/dist/core/events/subscribers.js.map +1 -0
  71. package/dist/core/events/tool-bridge-logging.d.ts +28 -0
  72. package/dist/core/events/tool-bridge-logging.d.ts.map +1 -0
  73. package/dist/core/events/tool-bridge-logging.js +94 -0
  74. package/dist/core/events/tool-bridge-logging.js.map +1 -0
  75. package/dist/core/events-metadata.d.ts +72 -0
  76. package/dist/core/events-metadata.d.ts.map +1 -0
  77. package/dist/core/events-metadata.js +167 -0
  78. package/dist/core/events-metadata.js.map +1 -0
  79. package/dist/core/events.d.ts +186 -0
  80. package/dist/core/events.d.ts.map +1 -0
  81. package/dist/core/events.js +1248 -0
  82. package/dist/core/events.js.map +1 -0
  83. package/dist/core/export.d.ts +106 -0
  84. package/dist/core/export.d.ts.map +1 -0
  85. package/dist/core/export.js +705 -0
  86. package/dist/core/export.js.map +1 -0
  87. package/dist/core/file-tools.d.ts +114 -0
  88. package/dist/core/file-tools.d.ts.map +1 -0
  89. package/dist/core/file-tools.js +370 -0
  90. package/dist/core/file-tools.js.map +1 -0
  91. package/dist/core/google-direct.d.ts +58 -0
  92. package/dist/core/google-direct.d.ts.map +1 -0
  93. package/dist/core/google-direct.js +298 -0
  94. package/dist/core/google-direct.js.map +1 -0
  95. package/dist/core/hitl.d.ts +54 -0
  96. package/dist/core/hitl.d.ts.map +1 -0
  97. package/dist/core/hitl.js +153 -0
  98. package/dist/core/hitl.js.map +1 -0
  99. package/dist/core/index.d.ts +59 -0
  100. package/dist/core/index.d.ts.map +1 -0
  101. package/dist/core/index.js +70 -0
  102. package/dist/core/index.js.map +1 -0
  103. package/dist/core/llm-config.d.ts +128 -0
  104. package/dist/core/llm-config.d.ts.map +1 -0
  105. package/dist/core/llm-config.js +164 -0
  106. package/dist/core/llm-config.js.map +1 -0
  107. package/dist/core/llm-manager.d.ts +163 -0
  108. package/dist/core/llm-manager.d.ts.map +1 -0
  109. package/dist/core/llm-manager.js +669 -0
  110. package/dist/core/llm-manager.js.map +1 -0
  111. package/dist/core/load-skill-tool.d.ts +55 -0
  112. package/dist/core/load-skill-tool.d.ts.map +1 -0
  113. package/dist/core/load-skill-tool.js +468 -0
  114. package/dist/core/load-skill-tool.js.map +1 -0
  115. package/dist/core/logger.d.ts +88 -0
  116. package/dist/core/logger.d.ts.map +1 -0
  117. package/dist/core/logger.js +358 -0
  118. package/dist/core/logger.js.map +1 -0
  119. package/dist/core/managers.d.ts +131 -0
  120. package/dist/core/managers.d.ts.map +1 -0
  121. package/dist/core/managers.js +1223 -0
  122. package/dist/core/managers.js.map +1 -0
  123. package/dist/core/mcp-server-registry.d.ts +304 -0
  124. package/dist/core/mcp-server-registry.d.ts.map +1 -0
  125. package/dist/core/mcp-server-registry.js +1769 -0
  126. package/dist/core/mcp-server-registry.js.map +1 -0
  127. package/dist/core/mcp-tools.d.ts +56 -0
  128. package/dist/core/mcp-tools.d.ts.map +1 -0
  129. package/dist/core/mcp-tools.js +186 -0
  130. package/dist/core/mcp-tools.js.map +1 -0
  131. package/dist/core/message-prep.d.ts +81 -0
  132. package/dist/core/message-prep.d.ts.map +1 -0
  133. package/dist/core/message-prep.js +223 -0
  134. package/dist/core/message-prep.js.map +1 -0
  135. package/dist/core/message-processing-control.d.ts +54 -0
  136. package/dist/core/message-processing-control.d.ts.map +1 -0
  137. package/dist/core/message-processing-control.js +139 -0
  138. package/dist/core/message-processing-control.js.map +1 -0
  139. package/dist/core/openai-direct.d.ts +80 -0
  140. package/dist/core/openai-direct.d.ts.map +1 -0
  141. package/dist/core/openai-direct.js +374 -0
  142. package/dist/core/openai-direct.js.map +1 -0
  143. package/dist/core/shell-cmd-tool.d.ts +235 -0
  144. package/dist/core/shell-cmd-tool.d.ts.map +1 -0
  145. package/dist/core/shell-cmd-tool.js +1157 -0
  146. package/dist/core/shell-cmd-tool.js.map +1 -0
  147. package/dist/core/shell-process-registry.d.ts +88 -0
  148. package/dist/core/shell-process-registry.d.ts.map +1 -0
  149. package/dist/core/shell-process-registry.js +309 -0
  150. package/dist/core/shell-process-registry.js.map +1 -0
  151. package/dist/core/skill-registry.d.ts +75 -0
  152. package/dist/core/skill-registry.d.ts.map +1 -0
  153. package/dist/core/skill-registry.js +369 -0
  154. package/dist/core/skill-registry.js.map +1 -0
  155. package/dist/core/skill-script-runner.d.ts +89 -0
  156. package/dist/core/skill-script-runner.d.ts.map +1 -0
  157. package/dist/core/skill-script-runner.js +274 -0
  158. package/dist/core/skill-script-runner.js.map +1 -0
  159. package/dist/core/skill-selector.d.ts +65 -0
  160. package/dist/core/skill-selector.d.ts.map +1 -0
  161. package/dist/core/skill-selector.js +190 -0
  162. package/dist/core/skill-selector.js.map +1 -0
  163. package/dist/core/skill-settings.d.ts +20 -0
  164. package/dist/core/skill-settings.d.ts.map +1 -0
  165. package/dist/core/skill-settings.js +40 -0
  166. package/dist/core/skill-settings.js.map +1 -0
  167. package/dist/core/storage/agent-storage.d.ts +134 -0
  168. package/dist/core/storage/agent-storage.d.ts.map +1 -0
  169. package/dist/core/storage/agent-storage.js +498 -0
  170. package/dist/core/storage/agent-storage.js.map +1 -0
  171. package/dist/core/storage/eventStorage/fileEventStorage.d.ts +100 -0
  172. package/dist/core/storage/eventStorage/fileEventStorage.d.ts.map +1 -0
  173. package/dist/core/storage/eventStorage/fileEventStorage.js +494 -0
  174. package/dist/core/storage/eventStorage/fileEventStorage.js.map +1 -0
  175. package/dist/core/storage/eventStorage/index.d.ts +31 -0
  176. package/dist/core/storage/eventStorage/index.d.ts.map +1 -0
  177. package/dist/core/storage/eventStorage/index.js +31 -0
  178. package/dist/core/storage/eventStorage/index.js.map +1 -0
  179. package/dist/core/storage/eventStorage/memoryEventStorage.d.ts +87 -0
  180. package/dist/core/storage/eventStorage/memoryEventStorage.d.ts.map +1 -0
  181. package/dist/core/storage/eventStorage/memoryEventStorage.js +244 -0
  182. package/dist/core/storage/eventStorage/memoryEventStorage.js.map +1 -0
  183. package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts +45 -0
  184. package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts.map +1 -0
  185. package/dist/core/storage/eventStorage/sqliteEventStorage.js +301 -0
  186. package/dist/core/storage/eventStorage/sqliteEventStorage.js.map +1 -0
  187. package/dist/core/storage/eventStorage/types.d.ts +142 -0
  188. package/dist/core/storage/eventStorage/types.d.ts.map +1 -0
  189. package/dist/core/storage/eventStorage/types.js +43 -0
  190. package/dist/core/storage/eventStorage/types.js.map +1 -0
  191. package/dist/core/storage/eventStorage/validation.d.ts +30 -0
  192. package/dist/core/storage/eventStorage/validation.d.ts.map +1 -0
  193. package/dist/core/storage/eventStorage/validation.js +68 -0
  194. package/dist/core/storage/eventStorage/validation.js.map +1 -0
  195. package/dist/core/storage/legacy-migrations.d.ts +45 -0
  196. package/dist/core/storage/legacy-migrations.d.ts.map +1 -0
  197. package/dist/core/storage/legacy-migrations.js +295 -0
  198. package/dist/core/storage/legacy-migrations.js.map +1 -0
  199. package/dist/core/storage/memory-storage.d.ts +105 -0
  200. package/dist/core/storage/memory-storage.d.ts.map +1 -0
  201. package/dist/core/storage/memory-storage.js +415 -0
  202. package/dist/core/storage/memory-storage.js.map +1 -0
  203. package/dist/core/storage/migration-runner.d.ts +96 -0
  204. package/dist/core/storage/migration-runner.d.ts.map +1 -0
  205. package/dist/core/storage/migration-runner.js +306 -0
  206. package/dist/core/storage/migration-runner.js.map +1 -0
  207. package/dist/core/storage/queue-storage.d.ts +147 -0
  208. package/dist/core/storage/queue-storage.d.ts.map +1 -0
  209. package/dist/core/storage/queue-storage.js +290 -0
  210. package/dist/core/storage/queue-storage.js.map +1 -0
  211. package/dist/core/storage/skill-storage.d.ts +136 -0
  212. package/dist/core/storage/skill-storage.d.ts.map +1 -0
  213. package/dist/core/storage/skill-storage.js +474 -0
  214. package/dist/core/storage/skill-storage.js.map +1 -0
  215. package/dist/core/storage/sqlite-schema.d.ts +95 -0
  216. package/dist/core/storage/sqlite-schema.d.ts.map +1 -0
  217. package/dist/core/storage/sqlite-schema.js +156 -0
  218. package/dist/core/storage/sqlite-schema.js.map +1 -0
  219. package/dist/core/storage/sqlite-storage.d.ts +146 -0
  220. package/dist/core/storage/sqlite-storage.d.ts.map +1 -0
  221. package/dist/core/storage/sqlite-storage.js +709 -0
  222. package/dist/core/storage/sqlite-storage.js.map +1 -0
  223. package/dist/core/storage/storage-factory.d.ts +61 -0
  224. package/dist/core/storage/storage-factory.d.ts.map +1 -0
  225. package/dist/core/storage/storage-factory.js +794 -0
  226. package/dist/core/storage/storage-factory.js.map +1 -0
  227. package/dist/core/storage/validation.d.ts +36 -0
  228. package/dist/core/storage/validation.d.ts.map +1 -0
  229. package/dist/core/storage/validation.js +79 -0
  230. package/dist/core/storage/validation.js.map +1 -0
  231. package/dist/core/storage/world-storage.d.ts +114 -0
  232. package/dist/core/storage/world-storage.d.ts.map +1 -0
  233. package/dist/core/storage/world-storage.js +378 -0
  234. package/dist/core/storage/world-storage.js.map +1 -0
  235. package/dist/core/subscription.d.ts +43 -0
  236. package/dist/core/subscription.d.ts.map +1 -0
  237. package/dist/core/subscription.js +227 -0
  238. package/dist/core/subscription.js.map +1 -0
  239. package/dist/core/tool-utils.d.ts +80 -0
  240. package/dist/core/tool-utils.d.ts.map +1 -0
  241. package/dist/core/tool-utils.js +273 -0
  242. package/dist/core/tool-utils.js.map +1 -0
  243. package/dist/core/types.d.ts +595 -0
  244. package/dist/core/types.d.ts.map +1 -0
  245. package/dist/core/types.js +158 -0
  246. package/dist/core/types.js.map +1 -0
  247. package/dist/core/utils.d.ts +138 -0
  248. package/dist/core/utils.d.ts.map +1 -0
  249. package/dist/core/utils.js +478 -0
  250. package/dist/core/utils.js.map +1 -0
  251. package/dist/core/world-class.d.ts +43 -0
  252. package/dist/core/world-class.d.ts.map +1 -0
  253. package/dist/core/world-class.js +90 -0
  254. package/dist/core/world-class.js.map +1 -0
  255. package/dist/index.d.ts +18 -0
  256. package/dist/public/assets/agent-sprites-DJFgj-zP.png +0 -0
  257. package/dist/public/assets/border-KHK37r8y.svg +83 -0
  258. package/dist/public/assets/index-C9kPXL6G.css +1 -0
  259. package/dist/public/assets/index-DOQEHGWt.js +96 -0
  260. package/dist/public/index.html +21 -0
  261. package/dist/server/api.d.ts +2 -0
  262. package/dist/server/api.js +1124 -0
  263. package/dist/server/index.d.ts +29 -0
  264. package/dist/server/sse-handler.d.ts +62 -0
  265. package/dist/server/sse-handler.js +234 -0
  266. package/package.json +15 -3
  267. package/scripts/launch-electron.js +0 -58
@@ -0,0 +1,1223 @@
1
+ /**
2
+ * Unified Managers Module - World, Agent, and Chat Management
3
+ *
4
+ * Provides complete lifecycle management for worlds, agents, and chat sessions with:
5
+ * - EventEmitter integration for runtime world instances
6
+ * - Memory management with archiving and restoration capabilities
7
+ * - Chat session management with auto-save and title generation
8
+ * - Automatic ID normalization to kebab-case for consistency
9
+ * - Environment-aware storage operations through storage-factory
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
14
+ *
15
+ * 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)
18
+ *
19
+ * Implementation Details:
20
+ * - Ensures all agent messages include agentId for proper export functionality
21
+ * - Compatible with both SQLite and memory storage backends
22
+ * - 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
+ * Recent Changes:
28
+ * - 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.
29
+ * - 2026-02-14: Updated `editUserMessage` to be fully core-managed for clear+resend behavior without client-side subscription refresh logic.
30
+ * - Edit resubmission now prefers active subscribed world runtimes.
31
+ * - Removed current-session gating checks and always resubmits to the provided `chatId`.
32
+ * - Synchronizes runtime agent memory from storage after removal before resubmission.
33
+ * - 2026-02-13: Added world-level `mainAgent` routing config and agent-level `autoReply` toggle support.
34
+ * - 2026-02-13: Moved edit-resubmission title-regeneration reset into core `editUserMessage` so all clients share the same behavior.
35
+ * - Auto-generated chat titles are reset to `New Chat` before edit resubmission only when the latest persisted
36
+ * chat-title CRUD payload name still matches the current chat name.
37
+ * - 2026-02-13: Centralized default chat-title semantics via shared chat constants.
38
+ * - Uses a single `NEW_CHAT_TITLE` source for reusable chat detection and creation paths.
39
+ * - 2026-02-12: Hardened `getMemory` to auto-migrate legacy messages missing `messageId` before returning memory payloads.
40
+ * - Detects missing IDs, runs idempotent `migrateMessageIds`, and re-reads memory.
41
+ * - Ensures message-list consumers receive canonical `messageId` values from core.
42
+ * - 2026-02-11: Made `deleteWorld` side-effect-free by removing `getWorld` usage.
43
+ * - `deleteWorld` now avoids world runtime hydration/chat creation paths during deletion.
44
+ * - Cleanup hooks are invoked only if present on directly loaded world data.
45
+ *
46
+ * Changes:
47
+ * - 2026-02-10: Added agent identifier resolution across manager APIs.
48
+ * - Agent operations now accept either stored agent ID or agent name.
49
+ * - Fallback lookup resolves renamed agents where `id` and `toKebabCase(name)` differ.
50
+ * - 2026-02-10: Added world identifier resolution across manager APIs.
51
+ * - World operations now accept either stored world ID or world name.
52
+ * - Fallback lookup resolves renamed worlds where `id` and `toKebabCase(name)` differ.
53
+ * - List APIs return normalized world IDs for consistent client routing.
54
+ * - 2025-10-26: Consolidated message publishing - removed resubmitMessageToWorld
55
+ * - Added chatId to WorldMessageEvent and publishMessage parameters
56
+ * - editUserMessage now calls publishMessage directly with validation
57
+ * - Simplified API by removing redundant resubmit wrapper function
58
+ * - 2025-10-25: Fixed messageId bug in editUserMessage resubmission
59
+ * - Bug: Generated unused messageId instead of capturing actual from publishMessage
60
+ * - Fix: Use messageEvent.messageId from publishMessage return value
61
+ * - Impact: Prevents "undefined" string serialization in JSON responses
62
+ * - 2025-10-21: Added message ID migration and user message edit feature (Phases 1 & 2)
63
+ * - migrateMessageIds: Auto-assign IDs to existing messages (idempotent)
64
+ * - removeMessagesFrom: Remove target + subsequent messages by timestamp
65
+ * - editUserMessage: Combined removal + resubmission operation
66
+ * - logEditError/getEditErrors: Error persistence in edit-errors.json
67
+ *
68
+ * Note: Export functionality has been moved to core/export.ts
69
+ */ // Core module imports
70
+ import { createCategoryLogger, initializeLogger } from './logger.js';
71
+ import { EventEmitter } from 'events';
72
+ import { createStorageWithWrappers } from './storage/storage-factory.js';
73
+ import * as utils from './utils.js';
74
+ import { nanoid } from 'nanoid';
75
+ import * as fs from 'fs';
76
+ import * as path from 'path';
77
+ import { getWorldDir } from './storage/world-storage.js';
78
+ import { getDefaultRootPath } from './storage/storage-factory.js';
79
+ import { publishCRUDEvent } from './events/index.js';
80
+ import { NEW_CHAT_TITLE, isDefaultChatTitle } from './chat-constants.js';
81
+ import { hasActiveChatMessageProcessing, stopMessageProcessing } from './message-processing-control.js';
82
+ // Initialize logger and storage
83
+ const logger = createCategoryLogger('core.managers');
84
+ let storageWrappers = null;
85
+ let moduleInitialization = null;
86
+ async function initializeModules() {
87
+ if (storageWrappers) {
88
+ return; // Already initialized
89
+ }
90
+ try {
91
+ initializeLogger();
92
+ storageWrappers = await createStorageWithWrappers();
93
+ }
94
+ catch (error) {
95
+ // Log error but don't throw - allows tests to proceed with mocked storage
96
+ logger.error('Failed to initialize storage', { error: error instanceof Error ? error.message : error });
97
+ throw error;
98
+ }
99
+ }
100
+ function ensureInitialization() {
101
+ if (!moduleInitialization) {
102
+ moduleInitialization = initializeModules();
103
+ }
104
+ return moduleInitialization;
105
+ }
106
+ const NEW_CHAT_CONFIG = { REUSABLE_CHAT_TITLE: NEW_CHAT_TITLE };
107
+ function extractGeneratedChatTitleFromCrudPayload(payload) {
108
+ if (!payload || typeof payload !== 'object')
109
+ return null;
110
+ if (payload.operation !== 'update')
111
+ return null;
112
+ if (payload.entityType !== 'chat')
113
+ return null;
114
+ const entityData = payload.entityData && typeof payload.entityData === 'object' ? payload.entityData : null;
115
+ const title = typeof entityData?.name === 'string' ? entityData.name.trim() : '';
116
+ return title || null;
117
+ }
118
+ async function resetAutoGeneratedChatTitleForEditResubmission(world, chatId) {
119
+ const chat = world.chats.get(chatId) ?? await storageWrappers.loadChatData(world.id, chatId);
120
+ if (!chat)
121
+ return;
122
+ const currentTitle = String(chat.name || '').trim();
123
+ if (!currentTitle || isDefaultChatTitle(currentTitle)) {
124
+ return;
125
+ }
126
+ const eventStorage = world.eventStorage;
127
+ if (!eventStorage) {
128
+ return;
129
+ }
130
+ let latestGeneratedTitle = null;
131
+ try {
132
+ const crudEvents = await eventStorage.getEventsByWorldAndChat(world.id, chatId, {
133
+ types: ['crud'],
134
+ order: 'desc',
135
+ limit: 25
136
+ });
137
+ for (const event of crudEvents) {
138
+ const generatedTitle = extractGeneratedChatTitleFromCrudPayload(event?.payload);
139
+ if (generatedTitle) {
140
+ latestGeneratedTitle = generatedTitle;
141
+ break;
142
+ }
143
+ }
144
+ }
145
+ catch (error) {
146
+ logger.debug('Skipping auto-title reset because chat CRUD events could not be queried', {
147
+ worldId: world.id,
148
+ chatId,
149
+ error: error instanceof Error ? error.message : String(error)
150
+ });
151
+ return;
152
+ }
153
+ if (!latestGeneratedTitle || latestGeneratedTitle !== currentTitle) {
154
+ return;
155
+ }
156
+ let resetSucceeded = false;
157
+ if (typeof storageWrappers.updateChatNameIfCurrent === 'function') {
158
+ resetSucceeded = await storageWrappers.updateChatNameIfCurrent(world.id, chatId, currentTitle, NEW_CHAT_TITLE);
159
+ }
160
+ else {
161
+ const updated = await storageWrappers.updateChatData(world.id, chatId, { name: NEW_CHAT_TITLE });
162
+ resetSucceeded = !!updated;
163
+ }
164
+ if (!resetSucceeded) {
165
+ return;
166
+ }
167
+ const runtimeChat = world.chats.get(chatId);
168
+ if (runtimeChat) {
169
+ runtimeChat.name = NEW_CHAT_TITLE;
170
+ }
171
+ }
172
+ async function syncRuntimeAgentMemoryFromStorage(world, worldId) {
173
+ if (!world?.agents || world.agents.size === 0)
174
+ return;
175
+ for (const runtimeAgent of world.agents.values()) {
176
+ const persistedAgent = await storageWrappers.loadAgent(worldId, runtimeAgent.id);
177
+ runtimeAgent.memory = Array.isArray(persistedAgent?.memory)
178
+ ? [...persistedAgent.memory]
179
+ : [];
180
+ }
181
+ }
182
+ /**
183
+ * Resolve a world identifier to the persisted world ID.
184
+ * Accepts either world ID or world name and supports historical rename drift.
185
+ */
186
+ async function resolveWorldIdentifier(worldIdOrName) {
187
+ const normalizedInput = utils.toKebabCase(worldIdOrName);
188
+ if (!normalizedInput)
189
+ return null;
190
+ // Fast path: direct normalized ID lookup
191
+ const directWorld = await storageWrappers.loadWorld(normalizedInput);
192
+ if (directWorld?.id) {
193
+ return directWorld.id;
194
+ }
195
+ // Fallback: scan worlds and match by normalized ID or normalized name
196
+ const worlds = await storageWrappers.listWorlds();
197
+ const matched = worlds.find((world) => {
198
+ const storedId = String(world.id || '');
199
+ const storedName = String(world.name || '');
200
+ return (storedId === worldIdOrName ||
201
+ storedName === worldIdOrName ||
202
+ utils.toKebabCase(storedId) === normalizedInput ||
203
+ utils.toKebabCase(storedName) === normalizedInput);
204
+ });
205
+ return matched?.id || null;
206
+ }
207
+ async function getResolvedWorldId(worldIdOrName) {
208
+ const resolved = await resolveWorldIdentifier(worldIdOrName);
209
+ return resolved || utils.toKebabCase(worldIdOrName);
210
+ }
211
+ /**
212
+ * Resolve an agent identifier to the persisted agent ID within a world.
213
+ * Accepts either agent ID or agent name and supports historical rename drift.
214
+ */
215
+ async function resolveAgentIdentifier(worldIdOrName, agentIdOrName) {
216
+ const resolvedWorldId = await getResolvedWorldId(worldIdOrName);
217
+ const normalizedInput = utils.toKebabCase(agentIdOrName);
218
+ if (!normalizedInput)
219
+ return null;
220
+ // Fast path: direct normalized ID lookup
221
+ const directAgent = await storageWrappers.loadAgent(resolvedWorldId, normalizedInput);
222
+ if (directAgent?.id) {
223
+ return directAgent.id;
224
+ }
225
+ // Fallback: scan agents and match by normalized ID or normalized name
226
+ const agents = await storageWrappers.listAgents(resolvedWorldId);
227
+ const matched = agents.find((agent) => {
228
+ const storedId = String(agent.id || '');
229
+ const storedName = String(agent.name || '');
230
+ return (storedId === agentIdOrName ||
231
+ storedName === agentIdOrName ||
232
+ utils.toKebabCase(storedId) === normalizedInput ||
233
+ utils.toKebabCase(storedName) === normalizedInput);
234
+ });
235
+ return matched?.id || null;
236
+ }
237
+ async function getResolvedAgentId(worldIdOrName, agentIdOrName) {
238
+ const resolved = await resolveAgentIdentifier(worldIdOrName, agentIdOrName);
239
+ return resolved || utils.toKebabCase(agentIdOrName);
240
+ }
241
+ /**
242
+ * Create new world with configuration and automatically create a new chat
243
+ */
244
+ export async function createWorld(params) {
245
+ await ensureInitialization();
246
+ const worldId = utils.toKebabCase(params.name);
247
+ const exists = await storageWrappers.worldExists(worldId);
248
+ if (exists) {
249
+ throw new Error(`World with name '${params.name}' already exists`);
250
+ }
251
+ const worldData = {
252
+ id: worldId,
253
+ name: params.name,
254
+ description: params.description,
255
+ turnLimit: params.turnLimit || 5,
256
+ mainAgent: params.mainAgent ? String(params.mainAgent).trim() : null,
257
+ chatLLMProvider: params.chatLLMProvider,
258
+ chatLLMModel: params.chatLLMModel,
259
+ mcpConfig: params.mcpConfig,
260
+ variables: params.variables,
261
+ createdAt: new Date(),
262
+ lastUpdated: new Date(),
263
+ totalAgents: 0,
264
+ totalMessages: 0,
265
+ eventEmitter: new EventEmitter(),
266
+ agents: new Map(),
267
+ chats: new Map(),
268
+ eventStorage: storageWrappers?.eventStorage,
269
+ };
270
+ // Setup event persistence
271
+ if (worldData.eventStorage) {
272
+ const { setupEventPersistence, setupWorldActivityListener } = await import('./events/index.js');
273
+ worldData._eventPersistenceCleanup = setupEventPersistence(worldData);
274
+ worldData._activityListenerCleanup = setupWorldActivityListener(worldData);
275
+ }
276
+ await storageWrappers.saveWorld(worldData);
277
+ // Automatically create a new chat for the world
278
+ const world = await getWorld(worldId);
279
+ if (world) {
280
+ await newChat(worldId);
281
+ return await getWorld(worldId);
282
+ }
283
+ return world;
284
+ }
285
+ /**
286
+ * Update world configuration
287
+ */
288
+ export async function updateWorld(worldId, updates) {
289
+ await ensureInitialization();
290
+ const resolvedWorldId = await getResolvedWorldId(worldId);
291
+ const existingData = await storageWrappers.loadWorld(resolvedWorldId);
292
+ if (!existingData) {
293
+ return null;
294
+ }
295
+ const normalizedUpdates = {
296
+ ...updates,
297
+ ...(updates.mainAgent !== undefined ? { mainAgent: updates.mainAgent ? String(updates.mainAgent).trim() : null } : {})
298
+ };
299
+ const updatedData = {
300
+ ...existingData,
301
+ ...normalizedUpdates,
302
+ lastUpdated: new Date()
303
+ };
304
+ await storageWrappers.saveWorld(updatedData);
305
+ return getWorld(resolvedWorldId);
306
+ }
307
+ /**
308
+ * Set the raw .env-style variables text for a world
309
+ */
310
+ export async function setWorldVariablesText(worldId, variablesText) {
311
+ await ensureInitialization();
312
+ return updateWorld(worldId, { variables: variablesText });
313
+ }
314
+ /**
315
+ * Get the raw .env-style variables text for a world
316
+ */
317
+ export async function getWorldVariablesText(worldId) {
318
+ await ensureInitialization();
319
+ const resolvedWorldId = await getResolvedWorldId(worldId);
320
+ const world = await storageWrappers.loadWorld(resolvedWorldId);
321
+ if (!world) {
322
+ return '';
323
+ }
324
+ return typeof world.variables === 'string' ? world.variables : '';
325
+ }
326
+ /**
327
+ * Get parsed environment map from world variables text
328
+ */
329
+ export async function getWorldEnvMap(worldId) {
330
+ await ensureInitialization();
331
+ const variablesText = await getWorldVariablesText(worldId);
332
+ return utils.parseEnvText(variablesText);
333
+ }
334
+ /**
335
+ * Get a single env value from world variables text
336
+ */
337
+ export async function getWorldEnvValue(worldId, key) {
338
+ await ensureInitialization();
339
+ if (!key) {
340
+ return undefined;
341
+ }
342
+ const envMap = await getWorldEnvMap(worldId);
343
+ return envMap[key];
344
+ }
345
+ /**
346
+ * Delete world and all associated data
347
+ */
348
+ export async function deleteWorld(worldId) {
349
+ await ensureInitialization();
350
+ const resolvedWorldId = await getResolvedWorldId(worldId);
351
+ // Side-effect-free cleanup path: avoid getWorld() because it can hydrate runtime state.
352
+ const worldData = await storageWrappers.loadWorld(resolvedWorldId);
353
+ if (worldData?._eventPersistenceCleanup) {
354
+ worldData._eventPersistenceCleanup();
355
+ }
356
+ if (worldData?._activityListenerCleanup) {
357
+ worldData._activityListenerCleanup();
358
+ }
359
+ return await storageWrappers.deleteWorld(resolvedWorldId);
360
+ }
361
+ /**
362
+ * Get all world IDs and basic information
363
+ */
364
+ export async function listWorlds() {
365
+ await ensureInitialization();
366
+ const allWorldData = await storageWrappers.listWorlds();
367
+ const worldsWithAgentCount = await Promise.all(allWorldData.map(async (data) => {
368
+ try {
369
+ const normalizedId = utils.toKebabCase(data.id || data.name || '');
370
+ const agents = await storageWrappers.listAgents(data.id);
371
+ return { ...data, id: normalizedId || data.id, agentCount: agents.length };
372
+ }
373
+ catch (error) {
374
+ const normalizedId = utils.toKebabCase(data.id || data.name || '');
375
+ return { ...data, id: normalizedId || data.id, agentCount: 0 };
376
+ }
377
+ }));
378
+ return worldsWithAgentCount;
379
+ }
380
+ /**
381
+ * Get world configuration and create runtime instance, creating a new chat if none exist
382
+ */
383
+ export async function getWorld(worldId) {
384
+ await ensureInitialization();
385
+ const resolvedWorldId = await getResolvedWorldId(worldId);
386
+ logger.debug('getWorldConfig called', {
387
+ originalWorldId: worldId,
388
+ resolvedWorldId
389
+ });
390
+ const worldData = await storageWrappers.loadWorld(resolvedWorldId);
391
+ logger.debug('loadWorld result', {
392
+ worldFound: !!worldData,
393
+ worldId: worldData?.id,
394
+ worldName: worldData?.name
395
+ });
396
+ if (!worldData) {
397
+ logger.debug('World not found, returning null');
398
+ return null;
399
+ }
400
+ let agents = await storageWrappers.listAgents(resolvedWorldId);
401
+ let chats = await storageWrappers.listChats(resolvedWorldId);
402
+ // If there are no chats, create a new one
403
+ if (chats.length === 0) {
404
+ logger.debug('No chats found for world, creating new chat');
405
+ await newChat(resolvedWorldId);
406
+ chats = await storageWrappers.listChats(resolvedWorldId);
407
+ }
408
+ const world = {
409
+ ...worldData,
410
+ eventEmitter: new EventEmitter(),
411
+ agents: new Map(agents.map((agent) => [agent.id, agent])),
412
+ chats: new Map(chats.map((chat) => [chat.id, chat])),
413
+ eventStorage: storageWrappers?.eventStorage,
414
+ _eventPersistenceCleanup: undefined, // Will be set by setupEventPersistence
415
+ _activityListenerCleanup: undefined, // Will be set by setupWorldActivityListener
416
+ };
417
+ // Setup event persistence and activity listener
418
+ if (world.eventStorage) {
419
+ const { setupEventPersistence, setupWorldActivityListener } = await import('./events/index.js');
420
+ world._eventPersistenceCleanup = setupEventPersistence(world);
421
+ world._activityListenerCleanup = setupWorldActivityListener(world);
422
+ }
423
+ return world;
424
+ }
425
+ /**
426
+ * Create new agent with configuration and system prompt
427
+ */
428
+ export async function createAgent(worldId, params, options) {
429
+ await ensureInitialization();
430
+ const resolvedWorldId = await getResolvedWorldId(worldId);
431
+ // Check if world is processing to prevent agent creation during concurrent chat sessions
432
+ const { getActiveSubscribedWorld } = await import('./subscription.js');
433
+ const activeSubscribedWorld = getActiveSubscribedWorld(resolvedWorldId);
434
+ const world = activeSubscribedWorld || await getWorld(resolvedWorldId);
435
+ if (world?.isProcessing && options?.allowWhileProcessing !== true) {
436
+ throw new Error('Cannot create agent while world is processing');
437
+ }
438
+ const agentId = params.id || utils.toKebabCase(params.name);
439
+ const exists = await storageWrappers.agentExists(resolvedWorldId, agentId);
440
+ if (exists) {
441
+ throw new Error(`Agent with ID '${agentId}' already exists`);
442
+ }
443
+ const now = new Date();
444
+ const agent = {
445
+ id: agentId,
446
+ name: params.name,
447
+ type: params.type,
448
+ autoReply: params.autoReply ?? true,
449
+ status: 'inactive',
450
+ provider: params.provider,
451
+ model: params.model,
452
+ systemPrompt: params.systemPrompt,
453
+ temperature: params.temperature,
454
+ maxTokens: params.maxTokens,
455
+ createdAt: now,
456
+ lastActive: now,
457
+ llmCallCount: 0,
458
+ memory: [],
459
+ };
460
+ await storageWrappers.saveAgent(resolvedWorldId, agent);
461
+ // Emit CRUD event for real-time updates
462
+ if (world) {
463
+ world.agents.set(agent.id, agent);
464
+ publishCRUDEvent(world, 'create', 'agent', agent.id, agent);
465
+ }
466
+ return agent;
467
+ }
468
+ /**
469
+ * Load agent by ID with full configuration and memory
470
+ */
471
+ export async function getAgent(worldId, agentId) {
472
+ await ensureInitialization();
473
+ const resolvedWorldId = await getResolvedWorldId(worldId);
474
+ const resolvedAgentId = await getResolvedAgentId(resolvedWorldId, agentId);
475
+ const agentData = await storageWrappers.loadAgent(resolvedWorldId, resolvedAgentId);
476
+ if (!agentData)
477
+ return null;
478
+ return agentData;
479
+ }
480
+ /**
481
+ * Update agent configuration and/or memory
482
+ */
483
+ export async function updateAgent(worldId, agentId, updates) {
484
+ await ensureInitialization();
485
+ const resolvedWorldId = await getResolvedWorldId(worldId);
486
+ const resolvedAgentId = await getResolvedAgentId(resolvedWorldId, agentId);
487
+ // Check if world is processing to prevent agent modification during concurrent chat sessions
488
+ const { getActiveSubscribedWorld } = await import('./subscription.js');
489
+ const activeSubscribedWorld = getActiveSubscribedWorld(resolvedWorldId);
490
+ const world = activeSubscribedWorld || await getWorld(resolvedWorldId);
491
+ if (world?.isProcessing) {
492
+ throw new Error('Cannot update agent while world is processing');
493
+ }
494
+ const existingAgentData = await storageWrappers.loadAgent(resolvedWorldId, resolvedAgentId);
495
+ if (!existingAgentData) {
496
+ return null;
497
+ }
498
+ const updatedAgent = {
499
+ ...existingAgentData,
500
+ name: updates.name || existingAgentData.name,
501
+ type: updates.type || existingAgentData.type,
502
+ autoReply: updates.autoReply !== undefined ? updates.autoReply : (existingAgentData.autoReply ?? true),
503
+ status: updates.status || existingAgentData.status,
504
+ provider: updates.provider || existingAgentData.provider,
505
+ model: updates.model || existingAgentData.model,
506
+ systemPrompt: updates.systemPrompt !== undefined ? updates.systemPrompt : existingAgentData.systemPrompt,
507
+ temperature: updates.temperature !== undefined ? updates.temperature : existingAgentData.temperature,
508
+ maxTokens: updates.maxTokens !== undefined ? updates.maxTokens : existingAgentData.maxTokens,
509
+ lastActive: new Date()
510
+ };
511
+ await storageWrappers.saveAgent(resolvedWorldId, updatedAgent);
512
+ // Emit CRUD event for real-time updates
513
+ if (world) {
514
+ const runtimeAgent = world.agents.get(resolvedAgentId);
515
+ if (runtimeAgent) {
516
+ Object.assign(runtimeAgent, updatedAgent);
517
+ world.agents.set(resolvedAgentId, runtimeAgent);
518
+ publishCRUDEvent(world, 'update', 'agent', resolvedAgentId, runtimeAgent);
519
+ }
520
+ else {
521
+ world.agents.set(resolvedAgentId, updatedAgent);
522
+ publishCRUDEvent(world, 'update', 'agent', resolvedAgentId, updatedAgent);
523
+ }
524
+ }
525
+ return updatedAgent;
526
+ }
527
+ /**
528
+ * Delete agent and all associated data
529
+ */
530
+ export async function deleteAgent(worldId, agentId) {
531
+ await ensureInitialization();
532
+ const resolvedWorldId = await getResolvedWorldId(worldId);
533
+ const resolvedAgentId = await getResolvedAgentId(resolvedWorldId, agentId);
534
+ // Check if world is processing to prevent agent deletion during concurrent chat sessions
535
+ const { getActiveSubscribedWorld } = await import('./subscription.js');
536
+ const activeSubscribedWorld = getActiveSubscribedWorld(resolvedWorldId);
537
+ const world = activeSubscribedWorld || await getWorld(resolvedWorldId);
538
+ if (world?.isProcessing) {
539
+ throw new Error('Cannot delete agent while world is processing');
540
+ }
541
+ const success = await storageWrappers.deleteAgent(resolvedWorldId, resolvedAgentId);
542
+ // Emit CRUD event for real-time updates
543
+ if (success && world) {
544
+ world.agents.delete(resolvedAgentId);
545
+ publishCRUDEvent(world, 'delete', 'agent', resolvedAgentId);
546
+ }
547
+ return success;
548
+ }
549
+ /**
550
+ * Get all agent IDs and basic information
551
+ */
552
+ export async function listAgents(worldId) {
553
+ await ensureInitialization();
554
+ const resolvedWorldId = await getResolvedWorldId(worldId);
555
+ return await storageWrappers.listAgents(resolvedWorldId);
556
+ }
557
+ /**
558
+ * Add messages to agent memory
559
+ */
560
+ export async function updateAgentMemory(worldId, agentId, messages) {
561
+ await ensureInitialization();
562
+ const resolvedWorldId = await getResolvedWorldId(worldId);
563
+ const resolvedAgentId = await getResolvedAgentId(resolvedWorldId, agentId);
564
+ // Check if world is processing to prevent memory modification during concurrent chat sessions
565
+ const world = await getWorld(resolvedWorldId);
566
+ if (world?.isProcessing) {
567
+ throw new Error('Cannot update agent memory while world is processing');
568
+ }
569
+ const existingAgentData = await storageWrappers.loadAgent(resolvedWorldId, resolvedAgentId);
570
+ if (!existingAgentData) {
571
+ return null;
572
+ }
573
+ // Ensure messages have the agentId set
574
+ const messagesWithAgentId = messages.map(msg => ({
575
+ ...msg,
576
+ agentId: resolvedAgentId
577
+ }));
578
+ const updatedAgent = {
579
+ ...existingAgentData,
580
+ memory: [...existingAgentData.memory, ...messagesWithAgentId],
581
+ lastActive: new Date()
582
+ };
583
+ await storageWrappers.saveAgentMemory(resolvedWorldId, resolvedAgentId, updatedAgent.memory);
584
+ await storageWrappers.saveAgent(resolvedWorldId, updatedAgent);
585
+ return updatedAgent;
586
+ }
587
+ /**
588
+ * Clear agent memory and reset LLM call count
589
+ */
590
+ export async function clearAgentMemory(worldId, agentId) {
591
+ await ensureInitialization();
592
+ const resolvedWorldId = await getResolvedWorldId(worldId);
593
+ const resolvedAgentId = await getResolvedAgentId(resolvedWorldId, agentId);
594
+ // Check if world is processing to prevent memory clearing during concurrent chat sessions
595
+ const world = await getWorld(resolvedWorldId);
596
+ if (world?.isProcessing) {
597
+ throw new Error('Cannot clear agent memory while world is processing');
598
+ }
599
+ logger.debug('Core clearAgentMemory called', {
600
+ worldId,
601
+ resolvedWorldId,
602
+ originalAgentId: agentId,
603
+ resolvedAgentId
604
+ });
605
+ const existingAgentData = await storageWrappers.loadAgent(resolvedWorldId, resolvedAgentId);
606
+ logger.debug('loadAgent result', {
607
+ agentFound: !!existingAgentData,
608
+ agentName: existingAgentData?.name,
609
+ memoryLength: existingAgentData?.memory?.length || 0,
610
+ currentLLMCallCount: existingAgentData?.llmCallCount || 0
611
+ });
612
+ if (!existingAgentData) {
613
+ logger.debug('Agent not found on disk, returning null');
614
+ return null;
615
+ }
616
+ if (existingAgentData.memory && existingAgentData.memory.length > 0) {
617
+ try {
618
+ logger.debug('Archiving existing memory');
619
+ await storageWrappers.archiveMemory(resolvedWorldId, resolvedAgentId, existingAgentData.memory);
620
+ logger.debug('Memory archived successfully');
621
+ }
622
+ catch (error) {
623
+ logger.error('Failed to archive memory', { resolvedAgentId, error: error instanceof Error ? error.message : error });
624
+ }
625
+ }
626
+ const updatedAgent = {
627
+ ...existingAgentData,
628
+ memory: [],
629
+ llmCallCount: 0,
630
+ lastActive: new Date()
631
+ };
632
+ logger.debug('Saving cleared memory to disk');
633
+ await storageWrappers.saveAgentMemory(resolvedWorldId, resolvedAgentId, []);
634
+ await storageWrappers.saveAgent(resolvedWorldId, updatedAgent);
635
+ logger.debug('Memory and LLM call count cleared and saved successfully', {
636
+ resolvedAgentId,
637
+ newLLMCallCount: updatedAgent.llmCallCount
638
+ });
639
+ return updatedAgent;
640
+ }
641
+ /**
642
+ * Create new chat data entry with optional world snapshot
643
+ */
644
+ async function createChat(worldId, params) {
645
+ await ensureInitialization();
646
+ const chatId = `chat-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
647
+ const now = new Date();
648
+ const chatData = {
649
+ id: chatId,
650
+ worldId,
651
+ name: NEW_CHAT_CONFIG.REUSABLE_CHAT_TITLE,
652
+ description: params.description,
653
+ createdAt: now,
654
+ updatedAt: now,
655
+ messageCount: 0,
656
+ };
657
+ await storageWrappers.saveChatData(worldId, chatData);
658
+ // Emit CRUD event for real-time updates
659
+ const world = await getWorld(worldId);
660
+ if (world) {
661
+ world.chats.set(chatData.id, chatData);
662
+ publishCRUDEvent(world, 'create', 'chat', chatData.id, chatData, chatData.id);
663
+ }
664
+ return chatData;
665
+ }
666
+ /**
667
+ * Create a new chat and optionally set it as current for a world
668
+ */
669
+ export async function newChat(worldId) {
670
+ await ensureInitialization();
671
+ const resolvedWorldId = await getResolvedWorldId(worldId);
672
+ const chats = await listChats(resolvedWorldId);
673
+ const existingChat = chats.find(chat => isDefaultChatTitle(chat.name));
674
+ // Only reuse existing "New Chat" if it's empty (has no messages)
675
+ if (existingChat) {
676
+ const messages = await storageWrappers.getMemory(resolvedWorldId, existingChat.id);
677
+ if (messages.length === 0) {
678
+ return await updateWorld(resolvedWorldId, { currentChatId: existingChat.id });
679
+ }
680
+ // If chat has messages, fall through to create a new one
681
+ }
682
+ const chatData = await createChat(resolvedWorldId, {
683
+ name: NEW_CHAT_TITLE,
684
+ captureChat: false
685
+ });
686
+ return await updateWorld(resolvedWorldId, { currentChatId: chatData.id });
687
+ }
688
+ /**
689
+ * Create a branched chat from a source chat up to (and including) the provided message.
690
+ */
691
+ export async function branchChatFromMessage(worldId, sourceChatId, messageId) {
692
+ await ensureInitialization();
693
+ const resolvedWorldId = await getResolvedWorldId(worldId);
694
+ const normalizedSourceChatId = String(sourceChatId || '').trim();
695
+ const normalizedMessageId = String(messageId || '').trim();
696
+ if (!normalizedSourceChatId) {
697
+ throw new Error('Source chat ID is required.');
698
+ }
699
+ if (!normalizedMessageId) {
700
+ throw new Error('Message ID is required.');
701
+ }
702
+ const sourceChat = await storageWrappers.loadChatData(resolvedWorldId, normalizedSourceChatId);
703
+ if (!sourceChat) {
704
+ throw new Error(`Source chat not found: ${normalizedSourceChatId}`);
705
+ }
706
+ const sourceMessages = await storageWrappers.getMemory(resolvedWorldId, normalizedSourceChatId);
707
+ const targetIndex = sourceMessages.findIndex((entry) => String(entry?.messageId || '') === normalizedMessageId &&
708
+ String(entry?.chatId || '') === normalizedSourceChatId);
709
+ if (targetIndex < 0) {
710
+ throw new Error(`Message not found in source chat: ${normalizedMessageId}`);
711
+ }
712
+ const targetMessage = sourceMessages[targetIndex];
713
+ const targetRole = String(targetMessage?.role || '').trim().toLowerCase();
714
+ const targetSender = String(targetMessage?.sender || '').trim().toLowerCase();
715
+ const targetContent = String(targetMessage?.content || '').trim().toLowerCase();
716
+ const hasToolCalls = Array.isArray(targetMessage?.tool_calls) && targetMessage.tool_calls.length > 0;
717
+ const hasToolCallId = Boolean(targetMessage?.tool_call_id);
718
+ const hasToolCallStatus = Boolean(targetMessage?.toolCallStatus);
719
+ const isSystemOrToolSender = targetSender === 'system' || targetSender === 'tool';
720
+ const isErrorLikeAssistantMessage = targetContent.startsWith('[error]') || targetContent.startsWith('error:');
721
+ if (targetRole !== 'assistant' ||
722
+ isSystemOrToolSender ||
723
+ hasToolCalls ||
724
+ hasToolCallId ||
725
+ hasToolCallStatus ||
726
+ isErrorLikeAssistantMessage) {
727
+ throw new Error('Can only branch from assistant messages.');
728
+ }
729
+ const toEpochMillis = (value) => {
730
+ if (value instanceof Date) {
731
+ return value.getTime();
732
+ }
733
+ if (value) {
734
+ const parsed = new Date(String(value)).getTime();
735
+ if (Number.isFinite(parsed)) {
736
+ return parsed;
737
+ }
738
+ }
739
+ return Date.now();
740
+ };
741
+ const cutoffTimestamp = toEpochMillis(targetMessage?.createdAt);
742
+ const updatedWorld = await newChat(resolvedWorldId);
743
+ if (!updatedWorld || !updatedWorld.currentChatId) {
744
+ throw new Error('Failed to create branch chat.');
745
+ }
746
+ const newChatId = String(updatedWorld.currentChatId || '').trim();
747
+ if (!newChatId) {
748
+ throw new Error('Failed to resolve new chat ID for branch.');
749
+ }
750
+ let copiedMessageCount = 0;
751
+ const agents = await listAgents(resolvedWorldId);
752
+ try {
753
+ for (const agent of agents) {
754
+ const loadedAgent = await storageWrappers.loadAgent(resolvedWorldId, agent.id);
755
+ if (!loadedAgent || !Array.isArray(loadedAgent.memory)) {
756
+ continue;
757
+ }
758
+ const sourceAgentMessages = loadedAgent.memory.filter((entry) => String(entry?.chatId || '') === normalizedSourceChatId);
759
+ if (sourceAgentMessages.length === 0) {
760
+ continue;
761
+ }
762
+ const branchMessages = [];
763
+ let reachedTarget = false;
764
+ for (const sourceEntry of sourceAgentMessages) {
765
+ branchMessages.push({
766
+ ...sourceEntry,
767
+ chatId: newChatId
768
+ });
769
+ if (String(sourceEntry?.messageId || '') === normalizedMessageId) {
770
+ reachedTarget = true;
771
+ break;
772
+ }
773
+ }
774
+ const effectiveBranchMessages = reachedTarget
775
+ ? branchMessages
776
+ : sourceAgentMessages
777
+ .filter((entry) => toEpochMillis(entry?.createdAt) <= cutoffTimestamp)
778
+ .map((entry) => ({
779
+ ...entry,
780
+ chatId: newChatId
781
+ }));
782
+ if (effectiveBranchMessages.length === 0) {
783
+ continue;
784
+ }
785
+ copiedMessageCount += effectiveBranchMessages.length;
786
+ await storageWrappers.saveAgentMemory(resolvedWorldId, loadedAgent.id, [...loadedAgent.memory, ...effectiveBranchMessages]);
787
+ }
788
+ }
789
+ catch (error) {
790
+ try {
791
+ await deleteChat(resolvedWorldId, newChatId);
792
+ }
793
+ catch (rollbackError) {
794
+ logger.error('Failed to rollback branched chat after copy error', {
795
+ worldId: resolvedWorldId,
796
+ newChatId,
797
+ rollbackError: rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
798
+ });
799
+ }
800
+ throw error;
801
+ }
802
+ return {
803
+ world: updatedWorld,
804
+ newChatId,
805
+ copiedMessageCount
806
+ };
807
+ }
808
+ export async function listChats(worldId) {
809
+ await ensureInitialization();
810
+ const resolvedWorldId = await getResolvedWorldId(worldId);
811
+ return await storageWrappers.listChats(resolvedWorldId);
812
+ }
813
+ export async function updateChat(worldId, chatId, updates) {
814
+ await ensureInitialization();
815
+ const resolvedWorldId = await getResolvedWorldId(worldId);
816
+ const chat = await storageWrappers.updateChatData(resolvedWorldId, chatId, updates);
817
+ if (!chat) {
818
+ return null;
819
+ }
820
+ // When a chat is updated we refresh the cached world representation
821
+ const world = await getWorld(resolvedWorldId);
822
+ if (world && world.chats.has(chatId)) {
823
+ world.chats.set(chatId, {
824
+ ...world.chats.get(chatId),
825
+ ...chat
826
+ });
827
+ }
828
+ return chat;
829
+ }
830
+ export async function deleteChat(worldId, chatId) {
831
+ await ensureInitialization();
832
+ const resolvedWorldId = await getResolvedWorldId(worldId);
833
+ // First, delete all agent memory items associated with this chat
834
+ const deletedMemoryCount = await storageWrappers.deleteMemoryByChatId(resolvedWorldId, chatId);
835
+ logger.debug('Deleted memory items for chat', { worldId, resolvedWorldId, chatId, deletedMemoryCount });
836
+ // Get the world to check if this was the current chat
837
+ const world = await getWorld(resolvedWorldId);
838
+ let shouldSetNewCurrentChat = false;
839
+ if (world && world.currentChatId === chatId) {
840
+ shouldSetNewCurrentChat = true;
841
+ }
842
+ // Emit CRUD event BEFORE deletion (while chat_id still exists in DB)
843
+ if (world) {
844
+ publishCRUDEvent(world, 'delete', 'chat', chatId, undefined, chatId);
845
+ }
846
+ // Then delete the chat itself
847
+ const chatDeleted = await storageWrappers.deleteChatData(resolvedWorldId, chatId);
848
+ // Remove from world's in-memory chat map
849
+ if (chatDeleted && world) {
850
+ world.chats.delete(chatId);
851
+ }
852
+ // If this was the current chat, set a fallback current chat
853
+ if (shouldSetNewCurrentChat && chatDeleted) {
854
+ const remainingChats = await storageWrappers.listChats(resolvedWorldId);
855
+ if (remainingChats.length > 0) {
856
+ // Set the most recently updated chat as current
857
+ const latestChat = remainingChats.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())[0];
858
+ await updateWorld(resolvedWorldId, { currentChatId: latestChat.id });
859
+ }
860
+ else {
861
+ // No chats left, create a new one
862
+ logger.debug('No chats remaining after deletion, creating new chat');
863
+ await newChat(resolvedWorldId);
864
+ }
865
+ }
866
+ return chatDeleted;
867
+ }
868
+ export async function restoreChat(worldId, chatId) {
869
+ await ensureInitialization();
870
+ const resolvedWorldId = await getResolvedWorldId(worldId);
871
+ let world = await getWorld(resolvedWorldId);
872
+ if (!world) {
873
+ return null;
874
+ }
875
+ if (world.currentChatId === chatId) {
876
+ return world;
877
+ }
878
+ const runtimeChatExists = world.chats.has(chatId);
879
+ const persistedChatExists = runtimeChatExists
880
+ ? true
881
+ : !!(await storageWrappers.loadChatData(resolvedWorldId, chatId));
882
+ if (!persistedChatExists) {
883
+ return null;
884
+ }
885
+ world = await updateWorld(resolvedWorldId, {
886
+ currentChatId: chatId
887
+ });
888
+ return world;
889
+ }
890
+ export async function getMemory(worldId, chatId) {
891
+ await ensureInitialization();
892
+ const resolvedWorldId = await getResolvedWorldId(worldId);
893
+ let world = await getWorld(resolvedWorldId);
894
+ if (!world) {
895
+ return null;
896
+ }
897
+ const resolvedChatId = chatId || world.currentChatId;
898
+ const memory = await storageWrappers.getMemory(resolvedWorldId, resolvedChatId);
899
+ // Auto-repair legacy memories so downstream clients can rely on messageId without UI fallbacks.
900
+ if (memory.some(message => !message.messageId)) {
901
+ logger.warn('Detected messages without messageId in getMemory; running migration', {
902
+ worldId: resolvedWorldId,
903
+ chatId: resolvedChatId
904
+ });
905
+ await migrateMessageIds(resolvedWorldId);
906
+ return await storageWrappers.getMemory(resolvedWorldId, resolvedChatId);
907
+ }
908
+ return memory;
909
+ }
910
+ /**
911
+ * Migrate messages to include messageId for user message edit feature
912
+ * Automatically detects storage type and handles both file and SQL storage
913
+ * Idempotent - safe to run multiple times
914
+ *
915
+ * @param worldId - World ID to migrate messages for
916
+ * @returns Number of messages migrated
917
+ */
918
+ export async function migrateMessageIds(worldId) {
919
+ await ensureInitialization();
920
+ const resolvedWorldId = await getResolvedWorldId(worldId);
921
+ let totalMigrated = 0;
922
+ const world = await getWorld(resolvedWorldId);
923
+ if (!world) {
924
+ throw new Error(`World '${worldId}' not found`);
925
+ }
926
+ // Get all agents in the world
927
+ const agents = await listAgents(resolvedWorldId);
928
+ // Get all chats for the world
929
+ const chats = await storageWrappers.listChats(resolvedWorldId);
930
+ // Migrate messages for each chat
931
+ for (const chat of chats) {
932
+ const chatId = chat.id;
933
+ // Get all memory for this chat
934
+ const memory = await storageWrappers.getMemory(resolvedWorldId, chatId);
935
+ if (!memory || memory.length === 0) {
936
+ continue;
937
+ }
938
+ // Check which messages need messageId
939
+ let needsMigration = false;
940
+ const updatedMemory = [];
941
+ for (const message of memory) {
942
+ if (!message.messageId) {
943
+ needsMigration = true;
944
+ updatedMemory.push({
945
+ ...message,
946
+ messageId: nanoid(10)
947
+ });
948
+ totalMigrated++;
949
+ }
950
+ else {
951
+ updatedMemory.push(message);
952
+ }
953
+ }
954
+ // If any messages were updated, save the entire memory back
955
+ if (needsMigration) {
956
+ // For each agent, update their memory with the migrated messages
957
+ for (const agent of agents) {
958
+ const agentMessages = updatedMemory.filter(m => m.agentId === agent.id);
959
+ if (agentMessages.length > 0) {
960
+ await storageWrappers.saveAgentMemory(resolvedWorldId, agent.id, agentMessages);
961
+ }
962
+ }
963
+ }
964
+ }
965
+ logger.info(`Migrated ${totalMigrated} messages with messageId for world '${resolvedWorldId}'`);
966
+ return totalMigrated;
967
+ }
968
+ /**
969
+ * Remove a message and all subsequent messages from all agents in a world
970
+ * Used for user message editing feature
971
+ *
972
+ * @param worldId - World ID
973
+ * @param messageId - ID of the message to remove (and all after it)
974
+ * @param chatId - Chat ID to filter messages
975
+ * @returns RemovalResult with per-agent removal details
976
+ */
977
+ export async function removeMessagesFrom(worldId, messageId, chatId) {
978
+ await ensureInitialization();
979
+ const resolvedWorldId = await getResolvedWorldId(worldId);
980
+ const world = await getWorld(resolvedWorldId);
981
+ if (!world) {
982
+ throw new Error(`World '${worldId}' not found`);
983
+ }
984
+ // Get all agents
985
+ const agents = await listAgents(resolvedWorldId);
986
+ // Track results per agent
987
+ const processedAgents = [];
988
+ const failedAgents = [];
989
+ let messagesRemovedTotal = 0;
990
+ let foundTargetInAnyAgent = false;
991
+ let targetTimestampValue = null;
992
+ const loadedAgentsById = new Map();
993
+ const toTimestamp = (value) => {
994
+ if (value instanceof Date) {
995
+ return value.getTime();
996
+ }
997
+ if (value) {
998
+ const parsed = new Date(value).getTime();
999
+ if (Number.isFinite(parsed)) {
1000
+ return parsed;
1001
+ }
1002
+ }
1003
+ return Date.now();
1004
+ };
1005
+ // First pass: load each agent and discover the deletion cutoff timestamp
1006
+ for (const agent of agents) {
1007
+ try {
1008
+ const fullAgent = await storageWrappers.loadAgent(resolvedWorldId, agent.id);
1009
+ if (!fullAgent || !fullAgent.memory || fullAgent.memory.length === 0) {
1010
+ continue;
1011
+ }
1012
+ loadedAgentsById.set(agent.id, fullAgent);
1013
+ // Find the target message in this chat for global cutoff derivation
1014
+ const targetMsg = fullAgent.memory.find(m => m.messageId === messageId && m.chatId === chatId);
1015
+ if (targetMsg) {
1016
+ foundTargetInAnyAgent = true;
1017
+ const candidateTimestamp = toTimestamp(targetMsg.createdAt);
1018
+ if (targetTimestampValue === null || candidateTimestamp < targetTimestampValue) {
1019
+ targetTimestampValue = candidateTimestamp;
1020
+ }
1021
+ }
1022
+ }
1023
+ catch (error) {
1024
+ const errorMsg = error instanceof Error ? error.message : String(error);
1025
+ failedAgents.push({
1026
+ agentId: agent.id,
1027
+ error: errorMsg
1028
+ });
1029
+ }
1030
+ }
1031
+ if (!foundTargetInAnyAgent || targetTimestampValue === null) {
1032
+ const notFoundFailures = agents.length > 0
1033
+ ? [
1034
+ ...failedAgents,
1035
+ { agentId: 'all', error: `Message with ID '${messageId}' not found in chat '${chatId}'` }
1036
+ ]
1037
+ : failedAgents;
1038
+ return {
1039
+ success: false,
1040
+ messageId,
1041
+ totalAgents: agents.length,
1042
+ processedAgents,
1043
+ failedAgents: notFoundFailures,
1044
+ messagesRemovedTotal,
1045
+ requiresRetry: false,
1046
+ resubmissionStatus: 'skipped',
1047
+ newMessageId: undefined
1048
+ };
1049
+ }
1050
+ // Second pass: apply cutoff to all agents in the target chat
1051
+ for (const agent of agents) {
1052
+ if (failedAgents.some(entry => entry.agentId === agent.id)) {
1053
+ continue;
1054
+ }
1055
+ const fullAgent = loadedAgentsById.get(agent.id);
1056
+ if (!fullAgent || !Array.isArray(fullAgent.memory) || fullAgent.memory.length === 0) {
1057
+ processedAgents.push(agent.id);
1058
+ continue;
1059
+ }
1060
+ try {
1061
+ const messagesToKeep = fullAgent.memory.filter(m => {
1062
+ if (m.chatId !== chatId) {
1063
+ return true;
1064
+ }
1065
+ const msgTimestamp = toTimestamp(m.createdAt);
1066
+ return msgTimestamp < targetTimestampValue;
1067
+ });
1068
+ const removedCount = fullAgent.memory.length - messagesToKeep.length;
1069
+ if (removedCount === 0) {
1070
+ processedAgents.push(agent.id);
1071
+ continue;
1072
+ }
1073
+ await storageWrappers.saveAgentMemory(resolvedWorldId, agent.id, messagesToKeep);
1074
+ messagesRemovedTotal += removedCount;
1075
+ processedAgents.push(agent.id);
1076
+ }
1077
+ catch (error) {
1078
+ const errorMsg = error instanceof Error ? error.message : String(error);
1079
+ failedAgents.push({
1080
+ agentId: agent.id,
1081
+ error: errorMsg
1082
+ });
1083
+ }
1084
+ }
1085
+ logger.info('Message removal completed', {
1086
+ messageId,
1087
+ success: failedAgents.length === 0,
1088
+ totalAgents: agents.length,
1089
+ processedAgents: processedAgents.length,
1090
+ failedAgents: failedAgents.length,
1091
+ messagesRemovedTotal
1092
+ });
1093
+ return {
1094
+ success: failedAgents.length === 0,
1095
+ messageId,
1096
+ totalAgents: agents.length,
1097
+ processedAgents,
1098
+ failedAgents,
1099
+ messagesRemovedTotal,
1100
+ requiresRetry: failedAgents.length > 0,
1101
+ resubmissionStatus: 'skipped', // Will be updated by editUserMessage
1102
+ newMessageId: undefined
1103
+ };
1104
+ }
1105
+ /**
1106
+ * Edit a user message by removing it and all subsequent messages, then resubmitting with new content
1107
+ * Combines removal and resubmission in a single operation with comprehensive error tracking
1108
+ *
1109
+ * @param worldId - World ID
1110
+ * @param messageId - ID of the message to edit
1111
+ * @param newContent - New message content
1112
+ * @param chatId - Chat ID for the message
1113
+ * @returns RemovalResult with removal and resubmission details
1114
+ */
1115
+ export async function editUserMessage(worldId, messageId, newContent, chatId, targetWorld) {
1116
+ await ensureInitialization();
1117
+ const resolvedWorldId = await getResolvedWorldId(worldId);
1118
+ const { getActiveSubscribedWorld } = await import('./subscription.js');
1119
+ const activeSubscribedWorld = targetWorld || getActiveSubscribedWorld(resolvedWorldId);
1120
+ const world = activeSubscribedWorld || await getWorld(resolvedWorldId);
1121
+ if (!world) {
1122
+ throw new Error(`World '${worldId}' not found`);
1123
+ }
1124
+ if (hasActiveChatMessageProcessing(resolvedWorldId, chatId)) {
1125
+ stopMessageProcessing(resolvedWorldId, chatId);
1126
+ }
1127
+ // Step 1: Remove the message and all subsequent messages
1128
+ const removalResult = await removeMessagesFrom(resolvedWorldId, messageId, chatId);
1129
+ if (!removalResult.success) {
1130
+ return removalResult;
1131
+ }
1132
+ await syncRuntimeAgentMemoryFromStorage(activeSubscribedWorld || world, resolvedWorldId);
1133
+ // Step 2: Reset auto-generated chat title so post-resubmission title generation can run again.
1134
+ await resetAutoGeneratedChatTitleForEditResubmission(world, chatId);
1135
+ const worldForResubmission = activeSubscribedWorld || world;
1136
+ if (!activeSubscribedWorld) {
1137
+ const { subscribeAgentToMessages, subscribeWorldToMessages } = await import('./events/index.js');
1138
+ for (const agent of worldForResubmission.agents.values()) {
1139
+ subscribeAgentToMessages(worldForResubmission, agent);
1140
+ }
1141
+ subscribeWorldToMessages(worldForResubmission);
1142
+ }
1143
+ // Step 3: Attempt resubmission using publishMessage directly
1144
+ try {
1145
+ const { publishMessage } = await import('./events/index.js');
1146
+ const messageEvent = publishMessage(worldForResubmission, newContent, 'human', chatId);
1147
+ logger.info(`Resubmitted edited message to world '${resolvedWorldId}' with new messageId '${messageEvent.messageId}'`);
1148
+ return {
1149
+ ...removalResult,
1150
+ resubmissionStatus: 'success',
1151
+ newMessageId: messageEvent.messageId
1152
+ };
1153
+ }
1154
+ catch (error) {
1155
+ const errorMsg = error instanceof Error ? error.message : String(error);
1156
+ logger.error(`Failed to resubmit message to world '${resolvedWorldId}': ${errorMsg}`);
1157
+ return {
1158
+ ...removalResult,
1159
+ resubmissionStatus: 'failed',
1160
+ resubmissionError: errorMsg
1161
+ };
1162
+ }
1163
+ }
1164
+ /**
1165
+ * Log an error from a message edit operation for troubleshooting and retry
1166
+ * Stores errors in data/worlds/{worldName}/edit-errors.json
1167
+ * Keeps only the last 100 errors
1168
+ *
1169
+ * @param worldId - World ID
1170
+ * @param errorLog - EditErrorLog to persist
1171
+ */
1172
+ export async function logEditError(worldId, errorLog) {
1173
+ await ensureInitialization();
1174
+ const resolvedWorldId = await getResolvedWorldId(worldId);
1175
+ const rootPath = getDefaultRootPath();
1176
+ const worldDir = getWorldDir(rootPath, resolvedWorldId);
1177
+ const errorsFile = path.join(worldDir, 'edit-errors.json');
1178
+ try {
1179
+ // Read existing errors
1180
+ let errors = [];
1181
+ if (fs.existsSync(errorsFile)) {
1182
+ const data = fs.readFileSync(errorsFile, 'utf-8');
1183
+ errors = JSON.parse(data);
1184
+ }
1185
+ // Add new error
1186
+ errors.push(errorLog);
1187
+ // Keep only last 100 errors
1188
+ if (errors.length > 100) {
1189
+ errors = errors.slice(-100);
1190
+ }
1191
+ // Write back to file
1192
+ fs.writeFileSync(errorsFile, JSON.stringify(errors, null, 2), 'utf-8');
1193
+ logger.debug(`Logged edit error for world '${resolvedWorldId}'`);
1194
+ }
1195
+ catch (error) {
1196
+ logger.error(`Failed to log edit error for world '${resolvedWorldId}': ${error instanceof Error ? error.message : error}`);
1197
+ }
1198
+ }
1199
+ /**
1200
+ * Get edit error logs for a world
1201
+ *
1202
+ * @param worldId - World ID
1203
+ * @returns Array of EditErrorLog entries
1204
+ */
1205
+ export async function getEditErrors(worldId) {
1206
+ await ensureInitialization();
1207
+ const resolvedWorldId = await getResolvedWorldId(worldId);
1208
+ const rootPath = getDefaultRootPath();
1209
+ const worldDir = getWorldDir(rootPath, resolvedWorldId);
1210
+ const errorsFile = path.join(worldDir, 'edit-errors.json');
1211
+ try {
1212
+ if (!fs.existsSync(errorsFile)) {
1213
+ return [];
1214
+ }
1215
+ const data = fs.readFileSync(errorsFile, 'utf-8');
1216
+ return JSON.parse(data);
1217
+ }
1218
+ catch (error) {
1219
+ logger.error(`Failed to read edit errors for world '${resolvedWorldId}': ${error instanceof Error ? error.message : error}`);
1220
+ return [];
1221
+ }
1222
+ }
1223
+ //# sourceMappingURL=managers.js.map