agent-world 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/README.md +90 -17
  2. package/dist/cli/commands.d.ts +7 -1
  3. package/dist/cli/commands.js +27 -10
  4. package/dist/cli/hitl.d.ts +4 -1
  5. package/dist/cli/hitl.js +55 -20
  6. package/dist/cli/index.js +249 -97
  7. package/dist/cli/system-events.d.ts +27 -0
  8. package/dist/cli/system-events.js +63 -0
  9. package/dist/core/activity-tracker.d.ts +26 -0
  10. package/dist/core/activity-tracker.d.ts.map +1 -1
  11. package/dist/core/activity-tracker.js +21 -4
  12. package/dist/core/activity-tracker.js.map +1 -1
  13. package/dist/core/anthropic-direct.d.ts +2 -0
  14. package/dist/core/anthropic-direct.d.ts.map +1 -1
  15. package/dist/core/anthropic-direct.js +43 -1
  16. package/dist/core/anthropic-direct.js.map +1 -1
  17. package/dist/core/chat-constants.d.ts +12 -0
  18. package/dist/core/chat-constants.d.ts.map +1 -1
  19. package/dist/core/chat-constants.js +5 -0
  20. package/dist/core/chat-constants.js.map +1 -1
  21. package/dist/core/create-agent-tool.d.ts +5 -0
  22. package/dist/core/create-agent-tool.d.ts.map +1 -1
  23. package/dist/core/create-agent-tool.js +57 -34
  24. package/dist/core/create-agent-tool.js.map +1 -1
  25. package/dist/core/events/index.d.ts +5 -2
  26. package/dist/core/events/index.d.ts.map +1 -1
  27. package/dist/core/events/index.js +5 -2
  28. package/dist/core/events/index.js.map +1 -1
  29. package/dist/core/events/memory-manager.d.ts +26 -1
  30. package/dist/core/events/memory-manager.d.ts.map +1 -1
  31. package/dist/core/events/memory-manager.js +877 -72
  32. package/dist/core/events/memory-manager.js.map +1 -1
  33. package/dist/core/events/orchestrator.d.ts +8 -0
  34. package/dist/core/events/orchestrator.d.ts.map +1 -1
  35. package/dist/core/events/orchestrator.js +203 -36
  36. package/dist/core/events/orchestrator.js.map +1 -1
  37. package/dist/core/events/persistence.d.ts +21 -14
  38. package/dist/core/events/persistence.d.ts.map +1 -1
  39. package/dist/core/events/persistence.js +100 -35
  40. package/dist/core/events/persistence.js.map +1 -1
  41. package/dist/core/events/publishers.d.ts +13 -7
  42. package/dist/core/events/publishers.d.ts.map +1 -1
  43. package/dist/core/events/publishers.js +53 -37
  44. package/dist/core/events/publishers.js.map +1 -1
  45. package/dist/core/events/subscribers.d.ts +17 -14
  46. package/dist/core/events/subscribers.d.ts.map +1 -1
  47. package/dist/core/events/subscribers.js +61 -148
  48. package/dist/core/events/subscribers.js.map +1 -1
  49. package/dist/core/events/title-scheduler.d.ts +27 -0
  50. package/dist/core/events/title-scheduler.d.ts.map +1 -0
  51. package/dist/core/events/title-scheduler.js +135 -0
  52. package/dist/core/events/title-scheduler.js.map +1 -0
  53. package/dist/core/events/tool-bridge-logging.d.ts +4 -1
  54. package/dist/core/events/tool-bridge-logging.d.ts.map +1 -1
  55. package/dist/core/events/tool-bridge-logging.js +112 -13
  56. package/dist/core/events/tool-bridge-logging.js.map +1 -1
  57. package/dist/core/events-metadata.d.ts.map +1 -1
  58. package/dist/core/events-metadata.js +8 -4
  59. package/dist/core/events-metadata.js.map +1 -1
  60. package/dist/core/export.d.ts +1 -1
  61. package/dist/core/export.d.ts.map +1 -1
  62. package/dist/core/export.js +2 -15
  63. package/dist/core/export.js.map +1 -1
  64. package/dist/core/feature-path-logging.d.ts +50 -0
  65. package/dist/core/feature-path-logging.d.ts.map +1 -0
  66. package/dist/core/feature-path-logging.js +130 -0
  67. package/dist/core/feature-path-logging.js.map +1 -0
  68. package/dist/core/file-tools.d.ts +57 -1
  69. package/dist/core/file-tools.d.ts.map +1 -1
  70. package/dist/core/file-tools.js +329 -29
  71. package/dist/core/file-tools.js.map +1 -1
  72. package/dist/core/google-direct.d.ts +6 -1
  73. package/dist/core/google-direct.d.ts.map +1 -1
  74. package/dist/core/google-direct.js +76 -7
  75. package/dist/core/google-direct.js.map +1 -1
  76. package/dist/core/heartbeat.d.ts +34 -0
  77. package/dist/core/heartbeat.d.ts.map +1 -0
  78. package/dist/core/heartbeat.js +153 -0
  79. package/dist/core/heartbeat.js.map +1 -0
  80. package/dist/core/hitl-tool.d.ts +6 -12
  81. package/dist/core/hitl-tool.d.ts.map +1 -1
  82. package/dist/core/hitl-tool.js +66 -88
  83. package/dist/core/hitl-tool.js.map +1 -1
  84. package/dist/core/hitl.d.ts +61 -4
  85. package/dist/core/hitl.d.ts.map +1 -1
  86. package/dist/core/hitl.js +324 -60
  87. package/dist/core/hitl.js.map +1 -1
  88. package/dist/core/index.d.ts +11 -7
  89. package/dist/core/index.d.ts.map +1 -1
  90. package/dist/core/index.js +10 -6
  91. package/dist/core/index.js.map +1 -1
  92. package/dist/core/llm-manager.d.ts +15 -0
  93. package/dist/core/llm-manager.d.ts.map +1 -1
  94. package/dist/core/llm-manager.js +325 -40
  95. package/dist/core/llm-manager.js.map +1 -1
  96. package/dist/core/load-skill-tool.d.ts +36 -3
  97. package/dist/core/load-skill-tool.d.ts.map +1 -1
  98. package/dist/core/load-skill-tool.js +807 -93
  99. package/dist/core/load-skill-tool.js.map +1 -1
  100. package/dist/core/logger.d.ts +14 -0
  101. package/dist/core/logger.d.ts.map +1 -1
  102. package/dist/core/logger.js +15 -0
  103. package/dist/core/logger.js.map +1 -1
  104. package/dist/core/managers.d.ts +18 -50
  105. package/dist/core/managers.d.ts.map +1 -1
  106. package/dist/core/managers.js +340 -502
  107. package/dist/core/managers.js.map +1 -1
  108. package/dist/core/mcp-server-registry.d.ts +16 -1
  109. package/dist/core/mcp-server-registry.d.ts.map +1 -1
  110. package/dist/core/mcp-server-registry.js +162 -12
  111. package/dist/core/mcp-server-registry.js.map +1 -1
  112. package/dist/core/message-cutoff.d.ts +29 -0
  113. package/dist/core/message-cutoff.d.ts.map +1 -0
  114. package/dist/core/message-cutoff.js +63 -0
  115. package/dist/core/message-cutoff.js.map +1 -0
  116. package/dist/core/message-edit-manager.d.ts +54 -0
  117. package/dist/core/message-edit-manager.d.ts.map +1 -0
  118. package/dist/core/message-edit-manager.js +602 -0
  119. package/dist/core/message-edit-manager.js.map +1 -0
  120. package/dist/core/message-prep.d.ts +2 -0
  121. package/dist/core/message-prep.d.ts.map +1 -1
  122. package/dist/core/message-prep.js +39 -12
  123. package/dist/core/message-prep.js.map +1 -1
  124. package/dist/core/message-processing-control.d.ts +1 -0
  125. package/dist/core/message-processing-control.d.ts.map +1 -1
  126. package/dist/core/message-processing-control.js +23 -6
  127. package/dist/core/message-processing-control.js.map +1 -1
  128. package/dist/core/openai-direct.d.ts +9 -3
  129. package/dist/core/openai-direct.d.ts.map +1 -1
  130. package/dist/core/openai-direct.js +267 -33
  131. package/dist/core/openai-direct.js.map +1 -1
  132. package/dist/core/optional-tracers/opik-runtime.d.ts +32 -0
  133. package/dist/core/optional-tracers/opik-runtime.d.ts.map +1 -0
  134. package/dist/core/optional-tracers/opik-runtime.js +141 -0
  135. package/dist/core/optional-tracers/opik-runtime.js.map +1 -0
  136. package/dist/core/queue-manager.d.ts +84 -0
  137. package/dist/core/queue-manager.d.ts.map +1 -0
  138. package/dist/core/queue-manager.js +814 -0
  139. package/dist/core/queue-manager.js.map +1 -0
  140. package/dist/core/reasoning-controls.d.ts +30 -0
  141. package/dist/core/reasoning-controls.d.ts.map +1 -0
  142. package/dist/core/reasoning-controls.js +118 -0
  143. package/dist/core/reasoning-controls.js.map +1 -0
  144. package/dist/core/reliability-config.d.ts +82 -0
  145. package/dist/core/reliability-config.d.ts.map +1 -0
  146. package/dist/core/reliability-config.js +106 -0
  147. package/dist/core/reliability-config.js.map +1 -0
  148. package/dist/core/reliability-runtime.d.ts +53 -0
  149. package/dist/core/reliability-runtime.d.ts.map +1 -0
  150. package/dist/core/reliability-runtime.js +92 -0
  151. package/dist/core/reliability-runtime.js.map +1 -0
  152. package/dist/core/security/guardrails.d.ts +21 -0
  153. package/dist/core/security/guardrails.d.ts.map +1 -0
  154. package/dist/core/security/guardrails.js +111 -0
  155. package/dist/core/security/guardrails.js.map +1 -0
  156. package/dist/core/send-message-tool.d.ts +79 -0
  157. package/dist/core/send-message-tool.d.ts.map +1 -0
  158. package/dist/core/send-message-tool.js +222 -0
  159. package/dist/core/send-message-tool.js.map +1 -0
  160. package/dist/core/shell-cmd-tool.d.ts +82 -1
  161. package/dist/core/shell-cmd-tool.d.ts.map +1 -1
  162. package/dist/core/shell-cmd-tool.js +854 -42
  163. package/dist/core/shell-cmd-tool.js.map +1 -1
  164. package/dist/core/skill-registry.d.ts +2 -0
  165. package/dist/core/skill-registry.d.ts.map +1 -1
  166. package/dist/core/skill-registry.js +52 -2
  167. package/dist/core/skill-registry.js.map +1 -1
  168. package/dist/core/storage/eventStorage/fileEventStorage.d.ts +5 -0
  169. package/dist/core/storage/eventStorage/fileEventStorage.d.ts.map +1 -1
  170. package/dist/core/storage/eventStorage/fileEventStorage.js +61 -0
  171. package/dist/core/storage/eventStorage/fileEventStorage.js.map +1 -1
  172. package/dist/core/storage/eventStorage/memoryEventStorage.d.ts +5 -0
  173. package/dist/core/storage/eventStorage/memoryEventStorage.d.ts.map +1 -1
  174. package/dist/core/storage/eventStorage/memoryEventStorage.js +34 -0
  175. package/dist/core/storage/eventStorage/memoryEventStorage.js.map +1 -1
  176. package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts +1 -0
  177. package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts.map +1 -1
  178. package/dist/core/storage/eventStorage/sqliteEventStorage.js +19 -2
  179. package/dist/core/storage/eventStorage/sqliteEventStorage.js.map +1 -1
  180. package/dist/core/storage/eventStorage/types.d.ts +6 -0
  181. package/dist/core/storage/eventStorage/types.d.ts.map +1 -1
  182. package/dist/core/storage/eventStorage/types.js +1 -0
  183. package/dist/core/storage/eventStorage/types.js.map +1 -1
  184. package/dist/core/storage/eventStorage/validation.d.ts.map +1 -1
  185. package/dist/core/storage/eventStorage/validation.js +2 -1
  186. package/dist/core/storage/eventStorage/validation.js.map +1 -1
  187. package/dist/core/storage/github-world-import.d.ts +84 -0
  188. package/dist/core/storage/github-world-import.d.ts.map +1 -0
  189. package/dist/core/storage/github-world-import.js +365 -0
  190. package/dist/core/storage/github-world-import.js.map +1 -0
  191. package/dist/core/storage/memory-storage.d.ts +19 -8
  192. package/dist/core/storage/memory-storage.d.ts.map +1 -1
  193. package/dist/core/storage/memory-storage.js +147 -49
  194. package/dist/core/storage/memory-storage.js.map +1 -1
  195. package/dist/core/storage/queue-storage.d.ts +1 -0
  196. package/dist/core/storage/queue-storage.d.ts.map +1 -1
  197. package/dist/core/storage/queue-storage.js +3 -2
  198. package/dist/core/storage/queue-storage.js.map +1 -1
  199. package/dist/core/storage/sqlite-storage.d.ts +14 -9
  200. package/dist/core/storage/sqlite-storage.d.ts.map +1 -1
  201. package/dist/core/storage/sqlite-storage.js +131 -154
  202. package/dist/core/storage/sqlite-storage.js.map +1 -1
  203. package/dist/core/storage/storage-factory.d.ts +3 -0
  204. package/dist/core/storage/storage-factory.d.ts.map +1 -1
  205. package/dist/core/storage/storage-factory.js +175 -89
  206. package/dist/core/storage/storage-factory.js.map +1 -1
  207. package/dist/core/storage/world-storage.d.ts +1 -1
  208. package/dist/core/storage/world-storage.d.ts.map +1 -1
  209. package/dist/core/storage/world-storage.js +5 -1
  210. package/dist/core/storage/world-storage.js.map +1 -1
  211. package/dist/core/storage-init.d.ts +11 -0
  212. package/dist/core/storage-init.d.ts.map +1 -0
  213. package/dist/core/storage-init.js +122 -0
  214. package/dist/core/storage-init.js.map +1 -0
  215. package/dist/core/subscription.d.ts +8 -1
  216. package/dist/core/subscription.d.ts.map +1 -1
  217. package/dist/core/subscription.js +130 -23
  218. package/dist/core/subscription.js.map +1 -1
  219. package/dist/core/tool-approval.d.ts +45 -0
  220. package/dist/core/tool-approval.d.ts.map +1 -0
  221. package/dist/core/tool-approval.js +223 -0
  222. package/dist/core/tool-approval.js.map +1 -0
  223. package/dist/core/tool-execution-envelope.d.ts +87 -0
  224. package/dist/core/tool-execution-envelope.d.ts.map +1 -0
  225. package/dist/core/tool-execution-envelope.js +168 -0
  226. package/dist/core/tool-execution-envelope.js.map +1 -0
  227. package/dist/core/tool-utils.d.ts +7 -2
  228. package/dist/core/tool-utils.d.ts.map +1 -1
  229. package/dist/core/tool-utils.js +81 -17
  230. package/dist/core/tool-utils.js.map +1 -1
  231. package/dist/core/types.d.ts +67 -19
  232. package/dist/core/types.d.ts.map +1 -1
  233. package/dist/core/types.js +3 -0
  234. package/dist/core/types.js.map +1 -1
  235. package/dist/core/utils.d.ts +7 -0
  236. package/dist/core/utils.d.ts.map +1 -1
  237. package/dist/core/utils.js +71 -21
  238. package/dist/core/utils.js.map +1 -1
  239. package/dist/core/web-fetch-tool.d.ts +72 -0
  240. package/dist/core/web-fetch-tool.d.ts.map +1 -0
  241. package/dist/core/web-fetch-tool.js +491 -0
  242. package/dist/core/web-fetch-tool.js.map +1 -0
  243. package/dist/core/world-registry.d.ts +84 -0
  244. package/dist/core/world-registry.d.ts.map +1 -0
  245. package/dist/core/world-registry.js +247 -0
  246. package/dist/core/world-registry.js.map +1 -0
  247. package/dist/public/assets/index-Be-1xtV-.js +104 -0
  248. package/dist/public/assets/index-tsDdiXDU.css +1 -0
  249. package/dist/public/index.html +2 -2
  250. package/dist/public/mcp-sandbox-proxy.html +148 -0
  251. package/dist/server/api.js +260 -18
  252. package/dist/server/error-response.d.ts +27 -0
  253. package/dist/server/error-response.js +77 -0
  254. package/dist/server/index.d.ts +2 -1
  255. package/dist/server/index.js +6 -2
  256. package/dist/server/sse-handler.d.ts +11 -1
  257. package/dist/server/sse-handler.js +194 -34
  258. package/migrations/0015_add_message_queue.sql +36 -0
  259. package/migrations/0016_add_world_heartbeat.sql +13 -0
  260. package/migrations/0017_add_title_provenance.sql +7 -0
  261. package/package.json +31 -10
  262. package/dist/public/assets/index-BW41BxMy.css +0 -1
  263. package/dist/public/assets/index-kO6UJFwK.js +0 -96
@@ -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
+ }