@swarmclawai/swarmclaw 0.8.4 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/bin/swarmclaw.js +5 -1
- package/bin/worker-cmd.js +73 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +17 -7
- package/src/app/api/agents/route.ts +21 -8
- package/src/app/api/approvals/route.test.ts +6 -6
- package/src/app/api/approvals/route.ts +2 -1
- package/src/app/api/auth/route.ts +2 -3
- package/src/app/api/chatrooms/[id]/chat/route.test.ts +299 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +3 -2
- package/src/app/api/chatrooms/[id]/route.ts +7 -6
- package/src/app/api/chats/[id]/chat/route.test.ts +496 -0
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/clear/route.ts +9 -9
- package/src/app/api/chats/[id]/devserver/route.ts +2 -1
- package/src/app/api/chats/[id]/edit-resend/route.ts +3 -4
- package/src/app/api/chats/[id]/fork/route.ts +3 -5
- package/src/app/api/chats/[id]/restore/route.ts +6 -7
- package/src/app/api/chats/[id]/retry/route.ts +3 -4
- package/src/app/api/chats/[id]/route.ts +61 -62
- package/src/app/api/chats/route.ts +7 -1
- package/src/app/api/connectors/[id]/route.ts +7 -8
- package/src/app/api/connectors/route.ts +5 -4
- package/src/app/api/eval/run/route.ts +2 -1
- package/src/app/api/eval/suite/route.ts +2 -1
- package/src/app/api/external-agents/route.test.ts +1 -1
- package/src/app/api/external-agents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +1 -1
- package/src/app/api/gateways/[id]/route.ts +7 -5
- package/src/app/api/gateways/route.ts +1 -1
- package/src/app/api/knowledge/upload/route.ts +1 -1
- package/src/app/api/logs/route.ts +5 -7
- package/src/app/api/memory-images/[filename]/route.ts +2 -3
- package/src/app/api/openclaw/agent-files/route.ts +4 -3
- package/src/app/api/openclaw/approvals/route.ts +3 -4
- package/src/app/api/openclaw/config-sync/route.ts +3 -2
- package/src/app/api/openclaw/cron/route.ts +3 -2
- package/src/app/api/openclaw/dotenv-keys/route.ts +2 -1
- package/src/app/api/openclaw/exec-config/route.ts +3 -2
- package/src/app/api/openclaw/gateway/route.ts +5 -4
- package/src/app/api/openclaw/history/route.ts +3 -2
- package/src/app/api/openclaw/media/route.ts +2 -1
- package/src/app/api/openclaw/permissions/route.ts +3 -2
- package/src/app/api/openclaw/sandbox-env/route.ts +3 -2
- package/src/app/api/openclaw/skills/install/route.ts +2 -1
- package/src/app/api/openclaw/skills/remove/route.ts +2 -1
- package/src/app/api/openclaw/skills/route.ts +3 -2
- package/src/app/api/orchestrator/run/route.ts +5 -14
- package/src/app/api/perf/route.ts +43 -0
- package/src/app/api/plugins/dependencies/route.ts +2 -1
- package/src/app/api/plugins/install/route.ts +2 -1
- package/src/app/api/plugins/marketplace/route.ts +3 -2
- package/src/app/api/plugins/settings/route.ts +2 -1
- package/src/app/api/preview-server/route.ts +11 -10
- package/src/app/api/projects/[id]/route.ts +1 -1
- package/src/app/api/schedules/[id]/route.test.ts +128 -0
- package/src/app/api/schedules/[id]/route.ts +43 -43
- package/src/app/api/schedules/[id]/run/route.ts +11 -62
- package/src/app/api/schedules/route.ts +21 -87
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/doctor/route.ts +9 -8
- package/src/app/api/tasks/[id]/approve/route.ts +33 -30
- package/src/app/api/tasks/[id]/route.ts +12 -35
- package/src/app/api/tasks/import/github/route.ts +2 -1
- package/src/app/api/tasks/route.ts +79 -91
- package/src/app/api/wallets/[id]/approve/route.ts +2 -1
- package/src/app/api/wallets/[id]/route.ts +13 -19
- package/src/app/api/wallets/[id]/send/route.ts +2 -1
- package/src/app/api/wallets/route.ts +2 -1
- package/src/app/api/webhooks/[id]/route.ts +2 -1
- package/src/app/api/webhooks/route.test.ts +3 -1
- package/src/app/page.tsx +23 -331
- package/src/cli/index.js +19 -0
- package/src/cli/index.ts +38 -7
- package/src/cli/spec.js +9 -0
- package/src/components/activity/activity-feed.tsx +7 -4
- package/src/components/agents/agent-card.tsx +32 -6
- package/src/components/agents/agent-chat-list.tsx +55 -22
- package/src/components/agents/agent-files-editor.tsx +3 -2
- package/src/components/agents/agent-sheet.tsx +123 -22
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/openclaw-skills-panel.tsx +2 -1
- package/src/components/agents/trash-list.tsx +1 -1
- package/src/components/auth/access-key-gate.tsx +8 -2
- package/src/components/auth/setup-wizard.tsx +10 -9
- package/src/components/auth/user-picker.tsx +3 -2
- package/src/components/chat/chat-area.tsx +20 -1
- package/src/components/chat/chat-card.tsx +18 -3
- package/src/components/chat/chat-header.tsx +24 -4
- package/src/components/chat/chat-list.tsx +2 -11
- package/src/components/chat/heartbeat-history-panel.tsx +2 -1
- package/src/components/chat/message-bubble.tsx +45 -6
- package/src/components/chat/message-list.tsx +280 -145
- package/src/components/chat/streaming-bubble.tsx +217 -60
- package/src/components/chat/swarm-panel.test.ts +274 -0
- package/src/components/chat/swarm-panel.tsx +410 -0
- package/src/components/chat/swarm-status-card.tsx +346 -0
- package/src/components/chat/tool-call-bubble.tsx +48 -23
- package/src/components/chatrooms/chatroom-list.tsx +8 -5
- package/src/components/chatrooms/chatroom-message.tsx +10 -7
- package/src/components/chatrooms/chatroom-view.tsx +12 -9
- package/src/components/connectors/connector-health.tsx +6 -4
- package/src/components/connectors/connector-list.tsx +16 -11
- package/src/components/connectors/connector-sheet.tsx +12 -6
- package/src/components/home/home-view.tsx +38 -24
- package/src/components/input/chat-input.tsx +10 -1
- package/src/components/layout/app-layout.tsx +2 -38
- package/src/components/layout/sheet-layer.tsx +50 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +37 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +12 -2
- package/src/components/plugins/plugin-list.tsx +8 -4
- package/src/components/plugins/plugin-sheet.tsx +2 -1
- package/src/components/providers/provider-list.tsx +3 -2
- package/src/components/providers/provider-sheet.tsx +2 -1
- package/src/components/runs/run-list.tsx +11 -7
- package/src/components/schedules/schedule-card.tsx +5 -3
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/attachment-chip.tsx +19 -3
- package/src/components/shared/notification-center.tsx +6 -3
- package/src/components/shared/settings/plugin-manager.tsx +3 -2
- package/src/components/shared/settings/section-embedding.tsx +2 -1
- package/src/components/shared/settings/section-orchestrator.tsx +2 -1
- package/src/components/shared/settings/section-user-preferences.tsx +107 -0
- package/src/components/shared/settings/settings-page.tsx +13 -9
- package/src/components/skills/clawhub-browser.tsx +15 -4
- package/src/components/skills/skill-list.tsx +15 -4
- package/src/components/tasks/approvals-panel.tsx +2 -1
- package/src/components/tasks/task-board.tsx +35 -37
- package/src/components/tasks/task-sheet.tsx +4 -3
- package/src/components/ui/full-screen-loader.tsx +164 -0
- package/src/components/wallets/wallet-approval-dialog.tsx +2 -1
- package/src/components/wallets/wallet-panel.tsx +6 -5
- package/src/components/wallets/wallet-section.tsx +3 -2
- package/src/components/webhooks/webhook-list.tsx +4 -5
- package/src/components/webhooks/webhook-sheet.tsx +6 -6
- package/src/hooks/use-app-bootstrap.ts +202 -0
- package/src/hooks/use-mounted-ref.ts +14 -0
- package/src/hooks/use-now.ts +31 -0
- package/src/hooks/use-openclaw-gateway.ts +2 -1
- package/src/instrumentation.ts +20 -8
- package/src/lib/agent-default-tools.test.ts +52 -0
- package/src/lib/agent-default-tools.ts +40 -0
- package/src/lib/api-client.test.ts +21 -0
- package/src/lib/api-client.ts +6 -11
- package/src/lib/canvas-content.test.ts +360 -0
- package/src/lib/chat-streaming-state.test.ts +49 -2
- package/src/lib/chat-streaming-state.ts +26 -10
- package/src/lib/fetch-timeout.test.ts +54 -0
- package/src/lib/fetch-timeout.ts +60 -3
- package/src/lib/live-tool-events.test.ts +77 -0
- package/src/lib/live-tool-events.ts +73 -0
- package/src/lib/local-observability.test.ts +2 -2
- package/src/lib/openclaw-endpoint.test.ts +1 -1
- package/src/lib/providers/anthropic.ts +12 -16
- package/src/lib/providers/index.ts +4 -2
- package/src/lib/providers/ollama.ts +9 -6
- package/src/lib/providers/openai.ts +11 -14
- package/src/lib/runtime-env.test.ts +8 -8
- package/src/lib/schedule-dedupe-advanced.test.ts +2 -2
- package/src/lib/schedule-dedupe.test.ts +1 -1
- package/src/lib/schedule-dedupe.ts +3 -2
- package/src/lib/server/agent-thread-session.test.ts +6 -6
- package/src/lib/server/agent-thread-session.ts +6 -9
- package/src/lib/server/alert-dispatch.ts +2 -1
- package/src/lib/server/api-routes.test.ts +6 -6
- package/src/lib/server/approval-connector-notify.test.ts +4 -4
- package/src/lib/server/approvals-auto-approve.test.ts +29 -29
- package/src/lib/server/approvals.test.ts +317 -0
- package/src/lib/server/approvals.ts +5 -4
- package/src/lib/server/autonomy-runtime.test.ts +11 -11
- package/src/lib/server/browser-state.ts +2 -2
- package/src/lib/server/capability-router.test.ts +1 -1
- package/src/lib/server/capability-router.ts +3 -2
- package/src/lib/server/chat-execution-advanced.test.ts +15 -2
- package/src/lib/server/chat-execution-connector-delivery.ts +67 -0
- package/src/lib/server/chat-execution-disabled.test.ts +3 -3
- package/src/lib/server/chat-execution-eval-history.test.ts +3 -3
- package/src/lib/server/chat-execution-heartbeat.test.ts +42 -1
- package/src/lib/server/chat-execution-session-sync.test.ts +119 -0
- package/src/lib/server/chat-execution-tool-events.ts +116 -0
- package/src/lib/server/chat-execution-utils.test.ts +479 -0
- package/src/lib/server/chat-execution-utils.ts +533 -0
- package/src/lib/server/chat-execution.ts +153 -748
- package/src/lib/server/chat-streaming-utils.ts +174 -0
- package/src/lib/server/chat-turn-tool-routing.ts +310 -0
- package/src/lib/server/chatroom-session-persistence.test.ts +2 -2
- package/src/lib/server/clawhub-client.ts +2 -1
- package/src/lib/server/collection-helpers.test.ts +92 -0
- package/src/lib/server/collection-helpers.ts +25 -3
- package/src/lib/server/connectors/access.ts +146 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +1 -1
- package/src/lib/server/connectors/bluebubbles.ts +4 -4
- package/src/lib/server/connectors/commands.ts +367 -0
- package/src/lib/server/connectors/connector-routing.test.ts +4 -4
- package/src/lib/server/connectors/delivery.ts +142 -0
- package/src/lib/server/connectors/discord.ts +37 -40
- package/src/lib/server/connectors/email.ts +11 -10
- package/src/lib/server/connectors/googlechat.ts +4 -4
- package/src/lib/server/connectors/inbound-audio-transcription.ts +2 -1
- package/src/lib/server/connectors/ingress-delivery.ts +23 -0
- package/src/lib/server/connectors/manager-roundtrip.test.ts +300 -0
- package/src/lib/server/connectors/manager.test.ts +352 -77
- package/src/lib/server/connectors/manager.ts +134 -673
- package/src/lib/server/connectors/matrix.ts +4 -4
- package/src/lib/server/connectors/message-sentinel.ts +7 -0
- package/src/lib/server/connectors/openclaw.test.ts +1 -1
- package/src/lib/server/connectors/openclaw.ts +8 -10
- package/src/lib/server/connectors/outbox.test.ts +192 -0
- package/src/lib/server/connectors/outbox.ts +369 -0
- package/src/lib/server/connectors/pairing.test.ts +18 -1
- package/src/lib/server/connectors/pairing.ts +49 -4
- package/src/lib/server/connectors/policy.ts +9 -3
- package/src/lib/server/connectors/reconnect-state.ts +71 -0
- package/src/lib/server/connectors/response-media.ts +256 -0
- package/src/lib/server/connectors/runtime-state.ts +67 -0
- package/src/lib/server/connectors/session.test.ts +357 -0
- package/src/lib/server/connectors/session.ts +422 -0
- package/src/lib/server/connectors/signal.ts +7 -7
- package/src/lib/server/connectors/slack.ts +43 -43
- package/src/lib/server/connectors/teams.ts +4 -4
- package/src/lib/server/connectors/telegram.ts +37 -43
- package/src/lib/server/connectors/types.ts +31 -1
- package/src/lib/server/connectors/whatsapp.test.ts +108 -0
- package/src/lib/server/connectors/whatsapp.ts +106 -34
- package/src/lib/server/context-manager.test.ts +409 -0
- package/src/lib/server/cost.test.ts +1 -1
- package/src/lib/server/daemon-policy.ts +78 -0
- package/src/lib/server/daemon-state-connectors.test.ts +167 -0
- package/src/lib/server/daemon-state.test.ts +283 -55
- package/src/lib/server/daemon-state.ts +106 -109
- package/src/lib/server/data-dir.test.ts +5 -5
- package/src/lib/server/data-dir.ts +4 -0
- package/src/lib/server/delegation-jobs-advanced.test.ts +1 -1
- package/src/lib/server/delegation-jobs.test.ts +87 -0
- package/src/lib/server/delegation-jobs.ts +42 -48
- package/src/lib/server/devserver-launch.ts +1 -1
- package/src/lib/server/document-utils.ts +7 -9
- package/src/lib/server/elevenlabs.ts +2 -1
- package/src/lib/server/embeddings.test.ts +105 -0
- package/src/lib/server/ethereum.ts +3 -2
- package/src/lib/server/eval/agent-regression.ts +3 -2
- package/src/lib/server/eval/runner.ts +2 -1
- package/src/lib/server/eval/scorer.ts +2 -1
- package/src/lib/server/evm-swap.ts +2 -1
- package/src/lib/server/gateway/protocol.test.ts +1 -1
- package/src/lib/server/guardian.ts +2 -1
- package/src/lib/server/heartbeat-blocked-suppression.test.ts +151 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +6 -6
- package/src/lib/server/heartbeat-service.test.ts +406 -0
- package/src/lib/server/heartbeat-service.ts +54 -7
- package/src/lib/server/heartbeat-wake.test.ts +19 -0
- package/src/lib/server/heartbeat-wake.ts +17 -16
- package/src/lib/server/integrity-monitor.test.ts +149 -0
- package/src/lib/server/json-utils.ts +22 -0
- package/src/lib/server/knowledge-db.test.ts +13 -13
- package/src/lib/server/link-understanding.ts +2 -1
- package/src/lib/server/llm-response-cache.test.ts +1 -1
- package/src/lib/server/main-agent-loop-advanced.test.ts +65 -3
- package/src/lib/server/main-agent-loop.test.ts +6 -6
- package/src/lib/server/main-agent-loop.ts +21 -7
- package/src/lib/server/mcp-client.test.ts +1 -1
- package/src/lib/server/mcp-conformance.test.ts +1 -1
- package/src/lib/server/mcp-conformance.ts +3 -2
- package/src/lib/server/memory-consolidation.ts +2 -1
- package/src/lib/server/memory-db.test.ts +485 -0
- package/src/lib/server/memory-db.ts +39 -26
- package/src/lib/server/memory-graph.test.ts +2 -2
- package/src/lib/server/memory-policy.test.ts +7 -7
- package/src/lib/server/memory-retrieval.test.ts +1 -1
- package/src/lib/server/openclaw-config-sync.ts +2 -1
- package/src/lib/server/openclaw-deploy.test.ts +1 -1
- package/src/lib/server/openclaw-deploy.ts +8 -12
- package/src/lib/server/openclaw-exec-config.ts +2 -1
- package/src/lib/server/openclaw-gateway.ts +6 -7
- package/src/lib/server/openclaw-skills-normalize.ts +2 -1
- package/src/lib/server/openclaw-sync.ts +7 -5
- package/src/lib/server/orchestrator-lg-structure.test.ts +17 -0
- package/src/lib/server/orchestrator-lg.ts +199 -327
- package/src/lib/server/path-utils.ts +31 -0
- package/src/lib/server/perf.ts +161 -0
- package/src/lib/server/plugins-approval-guidance.ts +115 -0
- package/src/lib/server/plugins.test.ts +1 -1
- package/src/lib/server/plugins.ts +22 -132
- package/src/lib/server/process-manager.ts +5 -8
- package/src/lib/server/provider-health.test.ts +137 -0
- package/src/lib/server/provider-health.ts +3 -3
- package/src/lib/server/provider-model-discovery.ts +3 -12
- package/src/lib/server/queue-followups.test.ts +9 -9
- package/src/lib/server/queue-reconcile.test.ts +2 -2
- package/src/lib/server/queue-recovery.test.ts +269 -0
- package/src/lib/server/queue.test.ts +570 -0
- package/src/lib/server/queue.ts +62 -455
- package/src/lib/server/resolve-image.ts +30 -0
- package/src/lib/server/runtime-settings.test.ts +4 -4
- package/src/lib/server/runtime-storage-write-paths.test.ts +60 -0
- package/src/lib/server/schedule-normalization.test.ts +279 -0
- package/src/lib/server/schedule-service.ts +263 -0
- package/src/lib/server/scheduler.ts +17 -74
- package/src/lib/server/session-mailbox.test.ts +191 -0
- package/src/lib/server/session-run-manager.test.ts +640 -0
- package/src/lib/server/session-run-manager.ts +59 -15
- package/src/lib/server/session-tools/autonomy-tools.test.ts +20 -20
- package/src/lib/server/session-tools/calendar.ts +2 -1
- package/src/lib/server/session-tools/canvas.ts +2 -1
- package/src/lib/server/session-tools/chatroom.ts +2 -1
- package/src/lib/server/session-tools/connector.ts +26 -28
- package/src/lib/server/session-tools/context-mgmt.ts +3 -2
- package/src/lib/server/session-tools/crawl.ts +4 -3
- package/src/lib/server/session-tools/crud.ts +105 -324
- package/src/lib/server/session-tools/delegate-fallback.test.ts +9 -9
- package/src/lib/server/session-tools/delegate.ts +6 -8
- package/src/lib/server/session-tools/discovery-approvals.test.ts +15 -15
- package/src/lib/server/session-tools/discovery.ts +4 -3
- package/src/lib/server/session-tools/document.ts +2 -1
- package/src/lib/server/session-tools/email.ts +2 -1
- package/src/lib/server/session-tools/extract.ts +2 -1
- package/src/lib/server/session-tools/file.ts +4 -3
- package/src/lib/server/session-tools/http.ts +2 -1
- package/src/lib/server/session-tools/human-loop.ts +2 -1
- package/src/lib/server/session-tools/image-gen.ts +4 -3
- package/src/lib/server/session-tools/index.ts +26 -30
- package/src/lib/server/session-tools/mailbox.ts +2 -1
- package/src/lib/server/session-tools/manage-connectors.test.ts +4 -4
- package/src/lib/server/session-tools/manage-schedules.test.ts +12 -12
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +5 -5
- package/src/lib/server/session-tools/manage-tasks.test.ts +2 -2
- package/src/lib/server/session-tools/monitor.ts +2 -1
- package/src/lib/server/session-tools/platform.ts +2 -1
- package/src/lib/server/session-tools/plugin-creator.ts +2 -1
- package/src/lib/server/session-tools/replicate.ts +3 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +6 -6
- package/src/lib/server/session-tools/shell.ts +4 -9
- package/src/lib/server/session-tools/subagent.ts +322 -170
- package/src/lib/server/session-tools/table.ts +6 -5
- package/src/lib/server/session-tools/wallet-tool.test.ts +3 -3
- package/src/lib/server/session-tools/wallet.ts +7 -6
- package/src/lib/server/session-tools/web-browser-config.test.ts +1 -0
- package/src/lib/server/session-tools/web-utils.ts +317 -0
- package/src/lib/server/session-tools/web.ts +62 -328
- package/src/lib/server/skill-prompt-budget.test.ts +1 -1
- package/src/lib/server/skills-normalize.ts +2 -1
- package/src/lib/server/storage-item-access.test.ts +302 -0
- package/src/lib/server/storage.ts +366 -314
- package/src/lib/server/stream-agent-chat.test.ts +82 -3
- package/src/lib/server/stream-agent-chat.ts +146 -510
- package/src/lib/server/stream-continuation.ts +412 -0
- package/src/lib/server/subagent-lineage.test.ts +647 -0
- package/src/lib/server/subagent-lineage.ts +435 -0
- package/src/lib/server/subagent-runtime.test.ts +484 -0
- package/src/lib/server/subagent-runtime.ts +419 -0
- package/src/lib/server/subagent-swarm.test.ts +391 -0
- package/src/lib/server/subagent-swarm.ts +564 -0
- package/src/lib/server/system-events.ts +3 -3
- package/src/lib/server/task-followups.test.ts +491 -0
- package/src/lib/server/task-followups.ts +391 -0
- package/src/lib/server/task-lifecycle.test.ts +205 -0
- package/src/lib/server/task-lifecycle.ts +200 -0
- package/src/lib/server/task-quality-gate.test.ts +1 -1
- package/src/lib/server/task-resume.ts +208 -0
- package/src/lib/server/task-service.test.ts +108 -0
- package/src/lib/server/task-service.ts +264 -0
- package/src/lib/server/task-validation.test.ts +1 -1
- package/src/lib/server/test-utils/run-with-temp-data-dir.ts +42 -0
- package/src/lib/server/tool-capability-policy.test.ts +2 -2
- package/src/lib/server/tool-capability-policy.ts +3 -2
- package/src/lib/server/tool-planning.ts +2 -1
- package/src/lib/server/tool-retry.ts +2 -3
- package/src/lib/server/wake-dispatcher.test.ts +303 -0
- package/src/lib/server/wake-dispatcher.ts +318 -0
- package/src/lib/server/wake-mode.test.ts +161 -0
- package/src/lib/server/wake-mode.ts +174 -0
- package/src/lib/server/wallet-service.ts +8 -9
- package/src/lib/server/watch-jobs.ts +2 -1
- package/src/lib/server/workspace-context.ts +2 -2
- package/src/lib/shared-utils.test.ts +142 -0
- package/src/lib/shared-utils.ts +62 -0
- package/src/lib/tool-event-summary.ts +2 -1
- package/src/lib/view-routes.test.ts +100 -0
- package/src/lib/wallet.test.ts +322 -6
- package/src/proxy.test.ts +4 -4
- package/src/proxy.ts +2 -3
- package/src/stores/set-if-changed.ts +40 -0
- package/src/stores/slices/agent-slice.ts +111 -0
- package/src/stores/slices/auth-slice.ts +25 -0
- package/src/stores/slices/data-slice.ts +301 -0
- package/src/stores/slices/index.ts +7 -0
- package/src/stores/slices/session-slice.ts +112 -0
- package/src/stores/slices/task-slice.ts +63 -0
- package/src/stores/slices/ui-slice.ts +192 -0
- package/src/stores/use-app-store.ts +17 -822
- package/src/stores/use-approval-store.ts +2 -1
- package/src/stores/use-chat-store.ts +8 -1
- package/src/types/index.ts +10 -0
|
@@ -6,9 +6,8 @@ import {
|
|
|
6
6
|
upsertConnectorHealthEvent,
|
|
7
7
|
} from '../storage'
|
|
8
8
|
import type { ConnectorHealthEventType } from '@/types'
|
|
9
|
+
import { dedup, errorMessage, sleep } from '@/lib/shared-utils'
|
|
9
10
|
import { WORKSPACE_DIR } from '../data-dir'
|
|
10
|
-
import { UPLOAD_DIR } from '../storage'
|
|
11
|
-
import fs from 'fs'
|
|
12
11
|
import path from 'path'
|
|
13
12
|
import { streamAgentChat } from '../stream-agent-chat'
|
|
14
13
|
import { notify } from '../ws-hub'
|
|
@@ -33,15 +32,13 @@ import { buildIdentityContinuityContext } from '../identity-continuity'
|
|
|
33
32
|
import { ensureAgentThreadSession } from '../agent-thread-session'
|
|
34
33
|
import { getProvider } from '@/lib/providers'
|
|
35
34
|
import type { Agent, Connector, MessageSource, Chatroom, ChatroomMessage, Session } from '@/types'
|
|
36
|
-
import type { ConnectorInstance, InboundMessage
|
|
35
|
+
import type { ConnectorInstance, InboundMessage } from './types'
|
|
37
36
|
import {
|
|
38
37
|
addAllowedSender,
|
|
39
38
|
approvePairingCode,
|
|
40
39
|
createOrTouchPairingRequest,
|
|
41
|
-
isSenderAllowed,
|
|
42
40
|
listPendingPairingRequests,
|
|
43
41
|
listStoredAllowedSenders,
|
|
44
|
-
parseAllowFromCsv,
|
|
45
42
|
parsePairingPolicy,
|
|
46
43
|
type PairingPolicy,
|
|
47
44
|
} from './pairing'
|
|
@@ -61,7 +58,66 @@ import {
|
|
|
61
58
|
} from './policy'
|
|
62
59
|
import { buildConnectorThreadContextBlock, resolveThreadPersonaLabel } from './thread-context'
|
|
63
60
|
import { shouldSuppressHiddenControlText, stripHiddenControlTokens } from '../assistant-control'
|
|
64
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
buildInboundApprovalSubject as buildInboundApprovalSubjectHelper,
|
|
63
|
+
enforceInboundAccessPolicy as enforceInboundAccessPolicyHelper,
|
|
64
|
+
resolveInboundApprovalSenderId as resolveInboundApprovalSenderIdHelper,
|
|
65
|
+
resolvePairingAccess as resolvePairingAccessHelper,
|
|
66
|
+
} from './access'
|
|
67
|
+
import {
|
|
68
|
+
findDirectSessionForInbound as findDirectSessionForInboundHelper,
|
|
69
|
+
pushSessionMessage as pushSessionMessageHelper,
|
|
70
|
+
resolveDirectSession as resolveDirectSessionHelper,
|
|
71
|
+
} from './session'
|
|
72
|
+
import { NO_MESSAGE_SENTINEL, isNoMessage } from './message-sentinel'
|
|
73
|
+
import {
|
|
74
|
+
buildInboundAttachmentPaths,
|
|
75
|
+
connectorSupportsBinaryMedia,
|
|
76
|
+
extractEmbeddedMedia,
|
|
77
|
+
formatInboundUserText,
|
|
78
|
+
formatMediaLine,
|
|
79
|
+
normalizeWhatsappTarget,
|
|
80
|
+
parseConnectorToolInput,
|
|
81
|
+
parseConnectorToolResult,
|
|
82
|
+
parseSseDataEvents,
|
|
83
|
+
selectOutboundMediaFiles,
|
|
84
|
+
uploadApiUrlFromPath,
|
|
85
|
+
visibleConnectorToolText,
|
|
86
|
+
} from './response-media'
|
|
87
|
+
import {
|
|
88
|
+
getConnectorReplySendOptions,
|
|
89
|
+
maybeSendStatusReaction,
|
|
90
|
+
recordConnectorOutboundDelivery,
|
|
91
|
+
} from './delivery'
|
|
92
|
+
import { enqueueConnectorOutbox } from './outbox'
|
|
93
|
+
import {
|
|
94
|
+
advanceConnectorReconnectState,
|
|
95
|
+
clearReconnectState,
|
|
96
|
+
connectorReconnectStateStore,
|
|
97
|
+
createConnectorReconnectState,
|
|
98
|
+
getAllReconnectStates,
|
|
99
|
+
getReconnectState,
|
|
100
|
+
setReconnectState,
|
|
101
|
+
type ConnectorReconnectState,
|
|
102
|
+
} from './reconnect-state'
|
|
103
|
+
import { connectorRuntimeState, runningConnectors } from './runtime-state'
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
advanceConnectorReconnectState,
|
|
107
|
+
clearReconnectState,
|
|
108
|
+
createConnectorReconnectState,
|
|
109
|
+
extractEmbeddedMedia,
|
|
110
|
+
formatInboundUserText,
|
|
111
|
+
formatMediaLine,
|
|
112
|
+
getAllReconnectStates,
|
|
113
|
+
getConnectorReplySendOptions,
|
|
114
|
+
getReconnectState,
|
|
115
|
+
isNoMessage,
|
|
116
|
+
recordConnectorOutboundDelivery,
|
|
117
|
+
selectOutboundMediaFiles,
|
|
118
|
+
setReconnectState,
|
|
119
|
+
}
|
|
120
|
+
export type { ConnectorReconnectState }
|
|
65
121
|
|
|
66
122
|
let streamAgentChatImpl = streamAgentChat
|
|
67
123
|
|
|
@@ -71,363 +127,21 @@ export function setStreamAgentChatForTest(
|
|
|
71
127
|
streamAgentChatImpl = handler || streamAgentChat
|
|
72
128
|
}
|
|
73
129
|
|
|
74
|
-
function resolveUploadPathFromUrl(rawUrl: string): string | null {
|
|
75
|
-
if (!rawUrl) return null
|
|
76
|
-
const normalized = rawUrl.trim()
|
|
77
|
-
const match = normalized.match(/\/api\/uploads\/([^?#)\s]+)/)
|
|
78
|
-
if (!match) return null
|
|
79
|
-
let decoded: string
|
|
80
|
-
try { decoded = decodeURIComponent(match[1]) } catch { decoded = match[1] }
|
|
81
|
-
const safeName = decoded.replace(/[^a-zA-Z0-9._-]/g, '')
|
|
82
|
-
if (!safeName) return null
|
|
83
|
-
const filePath = path.join(UPLOAD_DIR, safeName)
|
|
84
|
-
return fs.existsSync(filePath) ? filePath : null
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function uploadApiUrlFromPath(filePath: string): string | null {
|
|
88
|
-
const rel = path.relative(UPLOAD_DIR, filePath)
|
|
89
|
-
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return null
|
|
90
|
-
const fileName = path.basename(rel)
|
|
91
|
-
return `/api/uploads/${encodeURIComponent(fileName)}`
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function parseSseDataEvents(raw: string): Array<Record<string, unknown>> {
|
|
95
|
-
if (!raw) return []
|
|
96
|
-
const events: Array<Record<string, unknown>> = []
|
|
97
|
-
const lines = raw.split('\n')
|
|
98
|
-
for (const line of lines) {
|
|
99
|
-
if (!line.startsWith('data: ')) continue
|
|
100
|
-
try {
|
|
101
|
-
const parsed = JSON.parse(line.slice(6).trim())
|
|
102
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
103
|
-
events.push(parsed as Record<string, unknown>)
|
|
104
|
-
}
|
|
105
|
-
} catch { /* ignore malformed event lines */ }
|
|
106
|
-
}
|
|
107
|
-
return events
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function parseConnectorToolResult(toolOutput: string): { status?: string; to?: string; followUpId?: string; messageId?: string } | null {
|
|
111
|
-
const raw = toolOutput.trim()
|
|
112
|
-
if (!raw) return null
|
|
113
|
-
try {
|
|
114
|
-
const parsed = JSON.parse(raw)
|
|
115
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null
|
|
116
|
-
const record = parsed as Record<string, unknown>
|
|
117
|
-
const status = typeof record.status === 'string' ? String(record.status) : undefined
|
|
118
|
-
const to = typeof record.to === 'string' ? String(record.to) : undefined
|
|
119
|
-
const followUpId = typeof record.followUpId === 'string' ? String(record.followUpId) : undefined
|
|
120
|
-
const messageId = typeof record.messageId === 'string' ? String(record.messageId) : undefined
|
|
121
|
-
return { status, to, followUpId, messageId }
|
|
122
|
-
} catch {
|
|
123
|
-
return null
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function parseConnectorToolInput(toolInput: string): Record<string, unknown> | null {
|
|
128
|
-
const raw = toolInput.trim()
|
|
129
|
-
if (!raw) return null
|
|
130
|
-
try {
|
|
131
|
-
const parsed = JSON.parse(raw)
|
|
132
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
133
|
-
? parsed as Record<string, unknown>
|
|
134
|
-
: null
|
|
135
|
-
} catch {
|
|
136
|
-
return null
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function visibleConnectorToolText(input: Record<string, unknown> | null): string {
|
|
141
|
-
if (!input) return ''
|
|
142
|
-
const voiceText = typeof input.voiceText === 'string' ? input.voiceText.trim() : ''
|
|
143
|
-
if (voiceText) return voiceText
|
|
144
|
-
const message = typeof input.message === 'string' ? input.message.trim() : ''
|
|
145
|
-
if (message) return message
|
|
146
|
-
const caption = typeof input.caption === 'string' ? input.caption.trim() : ''
|
|
147
|
-
if (caption) return caption
|
|
148
|
-
const text = typeof input.text === 'string' ? input.text.trim() : ''
|
|
149
|
-
if (text) return text
|
|
150
|
-
return ''
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function canonicalUploadMediaKey(filePath: string): string {
|
|
154
|
-
const base = path.basename(filePath)
|
|
155
|
-
const ext = path.extname(base).toLowerCase()
|
|
156
|
-
const normalized = base
|
|
157
|
-
.replace(/^\d{10,16}-/, '')
|
|
158
|
-
.replace(/^(?:browser|screenshot)-\d{10,16}(?:-\d+)?\./, `playwright-capture.`)
|
|
159
|
-
.toLowerCase()
|
|
160
|
-
return normalized || `unknown${ext}`
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function shouldAllowMultipleMediaSends(userText: string): boolean {
|
|
164
|
-
const text = (userText || '').toLowerCase()
|
|
165
|
-
return /\b(all|both|multiple|several|many|every|each|two|three|4|four|screenshots|images|photos|files|documents)\b/.test(text)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function preferSingleBestMediaFile(files: Array<{ path: string; alt: string }>): Array<{ path: string; alt: string }> {
|
|
169
|
-
if (files.length <= 1) return files
|
|
170
|
-
const ranked = [...files].sort((a, b) => {
|
|
171
|
-
const score = (entry: { path: string }) => {
|
|
172
|
-
const base = path.basename(entry.path).toLowerCase()
|
|
173
|
-
let value = 0
|
|
174
|
-
if (/^\d{10,16}-/.test(base)) value += 20
|
|
175
|
-
if (!base.startsWith('browser-') && !base.startsWith('screenshot-')) value += 10
|
|
176
|
-
if (base.endsWith('.pdf')) value += 8
|
|
177
|
-
if (base.endsWith('.png') || base.endsWith('.jpg') || base.endsWith('.jpeg') || base.endsWith('.webp')) value += 6
|
|
178
|
-
try {
|
|
179
|
-
const stat = fs.statSync(entry.path)
|
|
180
|
-
value += Math.min(5, Math.round((stat.mtimeMs % 10_000) / 2_000))
|
|
181
|
-
} catch { /* ignore stat errors */ }
|
|
182
|
-
return value
|
|
183
|
-
}
|
|
184
|
-
return score(b) - score(a)
|
|
185
|
-
})
|
|
186
|
-
return [ranked[0]]
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function selectOutboundMediaFiles(
|
|
190
|
-
files: Array<{ path: string; alt: string }>,
|
|
191
|
-
userText: string,
|
|
192
|
-
): Array<{ path: string; alt: string }> {
|
|
193
|
-
if (files.length === 0) return []
|
|
194
|
-
const mergedFiles: Array<{ path: string; alt: string }> = []
|
|
195
|
-
const seenMediaKeys = new Set<string>()
|
|
196
|
-
for (const candidate of files) {
|
|
197
|
-
const mediaKey = canonicalUploadMediaKey(candidate.path)
|
|
198
|
-
if (seenMediaKeys.has(mediaKey)) continue
|
|
199
|
-
seenMediaKeys.add(mediaKey)
|
|
200
|
-
mergedFiles.push(candidate)
|
|
201
|
-
}
|
|
202
|
-
return shouldAllowMultipleMediaSends(userText || '')
|
|
203
|
-
? mergedFiles
|
|
204
|
-
: preferSingleBestMediaFile(mergedFiles)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Extract embedded media references from agent response text.
|
|
209
|
-
* Supports markdown images/links and bare upload URLs.
|
|
210
|
-
*/
|
|
211
|
-
export function extractEmbeddedMedia(text: string): { cleanText: string; files: Array<{ path: string; alt: string }> } {
|
|
212
|
-
const files: Array<{ path: string; alt: string }> = []
|
|
213
|
-
const seen = new Set<string>()
|
|
214
|
-
let cleanText = text
|
|
215
|
-
|
|
216
|
-
const pushFile = (filePath: string, alt: string) => {
|
|
217
|
-
if (!filePath || seen.has(filePath)) return
|
|
218
|
-
seen.add(filePath)
|
|
219
|
-
files.push({ path: filePath, alt: alt.trim() })
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g
|
|
223
|
-
cleanText = cleanText.replace(imageRegex, (full, altRaw, urlRaw) => {
|
|
224
|
-
const filePath = resolveUploadPathFromUrl(String(urlRaw || ''))
|
|
225
|
-
if (!filePath) return full
|
|
226
|
-
pushFile(filePath, String(altRaw || ''))
|
|
227
|
-
return ''
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
const linkRegex = /(?<!!)\[([^\]]*)\]\(([^)]+)\)/g
|
|
231
|
-
cleanText = cleanText.replace(linkRegex, (full, altRaw, urlRaw) => {
|
|
232
|
-
const filePath = resolveUploadPathFromUrl(String(urlRaw || ''))
|
|
233
|
-
if (!filePath) return full
|
|
234
|
-
pushFile(filePath, String(altRaw || ''))
|
|
235
|
-
return ''
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
const bareUploadUrlRegex = /(?:https?:\/\/[^\s)]+)?\/api\/uploads\/[^\s)\]]+/g
|
|
239
|
-
cleanText = cleanText.replace(bareUploadUrlRegex, (full) => {
|
|
240
|
-
const filePath = resolveUploadPathFromUrl(full)
|
|
241
|
-
if (!filePath) return full
|
|
242
|
-
pushFile(filePath, '')
|
|
243
|
-
return ''
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
if (files.length === 0) return { cleanText: text, files }
|
|
247
|
-
cleanText = cleanText.replace(/\n{3,}/g, '\n\n').trim()
|
|
248
|
-
return { cleanText, files }
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function buildInboundAttachmentPaths(msg: InboundMessage): string[] {
|
|
252
|
-
if (!Array.isArray(msg.media) || msg.media.length === 0) return []
|
|
253
|
-
const paths: string[] = []
|
|
254
|
-
const seen = new Set<string>()
|
|
255
|
-
for (const media of msg.media) {
|
|
256
|
-
const localPath = typeof media.localPath === 'string' ? media.localPath.trim() : ''
|
|
257
|
-
if (!localPath || seen.has(localPath)) continue
|
|
258
|
-
if (!fs.existsSync(localPath)) continue
|
|
259
|
-
seen.add(localPath)
|
|
260
|
-
paths.push(localPath)
|
|
261
|
-
}
|
|
262
|
-
return paths
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function normalizeWhatsappTarget(raw: string): string {
|
|
266
|
-
const trimmed = raw.trim()
|
|
267
|
-
if (!trimmed) return trimmed
|
|
268
|
-
if (trimmed.includes('@')) return trimmed
|
|
269
|
-
let cleaned = trimmed.replace(/[^\d+]/g, '')
|
|
270
|
-
if (cleaned.startsWith('+')) cleaned = cleaned.slice(1)
|
|
271
|
-
if (cleaned.startsWith('0') && cleaned.length >= 10) {
|
|
272
|
-
cleaned = `44${cleaned.slice(1)}`
|
|
273
|
-
}
|
|
274
|
-
cleaned = cleaned.replace(/[^\d]/g, '')
|
|
275
|
-
return cleaned ? `${cleaned}@s.whatsapp.net` : trimmed
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function connectorSupportsBinaryMedia(platform: string): boolean {
|
|
279
|
-
return platform === 'whatsapp'
|
|
280
|
-
|| platform === 'telegram'
|
|
281
|
-
|| platform === 'slack'
|
|
282
|
-
|| platform === 'discord'
|
|
283
|
-
|| platform === 'openclaw'
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** Sentinel value agents return when no outbound reply should be sent */
|
|
287
|
-
export const NO_MESSAGE_SENTINEL = 'NO_MESSAGE'
|
|
288
|
-
|
|
289
|
-
/** Check if an agent response is the NO_MESSAGE sentinel (case-insensitive, trimmed) */
|
|
290
|
-
export function isNoMessage(text: string): boolean {
|
|
291
|
-
return text.trim().toUpperCase() === NO_MESSAGE_SENTINEL
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/** Map of running connector instances by connector ID.
|
|
295
|
-
* Stored on globalThis to survive HMR reloads in dev mode —
|
|
296
|
-
* prevents duplicate sockets fighting for the same WhatsApp session. */
|
|
297
|
-
const globalKey = '__swarmclaw_running_connectors__' as const
|
|
298
|
-
const g = globalThis as typeof globalThis & Record<string, unknown>
|
|
299
|
-
|
|
300
|
-
function getOrInitGlobalValue<T>(key: string, factory: () => T): T {
|
|
301
|
-
const existing = g[key]
|
|
302
|
-
if (existing !== undefined) return existing as T
|
|
303
|
-
const created = factory()
|
|
304
|
-
g[key] = created
|
|
305
|
-
return created
|
|
306
|
-
}
|
|
307
|
-
|
|
308
130
|
type ConnectorSession = Session
|
|
309
131
|
type ConnectorAgent = Agent
|
|
310
132
|
|
|
311
|
-
const running
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
/** Per-connector lock to prevent concurrent start/stop operations */
|
|
325
|
-
const lockKey = '__swarmclaw_connector_locks__' as const
|
|
326
|
-
const locks: Map<string, Promise<void>> =
|
|
327
|
-
getOrInitGlobalValue(lockKey, () => new Map<string, Promise<void>>())
|
|
328
|
-
|
|
329
|
-
/** Generation counter per connector — used to detect stale lifecycle events after restart */
|
|
330
|
-
const genCounterKey = '__swarmclaw_connector_gen__' as const
|
|
331
|
-
const generationCounter: Map<string, number> =
|
|
332
|
-
getOrInitGlobalValue(genCounterKey, () => new Map<string, number>())
|
|
333
|
-
|
|
334
|
-
type ScheduledConnectorFollowup = {
|
|
335
|
-
id: string
|
|
336
|
-
connectorId?: string
|
|
337
|
-
platform?: string
|
|
338
|
-
channelId: string
|
|
339
|
-
sendAt: number
|
|
340
|
-
timer: ReturnType<typeof setTimeout>
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const followupKey = '__swarmclaw_connector_followups__' as const
|
|
344
|
-
const scheduledFollowups: Map<string, ScheduledConnectorFollowup> =
|
|
345
|
-
getOrInitGlobalValue(followupKey, () => new Map<string, ScheduledConnectorFollowup>())
|
|
346
|
-
|
|
347
|
-
const inboundDedupeKey = '__swarmclaw_connector_inbound_dedupe__' as const
|
|
348
|
-
const recentInboundByKey: Map<string, number> =
|
|
349
|
-
getOrInitGlobalValue(inboundDedupeKey, () => new Map<string, number>())
|
|
350
|
-
|
|
351
|
-
type DebouncedInboundEntry = {
|
|
352
|
-
connector: Connector
|
|
353
|
-
messages: InboundMessage[]
|
|
354
|
-
timer: ReturnType<typeof setTimeout>
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const inboundDebounceKey = '__swarmclaw_connector_inbound_debounce__' as const
|
|
358
|
-
const pendingInboundDebounce: Map<string, DebouncedInboundEntry> =
|
|
359
|
-
getOrInitGlobalValue(inboundDebounceKey, () => new Map<string, DebouncedInboundEntry>())
|
|
360
|
-
|
|
361
|
-
const followupDedupeKey = '__swarmclaw_connector_followup_dedupe__' as const
|
|
362
|
-
const scheduledFollowupByDedupe: Map<string, { id: string; sendAt: number }> =
|
|
363
|
-
getOrInitGlobalValue(followupDedupeKey, () => new Map<string, { id: string; sendAt: number }>())
|
|
364
|
-
|
|
365
|
-
/** Reconnect state per connector — tracks backoff and retry attempts for crash recovery */
|
|
366
|
-
export interface ConnectorReconnectState {
|
|
367
|
-
attempts: number
|
|
368
|
-
lastAttemptAt: number
|
|
369
|
-
nextRetryAt: number
|
|
370
|
-
backoffMs: number
|
|
371
|
-
error: string
|
|
372
|
-
exhausted: boolean
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const reconnectStateKey = '__swarmclaw_connector_reconnect_state__' as const
|
|
376
|
-
const reconnectState: Map<string, ConnectorReconnectState> =
|
|
377
|
-
getOrInitGlobalValue(reconnectStateKey, () => new Map<string, ConnectorReconnectState>())
|
|
378
|
-
|
|
379
|
-
const RECONNECT_INITIAL_BACKOFF_MS = 1_000
|
|
380
|
-
const RECONNECT_MAX_BACKOFF_MS = 5 * 60 * 1_000
|
|
381
|
-
const RECONNECT_MAX_ATTEMPTS = 10
|
|
382
|
-
|
|
383
|
-
interface ConnectorReconnectPolicy {
|
|
384
|
-
initialBackoffMs?: number
|
|
385
|
-
maxBackoffMs?: number
|
|
386
|
-
maxAttempts?: number
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export function createConnectorReconnectState(
|
|
390
|
-
init: Partial<ConnectorReconnectState> = {},
|
|
391
|
-
policy: ConnectorReconnectPolicy = {},
|
|
392
|
-
): ConnectorReconnectState {
|
|
393
|
-
return {
|
|
394
|
-
attempts: init.attempts ?? 0,
|
|
395
|
-
lastAttemptAt: init.lastAttemptAt ?? 0,
|
|
396
|
-
nextRetryAt: init.nextRetryAt ?? 0,
|
|
397
|
-
backoffMs: init.backoffMs ?? policy.initialBackoffMs ?? RECONNECT_INITIAL_BACKOFF_MS,
|
|
398
|
-
error: init.error ?? '',
|
|
399
|
-
exhausted: init.exhausted ?? false,
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
export function advanceConnectorReconnectState(
|
|
404
|
-
previous: ConnectorReconnectState,
|
|
405
|
-
error: string,
|
|
406
|
-
now = Date.now(),
|
|
407
|
-
policy: ConnectorReconnectPolicy = {},
|
|
408
|
-
): ConnectorReconnectState {
|
|
409
|
-
const initialBackoffMs = policy.initialBackoffMs ?? RECONNECT_INITIAL_BACKOFF_MS
|
|
410
|
-
const maxBackoffMs = policy.maxBackoffMs ?? RECONNECT_MAX_BACKOFF_MS
|
|
411
|
-
const maxAttempts = policy.maxAttempts ?? RECONNECT_MAX_ATTEMPTS
|
|
412
|
-
const attempts = previous.attempts + 1
|
|
413
|
-
const backoffMs = Math.min(maxBackoffMs, initialBackoffMs * (2 ** Math.max(0, attempts - 1)))
|
|
414
|
-
return {
|
|
415
|
-
attempts,
|
|
416
|
-
lastAttemptAt: now,
|
|
417
|
-
nextRetryAt: now + backoffMs,
|
|
418
|
-
backoffMs,
|
|
419
|
-
error,
|
|
420
|
-
exhausted: attempts >= maxAttempts,
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
export function clearReconnectState(connectorId: string): void {
|
|
425
|
-
reconnectState.delete(connectorId)
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
export function setReconnectState(connectorId: string, state: ConnectorReconnectState): void {
|
|
429
|
-
reconnectState.set(connectorId, state)
|
|
430
|
-
}
|
|
133
|
+
const running = runningConnectors
|
|
134
|
+
const {
|
|
135
|
+
lastInboundChannelByConnector,
|
|
136
|
+
lastInboundTimeByConnector,
|
|
137
|
+
locks,
|
|
138
|
+
generationCounter,
|
|
139
|
+
scheduledFollowups,
|
|
140
|
+
recentInboundByKey,
|
|
141
|
+
pendingInboundDebounce,
|
|
142
|
+
scheduledFollowupByDedupe,
|
|
143
|
+
routeMessageHandlerRef,
|
|
144
|
+
} = connectorRuntimeState
|
|
431
145
|
|
|
432
146
|
/** Record a health event for a connector (persisted to connector_health collection) */
|
|
433
147
|
function recordHealthEvent(connectorId: string, event: ConnectorHealthEventType, message?: string): void {
|
|
@@ -441,17 +155,6 @@ function recordHealthEvent(connectorId: string, event: ConnectorHealthEventType,
|
|
|
441
155
|
})
|
|
442
156
|
}
|
|
443
157
|
|
|
444
|
-
function statusReactionForPlatform(platform: string, state: 'processing' | 'sent' | 'silent'): string {
|
|
445
|
-
if (platform === 'slack') {
|
|
446
|
-
if (state === 'processing') return 'eyes'
|
|
447
|
-
if (state === 'sent') return 'white_check_mark'
|
|
448
|
-
return 'zipper_mouth_face'
|
|
449
|
-
}
|
|
450
|
-
if (state === 'processing') return '👀'
|
|
451
|
-
if (state === 'sent') return '✅'
|
|
452
|
-
return '🤐'
|
|
453
|
-
}
|
|
454
|
-
|
|
455
158
|
function pruneTransientConnectorState(now = Date.now()): void {
|
|
456
159
|
for (const [key, seenAt] of recentInboundByKey.entries()) {
|
|
457
160
|
if (now - seenAt > 120_000) recentInboundByKey.delete(key)
|
|
@@ -470,41 +173,7 @@ function rememberRecentInbound(key: string, now = Date.now(), ttlMs = 120_000):
|
|
|
470
173
|
}
|
|
471
174
|
|
|
472
175
|
function findDirectSessionForInbound(connector: Connector, msg: InboundMessage): ConnectorSession | null {
|
|
473
|
-
|
|
474
|
-
const effectiveAgentId = msg.agentIdOverride || connector.agentId
|
|
475
|
-
const channelIds = new Set([msg.channelId, msg.channelIdAlt].filter(Boolean))
|
|
476
|
-
const senderIds = new Set([msg.senderId, msg.senderIdAlt].filter(Boolean))
|
|
477
|
-
const sessions = Object.values(loadSessions() as Record<string, ConnectorSession>)
|
|
478
|
-
const candidates = sessions.filter((session) =>
|
|
479
|
-
session?.agentId === effectiveAgentId
|
|
480
|
-
&& session?.connectorContext?.connectorId === connector.id
|
|
481
|
-
&& channelIds.has(session?.connectorContext?.channelId || ''),
|
|
482
|
-
)
|
|
483
|
-
if (msg.threadId) {
|
|
484
|
-
const threadExact = candidates.find((session) => session?.connectorContext?.threadId === msg.threadId)
|
|
485
|
-
if (threadExact) return threadExact
|
|
486
|
-
}
|
|
487
|
-
const senderExact = candidates.find((session) => senderIds.has(session?.connectorContext?.senderId || ''))
|
|
488
|
-
if (senderExact) return senderExact
|
|
489
|
-
return candidates[0] || null
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
async function maybeSendStatusReaction(
|
|
493
|
-
connector: Connector,
|
|
494
|
-
msg: InboundMessage,
|
|
495
|
-
state: 'processing' | 'sent' | 'silent',
|
|
496
|
-
): Promise<void> {
|
|
497
|
-
if (!msg.messageId) return
|
|
498
|
-
const session = findDirectSessionForInbound(connector, msg)
|
|
499
|
-
const policy = resolveConnectorSessionPolicy(connector, msg, session)
|
|
500
|
-
if (!policy.statusReactions) return
|
|
501
|
-
const instance = running.get(connector.id)
|
|
502
|
-
if (!instance?.sendReaction) return
|
|
503
|
-
try {
|
|
504
|
-
await instance.sendReaction(msg.channelId, msg.messageId, statusReactionForPlatform(connector.platform, state))
|
|
505
|
-
} catch {
|
|
506
|
-
// Ignore reaction failures — connectors vary widely here.
|
|
507
|
-
}
|
|
176
|
+
return findDirectSessionForInboundHelper(connector, msg)
|
|
508
177
|
}
|
|
509
178
|
|
|
510
179
|
function startConnectorTypingLoop(connector: Connector, msg: InboundMessage): (() => void) | null {
|
|
@@ -527,11 +196,6 @@ function startConnectorTypingLoop(connector: Connector, msg: InboundMessage): ((
|
|
|
527
196
|
return () => clearInterval(timer)
|
|
528
197
|
}
|
|
529
198
|
|
|
530
|
-
type RouteMessageHandler = (connector: Connector, msg: InboundMessage) => Promise<string>
|
|
531
|
-
const routeHandlerKey = '__swarmclaw_connector_route_handler__' as const
|
|
532
|
-
const routeMessageHandlerRef: { current: RouteMessageHandler } =
|
|
533
|
-
getOrInitGlobalValue(routeHandlerKey, () => ({ current: async () => '[Error] Connector router unavailable.' }))
|
|
534
|
-
|
|
535
199
|
async function flushDebouncedInbound(key: string): Promise<void> {
|
|
536
200
|
const entry = pendingInboundDebounce.get(key)
|
|
537
201
|
if (!entry) return
|
|
@@ -573,14 +237,14 @@ async function routeOrDebounceInbound(connector: Connector, msg: InboundMessage)
|
|
|
573
237
|
clearTimeout(pending.timer)
|
|
574
238
|
pending.timer = setTimeout(() => {
|
|
575
239
|
void flushDebouncedInbound(debounceKey).catch((err: unknown) => {
|
|
576
|
-
console.warn(`[connector] Debounced inbound flush failed: ${
|
|
240
|
+
console.warn(`[connector] Debounced inbound flush failed: ${errorMessage(err)}`)
|
|
577
241
|
})
|
|
578
242
|
}, policy.inboundDebounceMs)
|
|
579
243
|
pending.timer.unref?.()
|
|
580
244
|
} else {
|
|
581
245
|
const timer = setTimeout(() => {
|
|
582
246
|
void flushDebouncedInbound(debounceKey).catch((err: unknown) => {
|
|
583
|
-
console.warn(`[connector] Debounced inbound flush failed: ${
|
|
247
|
+
console.warn(`[connector] Debounced inbound flush failed: ${errorMessage(err)}`)
|
|
584
248
|
})
|
|
585
249
|
}, policy.inboundDebounceMs)
|
|
586
250
|
timer.unref?.()
|
|
@@ -651,39 +315,12 @@ export async function getPlatform(platform: string) {
|
|
|
651
315
|
}
|
|
652
316
|
}
|
|
653
317
|
} catch (err: unknown) {
|
|
654
|
-
console.warn(`[connector] Failed to check plugins for platform "${platform}":`,
|
|
318
|
+
console.warn(`[connector] Failed to check plugins for platform "${platform}":`, errorMessage(err))
|
|
655
319
|
}
|
|
656
320
|
|
|
657
321
|
throw new Error(`Unknown platform: ${platform}`)
|
|
658
322
|
}
|
|
659
323
|
|
|
660
|
-
export function formatMediaLine(media: InboundMedia): string {
|
|
661
|
-
const typeLabel = media.type.toUpperCase()
|
|
662
|
-
const name = media.fileName || media.mimeType || 'attachment'
|
|
663
|
-
const size = media.sizeBytes ? ` (${Math.max(1, Math.round(media.sizeBytes / 1024))} KB)` : ''
|
|
664
|
-
if (media.url) return `- ${typeLabel}: ${name}${size} -> ${media.url}`
|
|
665
|
-
return `- ${typeLabel}: ${name}${size}`
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
export function formatInboundUserText(msg: InboundMessage): string {
|
|
669
|
-
const baseText = (msg.text || '').trim()
|
|
670
|
-
const lines: string[] = []
|
|
671
|
-
if (baseText) lines.push(`[${msg.senderName}] ${baseText}`)
|
|
672
|
-
else lines.push(`[${msg.senderName}]`)
|
|
673
|
-
|
|
674
|
-
if (Array.isArray(msg.media) && msg.media.length > 0) {
|
|
675
|
-
lines.push('')
|
|
676
|
-
lines.push('Media received:')
|
|
677
|
-
const preview = msg.media.slice(0, 6)
|
|
678
|
-
for (const media of preview) lines.push(formatMediaLine(media))
|
|
679
|
-
if (msg.media.length > preview.length) {
|
|
680
|
-
lines.push(`- ...and ${msg.media.length - preview.length} more attachment(s)`)
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
return lines.join('\n').trim()
|
|
685
|
-
}
|
|
686
|
-
|
|
687
324
|
type ConnectorCommandName =
|
|
688
325
|
| 'help'
|
|
689
326
|
| 'status'
|
|
@@ -1076,12 +713,7 @@ function pushSessionMessage(
|
|
|
1076
713
|
text: string,
|
|
1077
714
|
extra: Record<string, unknown> = {},
|
|
1078
715
|
): void {
|
|
1079
|
-
|
|
1080
|
-
if (!Array.isArray(session.messages)) session.messages = []
|
|
1081
|
-
const message = { role, text: text.trim(), time: Date.now(), ...extra }
|
|
1082
|
-
session.messages.push(message)
|
|
1083
|
-
session.lastActiveAt = Date.now()
|
|
1084
|
-
mirrorConnectorMessageToAgentThread(session, message)
|
|
716
|
+
pushSessionMessageHelper(session, role, text, extra)
|
|
1085
717
|
}
|
|
1086
718
|
|
|
1087
719
|
function modelHistoryTail(
|
|
@@ -1102,7 +734,7 @@ function persistSession(session: ConnectorSession): void {
|
|
|
1102
734
|
}
|
|
1103
735
|
|
|
1104
736
|
function isRecoverableConnectorSendError(err: unknown): boolean {
|
|
1105
|
-
const message =
|
|
737
|
+
const message = errorMessage(err)
|
|
1106
738
|
return /connection closed|not connected|socket closed|connection terminated|stream errored|connector .* is not running/i.test(message)
|
|
1107
739
|
}
|
|
1108
740
|
|
|
@@ -1132,25 +764,7 @@ function resolvePairingAccess(connector: Connector, msg: InboundMessage): {
|
|
|
1132
764
|
isAllowed: boolean
|
|
1133
765
|
hasAnyApprover: boolean
|
|
1134
766
|
} {
|
|
1135
|
-
|
|
1136
|
-
const configAllowFrom = parseAllowFromCsv(connector.config?.allowFrom)
|
|
1137
|
-
const stored = listStoredAllowedSenders(connector.id)
|
|
1138
|
-
const isAllowed = [
|
|
1139
|
-
msg.senderId,
|
|
1140
|
-
msg.senderIdAlt,
|
|
1141
|
-
]
|
|
1142
|
-
.filter((senderId): senderId is string => typeof senderId === 'string' && !!senderId.trim())
|
|
1143
|
-
.some((senderId) => isSenderAllowed({
|
|
1144
|
-
connectorId: connector.id,
|
|
1145
|
-
senderId,
|
|
1146
|
-
configAllowFrom,
|
|
1147
|
-
}))
|
|
1148
|
-
return {
|
|
1149
|
-
policy,
|
|
1150
|
-
configAllowFrom,
|
|
1151
|
-
isAllowed,
|
|
1152
|
-
hasAnyApprover: (configAllowFrom.length + stored.length) > 0,
|
|
1153
|
-
}
|
|
767
|
+
return resolvePairingAccessHelper(connector, msg)
|
|
1154
768
|
}
|
|
1155
769
|
|
|
1156
770
|
async function handlePairCommand(params: {
|
|
@@ -1229,16 +843,11 @@ async function handlePairCommand(params: {
|
|
|
1229
843
|
}
|
|
1230
844
|
|
|
1231
845
|
function resolveInboundApprovalSenderId(msg: InboundMessage): string {
|
|
1232
|
-
|
|
1233
|
-
if (alt) return alt
|
|
1234
|
-
return typeof msg.senderId === 'string' ? msg.senderId.trim() : ''
|
|
846
|
+
return resolveInboundApprovalSenderIdHelper(msg)
|
|
1235
847
|
}
|
|
1236
848
|
|
|
1237
849
|
function buildInboundApprovalSubject(msg: InboundMessage): string {
|
|
1238
|
-
|
|
1239
|
-
const senderId = resolveInboundApprovalSenderId(msg)
|
|
1240
|
-
if (senderName && senderId && senderName !== senderId) return `${senderName} (${senderId})`
|
|
1241
|
-
return senderName || senderId || 'this sender'
|
|
850
|
+
return buildInboundApprovalSubjectHelper(msg)
|
|
1242
851
|
}
|
|
1243
852
|
|
|
1244
853
|
async function enforceInboundAccessPolicy(params: {
|
|
@@ -1247,60 +856,10 @@ async function enforceInboundAccessPolicy(params: {
|
|
|
1247
856
|
session: ConnectorSession
|
|
1248
857
|
agent: ConnectorAgent
|
|
1249
858
|
}): Promise<string | null> {
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
if (policy === 'open') return null
|
|
1254
|
-
|
|
1255
|
-
if (policy === 'disabled') return NO_MESSAGE_SENTINEL
|
|
1256
|
-
if (isAllowed) return null
|
|
1257
|
-
|
|
1258
|
-
const senderId = resolveInboundApprovalSenderId(msg)
|
|
1259
|
-
const senderSubject = buildInboundApprovalSubject(msg)
|
|
1260
|
-
const approval = await requestApprovalMaybeAutoApprove({
|
|
1261
|
-
category: 'connector_sender',
|
|
1262
|
-
title: `Approve ${senderSubject} on ${connector.name}`,
|
|
1263
|
-
description: `Allow ${senderSubject} to message ${agent.name} via ${connector.platform}/${connector.name}.`,
|
|
1264
|
-
data: {
|
|
1265
|
-
connectorId: connector.id,
|
|
1266
|
-
connectorName: connector.name,
|
|
1267
|
-
platform: connector.platform,
|
|
1268
|
-
senderId,
|
|
1269
|
-
senderIdRaw: typeof msg.senderId === 'string' ? msg.senderId.trim() : '',
|
|
1270
|
-
senderName: typeof msg.senderName === 'string' ? msg.senderName.trim() : '',
|
|
1271
|
-
channelId: typeof msg.channelId === 'string' ? msg.channelId.trim() : '',
|
|
1272
|
-
policy,
|
|
1273
|
-
},
|
|
1274
|
-
agentId: agent.id,
|
|
1275
|
-
sessionId: session.id,
|
|
859
|
+
return enforceInboundAccessPolicyHelper({
|
|
860
|
+
...params,
|
|
861
|
+
noMessageSentinel: NO_MESSAGE_SENTINEL,
|
|
1276
862
|
})
|
|
1277
|
-
|
|
1278
|
-
if (approval.status === 'approved') return null
|
|
1279
|
-
|
|
1280
|
-
if (policy === 'allowlist') {
|
|
1281
|
-
return [
|
|
1282
|
-
`${senderSubject} is pending approval for this connector.`,
|
|
1283
|
-
'A SwarmClaw approval request has been created for this sender.',
|
|
1284
|
-
'An approved operator can allow this sender in the app or via /pair allow <senderId>.',
|
|
1285
|
-
].join('\n')
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
if (policy === 'pairing') {
|
|
1289
|
-
const request = createOrTouchPairingRequest({
|
|
1290
|
-
connectorId: connector.id,
|
|
1291
|
-
senderId,
|
|
1292
|
-
senderName: msg.senderName,
|
|
1293
|
-
channelId: msg.channelId,
|
|
1294
|
-
})
|
|
1295
|
-
return [
|
|
1296
|
-
`${senderSubject} is pending approval for this connector.`,
|
|
1297
|
-
'A SwarmClaw approval request has been created for this sender.',
|
|
1298
|
-
`Pairing code: ${request.code}`,
|
|
1299
|
-
'Approve in the app, or ask an approved sender to run /pair approve <code>.',
|
|
1300
|
-
].join('\n')
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
return 'This sender is not authorized for this connector.'
|
|
1304
863
|
}
|
|
1305
864
|
|
|
1306
865
|
async function handleConnectorCommand(params: {
|
|
@@ -1484,7 +1043,7 @@ async function handleConnectorCommand(params: {
|
|
|
1484
1043
|
persistSession(session)
|
|
1485
1044
|
return text
|
|
1486
1045
|
} catch (err: unknown) {
|
|
1487
|
-
return
|
|
1046
|
+
return errorMessage(err)
|
|
1488
1047
|
}
|
|
1489
1048
|
}
|
|
1490
1049
|
return 'Usage: /session, /session show, /session set <key> <value>, /session reset'
|
|
@@ -1695,7 +1254,7 @@ async function routeMessageToChatroom(connector: Connector, msg: InboundMessage)
|
|
|
1695
1254
|
markProviderSuccess(agent.provider)
|
|
1696
1255
|
}
|
|
1697
1256
|
} catch (err: unknown) {
|
|
1698
|
-
const errMsg =
|
|
1257
|
+
const errMsg = errorMessage(err)
|
|
1699
1258
|
markProviderFailure(agent.provider, errMsg)
|
|
1700
1259
|
console.error(`[connector] Chatroom agent ${agent.name} error:`, errMsg)
|
|
1701
1260
|
}
|
|
@@ -1727,7 +1286,7 @@ async function routeMessageToChatroom(connector: Connector, msg: InboundMessage)
|
|
|
1727
1286
|
})
|
|
1728
1287
|
console.log(`[connector] Sent chatroom media to ${msg.platform}: ${path.basename(file.path)}`)
|
|
1729
1288
|
} catch (err: unknown) {
|
|
1730
|
-
console.error(`[connector] Failed to send chatroom media ${path.basename(file.path)}:`,
|
|
1289
|
+
console.error(`[connector] Failed to send chatroom media ${path.basename(file.path)}:`, errorMessage(err))
|
|
1731
1290
|
}
|
|
1732
1291
|
}
|
|
1733
1292
|
}
|
|
@@ -1761,7 +1320,7 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
1761
1320
|
preferredCredentialId: agent.credentialId || null,
|
|
1762
1321
|
})
|
|
1763
1322
|
|
|
1764
|
-
const { session, sessionKey, wasCreated, staleReason, clearedMessages } =
|
|
1323
|
+
const { session, sessionKey, wasCreated, staleReason, clearedMessages } = resolveDirectSessionHelper({
|
|
1765
1324
|
connector,
|
|
1766
1325
|
msg,
|
|
1767
1326
|
agent,
|
|
@@ -1885,14 +1444,19 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
1885
1444
|
const stopTyping = startConnectorTypingLoop(connector, msg)
|
|
1886
1445
|
try {
|
|
1887
1446
|
// Enqueue system event + heartbeat wake for the agent only after access/gating checks pass.
|
|
1447
|
+
const threadSession = agent.threadSessionId
|
|
1448
|
+
? (loadSessions()[agent.threadSessionId] as ConnectorSession | undefined) || ensureAgentThreadSession(agent.id)
|
|
1449
|
+
: ensureAgentThreadSession(agent.id)
|
|
1450
|
+
const wakeSessionId = threadSession?.id || session.id
|
|
1888
1451
|
const preview = (msg.text || '').slice(0, 80)
|
|
1889
1452
|
enqueueSystemEvent(
|
|
1890
|
-
|
|
1453
|
+
wakeSessionId,
|
|
1891
1454
|
`Inbound message from ${msg.platform}: ${preview}`,
|
|
1892
1455
|
'connector-message',
|
|
1893
1456
|
)
|
|
1894
1457
|
requestHeartbeatNow({
|
|
1895
1458
|
agentId: effectiveAgentId,
|
|
1459
|
+
sessionId: wakeSessionId,
|
|
1896
1460
|
eventId: `${connector.id}:${msg.messageId || msg.replyToMessageId || Date.now()}`,
|
|
1897
1461
|
reason: 'connector-message',
|
|
1898
1462
|
source: `connector:${msg.platform}`,
|
|
@@ -2086,7 +1650,7 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
2086
1650
|
mediaExtractionText = [result.fullText || '', ...toolMediaOutputs].filter(Boolean).join('\n\n')
|
|
2087
1651
|
console.log(`[connector] streamAgentChat returned ${result.fullText.length} chars total, ${fullText.length} chars final`)
|
|
2088
1652
|
} catch (err: unknown) {
|
|
2089
|
-
const message =
|
|
1653
|
+
const message = errorMessage(err)
|
|
2090
1654
|
console.error(`[connector] streamAgentChat error:`, message)
|
|
2091
1655
|
return `[Error] ${message}`
|
|
2092
1656
|
}
|
|
@@ -2234,7 +1798,7 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
2234
1798
|
},
|
|
2235
1799
|
})
|
|
2236
1800
|
} catch (err: unknown) {
|
|
2237
|
-
console.error(`[connector] Failed to send media ${path.basename(file.path)}:`,
|
|
1801
|
+
console.error(`[connector] Failed to send media ${path.basename(file.path)}:`, errorMessage(err))
|
|
2238
1802
|
logExecution(session.id, 'error', 'Connector media send failed', {
|
|
2239
1803
|
agentId: agent.id,
|
|
2240
1804
|
detail: {
|
|
@@ -2242,7 +1806,7 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
2242
1806
|
channelId: msg.channelId,
|
|
2243
1807
|
filePath: file.path,
|
|
2244
1808
|
fileName: path.basename(file.path),
|
|
2245
|
-
error:
|
|
1809
|
+
error: errorMessage(err),
|
|
2246
1810
|
},
|
|
2247
1811
|
})
|
|
2248
1812
|
}
|
|
@@ -2278,7 +1842,7 @@ export async function startConnector(connectorId: string): Promise<void> {
|
|
|
2278
1842
|
// Wait for any pending operation on this connector to finish (with timeout)
|
|
2279
1843
|
const pending = locks.get(connectorId)
|
|
2280
1844
|
if (pending) {
|
|
2281
|
-
await Promise.race([pending,
|
|
1845
|
+
await Promise.race([pending, sleep(15_000)]).catch(() => {})
|
|
2282
1846
|
locks.delete(connectorId)
|
|
2283
1847
|
}
|
|
2284
1848
|
|
|
@@ -2367,7 +1931,7 @@ async function _startConnectorImpl(connectorId: string): Promise<void> {
|
|
|
2367
1931
|
console.log(`[connector] Started ${connector.platform} connector: ${connector.name}`)
|
|
2368
1932
|
recordHealthEvent(connectorId, 'started', `${connector.platform} connector "${connector.name}" started`)
|
|
2369
1933
|
} catch (err: unknown) {
|
|
2370
|
-
const errMsg =
|
|
1934
|
+
const errMsg = errorMessage(err)
|
|
2371
1935
|
connector.status = 'error'
|
|
2372
1936
|
connector.isEnabled = true
|
|
2373
1937
|
connector.lastError = errMsg
|
|
@@ -2381,7 +1945,11 @@ async function _startConnectorImpl(connectorId: string): Promise<void> {
|
|
|
2381
1945
|
}
|
|
2382
1946
|
|
|
2383
1947
|
/** Stop a connector */
|
|
2384
|
-
export async function stopConnector(
|
|
1948
|
+
export async function stopConnector(
|
|
1949
|
+
connectorId: string,
|
|
1950
|
+
options?: { disable?: boolean },
|
|
1951
|
+
): Promise<void> {
|
|
1952
|
+
const disable = options?.disable !== false
|
|
2385
1953
|
const instance = running.get(connectorId)
|
|
2386
1954
|
if (instance) {
|
|
2387
1955
|
await instance.stop()
|
|
@@ -2410,7 +1978,7 @@ export async function stopConnector(connectorId: string): Promise<void> {
|
|
|
2410
1978
|
const connector = connectors[connectorId]
|
|
2411
1979
|
if (connector) {
|
|
2412
1980
|
connector.status = 'stopped'
|
|
2413
|
-
connector.isEnabled = false
|
|
1981
|
+
connector.isEnabled = disable ? false : connector.isEnabled === true
|
|
2414
1982
|
connector.lastError = null
|
|
2415
1983
|
connector.updatedAt = Date.now()
|
|
2416
1984
|
connectors[connectorId] = connector
|
|
@@ -2466,9 +2034,9 @@ export async function repairConnector(connectorId: string): Promise<void> {
|
|
|
2466
2034
|
}
|
|
2467
2035
|
|
|
2468
2036
|
/** Stop all running connectors (for cleanup) */
|
|
2469
|
-
export async function stopAllConnectors(): Promise<void> {
|
|
2037
|
+
export async function stopAllConnectors(options?: { disable?: boolean }): Promise<void> {
|
|
2470
2038
|
for (const [id] of running) {
|
|
2471
|
-
await stopConnector(id)
|
|
2039
|
+
await stopConnector(id, options)
|
|
2472
2040
|
}
|
|
2473
2041
|
}
|
|
2474
2042
|
|
|
@@ -2530,7 +2098,7 @@ export function listRunningConnectors(platform?: string): Array<{
|
|
|
2530
2098
|
platform: connector.platform,
|
|
2531
2099
|
agentId: connector.agentId || null,
|
|
2532
2100
|
supportsSend: typeof instance.sendMessage === 'function',
|
|
2533
|
-
configuredTargets:
|
|
2101
|
+
configuredTargets: dedup(configuredTargets),
|
|
2534
2102
|
recentChannelId: lastInboundChannelByConnector.get(id) || null,
|
|
2535
2103
|
})
|
|
2536
2104
|
}
|
|
@@ -2556,69 +2124,6 @@ export function getRunningInstance(connectorId: string): ConnectorInstance | und
|
|
|
2556
2124
|
return running.get(connectorId)
|
|
2557
2125
|
}
|
|
2558
2126
|
|
|
2559
|
-
export function getConnectorReplySendOptions(params: {
|
|
2560
|
-
connectorId: string
|
|
2561
|
-
inbound: InboundMessage
|
|
2562
|
-
}): { replyToMessageId?: string; threadId?: string } {
|
|
2563
|
-
const connectors = loadConnectors()
|
|
2564
|
-
const connector = connectors[params.connectorId] as Connector | undefined
|
|
2565
|
-
if (!connector) return {}
|
|
2566
|
-
const session = findDirectSessionForInbound(connector, params.inbound)
|
|
2567
|
-
const policy = resolveConnectorSessionPolicy(connector, params.inbound, session)
|
|
2568
|
-
return shouldReplyToInboundMessage({
|
|
2569
|
-
msg: params.inbound,
|
|
2570
|
-
session,
|
|
2571
|
-
policy,
|
|
2572
|
-
})
|
|
2573
|
-
}
|
|
2574
|
-
|
|
2575
|
-
export async function recordConnectorOutboundDelivery(params: {
|
|
2576
|
-
connectorId: string
|
|
2577
|
-
inbound: InboundMessage
|
|
2578
|
-
messageId?: string
|
|
2579
|
-
state?: 'sent' | 'silent'
|
|
2580
|
-
}): Promise<void> {
|
|
2581
|
-
const connectors = loadConnectors()
|
|
2582
|
-
const connector = connectors[params.connectorId] as Connector | undefined
|
|
2583
|
-
if (!connector) return
|
|
2584
|
-
const session = findDirectSessionForInbound(connector, params.inbound)
|
|
2585
|
-
if (session) {
|
|
2586
|
-
session.connectorContext = {
|
|
2587
|
-
...(session.connectorContext || {}),
|
|
2588
|
-
lastOutboundAt: Date.now(),
|
|
2589
|
-
lastOutboundMessageId: params.messageId || session.connectorContext?.lastOutboundMessageId || null,
|
|
2590
|
-
threadId: params.inbound.threadId || session.connectorContext?.threadId || null,
|
|
2591
|
-
}
|
|
2592
|
-
const history = Array.isArray(session.messages) ? session.messages : []
|
|
2593
|
-
for (let i = history.length - 1; i >= 0; i -= 1) {
|
|
2594
|
-
const entry = history[i]
|
|
2595
|
-
if (entry?.role !== 'assistant') continue
|
|
2596
|
-
const source: Partial<MessageSource> = entry?.source || {}
|
|
2597
|
-
if (source.connectorId !== connector.id) continue
|
|
2598
|
-
if (source.channelId !== params.inbound.channelId) continue
|
|
2599
|
-
if (!source.messageId && params.messageId) {
|
|
2600
|
-
entry.source = {
|
|
2601
|
-
platform: source.platform || connector.platform,
|
|
2602
|
-
connectorId: source.connectorId || connector.id,
|
|
2603
|
-
connectorName: source.connectorName || connector.name,
|
|
2604
|
-
channelId: source.channelId || params.inbound.channelId,
|
|
2605
|
-
senderId: source.senderId,
|
|
2606
|
-
senderName: source.senderName,
|
|
2607
|
-
messageId: params.messageId,
|
|
2608
|
-
replyToMessageId: source.replyToMessageId || params.inbound.messageId,
|
|
2609
|
-
threadId: source.threadId || params.inbound.threadId,
|
|
2610
|
-
}
|
|
2611
|
-
}
|
|
2612
|
-
break
|
|
2613
|
-
}
|
|
2614
|
-
persistSessionRecord(session)
|
|
2615
|
-
notify(`messages:${session.id}`)
|
|
2616
|
-
}
|
|
2617
|
-
if (params.state) {
|
|
2618
|
-
await maybeSendStatusReaction(connector, params.inbound, params.state)
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
2127
|
export async function performConnectorMessageAction(params: {
|
|
2623
2128
|
connectorId?: string
|
|
2624
2129
|
platform?: string
|
|
@@ -2734,7 +2239,7 @@ export async function sendConnectorMessage(params: {
|
|
|
2734
2239
|
replyToMessageId?: string
|
|
2735
2240
|
threadId?: string
|
|
2736
2241
|
ptt?: boolean
|
|
2737
|
-
}): Promise<{ connectorId: string; platform: string; channelId: string; messageId?: string }> {
|
|
2242
|
+
}): Promise<{ connectorId: string; platform: string; channelId: string; messageId?: string; suppressed?: boolean }> {
|
|
2738
2243
|
const connectors = loadConnectors()
|
|
2739
2244
|
const requestedId = params.connectorId?.trim()
|
|
2740
2245
|
let connector: Connector | undefined
|
|
@@ -2772,7 +2277,7 @@ export async function sendConnectorMessage(params: {
|
|
|
2772
2277
|
// Apply NO_MESSAGE filter at the delivery layer so all outbound paths respect it
|
|
2773
2278
|
if ((suppressHiddenText || isNoMessage(sanitizedText)) && !params.imageUrl && !params.fileUrl && !params.mediaPath) {
|
|
2774
2279
|
console.log(`[connector] sendConnectorMessage: NO_MESSAGE — suppressing outbound send`)
|
|
2775
|
-
return { connectorId, platform: connector.platform, channelId: params.channelId }
|
|
2280
|
+
return { connectorId, platform: connector.platform, channelId: params.channelId, suppressed: true }
|
|
2776
2281
|
}
|
|
2777
2282
|
|
|
2778
2283
|
const hasMedia = !!(params.imageUrl || params.fileUrl || params.mediaPath)
|
|
@@ -2823,7 +2328,7 @@ export async function sendConnectorMessage(params: {
|
|
|
2823
2328
|
result = await sendThroughCurrentInstance()
|
|
2824
2329
|
} catch (err: unknown) {
|
|
2825
2330
|
if (!isRecoverableConnectorSendError(err)) throw err
|
|
2826
|
-
const errMsg =
|
|
2331
|
+
const errMsg = errorMessage(err)
|
|
2827
2332
|
console.warn(`[connector] Outbound send failed for ${connectorId}; attempting automatic restart`, { error: errMsg })
|
|
2828
2333
|
recordHealthEvent(connectorId, 'disconnected', `Outbound send failed: ${errMsg}`)
|
|
2829
2334
|
await startConnector(connectorId)
|
|
@@ -2871,6 +2376,7 @@ export async function sendConnectorMessage(params: {
|
|
|
2871
2376
|
platform: connector.platform,
|
|
2872
2377
|
channelId,
|
|
2873
2378
|
messageId: result?.messageId,
|
|
2379
|
+
suppressed: false,
|
|
2874
2380
|
}
|
|
2875
2381
|
}
|
|
2876
2382
|
|
|
@@ -2901,59 +2407,28 @@ export function scheduleConnectorFollowUp(params: {
|
|
|
2901
2407
|
params.threadId || '',
|
|
2902
2408
|
(params.text || '').trim().slice(0, 160),
|
|
2903
2409
|
].join('|')
|
|
2904
|
-
const
|
|
2905
|
-
if (existing && existing.sendAt > Date.now() && !params.replaceExisting) {
|
|
2906
|
-
return { followUpId: existing.id, sendAt: existing.sendAt }
|
|
2907
|
-
}
|
|
2908
|
-
if (existing && params.replaceExisting) {
|
|
2909
|
-
const scheduled = scheduledFollowups.get(existing.id)
|
|
2910
|
-
if (scheduled) {
|
|
2911
|
-
clearTimeout(scheduled.timer)
|
|
2912
|
-
scheduledFollowups.delete(existing.id)
|
|
2913
|
-
}
|
|
2914
|
-
scheduledFollowupByDedupe.delete(dedupeKey)
|
|
2915
|
-
}
|
|
2916
|
-
const followUpId = genId()
|
|
2917
|
-
const sendAt = Date.now() + delayMs
|
|
2918
|
-
|
|
2919
|
-
const timer = setTimeout(() => {
|
|
2920
|
-
void sendConnectorMessage({
|
|
2921
|
-
connectorId: params.connectorId,
|
|
2922
|
-
platform: params.platform,
|
|
2923
|
-
channelId: params.channelId,
|
|
2924
|
-
text: params.text,
|
|
2925
|
-
sessionId: params.sessionId,
|
|
2926
|
-
imageUrl: params.imageUrl,
|
|
2927
|
-
fileUrl: params.fileUrl,
|
|
2928
|
-
mediaPath: params.mediaPath,
|
|
2929
|
-
mimeType: params.mimeType,
|
|
2930
|
-
fileName: params.fileName,
|
|
2931
|
-
caption: params.caption,
|
|
2932
|
-
replyToMessageId: params.replyToMessageId,
|
|
2933
|
-
threadId: params.threadId,
|
|
2934
|
-
ptt: params.ptt,
|
|
2935
|
-
}).catch((err: unknown) => {
|
|
2936
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
2937
|
-
console.warn(`[connector] Scheduled follow-up ${followUpId} failed: ${msg}`)
|
|
2938
|
-
}).finally(() => {
|
|
2939
|
-
scheduledFollowups.delete(followUpId)
|
|
2940
|
-
if (scheduledFollowupByDedupe.get(dedupeKey)?.id === followUpId) {
|
|
2941
|
-
scheduledFollowupByDedupe.delete(dedupeKey)
|
|
2942
|
-
}
|
|
2943
|
-
})
|
|
2944
|
-
}, delayMs)
|
|
2945
|
-
|
|
2946
|
-
scheduledFollowups.set(followUpId, {
|
|
2947
|
-
id: followUpId,
|
|
2410
|
+
const { outboxId, sendAt } = enqueueConnectorOutbox({
|
|
2948
2411
|
connectorId: params.connectorId,
|
|
2949
2412
|
platform: params.platform,
|
|
2950
2413
|
channelId: params.channelId,
|
|
2951
|
-
|
|
2952
|
-
|
|
2414
|
+
text: params.text,
|
|
2415
|
+
sessionId: params.sessionId,
|
|
2416
|
+
imageUrl: params.imageUrl,
|
|
2417
|
+
fileUrl: params.fileUrl,
|
|
2418
|
+
mediaPath: params.mediaPath,
|
|
2419
|
+
mimeType: params.mimeType,
|
|
2420
|
+
fileName: params.fileName,
|
|
2421
|
+
caption: params.caption,
|
|
2422
|
+
replyToMessageId: params.replyToMessageId,
|
|
2423
|
+
threadId: params.threadId,
|
|
2424
|
+
ptt: params.ptt,
|
|
2425
|
+
sendAt: Date.now() + delayMs,
|
|
2426
|
+
dedupeKey,
|
|
2427
|
+
}, {
|
|
2428
|
+
replaceExisting: params.replaceExisting,
|
|
2953
2429
|
})
|
|
2954
|
-
scheduledFollowupByDedupe.set(dedupeKey, { id: followUpId, sendAt })
|
|
2955
2430
|
|
|
2956
|
-
return { followUpId, sendAt }
|
|
2431
|
+
return { followUpId: outboxId, sendAt }
|
|
2957
2432
|
}
|
|
2958
2433
|
|
|
2959
2434
|
/**
|
|
@@ -2971,7 +2446,7 @@ export async function checkConnectorHealth(): Promise<void> {
|
|
|
2971
2446
|
|
|
2972
2447
|
if (instance.isAlive()) {
|
|
2973
2448
|
// Connector is healthy — clear any reconnect state
|
|
2974
|
-
if (
|
|
2449
|
+
if (connectorReconnectStateStore.has(id)) {
|
|
2975
2450
|
console.log(`[connector-health] Connector "${instance.connector.name}" recovered`)
|
|
2976
2451
|
clearReconnectState(id)
|
|
2977
2452
|
}
|
|
@@ -3000,7 +2475,7 @@ export async function checkConnectorHealth(): Promise<void> {
|
|
|
3000
2475
|
connector.updatedAt = Date.now()
|
|
3001
2476
|
connectors[id] = connector
|
|
3002
2477
|
connectorsDirty = true
|
|
3003
|
-
if (!
|
|
2478
|
+
if (!connectorReconnectStateStore.has(id)) {
|
|
3004
2479
|
setReconnectState(id, createConnectorReconnectState({
|
|
3005
2480
|
error: connector.lastError || 'Connection lost',
|
|
3006
2481
|
}))
|
|
@@ -3013,21 +2488,7 @@ export async function checkConnectorHealth(): Promise<void> {
|
|
|
3013
2488
|
}
|
|
3014
2489
|
|
|
3015
2490
|
// Purge reconnect state for connectors that no longer exist
|
|
3016
|
-
for (const id of
|
|
2491
|
+
for (const id of connectorReconnectStateStore.keys()) {
|
|
3017
2492
|
if (!connectors[id] || connectors[id]?.isEnabled !== true || running.has(id)) clearReconnectState(id)
|
|
3018
2493
|
}
|
|
3019
2494
|
}
|
|
3020
|
-
|
|
3021
|
-
/** Get the reconnect state for a specific connector (null if not in reconnect cycle) */
|
|
3022
|
-
export function getReconnectState(connectorId: string): ConnectorReconnectState | null {
|
|
3023
|
-
return reconnectState.get(connectorId) ?? null
|
|
3024
|
-
}
|
|
3025
|
-
|
|
3026
|
-
/** Get all reconnect states (for dashboard/API) */
|
|
3027
|
-
export function getAllReconnectStates(): Record<string, ConnectorReconnectState> {
|
|
3028
|
-
const result: Record<string, ConnectorReconnectState> = {}
|
|
3029
|
-
for (const [id, state] of reconnectState.entries()) {
|
|
3030
|
-
result[id] = { ...state }
|
|
3031
|
-
}
|
|
3032
|
-
return result
|
|
3033
|
-
}
|