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
@@ -20,6 +20,27 @@
20
20
  * - storage (runtime)
21
21
  *
22
22
  * Changes:
23
+ * - 2026-03-13: Phase 1 — weak fallback no-commit: `pickFallbackTitle` returns '' instead of 'Chat Session' so low-signal LLM results keep the chat in 'New Chat' state.
24
+ * - 2026-03-13: Phase 2 — bounded context window: `buildTitlePromptMessages` collects up to TITLE_CONTEXT_WINDOW_TURNS*2 recent user+assistant messages for richer prompt context.
25
+ * - 2026-03-13: Improved title-gen prompt: explicit @mention semantics, no-verbatim-copy rule, noun-phrase Title Case format constraint.
26
+ * - 2026-03-13: Title-generation LLM calls now strip world `reasoning_effort` so background title requests omit provider reasoning params by default.
27
+ * - 2026-03-06: Required explicit chat scope in memory-save/continuation/assistant-response paths; removed `world.currentChatId` fallback from agent event routing.
28
+ * - 2026-03-06: Normalized shell continuation parse/validation/policy failures through explicit canonical shell failure reasons and updated continuation comments to reflect bounded-preview tool persistence.
29
+ * - 2026-03-06: Collapsed shell continuation result-mode selection to one bounded-preview mode and normalized persisted shell tool failures through the canonical shell-result formatter.
30
+ * - 2026-02-28: Added canonical `message.publish` logs for assistant publish events in direct and continuation response paths.
31
+ * - 2026-02-27: Passed explicit chat scope to continuation system events (`publishEvent`) to prevent fallback routing to `world.currentChatId` during chat switches.
32
+ * - 2026-02-27: Suppress repeated identical `load_skill` tool calls within the same continuation run once a prior same-run load succeeded.
33
+ * - 2026-02-27: Added per-chat/agent continuation run lock in `continueLLMAfterToolExecution` to skip concurrent duplicate continuation runs while tools are pending/executing (prevents duplicate HITL approval prompts).
34
+ * - 2026-03-01: Expanded script-like shell command detection to treat path-based interpreter executables (for example `.venv/bin/python`) as script hosts so continuation prefers smart shell result mode after skill-driven script calls.
35
+ * - 2026-03-01: Generalized script-host detection for smart shell continuation mode to include additional interpreter families (`bash`, `node`, `deno`, `bun`, `ruby`, `perl`, `php`, `pwsh`) and `env <interpreter> script` invocation patterns.
36
+ * - 2026-03-01: Updated duplicate `shell_cmd` matching to ignore `output_format`/`output_detail` differences and redact those fields from continuation tool telemetry payloads.
37
+ * - 2026-02-26: Replaced `resumePendingToolCallsForChat` console traces with categorized structured logger events (`chat.restore.resume.tools`) for env-controlled restore diagnostics.
38
+ * - 2026-02-24: Commented out hardcoded Infinite-Etude handoff safeguard to respect separation of concerns (logic moved to agent prompt).
39
+ * - 2026-02-25: Added detailed resume tracing in `resumePendingToolCallsForChat` (start/skip/execute/error/continue) for cross-layer restore diagnostics.
40
+ * - 2026-02-25: Added duplicate messageId guard in `saveIncomingMessageToMemory` so chat-restore replay can re-emit pending user messages without duplicating persisted agent memory.
41
+ * - 2026-02-25: Added `resumePendingToolCallsForChat` to restore unresolved persisted tool calls (e.g. `load_skill`) on chat load/switch and continue the LLM loop.
42
+ * - 2026-02-21: Shell tool continuation context now requests minimal LLM result mode (`status`/`exit_code`) and passes agent name for assistant-stream shell SSE attribution.
43
+ * - 2026-02-20: Added Infinite-Etude handoff safeguard to enforce Pedagogue -> Engraver final mention when missing.
23
44
  * - 2026-02-16: Added plain-text tool-intent fallback parser in continuation to synthesize executable `tool_calls` when providers return `Calling tool: ...` text.
24
45
  * - 2026-02-16: Max tool-hop guardrail now emits UI/tool errors and injects transient LLM context, then continues loop instead of returning.
25
46
  * - 2026-02-16: Removed plain-text tool-intent reminder/retry path; continuation now relies only on tool-call loop + hop guardrail.
@@ -47,6 +68,8 @@ import { createCategoryLogger } from '../logger.js';
47
68
  import { beginWorldActivity } from '../activity-tracker.js';
48
69
  import { createStorageWithWrappers } from '../storage/storage-factory.js';
49
70
  import { generateAgentResponse } from '../llm-manager.js';
71
+ import { formatShellToolErrorEnvelopeContent, } from '../shell-cmd-tool.js';
72
+ import { createTextToolPreview, getToolEventPreviewPayload, parseToolExecutionEnvelopeContent, serializeToolExecutionEnvelope, stringifyToolExecutionResult, } from '../tool-execution-envelope.js';
50
73
  import { isMessageProcessingCanceledError, throwIfMessageProcessingStopped } from '../message-processing-control.js';
51
74
  import { shouldAutoMention, addAutoMention, hasAnyMentionAtBeginning, removeSelfMentions } from './mention-logic.js';
52
75
  import { publishMessageWithId, publishSSE, publishEvent, publishToolEvent, isStreamingEnabled } from './publishers.js';
@@ -56,8 +79,10 @@ const loggerAgent = createCategoryLogger('agent');
56
79
  const loggerTurnLimit = createCategoryLogger('turnlimit');
57
80
  const loggerChatTitle = createCategoryLogger('chattitle');
58
81
  const loggerAutoMention = createCategoryLogger('automention');
59
- const TITLE_PROMPT_MAX_TURNS = 24;
82
+ const loggerRestoreResumeTools = createCategoryLogger('chat.restore.resume.tools');
83
+ const loggerMessagePublish = createCategoryLogger('message.publish');
60
84
  const TITLE_PROMPT_MAX_CHARS_PER_TURN = 240;
85
+ const TITLE_CONTEXT_WINDOW_TURNS = 3;
61
86
  // Storage wrapper instance - initialized lazily
62
87
  let storageWrappers = null;
63
88
  async function getStorageWrappers() {
@@ -66,6 +91,103 @@ async function getStorageWrappers() {
66
91
  }
67
92
  return storageWrappers;
68
93
  }
94
+ const activeContinuationRuns = new Map();
95
+ const continuationRunLoadedSkills = new Map();
96
+ const continuationRunShellCommandResults = new Map();
97
+ function normalizeContinuationChatId(chatId) {
98
+ if (chatId === undefined || chatId === null) {
99
+ return '__null__';
100
+ }
101
+ const normalized = String(chatId).trim();
102
+ return normalized || '__null__';
103
+ }
104
+ function getContinuationScopeKey(worldId, agentId, chatId) {
105
+ return `${worldId}::${agentId}::${normalizeContinuationChatId(chatId)}`;
106
+ }
107
+ function enterContinuationScope(scopeKey, runId) {
108
+ const activeRun = activeContinuationRuns.get(scopeKey);
109
+ if (!activeRun) {
110
+ activeContinuationRuns.set(scopeKey, { runId, depth: 1 });
111
+ return true;
112
+ }
113
+ if (activeRun.runId !== runId) {
114
+ return false;
115
+ }
116
+ activeRun.depth += 1;
117
+ return true;
118
+ }
119
+ function leaveContinuationScope(scopeKey, runId) {
120
+ const activeRun = activeContinuationRuns.get(scopeKey);
121
+ if (!activeRun || activeRun.runId !== runId) {
122
+ return;
123
+ }
124
+ activeRun.depth -= 1;
125
+ if (activeRun.depth <= 0) {
126
+ activeContinuationRuns.delete(scopeKey);
127
+ }
128
+ }
129
+ function isContinuationRunActive(runId) {
130
+ for (const activeRun of activeContinuationRuns.values()) {
131
+ if (activeRun.runId === runId) {
132
+ return true;
133
+ }
134
+ }
135
+ return false;
136
+ }
137
+ function getLoadedSkillsForContinuationRun(runId) {
138
+ const existing = continuationRunLoadedSkills.get(runId);
139
+ if (existing) {
140
+ return existing;
141
+ }
142
+ const created = new Set();
143
+ continuationRunLoadedSkills.set(runId, created);
144
+ return created;
145
+ }
146
+ function getShellCommandResultsForContinuationRun(runId) {
147
+ const existing = continuationRunShellCommandResults.get(runId);
148
+ if (existing) {
149
+ return existing;
150
+ }
151
+ const created = new Map();
152
+ continuationRunShellCommandResults.set(runId, created);
153
+ return created;
154
+ }
155
+ function normalizeShellCommandParameterList(parameters) {
156
+ if (!Array.isArray(parameters)) {
157
+ return [];
158
+ }
159
+ return parameters.map((parameter) => String(parameter));
160
+ }
161
+ function buildShellCommandSignature(toolArgs, trustedWorkingDirectory) {
162
+ const command = String(toolArgs?.command || '').trim();
163
+ const parameters = normalizeShellCommandParameterList(toolArgs?.parameters);
164
+ const requestedDirectory = typeof toolArgs?.directory === 'string' && toolArgs.directory.trim()
165
+ ? toolArgs.directory.trim()
166
+ : trustedWorkingDirectory;
167
+ return JSON.stringify({
168
+ command,
169
+ parameters,
170
+ directory: requestedDirectory,
171
+ });
172
+ }
173
+ function sanitizeToolArgsForEventPayload(toolName, toolArgs) {
174
+ if (toolName !== 'shell_cmd' || !toolArgs || typeof toolArgs !== 'object') {
175
+ return toolArgs;
176
+ }
177
+ const sanitized = { ...toolArgs };
178
+ delete sanitized.output_format;
179
+ delete sanitized.output_detail;
180
+ return sanitized;
181
+ }
182
+ function cleanupContinuationRunState(runId) {
183
+ if (isContinuationRunActive(runId)) {
184
+ return;
185
+ }
186
+ continuationRunLoadedSkills.delete(runId);
187
+ continuationRunShellCommandResults.delete(runId);
188
+ }
189
+ // Fallback title candidates longer than this are likely full sentences/commands, not titles.
190
+ const FALLBACK_TITLE_MAX_CHARS = 60;
69
191
  const GENERIC_TITLES = new Set([
70
192
  'chat',
71
193
  'new chat',
@@ -74,7 +196,9 @@ const GENERIC_TITLES = new Set([
74
196
  'title',
75
197
  'assistant chat',
76
198
  'user chat',
77
- 'chat title'
199
+ 'chat title',
200
+ 'chat session',
201
+ 'session'
78
202
  ]);
79
203
  function normalizeTitlePromptText(content) {
80
204
  return content
@@ -88,34 +212,62 @@ function clipTitlePromptText(content) {
88
212
  }
89
213
  return `${content.substring(0, TITLE_PROMPT_MAX_CHARS_PER_TURN - 3)}...`;
90
214
  }
91
- function buildTitlePromptMessages(messages) {
92
- const dedupKeys = new Set();
93
- const filtered = [];
94
- for (const message of messages) {
95
- if (message.role !== 'user' && message.role !== 'assistant') {
96
- continue;
97
- }
98
- if (typeof message.content !== 'string') {
215
+ function selectTitleSourceUserMessage(messages, content) {
216
+ const contentCandidate = normalizeTitlePromptText(content || '');
217
+ if (contentCandidate) {
218
+ return clipTitlePromptText(contentCandidate);
219
+ }
220
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
221
+ const message = messages[index];
222
+ if (!message || message.role !== 'user' || typeof message.content !== 'string') {
99
223
  continue;
100
224
  }
101
225
  const normalized = normalizeTitlePromptText(message.content);
102
226
  if (!normalized) {
103
227
  continue;
104
228
  }
105
- const clipped = clipTitlePromptText(normalized);
106
- const dedupKey = message.messageId
107
- ? `id:${message.messageId}`
108
- : `${message.role}:${clipped.toLowerCase()}`;
109
- if (dedupKeys.has(dedupKey)) {
229
+ return clipTitlePromptText(normalized);
230
+ }
231
+ return '';
232
+ }
233
+ function buildTitlePromptMessages(messages, content) {
234
+ // Collect up to TITLE_CONTEXT_WINDOW_TURNS * 2 most recent eligible messages (user + assistant only).
235
+ const maxMessages = TITLE_CONTEXT_WINDOW_TURNS * 2;
236
+ const window = [];
237
+ for (let i = messages.length - 1; i >= 0 && window.length < maxMessages; i -= 1) {
238
+ const message = messages[i];
239
+ if (!message)
110
240
  continue;
111
- }
112
- dedupKeys.add(dedupKey);
113
- filtered.push({
114
- role: message.role,
115
- content: clipped
241
+ const role = String(message.role || '').toLowerCase();
242
+ if (role !== 'user' && role !== 'assistant')
243
+ continue;
244
+ if (typeof message.content !== 'string')
245
+ continue;
246
+ const normalized = normalizeTitlePromptText(message.content);
247
+ if (!normalized)
248
+ continue;
249
+ window.unshift({
250
+ role: role,
251
+ content: clipTitlePromptText(normalized)
116
252
  });
117
253
  }
118
- return filtered.slice(-TITLE_PROMPT_MAX_TURNS);
254
+ // Apply explicit content override to the last user slot (or append if none found).
255
+ const overrideText = normalizeTitlePromptText(content || '');
256
+ if (overrideText) {
257
+ const clipped = clipTitlePromptText(overrideText);
258
+ let replaced = false;
259
+ for (let i = window.length - 1; i >= 0; i -= 1) {
260
+ if (window[i].role === 'user') {
261
+ window[i] = { role: 'user', content: clipped };
262
+ replaced = true;
263
+ break;
264
+ }
265
+ }
266
+ if (!replaced) {
267
+ window.push({ role: 'user', content: clipped });
268
+ }
269
+ }
270
+ return window;
119
271
  }
120
272
  function sanitizeGeneratedTitle(rawTitle) {
121
273
  const firstLine = String(rawTitle || '').split(/\r?\n/).find((line) => line.trim()) || '';
@@ -125,13 +277,26 @@ function sanitizeGeneratedTitle(rawTitle) {
125
277
  .replace(/^[-*]\s+/, '')
126
278
  .replace(/^\d+[.)]\s+/, '')
127
279
  .replace(/^title\s*[:\-]\s*/i, '')
128
- .replace(/^["'`]+|["'`]+$/g, '')
280
+ .replace(/\\"/g, '') // strip \" sequences before quote removal
281
+ .replace(/"/g, '') // strip all remaining double quotes
282
+ .replace(/^['`]+|['`]+$/g, '') // strip leading/trailing single quotes and backticks
129
283
  .replace(/[\r\n\*`_]+/g, ' ')
130
284
  .replace(/\s+/g, ' ')
131
285
  .trim();
132
286
  title = title.replace(/[.!?]+$/g, '').trim();
133
287
  return title;
134
288
  }
289
+ function removeEnvVariableFromText(variablesText, key) {
290
+ const targetKey = String(key || '').trim();
291
+ if (!targetKey) {
292
+ return String(variablesText || '');
293
+ }
294
+ return String(variablesText || '')
295
+ .split(/\r?\n/)
296
+ .map((line) => line.trim())
297
+ .filter((line) => line && !line.startsWith(`${targetKey}=`))
298
+ .join('\n');
299
+ }
135
300
  function isLowQualityTitle(title) {
136
301
  if (!title)
137
302
  return true;
@@ -146,18 +311,20 @@ function isLowQualityTitle(title) {
146
311
  }
147
312
  function pickFallbackTitle(content, promptMessages) {
148
313
  const contentCandidate = sanitizeGeneratedTitle(content);
149
- if (!isLowQualityTitle(contentCandidate)) {
314
+ if (!isLowQualityTitle(contentCandidate) && contentCandidate.length <= FALLBACK_TITLE_MAX_CHARS) {
150
315
  return contentCandidate;
151
316
  }
152
317
  for (const message of promptMessages) {
153
318
  if (message.role !== 'user')
154
319
  continue;
155
320
  const candidate = sanitizeGeneratedTitle(message.content);
156
- if (!isLowQualityTitle(candidate)) {
321
+ if (!isLowQualityTitle(candidate) && candidate.length <= FALLBACK_TITLE_MAX_CHARS) {
157
322
  return candidate;
158
323
  }
159
324
  }
160
- return 'Chat Session';
325
+ // No quality title found — return empty string so the caller retains 'New Chat'
326
+ // and the chat remains eligible for a future auto-title attempt.
327
+ return '';
161
328
  }
162
329
  function isTitleGenerationCanceledError(error) {
163
330
  if (!error)
@@ -196,6 +363,406 @@ function parseToolCallArguments(rawArguments) {
196
363
  }
197
364
  return {};
198
365
  }
366
+ function getLoadSkillIdFromToolArgs(toolArgs) {
367
+ const skillId = typeof toolArgs?.skill_id === 'string' ? toolArgs.skill_id.trim() : '';
368
+ return skillId || null;
369
+ }
370
+ function getLoadSkillIdFromRawToolArguments(rawArguments) {
371
+ const parsed = parseToolCallArguments(rawArguments);
372
+ return getLoadSkillIdFromToolArgs(parsed);
373
+ }
374
+ function isSuccessfulLoadSkillResult(toolResult) {
375
+ const envelope = parseToolExecutionEnvelopeContent(toolResult);
376
+ const normalized = envelope
377
+ ? stringifyToolExecutionResult(envelope.result)
378
+ : String(toolResult || '');
379
+ return /<skill_context\b/i.test(normalized) && !/<error>/i.test(normalized);
380
+ }
381
+ function formatToolErrorContent(options) {
382
+ if (options.toolName === 'shell_cmd') {
383
+ return formatShellToolErrorEnvelopeContent({
384
+ command: options.toolArgs?.command,
385
+ parameters: options.toolArgs?.parameters,
386
+ error: options.error,
387
+ failureReason: options.failureReason,
388
+ toolCallId: options.toolCallId,
389
+ });
390
+ }
391
+ const message = `Error executing tool: ${options.error instanceof Error ? options.error.message : String(options.error)}`;
392
+ if (options.toolName === 'load_skill') {
393
+ return serializeToolExecutionEnvelope({
394
+ __type: 'tool_execution_envelope',
395
+ version: 1,
396
+ tool: 'load_skill',
397
+ tool_call_id: options.toolCallId,
398
+ status: 'failed',
399
+ preview: createTextToolPreview(message),
400
+ result: message,
401
+ });
402
+ }
403
+ return message;
404
+ }
405
+ function resolveShellContinuationLlmResultMode(options) {
406
+ if (options.toolName !== 'shell_cmd') {
407
+ return 'verbose';
408
+ }
409
+ return 'minimal';
410
+ }
411
+ function getLatestUnresolvedToolCallForChat(agent, chatId) {
412
+ const chatMessages = agent.memory.filter((message) => message.chatId === chatId);
413
+ if (!chatMessages.length) {
414
+ return null;
415
+ }
416
+ const completedToolCallIds = new Set();
417
+ for (const message of chatMessages) {
418
+ if (message.role === 'tool' && typeof message.tool_call_id === 'string' && message.tool_call_id.trim()) {
419
+ completedToolCallIds.add(message.tool_call_id.trim());
420
+ }
421
+ }
422
+ for (let index = chatMessages.length - 1; index >= 0; index--) {
423
+ const message = chatMessages[index];
424
+ if (message.role !== 'assistant' || !Array.isArray(message.tool_calls) || message.tool_calls.length === 0) {
425
+ continue;
426
+ }
427
+ for (const toolCall of message.tool_calls) {
428
+ const toolCallId = String(toolCall?.id || '').trim();
429
+ const toolName = String(toolCall?.function?.name || '').trim();
430
+ if (!toolCallId || !toolName) {
431
+ continue;
432
+ }
433
+ if (completedToolCallIds.has(toolCallId)) {
434
+ continue;
435
+ }
436
+ return {
437
+ assistantMessage: message,
438
+ toolCall: {
439
+ id: toolCallId,
440
+ function: {
441
+ name: toolName,
442
+ arguments: toolCall?.function?.arguments,
443
+ },
444
+ },
445
+ };
446
+ }
447
+ }
448
+ return null;
449
+ }
450
+ export async function resumePendingToolCallsForChat(world, chatId, targetAssistantMessageId) {
451
+ if (!chatId) {
452
+ return 0;
453
+ }
454
+ const resumeStartedAt = Date.now();
455
+ loggerRestoreResumeTools.debug('Resume pending tool calls started', {
456
+ worldId: world.id,
457
+ chatId,
458
+ targetAssistantMessageId: targetAssistantMessageId || null,
459
+ });
460
+ const { getMCPToolsForWorld } = await import('../mcp-server-registry.js');
461
+ const mcpTools = await getMCPToolsForWorld(world.id);
462
+ const storage = await getStorageWrappers();
463
+ let resumedCount = 0;
464
+ for (const agent of world.agents.values()) {
465
+ const pending = getLatestUnresolvedToolCallForChat(agent, chatId);
466
+ if (!pending) {
467
+ loggerRestoreResumeTools.debug('Resume pending tool calls skipped agent with no pending tool call', {
468
+ worldId: world.id,
469
+ chatId,
470
+ agentId: agent.id,
471
+ });
472
+ continue;
473
+ }
474
+ if (targetAssistantMessageId
475
+ && pending.assistantMessage.messageId
476
+ && pending.assistantMessage.messageId !== targetAssistantMessageId) {
477
+ loggerRestoreResumeTools.debug('Resume pending tool calls skipped assistant target mismatch', {
478
+ worldId: world.id,
479
+ chatId,
480
+ agentId: agent.id,
481
+ pendingAssistantMessageId: pending.assistantMessage.messageId || null,
482
+ targetAssistantMessageId,
483
+ });
484
+ continue;
485
+ }
486
+ const { assistantMessage, toolCall } = pending;
487
+ loggerRestoreResumeTools.debug('Resume pending tool calls found pending tool call', {
488
+ worldId: world.id,
489
+ chatId,
490
+ agentId: agent.id,
491
+ toolName: toolCall.function.name,
492
+ toolCallId: toolCall.id,
493
+ assistantMessageId: assistantMessage.messageId || null,
494
+ });
495
+ let toolArgs = {};
496
+ try {
497
+ toolArgs = parseToolCallArguments(toolCall.function.arguments);
498
+ }
499
+ catch (parseError) {
500
+ loggerRestoreResumeTools.warn('Resume pending tool calls failed to parse tool arguments', {
501
+ worldId: world.id,
502
+ chatId,
503
+ agentId: agent.id,
504
+ toolName: toolCall.function.name,
505
+ toolCallId: toolCall.id,
506
+ error: parseError instanceof Error ? parseError.message : String(parseError),
507
+ });
508
+ const errorContent = toolCall.function.name === 'shell_cmd' || toolCall.function.name === 'load_skill'
509
+ ? formatToolErrorContent({
510
+ toolName: toolCall.function.name,
511
+ toolCallId: toolCall.id,
512
+ toolArgs,
513
+ error: `Invalid JSON in tool arguments: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
514
+ failureReason: 'validation_error',
515
+ })
516
+ : `Error executing tool: Invalid JSON in tool arguments: ${parseError instanceof Error ? parseError.message : String(parseError)}`;
517
+ const toolErrorMessage = {
518
+ role: 'tool',
519
+ content: errorContent,
520
+ tool_call_id: toolCall.id,
521
+ sender: agent.id,
522
+ createdAt: new Date(),
523
+ chatId,
524
+ messageId: generateId(),
525
+ replyToMessageId: assistantMessage.messageId,
526
+ agentId: agent.id,
527
+ };
528
+ agent.memory.push(toolErrorMessage);
529
+ if (assistantMessage.toolCallStatus) {
530
+ assistantMessage.toolCallStatus[toolCall.id] = { complete: true, result: errorContent };
531
+ }
532
+ await storage.saveAgent(world.id, agent);
533
+ await continueLLMAfterToolExecution(world, agent, chatId);
534
+ loggerRestoreResumeTools.debug('Resume pending tool calls continued after parse error', {
535
+ worldId: world.id,
536
+ chatId,
537
+ agentId: agent.id,
538
+ toolCallId: toolCall.id,
539
+ });
540
+ resumedCount += 1;
541
+ continue;
542
+ }
543
+ const toolDef = mcpTools[toolCall.function.name];
544
+ if (!toolDef) {
545
+ loggerRestoreResumeTools.warn('Resume pending tool calls tool definition missing', {
546
+ worldId: world.id,
547
+ chatId,
548
+ agentId: agent.id,
549
+ toolName: toolCall.function.name,
550
+ toolCallId: toolCall.id,
551
+ });
552
+ const errorContent = toolCall.function.name === 'shell_cmd' || toolCall.function.name === 'load_skill'
553
+ ? formatToolErrorContent({
554
+ toolName: toolCall.function.name,
555
+ toolCallId: toolCall.id,
556
+ toolArgs,
557
+ error: `Tool not found: ${toolCall.function.name}`,
558
+ failureReason: 'execution_error',
559
+ })
560
+ : `Error executing tool: Tool not found: ${toolCall.function.name}`;
561
+ const toolErrorMessage = {
562
+ role: 'tool',
563
+ content: errorContent,
564
+ tool_call_id: toolCall.id,
565
+ sender: agent.id,
566
+ createdAt: new Date(),
567
+ chatId,
568
+ messageId: generateId(),
569
+ replyToMessageId: assistantMessage.messageId,
570
+ agentId: agent.id,
571
+ };
572
+ agent.memory.push(toolErrorMessage);
573
+ if (assistantMessage.toolCallStatus) {
574
+ assistantMessage.toolCallStatus[toolCall.id] = { complete: true, result: errorContent };
575
+ }
576
+ publishToolEvent(world, {
577
+ agentName: agent.id,
578
+ type: 'tool-error',
579
+ messageId: toolCall.id,
580
+ chatId,
581
+ toolExecution: {
582
+ toolName: toolCall.function.name,
583
+ toolCallId: toolCall.id,
584
+ input: toolArgs,
585
+ error: `Tool not found: ${toolCall.function.name}`,
586
+ },
587
+ });
588
+ await storage.saveAgent(world.id, agent);
589
+ await continueLLMAfterToolExecution(world, agent, chatId);
590
+ loggerRestoreResumeTools.debug('Resume pending tool calls continued after missing tool definition', {
591
+ worldId: world.id,
592
+ chatId,
593
+ agentId: agent.id,
594
+ toolCallId: toolCall.id,
595
+ });
596
+ resumedCount += 1;
597
+ continue;
598
+ }
599
+ loggerRestoreResumeTools.debug('Resume pending tool calls executing tool', {
600
+ worldId: world.id,
601
+ chatId,
602
+ agentId: agent.id,
603
+ toolName: toolCall.function.name,
604
+ toolCallId: toolCall.id,
605
+ });
606
+ let seededLoadSkillIdForContinuation = null;
607
+ publishToolEvent(world, {
608
+ agentName: agent.id,
609
+ type: 'tool-start',
610
+ messageId: toolCall.id,
611
+ chatId,
612
+ toolExecution: {
613
+ toolName: toolCall.function.name,
614
+ toolCallId: toolCall.id,
615
+ input: toolArgs,
616
+ },
617
+ });
618
+ try {
619
+ const trustedWorkingDirectory = String(getEnvValueFromText(world.variables, 'working_directory') || getDefaultWorkingDirectory()).trim() || getDefaultWorkingDirectory();
620
+ const toolContext = {
621
+ world,
622
+ messages: agent.memory,
623
+ toolCallId: toolCall.id,
624
+ chatId,
625
+ workingDirectory: trustedWorkingDirectory,
626
+ agentName: agent.id,
627
+ llmResultMode: resolveShellContinuationLlmResultMode({
628
+ toolName: toolCall.function.name,
629
+ }),
630
+ persistToolEnvelope: toolCall.function.name === 'shell_cmd' || toolCall.function.name === 'load_skill',
631
+ };
632
+ const toolResult = await toolDef.execute(toolArgs, undefined, undefined, toolContext);
633
+ const serializedToolResult = typeof toolResult === 'string'
634
+ ? toolResult
635
+ : JSON.stringify(toolResult) ?? String(toolResult);
636
+ if (toolCall.function.name === 'load_skill') {
637
+ const requestedSkillId = getLoadSkillIdFromToolArgs(toolArgs);
638
+ if (requestedSkillId && isSuccessfulLoadSkillResult(serializedToolResult)) {
639
+ seededLoadSkillIdForContinuation = requestedSkillId;
640
+ }
641
+ }
642
+ const toolResultMessage = {
643
+ role: 'tool',
644
+ content: serializedToolResult,
645
+ tool_call_id: toolCall.id,
646
+ sender: agent.id,
647
+ createdAt: new Date(),
648
+ chatId,
649
+ messageId: generateId(),
650
+ replyToMessageId: assistantMessage.messageId,
651
+ agentId: agent.id,
652
+ };
653
+ agent.memory.push(toolResultMessage);
654
+ if (assistantMessage.toolCallStatus) {
655
+ assistantMessage.toolCallStatus[toolCall.id] = {
656
+ complete: true,
657
+ result: serializedToolResult,
658
+ };
659
+ }
660
+ const toolEnvelope = parseToolExecutionEnvelopeContent(serializedToolResult);
661
+ const toolEventPreview = getToolEventPreviewPayload(serializedToolResult);
662
+ const toolEventResult = toolEnvelope ? toolEnvelope.result : toolResult;
663
+ publishToolEvent(world, {
664
+ agentName: agent.id,
665
+ type: 'tool-result',
666
+ messageId: toolCall.id,
667
+ chatId,
668
+ toolExecution: {
669
+ toolName: toolCall.function.name,
670
+ toolCallId: toolCall.id,
671
+ input: toolArgs,
672
+ ...(toolEventPreview !== undefined ? { preview: toolEventPreview } : {}),
673
+ result: toolEventResult,
674
+ resultType: Array.isArray(toolEventResult)
675
+ ? 'array'
676
+ : toolEventResult === null
677
+ ? 'null'
678
+ : typeof toolEventResult === 'string'
679
+ ? 'string'
680
+ : 'object',
681
+ resultSize: serializedToolResult.length,
682
+ },
683
+ });
684
+ loggerRestoreResumeTools.debug('Resume pending tool calls tool execution result persisted', {
685
+ worldId: world.id,
686
+ chatId,
687
+ agentId: agent.id,
688
+ toolName: toolCall.function.name,
689
+ toolCallId: toolCall.id,
690
+ resultSize: serializedToolResult.length,
691
+ });
692
+ }
693
+ catch (toolError) {
694
+ loggerRestoreResumeTools.warn('Resume pending tool calls tool execution failed', {
695
+ worldId: world.id,
696
+ chatId,
697
+ agentId: agent.id,
698
+ toolName: toolCall.function.name,
699
+ toolCallId: toolCall.id,
700
+ error: toolError instanceof Error ? toolError.message : String(toolError),
701
+ });
702
+ const errorContent = formatToolErrorContent({
703
+ toolName: toolCall.function.name,
704
+ toolCallId: toolCall.id,
705
+ toolArgs,
706
+ error: toolError,
707
+ });
708
+ const toolErrorMessage = {
709
+ role: 'tool',
710
+ content: errorContent,
711
+ tool_call_id: toolCall.id,
712
+ sender: agent.id,
713
+ createdAt: new Date(),
714
+ chatId,
715
+ messageId: generateId(),
716
+ replyToMessageId: assistantMessage.messageId,
717
+ agentId: agent.id,
718
+ };
719
+ agent.memory.push(toolErrorMessage);
720
+ if (assistantMessage.toolCallStatus) {
721
+ assistantMessage.toolCallStatus[toolCall.id] = {
722
+ complete: true,
723
+ result: errorContent,
724
+ };
725
+ }
726
+ publishToolEvent(world, {
727
+ agentName: agent.id,
728
+ type: 'tool-error',
729
+ messageId: toolCall.id,
730
+ chatId,
731
+ toolExecution: {
732
+ toolName: toolCall.function.name,
733
+ toolCallId: toolCall.id,
734
+ input: toolArgs,
735
+ error: toolError instanceof Error ? toolError.message : String(toolError),
736
+ },
737
+ });
738
+ }
739
+ await storage.saveAgent(world.id, agent);
740
+ loggerRestoreResumeTools.debug('Resume pending tool calls saved agent state', {
741
+ worldId: world.id,
742
+ chatId,
743
+ agentId: agent.id,
744
+ toolCallId: toolCall.id,
745
+ });
746
+ await continueLLMAfterToolExecution(world, agent, chatId, {
747
+ ...(seededLoadSkillIdForContinuation ? { preloadedSkillIds: [seededLoadSkillIdForContinuation] } : {}),
748
+ });
749
+ loggerRestoreResumeTools.debug('Resume pending tool calls continued LLM after tool execution', {
750
+ worldId: world.id,
751
+ chatId,
752
+ agentId: agent.id,
753
+ toolCallId: toolCall.id,
754
+ ...(seededLoadSkillIdForContinuation ? { seededLoadSkillIdForContinuation } : {}),
755
+ });
756
+ resumedCount += 1;
757
+ }
758
+ loggerRestoreResumeTools.debug('Resume pending tool calls completed', {
759
+ worldId: world.id,
760
+ chatId,
761
+ resumedCount,
762
+ elapsedMs: Date.now() - resumeStartedAt,
763
+ });
764
+ return resumedCount;
765
+ }
199
766
  function decodeControlTokens(value) {
200
767
  return value.replace(/<ctrl(\d+)>/gi, (_match, codeRaw) => {
201
768
  const code = Number(codeRaw);
@@ -341,15 +908,29 @@ export async function saveIncomingMessageToMemory(world, agent, messageEvent) {
341
908
  }
342
909
  // Derive chatId from the message event for concurrency-safe processing
343
910
  // This ensures messages stay bound to their originating session
344
- const targetChatId = messageEvent.chatId ?? world.currentChatId ?? null;
911
+ const targetChatId = typeof messageEvent.chatId === 'string' ? messageEvent.chatId.trim() : '';
345
912
  if (!targetChatId) {
346
- loggerMemory.warn('Saving message without chatId', {
913
+ loggerMemory.error('Saving message without explicit chatId', {
914
+ worldId: world.id,
347
915
  agentId: agent.id,
348
916
  messageId: messageEvent.messageId
349
917
  });
918
+ return;
350
919
  }
351
920
  // Parse message content to detect enhanced format (e.g., tool results)
352
921
  const { message: parsedMessage } = parseMessageContent(messageEvent.content, 'user');
922
+ if (messageEvent.messageId && targetChatId) {
923
+ const duplicate = agent.memory.some((message) => message.chatId === targetChatId && message.messageId === messageEvent.messageId);
924
+ if (duplicate) {
925
+ loggerMemory.debug('Skipping duplicate incoming message memory save', {
926
+ worldId: world.id,
927
+ agentId: agent.id,
928
+ chatId: targetChatId,
929
+ messageId: messageEvent.messageId,
930
+ });
931
+ return;
932
+ }
933
+ }
353
934
  const userMessage = {
354
935
  ...parsedMessage,
355
936
  sender: messageEvent.sender,
@@ -382,7 +963,37 @@ export async function saveIncomingMessageToMemory(world, agent, messageEvent) {
382
963
  * Used for auto-execution flow where tools are executed automatically
383
964
  */
384
965
  export async function continueLLMAfterToolExecution(world, agent, chatId, options) {
385
- const completeActivity = beginWorldActivity(world, `agent:${agent.id}`, chatId ?? undefined);
966
+ const continuationChatId = typeof chatId === 'string' ? chatId.trim() : '';
967
+ if (!continuationChatId) {
968
+ throw new Error(`continueLLMAfterToolExecution: explicit chatId is required for agent ${agent.id}`);
969
+ }
970
+ const targetChatId = continuationChatId;
971
+ const continuationRunId = String(options?.continuationRunId || '').trim() || generateId();
972
+ const continuationScopeKey = getContinuationScopeKey(world.id, agent.id, continuationChatId);
973
+ const enteredScope = enterContinuationScope(continuationScopeKey, continuationRunId);
974
+ if (!enteredScope) {
975
+ loggerAgent.debug('Skipping duplicate continuation run while another run is active', {
976
+ worldId: world.id,
977
+ agentId: agent.id,
978
+ chatId: continuationChatId,
979
+ });
980
+ logToolBridge('CONTINUE SKIP_INFLIGHT', {
981
+ worldId: world.id,
982
+ agentId: agent.id,
983
+ chatId: continuationChatId,
984
+ responseType: 'skipped',
985
+ });
986
+ return;
987
+ }
988
+ const completeActivity = beginWorldActivity(world, `agent:${agent.id}`, targetChatId);
989
+ const loadedSkillsForRun = getLoadedSkillsForContinuationRun(continuationRunId);
990
+ const shellCommandResultsForRun = getShellCommandResultsForContinuationRun(continuationRunId);
991
+ for (const preloadedSkillId of options?.preloadedSkillIds || []) {
992
+ const normalizedSkillId = String(preloadedSkillId || '').trim();
993
+ if (normalizedSkillId) {
994
+ loadedSkillsForRun.add(normalizedSkillId);
995
+ }
996
+ }
386
997
  try {
387
998
  let hopCount = options?.hopCount ?? 0;
388
999
  const maxToolHops = 50;
@@ -390,25 +1001,25 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
390
1001
  const maxEmptyTextRetries = 2;
391
1002
  const emptyToolCallRetryCount = options?.emptyToolCallRetryCount ?? 0;
392
1003
  const maxEmptyToolCallRetries = 2;
393
- let transientGuardrailError;
1004
+ let transientGuardrailError = options?.transientContinuationInstruction;
394
1005
  if (hopCount > maxToolHops) {
395
1006
  const guardrailErrorMessage = `[Error] Tool continuation exceeded ${maxToolHops} hops. Guardrail triggered; reporting error and continuing.`;
396
1007
  const guardrailToolCallId = generateId();
397
1008
  loggerAgent.error('Tool continuation hop limit reached; reporting error and continuing loop', {
398
1009
  agentId: agent.id,
399
- chatId: chatId ?? world.currentChatId ?? null,
1010
+ chatId: targetChatId,
400
1011
  hopCount,
401
1012
  maxToolHops,
402
1013
  });
403
1014
  publishEvent(world, 'system', {
404
1015
  message: guardrailErrorMessage,
405
1016
  type: 'error',
406
- });
1017
+ }, targetChatId);
407
1018
  publishToolEvent(world, {
408
1019
  agentName: agent.id,
409
1020
  type: 'tool-error',
410
1021
  messageId: guardrailToolCallId,
411
- chatId: chatId ?? world.currentChatId ?? null,
1022
+ chatId: targetChatId,
412
1023
  toolExecution: {
413
1024
  toolName: '__tool_continuation_guardrail__',
414
1025
  toolCallId: guardrailToolCallId,
@@ -418,7 +1029,7 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
418
1029
  logToolBridge('CONTINUE HOP_GUARDRAIL', {
419
1030
  worldId: world.id,
420
1031
  agentId: agent.id,
421
- chatId: chatId ?? world.currentChatId ?? null,
1032
+ chatId: targetChatId,
422
1033
  hopCount,
423
1034
  maxToolHops,
424
1035
  guardrailToolCallId,
@@ -428,14 +1039,11 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
428
1039
  hopCount = 0;
429
1040
  }
430
1041
  throwIfMessageProcessingStopped(options?.abortSignal);
431
- // Use explicit chatId when provided, fallback to world.currentChatId.
432
- const targetChatId = chatId !== undefined ? chatId : world.currentChatId;
433
1042
  // Filter memory to current chat only
434
1043
  const currentChatMessages = agent.memory.filter(m => m.chatId === targetChatId);
435
1044
  loggerAgent.debug('Continuing LLM execution with tool result in memory', {
436
1045
  agentId: agent.id,
437
1046
  targetChatId,
438
- worldCurrentChatId: world.currentChatId,
439
1047
  totalMemoryLength: agent.memory.length,
440
1048
  currentChatLength: currentChatMessages.length,
441
1049
  lastFewMessages: currentChatMessages.slice(-5).map(m => ({
@@ -446,7 +1054,7 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
446
1054
  }))
447
1055
  });
448
1056
  // Tool execution already happened before this function was called
449
- // The tool result is already in memory with the actual stdout/stderr
1057
+ // The canonical tool result is already in memory with bounded preview fields for shell output
450
1058
  // Now prepare messages for LLM - loads fresh data from storage
451
1059
  // Prepare messages with system prompt and complete conversation history
452
1060
  const messages = await prepareMessagesForLLM(world.id, agent, targetChatId ?? null);
@@ -485,6 +1093,8 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
485
1093
  }
486
1094
  catch (error) {
487
1095
  loggerAgent.error('Failed to save agent after LLM call increment', {
1096
+ worldId: world.id,
1097
+ chatId: targetChatId,
488
1098
  agentId: agent.id,
489
1099
  error: error instanceof Error ? error.message : error
490
1100
  });
@@ -603,7 +1213,15 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
603
1213
  ? firstInvalidToolCall.function.arguments
604
1214
  : '{}';
605
1215
  const malformedToolErrorContent = rawToolName
606
- ? `Error executing tool: Invalid tool call payload for '${rawToolName}'`
1216
+ ? rawToolName === 'shell_cmd' || rawToolName === 'load_skill'
1217
+ ? formatToolErrorContent({
1218
+ toolName: rawToolName,
1219
+ toolCallId,
1220
+ toolArgs: {},
1221
+ error: `Invalid tool call payload for '${rawToolName}'`,
1222
+ failureReason: 'validation_error',
1223
+ })
1224
+ : `Error executing tool: Invalid tool call payload for '${rawToolName}'`
607
1225
  : 'Error executing tool: Invalid tool call payload - empty or missing tool name';
608
1226
  loggerAgent.warn('Continuation returned tool_calls without executable tool; reporting tool error back to LLM context', {
609
1227
  agentId: agent.id,
@@ -689,7 +1307,10 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
689
1307
  }
690
1308
  catch (error) {
691
1309
  loggerMemory.error('Failed to save malformed continuation tool error context', {
1310
+ worldId: world.id,
1311
+ chatId: targetChatId,
692
1312
  agentId: agent.id,
1313
+ toolCallId,
693
1314
  error: error instanceof Error ? error.message : String(error),
694
1315
  });
695
1316
  }
@@ -699,12 +1320,40 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
699
1320
  ...options,
700
1321
  hopCount: hopCount + 1,
701
1322
  emptyToolCallRetryCount: emptyToolCallRetryCount + 1,
1323
+ continuationRunId,
702
1324
  });
703
1325
  return;
704
1326
  }
705
1327
  publishEvent(world, 'system', {
706
1328
  message: '[Warning] Agent repeatedly returned invalid tool calls after tool execution. Please refine the prompt.',
707
1329
  type: 'warning',
1330
+ }, targetChatId);
1331
+ return;
1332
+ }
1333
+ let requestedLoadSkillId = null;
1334
+ if (toolCall.function.name === 'load_skill') {
1335
+ try {
1336
+ requestedLoadSkillId = getLoadSkillIdFromRawToolArguments(toolCall.function.arguments);
1337
+ }
1338
+ catch {
1339
+ requestedLoadSkillId = null;
1340
+ }
1341
+ }
1342
+ if (requestedLoadSkillId && loadedSkillsForRun.has(requestedLoadSkillId)) {
1343
+ loggerAgent.debug('Suppressing duplicate load_skill call in continuation run', {
1344
+ worldId: world.id,
1345
+ agentId: agent.id,
1346
+ chatId: targetChatId,
1347
+ continuationRunId,
1348
+ skillId: requestedLoadSkillId,
1349
+ toolCallId: toolCall.id,
1350
+ });
1351
+ throwIfMessageProcessingStopped(options?.abortSignal);
1352
+ await continueLLMAfterToolExecution(world, agent, targetChatId, {
1353
+ ...options,
1354
+ hopCount: hopCount + 1,
1355
+ continuationRunId,
1356
+ transientContinuationInstruction: `System notice: Suppressed duplicate load_skill("${requestedLoadSkillId}") in this run because the skill was already loaded. Continue the task using the existing skill context without calling load_skill again for this skill.`,
708
1357
  });
709
1358
  return;
710
1359
  }
@@ -729,7 +1378,10 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
729
1378
  }
730
1379
  catch (error) {
731
1380
  loggerMemory.error('Failed to save assistant tool_call message during continuation', {
1381
+ worldId: world.id,
1382
+ chatId: targetChatId,
732
1383
  agentId: agent.id,
1384
+ toolCallId: toolCall.id,
733
1385
  error: error instanceof Error ? error.message : String(error),
734
1386
  });
735
1387
  }
@@ -751,7 +1403,15 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
751
1403
  if (!toolDef) {
752
1404
  const missingToolResult = {
753
1405
  role: 'tool',
754
- content: `Error executing tool: Tool not found: ${toolCall.function.name}`,
1406
+ content: toolCall.function.name === 'shell_cmd' || toolCall.function.name === 'load_skill'
1407
+ ? formatToolErrorContent({
1408
+ toolName: toolCall.function.name,
1409
+ toolCallId: toolCall.id,
1410
+ toolArgs: {},
1411
+ error: `Tool not found: ${toolCall.function.name}`,
1412
+ failureReason: 'execution_error',
1413
+ })
1414
+ : `Error executing tool: Tool not found: ${toolCall.function.name}`,
755
1415
  tool_call_id: toolCall.id,
756
1416
  sender: agent.id,
757
1417
  createdAt: new Date(),
@@ -783,17 +1443,27 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
783
1443
  await continueLLMAfterToolExecution(world, agent, targetChatId, {
784
1444
  ...options,
785
1445
  hopCount: hopCount + 1,
1446
+ continuationRunId,
786
1447
  });
787
1448
  return;
788
1449
  }
789
1450
  let toolArgs = {};
790
1451
  try {
791
1452
  toolArgs = parseToolCallArguments(toolCall.function.arguments);
1453
+ requestedLoadSkillId = requestedLoadSkillId || getLoadSkillIdFromToolArgs(toolArgs);
792
1454
  }
793
1455
  catch (parseError) {
794
1456
  const parseErrorResult = {
795
1457
  role: 'tool',
796
- content: `Error executing tool: Invalid JSON in tool arguments: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
1458
+ content: toolCall.function.name === 'shell_cmd' || toolCall.function.name === 'load_skill'
1459
+ ? formatToolErrorContent({
1460
+ toolName: toolCall.function.name,
1461
+ toolCallId: toolCall.id,
1462
+ toolArgs,
1463
+ error: `Invalid JSON in tool arguments: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
1464
+ failureReason: 'validation_error',
1465
+ })
1466
+ : `Error executing tool: Invalid JSON in tool arguments: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
797
1467
  tool_call_id: toolCall.id,
798
1468
  sender: agent.id,
799
1469
  createdAt: new Date(),
@@ -825,9 +1495,77 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
825
1495
  await continueLLMAfterToolExecution(world, agent, targetChatId, {
826
1496
  ...options,
827
1497
  hopCount: hopCount + 1,
1498
+ continuationRunId,
828
1499
  });
829
1500
  return;
830
1501
  }
1502
+ let shellCommandSignature = null;
1503
+ const sanitizedToolArgsForEventPayload = sanitizeToolArgsForEventPayload(toolCall.function.name, toolArgs);
1504
+ if (toolCall.function.name === 'shell_cmd') {
1505
+ shellCommandSignature = buildShellCommandSignature(toolArgs, trustedWorkingDirectory);
1506
+ const reusedShellCommandResult = shellCommandResultsForRun.get(shellCommandSignature);
1507
+ if (reusedShellCommandResult !== undefined) {
1508
+ loggerAgent.debug('Suppressing duplicate shell_cmd call in continuation run', {
1509
+ worldId: world.id,
1510
+ agentId: agent.id,
1511
+ chatId: targetChatId,
1512
+ continuationRunId,
1513
+ toolCallId: toolCall.id,
1514
+ });
1515
+ const reusedToolResultMessage = {
1516
+ role: 'tool',
1517
+ content: reusedShellCommandResult,
1518
+ tool_call_id: toolCall.id,
1519
+ sender: agent.id,
1520
+ createdAt: new Date(),
1521
+ chatId: targetChatId,
1522
+ messageId: generateId(),
1523
+ replyToMessageId: messageId,
1524
+ agentId: agent.id,
1525
+ };
1526
+ agent.memory.push(reusedToolResultMessage);
1527
+ if (assistantToolCallMessage.toolCallStatus) {
1528
+ assistantToolCallMessage.toolCallStatus[toolCall.id] = {
1529
+ complete: true,
1530
+ result: reusedShellCommandResult,
1531
+ };
1532
+ }
1533
+ const reusedToolEnvelope = parseToolExecutionEnvelopeContent(reusedShellCommandResult);
1534
+ const reusedToolEventPreview = getToolEventPreviewPayload(reusedShellCommandResult);
1535
+ const reusedToolEventResult = reusedToolEnvelope ? reusedToolEnvelope.result : reusedShellCommandResult;
1536
+ publishToolEvent(world, {
1537
+ agentName: agent.id,
1538
+ type: 'tool-result',
1539
+ messageId: toolCall.id,
1540
+ chatId: targetChatId,
1541
+ toolExecution: {
1542
+ toolName: toolCall.function.name,
1543
+ toolCallId: toolCall.id,
1544
+ input: sanitizedToolArgsForEventPayload,
1545
+ ...(reusedToolEventPreview !== undefined ? { preview: reusedToolEventPreview } : {}),
1546
+ result: reusedToolEventResult,
1547
+ resultType: Array.isArray(reusedToolEventResult)
1548
+ ? 'array'
1549
+ : reusedToolEventResult === null
1550
+ ? 'null'
1551
+ : typeof reusedToolEventResult === 'string'
1552
+ ? 'string'
1553
+ : 'object',
1554
+ resultSize: reusedShellCommandResult.length,
1555
+ metadata: { reusedFromContinuationRun: true },
1556
+ },
1557
+ });
1558
+ const storage = await getStorageWrappers();
1559
+ await storage.saveAgent(world.id, agent);
1560
+ await continueLLMAfterToolExecution(world, agent, targetChatId, {
1561
+ ...options,
1562
+ hopCount: hopCount + 1,
1563
+ continuationRunId,
1564
+ transientContinuationInstruction: 'System notice: Suppressed duplicate shell_cmd call in this continuation run and reused its previous result. Continue from the existing command output without rerunning the same command.',
1565
+ });
1566
+ return;
1567
+ }
1568
+ }
831
1569
  publishToolEvent(world, {
832
1570
  agentName: agent.id,
833
1571
  type: 'tool-start',
@@ -836,7 +1574,7 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
836
1574
  toolExecution: {
837
1575
  toolName: toolCall.function.name,
838
1576
  toolCallId: toolCall.id,
839
- input: toolArgs,
1577
+ input: sanitizedToolArgsForEventPayload,
840
1578
  metadata: {
841
1579
  isStreaming: isStreamingEnabled(),
842
1580
  },
@@ -850,11 +1588,24 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
850
1588
  chatId: targetChatId,
851
1589
  abortSignal: options?.abortSignal,
852
1590
  workingDirectory: trustedWorkingDirectory,
1591
+ agentName: agent.id,
1592
+ llmResultMode: resolveShellContinuationLlmResultMode({
1593
+ toolName: toolCall.function.name,
1594
+ }),
1595
+ persistToolEnvelope: toolCall.function.name === 'shell_cmd' || toolCall.function.name === 'load_skill',
853
1596
  };
854
1597
  const toolResult = await toolDef.execute(toolArgs, undefined, undefined, toolContext);
855
1598
  const serializedToolResult = typeof toolResult === 'string'
856
1599
  ? toolResult
857
1600
  : JSON.stringify(toolResult) ?? String(toolResult);
1601
+ if (toolCall.function.name === 'load_skill'
1602
+ && requestedLoadSkillId
1603
+ && isSuccessfulLoadSkillResult(serializedToolResult)) {
1604
+ loadedSkillsForRun.add(requestedLoadSkillId);
1605
+ }
1606
+ if (toolCall.function.name === 'shell_cmd' && shellCommandSignature) {
1607
+ shellCommandResultsForRun.set(shellCommandSignature, serializedToolResult);
1608
+ }
858
1609
  const toolResultMessage = {
859
1610
  role: 'tool',
860
1611
  content: serializedToolResult,
@@ -873,6 +1624,9 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
873
1624
  result: serializedToolResult,
874
1625
  };
875
1626
  }
1627
+ const toolEnvelope = parseToolExecutionEnvelopeContent(serializedToolResult);
1628
+ const toolEventPreview = getToolEventPreviewPayload(serializedToolResult);
1629
+ const toolEventResult = toolEnvelope ? toolEnvelope.result : toolResult;
876
1630
  publishToolEvent(world, {
877
1631
  agentName: agent.id,
878
1632
  type: 'tool-result',
@@ -881,21 +1635,27 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
881
1635
  toolExecution: {
882
1636
  toolName: toolCall.function.name,
883
1637
  toolCallId: toolCall.id,
884
- input: toolArgs,
885
- result: serializedToolResult.slice(0, 4000),
886
- resultType: typeof toolResult === 'string'
887
- ? 'string'
888
- : Array.isArray(toolResult)
889
- ? 'array'
890
- : toolResult === null
891
- ? 'null'
1638
+ input: sanitizedToolArgsForEventPayload,
1639
+ ...(toolEventPreview !== undefined ? { preview: toolEventPreview } : {}),
1640
+ result: toolEventResult,
1641
+ resultType: Array.isArray(toolEventResult)
1642
+ ? 'array'
1643
+ : toolEventResult === null
1644
+ ? 'null'
1645
+ : typeof toolEventResult === 'string'
1646
+ ? 'string'
892
1647
  : 'object',
893
1648
  resultSize: serializedToolResult.length,
894
1649
  },
895
1650
  });
896
1651
  }
897
1652
  catch (toolError) {
898
- const errorContent = `Error executing tool: ${toolError instanceof Error ? toolError.message : String(toolError)}`;
1653
+ const errorContent = formatToolErrorContent({
1654
+ toolName: toolCall.function.name,
1655
+ toolCallId: toolCall.id,
1656
+ toolArgs,
1657
+ error: toolError,
1658
+ });
899
1659
  const toolErrorMessage = {
900
1660
  role: 'tool',
901
1661
  content: errorContent,
@@ -922,7 +1682,7 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
922
1682
  toolExecution: {
923
1683
  toolName: toolCall.function.name,
924
1684
  toolCallId: toolCall.id,
925
- input: toolArgs,
1685
+ input: sanitizedToolArgsForEventPayload,
926
1686
  error: toolError instanceof Error ? toolError.message : String(toolError),
927
1687
  },
928
1688
  });
@@ -933,7 +1693,10 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
933
1693
  }
934
1694
  catch (error) {
935
1695
  loggerMemory.error('Failed to save continuation tool result to memory', {
1696
+ worldId: world.id,
1697
+ chatId: targetChatId,
936
1698
  agentId: agent.id,
1699
+ toolCallId: toolCall.id,
937
1700
  error: error instanceof Error ? error.message : String(error),
938
1701
  });
939
1702
  }
@@ -941,6 +1704,7 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
941
1704
  await continueLLMAfterToolExecution(world, agent, targetChatId, {
942
1705
  ...options,
943
1706
  hopCount: hopCount + 1,
1707
+ continuationRunId,
944
1708
  });
945
1709
  return;
946
1710
  }
@@ -964,6 +1728,7 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
964
1728
  await continueLLMAfterToolExecution(world, agent, targetChatId, {
965
1729
  ...options,
966
1730
  emptyTextRetryCount: emptyTextRetryCount + 1,
1731
+ continuationRunId,
967
1732
  });
968
1733
  return;
969
1734
  }
@@ -981,7 +1746,7 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
981
1746
  publishEvent(world, 'system', {
982
1747
  message: '[Warning] Agent returned empty follow-up after tool execution. Please retry or refine the prompt.',
983
1748
  type: 'warning'
984
- });
1749
+ }, targetChatId);
985
1750
  logToolBridge('CONTINUE EMPTY_TEXT_STOP', {
986
1751
  worldId: world.id,
987
1752
  agentId: agent.id,
@@ -1015,12 +1780,24 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
1015
1780
  }
1016
1781
  catch (error) {
1017
1782
  loggerMemory.error('Failed to save agent response after tool execution', {
1783
+ worldId: world.id,
1784
+ chatId: targetChatId,
1018
1785
  agentId: agent.id,
1786
+ messageId,
1019
1787
  error: error instanceof Error ? error.message : String(error)
1020
1788
  });
1021
1789
  }
1022
1790
  // Publish the response message using the same messageId from streaming
1023
1791
  publishMessageWithId(world, sanitizedResponse, agent.id, messageId, targetChatId, undefined);
1792
+ loggerMessagePublish.debug('Published assistant response message', {
1793
+ worldId: world.id,
1794
+ chatId: targetChatId,
1795
+ agentId: agent.id,
1796
+ messageId,
1797
+ turnId: messageId,
1798
+ responseLength: sanitizedResponse.length,
1799
+ source: 'continuation',
1800
+ });
1024
1801
  loggerAgent.debug('Agent response published after tool execution', {
1025
1802
  agentId: agent.id,
1026
1803
  messageId,
@@ -1031,46 +1808,61 @@ export async function continueLLMAfterToolExecution(world, agent, chatId, option
1031
1808
  if (isMessageProcessingCanceledError(error) || options?.abortSignal?.aborted) {
1032
1809
  loggerAgent.info('Skipped continuation after stop request', {
1033
1810
  agentId: agent.id,
1034
- chatId: chatId ?? world.currentChatId ?? null,
1811
+ chatId: targetChatId,
1035
1812
  error: error instanceof Error ? error.message : String(error)
1036
1813
  });
1037
1814
  logToolBridge('CONTINUE CANCELED', {
1038
1815
  worldId: world.id,
1039
1816
  agentId: agent.id,
1040
- chatId: chatId ?? world.currentChatId ?? null,
1817
+ chatId: targetChatId,
1041
1818
  error: error instanceof Error ? error.message : String(error),
1042
1819
  });
1043
1820
  return;
1044
1821
  }
1045
1822
  loggerAgent.error('Failed to continue LLM after tool execution', {
1823
+ worldId: world.id,
1824
+ chatId: targetChatId,
1046
1825
  agentId: agent.id,
1047
1826
  error: error instanceof Error ? error.message : error
1048
1827
  });
1049
1828
  publishEvent(world, 'system', {
1050
1829
  message: `[Error] ${error.message}`,
1051
1830
  type: 'error'
1052
- });
1831
+ }, targetChatId);
1053
1832
  logToolBridge('CONTINUE ERROR', {
1054
1833
  worldId: world.id,
1055
1834
  agentId: agent.id,
1056
- chatId: chatId ?? world.currentChatId ?? null,
1835
+ chatId: targetChatId,
1057
1836
  error: error instanceof Error ? error.message : String(error),
1058
1837
  });
1059
1838
  }
1060
1839
  finally {
1840
+ leaveContinuationScope(continuationScopeKey, continuationRunId);
1841
+ cleanupContinuationRunState(continuationRunId);
1061
1842
  completeActivity();
1062
1843
  }
1063
1844
  }
1064
1845
  /**
1065
1846
  * Handle text response from LLM (extracted for clarity)
1066
- * @param chatId - Explicit chat ID for concurrency-safe processing. If not provided, uses messageEvent.chatId or world.currentChatId.
1847
+ * @param chatId - Explicit chat ID for concurrency-safe processing. When omitted, `messageEvent.chatId` is used.
1067
1848
  */
1068
1849
  export async function handleTextResponse(world, agent, responseText, messageId, messageEvent, chatId) {
1069
- // Derive target chatId: explicit parameter > message event > world.currentChatId
1070
- const targetChatId = chatId !== undefined ? chatId : (messageEvent.chatId ?? world.currentChatId ?? null);
1850
+ const explicitChatId = typeof chatId === 'string' ? chatId.trim() : '';
1851
+ const messageChatId = typeof messageEvent.chatId === 'string' ? messageEvent.chatId.trim() : '';
1852
+ const targetChatId = explicitChatId || messageChatId;
1853
+ if (!targetChatId) {
1854
+ throw new Error(`handleTextResponse: explicit chatId is required for agent ${agent.id}`);
1855
+ }
1071
1856
  const sanitizedResponse = removeSelfMentions(responseText, agent.id);
1857
+ // const needsInfiniteEtudePedagogueHandoff =
1858
+ // world.id === 'infinite-etude' &&
1859
+ // agent.id === 'madame-pedagogue' &&
1860
+ // !/^\s*@monsieur-engraver\b/im.test(sanitizedResponse);
1861
+ // const responseWithRequiredHandoff = needsInfiniteEtudePedagogueHandoff
1862
+ // ? `${sanitizedResponse.trimEnd()}\n\n@monsieur-engraver please render this.`
1863
+ // : sanitizedResponse;
1072
1864
  // Apply auto-mention logic if needed
1073
- let finalResponse = sanitizedResponse;
1865
+ let finalResponse = sanitizedResponse; // responseWithRequiredHandoff;
1074
1866
  if (agent.autoReply !== false && shouldAutoMention(sanitizedResponse, messageEvent.sender, agent.id)) {
1075
1867
  finalResponse = addAutoMention(sanitizedResponse, messageEvent.sender);
1076
1868
  loggerAutoMention.debug('Auto-mention applied', {
@@ -1108,12 +1900,25 @@ export async function handleTextResponse(world, agent, responseText, messageId,
1108
1900
  }
1109
1901
  catch (error) {
1110
1902
  loggerMemory.error('Failed to save agent response', {
1903
+ worldId: world.id,
1904
+ chatId: targetChatId,
1111
1905
  agentId: agent.id,
1906
+ messageId,
1112
1907
  error: error instanceof Error ? error.message : String(error)
1113
1908
  });
1114
1909
  }
1115
1910
  // Publish the response message using the same messageId from streaming
1116
1911
  publishMessageWithId(world, finalResponse, agent.id, messageId, targetChatId, messageEvent.messageId);
1912
+ loggerMessagePublish.debug('Published assistant response message', {
1913
+ worldId: world.id,
1914
+ chatId: targetChatId,
1915
+ agentId: agent.id,
1916
+ messageId,
1917
+ turnId: messageId,
1918
+ replyToMessageId: messageEvent.messageId,
1919
+ responseLength: finalResponse.length,
1920
+ source: 'direct',
1921
+ });
1117
1922
  loggerAgent.debug('Agent response published', {
1118
1923
  agentId: agent.id,
1119
1924
  messageId,
@@ -1156,10 +1961,7 @@ export async function generateChatTitleFromMessages(world, content, targetChatId
1156
1961
  const storage = await getStorageWrappers();
1157
1962
  // Load messages for the target chat only, not all messages.
1158
1963
  messages = targetChatId ? await storage.getMemory(world.id, targetChatId) : [];
1159
- if (content) {
1160
- messages.push({ role: 'user', content });
1161
- }
1162
- promptMessages = buildTitlePromptMessages(messages);
1964
+ promptMessages = buildTitlePromptMessages(messages, content);
1163
1965
  loggerChatTitle.debug('Calling LLM for title generation', {
1164
1966
  messageCount: messages.length,
1165
1967
  promptMessageCount: promptMessages.length,
@@ -1170,19 +1972,22 @@ export async function generateChatTitleFromMessages(world, content, targetChatId
1170
1972
  const tempAgent = {
1171
1973
  provider: world.chatLLMProvider || firstAgent?.provider || 'openai',
1172
1974
  model: world.chatLLMModel || firstAgent?.model || 'gpt-4',
1173
- systemPrompt: 'You are a helpful assistant that turns conversations into concise titles.',
1975
+ systemPrompt: 'You are a concise title generator. Given a conversation snippet, output a short noun-phrase title (3–6 words, Title Case). Rules: output the title only — no explanation, no punctuation at the end; never copy the user message verbatim; if the user message begins with @agentname, that is an agent mention — base the title on the topic or task after it, not on the mention itself.',
1174
1976
  maxTokens: 20,
1175
1977
  };
1176
1978
  const userPrompt = {
1177
1979
  role: 'user',
1178
- content: `Below is a conversation between a user and an assistant. Generate a short, punchy title (3–6 words) that captures its main topic.
1179
-
1180
- ${promptMessages.map(msg => `-${msg.role}: ${msg.content}`).join('\n')}
1181
- `
1980
+ content: `Generate a short title (3–6 words, Title Case) for this conversation.\nRules:\n- Do NOT copy the user message word-for-word.\n- An @name prefix (e.g. "@gemini") is an agent mention — base the title on the topic or task, not the mention.\n- Output the title only.\n\n${promptMessages.map(msg => `-${msg.role}: ${msg.content}`).join('\n')}`
1981
+ };
1982
+ const titleGenerationWorld = {
1983
+ ...world,
1984
+ // Force reasoning_effort=none so thinking models (e.g. Gemini 2.5 Flash)
1985
+ // don't exhaust the maxOutputTokens budget on thinking before outputting text.
1986
+ variables: removeEnvVariableFromText(world.variables, 'reasoning_effort') + '\nreasoning_effort=none',
1182
1987
  };
1183
- const { response: titleResponse } = await generateAgentResponse(world, tempAgent, [userPrompt], undefined, true, targetChatId); // skipTools = true for title generation
1184
- // Title generation should return plain text when skipTools=true; keep a guard for safety.
1185
- title = typeof titleResponse === 'string' ? titleResponse : '';
1988
+ const { response: titleResponse } = await generateAgentResponse(titleGenerationWorld, tempAgent, [userPrompt], undefined, true, targetChatId); // skipTools = true for title generation
1989
+ // LLMResponse is an object {type, content?} extract the text content.
1990
+ title = titleResponse?.type === 'text' ? (titleResponse?.content ?? '') : '';
1186
1991
  loggerChatTitle.debug('LLM generated title', { rawTitle: title });
1187
1992
  }
1188
1993
  catch (error) {