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
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Manager Module
|
|
3
|
+
*
|
|
4
|
+
* Purpose: Complete message queue management for per-chat ordered message dispatch.
|
|
5
|
+
*
|
|
6
|
+
* Key features:
|
|
7
|
+
* - Per-chat FIFO queue with pause/resume/stop/clear lifecycle
|
|
8
|
+
* - Explicit-recovery failure handling for user-authored turns
|
|
9
|
+
* - Responder preflight: checks eligible agents before marking a row 'sending';
|
|
10
|
+
* performs a single runtime refresh attempt when no responders are found
|
|
11
|
+
* - No-response timeout fallback: escalates to durable error if no response-start
|
|
12
|
+
* event arrives within QUEUE_NO_RESPONSE_FALLBACK_MS
|
|
13
|
+
* - World-advance event listener chaining: next queue item is triggered when the
|
|
14
|
+
* current chat's processing goes idle
|
|
15
|
+
* - Startup recovery: stale 'sending' rows are reset on module init
|
|
16
|
+
* - Queue state is scoped per-world+chat (no cross-world leakage)
|
|
17
|
+
*
|
|
18
|
+
* Notes:
|
|
19
|
+
* - subscription.js imports managers.ts, so getActiveSubscribedWorld must stay
|
|
20
|
+
* a dynamic import here to avoid a static circular dependency.
|
|
21
|
+
* - getWorld from managers.ts is also a dynamic import (managers re-exports this module).
|
|
22
|
+
* - All other dependencies (events/index.js, storage-init.ts, etc.) are static.
|
|
23
|
+
*
|
|
24
|
+
* Recent Changes:
|
|
25
|
+
* - 2026-03-14: Allowed `world` sender messages onto the queue so scheduled/system-originated
|
|
26
|
+
* world prompts use the same ordered ingress and mention preflight rules as human turns.
|
|
27
|
+
* - 2026-03-12: Registered queue advance listeners in the detachable listener map so sequential queued
|
|
28
|
+
* turns in the same chat can reattach completion cleanup after the previous turn finishes.
|
|
29
|
+
* - 2026-03-10: Treat unresolved persisted HITL prompts as explicit recovery boundaries during stale `sending` recovery so restore does not replay turns that are already waiting for approval.
|
|
30
|
+
* - 2026-03-10: Queue dispatch failures that occur before streaming starts now publish a durable structured `system/error` artifact so failed turns remain visible in the transcript.
|
|
31
|
+
* - 2026-03-10: Split queue-backed user submission from immediate non-user dispatch; queue APIs are now user-turn-only by contract.
|
|
32
|
+
* - 2026-03-10: Removed memory-based restore resend and automatic queue backoff replay for failed user turns.
|
|
33
|
+
* - 2026-03-09: Extracted from managers.ts as part of god-module decomposition.
|
|
34
|
+
* - triggerPendingUserMessageResume moved here (uses queue infrastructure).
|
|
35
|
+
* - autoPauseQueueForChat / clearQueuePauseForChat exported for managers.ts use.
|
|
36
|
+
*/
|
|
37
|
+
import { storageWrappers, ensureInitialization, getResolvedWorldId } from './storage-init.js';
|
|
38
|
+
import { RELIABILITY_CONFIG } from './reliability-config.js';
|
|
39
|
+
import { createCategoryLogger } from './logger.js';
|
|
40
|
+
import * as utils from './utils.js';
|
|
41
|
+
import { hasActiveChatMessageProcessing } from './message-processing-control.js';
|
|
42
|
+
import { publishMessageWithId, publishMessage, publishEvent, subscribeAgentToMessages, subscribeWorldToMessages, setupWorldActivityListener, } from './events/index.js';
|
|
43
|
+
import { listPendingHitlPromptEventsFromMessages } from './hitl.js';
|
|
44
|
+
import { nanoid } from 'nanoid';
|
|
45
|
+
const loggerQueue = createCategoryLogger('message.queue');
|
|
46
|
+
// ─── Queue management state ──────────────────────────────────────────────────
|
|
47
|
+
// keyed by `${worldId}:${chatId}`
|
|
48
|
+
const inFlightQueueResumeKeys = new Set();
|
|
49
|
+
export const pausedQueues = new Set();
|
|
50
|
+
const queueListenerActive = new Set();
|
|
51
|
+
const queueAdvanceListeners = new Map();
|
|
52
|
+
const queueResponderRefreshAttempted = new Set();
|
|
53
|
+
const QUEUE_NO_RESPONSE_FALLBACK_MS = RELIABILITY_CONFIG.queue.noResponseFallbackMs;
|
|
54
|
+
const queueDispatchStateByChat = new Map();
|
|
55
|
+
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
56
|
+
function getQueueKey(worldId, chatId) {
|
|
57
|
+
return `${worldId}:${chatId}`;
|
|
58
|
+
}
|
|
59
|
+
function getQueueMessageKey(worldId, chatId, messageId) {
|
|
60
|
+
return `${worldId}:${chatId}:${messageId}`;
|
|
61
|
+
}
|
|
62
|
+
function clearQueueResponderRefreshAttempt(worldId, chatId, messageId) {
|
|
63
|
+
queueResponderRefreshAttempted.delete(getQueueMessageKey(worldId, chatId, messageId));
|
|
64
|
+
}
|
|
65
|
+
export function getQueueStorageOrThrow(caller) {
|
|
66
|
+
const missingMethods = [];
|
|
67
|
+
if (!storageWrappers?.getQueuedMessages)
|
|
68
|
+
missingMethods.push('getQueuedMessages');
|
|
69
|
+
if (!storageWrappers?.addQueuedMessage)
|
|
70
|
+
missingMethods.push('addQueuedMessage');
|
|
71
|
+
if (!storageWrappers?.updateMessageQueueStatus)
|
|
72
|
+
missingMethods.push('updateMessageQueueStatus');
|
|
73
|
+
if (!storageWrappers?.incrementQueueMessageRetry)
|
|
74
|
+
missingMethods.push('incrementQueueMessageRetry');
|
|
75
|
+
if (!storageWrappers?.removeQueuedMessage)
|
|
76
|
+
missingMethods.push('removeQueuedMessage');
|
|
77
|
+
if (missingMethods.length > 0) {
|
|
78
|
+
throw new Error(`${caller}: queue storage backend missing required operations (${missingMethods.join(', ')}).`);
|
|
79
|
+
}
|
|
80
|
+
return storageWrappers;
|
|
81
|
+
}
|
|
82
|
+
function detachQueueAdvanceListener(world, chatId) {
|
|
83
|
+
const listenerKey = getQueueKey(world.id, chatId);
|
|
84
|
+
const listener = queueAdvanceListeners.get(listenerKey);
|
|
85
|
+
if (!listener)
|
|
86
|
+
return;
|
|
87
|
+
world.eventEmitter.removeListener('world', listener);
|
|
88
|
+
queueAdvanceListeners.delete(listenerKey);
|
|
89
|
+
queueListenerActive.delete(listenerKey);
|
|
90
|
+
}
|
|
91
|
+
function normalizeQueueMentionToken(value) {
|
|
92
|
+
return String(value || '')
|
|
93
|
+
.trim()
|
|
94
|
+
.toLowerCase()
|
|
95
|
+
.replace(/[^a-z0-9-_]/g, '-')
|
|
96
|
+
.replace(/-+/g, '-')
|
|
97
|
+
.replace(/^-|-$/g, '');
|
|
98
|
+
}
|
|
99
|
+
function resolveQueueMainAgentId(world) {
|
|
100
|
+
const rawMainAgent = String(world.mainAgent || '').trim();
|
|
101
|
+
if (!rawMainAgent)
|
|
102
|
+
return null;
|
|
103
|
+
const normalizedMainAgent = normalizeQueueMentionToken(rawMainAgent);
|
|
104
|
+
if (!normalizedMainAgent)
|
|
105
|
+
return null;
|
|
106
|
+
const agentMap = world?.agents instanceof Map ? world.agents : new Map();
|
|
107
|
+
if (agentMap.has(normalizedMainAgent))
|
|
108
|
+
return normalizedMainAgent;
|
|
109
|
+
for (const agent of agentMap.values()) {
|
|
110
|
+
const normalizedAgentId = normalizeQueueMentionToken(agent.id);
|
|
111
|
+
const normalizedAgentName = normalizeQueueMentionToken(agent.name || '');
|
|
112
|
+
if (normalizedAgentId === normalizedMainAgent || normalizedAgentName === normalizedMainAgent) {
|
|
113
|
+
return agent.id;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function collectQueueAgentStatus(world, chatId, content, sender) {
|
|
119
|
+
const agentMap = world?.agents instanceof Map ? world.agents : new Map();
|
|
120
|
+
const agents = Array.from(agentMap.values());
|
|
121
|
+
const totalAgents = agents.length;
|
|
122
|
+
const activeAgents = agents.filter((agent) => agent.status === 'active').length;
|
|
123
|
+
const autoReplyEnabledAgents = agents.filter((agent) => agent.autoReply !== false).length;
|
|
124
|
+
const subscribedAgentCount = world?._agentUnsubscribers instanceof Map
|
|
125
|
+
? world._agentUnsubscribers.size
|
|
126
|
+
: 0;
|
|
127
|
+
const messageListenerCount = typeof world?.eventEmitter?.listenerCount === 'function'
|
|
128
|
+
? world.eventEmitter.listenerCount('message')
|
|
129
|
+
: 0;
|
|
130
|
+
const worldListenerCount = typeof world?.eventEmitter?.listenerCount === 'function'
|
|
131
|
+
? world.eventEmitter.listenerCount('world')
|
|
132
|
+
: 0;
|
|
133
|
+
const sseListenerCount = typeof world?.eventEmitter?.listenerCount === 'function'
|
|
134
|
+
? world.eventEmitter.listenerCount('sse')
|
|
135
|
+
: 0;
|
|
136
|
+
const paragraphMentions = utils.extractParagraphBeginningMentions(content || '');
|
|
137
|
+
const anyMentions = utils.extractMentions(content || '');
|
|
138
|
+
const isUserSender = isUserQueueSender(sender);
|
|
139
|
+
const resolvedMainAgentId = resolveQueueMainAgentId(world);
|
|
140
|
+
const effectiveMentions = paragraphMentions.length === 0 && isUserSender && resolvedMainAgentId
|
|
141
|
+
? [normalizeQueueMentionToken(resolvedMainAgentId)]
|
|
142
|
+
: paragraphMentions.map((mention) => normalizeQueueMentionToken(mention));
|
|
143
|
+
let eligibleResponderAgentIds = [];
|
|
144
|
+
if (!isUserSender) {
|
|
145
|
+
eligibleResponderAgentIds = agents
|
|
146
|
+
.map((agent) => String(agent.id || '').trim())
|
|
147
|
+
.filter((agentId) => agentId.length > 0);
|
|
148
|
+
}
|
|
149
|
+
else if (effectiveMentions.length === 0 && anyMentions.length > 0) {
|
|
150
|
+
eligibleResponderAgentIds = [];
|
|
151
|
+
}
|
|
152
|
+
else if (effectiveMentions.length === 0) {
|
|
153
|
+
eligibleResponderAgentIds = agents
|
|
154
|
+
.map((agent) => String(agent.id || '').trim())
|
|
155
|
+
.filter((agentId) => agentId.length > 0);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
eligibleResponderAgentIds = agents
|
|
159
|
+
.filter((agent) => {
|
|
160
|
+
const normalizedAgentId = String(agent.id || '').toLowerCase().replace(/\s+/g, '-');
|
|
161
|
+
return effectiveMentions.includes(normalizedAgentId);
|
|
162
|
+
})
|
|
163
|
+
.map((agent) => String(agent.id || '').trim())
|
|
164
|
+
.filter((agentId) => agentId.length > 0);
|
|
165
|
+
}
|
|
166
|
+
let reasonHint = null;
|
|
167
|
+
if (totalAgents === 0) {
|
|
168
|
+
reasonHint = 'no-agents-loaded';
|
|
169
|
+
}
|
|
170
|
+
else if (messageListenerCount === 0) {
|
|
171
|
+
reasonHint = 'no-message-listeners';
|
|
172
|
+
}
|
|
173
|
+
else if (subscribedAgentCount === 0) {
|
|
174
|
+
reasonHint = 'no-agent-subscribers';
|
|
175
|
+
}
|
|
176
|
+
else if (eligibleResponderAgentIds.length === 0) {
|
|
177
|
+
reasonHint = 'no-eligible-responders-for-message';
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
queueChatId: chatId,
|
|
181
|
+
totalAgents,
|
|
182
|
+
activeAgents,
|
|
183
|
+
autoReplyEnabledAgents,
|
|
184
|
+
subscribedAgentCount,
|
|
185
|
+
messageListenerCount,
|
|
186
|
+
worldListenerCount,
|
|
187
|
+
sseListenerCount,
|
|
188
|
+
mainAgent: String(world.mainAgent || '').trim() || null,
|
|
189
|
+
resolvedMainAgentId,
|
|
190
|
+
paragraphMentions,
|
|
191
|
+
anyMentions,
|
|
192
|
+
effectiveMentions,
|
|
193
|
+
eligibleResponderAgentIds,
|
|
194
|
+
eligibleResponderCount: eligibleResponderAgentIds.length,
|
|
195
|
+
reasonHint,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function hasQueueResponderAvailability(status) {
|
|
199
|
+
return status.eligibleResponderCount > 0;
|
|
200
|
+
}
|
|
201
|
+
async function refreshQueueRespondersFromStorage(world) {
|
|
202
|
+
if (!storageWrappers?.listAgents) {
|
|
203
|
+
throw new Error('refreshQueueRespondersFromStorage: listAgents is unavailable.');
|
|
204
|
+
}
|
|
205
|
+
if (world?._agentUnsubscribers instanceof Map) {
|
|
206
|
+
for (const unsubscribe of world._agentUnsubscribers.values()) {
|
|
207
|
+
try {
|
|
208
|
+
unsubscribe();
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// best effort cleanup
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
world._agentUnsubscribers.clear();
|
|
215
|
+
}
|
|
216
|
+
const persistedAgents = await storageWrappers.listAgents(world.id);
|
|
217
|
+
world.agents = new Map((persistedAgents || []).map((agent) => [agent.id, agent]));
|
|
218
|
+
for (const agent of world.agents.values()) {
|
|
219
|
+
subscribeAgentToMessages(world, agent);
|
|
220
|
+
}
|
|
221
|
+
subscribeWorldToMessages(world);
|
|
222
|
+
setupWorldActivityListener(world);
|
|
223
|
+
}
|
|
224
|
+
async function runQueueResponderPreflight(world, chatId, queuedMessage) {
|
|
225
|
+
let agentStatus = collectQueueAgentStatus(world, chatId, queuedMessage.content, queuedMessage.sender);
|
|
226
|
+
if (hasQueueResponderAvailability(agentStatus)) {
|
|
227
|
+
return { ready: true, agentStatus, refreshed: false };
|
|
228
|
+
}
|
|
229
|
+
const messageKey = getQueueMessageKey(world.id, chatId, queuedMessage.messageId);
|
|
230
|
+
if (queueResponderRefreshAttempted.has(messageKey)) {
|
|
231
|
+
return { ready: false, agentStatus, refreshed: false };
|
|
232
|
+
}
|
|
233
|
+
queueResponderRefreshAttempted.add(messageKey);
|
|
234
|
+
loggerQueue.warn('Queue responder preflight detected no eligible responders; attempting one runtime refresh', {
|
|
235
|
+
worldId: world.id,
|
|
236
|
+
chatId,
|
|
237
|
+
messageId: queuedMessage.messageId,
|
|
238
|
+
agentStatus,
|
|
239
|
+
});
|
|
240
|
+
try {
|
|
241
|
+
await refreshQueueRespondersFromStorage(world);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
loggerQueue.warn('Queue responder preflight refresh failed', {
|
|
245
|
+
worldId: world.id,
|
|
246
|
+
chatId,
|
|
247
|
+
messageId: queuedMessage.messageId,
|
|
248
|
+
error: error instanceof Error ? error.message : String(error),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
agentStatus = collectQueueAgentStatus(world, chatId, queuedMessage.content, queuedMessage.sender);
|
|
252
|
+
if (hasQueueResponderAvailability(agentStatus)) {
|
|
253
|
+
return { ready: true, agentStatus, refreshed: true };
|
|
254
|
+
}
|
|
255
|
+
return { ready: false, agentStatus, refreshed: true };
|
|
256
|
+
}
|
|
257
|
+
async function resolveStaleSendingRecoveryDecision(world, chatId, messageId) {
|
|
258
|
+
const eventStorage = world.eventStorage;
|
|
259
|
+
if (!eventStorage || typeof eventStorage.getEventsByWorldAndChat !== 'function') {
|
|
260
|
+
return {
|
|
261
|
+
action: 'recover',
|
|
262
|
+
reason: 'no-event-storage',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const latestMessageEvents = await eventStorage.getEventsByWorldAndChat(world.id, chatId, {
|
|
266
|
+
types: ['message'],
|
|
267
|
+
order: 'desc',
|
|
268
|
+
limit: 1,
|
|
269
|
+
});
|
|
270
|
+
const latestMessageEvent = latestMessageEvents[0] ?? null;
|
|
271
|
+
if (!latestMessageEvent || typeof latestMessageEvent.seq !== 'number') {
|
|
272
|
+
return {
|
|
273
|
+
action: 'recover',
|
|
274
|
+
reason: 'message-event-missing',
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (String(latestMessageEvent.id || '').trim() !== messageId) {
|
|
278
|
+
return {
|
|
279
|
+
action: 'mark-error',
|
|
280
|
+
reason: `superseded-by-message:${String(latestMessageEvent.id || '').trim() || 'unknown'}`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const currentMemory = typeof storageWrappers?.getMemory === 'function'
|
|
284
|
+
? await storageWrappers.getMemory(world.id, chatId)
|
|
285
|
+
: [];
|
|
286
|
+
const pendingHitlPrompts = listPendingHitlPromptEventsFromMessages(Array.isArray(currentMemory) ? currentMemory : [], chatId);
|
|
287
|
+
if (pendingHitlPrompts.length > 0) {
|
|
288
|
+
return {
|
|
289
|
+
action: 'mark-error',
|
|
290
|
+
reason: `pending-hitl:${pendingHitlPrompts[0]?.prompt?.requestId || 'unknown'}`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const latestSseEvents = await eventStorage.getEventsByWorldAndChat(world.id, chatId, {
|
|
294
|
+
types: ['sse'],
|
|
295
|
+
order: 'desc',
|
|
296
|
+
limit: 1,
|
|
297
|
+
});
|
|
298
|
+
const latestPostMessageSse = latestSseEvents[0] ?? null;
|
|
299
|
+
if (!latestPostMessageSse
|
|
300
|
+
|| typeof latestPostMessageSse.seq !== 'number'
|
|
301
|
+
|| latestPostMessageSse.seq <= latestMessageEvent.seq) {
|
|
302
|
+
return {
|
|
303
|
+
action: 'recover',
|
|
304
|
+
reason: 'no-post-message-sse',
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const latestSseType = String(latestPostMessageSse.payload?.type || '').trim().toLowerCase();
|
|
308
|
+
if (latestSseType === 'error' || latestSseType === 'end') {
|
|
309
|
+
return {
|
|
310
|
+
action: 'mark-error',
|
|
311
|
+
reason: `terminal-sse:${latestSseType}`,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
action: 'recover',
|
|
316
|
+
reason: latestSseType === 'start' ? 'interrupted-after-start' : `non-terminal-sse:${latestSseType || 'unknown'}`,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
async function handleQueueDispatchFailure(world, chatId, messageId, reason, context) {
|
|
320
|
+
const queueKey = getQueueKey(world.id, chatId);
|
|
321
|
+
clearQueueResponderRefreshAttempt(world.id, chatId, messageId);
|
|
322
|
+
queueDispatchStateByChat.delete(queueKey);
|
|
323
|
+
detachQueueAdvanceListener(world, chatId);
|
|
324
|
+
const agentStatus = collectQueueAgentStatus(world, chatId, String(context?.content || ''), String(context?.sender || 'human'));
|
|
325
|
+
let queueStorage;
|
|
326
|
+
try {
|
|
327
|
+
queueStorage = getQueueStorageOrThrow('handleQueueDispatchFailure');
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
loggerQueue.error('Queue dispatch failure could not be recorded', {
|
|
331
|
+
worldId: world.id,
|
|
332
|
+
chatId,
|
|
333
|
+
messageId,
|
|
334
|
+
reason,
|
|
335
|
+
error: error instanceof Error ? error.message : String(error),
|
|
336
|
+
});
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
const newRetryCount = await queueStorage.incrementQueueMessageRetry(messageId);
|
|
341
|
+
await queueStorage.updateMessageQueueStatus(messageId, 'error');
|
|
342
|
+
world._queuedChatIds?.delete(chatId);
|
|
343
|
+
publishEvent(world, 'system', {
|
|
344
|
+
type: 'error',
|
|
345
|
+
eventType: 'error',
|
|
346
|
+
message: `Queue failed to dispatch user turn: ${reason}.`,
|
|
347
|
+
triggeringMessageId: messageId,
|
|
348
|
+
failureKind: 'queue-dispatch',
|
|
349
|
+
}, chatId);
|
|
350
|
+
loggerQueue.warn('Queue dispatch failed; message marked error for explicit recovery', {
|
|
351
|
+
worldId: world.id,
|
|
352
|
+
chatId,
|
|
353
|
+
messageId,
|
|
354
|
+
reason,
|
|
355
|
+
retryCount: newRetryCount,
|
|
356
|
+
agentStatus,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
catch (retryErr) {
|
|
360
|
+
loggerQueue.error('Failed to handle queue dispatch retry transition', {
|
|
361
|
+
worldId: world.id,
|
|
362
|
+
chatId,
|
|
363
|
+
messageId,
|
|
364
|
+
reason,
|
|
365
|
+
error: String(retryErr),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function attachQueueAdvanceListener(world, chatId) {
|
|
370
|
+
const listenerKey = getQueueKey(world.id, chatId);
|
|
371
|
+
if (queueListenerActive.has(listenerKey))
|
|
372
|
+
return;
|
|
373
|
+
queueListenerActive.add(listenerKey);
|
|
374
|
+
function onWorldActivity(payload) {
|
|
375
|
+
if (payload.chatId !== chatId)
|
|
376
|
+
return;
|
|
377
|
+
const queueDispatchState = queueDispatchStateByChat.get(listenerKey);
|
|
378
|
+
if (payload.type === 'response-start') {
|
|
379
|
+
if (queueDispatchState) {
|
|
380
|
+
queueDispatchState.responseStarted = true;
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (payload.type !== 'idle' && payload.type !== 'response-end')
|
|
385
|
+
return;
|
|
386
|
+
const activeChatIds = payload.activeChatIds || [];
|
|
387
|
+
if (activeChatIds.includes(chatId))
|
|
388
|
+
return; // this chat is still processing
|
|
389
|
+
// Chat processing just completed — clean up listener
|
|
390
|
+
detachQueueAdvanceListener(world, chatId);
|
|
391
|
+
void (async () => {
|
|
392
|
+
const inFlightMessageId = queueDispatchStateByChat.get(listenerKey)?.messageId || null;
|
|
393
|
+
queueDispatchStateByChat.delete(listenerKey);
|
|
394
|
+
try {
|
|
395
|
+
if (!inFlightMessageId) {
|
|
396
|
+
loggerQueue.warn('Queue completion observed without tracked in-flight message', {
|
|
397
|
+
worldId: world.id,
|
|
398
|
+
chatId,
|
|
399
|
+
});
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const queueStorage = getQueueStorageOrThrow('attachQueueAdvanceListener');
|
|
403
|
+
const messages = await queueStorage.getQueuedMessages(world.id, chatId);
|
|
404
|
+
const sendingMsg = messages?.find((m) => m.messageId === inFlightMessageId && m.status === 'sending');
|
|
405
|
+
if (sendingMsg) {
|
|
406
|
+
// Successful completion — remove from queue (message now lives in agent_memory)
|
|
407
|
+
await queueStorage.removeQueuedMessage(sendingMsg.messageId);
|
|
408
|
+
clearQueueResponderRefreshAttempt(world.id, chatId, sendingMsg.messageId);
|
|
409
|
+
// Safety: ensure chatId is not left in the queued cache
|
|
410
|
+
world._queuedChatIds?.delete(chatId);
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
loggerQueue.warn('Queue completion observed but no matching sending row found', {
|
|
414
|
+
worldId: world.id,
|
|
415
|
+
chatId,
|
|
416
|
+
messageId: inFlightMessageId,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
loggerQueue.warn('Failed to mark queued message complete', {
|
|
422
|
+
worldId: world.id,
|
|
423
|
+
chatId,
|
|
424
|
+
messageId: inFlightMessageId,
|
|
425
|
+
error: String(err),
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
// Chain: trigger the next queued message
|
|
429
|
+
triggerPendingQueueResume(world, chatId);
|
|
430
|
+
})();
|
|
431
|
+
}
|
|
432
|
+
queueAdvanceListeners.set(listenerKey, onWorldActivity);
|
|
433
|
+
world.eventEmitter.on('world', onWorldActivity);
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Guardrail for worlds/chats where a queued human message is published but no
|
|
437
|
+
* responder starts processing. In that case no world idle/response-end event is
|
|
438
|
+
* emitted, so we transition the queue row to explicit recovery error state.
|
|
439
|
+
*/
|
|
440
|
+
function scheduleQueueNoResponseFallback(world, chatId, messageId) {
|
|
441
|
+
setTimeout(() => {
|
|
442
|
+
void (async () => {
|
|
443
|
+
try {
|
|
444
|
+
const queueStorage = getQueueStorageOrThrow('scheduleQueueNoResponseFallback');
|
|
445
|
+
const queueKey = getQueueKey(world.id, chatId);
|
|
446
|
+
const queueDispatchState = queueDispatchStateByChat.get(queueKey);
|
|
447
|
+
if (!queueDispatchState || queueDispatchState.messageId !== messageId)
|
|
448
|
+
return;
|
|
449
|
+
if (queueDispatchState.responseStarted) {
|
|
450
|
+
loggerQueue.debug('Queue fallback skipped because response-start was observed', {
|
|
451
|
+
worldId: world.id,
|
|
452
|
+
chatId,
|
|
453
|
+
messageId,
|
|
454
|
+
agentStatus: collectQueueAgentStatus(world, chatId, '', 'human'),
|
|
455
|
+
});
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (hasActiveChatMessageProcessing(world.id, chatId))
|
|
459
|
+
return;
|
|
460
|
+
const messages = await queueStorage.getQueuedMessages(world.id, chatId);
|
|
461
|
+
const sendingMessage = messages?.find((m) => m.messageId === messageId && m.status === 'sending');
|
|
462
|
+
if (!sendingMessage)
|
|
463
|
+
return;
|
|
464
|
+
await handleQueueDispatchFailure(world, chatId, messageId, 'no-response-timeout', {
|
|
465
|
+
content: sendingMessage.content,
|
|
466
|
+
sender: sendingMessage.sender,
|
|
467
|
+
});
|
|
468
|
+
loggerQueue.warn('Queue fallback marked message error after no responder start', {
|
|
469
|
+
worldId: world.id,
|
|
470
|
+
chatId,
|
|
471
|
+
messageId,
|
|
472
|
+
agentStatus: collectQueueAgentStatus(world, chatId, sendingMessage.content, sendingMessage.sender),
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
loggerQueue.warn('Queue fallback cleanup failed', {
|
|
477
|
+
worldId: world.id,
|
|
478
|
+
chatId,
|
|
479
|
+
messageId,
|
|
480
|
+
error: String(err),
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
})();
|
|
484
|
+
}, QUEUE_NO_RESPONSE_FALLBACK_MS);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Core queue processing trigger. Finds the next 'queued' message for the chat,
|
|
488
|
+
* marks it 'sending', and publishes it. Registers a world-activity listener to
|
|
489
|
+
* chain to the next message after response completion.
|
|
490
|
+
*
|
|
491
|
+
* Guards: pause flag, active-processing check, per-chat dedup.
|
|
492
|
+
*/
|
|
493
|
+
export function triggerPendingQueueResume(world, chatId, options) {
|
|
494
|
+
if (!chatId)
|
|
495
|
+
return;
|
|
496
|
+
const queueKey = getQueueKey(world.id, chatId);
|
|
497
|
+
if (pausedQueues.has(queueKey)) {
|
|
498
|
+
loggerQueue.debug('Queue processing skipped: paused', { worldId: world.id, chatId });
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (hasActiveChatMessageProcessing(world.id, chatId)) {
|
|
502
|
+
// An agent is actively processing — the advance listener will pick up when done
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (inFlightQueueResumeKeys.has(queueKey)) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
inFlightQueueResumeKeys.add(queueKey);
|
|
509
|
+
void (async () => {
|
|
510
|
+
let nextMessage;
|
|
511
|
+
const recoverStaleSending = options?.recoverStaleSending === true;
|
|
512
|
+
try {
|
|
513
|
+
const queueStorage = getQueueStorageOrThrow('triggerPendingQueueResume');
|
|
514
|
+
let messages = await queueStorage.getQueuedMessages(world.id, chatId);
|
|
515
|
+
const staleSendingMessages = recoverStaleSending
|
|
516
|
+
? messages?.filter((m) => m.status === 'sending') ?? []
|
|
517
|
+
: [];
|
|
518
|
+
if (staleSendingMessages.length > 0) {
|
|
519
|
+
loggerQueue.warn('Detected stale sending queue messages during resume; evaluating recovery state', {
|
|
520
|
+
worldId: world.id,
|
|
521
|
+
chatId,
|
|
522
|
+
count: staleSendingMessages.length,
|
|
523
|
+
messageIds: staleSendingMessages.map((m) => m.messageId),
|
|
524
|
+
});
|
|
525
|
+
for (const staleMessage of staleSendingMessages) {
|
|
526
|
+
const decision = await resolveStaleSendingRecoveryDecision(world, chatId, staleMessage.messageId);
|
|
527
|
+
if (decision.action === 'mark-error') {
|
|
528
|
+
await queueStorage.updateMessageQueueStatus(staleMessage.messageId, 'error');
|
|
529
|
+
loggerQueue.debug('Marked stale sending queue message error during resume', {
|
|
530
|
+
worldId: world.id,
|
|
531
|
+
chatId,
|
|
532
|
+
messageId: staleMessage.messageId,
|
|
533
|
+
reason: decision.reason,
|
|
534
|
+
});
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
await queueStorage.updateMessageQueueStatus(staleMessage.messageId, 'queued');
|
|
538
|
+
loggerQueue.debug('Recovered stale sending queue message back to queued during resume', {
|
|
539
|
+
worldId: world.id,
|
|
540
|
+
chatId,
|
|
541
|
+
messageId: staleMessage.messageId,
|
|
542
|
+
reason: decision.reason,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
messages = await queueStorage.getQueuedMessages(world.id, chatId);
|
|
546
|
+
}
|
|
547
|
+
nextMessage = messages?.find((m) => m.status === 'queued');
|
|
548
|
+
if (!nextMessage) {
|
|
549
|
+
loggerQueue.debug('No queued messages remaining', { worldId: world.id, chatId });
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const preflight = await runQueueResponderPreflight(world, chatId, {
|
|
553
|
+
messageId: nextMessage.messageId,
|
|
554
|
+
content: nextMessage.content,
|
|
555
|
+
sender: nextMessage.sender,
|
|
556
|
+
});
|
|
557
|
+
if (!preflight.ready) {
|
|
558
|
+
await handleQueueDispatchFailure(world, chatId, nextMessage.messageId, 'no-responder-preflight', {
|
|
559
|
+
content: nextMessage.content,
|
|
560
|
+
sender: nextMessage.sender,
|
|
561
|
+
});
|
|
562
|
+
loggerQueue.warn('Queue dispatch blocked: no eligible responders after preflight refresh; moved to explicit recovery error state', {
|
|
563
|
+
worldId: world.id,
|
|
564
|
+
chatId,
|
|
565
|
+
messageId: nextMessage.messageId,
|
|
566
|
+
refreshed: preflight.refreshed,
|
|
567
|
+
reason: 'no-responder-preflight',
|
|
568
|
+
agentStatus: preflight.agentStatus,
|
|
569
|
+
});
|
|
570
|
+
clearQueueResponderRefreshAttempt(world.id, chatId, nextMessage.messageId);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
await queueStorage.updateMessageQueueStatus(nextMessage.messageId, 'sending');
|
|
574
|
+
// Remove from queued cache: this chat is now transitioning to active processing
|
|
575
|
+
world._queuedChatIds?.delete(chatId);
|
|
576
|
+
queueDispatchStateByChat.set(queueKey, {
|
|
577
|
+
messageId: nextMessage.messageId,
|
|
578
|
+
responseStarted: false,
|
|
579
|
+
dispatchedAt: Date.now(),
|
|
580
|
+
});
|
|
581
|
+
// Register listener BEFORE publishing so the idle event is never missed
|
|
582
|
+
attachQueueAdvanceListener(world, chatId);
|
|
583
|
+
loggerQueue.debug('Publishing queued message', {
|
|
584
|
+
worldId: world.id,
|
|
585
|
+
chatId,
|
|
586
|
+
messageId: nextMessage.messageId,
|
|
587
|
+
agentStatus: preflight.agentStatus,
|
|
588
|
+
});
|
|
589
|
+
publishMessageWithId(world, nextMessage.content, nextMessage.sender, nextMessage.messageId, chatId);
|
|
590
|
+
scheduleQueueNoResponseFallback(world, chatId, nextMessage.messageId);
|
|
591
|
+
}
|
|
592
|
+
catch (error) {
|
|
593
|
+
if (nextMessage?.messageId) {
|
|
594
|
+
void handleQueueDispatchFailure(world, chatId, nextMessage.messageId, error instanceof Error ? error.message : String(error), {
|
|
595
|
+
content: nextMessage.content,
|
|
596
|
+
sender: nextMessage.sender,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
loggerQueue.warn('Failed to publish queued message', {
|
|
600
|
+
worldId: world.id,
|
|
601
|
+
chatId,
|
|
602
|
+
messageId: nextMessage?.messageId ?? null,
|
|
603
|
+
error: error instanceof Error ? error.message : String(error),
|
|
604
|
+
agentStatus: nextMessage
|
|
605
|
+
? collectQueueAgentStatus(world, chatId, nextMessage.content, nextMessage.sender)
|
|
606
|
+
: collectQueueAgentStatus(world, chatId, '', 'human'),
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
finally {
|
|
610
|
+
inFlightQueueResumeKeys.delete(queueKey);
|
|
611
|
+
}
|
|
612
|
+
})();
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Synchronously add the old chat's queue to the paused set on chat switch.
|
|
616
|
+
* Called by managers.ts restoreChat.
|
|
617
|
+
*/
|
|
618
|
+
export function autoPauseQueueForChat(worldId, chatId) {
|
|
619
|
+
pausedQueues.add(getQueueKey(worldId, chatId));
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Clear the pause flag for a chat (used when activating/restoring a chat).
|
|
623
|
+
*/
|
|
624
|
+
export function clearQueuePauseForChat(worldId, chatId) {
|
|
625
|
+
pausedQueues.delete(getQueueKey(worldId, chatId));
|
|
626
|
+
}
|
|
627
|
+
// ─── Sender classification helper ─────────────────────────────────────────────
|
|
628
|
+
function isUserQueueSender(sender) {
|
|
629
|
+
const normalized = String(sender || '').trim().toLowerCase();
|
|
630
|
+
return normalized === 'human' || normalized === 'world' || normalized.startsWith('user');
|
|
631
|
+
}
|
|
632
|
+
async function resolveRuntimeWorldForChat(worldId, chatId, targetWorld) {
|
|
633
|
+
const resolvedWorldId = await getResolvedWorldId(worldId);
|
|
634
|
+
const { getActiveSubscribedWorld } = await import('./subscription.js');
|
|
635
|
+
return (getActiveSubscribedWorld(resolvedWorldId, chatId) ||
|
|
636
|
+
targetWorld ||
|
|
637
|
+
await (async () => {
|
|
638
|
+
const { getWorld } = await import('./managers.js');
|
|
639
|
+
return getWorld(resolvedWorldId);
|
|
640
|
+
})());
|
|
641
|
+
}
|
|
642
|
+
// ─── Public Queue API ─────────────────────────────────────────────────────────
|
|
643
|
+
/**
|
|
644
|
+
* Add a message to the queue for a chat. Returns the created QueuedMessage or null.
|
|
645
|
+
*/
|
|
646
|
+
export async function addToQueue(worldId, chatId, content, sender, options) {
|
|
647
|
+
await ensureInitialization();
|
|
648
|
+
const resolvedWorldId = await getResolvedWorldId(worldId);
|
|
649
|
+
const queueStorage = getQueueStorageOrThrow('addToQueue');
|
|
650
|
+
const messageId = String(options?.preassignedMessageId || '').trim() || `msg-${Date.now()}-${nanoid(6)}`;
|
|
651
|
+
await queueStorage.addQueuedMessage(resolvedWorldId, chatId, messageId, content.trim(), sender || 'human');
|
|
652
|
+
const messages = await queueStorage.getQueuedMessages(resolvedWorldId, chatId);
|
|
653
|
+
const queuedMessage = messages?.find((m) => m.messageId === messageId) ?? null;
|
|
654
|
+
if (!queuedMessage) {
|
|
655
|
+
throw new Error(`addToQueue: failed to persist queue row for message '${messageId}'.`);
|
|
656
|
+
}
|
|
657
|
+
// Always look up the runtime world so we can update the _queuedChatIds cache
|
|
658
|
+
const { getActiveSubscribedWorld } = await import('./subscription.js');
|
|
659
|
+
const runtimeWorld = getActiveSubscribedWorld(resolvedWorldId, chatId) ||
|
|
660
|
+
options?.targetWorld ||
|
|
661
|
+
await (async () => {
|
|
662
|
+
const { getWorld } = await import('./managers.js');
|
|
663
|
+
return getWorld(resolvedWorldId);
|
|
664
|
+
})();
|
|
665
|
+
if (runtimeWorld) {
|
|
666
|
+
if (!runtimeWorld._queuedChatIds)
|
|
667
|
+
runtimeWorld._queuedChatIds = new Set();
|
|
668
|
+
runtimeWorld._queuedChatIds.add(chatId);
|
|
669
|
+
if (options?.triggerProcessing ?? true) {
|
|
670
|
+
triggerPendingQueueResume(runtimeWorld, chatId);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return queuedMessage;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Runtime-start recovery hook.
|
|
677
|
+
* Resets interrupted `sending` queue rows back to `queued`.
|
|
678
|
+
*/
|
|
679
|
+
export async function recoverQueueSendingMessages() {
|
|
680
|
+
await ensureInitialization();
|
|
681
|
+
return await storageWrappers?.recoverSendingMessages?.() ?? 0;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Immediate dispatch helper for non-user chat messages.
|
|
685
|
+
*/
|
|
686
|
+
export async function dispatchImmediateChatMessage(worldId, chatId, content, sender, targetWorld, options) {
|
|
687
|
+
const targetChatId = String(chatId || '').trim();
|
|
688
|
+
if (!targetChatId) {
|
|
689
|
+
throw new Error('dispatchImmediateChatMessage: chatId is required for immediate dispatch.');
|
|
690
|
+
}
|
|
691
|
+
const resolvedWorldId = await getResolvedWorldId(worldId);
|
|
692
|
+
const runtimeWorld = await resolveRuntimeWorldForChat(resolvedWorldId, targetChatId, targetWorld);
|
|
693
|
+
if (!runtimeWorld) {
|
|
694
|
+
throw new Error(`dispatchImmediateChatMessage: world not found for immediate dispatch (${resolvedWorldId}).`);
|
|
695
|
+
}
|
|
696
|
+
const forcedMessageId = String(options?.preassignedMessageId || '').trim();
|
|
697
|
+
if (forcedMessageId) {
|
|
698
|
+
return publishMessageWithId(runtimeWorld, content, sender, forcedMessageId, targetChatId);
|
|
699
|
+
}
|
|
700
|
+
return publishMessage(runtimeWorld, content, sender, targetChatId);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* External user-send ingress helper.
|
|
704
|
+
* Queue-backed submission is restricted to user-authored turns only.
|
|
705
|
+
*/
|
|
706
|
+
export async function enqueueAndProcessUserTurn(worldId, chatId, content, sender, targetWorld, options) {
|
|
707
|
+
const targetChatId = String(chatId || '').trim();
|
|
708
|
+
if (!targetChatId) {
|
|
709
|
+
throw new Error('enqueueAndProcessUserTurn: chatId is required for user message dispatch.');
|
|
710
|
+
}
|
|
711
|
+
if (!isUserQueueSender(sender)) {
|
|
712
|
+
throw new Error(`enqueueAndProcessUserTurn: sender '${sender}' is not a queue-eligible user sender.`);
|
|
713
|
+
}
|
|
714
|
+
return addToQueue(worldId, targetChatId, content, sender, {
|
|
715
|
+
triggerProcessing: true,
|
|
716
|
+
targetWorld,
|
|
717
|
+
source: options?.source,
|
|
718
|
+
preassignedMessageId: options?.preassignedMessageId,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Return all queue messages (queued, sending, error) for a chat.
|
|
723
|
+
*/
|
|
724
|
+
export async function getQueueMessages(worldId, chatId) {
|
|
725
|
+
await ensureInitialization();
|
|
726
|
+
const resolvedWorldId = await getResolvedWorldId(worldId);
|
|
727
|
+
if (!storageWrappers?.getQueuedMessages)
|
|
728
|
+
return [];
|
|
729
|
+
return storageWrappers.getQueuedMessages(resolvedWorldId, chatId);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Remove a specific message from the queue by messageId.
|
|
733
|
+
*/
|
|
734
|
+
export async function removeFromQueue(worldId, messageId) {
|
|
735
|
+
await ensureInitialization();
|
|
736
|
+
await storageWrappers?.removeQueuedMessage?.(messageId);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Pause queue processing for a chat. Current in-flight message completes normally.
|
|
740
|
+
*/
|
|
741
|
+
export async function pauseChatQueue(worldId, chatId) {
|
|
742
|
+
await ensureInitialization();
|
|
743
|
+
const resolvedWorldId = await getResolvedWorldId(worldId);
|
|
744
|
+
pausedQueues.add(`${resolvedWorldId}:${chatId}`);
|
|
745
|
+
loggerQueue.debug('Queue paused by user', { worldId: resolvedWorldId, chatId });
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Resume queue processing for a chat. Clears the pause flag and triggers the next item.
|
|
749
|
+
*/
|
|
750
|
+
export async function resumeChatQueue(worldId, chatId) {
|
|
751
|
+
await ensureInitialization();
|
|
752
|
+
const resolvedWorldId = await getResolvedWorldId(worldId);
|
|
753
|
+
pausedQueues.delete(`${resolvedWorldId}:${chatId}`);
|
|
754
|
+
const { getActiveSubscribedWorld } = await import('./subscription.js');
|
|
755
|
+
const world = getActiveSubscribedWorld(resolvedWorldId, chatId) ||
|
|
756
|
+
await (async () => {
|
|
757
|
+
const { getWorld } = await import('./managers.js');
|
|
758
|
+
return getWorld(resolvedWorldId);
|
|
759
|
+
})();
|
|
760
|
+
if (world) {
|
|
761
|
+
triggerPendingQueueResume(world, chatId, { recoverStaleSending: true });
|
|
762
|
+
}
|
|
763
|
+
loggerQueue.debug('Queue resumed by user', { worldId: resolvedWorldId, chatId });
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Stop queue processing: cancel remaining queued messages and set pause flag.
|
|
767
|
+
* The current in-flight message completes normally.
|
|
768
|
+
*/
|
|
769
|
+
export async function stopChatQueue(worldId, chatId) {
|
|
770
|
+
await ensureInitialization();
|
|
771
|
+
const resolvedWorldId = await getResolvedWorldId(worldId);
|
|
772
|
+
// Cancel all remaining 'queued' rows
|
|
773
|
+
await storageWrappers?.cancelQueuedMessages?.(resolvedWorldId, chatId);
|
|
774
|
+
// Prevent any follow-up trigger
|
|
775
|
+
pausedQueues.add(`${resolvedWorldId}:${chatId}`);
|
|
776
|
+
// Update queued chat ID cache
|
|
777
|
+
const { getActiveSubscribedWorld } = await import('./subscription.js');
|
|
778
|
+
getActiveSubscribedWorld(resolvedWorldId, chatId)?._queuedChatIds?.delete(chatId);
|
|
779
|
+
loggerQueue.debug('Queue stopped by user', { worldId: resolvedWorldId, chatId });
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Clear the entire queue for a chat (removes all queue rows including cancelled/error).
|
|
783
|
+
*/
|
|
784
|
+
export async function clearChatQueue(worldId, chatId) {
|
|
785
|
+
await ensureInitialization();
|
|
786
|
+
const resolvedWorldId = await getResolvedWorldId(worldId);
|
|
787
|
+
await storageWrappers?.deleteQueueForChat?.(resolvedWorldId, chatId);
|
|
788
|
+
pausedQueues.delete(`${resolvedWorldId}:${chatId}`);
|
|
789
|
+
// Update queued chat ID cache
|
|
790
|
+
const { getActiveSubscribedWorld } = await import('./subscription.js');
|
|
791
|
+
getActiveSubscribedWorld(resolvedWorldId, chatId)?._queuedChatIds?.delete(chatId);
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Reset a failed queue message (status='error') back to 'queued' with retry_count=0,
|
|
795
|
+
* then trigger queue processing. Allows the user to manually retry after automatic
|
|
796
|
+
* retries have been exhausted.
|
|
797
|
+
*/
|
|
798
|
+
export async function retryQueueMessage(worldId, messageId, chatId) {
|
|
799
|
+
await ensureInitialization();
|
|
800
|
+
const resolvedWorldId = await getResolvedWorldId(worldId);
|
|
801
|
+
if (!storageWrappers?.resetQueueMessageForRetry)
|
|
802
|
+
return;
|
|
803
|
+
// Reset status to 'queued' and retry_count to 0 so automatic retry logic applies fresh
|
|
804
|
+
await storageWrappers.resetQueueMessageForRetry(messageId);
|
|
805
|
+
const { getActiveSubscribedWorld } = await import('./subscription.js');
|
|
806
|
+
const world = getActiveSubscribedWorld(resolvedWorldId, chatId) ||
|
|
807
|
+
await (async () => {
|
|
808
|
+
const { getWorld } = await import('./managers.js');
|
|
809
|
+
return getWorld(resolvedWorldId);
|
|
810
|
+
})();
|
|
811
|
+
if (!world)
|
|
812
|
+
return;
|
|
813
|
+
triggerPendingQueueResume(world, chatId);
|
|
814
|
+
}
|