@poolzin/pool-bot 2026.3.25 → 2026.3.26

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 (271) hide show
  1. package/dist/agents/model-fallback.js +5 -4
  2. package/dist/agents/tools/common.js +16 -201
  3. package/dist/auto-reply/auto-reply/reply/agent-runner-execution.js +502 -0
  4. package/dist/auto-reply/auto-reply/reply/agent-runner-helpers.js +65 -0
  5. package/dist/auto-reply/auto-reply/reply/agent-runner-memory.js +160 -0
  6. package/dist/auto-reply/auto-reply/reply/agent-runner-payloads.js +85 -0
  7. package/dist/auto-reply/auto-reply/reply/agent-runner-utils.js +101 -0
  8. package/dist/auto-reply/auto-reply/reply/bash-command.js +338 -0
  9. package/dist/auto-reply/auto-reply/reply/block-streaming.js +91 -0
  10. package/dist/auto-reply/auto-reply/reply/commands-approve.js +88 -0
  11. package/dist/auto-reply/auto-reply/reply/commands-bash.js +26 -0
  12. package/dist/auto-reply/auto-reply/reply/commands-compact.js +107 -0
  13. package/dist/auto-reply/auto-reply/reply/commands-config.js +241 -0
  14. package/dist/auto-reply/auto-reply/reply/commands-context-report.js +295 -0
  15. package/dist/auto-reply/auto-reply/reply/commands-context.js +30 -0
  16. package/dist/auto-reply/auto-reply/reply/commands-core.js +151 -0
  17. package/dist/auto-reply/auto-reply/reply/commands-export-session.js +163 -0
  18. package/dist/auto-reply/auto-reply/reply/commands-info.js +184 -0
  19. package/dist/auto-reply/auto-reply/reply/commands-models.js +299 -0
  20. package/dist/auto-reply/auto-reply/reply/commands-plugin.js +35 -0
  21. package/dist/auto-reply/auto-reply/reply/commands-ptt.js +171 -0
  22. package/dist/auto-reply/auto-reply/reply/commands-setunset-standard.js +13 -0
  23. package/dist/auto-reply/auto-reply/reply/commands-setunset.js +73 -0
  24. package/dist/auto-reply/auto-reply/reply/commands-slash-parse.js +31 -0
  25. package/dist/auto-reply/auto-reply/reply/commands-status.js +178 -0
  26. package/dist/auto-reply/auto-reply/reply/commands-subagents.js +73 -0
  27. package/dist/auto-reply/auto-reply/reply/commands-system-prompt.js +117 -0
  28. package/dist/auto-reply/auto-reply/reply/commands-tts.js +231 -0
  29. package/dist/auto-reply/auto-reply/reply/directive-handling.impl.js +380 -0
  30. package/dist/auto-reply/auto-reply/reply/followup-runner.js +227 -0
  31. package/dist/auto-reply/auto-reply/reply/get-reply-directives-apply.js +201 -0
  32. package/dist/auto-reply/auto-reply/reply/get-reply-directives-utils.js +54 -0
  33. package/dist/auto-reply/auto-reply/reply/get-reply-directives.js +332 -0
  34. package/dist/auto-reply/auto-reply/reply/get-reply-inline-actions.js +258 -0
  35. package/dist/auto-reply/auto-reply/reply/get-reply-run.js +297 -0
  36. package/dist/auto-reply/auto-reply/reply/groups.js +102 -0
  37. package/dist/auto-reply/auto-reply/reply/mentions.js +129 -0
  38. package/dist/auto-reply/auto-reply/reply/reply-delivery.js +92 -0
  39. package/dist/auto-reply/auto-reply/reply/reply-directives.js +30 -0
  40. package/dist/auto-reply/auto-reply/reply/reply-dispatcher.js +152 -0
  41. package/dist/auto-reply/auto-reply/reply/reply-elevated.js +166 -0
  42. package/dist/auto-reply/auto-reply/reply/reply-inline.js +28 -0
  43. package/dist/auto-reply/auto-reply/reply/reply-payloads.js +114 -0
  44. package/dist/auto-reply/auto-reply/reply/reply-reference.js +36 -0
  45. package/dist/auto-reply/auto-reply/reply/reply-tags.js +13 -0
  46. package/dist/auto-reply/auto-reply/reply/reply-threading.js +41 -0
  47. package/dist/auto-reply/auto-reply/reply/session-updates.js +233 -0
  48. package/dist/auto-reply/auto-reply/reply/stage-sandbox-media.js +146 -0
  49. package/dist/build-info.json +3 -3
  50. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  51. package/dist/canvas-host/a2ui/a2ui.bundle.js +2 -17772
  52. package/dist/canvas-host/a2ui/index.html +1 -307
  53. package/dist/channels/channels/directory-config.js +185 -0
  54. package/dist/channels/channels/discord/handle-action.guild-admin.js +332 -0
  55. package/dist/channels/channels/discord/handle-action.js +165 -0
  56. package/dist/channels/channels/discord.js +413 -0
  57. package/dist/channels/channels/dock.js +436 -0
  58. package/dist/channels/channels/index.js +51 -0
  59. package/dist/channels/channels/plugins/outbound/discord.js +101 -0
  60. package/dist/channels/channels/whatsapp.js +17 -0
  61. package/dist/channels/plugins/types.js +1 -1
  62. package/dist/channels/run-state-machine.js +7 -0
  63. package/dist/commands/models/auth.js +47 -1
  64. package/dist/commands-subagents/action-agents.js +44 -0
  65. package/dist/commands-subagents/action-focus.js +64 -0
  66. package/dist/commands-subagents/action-help.js +4 -0
  67. package/dist/commands-subagents/action-info.js +45 -0
  68. package/dist/commands-subagents/action-kill.js +60 -0
  69. package/dist/commands-subagents/action-list.js +44 -0
  70. package/dist/commands-subagents/action-log.js +29 -0
  71. package/dist/commands-subagents/action-send.js +119 -0
  72. package/dist/commands-subagents/action-spawn.js +52 -0
  73. package/dist/commands-subagents/action-unfocus.js +30 -0
  74. package/dist/commands-subagents/shared.js +303 -0
  75. package/dist/config/config.js +1 -8
  76. package/dist/config/types.secrets.js +61 -0
  77. package/dist/control-ui/assets/{index-D7shnQwQ.js → index-umCsvrWy.js} +884 -741
  78. package/dist/control-ui/assets/index-umCsvrWy.js.map +1 -0
  79. package/dist/control-ui/assets/pt-BR-DedEVAvY.js +2 -0
  80. package/dist/control-ui/assets/pt-BR-DedEVAvY.js.map +1 -0
  81. package/dist/control-ui/assets/zh-CN-CDzeklK-.js +2 -0
  82. package/dist/control-ui/assets/zh-CN-CDzeklK-.js.map +1 -0
  83. package/dist/control-ui/assets/zh-TW-BJCRYNWH.js +2 -0
  84. package/dist/control-ui/assets/zh-TW-BJCRYNWH.js.map +1 -0
  85. package/dist/control-ui/index.html +1 -1
  86. package/dist/gateway/method-scopes.js +9 -1
  87. package/dist/gateway/node-pending-work.js +142 -0
  88. package/dist/gateway/protocol/index.js +5 -1
  89. package/dist/gateway/protocol/schema/nodes.js +18 -0
  90. package/dist/gateway/server-methods/nodes-pending.js +96 -0
  91. package/dist/gateway/server-methods-list.js +4 -0
  92. package/dist/gateway/server-methods.js +2 -0
  93. package/dist/imessage/channel.js +253 -0
  94. package/dist/imessage/monitor/echo-cache.js +70 -0
  95. package/dist/imessage/monitor/loop-rate-limiter.js +51 -0
  96. package/dist/imessage/monitor/reflection-guard.js +50 -0
  97. package/dist/imessage/monitor/sanitize-outbound.js +25 -0
  98. package/dist/imessage/monitor/self-chat-cache.js +75 -0
  99. package/dist/imessage/runtime.js +3 -0
  100. package/dist/infra/exec-approval-reply.js +7 -0
  101. package/dist/infra/tmp-openclaw-dir.js +84 -0
  102. package/dist/pairing/pairing-challenge.js +15 -0
  103. package/dist/plugin-sdk/account-id.d.ts +1 -0
  104. package/dist/plugin-sdk/agent-media-payload.d.ts +12 -0
  105. package/dist/plugin-sdk/allow-from.d.ts +27 -0
  106. package/dist/plugin-sdk/command-auth.d.ts +25 -0
  107. package/dist/plugin-sdk/command-auth.js +3 -1
  108. package/dist/plugin-sdk/config-paths.d.ts +6 -0
  109. package/dist/plugin-sdk/file-lock.d.ts +16 -0
  110. package/dist/plugin-sdk/index.d.ts +428 -0
  111. package/dist/plugin-sdk/index.js +237 -103
  112. package/dist/plugin-sdk/json-store.d.ts +5 -0
  113. package/dist/plugin-sdk/keyed-async-queue.d.ts +12 -0
  114. package/dist/plugin-sdk/onboarding.d.ts +11 -0
  115. package/dist/plugin-sdk/provider-auth-result.d.ts +14 -0
  116. package/dist/plugin-sdk/slack-message-actions.d.ts +11 -0
  117. package/dist/plugin-sdk/status-helpers.d.ts +25 -0
  118. package/dist/plugin-sdk/temp-path.d.ts +12 -0
  119. package/dist/plugin-sdk/text-chunking.d.ts +1 -0
  120. package/dist/plugin-sdk/tool-send.d.ts +4 -0
  121. package/dist/plugin-sdk/webhook-path.d.ts +6 -0
  122. package/dist/plugin-sdk/webhook-targets.d.ts +23 -0
  123. package/dist/plugin-sdk/windows-spawn.d.ts +39 -0
  124. package/dist/plugin-sdk-internal/accounts.js +6 -0
  125. package/dist/plugin-sdk-internal/discord.js +23 -0
  126. package/dist/plugin-sdk-internal/imessage.js +13 -0
  127. package/dist/plugin-sdk-internal/setup.js +9 -0
  128. package/dist/plugin-sdk-internal/signal.js +13 -0
  129. package/dist/plugin-sdk-internal/slack.js +22 -0
  130. package/dist/plugin-sdk-internal/telegram.js +32 -0
  131. package/dist/plugin-sdk-internal/whatsapp.js +29 -0
  132. package/dist/routing/session-key.js +4 -185
  133. package/dist/shared/pid-alive.js +2 -61
  134. package/dist/shared/process-scoped-map.js +5 -7
  135. package/dist/signal/channel.js +264 -0
  136. package/dist/signal/monitor/access-policy.js +60 -0
  137. package/dist/signal/runtime.js +3 -0
  138. package/dist/slack/account-inspect.js +135 -0
  139. package/dist/slack/blocks-input.js +7 -38
  140. package/dist/slack/channel.js +394 -0
  141. package/dist/slack/interactive-replies.js +28 -0
  142. package/dist/slack/monitor/channel-type.js +31 -0
  143. package/dist/slack/monitor/dm-auth.js +49 -0
  144. package/dist/slack/monitor/events/interactions.modal.js +137 -0
  145. package/dist/slack/monitor/events/message-subtype-handlers.js +68 -0
  146. package/dist/slack/monitor/events/system-event-context.js +29 -0
  147. package/dist/slack/monitor/events/system-event-test-harness.js +41 -0
  148. package/dist/slack/monitor/external-arg-menu-store.js +46 -0
  149. package/dist/slack/monitor/message-handler/prepare-content.js +69 -0
  150. package/dist/slack/monitor/message-handler/prepare-thread-context.js +91 -0
  151. package/dist/slack/monitor/message-handler/prepare.test-helpers.js +55 -0
  152. package/dist/slack/monitor/reconnect-policy.js +78 -0
  153. package/dist/slack/monitor/slash-commands.runtime.js +1 -0
  154. package/dist/slack/monitor/slash-dispatch.runtime.js +9 -0
  155. package/dist/slack/monitor/slash-skill-commands.runtime.js +1 -0
  156. package/dist/slack/resolve-allowlist-common.js +36 -0
  157. package/dist/slack/runtime.js +3 -0
  158. package/dist/slack/sent-thread-cache.js +61 -0
  159. package/dist/slack/truncate.js +10 -0
  160. package/dist/telegram/account-inspect.js +175 -0
  161. package/dist/telegram/allow-from.js +10 -0
  162. package/dist/telegram/api-fetch.js +18 -0
  163. package/dist/telegram/approval-buttons.js +30 -0
  164. package/dist/telegram/audit-membership-runtime.js +61 -0
  165. package/dist/telegram/bot/delivery.replies.js +508 -0
  166. package/dist/telegram/bot/delivery.resolve-media.js +227 -0
  167. package/dist/telegram/bot/delivery.send.js +132 -0
  168. package/dist/telegram/bot/reply-threading.js +46 -0
  169. package/dist/telegram/bot-message-context.body.js +186 -0
  170. package/dist/telegram/bot-message-context.session.js +207 -0
  171. package/dist/telegram/bot-message-context.types.js +1 -0
  172. package/dist/telegram/bot-native-commands.test-helpers.js +117 -0
  173. package/dist/telegram/bot.media.e2e-harness.js +81 -0
  174. package/dist/telegram/bot.media.test-utils.js +81 -0
  175. package/dist/telegram/channel-actions.js +225 -0
  176. package/dist/telegram/channel.js +515 -0
  177. package/dist/telegram/conversation-route.js +107 -0
  178. package/dist/telegram/delivery.js +2 -0
  179. package/dist/telegram/delivery.replies.js +508 -0
  180. package/dist/telegram/dm-access.js +86 -0
  181. package/dist/telegram/draft-stream.test-helpers.js +62 -0
  182. package/dist/telegram/exec-approvals-handler.js +281 -0
  183. package/dist/telegram/exec-approvals.js +62 -0
  184. package/dist/telegram/forum-service-message.js +22 -0
  185. package/dist/telegram/group-config-helpers.js +10 -0
  186. package/dist/telegram/lane-delivery-state.js +19 -0
  187. package/dist/telegram/lane-delivery-text-deliverer.js +357 -0
  188. package/dist/telegram/lane-delivery.js +2 -0
  189. package/dist/telegram/normalize.js +37 -0
  190. package/dist/telegram/onboarding.js +192 -0
  191. package/dist/telegram/outbound-adapter.js +100 -0
  192. package/dist/telegram/polling-session.js +275 -0
  193. package/dist/telegram/runtime.js +3 -0
  194. package/dist/telegram/sendchataction-401-backoff.js +71 -0
  195. package/dist/telegram/sequential-key.js +46 -0
  196. package/dist/telegram/status-issues.js +105 -0
  197. package/dist/telegram/target-writeback.js +165 -0
  198. package/dist/telegram/thread-bindings.js +560 -0
  199. package/dist/utils.js +10 -276
  200. package/dist/wizard/prompts.js +5 -5
  201. package/extensions/feishu/src/policy.ts +1 -1
  202. package/extensions/firecrawl/index.test.ts +82 -0
  203. package/extensions/firecrawl/index.ts +20 -0
  204. package/extensions/firecrawl/openclaw.plugin.json +8 -0
  205. package/extensions/firecrawl/package.json +12 -0
  206. package/extensions/firecrawl/src/config.ts +159 -0
  207. package/extensions/firecrawl/src/firecrawl-client.ts +446 -0
  208. package/extensions/firecrawl/src/firecrawl-scrape-tool.ts +89 -0
  209. package/extensions/firecrawl/src/firecrawl-search-provider.ts +63 -0
  210. package/extensions/firecrawl/src/firecrawl-search-tool.ts +76 -0
  211. package/package.json +1 -1
  212. package/dist/.buildstamp +0 -1
  213. package/dist/acp/bindings-store.js +0 -209
  214. package/dist/acp/control-plane/runtime-cache.js +0 -54
  215. package/dist/acp/control-plane/runtime-options.js +0 -215
  216. package/dist/acp/control-plane/session-actor-queue.js +0 -36
  217. package/dist/acp/index.js +0 -2
  218. package/dist/acp/runtime/errors.js +0 -47
  219. package/dist/acp/runtime/registry.js +0 -86
  220. package/dist/acp/secret-file.js +0 -22
  221. package/dist/agents/auth-profiles.resolve-auth-profile-order.fixtures.js +0 -23
  222. package/dist/agents/bash-process-registry.test-helpers.js +0 -29
  223. package/dist/agents/bash-tools.exec-approval-request.js +0 -20
  224. package/dist/agents/bash-tools.exec-host-gateway.js +0 -240
  225. package/dist/agents/bash-tools.exec-host-node.js +0 -235
  226. package/dist/agents/checkpoint-manager.js +0 -290
  227. package/dist/agents/claude-cli-runner.js +0 -3
  228. package/dist/agents/error-classifier.js +0 -251
  229. package/dist/agents/live-model-filter.js +0 -84
  230. package/dist/agents/nvidia-models.js +0 -228
  231. package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +0 -34
  232. package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +0 -156
  233. package/dist/agents/pi-embedded-subscribe.handlers.tools.media.test-helpers.js +0 -30
  234. package/dist/agents/provider/config-loader.js +0 -76
  235. package/dist/agents/provider/index.js +0 -15
  236. package/dist/agents/provider/models-dev.js +0 -129
  237. package/dist/agents/provider/session-binding.js +0 -376
  238. package/dist/agents/queued-file-writer.js +0 -22
  239. package/dist/agents/skills/bundled-context.js +0 -23
  240. package/dist/agents/skills/security.js +0 -211
  241. package/dist/agents/skills/tools-dir.js +0 -9
  242. package/dist/agents/skills-install-download.js +0 -290
  243. package/dist/agents/skills-install-output.js +0 -30
  244. package/dist/agents/skills-install.download-test-utils.js +0 -36
  245. package/dist/agents/skills.test-helpers.js +0 -13
  246. package/dist/agents/subagent-announce-reliability.js +0 -160
  247. package/dist/agents/subagent-registry.mocks.shared.js +0 -12
  248. package/dist/agents/test-helpers/assistant-message-fixtures.js +0 -29
  249. package/dist/agents/test-helpers/fast-coding-tools.js +0 -1
  250. package/dist/agents/test-helpers/fast-core-tools.js +0 -8
  251. package/dist/agents/test-helpers/fast-tool-stubs.js +0 -18
  252. package/dist/agents/test-helpers/host-sandbox-fs-bridge.js +0 -74
  253. package/dist/agents/test-helpers/pi-tools-sandbox-context.js +0 -27
  254. package/dist/agents/tool-display-common.js +0 -915
  255. package/dist/agents/tool-policy-shared.js +0 -108
  256. package/dist/agents/tool-policy.conformance.js +0 -14
  257. package/dist/agents/tool-result-truncation.js +0 -299
  258. package/dist/agents/tools/cron-tool.test-helpers.js +0 -12
  259. package/dist/agents/tools/discord-actions-moderation-shared.js +0 -27
  260. package/dist/agents/tools/discord-actions-presence.js +0 -78
  261. package/dist/control-ui/assets/index-D7shnQwQ.js.map +0 -1
  262. package/dist/discord/discord-improvements.js +0 -167
  263. package/dist/discord/index.js +0 -2
  264. package/dist/hooks/bundled/boot-md/HOOK.md +0 -19
  265. package/dist/hooks/bundled/command-logger/HOOK.md +0 -122
  266. package/dist/hooks/bundled/session-memory/HOOK.md +0 -86
  267. package/dist/hooks/bundled/soul-evil/HOOK.md +0 -71
  268. package/dist/whatsapp/normalize.js +0 -66
  269. package/dist/whatsapp/resolve-outbound-target.js +0 -42
  270. /package/dist/{acp/runtime/types.js → auto-reply/auto-reply/reply/commands-types.js} +0 -0
  271. /package/dist/{agents/pi-embedded-payloads.js → slack/account-surface-fields.js} +0 -0
@@ -0,0 +1,502 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js";
4
+ import { runCliAgent } from "../../agents/cli-runner.js";
5
+ import { getCliSessionId } from "../../agents/cli-session.js";
6
+ import { runWithModelFallback } from "../../agents/model-fallback.js";
7
+ import { isCliProvider } from "../../agents/model-selection.js";
8
+ import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
9
+ import { isCompactionFailureError, isContextOverflowError, isLikelyContextOverflowError, isTransientHttpError, sanitizeUserFacingText, } from "../../agents/pi-embedded-helpers.js";
10
+ import { resolveAgentIdFromSessionKey, resolveGroupSessionKey, resolveSessionTranscriptPath, updateSessionStore, } from "../../config/sessions.js";
11
+ import { logVerbose } from "../../globals.js";
12
+ import { emitAgentEvent, registerAgentRunContext } from "../../infra/agent-events.js";
13
+ import { defaultRuntime } from "../../runtime.js";
14
+ import { isMarkdownCapableMessageChannel, resolveMessageChannel, } from "../../utils/message-channel.js";
15
+ import { stripHeartbeatToken } from "../heartbeat.js";
16
+ import { isSilentReplyPrefixText, isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
17
+ import { buildThreadingToolContext, resolveEnforceFinalTag } from "./agent-runner-utils.js";
18
+ import { createBlockReplyPayloadKey } from "./block-reply-pipeline.js";
19
+ import { parseReplyDirectives } from "./reply-directives.js";
20
+ import { applyReplyTagsToPayload, isRenderablePayload } from "./reply-payloads.js";
21
+ export async function runAgentTurnWithFallback(params) {
22
+ const TRANSIENT_HTTP_RETRY_DELAY_MS = 2_500;
23
+ let didLogHeartbeatStrip = false;
24
+ let autoCompactionCompleted = false;
25
+ let didRetryTransientHttpError = false;
26
+ // Track payloads sent directly (not via pipeline) during tool flush to avoid duplicates.
27
+ const directlySentBlockKeys = new Set();
28
+ const runId = params.opts?.runId ?? crypto.randomUUID();
29
+ params.opts?.onAgentRunStart?.(runId);
30
+ if (params.sessionKey) {
31
+ registerAgentRunContext(runId, {
32
+ sessionKey: params.sessionKey,
33
+ verboseLevel: params.resolvedVerboseLevel,
34
+ isHeartbeat: params.isHeartbeat,
35
+ });
36
+ }
37
+ let runResult;
38
+ let fallbackProvider = params.followupRun.run.provider;
39
+ let fallbackModel = params.followupRun.run.model;
40
+ let didResetAfterCompactionFailure = false;
41
+ let fallbackAttempts = [];
42
+ while (true) {
43
+ try {
44
+ const allowPartialStream = !(params.followupRun.run.reasoningLevel === "stream" && params.opts?.onReasoningStream);
45
+ const normalizeStreamingText = (payload) => {
46
+ if (!allowPartialStream)
47
+ return { skip: true };
48
+ let text = payload.text;
49
+ if (!params.isHeartbeat && text?.includes("HEARTBEAT_OK")) {
50
+ const stripped = stripHeartbeatToken(text, {
51
+ mode: "message",
52
+ });
53
+ if (stripped.didStrip && !didLogHeartbeatStrip) {
54
+ didLogHeartbeatStrip = true;
55
+ logVerbose("Stripped stray HEARTBEAT_OK token from reply");
56
+ }
57
+ if (stripped.shouldSkip && (payload.mediaUrls?.length ?? 0) === 0) {
58
+ return { skip: true };
59
+ }
60
+ text = stripped.text;
61
+ }
62
+ if (isSilentReplyText(text, SILENT_REPLY_TOKEN)) {
63
+ return { skip: true };
64
+ }
65
+ if (!text) {
66
+ // Allow media-only payloads through (no text but has media)
67
+ if (payload.mediaUrls?.length)
68
+ return { text: undefined, skip: false };
69
+ return { skip: true };
70
+ }
71
+ const sanitized = sanitizeUserFacingText(text, { errorContext: Boolean(payload.isError) });
72
+ if (!sanitized.trim())
73
+ return { skip: true };
74
+ return { text: sanitized, skip: false };
75
+ };
76
+ const handlePartialForTyping = async (payload) => {
77
+ if (isSilentReplyPrefixText(payload.text, SILENT_REPLY_TOKEN)) {
78
+ return undefined;
79
+ }
80
+ const { text, skip } = normalizeStreamingText(payload);
81
+ if (skip || !text)
82
+ return undefined;
83
+ await params.typingSignals.signalTextDelta(text);
84
+ return text;
85
+ };
86
+ const blockReplyPipeline = params.blockReplyPipeline;
87
+ const onToolResult = params.opts?.onToolResult;
88
+ const fallbackResult = await runWithModelFallback({
89
+ cfg: params.followupRun.run.config,
90
+ provider: params.followupRun.run.provider,
91
+ model: params.followupRun.run.model,
92
+ agentDir: params.followupRun.run.agentDir,
93
+ fallbacksOverride: resolveAgentModelFallbacksOverride(params.followupRun.run.config, resolveAgentIdFromSessionKey(params.followupRun.run.sessionKey)),
94
+ run: (provider, model) => {
95
+ // Notify that model selection is complete (including after fallback).
96
+ // This allows responsePrefix template interpolation with the actual model.
97
+ params.opts?.onModelSelected?.({
98
+ provider,
99
+ model,
100
+ thinkLevel: params.followupRun.run.thinkLevel,
101
+ });
102
+ if (isCliProvider(provider, params.followupRun.run.config)) {
103
+ const startedAt = Date.now();
104
+ emitAgentEvent({
105
+ runId,
106
+ stream: "lifecycle",
107
+ data: {
108
+ phase: "start",
109
+ startedAt,
110
+ },
111
+ });
112
+ const cliSessionId = getCliSessionId(params.getActiveSessionEntry(), provider);
113
+ return runCliAgent({
114
+ sessionId: params.followupRun.run.sessionId,
115
+ sessionKey: params.sessionKey,
116
+ sessionFile: params.followupRun.run.sessionFile,
117
+ workspaceDir: params.followupRun.run.workspaceDir,
118
+ config: params.followupRun.run.config,
119
+ prompt: params.commandBody,
120
+ provider,
121
+ model,
122
+ thinkLevel: params.followupRun.run.thinkLevel,
123
+ timeoutMs: params.followupRun.run.timeoutMs,
124
+ runId,
125
+ agentId: resolveAgentIdFromSessionKey(params.followupRun.run.sessionKey),
126
+ extraSystemPrompt: params.followupRun.run.extraSystemPrompt,
127
+ ownerNumbers: params.followupRun.run.ownerNumbers,
128
+ cliSessionId,
129
+ images: params.opts?.images,
130
+ })
131
+ .then((result) => {
132
+ // CLI backends don't emit streaming assistant events, so we need to
133
+ // emit one with the final text so server-chat can populate its buffer
134
+ // and send the response to TUI/WebSocket clients.
135
+ const cliText = result.payloads?.[0]?.text?.trim();
136
+ if (cliText) {
137
+ emitAgentEvent({
138
+ runId,
139
+ stream: "assistant",
140
+ data: { text: cliText },
141
+ });
142
+ }
143
+ emitAgentEvent({
144
+ runId,
145
+ stream: "lifecycle",
146
+ data: {
147
+ phase: "end",
148
+ startedAt,
149
+ endedAt: Date.now(),
150
+ },
151
+ });
152
+ return result;
153
+ })
154
+ .catch((err) => {
155
+ emitAgentEvent({
156
+ runId,
157
+ stream: "lifecycle",
158
+ data: {
159
+ phase: "error",
160
+ startedAt,
161
+ endedAt: Date.now(),
162
+ error: err instanceof Error ? err.message : String(err),
163
+ },
164
+ });
165
+ throw err;
166
+ });
167
+ }
168
+ const authProfileId = provider === params.followupRun.run.provider
169
+ ? params.followupRun.run.authProfileId
170
+ : undefined;
171
+ return runEmbeddedPiAgent({
172
+ sessionId: params.followupRun.run.sessionId,
173
+ sessionKey: params.sessionKey,
174
+ messageProvider: params.sessionCtx.Provider?.trim().toLowerCase() || undefined,
175
+ agentAccountId: params.sessionCtx.AccountId,
176
+ messageTo: params.sessionCtx.OriginatingTo ?? params.sessionCtx.To,
177
+ messageThreadId: params.sessionCtx.MessageThreadId ?? undefined,
178
+ groupId: resolveGroupSessionKey(params.sessionCtx)?.id,
179
+ groupChannel: params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim(),
180
+ groupSpace: params.sessionCtx.GroupSpace?.trim() ?? undefined,
181
+ senderId: params.sessionCtx.SenderId?.trim() || undefined,
182
+ senderName: params.sessionCtx.SenderName?.trim() || undefined,
183
+ senderUsername: params.sessionCtx.SenderUsername?.trim() || undefined,
184
+ senderE164: params.sessionCtx.SenderE164?.trim() || undefined,
185
+ // Provider threading context for tool auto-injection
186
+ ...buildThreadingToolContext({
187
+ sessionCtx: params.sessionCtx,
188
+ config: params.followupRun.run.config,
189
+ hasRepliedRef: params.opts?.hasRepliedRef,
190
+ }),
191
+ sessionFile: params.followupRun.run.sessionFile,
192
+ workspaceDir: params.followupRun.run.workspaceDir,
193
+ agentDir: params.followupRun.run.agentDir,
194
+ config: params.followupRun.run.config,
195
+ skillsSnapshot: params.followupRun.run.skillsSnapshot,
196
+ prompt: params.commandBody,
197
+ extraSystemPrompt: params.followupRun.run.extraSystemPrompt,
198
+ ownerNumbers: params.followupRun.run.ownerNumbers,
199
+ enforceFinalTag: resolveEnforceFinalTag(params.followupRun.run, provider),
200
+ provider,
201
+ model,
202
+ authProfileId,
203
+ authProfileIdSource: authProfileId
204
+ ? params.followupRun.run.authProfileIdSource
205
+ : undefined,
206
+ thinkLevel: params.followupRun.run.thinkLevel,
207
+ verboseLevel: params.followupRun.run.verboseLevel,
208
+ reasoningLevel: params.followupRun.run.reasoningLevel,
209
+ execOverrides: params.followupRun.run.execOverrides,
210
+ toolResultFormat: (() => {
211
+ const channel = resolveMessageChannel(params.sessionCtx.Surface, params.sessionCtx.Provider);
212
+ if (!channel)
213
+ return "markdown";
214
+ return isMarkdownCapableMessageChannel(channel) ? "markdown" : "plain";
215
+ })(),
216
+ bashElevated: params.followupRun.run.bashElevated,
217
+ timeoutMs: params.followupRun.run.timeoutMs,
218
+ runId,
219
+ images: params.opts?.images,
220
+ abortSignal: params.opts?.abortSignal,
221
+ blockReplyBreak: params.resolvedBlockStreamingBreak,
222
+ blockReplyChunking: params.blockReplyChunking,
223
+ onPartialReply: allowPartialStream
224
+ ? async (payload) => {
225
+ const textForTyping = await handlePartialForTyping(payload);
226
+ if (!params.opts?.onPartialReply || textForTyping === undefined)
227
+ return;
228
+ await params.opts.onPartialReply({
229
+ text: textForTyping,
230
+ mediaUrls: payload.mediaUrls,
231
+ });
232
+ }
233
+ : undefined,
234
+ onAssistantMessageStart: async () => {
235
+ await params.typingSignals.signalMessageStart();
236
+ },
237
+ onReasoningStream: params.typingSignals.shouldStartOnReasoning || params.opts?.onReasoningStream
238
+ ? async (payload) => {
239
+ await params.typingSignals.signalReasoningDelta();
240
+ await params.opts?.onReasoningStream?.({
241
+ text: payload.text,
242
+ mediaUrls: payload.mediaUrls,
243
+ });
244
+ }
245
+ : undefined,
246
+ onAgentEvent: async (evt) => {
247
+ // Trigger typing when tools start executing.
248
+ // Must await to ensure typing indicator starts before tool summaries are emitted.
249
+ if (evt.stream === "tool") {
250
+ const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
251
+ if (phase === "start" || phase === "update") {
252
+ await params.typingSignals.signalToolStart();
253
+ }
254
+ }
255
+ // Track auto-compaction completion
256
+ if (evt.stream === "compaction") {
257
+ const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
258
+ const willRetry = Boolean(evt.data.willRetry);
259
+ if (phase === "end" && !willRetry) {
260
+ autoCompactionCompleted = true;
261
+ }
262
+ }
263
+ },
264
+ // Always pass onBlockReply so flushBlockReplyBuffer works before tool execution,
265
+ // even when regular block streaming is disabled. The handler sends directly
266
+ // via opts.onBlockReply when the pipeline isn't available.
267
+ onBlockReply: params.opts?.onBlockReply
268
+ ? async (payload) => {
269
+ const { text, skip } = normalizeStreamingText(payload);
270
+ const hasPayloadMedia = (payload.mediaUrls?.length ?? 0) > 0;
271
+ if (skip && !hasPayloadMedia)
272
+ return;
273
+ const currentMessageId = params.sessionCtx.MessageSidFull ?? params.sessionCtx.MessageSid;
274
+ const taggedPayload = applyReplyTagsToPayload({
275
+ text,
276
+ mediaUrls: payload.mediaUrls,
277
+ mediaUrl: payload.mediaUrls?.[0],
278
+ replyToId: payload.replyToId,
279
+ replyToTag: payload.replyToTag,
280
+ replyToCurrent: payload.replyToCurrent,
281
+ }, currentMessageId);
282
+ // Let through payloads with audioAsVoice flag even if empty (need to track it)
283
+ if (!isRenderablePayload(taggedPayload) && !payload.audioAsVoice)
284
+ return;
285
+ const parsed = parseReplyDirectives(taggedPayload.text ?? "", {
286
+ currentMessageId,
287
+ silentToken: SILENT_REPLY_TOKEN,
288
+ });
289
+ const cleaned = parsed.text || undefined;
290
+ const hasRenderableMedia = Boolean(taggedPayload.mediaUrl) || (taggedPayload.mediaUrls?.length ?? 0) > 0;
291
+ // Skip empty payloads unless they have audioAsVoice flag (need to track it)
292
+ if (!cleaned &&
293
+ !hasRenderableMedia &&
294
+ !payload.audioAsVoice &&
295
+ !parsed.audioAsVoice)
296
+ return;
297
+ if (parsed.isSilent && !hasRenderableMedia)
298
+ return;
299
+ const blockPayload = params.applyReplyToMode({
300
+ ...taggedPayload,
301
+ text: cleaned,
302
+ audioAsVoice: Boolean(parsed.audioAsVoice || payload.audioAsVoice),
303
+ replyToId: taggedPayload.replyToId ?? parsed.replyToId,
304
+ replyToTag: taggedPayload.replyToTag || parsed.replyToTag,
305
+ replyToCurrent: taggedPayload.replyToCurrent || parsed.replyToCurrent,
306
+ });
307
+ void params.typingSignals
308
+ .signalTextDelta(cleaned ?? taggedPayload.text)
309
+ .catch((err) => {
310
+ logVerbose(`block reply typing signal failed: ${String(err)}`);
311
+ });
312
+ // Use pipeline if available (block streaming enabled), otherwise send directly
313
+ if (params.blockStreamingEnabled && params.blockReplyPipeline) {
314
+ params.blockReplyPipeline.enqueue(blockPayload);
315
+ }
316
+ else if (params.blockStreamingEnabled) {
317
+ // Send directly when flushing before tool execution (no pipeline but streaming enabled).
318
+ // Track sent key to avoid duplicate in final payloads.
319
+ directlySentBlockKeys.add(createBlockReplyPayloadKey(blockPayload));
320
+ await params.opts?.onBlockReply?.(blockPayload);
321
+ }
322
+ // When streaming is disabled entirely, blocks are accumulated in final text instead.
323
+ }
324
+ : undefined,
325
+ onBlockReplyFlush: params.blockStreamingEnabled && blockReplyPipeline
326
+ ? async () => {
327
+ await blockReplyPipeline.flush({ force: true });
328
+ }
329
+ : undefined,
330
+ shouldEmitToolResult: params.shouldEmitToolResult,
331
+ shouldEmitToolOutput: params.shouldEmitToolOutput,
332
+ onToolResult: onToolResult
333
+ ? (payload) => {
334
+ // `subscribeEmbeddedPiSession` may invoke tool callbacks without awaiting them.
335
+ // If a tool callback starts typing after the run finalized, we can end up with
336
+ // a typing loop that never sees a matching markRunComplete(). Track and drain.
337
+ const task = (async () => {
338
+ const { text, skip } = normalizeStreamingText(payload);
339
+ if (skip)
340
+ return;
341
+ await params.typingSignals.signalTextDelta(text);
342
+ await onToolResult({
343
+ text,
344
+ mediaUrls: payload.mediaUrls,
345
+ });
346
+ })()
347
+ .catch((err) => {
348
+ logVerbose(`tool result delivery failed: ${String(err)}`);
349
+ })
350
+ .finally(() => {
351
+ params.pendingToolTasks.delete(task);
352
+ });
353
+ params.pendingToolTasks.add(task);
354
+ }
355
+ : undefined,
356
+ });
357
+ },
358
+ });
359
+ runResult = fallbackResult.result;
360
+ fallbackProvider = fallbackResult.provider;
361
+ fallbackModel = fallbackResult.model;
362
+ fallbackAttempts = Array.isArray(fallbackResult.attempts)
363
+ ? fallbackResult.attempts.map((attempt) => ({
364
+ provider: String(attempt.provider ?? ""),
365
+ model: String(attempt.model ?? ""),
366
+ error: String(attempt.error ?? ""),
367
+ reason: attempt.reason ? String(attempt.reason) : undefined,
368
+ status: typeof attempt.status === "number" ? attempt.status : undefined,
369
+ code: attempt.code ? String(attempt.code) : undefined,
370
+ }))
371
+ : [];
372
+ // Some embedded runs surface context overflow as an error payload instead of throwing.
373
+ // Treat those as a session-level failure and auto-recover by starting a fresh session.
374
+ const embeddedError = runResult.meta?.error;
375
+ if (embeddedError &&
376
+ isContextOverflowError(embeddedError.message) &&
377
+ !didResetAfterCompactionFailure &&
378
+ (await params.resetSessionAfterCompactionFailure(embeddedError.message))) {
379
+ didResetAfterCompactionFailure = true;
380
+ return {
381
+ kind: "final",
382
+ payload: {
383
+ text: "⚠️ Context limit exceeded. I've reset our conversation to start fresh - please try again.\n\nTo prevent this, increase your compaction buffer by setting `agents.defaults.compaction.reserveTokensFloor` to 4000 or higher in your config.",
384
+ },
385
+ };
386
+ }
387
+ if (embeddedError?.kind === "role_ordering") {
388
+ const didReset = await params.resetSessionAfterRoleOrderingConflict(embeddedError.message);
389
+ if (didReset) {
390
+ return {
391
+ kind: "final",
392
+ payload: {
393
+ text: "⚠️ Message ordering conflict. I've reset the conversation - please try again.",
394
+ },
395
+ };
396
+ }
397
+ }
398
+ break;
399
+ }
400
+ catch (err) {
401
+ const message = err instanceof Error ? err.message : String(err);
402
+ const isContextOverflow = isLikelyContextOverflowError(message);
403
+ const isCompactionFailure = isCompactionFailureError(message);
404
+ const isSessionCorruption = /function call turn comes immediately after/i.test(message);
405
+ const isRoleOrderingError = /incorrect role information|roles must alternate/i.test(message);
406
+ const isTransientHttp = isTransientHttpError(message);
407
+ if (isCompactionFailure &&
408
+ !didResetAfterCompactionFailure &&
409
+ (await params.resetSessionAfterCompactionFailure(message))) {
410
+ didResetAfterCompactionFailure = true;
411
+ return {
412
+ kind: "final",
413
+ payload: {
414
+ text: "⚠️ Context limit exceeded during compaction. I've reset our conversation to start fresh - please try again.\n\nTo prevent this, increase your compaction buffer by setting `agents.defaults.compaction.reserveTokensFloor` to 4000 or higher in your config.",
415
+ },
416
+ };
417
+ }
418
+ if (isRoleOrderingError) {
419
+ const didReset = await params.resetSessionAfterRoleOrderingConflict(message);
420
+ if (didReset) {
421
+ return {
422
+ kind: "final",
423
+ payload: {
424
+ text: "⚠️ Message ordering conflict. I've reset the conversation - please try again.",
425
+ },
426
+ };
427
+ }
428
+ }
429
+ if (isTransientHttp && !didRetryTransientHttpError) {
430
+ didRetryTransientHttpError = true;
431
+ defaultRuntime.error(`Transient HTTP provider error before reply (${message}). Retrying once in ${TRANSIENT_HTTP_RETRY_DELAY_MS}ms.`);
432
+ await new Promise((resolve) => {
433
+ setTimeout(resolve, TRANSIENT_HTTP_RETRY_DELAY_MS);
434
+ });
435
+ continue;
436
+ }
437
+ // Auto-recover from Gemini session corruption by resetting the session
438
+ if (isSessionCorruption &&
439
+ params.sessionKey &&
440
+ params.activeSessionStore &&
441
+ params.storePath) {
442
+ const sessionKey = params.sessionKey;
443
+ const corruptedSessionId = params.getActiveSessionEntry()?.sessionId;
444
+ defaultRuntime.error(`Session history corrupted (Gemini function call ordering). Resetting session: ${params.sessionKey}`);
445
+ try {
446
+ // Delete transcript file if it exists
447
+ if (corruptedSessionId) {
448
+ const transcriptPath = resolveSessionTranscriptPath(corruptedSessionId);
449
+ try {
450
+ fs.unlinkSync(transcriptPath);
451
+ }
452
+ catch {
453
+ // Ignore if file doesn't exist
454
+ }
455
+ }
456
+ // Keep the in-memory snapshot consistent with the on-disk store reset.
457
+ delete params.activeSessionStore[sessionKey];
458
+ // Remove session entry from store using a fresh, locked snapshot.
459
+ await updateSessionStore(params.storePath, (store) => {
460
+ delete store[sessionKey];
461
+ });
462
+ }
463
+ catch (cleanupErr) {
464
+ defaultRuntime.error(`Failed to reset corrupted session ${params.sessionKey}: ${String(cleanupErr)}`);
465
+ }
466
+ return {
467
+ kind: "final",
468
+ payload: {
469
+ text: "⚠️ Session history was corrupted. I've reset the conversation - please try again!",
470
+ },
471
+ };
472
+ }
473
+ defaultRuntime.error(`Embedded agent failed before reply: ${message}`);
474
+ const safeMessage = isTransientHttp
475
+ ? sanitizeUserFacingText(message, { errorContext: true })
476
+ : message;
477
+ const trimmedMessage = safeMessage.replace(/\.\s*$/, "");
478
+ const fallbackText = isContextOverflow
479
+ ? "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model."
480
+ : isRoleOrderingError
481
+ ? "⚠️ Message ordering conflict - please try again. If this persists, use /new to start a fresh session."
482
+ : `⚠️ Agent failed before reply: ${trimmedMessage}.\nLogs: poolbot logs --follow`;
483
+ return {
484
+ kind: "final",
485
+ payload: {
486
+ text: fallbackText,
487
+ },
488
+ };
489
+ }
490
+ }
491
+ return {
492
+ kind: "success",
493
+ runResult,
494
+ fallbackProvider,
495
+ fallbackModel,
496
+ runId,
497
+ fallbackAttempts,
498
+ didLogHeartbeatStrip,
499
+ autoCompactionCompleted,
500
+ directlySentBlockKeys: directlySentBlockKeys.size > 0 ? directlySentBlockKeys : undefined,
501
+ };
502
+ }
@@ -0,0 +1,65 @@
1
+ import { loadSessionStore } from "../../config/sessions.js";
2
+ import { isAudioFileName } from "../../media/mime.js";
3
+ import { normalizeVerboseLevel } from "../thinking.js";
4
+ import { scheduleFollowupDrain } from "./queue.js";
5
+ const hasAudioMedia = (urls) => Boolean(urls?.some((url) => isAudioFileName(url)));
6
+ export const isAudioPayload = (payload) => hasAudioMedia(payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : undefined));
7
+ export const createShouldEmitToolResult = (params) => {
8
+ // Normalize verbose values from session store/config so false/"false" still means off.
9
+ const fallbackVerbose = normalizeVerboseLevel(String(params.resolvedVerboseLevel ?? "")) ?? "off";
10
+ return () => {
11
+ if (!params.sessionKey || !params.storePath) {
12
+ return fallbackVerbose !== "off";
13
+ }
14
+ try {
15
+ const store = loadSessionStore(params.storePath);
16
+ const entry = store[params.sessionKey];
17
+ const current = normalizeVerboseLevel(String(entry?.verboseLevel ?? ""));
18
+ if (current)
19
+ return current !== "off";
20
+ }
21
+ catch {
22
+ // ignore store read failures
23
+ }
24
+ return fallbackVerbose !== "off";
25
+ };
26
+ };
27
+ export const createShouldEmitToolOutput = (params) => {
28
+ // Normalize verbose values from session store/config so false/"false" still means off.
29
+ const fallbackVerbose = normalizeVerboseLevel(String(params.resolvedVerboseLevel ?? "")) ?? "off";
30
+ return () => {
31
+ if (!params.sessionKey || !params.storePath) {
32
+ return fallbackVerbose === "full";
33
+ }
34
+ try {
35
+ const store = loadSessionStore(params.storePath);
36
+ const entry = store[params.sessionKey];
37
+ const current = normalizeVerboseLevel(String(entry?.verboseLevel ?? ""));
38
+ if (current)
39
+ return current === "full";
40
+ }
41
+ catch {
42
+ // ignore store read failures
43
+ }
44
+ return fallbackVerbose === "full";
45
+ };
46
+ };
47
+ export const finalizeWithFollowup = (value, queueKey, runFollowupTurn) => {
48
+ scheduleFollowupDrain(queueKey, runFollowupTurn);
49
+ return value;
50
+ };
51
+ export const signalTypingIfNeeded = async (payloads, typingSignals) => {
52
+ const shouldSignalTyping = payloads.some((payload) => {
53
+ const trimmed = payload.text?.trim();
54
+ if (trimmed)
55
+ return true;
56
+ if (payload.mediaUrl)
57
+ return true;
58
+ if (payload.mediaUrls && payload.mediaUrls.length > 0)
59
+ return true;
60
+ return false;
61
+ });
62
+ if (shouldSignalTyping) {
63
+ await typingSignals.signalRunStart();
64
+ }
65
+ };