agent-world 0.12.3 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -17
- package/dist/cli/commands.d.ts +7 -1
- package/dist/cli/commands.js +27 -10
- package/dist/cli/hitl.d.ts +9 -2
- package/dist/cli/hitl.js +61 -20
- package/dist/cli/index.js +250 -96
- package/dist/cli/system-events.d.ts +27 -0
- package/dist/cli/system-events.js +63 -0
- package/dist/core/activity-tracker.d.ts +38 -2
- package/dist/core/activity-tracker.d.ts.map +1 -1
- package/dist/core/activity-tracker.js +62 -9
- package/dist/core/activity-tracker.js.map +1 -1
- package/dist/core/anthropic-direct.d.ts +2 -0
- package/dist/core/anthropic-direct.d.ts.map +1 -1
- package/dist/core/anthropic-direct.js +43 -1
- package/dist/core/anthropic-direct.js.map +1 -1
- package/dist/core/chat-constants.d.ts +12 -0
- package/dist/core/chat-constants.d.ts.map +1 -1
- package/dist/core/chat-constants.js +5 -0
- package/dist/core/chat-constants.js.map +1 -1
- package/dist/core/create-agent-tool.d.ts +28 -25
- package/dist/core/create-agent-tool.d.ts.map +1 -1
- package/dist/core/create-agent-tool.js +264 -141
- package/dist/core/create-agent-tool.js.map +1 -1
- package/dist/core/events/index.d.ts +5 -2
- package/dist/core/events/index.d.ts.map +1 -1
- package/dist/core/events/index.js +5 -2
- package/dist/core/events/index.js.map +1 -1
- package/dist/core/events/memory-manager.d.ts +26 -1
- package/dist/core/events/memory-manager.d.ts.map +1 -1
- package/dist/core/events/memory-manager.js +877 -72
- package/dist/core/events/memory-manager.js.map +1 -1
- package/dist/core/events/orchestrator.d.ts +8 -0
- package/dist/core/events/orchestrator.d.ts.map +1 -1
- package/dist/core/events/orchestrator.js +214 -38
- package/dist/core/events/orchestrator.js.map +1 -1
- package/dist/core/events/persistence.d.ts +21 -14
- package/dist/core/events/persistence.d.ts.map +1 -1
- package/dist/core/events/persistence.js +100 -61
- package/dist/core/events/persistence.js.map +1 -1
- package/dist/core/events/publishers.d.ts +13 -16
- package/dist/core/events/publishers.d.ts.map +1 -1
- package/dist/core/events/publishers.js +54 -55
- package/dist/core/events/publishers.js.map +1 -1
- package/dist/core/events/subscribers.d.ts +17 -14
- package/dist/core/events/subscribers.d.ts.map +1 -1
- package/dist/core/events/subscribers.js +68 -147
- package/dist/core/events/subscribers.js.map +1 -1
- package/dist/core/events/title-scheduler.d.ts +27 -0
- package/dist/core/events/title-scheduler.d.ts.map +1 -0
- package/dist/core/events/title-scheduler.js +135 -0
- package/dist/core/events/title-scheduler.js.map +1 -0
- package/dist/core/events/tool-bridge-logging.d.ts +4 -1
- package/dist/core/events/tool-bridge-logging.d.ts.map +1 -1
- package/dist/core/events/tool-bridge-logging.js +112 -13
- package/dist/core/events/tool-bridge-logging.js.map +1 -1
- package/dist/core/events-metadata.d.ts.map +1 -1
- package/dist/core/events-metadata.js +8 -4
- package/dist/core/events-metadata.js.map +1 -1
- package/dist/core/export.d.ts +1 -1
- package/dist/core/export.d.ts.map +1 -1
- package/dist/core/export.js +2 -15
- package/dist/core/export.js.map +1 -1
- package/dist/core/feature-path-logging.d.ts +50 -0
- package/dist/core/feature-path-logging.d.ts.map +1 -0
- package/dist/core/feature-path-logging.js +130 -0
- package/dist/core/feature-path-logging.js.map +1 -0
- package/dist/core/file-tools.d.ts +57 -1
- package/dist/core/file-tools.d.ts.map +1 -1
- package/dist/core/file-tools.js +329 -29
- package/dist/core/file-tools.js.map +1 -1
- package/dist/core/google-direct.d.ts +6 -1
- package/dist/core/google-direct.d.ts.map +1 -1
- package/dist/core/google-direct.js +76 -7
- package/dist/core/google-direct.js.map +1 -1
- package/dist/core/heartbeat.d.ts +34 -0
- package/dist/core/heartbeat.d.ts.map +1 -0
- package/dist/core/heartbeat.js +153 -0
- package/dist/core/heartbeat.js.map +1 -0
- package/dist/core/hitl-tool.d.ts +73 -0
- package/dist/core/hitl-tool.d.ts.map +1 -0
- package/dist/core/hitl-tool.js +284 -0
- package/dist/core/hitl-tool.js.map +1 -0
- package/dist/core/hitl.d.ts +85 -8
- package/dist/core/hitl.d.ts.map +1 -1
- package/dist/core/hitl.js +375 -61
- package/dist/core/hitl.js.map +1 -1
- package/dist/core/index.d.ts +12 -7
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +11 -6
- package/dist/core/index.js.map +1 -1
- package/dist/core/llm-manager.d.ts +17 -0
- package/dist/core/llm-manager.d.ts.map +1 -1
- package/dist/core/llm-manager.js +335 -43
- package/dist/core/llm-manager.js.map +1 -1
- package/dist/core/load-skill-tool.d.ts +36 -3
- package/dist/core/load-skill-tool.d.ts.map +1 -1
- package/dist/core/load-skill-tool.js +807 -93
- package/dist/core/load-skill-tool.js.map +1 -1
- package/dist/core/logger.d.ts +14 -0
- package/dist/core/logger.d.ts.map +1 -1
- package/dist/core/logger.js +15 -0
- package/dist/core/logger.js.map +1 -1
- package/dist/core/managers.d.ts +41 -52
- package/dist/core/managers.d.ts.map +1 -1
- package/dist/core/managers.js +422 -533
- package/dist/core/managers.js.map +1 -1
- package/dist/core/mcp-server-registry.d.ts +19 -2
- package/dist/core/mcp-server-registry.d.ts.map +1 -1
- package/dist/core/mcp-server-registry.js +168 -12
- package/dist/core/mcp-server-registry.js.map +1 -1
- package/dist/core/message-cutoff.d.ts +29 -0
- package/dist/core/message-cutoff.d.ts.map +1 -0
- package/dist/core/message-cutoff.js +63 -0
- package/dist/core/message-cutoff.js.map +1 -0
- package/dist/core/message-edit-manager.d.ts +54 -0
- package/dist/core/message-edit-manager.d.ts.map +1 -0
- package/dist/core/message-edit-manager.js +602 -0
- package/dist/core/message-edit-manager.js.map +1 -0
- package/dist/core/message-prep.d.ts +2 -0
- package/dist/core/message-prep.d.ts.map +1 -1
- package/dist/core/message-prep.js +39 -12
- package/dist/core/message-prep.js.map +1 -1
- package/dist/core/message-processing-control.d.ts +1 -0
- package/dist/core/message-processing-control.d.ts.map +1 -1
- package/dist/core/message-processing-control.js +23 -6
- package/dist/core/message-processing-control.js.map +1 -1
- package/dist/core/openai-direct.d.ts +9 -3
- package/dist/core/openai-direct.d.ts.map +1 -1
- package/dist/core/openai-direct.js +267 -33
- package/dist/core/openai-direct.js.map +1 -1
- package/dist/core/optional-tracers/opik-runtime.d.ts +32 -0
- package/dist/core/optional-tracers/opik-runtime.d.ts.map +1 -0
- package/dist/core/optional-tracers/opik-runtime.js +141 -0
- package/dist/core/optional-tracers/opik-runtime.js.map +1 -0
- package/dist/core/queue-manager.d.ts +84 -0
- package/dist/core/queue-manager.d.ts.map +1 -0
- package/dist/core/queue-manager.js +814 -0
- package/dist/core/queue-manager.js.map +1 -0
- package/dist/core/reasoning-controls.d.ts +30 -0
- package/dist/core/reasoning-controls.d.ts.map +1 -0
- package/dist/core/reasoning-controls.js +118 -0
- package/dist/core/reasoning-controls.js.map +1 -0
- package/dist/core/reliability-config.d.ts +82 -0
- package/dist/core/reliability-config.d.ts.map +1 -0
- package/dist/core/reliability-config.js +106 -0
- package/dist/core/reliability-config.js.map +1 -0
- package/dist/core/reliability-runtime.d.ts +53 -0
- package/dist/core/reliability-runtime.d.ts.map +1 -0
- package/dist/core/reliability-runtime.js +92 -0
- package/dist/core/reliability-runtime.js.map +1 -0
- package/dist/core/security/guardrails.d.ts +21 -0
- package/dist/core/security/guardrails.d.ts.map +1 -0
- package/dist/core/security/guardrails.js +111 -0
- package/dist/core/security/guardrails.js.map +1 -0
- package/dist/core/send-message-tool.d.ts +79 -0
- package/dist/core/send-message-tool.d.ts.map +1 -0
- package/dist/core/send-message-tool.js +222 -0
- package/dist/core/send-message-tool.js.map +1 -0
- package/dist/core/shell-cmd-tool.d.ts +82 -1
- package/dist/core/shell-cmd-tool.d.ts.map +1 -1
- package/dist/core/shell-cmd-tool.js +854 -42
- package/dist/core/shell-cmd-tool.js.map +1 -1
- package/dist/core/skill-registry.d.ts +2 -0
- package/dist/core/skill-registry.d.ts.map +1 -1
- package/dist/core/skill-registry.js +52 -2
- package/dist/core/skill-registry.js.map +1 -1
- package/dist/core/storage/eventStorage/fileEventStorage.d.ts +5 -0
- package/dist/core/storage/eventStorage/fileEventStorage.d.ts.map +1 -1
- package/dist/core/storage/eventStorage/fileEventStorage.js +61 -0
- package/dist/core/storage/eventStorage/fileEventStorage.js.map +1 -1
- package/dist/core/storage/eventStorage/memoryEventStorage.d.ts +5 -0
- package/dist/core/storage/eventStorage/memoryEventStorage.d.ts.map +1 -1
- package/dist/core/storage/eventStorage/memoryEventStorage.js +34 -0
- package/dist/core/storage/eventStorage/memoryEventStorage.js.map +1 -1
- package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts +1 -0
- package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts.map +1 -1
- package/dist/core/storage/eventStorage/sqliteEventStorage.js +19 -2
- package/dist/core/storage/eventStorage/sqliteEventStorage.js.map +1 -1
- package/dist/core/storage/eventStorage/types.d.ts +6 -0
- package/dist/core/storage/eventStorage/types.d.ts.map +1 -1
- package/dist/core/storage/eventStorage/types.js +1 -0
- package/dist/core/storage/eventStorage/types.js.map +1 -1
- package/dist/core/storage/eventStorage/validation.d.ts.map +1 -1
- package/dist/core/storage/eventStorage/validation.js +2 -1
- package/dist/core/storage/eventStorage/validation.js.map +1 -1
- package/dist/core/storage/github-world-import.d.ts +84 -0
- package/dist/core/storage/github-world-import.d.ts.map +1 -0
- package/dist/core/storage/github-world-import.js +365 -0
- package/dist/core/storage/github-world-import.js.map +1 -0
- package/dist/core/storage/memory-storage.d.ts +19 -8
- package/dist/core/storage/memory-storage.d.ts.map +1 -1
- package/dist/core/storage/memory-storage.js +147 -49
- package/dist/core/storage/memory-storage.js.map +1 -1
- package/dist/core/storage/queue-storage.d.ts +1 -0
- package/dist/core/storage/queue-storage.d.ts.map +1 -1
- package/dist/core/storage/queue-storage.js +3 -2
- package/dist/core/storage/queue-storage.js.map +1 -1
- package/dist/core/storage/sqlite-storage.d.ts +14 -9
- package/dist/core/storage/sqlite-storage.d.ts.map +1 -1
- package/dist/core/storage/sqlite-storage.js +131 -154
- package/dist/core/storage/sqlite-storage.js.map +1 -1
- package/dist/core/storage/storage-factory.d.ts +3 -0
- package/dist/core/storage/storage-factory.d.ts.map +1 -1
- package/dist/core/storage/storage-factory.js +175 -89
- package/dist/core/storage/storage-factory.js.map +1 -1
- package/dist/core/storage/world-storage.d.ts +1 -1
- package/dist/core/storage/world-storage.d.ts.map +1 -1
- package/dist/core/storage/world-storage.js +5 -1
- package/dist/core/storage/world-storage.js.map +1 -1
- package/dist/core/storage-init.d.ts +11 -0
- package/dist/core/storage-init.d.ts.map +1 -0
- package/dist/core/storage-init.js +122 -0
- package/dist/core/storage-init.js.map +1 -0
- package/dist/core/subscription.d.ts +8 -1
- package/dist/core/subscription.d.ts.map +1 -1
- package/dist/core/subscription.js +130 -23
- package/dist/core/subscription.js.map +1 -1
- package/dist/core/tool-approval.d.ts +45 -0
- package/dist/core/tool-approval.d.ts.map +1 -0
- package/dist/core/tool-approval.js +223 -0
- package/dist/core/tool-approval.js.map +1 -0
- package/dist/core/tool-execution-envelope.d.ts +87 -0
- package/dist/core/tool-execution-envelope.d.ts.map +1 -0
- package/dist/core/tool-execution-envelope.js +168 -0
- package/dist/core/tool-execution-envelope.js.map +1 -0
- package/dist/core/tool-utils.d.ts +9 -2
- package/dist/core/tool-utils.d.ts.map +1 -1
- package/dist/core/tool-utils.js +122 -28
- package/dist/core/tool-utils.js.map +1 -1
- package/dist/core/types.d.ts +69 -36
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +3 -2
- package/dist/core/types.js.map +1 -1
- package/dist/core/utils.d.ts +16 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/utils.js +99 -24
- package/dist/core/utils.js.map +1 -1
- package/dist/core/web-fetch-tool.d.ts +72 -0
- package/dist/core/web-fetch-tool.d.ts.map +1 -0
- package/dist/core/web-fetch-tool.js +491 -0
- package/dist/core/web-fetch-tool.js.map +1 -0
- package/dist/core/world-registry.d.ts +84 -0
- package/dist/core/world-registry.d.ts.map +1 -0
- package/dist/core/world-registry.js +247 -0
- package/dist/core/world-registry.js.map +1 -0
- package/dist/public/assets/index-Be-1xtV-.js +104 -0
- package/dist/public/assets/index-tsDdiXDU.css +1 -0
- package/dist/public/index.html +2 -2
- package/dist/public/mcp-sandbox-proxy.html +148 -0
- package/dist/server/api.js +288 -58
- package/dist/server/error-response.d.ts +27 -0
- package/dist/server/error-response.js +77 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +6 -2
- package/dist/server/sse-handler.d.ts +13 -2
- package/dist/server/sse-handler.js +194 -26
- package/migrations/0015_add_message_queue.sql +36 -0
- package/migrations/0016_add_world_heartbeat.sql +13 -0
- package/migrations/0017_add_title_provenance.sql +7 -0
- package/package.json +31 -10
- package/dist/public/assets/index-BO20H4xt.js +0 -96
- package/dist/public/assets/index-ETY7W5_S.css +0 -1
package/dist/core/managers.js
CHANGED
|
@@ -8,23 +8,41 @@
|
|
|
8
8
|
* - Automatic ID normalization to kebab-case for consistency
|
|
9
9
|
* - Environment-aware storage operations through storage-factory
|
|
10
10
|
* - Agent message management with automatic agentId assignment
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
11
|
+
*
|
|
12
|
+
* Queue management, message editing, and storage initialization are handled by
|
|
13
|
+
* dedicated sub-modules (queue-manager.ts, message-edit-manager.ts, storage-init.ts)
|
|
14
|
+
* whose public APIs are re-exported here for backward compatibility.
|
|
14
15
|
*
|
|
15
16
|
* API: World (create/get/update/delete/list), Agent (create/get/update/delete/list/updateMemory/clearMemory),
|
|
16
|
-
* Chat (newChat/listChats/deleteChat/restoreChat),
|
|
17
|
-
*
|
|
17
|
+
* Chat (newChat/listChats/deleteChat/restoreChat),
|
|
18
|
+
* re-exports: migrateMessageIds, removeMessagesFrom, editUserMessage, logEditError, getEditErrors,
|
|
19
|
+
* addToQueue, enqueueAndProcessUserTurn, dispatchImmediateChatMessage, getQueueMessages,
|
|
20
|
+
* pauseChatQueue, resumeChatQueue, stopChatQueue, clearChatQueue, retryQueueMessage,
|
|
21
|
+
* recoverQueueSendingMessages
|
|
18
22
|
*
|
|
19
23
|
* Implementation Details:
|
|
20
24
|
* - Ensures all agent messages include agentId for proper export functionality
|
|
21
25
|
* - Compatible with both SQLite and memory storage backends
|
|
22
26
|
* - Automatic agent identification for message source tracking
|
|
23
|
-
* - Idempotent message ID migration supporting both file and SQL storage
|
|
24
|
-
* - Comprehensive error tracking for partial failures
|
|
25
|
-
* - Error log persistence with 100-entry retention policy
|
|
26
27
|
*
|
|
27
28
|
* Recent Changes:
|
|
29
|
+
* - 2026-03-11: Synchronized active runtime world metadata during `updateWorld` so live chats pick up
|
|
30
|
+
* updated `variables`/`working_directory` and other top-level config without requiring a runtime refresh.
|
|
31
|
+
* - 2026-03-10: Renamed queue-backed ingress to `enqueueAndProcessUserTurn` and split non-user immediate dispatch into `dispatchImmediateChatMessage`.
|
|
32
|
+
* - 2026-03-10: Removed restore-time user-last resend from persisted chat memory; queue-owned rows are now the only automatic resume authority.
|
|
33
|
+
* - 2026-03-10: Added `restoreChat(..., { suppressAutoResume: true })` support for edit/delete mutation flows so failed last-turn messages are not replayed before mutation.
|
|
34
|
+
* - 2026-03-09: God-module decomposition — extracted queue management (queue-manager.ts),
|
|
35
|
+
* message edit/migration logic (message-edit-manager.ts), and storage singleton +
|
|
36
|
+
* identifier resolution (storage-init.ts) into focused sub-modules. managers.ts reduced
|
|
37
|
+
* from 2928 → 1300 lines; all public exports preserved via re-exports.
|
|
38
|
+
* - 2026-03-09: Added `activateChatResources` helper to consolidate the resource-reactivation
|
|
39
|
+
* sequence (memory sync, skill approvals, HITL replay, queue resume) shared between the
|
|
40
|
+
* two branches of `restoreChat`.
|
|
41
|
+
* - 2026-03-09: Added SSE terminal-state guard in `triggerPendingLastMessageResume` to prevent
|
|
42
|
+
* infinite auto-resume loops when the last SSE event is already terminal.
|
|
43
|
+
* - 2026-03-06: Moved chat-selection runtime control flow onto explicit/persisted chat helpers.
|
|
44
|
+
* - 2026-03-06: Removed runtime `world.currentChatId` fallback from exported chat-memory helpers.
|
|
45
|
+
* - 2026-02-20: Added `claimAgentCreationSlot` / `allowWhileWorldProcessing` for approval-gated tool calls.
|
|
28
46
|
* - 2026-02-16: Added `branchChatFromMessage` to create a new chat branched from an assistant message and copy source-chat history up to the target message.
|
|
29
47
|
* - 2026-02-14: Updated `editUserMessage` to be fully core-managed for clear+resend behavior without client-side subscription refresh logic.
|
|
30
48
|
* - Edit resubmission now prefers active subscribed world runtimes.
|
|
@@ -33,7 +51,7 @@
|
|
|
33
51
|
* - 2026-02-13: Added world-level `mainAgent` routing config and agent-level `autoReply` toggle support.
|
|
34
52
|
* - 2026-02-13: Moved edit-resubmission title-regeneration reset into core `editUserMessage` so all clients share the same behavior.
|
|
35
53
|
* - Auto-generated chat titles are reset to `New Chat` before edit resubmission only when the latest persisted
|
|
36
|
-
* chat-title
|
|
54
|
+
* `chat-title-updated` payload title still matches the current chat name.
|
|
37
55
|
* - 2026-02-13: Centralized default chat-title semantics via shared chat constants.
|
|
38
56
|
* - Uses a single `NEW_CHAT_TITLE` source for reusable chat detection and creation paths.
|
|
39
57
|
* - 2026-02-12: Hardened `getMemory` to auto-migrate legacy messages missing `messageId` before returning memory payloads.
|
|
@@ -66,177 +84,159 @@
|
|
|
66
84
|
* - logEditError/getEditErrors: Error persistence in edit-errors.json
|
|
67
85
|
*
|
|
68
86
|
* Note: Export functionality has been moved to core/export.ts
|
|
69
|
-
*/
|
|
70
|
-
|
|
87
|
+
*/
|
|
88
|
+
// Core module imports
|
|
89
|
+
import { createCategoryLogger } from './logger.js';
|
|
71
90
|
import { EventEmitter } from 'events';
|
|
72
|
-
import { createStorageWithWrappers } from './storage/storage-factory.js';
|
|
73
91
|
import * as utils from './utils.js';
|
|
74
|
-
import {
|
|
75
|
-
import
|
|
76
|
-
import
|
|
77
|
-
import {
|
|
78
|
-
import {
|
|
79
|
-
import {
|
|
80
|
-
import {
|
|
81
|
-
import {
|
|
82
|
-
|
|
92
|
+
import { NEW_CHAT_TITLE, isDefaultChatTitle, TITLE_PROVENANCE_MANUAL } from './chat-constants.js';
|
|
93
|
+
import { hasActiveChatMessageProcessing } from './message-processing-control.js';
|
|
94
|
+
import { replayPendingHitlRequests, listPendingHitlPromptEventsFromMessages } from './hitl.js';
|
|
95
|
+
import { clearChatSkillApprovals, reconstructSkillApprovalsFromMessages } from './load-skill-tool.js';
|
|
96
|
+
import { resumePendingToolCallsForChat } from './events/memory-manager.js';
|
|
97
|
+
import { stopWorldRuntimesByWorldId } from './world-registry.js';
|
|
98
|
+
import { storageWrappers, ensureInitialization, getResolvedWorldId, getResolvedAgentId, } from './storage-init.js';
|
|
99
|
+
import { triggerPendingQueueResume, autoPauseQueueForChat, clearQueuePauseForChat, } from './queue-manager.js';
|
|
100
|
+
import { syncRuntimeAgentMemoryFromStorage, migrateMessageIds, } from './message-edit-manager.js';
|
|
101
|
+
// Per-module state
|
|
83
102
|
const logger = createCategoryLogger('core.managers');
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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)
|
|
103
|
+
const loggerRestore = createCategoryLogger('chat.restore');
|
|
104
|
+
const loggerRestoreResume = createCategoryLogger('chat.restore.resume');
|
|
105
|
+
const inFlightToolResumeKeys = new Set();
|
|
106
|
+
function triggerPendingToolCallResume(world, chatId, targetAssistantMessageId) {
|
|
107
|
+
if (!chatId) {
|
|
121
108
|
return;
|
|
122
|
-
|
|
123
|
-
if (
|
|
109
|
+
}
|
|
110
|
+
if (hasActiveChatMessageProcessing(world.id, chatId)) {
|
|
124
111
|
return;
|
|
125
112
|
}
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
113
|
+
const resumeKey = `${world.id}:${chatId}`;
|
|
114
|
+
if (inFlightToolResumeKeys.has(resumeKey)) {
|
|
128
115
|
return;
|
|
129
116
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
117
|
+
inFlightToolResumeKeys.add(resumeKey);
|
|
118
|
+
void (async () => {
|
|
119
|
+
try {
|
|
120
|
+
const resumedCount = await resumePendingToolCallsForChat(world, chatId, targetAssistantMessageId);
|
|
121
|
+
loggerRestoreResume.debug('Attempted pending tool-call resume after chat restore', {
|
|
122
|
+
worldId: world.id,
|
|
123
|
+
chatId,
|
|
124
|
+
targetAssistantMessageId: targetAssistantMessageId || null,
|
|
125
|
+
resumedCount,
|
|
126
|
+
});
|
|
127
|
+
if (resumedCount > 0) {
|
|
128
|
+
loggerRestoreResume.debug('Resumed pending persisted tool calls after chat restore', {
|
|
129
|
+
worldId: world.id,
|
|
130
|
+
chatId,
|
|
131
|
+
targetAssistantMessageId,
|
|
132
|
+
resumedCount,
|
|
133
|
+
});
|
|
142
134
|
}
|
|
143
135
|
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
loggerRestoreResume.warn('Failed to resume pending persisted tool calls after chat restore', {
|
|
138
|
+
worldId: world.id,
|
|
139
|
+
chatId,
|
|
140
|
+
targetAssistantMessageId,
|
|
141
|
+
error: error instanceof Error ? error.message : String(error),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
inFlightToolResumeKeys.delete(resumeKey);
|
|
146
|
+
}
|
|
147
|
+
})();
|
|
148
|
+
}
|
|
149
|
+
function hasPendingToolCallsOnLastAssistantMessage(lastMessage, chatMemory) {
|
|
150
|
+
if (lastMessage.role !== 'assistant' || !Array.isArray(lastMessage.tool_calls) || lastMessage.tool_calls.length === 0) {
|
|
151
|
+
return false;
|
|
144
152
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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;
|
|
153
|
+
const completedToolCallIds = new Set();
|
|
154
|
+
for (const message of chatMemory) {
|
|
155
|
+
if (message.role !== 'tool' || typeof message.tool_call_id !== 'string') {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const toolCallId = message.tool_call_id.trim();
|
|
159
|
+
if (toolCallId) {
|
|
160
|
+
completedToolCallIds.add(toolCallId);
|
|
161
|
+
}
|
|
166
162
|
}
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
163
|
+
for (const toolCall of lastMessage.tool_calls) {
|
|
164
|
+
const toolCallId = String(toolCall?.id || '').trim();
|
|
165
|
+
if (!toolCallId) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!completedToolCallIds.has(toolCallId)) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
170
171
|
}
|
|
172
|
+
return false;
|
|
171
173
|
}
|
|
172
|
-
|
|
173
|
-
if (!
|
|
174
|
+
function triggerPendingToolCallResumeFromLastMessage(world, chatId) {
|
|
175
|
+
if (!chatId) {
|
|
174
176
|
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
177
|
}
|
|
178
|
+
void (async () => {
|
|
179
|
+
try {
|
|
180
|
+
const chatMemory = await storageWrappers.getMemory(world.id, chatId);
|
|
181
|
+
if (!chatMemory || chatMemory.length === 0) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const lastMessage = chatMemory[chatMemory.length - 1];
|
|
185
|
+
if (!hasPendingToolCallsOnLastAssistantMessage(lastMessage, chatMemory)) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
loggerRestoreResume.debug('Detected pending tool-call-last message during chat-restore inspection', {
|
|
189
|
+
worldId: world.id,
|
|
190
|
+
chatId,
|
|
191
|
+
messageId: lastMessage.messageId || null,
|
|
192
|
+
});
|
|
193
|
+
triggerPendingToolCallResume(world, chatId, lastMessage.messageId);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
loggerRestoreResume.warn('Failed to inspect last message for pending tool-call resume during chat restore', {
|
|
197
|
+
worldId: world.id,
|
|
198
|
+
chatId,
|
|
199
|
+
error: error instanceof Error ? error.message : String(error),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
})();
|
|
181
203
|
}
|
|
204
|
+
// TOCTOU guard: prevents two concurrent createAgent calls from both passing the
|
|
205
|
+
// `agentExists` check before either write lands. Maps worldId → Set<agentId>.
|
|
206
|
+
const pendingAgentCreates = new Map();
|
|
182
207
|
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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.
|
|
208
|
+
* Pre-claim an agent creation slot before showing an approval dialog.
|
|
209
|
+
* Prevents race conditions where two concurrent create_agent tool calls both
|
|
210
|
+
* pass approval before either calls createAgent.
|
|
211
|
+
* Returns a release() function that MUST be called if createAgent is not called.
|
|
212
|
+
* createAgent({ slotAlreadyClaimed: true }) also cleans up the slot itself,
|
|
213
|
+
* so calling release() after createAgent is safe (idempotent).
|
|
214
214
|
*/
|
|
215
|
-
async function
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
215
|
+
export async function claimAgentCreationSlot(worldId, agentName) {
|
|
216
|
+
await ensureInitialization();
|
|
217
|
+
const resolvedWorldId = await getResolvedWorldId(worldId);
|
|
218
|
+
const agentId = utils.toKebabCase(agentName);
|
|
219
|
+
const worldPending = pendingAgentCreates.get(resolvedWorldId) ?? new Set();
|
|
220
|
+
if (!pendingAgentCreates.has(resolvedWorldId)) {
|
|
221
|
+
pendingAgentCreates.set(resolvedWorldId, worldPending);
|
|
222
|
+
}
|
|
223
|
+
if (worldPending.has(agentId)) {
|
|
224
|
+
return { claimed: false, reason: 'already_pending', name: agentName };
|
|
225
|
+
}
|
|
226
|
+
const exists = await storageWrappers.agentExists(resolvedWorldId, agentId);
|
|
227
|
+
if (exists) {
|
|
228
|
+
return { claimed: false, reason: 'already_exists', name: agentName };
|
|
229
|
+
}
|
|
230
|
+
worldPending.add(agentId);
|
|
231
|
+
return {
|
|
232
|
+
claimed: true,
|
|
233
|
+
release: () => {
|
|
234
|
+
worldPending.delete(agentId);
|
|
235
|
+
if (worldPending.size === 0) {
|
|
236
|
+
pendingAgentCreates.delete(resolvedWorldId);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
240
|
}
|
|
241
241
|
/**
|
|
242
242
|
* Create new world with configuration and automatically create a new chat
|
|
@@ -258,6 +258,9 @@ export async function createWorld(params) {
|
|
|
258
258
|
chatLLMModel: params.chatLLMModel,
|
|
259
259
|
mcpConfig: params.mcpConfig,
|
|
260
260
|
variables: params.variables,
|
|
261
|
+
heartbeatEnabled: params.heartbeatEnabled === true,
|
|
262
|
+
heartbeatInterval: params.heartbeatInterval ? String(params.heartbeatInterval).trim() : null,
|
|
263
|
+
heartbeatPrompt: params.heartbeatPrompt ? String(params.heartbeatPrompt) : null,
|
|
261
264
|
createdAt: new Date(),
|
|
262
265
|
lastUpdated: new Date(),
|
|
263
266
|
totalAgents: 0,
|
|
@@ -302,8 +305,59 @@ export async function updateWorld(worldId, updates) {
|
|
|
302
305
|
lastUpdated: new Date()
|
|
303
306
|
};
|
|
304
307
|
await storageWrappers.saveWorld(updatedData);
|
|
308
|
+
const { getActiveSubscribedWorlds } = await import('./subscription.js');
|
|
309
|
+
const activeRuntimeWorlds = getActiveSubscribedWorlds(resolvedWorldId);
|
|
310
|
+
if (activeRuntimeWorlds.length > 0) {
|
|
311
|
+
const shouldSyncCurrentChatId = normalizedUpdates.currentChatId !== undefined;
|
|
312
|
+
for (const activeRuntimeWorld of activeRuntimeWorlds) {
|
|
313
|
+
syncActiveRuntimeWorldMetadata(activeRuntimeWorld, updatedData);
|
|
314
|
+
if (shouldSyncCurrentChatId) {
|
|
315
|
+
activeRuntimeWorld.currentChatId = updatedData.currentChatId ?? null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return activeRuntimeWorlds[0] ?? null;
|
|
319
|
+
}
|
|
305
320
|
return getWorld(resolvedWorldId);
|
|
306
321
|
}
|
|
322
|
+
function syncActiveRuntimeWorldMetadata(targetWorld, persistedWorld) {
|
|
323
|
+
targetWorld.name = persistedWorld.name;
|
|
324
|
+
targetWorld.description = persistedWorld.description;
|
|
325
|
+
targetWorld.turnLimit = persistedWorld.turnLimit;
|
|
326
|
+
targetWorld.mainAgent = persistedWorld.mainAgent ?? null;
|
|
327
|
+
targetWorld.chatLLMProvider = persistedWorld.chatLLMProvider;
|
|
328
|
+
targetWorld.chatLLMModel = persistedWorld.chatLLMModel;
|
|
329
|
+
targetWorld.mcpConfig = persistedWorld.mcpConfig ?? null;
|
|
330
|
+
targetWorld.variables = persistedWorld.variables;
|
|
331
|
+
targetWorld.uiMode = persistedWorld.uiMode;
|
|
332
|
+
targetWorld.dashboardZones = persistedWorld.dashboardZones;
|
|
333
|
+
targetWorld.heartbeatEnabled = persistedWorld.heartbeatEnabled === true;
|
|
334
|
+
targetWorld.heartbeatInterval = persistedWorld.heartbeatInterval ?? null;
|
|
335
|
+
targetWorld.heartbeatPrompt = persistedWorld.heartbeatPrompt ?? null;
|
|
336
|
+
targetWorld.lastUpdated = persistedWorld.lastUpdated;
|
|
337
|
+
}
|
|
338
|
+
async function getPersistedCurrentChatId(worldId) {
|
|
339
|
+
const resolvedWorldId = await getResolvedWorldId(worldId);
|
|
340
|
+
const worldData = await storageWrappers.loadWorld(resolvedWorldId);
|
|
341
|
+
const chatId = String(worldData?.currentChatId || '').trim();
|
|
342
|
+
return chatId || null;
|
|
343
|
+
}
|
|
344
|
+
async function setPersistedCurrentChatId(worldId, chatId) {
|
|
345
|
+
const resolvedWorldId = await getResolvedWorldId(worldId);
|
|
346
|
+
const worldData = await storageWrappers.loadWorld(resolvedWorldId);
|
|
347
|
+
if (!worldData) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const normalizedChatId = String(chatId || '').trim() || null;
|
|
351
|
+
const existingChatId = String(worldData.currentChatId || '').trim() || null;
|
|
352
|
+
if (existingChatId === normalizedChatId) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
await storageWrappers.saveWorld({
|
|
356
|
+
...worldData,
|
|
357
|
+
currentChatId: normalizedChatId,
|
|
358
|
+
lastUpdated: new Date(),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
307
361
|
/**
|
|
308
362
|
* Set the raw .env-style variables text for a world
|
|
309
363
|
*/
|
|
@@ -356,7 +410,11 @@ export async function deleteWorld(worldId) {
|
|
|
356
410
|
if (worldData?._activityListenerCleanup) {
|
|
357
411
|
worldData._activityListenerCleanup();
|
|
358
412
|
}
|
|
359
|
-
|
|
413
|
+
const result = await storageWrappers.deleteWorld(resolvedWorldId);
|
|
414
|
+
// Stop any active runtime so subsequent subscriptions get a fresh world
|
|
415
|
+
// instead of reusing state from the deleted incarnation.
|
|
416
|
+
await stopWorldRuntimesByWorldId(resolvedWorldId);
|
|
417
|
+
return result;
|
|
360
418
|
}
|
|
361
419
|
/**
|
|
362
420
|
* Get all world IDs and basic information
|
|
@@ -420,50 +478,72 @@ export async function getWorld(worldId) {
|
|
|
420
478
|
world._eventPersistenceCleanup = setupEventPersistence(world);
|
|
421
479
|
world._activityListenerCleanup = setupWorldActivityListener(world);
|
|
422
480
|
}
|
|
481
|
+
// Initialize queued chat set; per-chat seeding deferred to explicit queue operations
|
|
482
|
+
world._queuedChatIds = new Set();
|
|
423
483
|
return world;
|
|
424
484
|
}
|
|
425
485
|
/**
|
|
426
486
|
* Create new agent with configuration and system prompt
|
|
427
487
|
*/
|
|
428
|
-
export async function createAgent(worldId, params, options) {
|
|
488
|
+
export async function createAgent(worldId, params, options = {}) {
|
|
429
489
|
await ensureInitialization();
|
|
430
490
|
const resolvedWorldId = await getResolvedWorldId(worldId);
|
|
431
491
|
// Check if world is processing to prevent agent creation during concurrent chat sessions
|
|
432
492
|
const { getActiveSubscribedWorld } = await import('./subscription.js');
|
|
433
493
|
const activeSubscribedWorld = getActiveSubscribedWorld(resolvedWorldId);
|
|
434
494
|
const world = activeSubscribedWorld || await getWorld(resolvedWorldId);
|
|
435
|
-
if (world?.isProcessing && options
|
|
495
|
+
if (world?.isProcessing && !options.allowWhileWorldProcessing) {
|
|
436
496
|
throw new Error('Cannot create agent while world is processing');
|
|
437
497
|
}
|
|
438
498
|
const agentId = params.id || utils.toKebabCase(params.name);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
499
|
+
// Resolve the pending-creates Set (needed in finally regardless of who claimed it).
|
|
500
|
+
const worldPending = pendingAgentCreates.get(resolvedWorldId) ?? new Set();
|
|
501
|
+
if (!pendingAgentCreates.has(resolvedWorldId)) {
|
|
502
|
+
pendingAgentCreates.set(resolvedWorldId, worldPending);
|
|
503
|
+
}
|
|
504
|
+
if (!options.slotAlreadyClaimed) {
|
|
505
|
+
// TOCTOU guard: claim the slot before the async agentExists check.
|
|
506
|
+
// Skipped when the caller already claimed the slot via claimAgentCreationSlot().
|
|
507
|
+
if (worldPending.has(agentId)) {
|
|
508
|
+
throw new Error(`Agent '${agentId}' is already being created`);
|
|
509
|
+
}
|
|
510
|
+
worldPending.add(agentId);
|
|
442
511
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
512
|
+
try {
|
|
513
|
+
const exists = await storageWrappers.agentExists(resolvedWorldId, agentId);
|
|
514
|
+
if (exists) {
|
|
515
|
+
throw new Error(`Agent with ID '${agentId}' already exists`);
|
|
516
|
+
}
|
|
517
|
+
const now = new Date();
|
|
518
|
+
const agent = {
|
|
519
|
+
id: agentId,
|
|
520
|
+
name: params.name,
|
|
521
|
+
type: params.type,
|
|
522
|
+
autoReply: params.autoReply ?? true,
|
|
523
|
+
status: 'inactive',
|
|
524
|
+
provider: params.provider,
|
|
525
|
+
model: params.model,
|
|
526
|
+
systemPrompt: params.systemPrompt,
|
|
527
|
+
temperature: params.temperature,
|
|
528
|
+
maxTokens: params.maxTokens,
|
|
529
|
+
createdAt: now,
|
|
530
|
+
lastActive: now,
|
|
531
|
+
llmCallCount: 0,
|
|
532
|
+
memory: [],
|
|
533
|
+
};
|
|
534
|
+
await storageWrappers.saveAgent(resolvedWorldId, agent);
|
|
535
|
+
if (world) {
|
|
536
|
+
world.agents.set(agent.id, agent);
|
|
537
|
+
}
|
|
538
|
+
return agent;
|
|
539
|
+
}
|
|
540
|
+
finally {
|
|
541
|
+
// Clean up the slot whether it was claimed here or via claimAgentCreationSlot().
|
|
542
|
+
worldPending.delete(agentId);
|
|
543
|
+
if (worldPending.size === 0) {
|
|
544
|
+
pendingAgentCreates.delete(resolvedWorldId);
|
|
545
|
+
}
|
|
465
546
|
}
|
|
466
|
-
return agent;
|
|
467
547
|
}
|
|
468
548
|
/**
|
|
469
549
|
* Load agent by ID with full configuration and memory
|
|
@@ -509,17 +589,14 @@ export async function updateAgent(worldId, agentId, updates) {
|
|
|
509
589
|
lastActive: new Date()
|
|
510
590
|
};
|
|
511
591
|
await storageWrappers.saveAgent(resolvedWorldId, updatedAgent);
|
|
512
|
-
// Emit CRUD event for real-time updates
|
|
513
592
|
if (world) {
|
|
514
593
|
const runtimeAgent = world.agents.get(resolvedAgentId);
|
|
515
594
|
if (runtimeAgent) {
|
|
516
595
|
Object.assign(runtimeAgent, updatedAgent);
|
|
517
596
|
world.agents.set(resolvedAgentId, runtimeAgent);
|
|
518
|
-
publishCRUDEvent(world, 'update', 'agent', resolvedAgentId, runtimeAgent);
|
|
519
597
|
}
|
|
520
598
|
else {
|
|
521
599
|
world.agents.set(resolvedAgentId, updatedAgent);
|
|
522
|
-
publishCRUDEvent(world, 'update', 'agent', resolvedAgentId, updatedAgent);
|
|
523
600
|
}
|
|
524
601
|
}
|
|
525
602
|
return updatedAgent;
|
|
@@ -539,10 +616,15 @@ export async function deleteAgent(worldId, agentId) {
|
|
|
539
616
|
throw new Error('Cannot delete agent while world is processing');
|
|
540
617
|
}
|
|
541
618
|
const success = await storageWrappers.deleteAgent(resolvedWorldId, resolvedAgentId);
|
|
542
|
-
// Emit CRUD event for real-time updates
|
|
543
619
|
if (success && world) {
|
|
620
|
+
// Remove the agent's message listener BEFORE removing from the agents map
|
|
621
|
+
// to prevent the stale closure from continuing to process messages.
|
|
622
|
+
const unsubscribe = world._agentUnsubscribers?.get(resolvedAgentId);
|
|
623
|
+
if (unsubscribe) {
|
|
624
|
+
unsubscribe();
|
|
625
|
+
world._agentUnsubscribers.delete(resolvedAgentId);
|
|
626
|
+
}
|
|
544
627
|
world.agents.delete(resolvedAgentId);
|
|
545
|
-
publishCRUDEvent(world, 'delete', 'agent', resolvedAgentId);
|
|
546
628
|
}
|
|
547
629
|
return success;
|
|
548
630
|
}
|
|
@@ -648,18 +730,16 @@ async function createChat(worldId, params) {
|
|
|
648
730
|
const chatData = {
|
|
649
731
|
id: chatId,
|
|
650
732
|
worldId,
|
|
651
|
-
name:
|
|
733
|
+
name: NEW_CHAT_TITLE,
|
|
652
734
|
description: params.description,
|
|
653
735
|
createdAt: now,
|
|
654
736
|
updatedAt: now,
|
|
655
737
|
messageCount: 0,
|
|
656
738
|
};
|
|
657
739
|
await storageWrappers.saveChatData(worldId, chatData);
|
|
658
|
-
// Emit CRUD event for real-time updates
|
|
659
740
|
const world = await getWorld(worldId);
|
|
660
741
|
if (world) {
|
|
661
742
|
world.chats.set(chatData.id, chatData);
|
|
662
|
-
publishCRUDEvent(world, 'create', 'chat', chatData.id, chatData, chatData.id);
|
|
663
743
|
}
|
|
664
744
|
return chatData;
|
|
665
745
|
}
|
|
@@ -675,7 +755,8 @@ export async function newChat(worldId) {
|
|
|
675
755
|
if (existingChat) {
|
|
676
756
|
const messages = await storageWrappers.getMemory(resolvedWorldId, existingChat.id);
|
|
677
757
|
if (messages.length === 0) {
|
|
678
|
-
|
|
758
|
+
await setPersistedCurrentChatId(resolvedWorldId, existingChat.id);
|
|
759
|
+
return await getWorld(resolvedWorldId);
|
|
679
760
|
}
|
|
680
761
|
// If chat has messages, fall through to create a new one
|
|
681
762
|
}
|
|
@@ -683,7 +764,8 @@ export async function newChat(worldId) {
|
|
|
683
764
|
name: NEW_CHAT_TITLE,
|
|
684
765
|
captureChat: false
|
|
685
766
|
});
|
|
686
|
-
|
|
767
|
+
await setPersistedCurrentChatId(resolvedWorldId, chatData.id);
|
|
768
|
+
return await getWorld(resolvedWorldId);
|
|
687
769
|
}
|
|
688
770
|
/**
|
|
689
771
|
* Create a branched chat from a source chat up to (and including) the provided message.
|
|
@@ -740,13 +822,10 @@ export async function branchChatFromMessage(worldId, sourceChatId, messageId) {
|
|
|
740
822
|
};
|
|
741
823
|
const cutoffTimestamp = toEpochMillis(targetMessage?.createdAt);
|
|
742
824
|
const updatedWorld = await newChat(resolvedWorldId);
|
|
743
|
-
|
|
825
|
+
const newChatId = await getPersistedCurrentChatId(resolvedWorldId);
|
|
826
|
+
if (!updatedWorld || !newChatId) {
|
|
744
827
|
throw new Error('Failed to create branch chat.');
|
|
745
828
|
}
|
|
746
|
-
const newChatId = String(updatedWorld.currentChatId || '').trim();
|
|
747
|
-
if (!newChatId) {
|
|
748
|
-
throw new Error('Failed to resolve new chat ID for branch.');
|
|
749
|
-
}
|
|
750
829
|
let copiedMessageCount = 0;
|
|
751
830
|
const agents = await listAgents(resolvedWorldId);
|
|
752
831
|
try {
|
|
@@ -813,6 +892,11 @@ export async function listChats(worldId) {
|
|
|
813
892
|
export async function updateChat(worldId, chatId, updates) {
|
|
814
893
|
await ensureInitialization();
|
|
815
894
|
const resolvedWorldId = await getResolvedWorldId(worldId);
|
|
895
|
+
// When the caller explicitly sets a name, treat it as a manual title so the
|
|
896
|
+
// auto-title scheduler will not overwrite it.
|
|
897
|
+
if (typeof updates.name === 'string' && updates.name.trim().length > 0) {
|
|
898
|
+
updates = { ...updates, titleProvenance: TITLE_PROVENANCE_MANUAL };
|
|
899
|
+
}
|
|
816
900
|
const chat = await storageWrappers.updateChatData(resolvedWorldId, chatId, updates);
|
|
817
901
|
if (!chat) {
|
|
818
902
|
return null;
|
|
@@ -833,16 +917,10 @@ export async function deleteChat(worldId, chatId) {
|
|
|
833
917
|
// First, delete all agent memory items associated with this chat
|
|
834
918
|
const deletedMemoryCount = await storageWrappers.deleteMemoryByChatId(resolvedWorldId, chatId);
|
|
835
919
|
logger.debug('Deleted memory items for chat', { worldId, resolvedWorldId, chatId, deletedMemoryCount });
|
|
836
|
-
|
|
920
|
+
const persistedCurrentChatId = await getPersistedCurrentChatId(resolvedWorldId);
|
|
921
|
+
// Get the world to update in-memory chat cache only
|
|
837
922
|
const world = await getWorld(resolvedWorldId);
|
|
838
|
-
|
|
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
|
-
}
|
|
923
|
+
const shouldSetNewCurrentChat = persistedCurrentChatId === chatId;
|
|
846
924
|
// Then delete the chat itself
|
|
847
925
|
const chatDeleted = await storageWrappers.deleteChatData(resolvedWorldId, chatId);
|
|
848
926
|
// Remove from world's in-memory chat map
|
|
@@ -855,7 +933,7 @@ export async function deleteChat(worldId, chatId) {
|
|
|
855
933
|
if (remainingChats.length > 0) {
|
|
856
934
|
// Set the most recently updated chat as current
|
|
857
935
|
const latestChat = remainingChats.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())[0];
|
|
858
|
-
await
|
|
936
|
+
await setPersistedCurrentChatId(resolvedWorldId, latestChat.id);
|
|
859
937
|
}
|
|
860
938
|
else {
|
|
861
939
|
// No chats left, create a new one
|
|
@@ -865,14 +943,64 @@ export async function deleteChat(worldId, chatId) {
|
|
|
865
943
|
}
|
|
866
944
|
return chatDeleted;
|
|
867
945
|
}
|
|
868
|
-
|
|
946
|
+
async function activateChatResources(world, resolvedWorldId, chatId, options) {
|
|
947
|
+
await syncRuntimeAgentMemoryFromStorage(world, resolvedWorldId);
|
|
948
|
+
loggerRestore.debug('Restore chat memory sync complete', { worldId: world.id, chatId });
|
|
949
|
+
const memoryForApprovals = await storageWrappers.getMemory(resolvedWorldId, chatId);
|
|
950
|
+
clearChatSkillApprovals(resolvedWorldId, chatId);
|
|
951
|
+
reconstructSkillApprovalsFromMessages(resolvedWorldId, chatId, Array.isArray(memoryForApprovals) ? memoryForApprovals : []);
|
|
952
|
+
const { getActiveSubscribedWorld } = await import('./subscription.js');
|
|
953
|
+
const runtimeWorld = getActiveSubscribedWorld(resolvedWorldId, chatId) || world;
|
|
954
|
+
const replayedHitlPromptCount = replayPendingHitlRequests(runtimeWorld, chatId);
|
|
955
|
+
loggerRestore.debug('Restore chat pending HITL replay triggered', { worldId: world.id, chatId });
|
|
956
|
+
if (options?.suppressAutoResume === true) {
|
|
957
|
+
loggerRestore.debug('Restore chat auto-resume suppressed for mutation flow', {
|
|
958
|
+
worldId: world.id,
|
|
959
|
+
chatId,
|
|
960
|
+
});
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (replayedHitlPromptCount > 0) {
|
|
964
|
+
loggerRestore.debug('Restore chat auto-resume skipped because chat is awaiting HITL input', {
|
|
965
|
+
worldId: world.id,
|
|
966
|
+
chatId,
|
|
967
|
+
replayedHitlPromptCount,
|
|
968
|
+
});
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
triggerPendingToolCallResumeFromLastMessage(runtimeWorld, chatId);
|
|
972
|
+
clearQueuePauseForChat(runtimeWorld.id, chatId);
|
|
973
|
+
triggerPendingQueueResume(runtimeWorld, chatId, { recoverStaleSending: true });
|
|
974
|
+
}
|
|
975
|
+
export async function restoreChat(worldId, chatId, options) {
|
|
869
976
|
await ensureInitialization();
|
|
870
977
|
const resolvedWorldId = await getResolvedWorldId(worldId);
|
|
978
|
+
const restoreStartedAt = Date.now();
|
|
979
|
+
loggerRestore.debug('Restore chat started', {
|
|
980
|
+
worldId: resolvedWorldId,
|
|
981
|
+
requestedChatId: chatId,
|
|
982
|
+
});
|
|
871
983
|
let world = await getWorld(resolvedWorldId);
|
|
984
|
+
const persistedCurrentChatId = await getPersistedCurrentChatId(resolvedWorldId);
|
|
872
985
|
if (!world) {
|
|
986
|
+
loggerRestore.debug('Restore chat aborted: world missing', {
|
|
987
|
+
worldId: resolvedWorldId,
|
|
988
|
+
requestedChatId: chatId,
|
|
989
|
+
});
|
|
873
990
|
return null;
|
|
874
991
|
}
|
|
875
|
-
if (
|
|
992
|
+
if (persistedCurrentChatId === chatId) {
|
|
993
|
+
loggerRestore.debug('Restore chat detected already-current chat', {
|
|
994
|
+
worldId: world.id,
|
|
995
|
+
chatId,
|
|
996
|
+
action: 'sync-memory+resume',
|
|
997
|
+
});
|
|
998
|
+
await activateChatResources(world, resolvedWorldId, chatId, options);
|
|
999
|
+
loggerRestore.debug('Restore chat resume inspection triggered', {
|
|
1000
|
+
worldId: world.id,
|
|
1001
|
+
chatId,
|
|
1002
|
+
elapsedMs: Date.now() - restoreStartedAt,
|
|
1003
|
+
});
|
|
876
1004
|
return world;
|
|
877
1005
|
}
|
|
878
1006
|
const runtimeChatExists = world.chats.has(chatId);
|
|
@@ -880,13 +1008,81 @@ export async function restoreChat(worldId, chatId) {
|
|
|
880
1008
|
? true
|
|
881
1009
|
: !!(await storageWrappers.loadChatData(resolvedWorldId, chatId));
|
|
882
1010
|
if (!persistedChatExists) {
|
|
1011
|
+
loggerRestore.debug('Restore chat aborted: chat missing', {
|
|
1012
|
+
worldId: resolvedWorldId,
|
|
1013
|
+
requestedChatId: chatId,
|
|
1014
|
+
runtimeChatExists,
|
|
1015
|
+
});
|
|
883
1016
|
return null;
|
|
884
1017
|
}
|
|
885
|
-
|
|
886
|
-
|
|
1018
|
+
// Auto-pause the old chat's queue when switching away (FR-8 safety)
|
|
1019
|
+
const previousChatId = persistedCurrentChatId;
|
|
1020
|
+
if (previousChatId && previousChatId !== chatId) {
|
|
1021
|
+
autoPauseQueueForChat(resolvedWorldId, previousChatId);
|
|
1022
|
+
loggerRestore.debug('Auto-paused queue for previous chat on switch', {
|
|
1023
|
+
worldId: resolvedWorldId,
|
|
1024
|
+
previousChatId,
|
|
1025
|
+
newChatId: chatId,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
loggerRestore.debug('Restore chat switching current chat', {
|
|
1029
|
+
worldId: resolvedWorldId,
|
|
1030
|
+
fromChatId: previousChatId,
|
|
1031
|
+
toChatId: chatId,
|
|
1032
|
+
});
|
|
1033
|
+
await setPersistedCurrentChatId(resolvedWorldId, chatId);
|
|
1034
|
+
world = await getWorld(resolvedWorldId);
|
|
1035
|
+
if (world) {
|
|
1036
|
+
await activateChatResources(world, resolvedWorldId, chatId, options);
|
|
1037
|
+
loggerRestore.debug('Restore chat resume inspection triggered', {
|
|
1038
|
+
worldId: world.id,
|
|
1039
|
+
chatId,
|
|
1040
|
+
elapsedMs: Date.now() - restoreStartedAt,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
loggerRestore.warn('Restore chat update-world returned null', {
|
|
1045
|
+
worldId: resolvedWorldId,
|
|
1046
|
+
requestedChatId: chatId,
|
|
1047
|
+
elapsedMs: Date.now() - restoreStartedAt,
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
loggerRestore.debug('Restore chat completed', {
|
|
1051
|
+
worldId: resolvedWorldId,
|
|
1052
|
+
requestedChatId: chatId,
|
|
1053
|
+
restoredCurrentChatId: await getPersistedCurrentChatId(resolvedWorldId),
|
|
1054
|
+
elapsedMs: Date.now() - restoreStartedAt,
|
|
887
1055
|
});
|
|
888
1056
|
return world;
|
|
889
1057
|
}
|
|
1058
|
+
export async function activateChatWithSnapshot(worldId, chatId) {
|
|
1059
|
+
const world = await restoreChat(worldId, chatId);
|
|
1060
|
+
if (!world) {
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
const resolvedChatId = String(chatId || '').trim();
|
|
1064
|
+
if (!resolvedChatId) {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
const memory = await storageWrappers.getMemory(world.id, resolvedChatId);
|
|
1068
|
+
const safeMemory = Array.isArray(memory) ? memory : [];
|
|
1069
|
+
// Message-authoritative: pending HITL state is derived from persisted messages only.
|
|
1070
|
+
// Runtime pending map serves transport/notification purposes; the snapshot always uses
|
|
1071
|
+
// messages as the single source of truth (AD-1, AD-4).
|
|
1072
|
+
const hitlPrompts = listPendingHitlPromptEventsFromMessages(safeMemory, resolvedChatId);
|
|
1073
|
+
loggerRestore.debug('Activate chat snapshot assembled', {
|
|
1074
|
+
worldId: world.id,
|
|
1075
|
+
chatId: resolvedChatId,
|
|
1076
|
+
memoryCount: safeMemory.length,
|
|
1077
|
+
hitlPromptCount: hitlPrompts.length,
|
|
1078
|
+
});
|
|
1079
|
+
return {
|
|
1080
|
+
world,
|
|
1081
|
+
chatId: resolvedChatId,
|
|
1082
|
+
memory: safeMemory,
|
|
1083
|
+
hitlPrompts,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
890
1086
|
export async function getMemory(worldId, chatId) {
|
|
891
1087
|
await ensureInitialization();
|
|
892
1088
|
const resolvedWorldId = await getResolvedWorldId(worldId);
|
|
@@ -894,7 +1090,10 @@ export async function getMemory(worldId, chatId) {
|
|
|
894
1090
|
if (!world) {
|
|
895
1091
|
return null;
|
|
896
1092
|
}
|
|
897
|
-
const resolvedChatId = chatId ||
|
|
1093
|
+
const resolvedChatId = String(chatId || '').trim();
|
|
1094
|
+
if (!resolvedChatId) {
|
|
1095
|
+
throw new Error('getMemory: chatId is required.');
|
|
1096
|
+
}
|
|
898
1097
|
const memory = await storageWrappers.getMemory(resolvedWorldId, resolvedChatId);
|
|
899
1098
|
// Auto-repair legacy memories so downstream clients can rely on messageId without UI fallbacks.
|
|
900
1099
|
if (memory.some(message => !message.messageId)) {
|
|
@@ -907,316 +1106,6 @@ export async function getMemory(worldId, chatId) {
|
|
|
907
1106
|
}
|
|
908
1107
|
return memory;
|
|
909
1108
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
-
}
|
|
1109
|
+
// Re-exports from extracted modules
|
|
1110
|
+
export { migrateMessageIds, removeMessagesFrom, editUserMessage, logEditError, getEditErrors } from './message-edit-manager.js';
|
|
1111
|
+
export { addToQueue, recoverQueueSendingMessages, enqueueAndProcessUserTurn, dispatchImmediateChatMessage, getQueueMessages, removeFromQueue, pauseChatQueue, resumeChatQueue, stopChatQueue, clearChatQueue, retryQueueMessage } from './queue-manager.js';
|