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.
- package/README.md +17 -7
- package/dist/cli/commands.d.ts +109 -0
- package/dist/cli/commands.js +2024 -0
- package/dist/cli/display.d.ts +124 -0
- package/dist/cli/display.js +381 -0
- package/dist/cli/hitl.d.ts +33 -0
- package/dist/cli/hitl.js +81 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/stream.d.ts +41 -0
- package/dist/cli/stream.js +222 -0
- package/dist/core/activity-tracker.d.ts +16 -0
- package/dist/core/activity-tracker.d.ts.map +1 -0
- package/dist/core/activity-tracker.js +91 -0
- package/dist/core/activity-tracker.js.map +1 -0
- package/dist/core/ai-commands.d.ts +16 -0
- package/dist/core/ai-commands.d.ts.map +1 -0
- package/dist/core/ai-commands.js +24 -0
- package/dist/core/ai-commands.js.map +1 -0
- package/dist/core/ai-sdk-patch.d.ts +24 -0
- package/dist/core/ai-sdk-patch.d.ts.map +1 -0
- package/dist/core/ai-sdk-patch.js +169 -0
- package/dist/core/ai-sdk-patch.js.map +1 -0
- package/dist/core/anthropic-direct.d.ts +52 -0
- package/dist/core/anthropic-direct.d.ts.map +1 -0
- package/dist/core/anthropic-direct.js +301 -0
- package/dist/core/anthropic-direct.js.map +1 -0
- package/dist/core/approval-cache.d.ts +104 -0
- package/dist/core/approval-cache.d.ts.map +1 -0
- package/dist/core/approval-cache.js +150 -0
- package/dist/core/approval-cache.js.map +1 -0
- package/dist/core/chat-constants.d.ts +20 -0
- package/dist/core/chat-constants.d.ts.map +1 -0
- package/dist/core/chat-constants.js +22 -0
- package/dist/core/chat-constants.js.map +1 -0
- package/dist/core/create-agent-tool.d.ts +66 -0
- package/dist/core/create-agent-tool.d.ts.map +1 -0
- package/dist/core/create-agent-tool.js +212 -0
- package/dist/core/create-agent-tool.js.map +1 -0
- package/dist/core/events/approval-checker.d.ts +61 -0
- package/dist/core/events/approval-checker.d.ts.map +1 -0
- package/dist/core/events/approval-checker.js +226 -0
- package/dist/core/events/approval-checker.js.map +1 -0
- package/dist/core/events/index.d.ts +25 -0
- package/dist/core/events/index.d.ts.map +1 -0
- package/dist/core/events/index.js +30 -0
- package/dist/core/events/index.js.map +1 -0
- package/dist/core/events/memory-manager.d.ts +73 -0
- package/dist/core/events/memory-manager.d.ts.map +1 -0
- package/dist/core/events/memory-manager.js +1218 -0
- package/dist/core/events/memory-manager.js.map +1 -0
- package/dist/core/events/mention-logic.d.ts +39 -0
- package/dist/core/events/mention-logic.d.ts.map +1 -0
- package/dist/core/events/mention-logic.js +163 -0
- package/dist/core/events/mention-logic.js.map +1 -0
- package/dist/core/events/orchestrator.d.ts +69 -0
- package/dist/core/events/orchestrator.d.ts.map +1 -0
- package/dist/core/events/orchestrator.js +883 -0
- package/dist/core/events/orchestrator.js.map +1 -0
- package/dist/core/events/persistence.d.ts +41 -0
- package/dist/core/events/persistence.d.ts.map +1 -0
- package/dist/core/events/persistence.js +296 -0
- package/dist/core/events/persistence.js.map +1 -0
- package/dist/core/events/publishers.d.ts +81 -0
- package/dist/core/events/publishers.d.ts.map +1 -0
- package/dist/core/events/publishers.js +272 -0
- package/dist/core/events/publishers.js.map +1 -0
- package/dist/core/events/subscribers.d.ts +45 -0
- package/dist/core/events/subscribers.d.ts.map +1 -0
- package/dist/core/events/subscribers.js +288 -0
- package/dist/core/events/subscribers.js.map +1 -0
- package/dist/core/events/tool-bridge-logging.d.ts +28 -0
- package/dist/core/events/tool-bridge-logging.d.ts.map +1 -0
- package/dist/core/events/tool-bridge-logging.js +94 -0
- package/dist/core/events/tool-bridge-logging.js.map +1 -0
- package/dist/core/events-metadata.d.ts +72 -0
- package/dist/core/events-metadata.d.ts.map +1 -0
- package/dist/core/events-metadata.js +167 -0
- package/dist/core/events-metadata.js.map +1 -0
- package/dist/core/events.d.ts +186 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +1248 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/export.d.ts +106 -0
- package/dist/core/export.d.ts.map +1 -0
- package/dist/core/export.js +705 -0
- package/dist/core/export.js.map +1 -0
- package/dist/core/file-tools.d.ts +114 -0
- package/dist/core/file-tools.d.ts.map +1 -0
- package/dist/core/file-tools.js +370 -0
- package/dist/core/file-tools.js.map +1 -0
- package/dist/core/google-direct.d.ts +58 -0
- package/dist/core/google-direct.d.ts.map +1 -0
- package/dist/core/google-direct.js +298 -0
- package/dist/core/google-direct.js.map +1 -0
- package/dist/core/hitl.d.ts +54 -0
- package/dist/core/hitl.d.ts.map +1 -0
- package/dist/core/hitl.js +153 -0
- package/dist/core/hitl.js.map +1 -0
- package/dist/core/index.d.ts +59 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +70 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/llm-config.d.ts +128 -0
- package/dist/core/llm-config.d.ts.map +1 -0
- package/dist/core/llm-config.js +164 -0
- package/dist/core/llm-config.js.map +1 -0
- package/dist/core/llm-manager.d.ts +163 -0
- package/dist/core/llm-manager.d.ts.map +1 -0
- package/dist/core/llm-manager.js +669 -0
- package/dist/core/llm-manager.js.map +1 -0
- package/dist/core/load-skill-tool.d.ts +55 -0
- package/dist/core/load-skill-tool.d.ts.map +1 -0
- package/dist/core/load-skill-tool.js +468 -0
- package/dist/core/load-skill-tool.js.map +1 -0
- package/dist/core/logger.d.ts +88 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +358 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/managers.d.ts +131 -0
- package/dist/core/managers.d.ts.map +1 -0
- package/dist/core/managers.js +1223 -0
- package/dist/core/managers.js.map +1 -0
- package/dist/core/mcp-server-registry.d.ts +304 -0
- package/dist/core/mcp-server-registry.d.ts.map +1 -0
- package/dist/core/mcp-server-registry.js +1769 -0
- package/dist/core/mcp-server-registry.js.map +1 -0
- package/dist/core/mcp-tools.d.ts +56 -0
- package/dist/core/mcp-tools.d.ts.map +1 -0
- package/dist/core/mcp-tools.js +186 -0
- package/dist/core/mcp-tools.js.map +1 -0
- package/dist/core/message-prep.d.ts +81 -0
- package/dist/core/message-prep.d.ts.map +1 -0
- package/dist/core/message-prep.js +223 -0
- package/dist/core/message-prep.js.map +1 -0
- package/dist/core/message-processing-control.d.ts +54 -0
- package/dist/core/message-processing-control.d.ts.map +1 -0
- package/dist/core/message-processing-control.js +139 -0
- package/dist/core/message-processing-control.js.map +1 -0
- package/dist/core/openai-direct.d.ts +80 -0
- package/dist/core/openai-direct.d.ts.map +1 -0
- package/dist/core/openai-direct.js +374 -0
- package/dist/core/openai-direct.js.map +1 -0
- package/dist/core/shell-cmd-tool.d.ts +235 -0
- package/dist/core/shell-cmd-tool.d.ts.map +1 -0
- package/dist/core/shell-cmd-tool.js +1157 -0
- package/dist/core/shell-cmd-tool.js.map +1 -0
- package/dist/core/shell-process-registry.d.ts +88 -0
- package/dist/core/shell-process-registry.d.ts.map +1 -0
- package/dist/core/shell-process-registry.js +309 -0
- package/dist/core/shell-process-registry.js.map +1 -0
- package/dist/core/skill-registry.d.ts +75 -0
- package/dist/core/skill-registry.d.ts.map +1 -0
- package/dist/core/skill-registry.js +369 -0
- package/dist/core/skill-registry.js.map +1 -0
- package/dist/core/skill-script-runner.d.ts +89 -0
- package/dist/core/skill-script-runner.d.ts.map +1 -0
- package/dist/core/skill-script-runner.js +274 -0
- package/dist/core/skill-script-runner.js.map +1 -0
- package/dist/core/skill-selector.d.ts +65 -0
- package/dist/core/skill-selector.d.ts.map +1 -0
- package/dist/core/skill-selector.js +190 -0
- package/dist/core/skill-selector.js.map +1 -0
- package/dist/core/skill-settings.d.ts +20 -0
- package/dist/core/skill-settings.d.ts.map +1 -0
- package/dist/core/skill-settings.js +40 -0
- package/dist/core/skill-settings.js.map +1 -0
- package/dist/core/storage/agent-storage.d.ts +134 -0
- package/dist/core/storage/agent-storage.d.ts.map +1 -0
- package/dist/core/storage/agent-storage.js +498 -0
- package/dist/core/storage/agent-storage.js.map +1 -0
- package/dist/core/storage/eventStorage/fileEventStorage.d.ts +100 -0
- package/dist/core/storage/eventStorage/fileEventStorage.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/fileEventStorage.js +494 -0
- package/dist/core/storage/eventStorage/fileEventStorage.js.map +1 -0
- package/dist/core/storage/eventStorage/index.d.ts +31 -0
- package/dist/core/storage/eventStorage/index.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/index.js +31 -0
- package/dist/core/storage/eventStorage/index.js.map +1 -0
- package/dist/core/storage/eventStorage/memoryEventStorage.d.ts +87 -0
- package/dist/core/storage/eventStorage/memoryEventStorage.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/memoryEventStorage.js +244 -0
- package/dist/core/storage/eventStorage/memoryEventStorage.js.map +1 -0
- package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts +45 -0
- package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/sqliteEventStorage.js +301 -0
- package/dist/core/storage/eventStorage/sqliteEventStorage.js.map +1 -0
- package/dist/core/storage/eventStorage/types.d.ts +142 -0
- package/dist/core/storage/eventStorage/types.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/types.js +43 -0
- package/dist/core/storage/eventStorage/types.js.map +1 -0
- package/dist/core/storage/eventStorage/validation.d.ts +30 -0
- package/dist/core/storage/eventStorage/validation.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/validation.js +68 -0
- package/dist/core/storage/eventStorage/validation.js.map +1 -0
- package/dist/core/storage/legacy-migrations.d.ts +45 -0
- package/dist/core/storage/legacy-migrations.d.ts.map +1 -0
- package/dist/core/storage/legacy-migrations.js +295 -0
- package/dist/core/storage/legacy-migrations.js.map +1 -0
- package/dist/core/storage/memory-storage.d.ts +105 -0
- package/dist/core/storage/memory-storage.d.ts.map +1 -0
- package/dist/core/storage/memory-storage.js +415 -0
- package/dist/core/storage/memory-storage.js.map +1 -0
- package/dist/core/storage/migration-runner.d.ts +96 -0
- package/dist/core/storage/migration-runner.d.ts.map +1 -0
- package/dist/core/storage/migration-runner.js +306 -0
- package/dist/core/storage/migration-runner.js.map +1 -0
- package/dist/core/storage/queue-storage.d.ts +147 -0
- package/dist/core/storage/queue-storage.d.ts.map +1 -0
- package/dist/core/storage/queue-storage.js +290 -0
- package/dist/core/storage/queue-storage.js.map +1 -0
- package/dist/core/storage/skill-storage.d.ts +136 -0
- package/dist/core/storage/skill-storage.d.ts.map +1 -0
- package/dist/core/storage/skill-storage.js +474 -0
- package/dist/core/storage/skill-storage.js.map +1 -0
- package/dist/core/storage/sqlite-schema.d.ts +95 -0
- package/dist/core/storage/sqlite-schema.d.ts.map +1 -0
- package/dist/core/storage/sqlite-schema.js +156 -0
- package/dist/core/storage/sqlite-schema.js.map +1 -0
- package/dist/core/storage/sqlite-storage.d.ts +146 -0
- package/dist/core/storage/sqlite-storage.d.ts.map +1 -0
- package/dist/core/storage/sqlite-storage.js +709 -0
- package/dist/core/storage/sqlite-storage.js.map +1 -0
- package/dist/core/storage/storage-factory.d.ts +61 -0
- package/dist/core/storage/storage-factory.d.ts.map +1 -0
- package/dist/core/storage/storage-factory.js +794 -0
- package/dist/core/storage/storage-factory.js.map +1 -0
- package/dist/core/storage/validation.d.ts +36 -0
- package/dist/core/storage/validation.d.ts.map +1 -0
- package/dist/core/storage/validation.js +79 -0
- package/dist/core/storage/validation.js.map +1 -0
- package/dist/core/storage/world-storage.d.ts +114 -0
- package/dist/core/storage/world-storage.d.ts.map +1 -0
- package/dist/core/storage/world-storage.js +378 -0
- package/dist/core/storage/world-storage.js.map +1 -0
- package/dist/core/subscription.d.ts +43 -0
- package/dist/core/subscription.d.ts.map +1 -0
- package/dist/core/subscription.js +227 -0
- package/dist/core/subscription.js.map +1 -0
- package/dist/core/tool-utils.d.ts +80 -0
- package/dist/core/tool-utils.d.ts.map +1 -0
- package/dist/core/tool-utils.js +273 -0
- package/dist/core/tool-utils.js.map +1 -0
- package/dist/core/types.d.ts +595 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +158 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/utils.d.ts +138 -0
- package/dist/core/utils.d.ts.map +1 -0
- package/dist/core/utils.js +478 -0
- package/dist/core/utils.js.map +1 -0
- package/dist/core/world-class.d.ts +43 -0
- package/dist/core/world-class.d.ts.map +1 -0
- package/dist/core/world-class.js +90 -0
- package/dist/core/world-class.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/public/assets/agent-sprites-DJFgj-zP.png +0 -0
- package/dist/public/assets/border-KHK37r8y.svg +83 -0
- package/dist/public/assets/index-C9kPXL6G.css +1 -0
- package/dist/public/assets/index-DOQEHGWt.js +96 -0
- package/dist/public/index.html +21 -0
- package/dist/server/api.d.ts +2 -0
- package/dist/server/api.js +1124 -0
- package/dist/server/index.d.ts +29 -0
- package/dist/server/sse-handler.d.ts +62 -0
- package/dist/server/sse-handler.js +234 -0
- package/package.json +15 -3
- 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
|