@poolzin/pool-bot 2026.2.21 → 2026.2.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/agents/api-key-rotation.js +47 -0
- package/dist/agents/apply-patch-update.js +19 -9
- package/dist/agents/apply-patch.js +72 -47
- package/dist/agents/bash-tools.exec.js +141 -559
- package/dist/agents/cli-backends.js +49 -6
- package/dist/agents/cli-runner/helpers.js +69 -152
- package/dist/agents/cli-runner.js +70 -19
- package/dist/agents/identity.js +20 -1
- package/dist/agents/image-sanitization.js +9 -0
- package/dist/agents/live-auth-keys.js +123 -26
- package/dist/agents/live-model-filter.js +13 -4
- package/dist/agents/model-catalog.js +40 -9
- package/dist/agents/model-forward-compat.js +60 -23
- package/dist/agents/model-selection.js +134 -41
- package/dist/agents/pi-auth-json.js +2 -2
- package/dist/agents/pi-embedded-helpers/bootstrap.js +65 -15
- package/dist/agents/pi-embedded-helpers/errors.js +140 -15
- package/dist/agents/pi-embedded-helpers/images.js +22 -12
- package/dist/agents/pi-embedded-helpers.js +2 -2
- package/dist/agents/pi-embedded-runner/abort.js +10 -3
- package/dist/agents/pi-embedded-runner/compact.js +230 -32
- package/dist/agents/pi-embedded-runner/extra-params.js +203 -12
- package/dist/agents/pi-embedded-runner/google.js +109 -19
- package/dist/agents/pi-embedded-runner/history.js +35 -17
- package/dist/agents/pi-embedded-runner/run/attempt.js +386 -95
- package/dist/agents/pi-embedded-runner/run/images.js +81 -55
- package/dist/agents/pi-embedded-runner/run/payloads.js +89 -39
- package/dist/agents/pi-embedded-runner/run.js +193 -25
- package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +2 -2
- package/dist/agents/pi-embedded-runner/runs.js +17 -8
- package/dist/agents/pi-embedded-runner/tool-result-context-guard.js +262 -0
- package/dist/agents/pi-embedded-runner.js +1 -1
- package/dist/agents/pi-embedded-subscribe.handlers.tools.js +180 -10
- package/dist/agents/pi-embedded-subscribe.js +37 -0
- package/dist/agents/pi-embedded-subscribe.tools.js +127 -30
- package/dist/agents/pi-model-discovery.js +9 -2
- package/dist/agents/pi-tool-definition-adapter.js +60 -8
- package/dist/agents/pi-tools.before-tool-call.js +1 -1
- package/dist/agents/pi-tools.js +113 -94
- package/dist/agents/pi-tools.read.js +337 -38
- package/dist/agents/poolbot-tools.js +14 -5
- package/dist/agents/sandbox/docker.js +10 -5
- package/dist/agents/sandbox/registry.js +96 -46
- package/dist/agents/sandbox/sanitize-env-vars.js +82 -0
- package/dist/agents/sandbox-paths.js +43 -10
- package/dist/agents/session-tool-result-guard-wrapper.js +23 -11
- package/dist/agents/session-tool-result-guard.js +39 -39
- package/dist/agents/session-transcript-repair.js +36 -33
- package/dist/agents/session-write-lock.js +62 -44
- package/dist/agents/skills/frontmatter.js +49 -88
- package/dist/agents/skills/workspace.js +335 -28
- package/dist/agents/subagent-announce.js +508 -174
- package/dist/agents/subagent-registry.js +45 -4
- package/dist/agents/subagent-spawn.js +16 -33
- package/dist/agents/system-prompt-report.js +27 -10
- package/dist/agents/system-prompt.js +26 -32
- package/dist/agents/tool-call-id.js +69 -17
- package/dist/agents/tool-display-common.js +1 -1
- package/dist/agents/tool-images.js +64 -31
- package/dist/agents/tools/canvas-tool.js +17 -11
- package/dist/agents/tools/common.js +37 -19
- package/dist/agents/tools/cron-tool.js +40 -38
- package/dist/agents/tools/gateway.js +70 -2
- package/dist/agents/tools/message-tool.js +181 -40
- package/dist/agents/tools/nodes-tool.js +128 -36
- package/dist/agents/tools/nodes-utils.js +12 -38
- package/dist/agents/tools/session-status-tool.js +24 -71
- package/dist/agents/tools/sessions-helpers.js +38 -210
- package/dist/agents/tools/sessions-spawn-tool.js +28 -198
- package/dist/agents/tools/telegram-actions.js +58 -7
- package/dist/agents/tools/web-fetch-utils.js +112 -7
- package/dist/agents/tools/web-fetch.js +279 -175
- package/dist/agents/tools/web-shared.js +71 -8
- package/dist/agents/usage.js +25 -16
- package/dist/auto-reply/commands-registry.data.js +85 -11
- package/dist/auto-reply/dispatch.js +40 -21
- package/dist/auto-reply/reply/abort.js +102 -33
- package/dist/auto-reply/reply/commands-core.js +82 -33
- package/dist/auto-reply/reply/commands-export-session.js +1 -1
- package/dist/auto-reply/reply/commands-info.js +41 -12
- package/dist/auto-reply/reply/commands-subagents.js +352 -100
- package/dist/auto-reply/reply/commands-system-prompt.js +2 -2
- package/dist/auto-reply/reply/dispatch-from-config.js +100 -29
- package/dist/auto-reply/reply/elevated-unavailable.js +1 -1
- package/dist/auto-reply/reply/inbound-meta.js +12 -1
- package/dist/auto-reply/reply/mentions.js +18 -11
- package/dist/auto-reply/reply/normalize-reply.js +17 -8
- package/dist/auto-reply/reply/reply-dispatcher.js +62 -10
- package/dist/auto-reply/reply/session.js +102 -21
- package/dist/auto-reply/reply/streaming-directives.js +16 -5
- package/dist/auto-reply/status.js +73 -50
- package/dist/browser/extension-relay.js +3 -3
- package/dist/browser/http-auth.js +1 -1
- package/dist/browser/paths.js +2 -2
- package/dist/build-info.json +3 -3
- package/dist/channels/allowlist-match.js +20 -0
- package/dist/channels/allowlists/resolve-utils.js +65 -2
- package/dist/channels/chat-type.js +8 -4
- package/dist/channels/dock.js +127 -35
- package/dist/channels/draft-stream-loop.js +6 -2
- package/dist/channels/plugins/actions/telegram.js +42 -18
- package/dist/channels/plugins/allowlist-match.js +1 -1
- package/dist/channels/plugins/group-mentions.js +51 -41
- package/dist/channels/plugins/message-action-names.js +2 -0
- package/dist/channels/plugins/message-actions.js +24 -5
- package/dist/channels/plugins/normalize/discord.js +26 -4
- package/dist/channels/plugins/normalize/signal.js +35 -22
- package/dist/channels/plugins/onboarding/helpers.js +8 -26
- package/dist/channels/plugins/outbound/imessage.js +15 -14
- package/dist/channels/registry.js +20 -7
- package/dist/cli/acp-cli.js +7 -5
- package/dist/cli/browser-cli-extension.js +25 -12
- package/dist/cli/browser-cli-state.cookies-storage.js +25 -6
- package/dist/cli/browser-cli-state.js +101 -145
- package/dist/cli/command-options.js +28 -0
- package/dist/cli/completion-cli.js +6 -6
- package/dist/cli/cron-cli/register.cron-add.js +25 -1
- package/dist/cli/cron-cli/register.cron-edit.js +44 -0
- package/dist/cli/cron-cli/shared.js +7 -1
- package/dist/cli/daemon-cli/lifecycle-core.js +23 -21
- package/dist/cli/daemon-cli/lifecycle.js +23 -247
- package/dist/cli/daemon-cli/register-service-commands.js +25 -4
- package/dist/cli/daemon-cli.js +1 -0
- package/dist/cli/devices-cli.js +33 -20
- package/dist/cli/gateway-cli/register.js +37 -105
- package/dist/cli/gateway-cli/run.js +49 -11
- package/dist/cli/nodes-camera.js +59 -4
- package/dist/cli/nodes-cli/register.camera.js +27 -24
- package/dist/cli/nodes-cli/rpc.js +21 -38
- package/dist/cli/qr-cli.js +2 -2
- package/dist/cli/skills-cli.format.js +2 -2
- package/dist/cli/update-cli/progress.js +2 -2
- package/dist/cli/update-cli/restart-helper.js +28 -7
- package/dist/cli/update-cli/shared.js +7 -7
- package/dist/cli/update-cli/status.js +1 -1
- package/dist/cli/update-cli/update-command.js +14 -8
- package/dist/cli/update-cli/wizard.js +2 -2
- package/dist/cli/update-cli.js +21 -1027
- package/dist/commands/auth-choice.apply.anthropic.js +10 -2
- package/dist/commands/channels/add-mutators.js +3 -35
- package/dist/commands/channels/add.js +39 -51
- package/dist/commands/config-validation.js +1 -1
- package/dist/commands/configure.gateway-auth.js +52 -15
- package/dist/commands/configure.gateway.js +84 -40
- package/dist/commands/doctor-completion.js +3 -3
- package/dist/commands/doctor-config-flow.js +536 -16
- package/dist/commands/doctor-gateway-services.js +103 -79
- package/dist/commands/doctor-memory-search.js +9 -9
- package/dist/commands/doctor-platform-notes.js +57 -30
- package/dist/commands/doctor-prompter.js +26 -15
- package/dist/commands/doctor-session-locks.js +1 -1
- package/dist/commands/doctor.js +21 -9
- package/dist/commands/model-picker.js +120 -95
- package/dist/commands/models/set.js +2 -21
- package/dist/commands/models/shared.js +65 -37
- package/dist/commands/onboard-helpers.js +81 -39
- package/dist/commands/openai-codex-oauth.js +1 -1
- package/dist/commands/sessions.js +52 -53
- package/dist/commands/status.summary.js +52 -34
- package/dist/commands/test-wizard-helpers.js +2 -2
- package/dist/config/defaults.js +79 -42
- package/dist/config/group-policy.js +50 -18
- package/dist/config/includes.js +37 -10
- package/dist/config/schema.help.js +5 -4
- package/dist/config/schema.hints.js +2 -2
- package/dist/config/schema.labels.js +1 -0
- package/dist/config/sessions/group.js +12 -11
- package/dist/config/sessions/paths.js +137 -11
- package/dist/config/sessions/store.js +185 -65
- package/dist/config/sessions/types.js +15 -1
- package/dist/config/sessions.js +1 -0
- package/dist/config/telegram-custom-commands.js +3 -2
- package/dist/config/types.js +2 -0
- package/dist/config/zod-schema.agent-defaults.js +6 -27
- package/dist/config/zod-schema.agent-runtime.js +171 -79
- package/dist/config/zod-schema.providers-core.js +138 -65
- package/dist/config/zod-schema.session.js +49 -22
- package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
- package/dist/cron/isolated-agent/run.js +224 -57
- package/dist/cron/normalize.js +48 -45
- package/dist/cron/run-log.js +14 -0
- package/dist/cron/service/jobs.js +190 -28
- package/dist/cron/service/normalize.js +29 -11
- package/dist/cron/service/store.js +30 -44
- package/dist/cron/service/timer.js +182 -96
- package/dist/cron/service.js +3 -0
- package/dist/cron/stagger.js +37 -0
- package/dist/daemon/inspect.js +132 -92
- package/dist/daemon/runtime-paths.js +25 -4
- package/dist/daemon/service-audit.js +47 -16
- package/dist/discord/accounts.js +23 -20
- package/dist/discord/monitor/agent-components.js +1115 -219
- package/dist/discord/monitor/allow-list.js +114 -34
- package/dist/discord/monitor/listeners.js +204 -97
- package/dist/discord/monitor/message-handler.js +21 -10
- package/dist/discord/monitor/message-handler.preflight.js +195 -101
- package/dist/discord/monitor/message-handler.process.js +384 -123
- package/dist/discord/monitor/message-utils.js +86 -23
- package/dist/discord/monitor/native-command.js +77 -57
- package/dist/discord/monitor/provider.js +122 -117
- package/dist/discord/monitor/reply-context.js +20 -16
- package/dist/discord/monitor/reply-delivery.js +40 -8
- package/dist/discord/monitor/rest-fetch.js +22 -0
- package/dist/discord/monitor/threading.js +117 -24
- package/dist/discord/send.js +2 -1
- package/dist/discord/send.outbound.js +124 -11
- package/dist/discord/send.shared.js +112 -72
- package/dist/discord/voice-message.js +3 -3
- package/dist/gateway/auth.js +119 -44
- package/dist/gateway/call.js +76 -34
- package/dist/gateway/channel-health-monitor.js +57 -50
- package/dist/gateway/client.js +63 -29
- package/dist/gateway/control-ui-contract.js +1 -1
- package/dist/gateway/gateway-config-prompts.shared.js +2 -2
- package/dist/gateway/net.js +109 -1
- package/dist/gateway/protocol/index.js +5 -8
- package/dist/gateway/protocol/schema/agent.js +19 -1
- package/dist/gateway/protocol/schema/channels.js +21 -0
- package/dist/gateway/protocol/schema/cron.js +43 -30
- package/dist/gateway/protocol/schema/protocol-schemas.js +6 -11
- package/dist/gateway/protocol/schema/sessions.js +5 -1
- package/dist/gateway/protocol/schema.js +0 -1
- package/dist/gateway/server/presence-events.js +12 -0
- package/dist/gateway/server/ws-connection/message-handler.js +203 -212
- package/dist/gateway/server/ws-connection.js +58 -21
- package/dist/gateway/server-broadcast.js +18 -13
- package/dist/gateway/server-cron.js +177 -10
- package/dist/gateway/server-methods/agent-job.js +131 -38
- package/dist/gateway/server-methods/send.js +60 -14
- package/dist/gateway/server-methods/sessions.js +160 -96
- package/dist/gateway/server-methods/system.js +5 -7
- package/dist/gateway/server-methods-list.js +8 -0
- package/dist/gateway/server-methods.js +24 -8
- package/dist/gateway/server-node-events.js +278 -68
- package/dist/gateway/session-utils.fs.js +316 -75
- package/dist/gateway/session-utils.js +224 -70
- package/dist/gateway/sessions-patch.js +63 -20
- package/dist/gateway/test-temp-config.js +1 -1
- package/dist/gateway/tools-invoke-http.js +118 -70
- package/dist/gateway/ws-log.js +135 -107
- package/dist/hooks/frontmatter.js +36 -82
- package/dist/hooks/install.js +149 -139
- package/dist/hooks/internal-hooks.js +29 -4
- package/dist/hooks/plugin-hooks.js +2 -1
- package/dist/imessage/monitor/deliver.js +10 -4
- package/dist/imessage/monitor/monitor-provider.js +138 -375
- package/dist/imessage/monitor/runtime.js +4 -8
- package/dist/imessage/send.js +65 -19
- package/dist/infra/exec-approvals-allowlist.js +7 -0
- package/dist/infra/exec-approvals.js +35 -920
- package/dist/infra/exec-safe-bin-trust.js +64 -0
- package/dist/infra/heartbeat-runner.js +207 -134
- package/dist/infra/heartbeat-wake.js +183 -22
- package/dist/infra/install-source-utils.js +47 -0
- package/dist/infra/net/ssrf.js +170 -36
- package/dist/infra/outbound/deliver.js +224 -58
- package/dist/infra/outbound/message-action-spec.js +12 -5
- package/dist/infra/outbound/outbound-session.js +27 -25
- package/dist/infra/poolbot-root.js +32 -22
- package/dist/infra/ports.js +14 -11
- package/dist/infra/skills-remote.js +48 -37
- package/dist/infra/system-events.js +25 -11
- package/dist/infra/system-presence.js +26 -33
- package/dist/infra/tmp-poolbot-dir.js +81 -2
- package/dist/infra/wsl.js +37 -1
- package/dist/line/bot-message-context.js +163 -191
- package/dist/logging/subsystem.js +59 -22
- package/dist/markdown/ir.js +124 -50
- package/dist/media/store.js +1 -1
- package/dist/media-understanding/runner.entries.js +42 -25
- package/dist/media-understanding/runner.js +53 -488
- package/dist/memory/embeddings-gemini.js +53 -38
- package/dist/memory/manager-embedding-ops.js +48 -69
- package/dist/pairing/pairing-store.js +178 -119
- package/dist/plugin-sdk/index.js +34 -6
- package/dist/plugins/hooks.js +135 -14
- package/dist/plugins/install.js +190 -152
- package/dist/polls.js +11 -0
- package/dist/routing/resolve-route.js +190 -56
- package/dist/routing/session-key.js +38 -22
- package/dist/runtime.js +35 -9
- package/dist/security/audit-channel.js +1 -1
- package/dist/sessions/session-key-utils.js +29 -11
- package/dist/shared/frontmatter.js +5 -5
- package/dist/shared/node-list-types.js +1 -0
- package/dist/shared/string-normalization.js +15 -0
- package/dist/signal/monitor/event-handler.js +68 -36
- package/dist/signal/send.js +29 -37
- package/dist/slack/monitor/allow-list.js +10 -11
- package/dist/slack/monitor/commands.js +14 -3
- package/dist/slack/monitor/events/interactions.js +4 -4
- package/dist/slack/monitor/media.js +224 -16
- package/dist/slack/monitor/message-handler/dispatch.js +247 -13
- package/dist/slack/monitor/message-handler/prepare.js +128 -45
- package/dist/slack/monitor/slash.js +357 -144
- package/dist/slack/streaming.js +77 -0
- package/dist/telegram/accounts.js +40 -13
- package/dist/telegram/allowed-updates.js +3 -0
- package/dist/telegram/bot/delivery.js +129 -66
- package/dist/telegram/bot/helpers.js +136 -122
- package/dist/telegram/bot-handlers.js +600 -339
- package/dist/telegram/bot-message-context.js +115 -73
- package/dist/telegram/bot-message-dispatch.js +235 -104
- package/dist/telegram/bot-native-command-menu.js +3 -1
- package/dist/telegram/bot-native-commands.js +213 -193
- package/dist/telegram/bot.js +24 -132
- package/dist/telegram/draft-stream.js +84 -75
- package/dist/telegram/format.js +150 -6
- package/dist/telegram/send.js +415 -255
- package/dist/telegram/targets.js +21 -2
- package/dist/telegram/update-offset-store.js +19 -3
- package/dist/terminal/restore.js +5 -2
- package/dist/test-utils/fetch-mock.js +5 -0
- package/dist/version.js +18 -5
- package/dist/web/auto-reply/monitor/broadcast.js +7 -3
- package/dist/web/auto-reply/monitor/on-message.js +6 -3
- package/dist/web/inbound/media.js +34 -8
- package/dist/web/inbound/monitor.js +34 -17
- package/dist/web/inbound/send-api.js +18 -17
- package/dist/web/outbound.js +12 -5
- package/dist/wizard/clack-prompter.js +40 -7
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/device-pair/index.ts +2 -2
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/feishu/package.json +1 -1
- package/extensions/google-antigravity-auth/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/irc/src/accounts.ts +1 -1
- package/extensions/irc/src/onboarding.ts +4 -4
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/matrix/CHANGELOG.md +10 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/mattermost/package.json +1 -1
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +10 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +10 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/openai-codex-auth/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +10 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +10 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +10 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +10 -0
- package/extensions/zalouser/package.json +1 -1
- package/package.json +1 -1
- package/skills/apple-reminders/SKILL.md +100 -49
- package/skills/coding-agent/SKILL.md +34 -28
- package/skills/github/SKILL.md +131 -16
- package/skills/imsg/SKILL.md +112 -15
- package/skills/openhue/SKILL.md +101 -19
- package/skills/tmux/SKILL.md +111 -79
- package/skills/weather/SKILL.md +88 -25
- package/dist/agents/openclaw-tools.js +0 -151
- package/dist/agents/tool-security.js +0 -96
- package/dist/gateway/url-validation.js +0 -94
- package/dist/infra/openclaw-root.js +0 -109
- package/dist/infra/tmp-openclaw-dir.js +0 -81
- package/dist/media/path-sanitization.js +0 -78
|
@@ -1,12 +1,6 @@
|
|
|
1
|
+
import { normalizeHostname } from "../../infra/net/hostname.js";
|
|
1
2
|
import { fetchRemoteMedia } from "../../media/fetch.js";
|
|
2
3
|
import { saveMediaBuffer } from "../../media/store.js";
|
|
3
|
-
function normalizeHostname(hostname) {
|
|
4
|
-
const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
|
|
5
|
-
if (normalized.startsWith("[") && normalized.endsWith("]")) {
|
|
6
|
-
return normalized.slice(1, -1);
|
|
7
|
-
}
|
|
8
|
-
return normalized;
|
|
9
|
-
}
|
|
10
4
|
function isSlackHostname(hostname) {
|
|
11
5
|
const normalized = normalizeHostname(hostname);
|
|
12
6
|
if (!normalized) {
|
|
@@ -94,12 +88,72 @@ export async function fetchWithSlackAuth(url, token) {
|
|
|
94
88
|
// (Slack's CDN URLs are pre-signed and don't need it)
|
|
95
89
|
return fetch(resolvedUrl.toString(), { redirect: "follow" });
|
|
96
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Slack voice messages (audio clips, huddle recordings) carry a `subtype` of
|
|
93
|
+
* `"slack_audio"` but are served with a `video/*` MIME type (e.g. `video/mp4`,
|
|
94
|
+
* `video/webm`). Override the primary type to `audio/` so the
|
|
95
|
+
* media-understanding pipeline routes them to transcription.
|
|
96
|
+
*/
|
|
97
|
+
function resolveSlackMediaMimetype(file, fetchedContentType) {
|
|
98
|
+
const mime = fetchedContentType ?? file.mimetype;
|
|
99
|
+
if (file.subtype === "slack_audio" && mime?.startsWith("video/")) {
|
|
100
|
+
return mime.replace("video/", "audio/");
|
|
101
|
+
}
|
|
102
|
+
return mime;
|
|
103
|
+
}
|
|
104
|
+
const MAX_SLACK_MEDIA_FILES = 8;
|
|
105
|
+
const MAX_SLACK_MEDIA_CONCURRENCY = 3;
|
|
106
|
+
const MAX_SLACK_FORWARDED_ATTACHMENTS = 8;
|
|
107
|
+
function isForwardedSlackAttachment(attachment) {
|
|
108
|
+
// Narrow this parser to Slack's explicit "shared/forwarded" attachment payloads.
|
|
109
|
+
return attachment.is_share === true;
|
|
110
|
+
}
|
|
111
|
+
function resolveForwardedAttachmentImageUrl(attachment) {
|
|
112
|
+
const rawUrl = attachment.image_url?.trim();
|
|
113
|
+
if (!rawUrl) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const parsed = new URL(rawUrl);
|
|
118
|
+
if (parsed.protocol !== "https:" || !isSlackHostname(parsed.hostname)) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return parsed.toString();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function mapLimit(items, limit, fn) {
|
|
128
|
+
if (items.length === 0) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
const results = [];
|
|
132
|
+
results.length = items.length;
|
|
133
|
+
let nextIndex = 0;
|
|
134
|
+
const workerCount = Math.max(1, Math.min(limit, items.length));
|
|
135
|
+
await Promise.all(Array.from({ length: workerCount }, async () => {
|
|
136
|
+
while (true) {
|
|
137
|
+
const idx = nextIndex++;
|
|
138
|
+
if (idx >= items.length) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
results[idx] = await fn(items[idx]);
|
|
142
|
+
}
|
|
143
|
+
}));
|
|
144
|
+
return results;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Downloads all files attached to a Slack message and returns them as an array.
|
|
148
|
+
* Returns `null` when no files could be downloaded.
|
|
149
|
+
*/
|
|
97
150
|
export async function resolveSlackMedia(params) {
|
|
98
151
|
const files = params.files ?? [];
|
|
99
|
-
|
|
152
|
+
const limitedFiles = files.length > MAX_SLACK_MEDIA_FILES ? files.slice(0, MAX_SLACK_MEDIA_FILES) : files;
|
|
153
|
+
const resolved = await mapLimit(limitedFiles, MAX_SLACK_MEDIA_CONCURRENCY, async (file) => {
|
|
100
154
|
const url = file.url_private_download ?? file.url_private;
|
|
101
155
|
if (!url) {
|
|
102
|
-
|
|
156
|
+
return null;
|
|
103
157
|
}
|
|
104
158
|
try {
|
|
105
159
|
// Note: fetchRemoteMedia calls fetchImpl(url) with the URL string today and
|
|
@@ -113,28 +167,116 @@ export async function resolveSlackMedia(params) {
|
|
|
113
167
|
maxBytes: params.maxBytes,
|
|
114
168
|
});
|
|
115
169
|
if (fetched.buffer.byteLength > params.maxBytes) {
|
|
116
|
-
|
|
170
|
+
return null;
|
|
117
171
|
}
|
|
118
|
-
const
|
|
172
|
+
const effectiveMime = resolveSlackMediaMimetype(file, fetched.contentType);
|
|
173
|
+
const saved = await saveMediaBuffer(fetched.buffer, effectiveMime, "inbound", params.maxBytes);
|
|
119
174
|
const label = fetched.fileName ?? file.name;
|
|
175
|
+
const contentType = effectiveMime ?? saved.contentType;
|
|
120
176
|
return {
|
|
121
177
|
path: saved.path,
|
|
122
|
-
contentType:
|
|
178
|
+
...(contentType ? { contentType } : {}),
|
|
123
179
|
placeholder: label ? `[Slack file: ${label}]` : "[Slack file]",
|
|
124
180
|
};
|
|
125
181
|
}
|
|
126
182
|
catch {
|
|
127
|
-
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
const results = resolved.filter((entry) => Boolean(entry));
|
|
187
|
+
return results.length > 0 ? results : null;
|
|
188
|
+
}
|
|
189
|
+
/** Extracts text and media from forwarded-message attachments. Returns null when empty. */
|
|
190
|
+
export async function resolveSlackAttachmentContent(params) {
|
|
191
|
+
const attachments = params.attachments;
|
|
192
|
+
if (!attachments || attachments.length === 0) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const forwardedAttachments = attachments
|
|
196
|
+
.filter((attachment) => isForwardedSlackAttachment(attachment))
|
|
197
|
+
.slice(0, MAX_SLACK_FORWARDED_ATTACHMENTS);
|
|
198
|
+
if (forwardedAttachments.length === 0) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const textBlocks = [];
|
|
202
|
+
const allMedia = [];
|
|
203
|
+
for (const att of forwardedAttachments) {
|
|
204
|
+
const text = att.text?.trim() || att.fallback?.trim();
|
|
205
|
+
if (text) {
|
|
206
|
+
const author = att.author_name;
|
|
207
|
+
const heading = author ? `[Forwarded message from ${author}]` : "[Forwarded message]";
|
|
208
|
+
textBlocks.push(`${heading}\n${text}`);
|
|
209
|
+
}
|
|
210
|
+
const imageUrl = resolveForwardedAttachmentImageUrl(att);
|
|
211
|
+
if (imageUrl) {
|
|
212
|
+
try {
|
|
213
|
+
const fetched = await fetchRemoteMedia({
|
|
214
|
+
url: imageUrl,
|
|
215
|
+
maxBytes: params.maxBytes,
|
|
216
|
+
});
|
|
217
|
+
if (fetched.buffer.byteLength <= params.maxBytes) {
|
|
218
|
+
const saved = await saveMediaBuffer(fetched.buffer, fetched.contentType, "inbound", params.maxBytes);
|
|
219
|
+
const label = fetched.fileName ?? "forwarded image";
|
|
220
|
+
allMedia.push({
|
|
221
|
+
path: saved.path,
|
|
222
|
+
contentType: fetched.contentType ?? saved.contentType,
|
|
223
|
+
placeholder: `[Forwarded image: ${label}]`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Skip images that fail to download
|
|
229
|
+
}
|
|
128
230
|
}
|
|
231
|
+
if (att.files && att.files.length > 0) {
|
|
232
|
+
const fileMedia = await resolveSlackMedia({
|
|
233
|
+
files: att.files,
|
|
234
|
+
token: params.token,
|
|
235
|
+
maxBytes: params.maxBytes,
|
|
236
|
+
});
|
|
237
|
+
if (fileMedia) {
|
|
238
|
+
allMedia.push(...fileMedia);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const combinedText = textBlocks.join("\n\n");
|
|
243
|
+
if (!combinedText && allMedia.length === 0) {
|
|
244
|
+
return null;
|
|
129
245
|
}
|
|
130
|
-
return
|
|
246
|
+
return { text: combinedText, media: allMedia };
|
|
131
247
|
}
|
|
132
248
|
const THREAD_STARTER_CACHE = new Map();
|
|
249
|
+
const THREAD_STARTER_CACHE_TTL_MS = 6 * 60 * 60_000;
|
|
250
|
+
const THREAD_STARTER_CACHE_MAX = 2000;
|
|
251
|
+
function evictThreadStarterCache() {
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
for (const [cacheKey, entry] of THREAD_STARTER_CACHE.entries()) {
|
|
254
|
+
if (now - entry.cachedAt > THREAD_STARTER_CACHE_TTL_MS) {
|
|
255
|
+
THREAD_STARTER_CACHE.delete(cacheKey);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (THREAD_STARTER_CACHE.size <= THREAD_STARTER_CACHE_MAX) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const excess = THREAD_STARTER_CACHE.size - THREAD_STARTER_CACHE_MAX;
|
|
262
|
+
let removed = 0;
|
|
263
|
+
for (const cacheKey of THREAD_STARTER_CACHE.keys()) {
|
|
264
|
+
THREAD_STARTER_CACHE.delete(cacheKey);
|
|
265
|
+
removed += 1;
|
|
266
|
+
if (removed >= excess) {
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
133
271
|
export async function resolveSlackThreadStarter(params) {
|
|
272
|
+
evictThreadStarterCache();
|
|
134
273
|
const cacheKey = `${params.channelId}:${params.threadTs}`;
|
|
135
274
|
const cached = THREAD_STARTER_CACHE.get(cacheKey);
|
|
275
|
+
if (cached && Date.now() - cached.cachedAt <= THREAD_STARTER_CACHE_TTL_MS) {
|
|
276
|
+
return cached.value;
|
|
277
|
+
}
|
|
136
278
|
if (cached) {
|
|
137
|
-
|
|
279
|
+
THREAD_STARTER_CACHE.delete(cacheKey);
|
|
138
280
|
}
|
|
139
281
|
try {
|
|
140
282
|
const response = (await params.client.conversations.replies({
|
|
@@ -154,10 +296,76 @@ export async function resolveSlackThreadStarter(params) {
|
|
|
154
296
|
ts: message.ts,
|
|
155
297
|
files: message.files,
|
|
156
298
|
};
|
|
157
|
-
THREAD_STARTER_CACHE.
|
|
299
|
+
if (THREAD_STARTER_CACHE.has(cacheKey)) {
|
|
300
|
+
THREAD_STARTER_CACHE.delete(cacheKey);
|
|
301
|
+
}
|
|
302
|
+
THREAD_STARTER_CACHE.set(cacheKey, {
|
|
303
|
+
value: starter,
|
|
304
|
+
cachedAt: Date.now(),
|
|
305
|
+
});
|
|
306
|
+
evictThreadStarterCache();
|
|
158
307
|
return starter;
|
|
159
308
|
}
|
|
160
309
|
catch {
|
|
161
310
|
return null;
|
|
162
311
|
}
|
|
163
312
|
}
|
|
313
|
+
export function resetSlackThreadStarterCacheForTest() {
|
|
314
|
+
THREAD_STARTER_CACHE.clear();
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Fetches the most recent messages in a Slack thread (excluding the current message).
|
|
318
|
+
* Used to populate thread context when a new thread session starts.
|
|
319
|
+
*
|
|
320
|
+
* Uses cursor pagination and keeps only the latest N retained messages so long threads
|
|
321
|
+
* still produce up-to-date context without unbounded memory growth.
|
|
322
|
+
*/
|
|
323
|
+
export async function resolveSlackThreadHistory(params) {
|
|
324
|
+
const maxMessages = params.limit ?? 20;
|
|
325
|
+
if (!Number.isFinite(maxMessages) || maxMessages <= 0) {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
// Slack recommends no more than 200 per page.
|
|
329
|
+
const fetchLimit = 200;
|
|
330
|
+
const retained = [];
|
|
331
|
+
let cursor;
|
|
332
|
+
try {
|
|
333
|
+
do {
|
|
334
|
+
const response = (await params.client.conversations.replies({
|
|
335
|
+
channel: params.channelId,
|
|
336
|
+
ts: params.threadTs,
|
|
337
|
+
limit: fetchLimit,
|
|
338
|
+
inclusive: true,
|
|
339
|
+
...(cursor ? { cursor } : {}),
|
|
340
|
+
}));
|
|
341
|
+
for (const msg of response.messages ?? []) {
|
|
342
|
+
// Keep messages with text OR file attachments
|
|
343
|
+
if (!msg.text?.trim() && !msg.files?.length) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (params.currentMessageTs && msg.ts === params.currentMessageTs) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
retained.push(msg);
|
|
350
|
+
if (retained.length > maxMessages) {
|
|
351
|
+
retained.shift();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const next = response.response_metadata?.next_cursor;
|
|
355
|
+
cursor = typeof next === "string" && next.trim().length > 0 ? next.trim() : undefined;
|
|
356
|
+
} while (cursor);
|
|
357
|
+
return retained.map((msg) => ({
|
|
358
|
+
// For file-only messages, create a placeholder showing attached filenames
|
|
359
|
+
text: msg.text?.trim()
|
|
360
|
+
? msg.text
|
|
361
|
+
: `[attached: ${msg.files?.map((f) => f.name ?? "file").join(", ")}]`,
|
|
362
|
+
userId: msg.user,
|
|
363
|
+
botId: msg.bot_id,
|
|
364
|
+
ts: msg.ts,
|
|
365
|
+
files: msg.files,
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -1,16 +1,43 @@
|
|
|
1
1
|
import { resolveHumanDelayConfig } from "../../../agents/identity.js";
|
|
2
2
|
import { dispatchInboundMessage } from "../../../auto-reply/dispatch.js";
|
|
3
3
|
import { clearHistoryEntriesIfEnabled } from "../../../auto-reply/reply/history.js";
|
|
4
|
+
import { createReplyDispatcherWithTyping } from "../../../auto-reply/reply/reply-dispatcher.js";
|
|
4
5
|
import { removeAckReactionAfterReply } from "../../../channels/ack-reactions.js";
|
|
5
6
|
import { logAckFailure, logTypingFailure } from "../../../channels/logging.js";
|
|
6
|
-
import {
|
|
7
|
+
import { createReplyPrefixOptions } from "../../../channels/reply-prefix.js";
|
|
7
8
|
import { createTypingCallbacks } from "../../../channels/typing.js";
|
|
8
|
-
import { createReplyDispatcherWithTyping } from "../../../auto-reply/reply/reply-dispatcher.js";
|
|
9
9
|
import { resolveStorePath, updateLastRoute } from "../../../config/sessions.js";
|
|
10
10
|
import { danger, logVerbose, shouldLogVerbose } from "../../../globals.js";
|
|
11
11
|
import { removeSlackReaction } from "../../actions.js";
|
|
12
|
+
import { createSlackDraftStream } from "../../draft-stream.js";
|
|
13
|
+
import { applyAppendOnlyStreamUpdate, buildStatusFinalPreviewText, resolveSlackStreamMode, } from "../../stream-mode.js";
|
|
14
|
+
import { appendSlackStream, startSlackStream, stopSlackStream } from "../../streaming.js";
|
|
12
15
|
import { resolveSlackThreadTargets } from "../../threading.js";
|
|
13
|
-
import { createSlackReplyDeliveryPlan, deliverReplies } from "../replies.js";
|
|
16
|
+
import { createSlackReplyDeliveryPlan, deliverReplies, resolveSlackThreadTs } from "../replies.js";
|
|
17
|
+
function hasMedia(payload) {
|
|
18
|
+
return Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
|
|
19
|
+
}
|
|
20
|
+
export function isSlackStreamingEnabled(streaming) {
|
|
21
|
+
return streaming !== false;
|
|
22
|
+
}
|
|
23
|
+
export function resolveSlackStreamingThreadHint(params) {
|
|
24
|
+
return resolveSlackThreadTs({
|
|
25
|
+
replyToMode: params.replyToMode,
|
|
26
|
+
incomingThreadTs: params.incomingThreadTs,
|
|
27
|
+
messageTs: params.messageTs,
|
|
28
|
+
hasReplied: false,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function shouldUseStreaming(params) {
|
|
32
|
+
if (!params.streamingEnabled) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (!params.threadTs) {
|
|
36
|
+
logVerbose("slack-stream: streaming disabled — no reply thread target available");
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
14
41
|
export async function dispatchPreparedSlackMessage(prepared) {
|
|
15
42
|
const { ctx, account, message, route } = prepared;
|
|
16
43
|
const cfg = ctx.cfg;
|
|
@@ -58,8 +85,9 @@ export async function dispatchPreparedSlackMessage(prepared) {
|
|
|
58
85
|
});
|
|
59
86
|
},
|
|
60
87
|
stop: async () => {
|
|
61
|
-
if (!didSetStatus)
|
|
88
|
+
if (!didSetStatus) {
|
|
62
89
|
return;
|
|
90
|
+
}
|
|
63
91
|
didSetStatus = false;
|
|
64
92
|
await ctx.setSlackThreadStatus({
|
|
65
93
|
channelId: message.channel,
|
|
@@ -86,12 +114,129 @@ export async function dispatchPreparedSlackMessage(prepared) {
|
|
|
86
114
|
});
|
|
87
115
|
},
|
|
88
116
|
});
|
|
89
|
-
const
|
|
117
|
+
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
|
|
118
|
+
cfg,
|
|
119
|
+
agentId: route.agentId,
|
|
120
|
+
channel: "slack",
|
|
121
|
+
accountId: route.accountId,
|
|
122
|
+
});
|
|
123
|
+
const streamingEnabled = isSlackStreamingEnabled(account.config.streaming);
|
|
124
|
+
const streamThreadHint = resolveSlackStreamingThreadHint({
|
|
125
|
+
replyToMode: ctx.replyToMode,
|
|
126
|
+
incomingThreadTs,
|
|
127
|
+
messageTs,
|
|
128
|
+
});
|
|
129
|
+
const useStreaming = shouldUseStreaming({
|
|
130
|
+
streamingEnabled,
|
|
131
|
+
threadTs: streamThreadHint,
|
|
132
|
+
});
|
|
133
|
+
let streamSession = null;
|
|
134
|
+
let streamFailed = false;
|
|
135
|
+
const deliverNormally = async (payload, forcedThreadTs) => {
|
|
136
|
+
const replyThreadTs = forcedThreadTs ?? replyPlan.nextThreadTs();
|
|
137
|
+
await deliverReplies({
|
|
138
|
+
replies: [payload],
|
|
139
|
+
target: prepared.replyTarget,
|
|
140
|
+
token: ctx.botToken,
|
|
141
|
+
accountId: account.accountId,
|
|
142
|
+
runtime,
|
|
143
|
+
textLimit: ctx.textLimit,
|
|
144
|
+
replyThreadTs,
|
|
145
|
+
});
|
|
146
|
+
replyPlan.markSent();
|
|
147
|
+
};
|
|
148
|
+
const deliverWithStreaming = async (payload) => {
|
|
149
|
+
if (streamFailed || hasMedia(payload) || !payload.text?.trim()) {
|
|
150
|
+
await deliverNormally(payload, streamSession?.threadTs);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const text = payload.text.trim();
|
|
154
|
+
let plannedThreadTs;
|
|
155
|
+
try {
|
|
156
|
+
if (!streamSession) {
|
|
157
|
+
const streamThreadTs = replyPlan.nextThreadTs();
|
|
158
|
+
plannedThreadTs = streamThreadTs;
|
|
159
|
+
if (!streamThreadTs) {
|
|
160
|
+
logVerbose("slack-stream: no reply thread target for stream start, falling back to normal delivery");
|
|
161
|
+
streamFailed = true;
|
|
162
|
+
await deliverNormally(payload);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
streamSession = await startSlackStream({
|
|
166
|
+
client: ctx.app.client,
|
|
167
|
+
channel: message.channel,
|
|
168
|
+
threadTs: streamThreadTs,
|
|
169
|
+
text,
|
|
170
|
+
});
|
|
171
|
+
replyPlan.markSent();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
await appendSlackStream({
|
|
175
|
+
session: streamSession,
|
|
176
|
+
text: "\n" + text,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
runtime.error?.(danger(`slack-stream: streaming API call failed: ${String(err)}, falling back`));
|
|
181
|
+
streamFailed = true;
|
|
182
|
+
await deliverNormally(payload, streamSession?.threadTs ?? plannedThreadTs);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
90
185
|
const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
|
|
91
|
-
|
|
92
|
-
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
186
|
+
...prefixOptions,
|
|
93
187
|
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
|
|
94
188
|
deliver: async (payload) => {
|
|
189
|
+
if (useStreaming) {
|
|
190
|
+
await deliverWithStreaming(payload);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const mediaCount = payload.mediaUrls?.length ?? (payload.mediaUrl ? 1 : 0);
|
|
194
|
+
const draftMessageId = draftStream?.messageId();
|
|
195
|
+
const draftChannelId = draftStream?.channelId();
|
|
196
|
+
const finalText = payload.text;
|
|
197
|
+
const canFinalizeViaPreviewEdit = streamMode !== "status_final" &&
|
|
198
|
+
mediaCount === 0 &&
|
|
199
|
+
!payload.isError &&
|
|
200
|
+
typeof finalText === "string" &&
|
|
201
|
+
finalText.trim().length > 0 &&
|
|
202
|
+
typeof draftMessageId === "string" &&
|
|
203
|
+
typeof draftChannelId === "string";
|
|
204
|
+
if (canFinalizeViaPreviewEdit) {
|
|
205
|
+
draftStream?.stop();
|
|
206
|
+
try {
|
|
207
|
+
await ctx.app.client.chat.update({
|
|
208
|
+
token: ctx.botToken,
|
|
209
|
+
channel: draftChannelId,
|
|
210
|
+
ts: draftMessageId,
|
|
211
|
+
text: finalText.trim(),
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
logVerbose(`slack: preview final edit failed; falling back to standard send (${String(err)})`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else if (streamMode === "status_final" && hasStreamedMessage) {
|
|
220
|
+
try {
|
|
221
|
+
const statusChannelId = draftStream?.channelId();
|
|
222
|
+
const statusMessageId = draftStream?.messageId();
|
|
223
|
+
if (statusChannelId && statusMessageId) {
|
|
224
|
+
await ctx.app.client.chat.update({
|
|
225
|
+
token: ctx.botToken,
|
|
226
|
+
channel: statusChannelId,
|
|
227
|
+
ts: statusMessageId,
|
|
228
|
+
text: "Status: complete. Final answer posted below.",
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
logVerbose(`slack: status_final completion update failed (${String(err)})`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else if (mediaCount > 0) {
|
|
237
|
+
await draftStream?.clear();
|
|
238
|
+
hasStreamedMessage = false;
|
|
239
|
+
}
|
|
95
240
|
const replyThreadTs = replyPlan.nextThreadTs();
|
|
96
241
|
await deliverReplies({
|
|
97
242
|
replies: [payload],
|
|
@@ -111,6 +256,53 @@ export async function dispatchPreparedSlackMessage(prepared) {
|
|
|
111
256
|
onReplyStart: typingCallbacks.onReplyStart,
|
|
112
257
|
onIdle: typingCallbacks.onIdle,
|
|
113
258
|
});
|
|
259
|
+
const draftStream = createSlackDraftStream({
|
|
260
|
+
target: prepared.replyTarget,
|
|
261
|
+
token: ctx.botToken,
|
|
262
|
+
accountId: account.accountId,
|
|
263
|
+
maxChars: Math.min(ctx.textLimit, 4000),
|
|
264
|
+
resolveThreadTs: () => replyPlan.nextThreadTs(),
|
|
265
|
+
onMessageSent: () => replyPlan.markSent(),
|
|
266
|
+
log: logVerbose,
|
|
267
|
+
warn: logVerbose,
|
|
268
|
+
});
|
|
269
|
+
let hasStreamedMessage = false;
|
|
270
|
+
const streamMode = resolveSlackStreamMode(account.config.streamMode);
|
|
271
|
+
let appendRenderedText = "";
|
|
272
|
+
let appendSourceText = "";
|
|
273
|
+
let statusUpdateCount = 0;
|
|
274
|
+
const updateDraftFromPartial = (text) => {
|
|
275
|
+
const trimmed = text?.trimEnd();
|
|
276
|
+
if (!trimmed) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (streamMode === "append") {
|
|
280
|
+
const next = applyAppendOnlyStreamUpdate({
|
|
281
|
+
incoming: trimmed,
|
|
282
|
+
rendered: appendRenderedText,
|
|
283
|
+
source: appendSourceText,
|
|
284
|
+
});
|
|
285
|
+
appendRenderedText = next.rendered;
|
|
286
|
+
appendSourceText = next.source;
|
|
287
|
+
if (!next.changed) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
draftStream.update(next.rendered);
|
|
291
|
+
hasStreamedMessage = true;
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (streamMode === "status_final") {
|
|
295
|
+
statusUpdateCount += 1;
|
|
296
|
+
if (statusUpdateCount > 1 && statusUpdateCount % 4 !== 0) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
draftStream.update(buildStatusFinalPreviewText(statusUpdateCount));
|
|
300
|
+
hasStreamedMessage = true;
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
draftStream.update(trimmed);
|
|
304
|
+
hasStreamedMessage = true;
|
|
305
|
+
};
|
|
114
306
|
const { queuedFinal, counts } = await dispatchInboundMessage({
|
|
115
307
|
ctx: prepared.ctxPayload,
|
|
116
308
|
cfg,
|
|
@@ -119,17 +311,59 @@ export async function dispatchPreparedSlackMessage(prepared) {
|
|
|
119
311
|
...replyOptions,
|
|
120
312
|
skillFilter: prepared.channelConfig?.skills,
|
|
121
313
|
hasRepliedRef,
|
|
122
|
-
disableBlockStreaming:
|
|
123
|
-
?
|
|
124
|
-
:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
314
|
+
disableBlockStreaming: useStreaming
|
|
315
|
+
? false
|
|
316
|
+
: typeof account.config.blockStreaming === "boolean"
|
|
317
|
+
? !account.config.blockStreaming
|
|
318
|
+
: undefined,
|
|
319
|
+
onModelSelected,
|
|
320
|
+
onPartialReply: useStreaming
|
|
321
|
+
? undefined
|
|
322
|
+
: async (payload) => {
|
|
323
|
+
updateDraftFromPartial(payload.text);
|
|
324
|
+
},
|
|
325
|
+
onAssistantMessageStart: useStreaming
|
|
326
|
+
? undefined
|
|
327
|
+
: async () => {
|
|
328
|
+
if (hasStreamedMessage) {
|
|
329
|
+
draftStream.forceNewMessage();
|
|
330
|
+
hasStreamedMessage = false;
|
|
331
|
+
appendRenderedText = "";
|
|
332
|
+
appendSourceText = "";
|
|
333
|
+
statusUpdateCount = 0;
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
onReasoningEnd: useStreaming
|
|
337
|
+
? undefined
|
|
338
|
+
: async () => {
|
|
339
|
+
if (hasStreamedMessage) {
|
|
340
|
+
draftStream.forceNewMessage();
|
|
341
|
+
hasStreamedMessage = false;
|
|
342
|
+
appendRenderedText = "";
|
|
343
|
+
appendSourceText = "";
|
|
344
|
+
statusUpdateCount = 0;
|
|
345
|
+
}
|
|
346
|
+
},
|
|
128
347
|
},
|
|
129
348
|
});
|
|
349
|
+
await draftStream.flush();
|
|
350
|
+
draftStream.stop();
|
|
130
351
|
markDispatchIdle();
|
|
352
|
+
// -----------------------------------------------------------------------
|
|
353
|
+
// Finalize the stream if one was started
|
|
354
|
+
// -----------------------------------------------------------------------
|
|
355
|
+
const finalStream = streamSession;
|
|
356
|
+
if (finalStream && !finalStream.stopped) {
|
|
357
|
+
try {
|
|
358
|
+
await stopSlackStream({ session: finalStream });
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
runtime.error?.(danger(`slack-stream: failed to stop stream: ${String(err)}`));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
131
364
|
const anyReplyDelivered = queuedFinal || (counts.block ?? 0) > 0 || (counts.final ?? 0) > 0;
|
|
132
365
|
if (!anyReplyDelivered) {
|
|
366
|
+
await draftStream.clear();
|
|
133
367
|
if (prepared.isRoomish) {
|
|
134
368
|
clearHistoryEntriesIfEnabled({
|
|
135
369
|
historyMap: ctx.channelHistories,
|