@swarmclawai/swarmclaw 0.7.7 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -30,6 +30,7 @@ import { evaluateRoutingRules } from '../chatroom-routing'
|
|
|
30
30
|
import { markProviderFailure, markProviderSuccess } from '../provider-health'
|
|
31
31
|
import { syncSessionArchiveMemory } from '../session-archive-memory'
|
|
32
32
|
import { buildIdentityContinuityContext } from '../identity-continuity'
|
|
33
|
+
import { ensureAgentThreadSession } from '../agent-thread-session'
|
|
33
34
|
import { getProvider } from '@/lib/providers'
|
|
34
35
|
import type { Agent, Connector, MessageSource, Chatroom, ChatroomMessage, Session } from '@/types'
|
|
35
36
|
import type { ConnectorInstance, InboundMessage, InboundMedia } from './types'
|
|
@@ -59,6 +60,16 @@ import {
|
|
|
59
60
|
textMentionsAlias,
|
|
60
61
|
} from './policy'
|
|
61
62
|
import { buildConnectorThreadContextBlock, resolveThreadPersonaLabel } from './thread-context'
|
|
63
|
+
import { shouldSuppressHiddenControlText, stripHiddenControlTokens } from '../assistant-control'
|
|
64
|
+
import { requestApprovalMaybeAutoApprove } from '../approvals'
|
|
65
|
+
|
|
66
|
+
let streamAgentChatImpl = streamAgentChat
|
|
67
|
+
|
|
68
|
+
export function setStreamAgentChatForTest(
|
|
69
|
+
handler: typeof streamAgentChat | null,
|
|
70
|
+
): void {
|
|
71
|
+
streamAgentChatImpl = handler || streamAgentChat
|
|
72
|
+
}
|
|
62
73
|
|
|
63
74
|
function resolveUploadPathFromUrl(rawUrl: string): string | null {
|
|
64
75
|
if (!rawUrl) return null
|
|
@@ -113,6 +124,32 @@ function parseConnectorToolResult(toolOutput: string): { status?: string; to?: s
|
|
|
113
124
|
}
|
|
114
125
|
}
|
|
115
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
|
+
|
|
116
153
|
function canonicalUploadMediaKey(filePath: string): string {
|
|
117
154
|
const base = path.basename(filePath)
|
|
118
155
|
const ext = path.extname(base).toLowerCase()
|
|
@@ -332,6 +369,7 @@ export interface ConnectorReconnectState {
|
|
|
332
369
|
nextRetryAt: number
|
|
333
370
|
backoffMs: number
|
|
334
371
|
error: string
|
|
372
|
+
exhausted: boolean
|
|
335
373
|
}
|
|
336
374
|
|
|
337
375
|
const reconnectStateKey = '__swarmclaw_connector_reconnect_state__' as const
|
|
@@ -342,6 +380,55 @@ const RECONNECT_INITIAL_BACKOFF_MS = 1_000
|
|
|
342
380
|
const RECONNECT_MAX_BACKOFF_MS = 5 * 60 * 1_000
|
|
343
381
|
const RECONNECT_MAX_ATTEMPTS = 10
|
|
344
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
|
+
}
|
|
431
|
+
|
|
345
432
|
/** Record a health event for a connector (persisted to connector_health collection) */
|
|
346
433
|
function recordHealthEvent(connectorId: string, event: ConnectorHealthEventType, message?: string): void {
|
|
347
434
|
const id = genId()
|
|
@@ -385,17 +472,19 @@ function rememberRecentInbound(key: string, now = Date.now(), ttlMs = 120_000):
|
|
|
385
472
|
function findDirectSessionForInbound(connector: Connector, msg: InboundMessage): ConnectorSession | null {
|
|
386
473
|
if (connector.chatroomId) return null
|
|
387
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))
|
|
388
477
|
const sessions = Object.values(loadSessions() as Record<string, ConnectorSession>)
|
|
389
478
|
const candidates = sessions.filter((session) =>
|
|
390
479
|
session?.agentId === effectiveAgentId
|
|
391
480
|
&& session?.connectorContext?.connectorId === connector.id
|
|
392
|
-
&& session?.connectorContext?.channelId
|
|
481
|
+
&& channelIds.has(session?.connectorContext?.channelId || ''),
|
|
393
482
|
)
|
|
394
483
|
if (msg.threadId) {
|
|
395
484
|
const threadExact = candidates.find((session) => session?.connectorContext?.threadId === msg.threadId)
|
|
396
485
|
if (threadExact) return threadExact
|
|
397
486
|
}
|
|
398
|
-
const senderExact = candidates.find((session) => session?.connectorContext?.senderId
|
|
487
|
+
const senderExact = candidates.find((session) => senderIds.has(session?.connectorContext?.senderId || ''))
|
|
399
488
|
if (senderExact) return senderExact
|
|
400
489
|
return candidates[0] || null
|
|
401
490
|
}
|
|
@@ -637,8 +726,10 @@ function parseConnectorCommand(text: string): ParsedConnectorCommand | null {
|
|
|
637
726
|
|
|
638
727
|
function persistSessionRecord(session: ConnectorSession): void {
|
|
639
728
|
const sessions = loadSessions()
|
|
729
|
+
session.updatedAt = Date.now()
|
|
640
730
|
sessions[session.id] = session
|
|
641
731
|
saveSessions(sessions)
|
|
732
|
+
notify('sessions')
|
|
642
733
|
}
|
|
643
734
|
|
|
644
735
|
function updateSessionConnectorContext(session: ConnectorSession, connector: Connector, msg: InboundMessage, sessionKey: string): void {
|
|
@@ -860,6 +951,9 @@ function resolveDirectSession(params: {
|
|
|
860
951
|
})
|
|
861
952
|
const sessions = loadSessions()
|
|
862
953
|
let session = Object.values(sessions as Record<string, ConnectorSession>).find((item) => item?.name === sessionKey)
|
|
954
|
+
if (!session) {
|
|
955
|
+
session = findDirectSessionForInbound(connector, msg) || undefined
|
|
956
|
+
}
|
|
863
957
|
let wasCreated = false
|
|
864
958
|
if (!session) {
|
|
865
959
|
const id = genId()
|
|
@@ -930,20 +1024,93 @@ function resolveDirectSession(params: {
|
|
|
930
1024
|
}
|
|
931
1025
|
}
|
|
932
1026
|
|
|
933
|
-
function
|
|
1027
|
+
function mirrorConnectorMessageToAgentThread(
|
|
1028
|
+
session: ConnectorSession,
|
|
1029
|
+
message: Record<string, unknown>,
|
|
1030
|
+
): void {
|
|
1031
|
+
if (!session.agentId) return
|
|
1032
|
+
if (typeof session.name !== 'string' || !session.name.startsWith('connector:')) return
|
|
1033
|
+
|
|
1034
|
+
const agents = loadAgents()
|
|
1035
|
+
const agent = agents[session.agentId]
|
|
1036
|
+
const threadSession = agent?.threadSessionId
|
|
1037
|
+
? loadSessions()[agent.threadSessionId]
|
|
1038
|
+
: ensureAgentThreadSession(session.agentId)
|
|
1039
|
+
if (!threadSession || threadSession.id === session.id) return
|
|
1040
|
+
|
|
1041
|
+
const last = Array.isArray(threadSession.messages) ? threadSession.messages[threadSession.messages.length - 1] : null
|
|
1042
|
+
const source = message.source as MessageSource | undefined
|
|
1043
|
+
const lastSource = (last?.source || null) as MessageSource | null
|
|
1044
|
+
if (
|
|
1045
|
+
last
|
|
1046
|
+
&& last.role === message.role
|
|
1047
|
+
&& last.text === message.text
|
|
1048
|
+
&& lastSource?.platform === source?.platform
|
|
1049
|
+
&& lastSource?.connectorId === source?.connectorId
|
|
1050
|
+
&& lastSource?.channelId === source?.channelId
|
|
1051
|
+
&& lastSource?.messageId === source?.messageId
|
|
1052
|
+
) {
|
|
1053
|
+
return
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (!Array.isArray(threadSession.messages)) threadSession.messages = []
|
|
1057
|
+
threadSession.messages.push({
|
|
1058
|
+
...message,
|
|
1059
|
+
time: typeof message.time === 'number' ? message.time : Date.now(),
|
|
1060
|
+
historyExcluded: true,
|
|
1061
|
+
} as Session['messages'][number])
|
|
1062
|
+
threadSession.lastActiveAt = Date.now()
|
|
1063
|
+
|
|
1064
|
+
const sessions = loadSessions()
|
|
1065
|
+
sessions[threadSession.id] = threadSession
|
|
1066
|
+
saveSessions(sessions)
|
|
1067
|
+
notify('sessions')
|
|
1068
|
+
notify(`messages:${threadSession.id}`)
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function pushSessionMessage(
|
|
1072
|
+
session: ConnectorSession,
|
|
1073
|
+
role: 'user' | 'assistant',
|
|
1074
|
+
text: string,
|
|
1075
|
+
extra: Record<string, unknown> = {},
|
|
1076
|
+
): void {
|
|
934
1077
|
if (!text.trim()) return
|
|
935
1078
|
if (!Array.isArray(session.messages)) session.messages = []
|
|
936
|
-
|
|
1079
|
+
const message = { role, text: text.trim(), time: Date.now(), ...extra }
|
|
1080
|
+
session.messages.push(message)
|
|
937
1081
|
session.lastActiveAt = Date.now()
|
|
1082
|
+
mirrorConnectorMessageToAgentThread(session, message)
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function modelHistoryTail(
|
|
1086
|
+
messages: Session['messages'] | null | undefined,
|
|
1087
|
+
limit = 20,
|
|
1088
|
+
) : Session['messages'] {
|
|
1089
|
+
const filtered = (Array.isArray(messages) ? messages : []).filter((message) => message?.historyExcluded !== true)
|
|
1090
|
+
return filtered.slice(-limit)
|
|
938
1091
|
}
|
|
939
1092
|
|
|
940
1093
|
function persistSession(session: ConnectorSession): void {
|
|
941
1094
|
const sessions = loadSessions()
|
|
1095
|
+
session.updatedAt = Date.now()
|
|
942
1096
|
sessions[session.id] = session
|
|
943
1097
|
saveSessions(sessions)
|
|
1098
|
+
notify('sessions')
|
|
944
1099
|
notify(`messages:${session.id}`)
|
|
945
1100
|
}
|
|
946
1101
|
|
|
1102
|
+
function isRecoverableConnectorSendError(err: unknown): boolean {
|
|
1103
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
1104
|
+
return /connection closed|not connected|socket closed|connection terminated|stream errored|connector .* is not running/i.test(message)
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function connectorEmptyReplyFallback(streamErrorText: string): string {
|
|
1108
|
+
if (/abort|timed?\s*out|network|socket|connection/i.test(streamErrorText)) {
|
|
1109
|
+
return 'Sorry, I hit a temporary issue while responding. Please try again.'
|
|
1110
|
+
}
|
|
1111
|
+
return 'Sorry, I could not produce a reply just now. Please try again.'
|
|
1112
|
+
}
|
|
1113
|
+
|
|
947
1114
|
function summarizeForCompaction(messages: Array<{ role?: string; text?: string }>): string {
|
|
948
1115
|
const preview = messages
|
|
949
1116
|
.slice(-8)
|
|
@@ -966,11 +1133,16 @@ function resolvePairingAccess(connector: Connector, msg: InboundMessage): {
|
|
|
966
1133
|
const policy = parsePairingPolicy(connector.config?.dmPolicy, 'open')
|
|
967
1134
|
const configAllowFrom = parseAllowFromCsv(connector.config?.allowFrom)
|
|
968
1135
|
const stored = listStoredAllowedSenders(connector.id)
|
|
969
|
-
const isAllowed =
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1136
|
+
const isAllowed = [
|
|
1137
|
+
msg.senderId,
|
|
1138
|
+
msg.senderIdAlt,
|
|
1139
|
+
]
|
|
1140
|
+
.filter((senderId): senderId is string => typeof senderId === 'string' && !!senderId.trim())
|
|
1141
|
+
.some((senderId) => isSenderAllowed({
|
|
1142
|
+
connectorId: connector.id,
|
|
1143
|
+
senderId,
|
|
1144
|
+
configAllowFrom,
|
|
1145
|
+
}))
|
|
974
1146
|
return {
|
|
975
1147
|
policy,
|
|
976
1148
|
configAllowFrom,
|
|
@@ -1054,38 +1226,79 @@ async function handlePairCommand(params: {
|
|
|
1054
1226
|
].join('\n')
|
|
1055
1227
|
}
|
|
1056
1228
|
|
|
1057
|
-
function
|
|
1229
|
+
function resolveInboundApprovalSenderId(msg: InboundMessage): string {
|
|
1230
|
+
const alt = typeof msg.senderIdAlt === 'string' ? msg.senderIdAlt.trim() : ''
|
|
1231
|
+
if (alt) return alt
|
|
1232
|
+
return typeof msg.senderId === 'string' ? msg.senderId.trim() : ''
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function buildInboundApprovalSubject(msg: InboundMessage): string {
|
|
1236
|
+
const senderName = typeof msg.senderName === 'string' ? msg.senderName.trim() : ''
|
|
1237
|
+
const senderId = resolveInboundApprovalSenderId(msg)
|
|
1238
|
+
if (senderName && senderId && senderName !== senderId) return `${senderName} (${senderId})`
|
|
1239
|
+
return senderName || senderId || 'this sender'
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
async function enforceInboundAccessPolicy(params: {
|
|
1243
|
+
connector: Connector
|
|
1244
|
+
msg: InboundMessage
|
|
1245
|
+
session: ConnectorSession
|
|
1246
|
+
agent: ConnectorAgent
|
|
1247
|
+
}): Promise<string | null> {
|
|
1248
|
+
const { connector, msg, session, agent } = params
|
|
1058
1249
|
if (msg.isGroup) return null
|
|
1059
|
-
const { policy,
|
|
1060
|
-
const storedAllowFrom = listStoredAllowedSenders(connector.id)
|
|
1250
|
+
const { policy, isAllowed } = resolvePairingAccess(connector, msg)
|
|
1061
1251
|
if (policy === 'open') return null
|
|
1062
1252
|
|
|
1063
1253
|
if (policy === 'disabled') return NO_MESSAGE_SENTINEL
|
|
1064
1254
|
if (isAllowed) return null
|
|
1065
1255
|
|
|
1256
|
+
const senderId = resolveInboundApprovalSenderId(msg)
|
|
1257
|
+
const senderSubject = buildInboundApprovalSubject(msg)
|
|
1258
|
+
const approval = await requestApprovalMaybeAutoApprove({
|
|
1259
|
+
category: 'connector_sender',
|
|
1260
|
+
title: `Approve ${senderSubject} on ${connector.name}`,
|
|
1261
|
+
description: `Allow ${senderSubject} to message ${agent.name} via ${connector.platform}/${connector.name}.`,
|
|
1262
|
+
data: {
|
|
1263
|
+
connectorId: connector.id,
|
|
1264
|
+
connectorName: connector.name,
|
|
1265
|
+
platform: connector.platform,
|
|
1266
|
+
senderId,
|
|
1267
|
+
senderIdRaw: typeof msg.senderId === 'string' ? msg.senderId.trim() : '',
|
|
1268
|
+
senderName: typeof msg.senderName === 'string' ? msg.senderName.trim() : '',
|
|
1269
|
+
channelId: typeof msg.channelId === 'string' ? msg.channelId.trim() : '',
|
|
1270
|
+
policy,
|
|
1271
|
+
},
|
|
1272
|
+
agentId: agent.id,
|
|
1273
|
+
sessionId: session.id,
|
|
1274
|
+
})
|
|
1275
|
+
|
|
1276
|
+
if (approval.status === 'approved') return null
|
|
1277
|
+
|
|
1066
1278
|
if (policy === 'allowlist') {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1279
|
+
return [
|
|
1280
|
+
`${senderSubject} is pending approval for this connector.`,
|
|
1281
|
+
'A SwarmClaw approval request has been created for this sender.',
|
|
1282
|
+
'An approved operator can allow this sender in the app or via /pair allow <senderId>.',
|
|
1283
|
+
].join('\n')
|
|
1071
1284
|
}
|
|
1072
1285
|
|
|
1073
1286
|
if (policy === 'pairing') {
|
|
1074
1287
|
const request = createOrTouchPairingRequest({
|
|
1075
1288
|
connectorId: connector.id,
|
|
1076
|
-
senderId
|
|
1289
|
+
senderId,
|
|
1077
1290
|
senderName: msg.senderName,
|
|
1078
1291
|
channelId: msg.channelId,
|
|
1079
1292
|
})
|
|
1080
1293
|
return [
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
'
|
|
1294
|
+
`${senderSubject} is pending approval for this connector.`,
|
|
1295
|
+
'A SwarmClaw approval request has been created for this sender.',
|
|
1296
|
+
`Pairing code: ${request.code}`,
|
|
1297
|
+
'Approve in the app, or ask an approved sender to run /pair approve <code>.',
|
|
1085
1298
|
].join('\n')
|
|
1086
1299
|
}
|
|
1087
1300
|
|
|
1088
|
-
return
|
|
1301
|
+
return 'This sender is not authorized for this connector.'
|
|
1089
1302
|
}
|
|
1090
1303
|
|
|
1091
1304
|
async function handleConnectorCommand(params: {
|
|
@@ -1443,7 +1656,7 @@ async function routeMessageToChatroom(connector: Connector, msg: InboundMessage)
|
|
|
1443
1656
|
history,
|
|
1444
1657
|
})
|
|
1445
1658
|
|
|
1446
|
-
const responseText = result.finalResponse || result.fullText
|
|
1659
|
+
const responseText = stripHiddenControlTokens(result.finalResponse || result.fullText)
|
|
1447
1660
|
if (responseText.trim() && !isNoMessage(responseText)) {
|
|
1448
1661
|
// Persist agent response to chatroom
|
|
1449
1662
|
const agentSource: MessageSource = {
|
|
@@ -1551,6 +1764,19 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
1551
1764
|
msg,
|
|
1552
1765
|
agent,
|
|
1553
1766
|
})
|
|
1767
|
+
const rawText = (msg.text || '').trim()
|
|
1768
|
+
const inboundText = formatInboundUserText(msg)
|
|
1769
|
+
const messageSource: MessageSource = {
|
|
1770
|
+
platform: connector.platform,
|
|
1771
|
+
connectorId: connector.id,
|
|
1772
|
+
connectorName: connector.name,
|
|
1773
|
+
channelId: msg.channelId,
|
|
1774
|
+
senderId: msg.senderId,
|
|
1775
|
+
senderName: msg.senderName,
|
|
1776
|
+
messageId: msg.messageId,
|
|
1777
|
+
replyToMessageId: msg.replyToMessageId,
|
|
1778
|
+
threadId: msg.threadId,
|
|
1779
|
+
}
|
|
1554
1780
|
|
|
1555
1781
|
const parsedCommand = parseConnectorCommand(msg.text || '')
|
|
1556
1782
|
if (parsedCommand?.name === 'pair') {
|
|
@@ -1571,8 +1797,36 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
1571
1797
|
return commandResult
|
|
1572
1798
|
}
|
|
1573
1799
|
|
|
1574
|
-
const accessPolicyResult = enforceInboundAccessPolicy(
|
|
1800
|
+
const accessPolicyResult = await enforceInboundAccessPolicy({
|
|
1801
|
+
connector,
|
|
1802
|
+
msg,
|
|
1803
|
+
session,
|
|
1804
|
+
agent,
|
|
1805
|
+
})
|
|
1575
1806
|
if (accessPolicyResult) {
|
|
1807
|
+
if (accessPolicyResult !== NO_MESSAGE_SENTINEL) {
|
|
1808
|
+
const assistantSource: MessageSource = {
|
|
1809
|
+
platform: connector.platform,
|
|
1810
|
+
connectorId: connector.id,
|
|
1811
|
+
connectorName: connector.name,
|
|
1812
|
+
channelId: msg.channelId,
|
|
1813
|
+
senderId: msg.senderId,
|
|
1814
|
+
senderName: msg.senderName,
|
|
1815
|
+
replyToMessageId: msg.messageId,
|
|
1816
|
+
threadId: msg.threadId,
|
|
1817
|
+
}
|
|
1818
|
+
pushSessionMessage(session, 'user', rawText || inboundText, {
|
|
1819
|
+
source: messageSource,
|
|
1820
|
+
historyExcluded: true,
|
|
1821
|
+
})
|
|
1822
|
+
pushSessionMessage(session, 'assistant', accessPolicyResult, {
|
|
1823
|
+
source: assistantSource,
|
|
1824
|
+
historyExcluded: true,
|
|
1825
|
+
})
|
|
1826
|
+
updateSessionConnectorContext(session, connector, msg, sessionKey)
|
|
1827
|
+
persistSessionRecord(session)
|
|
1828
|
+
notify(`messages:${session.id}`)
|
|
1829
|
+
}
|
|
1576
1830
|
logExecution(session.id, 'decision', 'Connector inbound blocked by access policy', {
|
|
1577
1831
|
agentId: agent.id,
|
|
1578
1832
|
detail: {
|
|
@@ -1635,7 +1889,18 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
1635
1889
|
`Inbound message from ${msg.platform}: ${preview}`,
|
|
1636
1890
|
'connector-message',
|
|
1637
1891
|
)
|
|
1638
|
-
requestHeartbeatNow({
|
|
1892
|
+
requestHeartbeatNow({
|
|
1893
|
+
agentId: effectiveAgentId,
|
|
1894
|
+
eventId: `${connector.id}:${msg.messageId || msg.replyToMessageId || Date.now()}`,
|
|
1895
|
+
reason: 'connector-message',
|
|
1896
|
+
source: `connector:${msg.platform}`,
|
|
1897
|
+
resumeMessage: `Inbound ${msg.platform} message from ${msg.senderName || msg.senderId || 'unknown sender'}.`,
|
|
1898
|
+
detail: [
|
|
1899
|
+
(msg.text || '').trim() ? `Text: ${(msg.text || '').slice(0, 240)}` : '',
|
|
1900
|
+
msg.imageUrl ? 'Includes image input.' : '',
|
|
1901
|
+
Array.isArray(msg.media) && msg.media.length > 0 ? `Media count: ${msg.media.length}` : '',
|
|
1902
|
+
].filter(Boolean).join(' '),
|
|
1903
|
+
})
|
|
1639
1904
|
|
|
1640
1905
|
logExecution(session.id, 'trigger', `${msg.platform} message from ${msg.senderName}`, {
|
|
1641
1906
|
agentId: agent.id,
|
|
@@ -1736,32 +2001,15 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
1736
2001
|
const firstImageUrl = msg.imageUrl || (firstImage?.url) || undefined
|
|
1737
2002
|
const firstImagePath = firstImage?.localPath || undefined
|
|
1738
2003
|
const inboundAttachmentPaths = buildInboundAttachmentPaths(msg)
|
|
1739
|
-
const inboundText = formatInboundUserText(msg)
|
|
1740
2004
|
const modelInputText = inboundText
|
|
1741
2005
|
// Store the raw user text for display (source.senderName handles attribution).
|
|
1742
2006
|
// The formatted text with [SenderName] prefix is only used for LLM history context.
|
|
1743
|
-
|
|
1744
|
-
const messageSource: MessageSource = {
|
|
1745
|
-
platform: connector.platform,
|
|
1746
|
-
connectorId: connector.id,
|
|
1747
|
-
connectorName: connector.name,
|
|
1748
|
-
channelId: msg.channelId,
|
|
1749
|
-
senderId: msg.senderId,
|
|
1750
|
-
senderName: msg.senderName,
|
|
1751
|
-
messageId: msg.messageId,
|
|
1752
|
-
replyToMessageId: msg.replyToMessageId,
|
|
1753
|
-
threadId: msg.threadId,
|
|
1754
|
-
}
|
|
1755
|
-
session.messages.push({
|
|
1756
|
-
role: 'user',
|
|
1757
|
-
text: rawText || inboundText,
|
|
1758
|
-
time: Date.now(),
|
|
2007
|
+
pushSessionMessage(session, 'user', rawText || inboundText, {
|
|
1759
2008
|
imageUrl: firstImageUrl,
|
|
1760
2009
|
imagePath: firstImagePath,
|
|
1761
2010
|
attachedFiles: inboundAttachmentPaths.length ? inboundAttachmentPaths : undefined,
|
|
1762
2011
|
source: messageSource,
|
|
1763
2012
|
})
|
|
1764
|
-
session.lastActiveAt = Date.now()
|
|
1765
2013
|
updateSessionConnectorContext(session, connector, msg, sessionKey)
|
|
1766
2014
|
persistSessionRecord(session)
|
|
1767
2015
|
notify(`messages:${session.id}`)
|
|
@@ -1771,13 +2019,16 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
1771
2019
|
let mediaExtractionText = ''
|
|
1772
2020
|
let connectorToolDeliveredCurrentChannel = false
|
|
1773
2021
|
let connectorToolDeliveredMessageId: string | undefined
|
|
2022
|
+
let streamErrorText = ''
|
|
2023
|
+
const connectorToolInputsByCallId = new Map<string, Record<string, unknown>>()
|
|
2024
|
+
const connectorToolMirrorTexts: string[] = []
|
|
1774
2025
|
const hasTools = session.plugins?.length && session.provider !== 'claude-cli'
|
|
1775
2026
|
console.log(`[connector] Routing message to agent "${agent.name}" (${session.provider}/${session.model}), hasTools=${!!hasTools}`)
|
|
1776
2027
|
|
|
1777
2028
|
if (hasTools) {
|
|
1778
2029
|
try {
|
|
1779
2030
|
const toolMediaOutputs: string[] = []
|
|
1780
|
-
const result = await
|
|
2031
|
+
const result = await streamAgentChatImpl({
|
|
1781
2032
|
session: session as Session,
|
|
1782
2033
|
message: modelInputText,
|
|
1783
2034
|
imagePath: firstImagePath,
|
|
@@ -1786,11 +2037,27 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
1786
2037
|
systemPrompt,
|
|
1787
2038
|
write: (raw) => {
|
|
1788
2039
|
for (const event of parseSseDataEvents(raw)) {
|
|
2040
|
+
if (event.t === 'err') {
|
|
2041
|
+
const errText = typeof event.text === 'string' ? event.text.trim() : ''
|
|
2042
|
+
if (errText) streamErrorText = errText
|
|
2043
|
+
continue
|
|
2044
|
+
}
|
|
2045
|
+
if (event.t === 'tool_call' && event.toolName === 'connector_message_tool') {
|
|
2046
|
+
const toolCallId = typeof event.toolCallId === 'string' ? event.toolCallId : ''
|
|
2047
|
+
const toolInput = typeof event.toolInput === 'string' ? event.toolInput : ''
|
|
2048
|
+
if (toolCallId && toolInput) {
|
|
2049
|
+
const parsedInput = parseConnectorToolInput(toolInput)
|
|
2050
|
+
if (parsedInput) connectorToolInputsByCallId.set(toolCallId, parsedInput)
|
|
2051
|
+
}
|
|
2052
|
+
continue
|
|
2053
|
+
}
|
|
1789
2054
|
if (event.t !== 'tool_result') continue
|
|
1790
2055
|
const toolOutput = typeof event.toolOutput === 'string' ? event.toolOutput : ''
|
|
1791
2056
|
if (!toolOutput) continue
|
|
1792
2057
|
toolMediaOutputs.push(toolOutput)
|
|
1793
2058
|
if (event.toolName === 'connector_message_tool') {
|
|
2059
|
+
const toolCallId = typeof event.toolCallId === 'string' ? event.toolCallId : ''
|
|
2060
|
+
const mirrorInput = toolCallId ? connectorToolInputsByCallId.get(toolCallId) || null : null
|
|
1794
2061
|
const parsed = parseConnectorToolResult(toolOutput)
|
|
1795
2062
|
if (!parsed?.status || !parsed.to) continue
|
|
1796
2063
|
const sentLikeStatus = parsed.status === 'sent' || parsed.status === 'voice_sent'
|
|
@@ -1804,11 +2071,13 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
1804
2071
|
if (inboundTarget && outboundTarget && inboundTarget === outboundTarget) {
|
|
1805
2072
|
connectorToolDeliveredCurrentChannel = true
|
|
1806
2073
|
if (parsed.messageId) connectorToolDeliveredMessageId = parsed.messageId
|
|
2074
|
+
const mirrorText = visibleConnectorToolText(mirrorInput)
|
|
2075
|
+
if (mirrorText) connectorToolMirrorTexts.push(mirrorText)
|
|
1807
2076
|
}
|
|
1808
2077
|
}
|
|
1809
2078
|
}
|
|
1810
2079
|
},
|
|
1811
|
-
history: session.messages
|
|
2080
|
+
history: modelHistoryTail(session.messages),
|
|
1812
2081
|
})
|
|
1813
2082
|
// Use finalResponse for connectors — strips intermediate planning/tool-use text
|
|
1814
2083
|
fullText = result.finalResponse || result.fullText
|
|
@@ -1841,26 +2110,54 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
1841
2110
|
}
|
|
1842
2111
|
},
|
|
1843
2112
|
active: new Map(),
|
|
1844
|
-
loadHistory: () => session.messages
|
|
2113
|
+
loadHistory: () => modelHistoryTail(session.messages),
|
|
1845
2114
|
})
|
|
1846
2115
|
mediaExtractionText = fullText
|
|
1847
2116
|
}
|
|
1848
2117
|
|
|
2118
|
+
if (!fullText.trim() && !connectorToolDeliveredCurrentChannel) {
|
|
2119
|
+
fullText = connectorEmptyReplyFallback(streamErrorText)
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
const suppressHiddenResponse = shouldSuppressHiddenControlText(fullText)
|
|
2123
|
+
fullText = stripHiddenControlTokens(fullText)
|
|
2124
|
+
|
|
1849
2125
|
// If the agent chose NO_MESSAGE, skip saving it to history — the user's message
|
|
1850
2126
|
// is already recorded, and saving the sentinel would pollute the LLM's context
|
|
1851
|
-
if (isNoMessage(fullText)) {
|
|
2127
|
+
if (suppressHiddenResponse || isNoMessage(fullText)) {
|
|
1852
2128
|
if (connectorToolDeliveredCurrentChannel) {
|
|
2129
|
+
const mirroredToolText = connectorToolMirrorTexts
|
|
2130
|
+
.map((entry) => entry.trim())
|
|
2131
|
+
.filter(Boolean)
|
|
2132
|
+
.join('\n\n')
|
|
2133
|
+
if (mirroredToolText) {
|
|
2134
|
+
const assistantSource: MessageSource = {
|
|
2135
|
+
platform: connector.platform,
|
|
2136
|
+
connectorId: connector.id,
|
|
2137
|
+
connectorName: connector.name,
|
|
2138
|
+
channelId: msg.channelId,
|
|
2139
|
+
senderId: msg.senderId,
|
|
2140
|
+
senderName: msg.senderName,
|
|
2141
|
+
messageId: connectorToolDeliveredMessageId,
|
|
2142
|
+
replyToMessageId: msg.messageId,
|
|
2143
|
+
threadId: msg.threadId,
|
|
2144
|
+
}
|
|
2145
|
+
pushSessionMessage(session, 'assistant', mirroredToolText, {
|
|
2146
|
+
source: assistantSource,
|
|
2147
|
+
})
|
|
2148
|
+
}
|
|
1853
2149
|
session.connectorContext = {
|
|
1854
2150
|
...(session.connectorContext || {}),
|
|
1855
2151
|
lastOutboundAt: Date.now(),
|
|
1856
2152
|
lastOutboundMessageId: connectorToolDeliveredMessageId || session.connectorContext?.lastOutboundMessageId || null,
|
|
1857
2153
|
}
|
|
1858
2154
|
persistSessionRecord(session)
|
|
2155
|
+
notify(`messages:${session.id}`)
|
|
1859
2156
|
await maybeSendStatusReaction(connector, msg, 'sent')
|
|
1860
2157
|
} else {
|
|
1861
2158
|
await maybeSendStatusReaction(connector, msg, 'silent')
|
|
1862
2159
|
}
|
|
1863
|
-
console.log(`[connector] Agent returned
|
|
2160
|
+
console.log(`[connector] Agent returned hidden control sentinel — suppressing outbound reply`)
|
|
1864
2161
|
logExecution(session.id, 'decision', 'Agent suppressed outbound (NO_MESSAGE)', {
|
|
1865
2162
|
agentId: agent.id,
|
|
1866
2163
|
detail: { platform: msg.platform, channelId: msg.channelId },
|
|
@@ -1886,12 +2183,13 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
1886
2183
|
connectorId: connector.id,
|
|
1887
2184
|
connectorName: connector.name,
|
|
1888
2185
|
channelId: msg.channelId,
|
|
2186
|
+
senderId: msg.senderId,
|
|
2187
|
+
senderName: msg.senderName,
|
|
1889
2188
|
replyToMessageId: msg.messageId,
|
|
1890
2189
|
threadId: msg.threadId,
|
|
1891
2190
|
}
|
|
1892
2191
|
if (fullText.trim()) {
|
|
1893
|
-
session
|
|
1894
|
-
session.lastActiveAt = Date.now()
|
|
2192
|
+
pushSessionMessage(session, 'assistant', fullText.trim(), { source: assistantSource })
|
|
1895
2193
|
persistSessionRecord(session)
|
|
1896
2194
|
notify(`messages:${session.id}`)
|
|
1897
2195
|
}
|
|
@@ -1971,6 +2269,8 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
1971
2269
|
|
|
1972
2270
|
routeMessageHandlerRef.current = routeMessage
|
|
1973
2271
|
|
|
2272
|
+
export const routeConnectorMessageForTest = routeMessage
|
|
2273
|
+
|
|
1974
2274
|
/** Start a connector (serialized per ID to prevent concurrent start/stop races) */
|
|
1975
2275
|
export async function startConnector(connectorId: string): Promise<void> {
|
|
1976
2276
|
// Wait for any pending operation on this connector to finish (with timeout)
|
|
@@ -2008,33 +2308,43 @@ async function _startConnectorImpl(connectorId: string): Promise<void> {
|
|
|
2008
2308
|
const connector = connectors[connectorId] as Connector | undefined
|
|
2009
2309
|
if (!connector) throw new Error('Connector not found')
|
|
2010
2310
|
|
|
2011
|
-
//
|
|
2012
|
-
|
|
2013
|
-
if (connector.
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
}
|
|
2020
|
-
// Also check config for inline token (some platforms)
|
|
2021
|
-
if (!botToken && connector.config.botToken) {
|
|
2022
|
-
botToken = connector.config.botToken
|
|
2023
|
-
}
|
|
2024
|
-
if (!botToken && connector.platform === 'bluebubbles' && connector.config.password) {
|
|
2025
|
-
botToken = connector.config.password
|
|
2311
|
+
// Starting a connector expresses durable intent: keep it enabled across
|
|
2312
|
+
// transient failures so daemon recovery and server restarts can retry it.
|
|
2313
|
+
if (connector.isEnabled !== true) {
|
|
2314
|
+
connector.isEnabled = true
|
|
2315
|
+
connector.updatedAt = Date.now()
|
|
2316
|
+
connectors[connectorId] = connector
|
|
2317
|
+
saveConnectors(connectors)
|
|
2318
|
+
notify('connectors')
|
|
2026
2319
|
}
|
|
2027
2320
|
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2321
|
+
try {
|
|
2322
|
+
// Resolve bot token from credential
|
|
2323
|
+
let botToken = ''
|
|
2324
|
+
if (connector.credentialId) {
|
|
2325
|
+
const creds = loadCredentials()
|
|
2326
|
+
const cred = creds[connector.credentialId]
|
|
2327
|
+
if (cred?.encryptedKey) {
|
|
2328
|
+
try { botToken = decryptKey(cred.encryptedKey) } catch { /* ignore */ }
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
// Also check config for inline token (some platforms)
|
|
2332
|
+
if (!botToken && connector.config.botToken) {
|
|
2333
|
+
botToken = connector.config.botToken
|
|
2334
|
+
}
|
|
2335
|
+
if (!botToken && connector.platform === 'bluebubbles' && connector.config.password) {
|
|
2336
|
+
botToken = connector.config.password
|
|
2337
|
+
}
|
|
2031
2338
|
|
|
2032
|
-
|
|
2339
|
+
if (!botToken && connector.platform !== 'whatsapp' && connector.platform !== 'openclaw' && connector.platform !== 'signal' && connector.platform !== 'email') {
|
|
2340
|
+
throw new Error('No bot token configured')
|
|
2341
|
+
}
|
|
2033
2342
|
|
|
2034
|
-
|
|
2035
|
-
|
|
2343
|
+
const platform = await getPlatform(connector.platform)
|
|
2344
|
+
|
|
2345
|
+
// Bump generation counter so stale events from previous instances are ignored
|
|
2346
|
+
generationCounter.set(connectorId, (generationCounter.get(connectorId) ?? 0) + 1)
|
|
2036
2347
|
|
|
2037
|
-
try {
|
|
2038
2348
|
const instance = await platform.start(
|
|
2039
2349
|
connector,
|
|
2040
2350
|
botToken,
|
|
@@ -2049,6 +2359,7 @@ async function _startConnectorImpl(connectorId: string): Promise<void> {
|
|
|
2049
2359
|
connector.updatedAt = Date.now()
|
|
2050
2360
|
connectors[connectorId] = connector
|
|
2051
2361
|
saveConnectors(connectors)
|
|
2362
|
+
clearReconnectState(connectorId)
|
|
2052
2363
|
notify('connectors')
|
|
2053
2364
|
|
|
2054
2365
|
console.log(`[connector] Started ${connector.platform} connector: ${connector.name}`)
|
|
@@ -2056,7 +2367,7 @@ async function _startConnectorImpl(connectorId: string): Promise<void> {
|
|
|
2056
2367
|
} catch (err: unknown) {
|
|
2057
2368
|
const errMsg = err instanceof Error ? err.message : String(err)
|
|
2058
2369
|
connector.status = 'error'
|
|
2059
|
-
connector.isEnabled =
|
|
2370
|
+
connector.isEnabled = true
|
|
2060
2371
|
connector.lastError = errMsg
|
|
2061
2372
|
connector.updatedAt = Date.now()
|
|
2062
2373
|
connectors[connectorId] = connector
|
|
@@ -2074,6 +2385,7 @@ export async function stopConnector(connectorId: string): Promise<void> {
|
|
|
2074
2385
|
await instance.stop()
|
|
2075
2386
|
running.delete(connectorId)
|
|
2076
2387
|
}
|
|
2388
|
+
clearReconnectState(connectorId)
|
|
2077
2389
|
|
|
2078
2390
|
for (const [debounceKey, entry] of pendingInboundDebounce.entries()) {
|
|
2079
2391
|
if (entry.connector.id !== connectorId) continue
|
|
@@ -2141,6 +2453,7 @@ export async function repairConnector(connectorId: string): Promise<void> {
|
|
|
2141
2453
|
await instance.stop()
|
|
2142
2454
|
running.delete(connectorId)
|
|
2143
2455
|
}
|
|
2456
|
+
clearReconnectState(connectorId)
|
|
2144
2457
|
|
|
2145
2458
|
// Clear auth directory
|
|
2146
2459
|
const { clearAuthDir } = await import('./whatsapp')
|
|
@@ -2376,6 +2689,30 @@ export async function performConnectorMessageAction(params: {
|
|
|
2376
2689
|
}
|
|
2377
2690
|
}
|
|
2378
2691
|
|
|
2692
|
+
export function sanitizeConnectorOutboundContent(params: {
|
|
2693
|
+
text?: string
|
|
2694
|
+
caption?: string
|
|
2695
|
+
}): {
|
|
2696
|
+
sanitizedText: string
|
|
2697
|
+
suppressHiddenText: boolean
|
|
2698
|
+
sanitizedCaptionText: string
|
|
2699
|
+
sanitizedCaption?: string
|
|
2700
|
+
} {
|
|
2701
|
+
const sanitizedText = stripHiddenControlTokens(params.text || '')
|
|
2702
|
+
const suppressHiddenText = shouldSuppressHiddenControlText(params.text || '')
|
|
2703
|
+
const sanitizedCaptionText = stripHiddenControlTokens(params.caption || '').trim()
|
|
2704
|
+
const sanitizedCaption = shouldSuppressHiddenControlText(params.caption || '')
|
|
2705
|
+
? undefined
|
|
2706
|
+
: (sanitizedCaptionText || undefined)
|
|
2707
|
+
|
|
2708
|
+
return {
|
|
2709
|
+
sanitizedText,
|
|
2710
|
+
suppressHiddenText,
|
|
2711
|
+
sanitizedCaptionText,
|
|
2712
|
+
sanitizedCaption,
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2379
2716
|
/**
|
|
2380
2717
|
* Send an outbound message through a running connector.
|
|
2381
2718
|
* Intended for proactive agent notifications (e.g. WhatsApp updates).
|
|
@@ -2420,16 +2757,18 @@ export async function sendConnectorMessage(params: {
|
|
|
2420
2757
|
|
|
2421
2758
|
if (!connector || !connectorId) throw new Error('Connector resolution failed.')
|
|
2422
2759
|
|
|
2423
|
-
const
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2760
|
+
const {
|
|
2761
|
+
sanitizedText,
|
|
2762
|
+
suppressHiddenText,
|
|
2763
|
+
sanitizedCaptionText,
|
|
2764
|
+
sanitizedCaption,
|
|
2765
|
+
} = sanitizeConnectorOutboundContent({
|
|
2766
|
+
text: params.text,
|
|
2767
|
+
caption: params.caption,
|
|
2768
|
+
})
|
|
2430
2769
|
|
|
2431
2770
|
// Apply NO_MESSAGE filter at the delivery layer so all outbound paths respect it
|
|
2432
|
-
if (isNoMessage(
|
|
2771
|
+
if ((suppressHiddenText || isNoMessage(sanitizedText)) && !params.imageUrl && !params.fileUrl && !params.mediaPath) {
|
|
2433
2772
|
console.log(`[connector] sendConnectorMessage: NO_MESSAGE — suppressing outbound send`)
|
|
2434
2773
|
return { connectorId, platform: connector.platform, channelId: params.channelId }
|
|
2435
2774
|
}
|
|
@@ -2439,14 +2778,14 @@ export async function sendConnectorMessage(params: {
|
|
|
2439
2778
|
? normalizeWhatsappTarget(params.channelId)
|
|
2440
2779
|
: params.channelId
|
|
2441
2780
|
|
|
2442
|
-
let outboundText =
|
|
2781
|
+
let outboundText = sanitizedText
|
|
2443
2782
|
let outboundOptions: Parameters<NonNullable<ConnectorInstance['sendMessage']>>[2] | undefined = {
|
|
2444
2783
|
imageUrl: params.imageUrl,
|
|
2445
2784
|
fileUrl: params.fileUrl,
|
|
2446
2785
|
mediaPath: params.mediaPath,
|
|
2447
2786
|
mimeType: params.mimeType,
|
|
2448
2787
|
fileName: params.fileName,
|
|
2449
|
-
caption:
|
|
2788
|
+
caption: sanitizedCaption,
|
|
2450
2789
|
replyToMessageId: params.replyToMessageId,
|
|
2451
2790
|
threadId: params.threadId,
|
|
2452
2791
|
ptt: params.ptt,
|
|
@@ -2457,8 +2796,8 @@ export async function sendConnectorMessage(params: {
|
|
|
2457
2796
|
|| params.fileUrl
|
|
2458
2797
|
|| (params.mediaPath ? uploadApiUrlFromPath(params.mediaPath) : null)
|
|
2459
2798
|
const fallbackParts = [
|
|
2460
|
-
|
|
2461
|
-
|
|
2799
|
+
sanitizedText.trim(),
|
|
2800
|
+
sanitizedCaptionText,
|
|
2462
2801
|
mediaLink ? `Attachment: ${mediaLink}` : '',
|
|
2463
2802
|
!mediaLink && params.mediaPath ? `Attachment: ${path.basename(params.mediaPath)}` : '',
|
|
2464
2803
|
].filter(Boolean)
|
|
@@ -2466,7 +2805,29 @@ export async function sendConnectorMessage(params: {
|
|
|
2466
2805
|
outboundOptions = undefined
|
|
2467
2806
|
}
|
|
2468
2807
|
|
|
2469
|
-
const
|
|
2808
|
+
const sendThroughCurrentInstance = async () => {
|
|
2809
|
+
const liveInstance = running.get(connectorId)
|
|
2810
|
+
if (!liveInstance) {
|
|
2811
|
+
throw new Error(`Connector "${connectorId}" is not running.`)
|
|
2812
|
+
}
|
|
2813
|
+
if (typeof liveInstance.sendMessage !== 'function') {
|
|
2814
|
+
throw new Error(`Connector "${connector.name}" (${connector.platform}) does not support outbound sends.`)
|
|
2815
|
+
}
|
|
2816
|
+
return liveInstance.sendMessage(channelId, outboundText, outboundOptions)
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
let result
|
|
2820
|
+
try {
|
|
2821
|
+
result = await sendThroughCurrentInstance()
|
|
2822
|
+
} catch (err: unknown) {
|
|
2823
|
+
if (!isRecoverableConnectorSendError(err)) throw err
|
|
2824
|
+
const errMsg = err instanceof Error ? err.message : String(err)
|
|
2825
|
+
console.warn(`[connector] Outbound send failed for ${connectorId}; attempting automatic restart`, { error: errMsg })
|
|
2826
|
+
recordHealthEvent(connectorId, 'disconnected', `Outbound send failed: ${errMsg}`)
|
|
2827
|
+
await startConnector(connectorId)
|
|
2828
|
+
result = await sendThroughCurrentInstance()
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2470
2831
|
if (params.sessionId) {
|
|
2471
2832
|
const sessions = loadSessions()
|
|
2472
2833
|
const session = sessions[params.sessionId]
|
|
@@ -2499,6 +2860,7 @@ export async function sendConnectorMessage(params: {
|
|
|
2499
2860
|
}
|
|
2500
2861
|
sessions[session.id] = session
|
|
2501
2862
|
saveSessions(sessions)
|
|
2863
|
+
notify('sessions')
|
|
2502
2864
|
notify(`messages:${session.id}`)
|
|
2503
2865
|
}
|
|
2504
2866
|
}
|
|
@@ -2609,7 +2971,7 @@ export async function checkConnectorHealth(): Promise<void> {
|
|
|
2609
2971
|
// Connector is healthy — clear any reconnect state
|
|
2610
2972
|
if (reconnectState.has(id)) {
|
|
2611
2973
|
console.log(`[connector-health] Connector "${instance.connector.name}" recovered`)
|
|
2612
|
-
|
|
2974
|
+
clearReconnectState(id)
|
|
2613
2975
|
}
|
|
2614
2976
|
continue
|
|
2615
2977
|
}
|
|
@@ -2627,68 +2989,30 @@ export async function checkConnectorHealth(): Promise<void> {
|
|
|
2627
2989
|
|
|
2628
2990
|
// If the connector is not enabled, don't attempt reconnect
|
|
2629
2991
|
if (!connector.isEnabled) {
|
|
2630
|
-
|
|
2992
|
+
clearReconnectState(id)
|
|
2631
2993
|
continue
|
|
2632
2994
|
}
|
|
2633
2995
|
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
// Check if we've exceeded max attempts
|
|
2644
|
-
if (state.attempts >= RECONNECT_MAX_ATTEMPTS) {
|
|
2645
|
-
console.warn(`[connector-health] Connector "${connector.name}" exceeded ${RECONNECT_MAX_ATTEMPTS} reconnect attempts — marking as error`)
|
|
2646
|
-
connector.status = 'error'
|
|
2647
|
-
connector.lastError = `Auto-reconnect gave up after ${RECONNECT_MAX_ATTEMPTS} attempts: ${state.error}`
|
|
2648
|
-
connector.updatedAt = Date.now()
|
|
2649
|
-
connectors[id] = connector
|
|
2650
|
-
connectorsDirty = true
|
|
2651
|
-
reconnectState.delete(id)
|
|
2652
|
-
notify('connectors')
|
|
2653
|
-
continue
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
const now = Date.now()
|
|
2657
|
-
|
|
2658
|
-
// Check if enough time has passed for the next retry
|
|
2659
|
-
if (now < state.nextRetryAt) {
|
|
2660
|
-
// Not yet time to retry — keep state and skip
|
|
2661
|
-
continue
|
|
2662
|
-
}
|
|
2663
|
-
|
|
2664
|
-
state.attempts += 1
|
|
2665
|
-
state.lastAttemptAt = now
|
|
2666
|
-
reconnectState.set(id, state)
|
|
2667
|
-
|
|
2668
|
-
try {
|
|
2669
|
-
console.log(`[connector-health] Reconnecting "${connector.name}" (attempt ${state.attempts}/${RECONNECT_MAX_ATTEMPTS})`)
|
|
2670
|
-
await startConnector(id)
|
|
2671
|
-
// Success — clear reconnect state
|
|
2672
|
-
reconnectState.delete(id)
|
|
2673
|
-
console.log(`[connector-health] Connector "${connector.name}" reconnected successfully`)
|
|
2674
|
-
recordHealthEvent(id, 'reconnected', `Connector "${connector.name}" reconnected after ${state.attempts} attempt(s)`)
|
|
2675
|
-
} catch (err: unknown) {
|
|
2676
|
-
const errorMsg = err instanceof Error ? err.message : String(err)
|
|
2677
|
-
state.error = errorMsg
|
|
2678
|
-
state.backoffMs = Math.min(RECONNECT_MAX_BACKOFF_MS, RECONNECT_INITIAL_BACKOFF_MS * (2 ** state.attempts))
|
|
2679
|
-
state.nextRetryAt = now + state.backoffMs
|
|
2680
|
-
reconnectState.set(id, state)
|
|
2681
|
-
console.warn(`[connector-health] Reconnect failed for "${connector.name}" (attempt ${state.attempts}/${RECONNECT_MAX_ATTEMPTS}): ${errorMsg}. Next retry at ${new Date(state.nextRetryAt).toISOString()}`)
|
|
2996
|
+
connector.status = 'error'
|
|
2997
|
+
connector.lastError = connector.lastError || 'Connection lost'
|
|
2998
|
+
connector.updatedAt = Date.now()
|
|
2999
|
+
connectors[id] = connector
|
|
3000
|
+
connectorsDirty = true
|
|
3001
|
+
if (!reconnectState.has(id)) {
|
|
3002
|
+
setReconnectState(id, createConnectorReconnectState({
|
|
3003
|
+
error: connector.lastError || 'Connection lost',
|
|
3004
|
+
}))
|
|
2682
3005
|
}
|
|
2683
3006
|
}
|
|
2684
3007
|
|
|
2685
3008
|
if (connectorsDirty) {
|
|
2686
3009
|
saveConnectors(connectors)
|
|
3010
|
+
notify('connectors')
|
|
2687
3011
|
}
|
|
2688
3012
|
|
|
2689
3013
|
// Purge reconnect state for connectors that no longer exist
|
|
2690
3014
|
for (const id of reconnectState.keys()) {
|
|
2691
|
-
if (!connectors[id]
|
|
3015
|
+
if (!connectors[id] || connectors[id]?.isEnabled !== true || running.has(id)) clearReconnectState(id)
|
|
2692
3016
|
}
|
|
2693
3017
|
}
|
|
2694
3018
|
|