@swarmclawai/swarmclaw 0.7.8 → 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 -15
- 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 +22 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +26 -1
- 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/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/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 +73 -24
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +44 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- 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/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/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 +7 -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 +191 -95
- package/src/components/tasks/task-board.tsx +273 -2
- package/src/components/tasks/task-card.tsx +38 -9
- 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 +11 -0
- package/src/lib/server/capability-router.ts +26 -1
- 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 +353 -72
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +362 -63
- 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 +1 -1
- 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 +189 -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 +15 -10
- 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/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 +2 -2
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +205 -5
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +262 -0
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +293 -61
- 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 +52 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +348 -61
- package/src/lib/server/session-tools/context.ts +12 -3
- package/src/lib/server/session-tools/crud.ts +221 -10
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate.ts +64 -8
- 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/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +546 -79
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- 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 +162 -1
- package/src/lib/server/session-tools/web.ts +468 -64
- 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 +419 -9
- package/src/lib/server/stream-agent-chat.ts +887 -83
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- 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.ts +4 -2
- 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-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 +210 -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()
|
|
@@ -435,17 +472,19 @@ function rememberRecentInbound(key: string, now = Date.now(), ttlMs = 120_000):
|
|
|
435
472
|
function findDirectSessionForInbound(connector: Connector, msg: InboundMessage): ConnectorSession | null {
|
|
436
473
|
if (connector.chatroomId) return null
|
|
437
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))
|
|
438
477
|
const sessions = Object.values(loadSessions() as Record<string, ConnectorSession>)
|
|
439
478
|
const candidates = sessions.filter((session) =>
|
|
440
479
|
session?.agentId === effectiveAgentId
|
|
441
480
|
&& session?.connectorContext?.connectorId === connector.id
|
|
442
|
-
&& session?.connectorContext?.channelId
|
|
481
|
+
&& channelIds.has(session?.connectorContext?.channelId || ''),
|
|
443
482
|
)
|
|
444
483
|
if (msg.threadId) {
|
|
445
484
|
const threadExact = candidates.find((session) => session?.connectorContext?.threadId === msg.threadId)
|
|
446
485
|
if (threadExact) return threadExact
|
|
447
486
|
}
|
|
448
|
-
const senderExact = candidates.find((session) => session?.connectorContext?.senderId
|
|
487
|
+
const senderExact = candidates.find((session) => senderIds.has(session?.connectorContext?.senderId || ''))
|
|
449
488
|
if (senderExact) return senderExact
|
|
450
489
|
return candidates[0] || null
|
|
451
490
|
}
|
|
@@ -687,8 +726,10 @@ function parseConnectorCommand(text: string): ParsedConnectorCommand | null {
|
|
|
687
726
|
|
|
688
727
|
function persistSessionRecord(session: ConnectorSession): void {
|
|
689
728
|
const sessions = loadSessions()
|
|
729
|
+
session.updatedAt = Date.now()
|
|
690
730
|
sessions[session.id] = session
|
|
691
731
|
saveSessions(sessions)
|
|
732
|
+
notify('sessions')
|
|
692
733
|
}
|
|
693
734
|
|
|
694
735
|
function updateSessionConnectorContext(session: ConnectorSession, connector: Connector, msg: InboundMessage, sessionKey: string): void {
|
|
@@ -910,6 +951,9 @@ function resolveDirectSession(params: {
|
|
|
910
951
|
})
|
|
911
952
|
const sessions = loadSessions()
|
|
912
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
|
+
}
|
|
913
957
|
let wasCreated = false
|
|
914
958
|
if (!session) {
|
|
915
959
|
const id = genId()
|
|
@@ -980,20 +1024,93 @@ function resolveDirectSession(params: {
|
|
|
980
1024
|
}
|
|
981
1025
|
}
|
|
982
1026
|
|
|
983
|
-
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 {
|
|
984
1077
|
if (!text.trim()) return
|
|
985
1078
|
if (!Array.isArray(session.messages)) session.messages = []
|
|
986
|
-
|
|
1079
|
+
const message = { role, text: text.trim(), time: Date.now(), ...extra }
|
|
1080
|
+
session.messages.push(message)
|
|
987
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)
|
|
988
1091
|
}
|
|
989
1092
|
|
|
990
1093
|
function persistSession(session: ConnectorSession): void {
|
|
991
1094
|
const sessions = loadSessions()
|
|
1095
|
+
session.updatedAt = Date.now()
|
|
992
1096
|
sessions[session.id] = session
|
|
993
1097
|
saveSessions(sessions)
|
|
1098
|
+
notify('sessions')
|
|
994
1099
|
notify(`messages:${session.id}`)
|
|
995
1100
|
}
|
|
996
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
|
+
|
|
997
1114
|
function summarizeForCompaction(messages: Array<{ role?: string; text?: string }>): string {
|
|
998
1115
|
const preview = messages
|
|
999
1116
|
.slice(-8)
|
|
@@ -1016,11 +1133,16 @@ function resolvePairingAccess(connector: Connector, msg: InboundMessage): {
|
|
|
1016
1133
|
const policy = parsePairingPolicy(connector.config?.dmPolicy, 'open')
|
|
1017
1134
|
const configAllowFrom = parseAllowFromCsv(connector.config?.allowFrom)
|
|
1018
1135
|
const stored = listStoredAllowedSenders(connector.id)
|
|
1019
|
-
const isAllowed =
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
+
}))
|
|
1024
1146
|
return {
|
|
1025
1147
|
policy,
|
|
1026
1148
|
configAllowFrom,
|
|
@@ -1104,38 +1226,79 @@ async function handlePairCommand(params: {
|
|
|
1104
1226
|
].join('\n')
|
|
1105
1227
|
}
|
|
1106
1228
|
|
|
1107
|
-
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
|
|
1108
1249
|
if (msg.isGroup) return null
|
|
1109
|
-
const { policy,
|
|
1110
|
-
const storedAllowFrom = listStoredAllowedSenders(connector.id)
|
|
1250
|
+
const { policy, isAllowed } = resolvePairingAccess(connector, msg)
|
|
1111
1251
|
if (policy === 'open') return null
|
|
1112
1252
|
|
|
1113
1253
|
if (policy === 'disabled') return NO_MESSAGE_SENTINEL
|
|
1114
1254
|
if (isAllowed) return null
|
|
1115
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
|
+
|
|
1116
1278
|
if (policy === 'allowlist') {
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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')
|
|
1121
1284
|
}
|
|
1122
1285
|
|
|
1123
1286
|
if (policy === 'pairing') {
|
|
1124
1287
|
const request = createOrTouchPairingRequest({
|
|
1125
1288
|
connectorId: connector.id,
|
|
1126
|
-
senderId
|
|
1289
|
+
senderId,
|
|
1127
1290
|
senderName: msg.senderName,
|
|
1128
1291
|
channelId: msg.channelId,
|
|
1129
1292
|
})
|
|
1130
1293
|
return [
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
'
|
|
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>.',
|
|
1135
1298
|
].join('\n')
|
|
1136
1299
|
}
|
|
1137
1300
|
|
|
1138
|
-
return
|
|
1301
|
+
return 'This sender is not authorized for this connector.'
|
|
1139
1302
|
}
|
|
1140
1303
|
|
|
1141
1304
|
async function handleConnectorCommand(params: {
|
|
@@ -1493,7 +1656,7 @@ async function routeMessageToChatroom(connector: Connector, msg: InboundMessage)
|
|
|
1493
1656
|
history,
|
|
1494
1657
|
})
|
|
1495
1658
|
|
|
1496
|
-
const responseText = result.finalResponse || result.fullText
|
|
1659
|
+
const responseText = stripHiddenControlTokens(result.finalResponse || result.fullText)
|
|
1497
1660
|
if (responseText.trim() && !isNoMessage(responseText)) {
|
|
1498
1661
|
// Persist agent response to chatroom
|
|
1499
1662
|
const agentSource: MessageSource = {
|
|
@@ -1601,6 +1764,19 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
1601
1764
|
msg,
|
|
1602
1765
|
agent,
|
|
1603
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
|
+
}
|
|
1604
1780
|
|
|
1605
1781
|
const parsedCommand = parseConnectorCommand(msg.text || '')
|
|
1606
1782
|
if (parsedCommand?.name === 'pair') {
|
|
@@ -1621,8 +1797,36 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
1621
1797
|
return commandResult
|
|
1622
1798
|
}
|
|
1623
1799
|
|
|
1624
|
-
const accessPolicyResult = enforceInboundAccessPolicy(
|
|
1800
|
+
const accessPolicyResult = await enforceInboundAccessPolicy({
|
|
1801
|
+
connector,
|
|
1802
|
+
msg,
|
|
1803
|
+
session,
|
|
1804
|
+
agent,
|
|
1805
|
+
})
|
|
1625
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
|
+
}
|
|
1626
1830
|
logExecution(session.id, 'decision', 'Connector inbound blocked by access policy', {
|
|
1627
1831
|
agentId: agent.id,
|
|
1628
1832
|
detail: {
|
|
@@ -1685,7 +1889,18 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
1685
1889
|
`Inbound message from ${msg.platform}: ${preview}`,
|
|
1686
1890
|
'connector-message',
|
|
1687
1891
|
)
|
|
1688
|
-
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
|
+
})
|
|
1689
1904
|
|
|
1690
1905
|
logExecution(session.id, 'trigger', `${msg.platform} message from ${msg.senderName}`, {
|
|
1691
1906
|
agentId: agent.id,
|
|
@@ -1786,32 +2001,15 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
1786
2001
|
const firstImageUrl = msg.imageUrl || (firstImage?.url) || undefined
|
|
1787
2002
|
const firstImagePath = firstImage?.localPath || undefined
|
|
1788
2003
|
const inboundAttachmentPaths = buildInboundAttachmentPaths(msg)
|
|
1789
|
-
const inboundText = formatInboundUserText(msg)
|
|
1790
2004
|
const modelInputText = inboundText
|
|
1791
2005
|
// Store the raw user text for display (source.senderName handles attribution).
|
|
1792
2006
|
// The formatted text with [SenderName] prefix is only used for LLM history context.
|
|
1793
|
-
|
|
1794
|
-
const messageSource: MessageSource = {
|
|
1795
|
-
platform: connector.platform,
|
|
1796
|
-
connectorId: connector.id,
|
|
1797
|
-
connectorName: connector.name,
|
|
1798
|
-
channelId: msg.channelId,
|
|
1799
|
-
senderId: msg.senderId,
|
|
1800
|
-
senderName: msg.senderName,
|
|
1801
|
-
messageId: msg.messageId,
|
|
1802
|
-
replyToMessageId: msg.replyToMessageId,
|
|
1803
|
-
threadId: msg.threadId,
|
|
1804
|
-
}
|
|
1805
|
-
session.messages.push({
|
|
1806
|
-
role: 'user',
|
|
1807
|
-
text: rawText || inboundText,
|
|
1808
|
-
time: Date.now(),
|
|
2007
|
+
pushSessionMessage(session, 'user', rawText || inboundText, {
|
|
1809
2008
|
imageUrl: firstImageUrl,
|
|
1810
2009
|
imagePath: firstImagePath,
|
|
1811
2010
|
attachedFiles: inboundAttachmentPaths.length ? inboundAttachmentPaths : undefined,
|
|
1812
2011
|
source: messageSource,
|
|
1813
2012
|
})
|
|
1814
|
-
session.lastActiveAt = Date.now()
|
|
1815
2013
|
updateSessionConnectorContext(session, connector, msg, sessionKey)
|
|
1816
2014
|
persistSessionRecord(session)
|
|
1817
2015
|
notify(`messages:${session.id}`)
|
|
@@ -1821,13 +2019,16 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
1821
2019
|
let mediaExtractionText = ''
|
|
1822
2020
|
let connectorToolDeliveredCurrentChannel = false
|
|
1823
2021
|
let connectorToolDeliveredMessageId: string | undefined
|
|
2022
|
+
let streamErrorText = ''
|
|
2023
|
+
const connectorToolInputsByCallId = new Map<string, Record<string, unknown>>()
|
|
2024
|
+
const connectorToolMirrorTexts: string[] = []
|
|
1824
2025
|
const hasTools = session.plugins?.length && session.provider !== 'claude-cli'
|
|
1825
2026
|
console.log(`[connector] Routing message to agent "${agent.name}" (${session.provider}/${session.model}), hasTools=${!!hasTools}`)
|
|
1826
2027
|
|
|
1827
2028
|
if (hasTools) {
|
|
1828
2029
|
try {
|
|
1829
2030
|
const toolMediaOutputs: string[] = []
|
|
1830
|
-
const result = await
|
|
2031
|
+
const result = await streamAgentChatImpl({
|
|
1831
2032
|
session: session as Session,
|
|
1832
2033
|
message: modelInputText,
|
|
1833
2034
|
imagePath: firstImagePath,
|
|
@@ -1836,11 +2037,27 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
1836
2037
|
systemPrompt,
|
|
1837
2038
|
write: (raw) => {
|
|
1838
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
|
+
}
|
|
1839
2054
|
if (event.t !== 'tool_result') continue
|
|
1840
2055
|
const toolOutput = typeof event.toolOutput === 'string' ? event.toolOutput : ''
|
|
1841
2056
|
if (!toolOutput) continue
|
|
1842
2057
|
toolMediaOutputs.push(toolOutput)
|
|
1843
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
|
|
1844
2061
|
const parsed = parseConnectorToolResult(toolOutput)
|
|
1845
2062
|
if (!parsed?.status || !parsed.to) continue
|
|
1846
2063
|
const sentLikeStatus = parsed.status === 'sent' || parsed.status === 'voice_sent'
|
|
@@ -1854,11 +2071,13 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
1854
2071
|
if (inboundTarget && outboundTarget && inboundTarget === outboundTarget) {
|
|
1855
2072
|
connectorToolDeliveredCurrentChannel = true
|
|
1856
2073
|
if (parsed.messageId) connectorToolDeliveredMessageId = parsed.messageId
|
|
2074
|
+
const mirrorText = visibleConnectorToolText(mirrorInput)
|
|
2075
|
+
if (mirrorText) connectorToolMirrorTexts.push(mirrorText)
|
|
1857
2076
|
}
|
|
1858
2077
|
}
|
|
1859
2078
|
}
|
|
1860
2079
|
},
|
|
1861
|
-
history: session.messages
|
|
2080
|
+
history: modelHistoryTail(session.messages),
|
|
1862
2081
|
})
|
|
1863
2082
|
// Use finalResponse for connectors — strips intermediate planning/tool-use text
|
|
1864
2083
|
fullText = result.finalResponse || result.fullText
|
|
@@ -1891,26 +2110,54 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
1891
2110
|
}
|
|
1892
2111
|
},
|
|
1893
2112
|
active: new Map(),
|
|
1894
|
-
loadHistory: () => session.messages
|
|
2113
|
+
loadHistory: () => modelHistoryTail(session.messages),
|
|
1895
2114
|
})
|
|
1896
2115
|
mediaExtractionText = fullText
|
|
1897
2116
|
}
|
|
1898
2117
|
|
|
2118
|
+
if (!fullText.trim() && !connectorToolDeliveredCurrentChannel) {
|
|
2119
|
+
fullText = connectorEmptyReplyFallback(streamErrorText)
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
const suppressHiddenResponse = shouldSuppressHiddenControlText(fullText)
|
|
2123
|
+
fullText = stripHiddenControlTokens(fullText)
|
|
2124
|
+
|
|
1899
2125
|
// If the agent chose NO_MESSAGE, skip saving it to history — the user's message
|
|
1900
2126
|
// is already recorded, and saving the sentinel would pollute the LLM's context
|
|
1901
|
-
if (isNoMessage(fullText)) {
|
|
2127
|
+
if (suppressHiddenResponse || isNoMessage(fullText)) {
|
|
1902
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
|
+
}
|
|
1903
2149
|
session.connectorContext = {
|
|
1904
2150
|
...(session.connectorContext || {}),
|
|
1905
2151
|
lastOutboundAt: Date.now(),
|
|
1906
2152
|
lastOutboundMessageId: connectorToolDeliveredMessageId || session.connectorContext?.lastOutboundMessageId || null,
|
|
1907
2153
|
}
|
|
1908
2154
|
persistSessionRecord(session)
|
|
2155
|
+
notify(`messages:${session.id}`)
|
|
1909
2156
|
await maybeSendStatusReaction(connector, msg, 'sent')
|
|
1910
2157
|
} else {
|
|
1911
2158
|
await maybeSendStatusReaction(connector, msg, 'silent')
|
|
1912
2159
|
}
|
|
1913
|
-
console.log(`[connector] Agent returned
|
|
2160
|
+
console.log(`[connector] Agent returned hidden control sentinel — suppressing outbound reply`)
|
|
1914
2161
|
logExecution(session.id, 'decision', 'Agent suppressed outbound (NO_MESSAGE)', {
|
|
1915
2162
|
agentId: agent.id,
|
|
1916
2163
|
detail: { platform: msg.platform, channelId: msg.channelId },
|
|
@@ -1936,12 +2183,13 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
1936
2183
|
connectorId: connector.id,
|
|
1937
2184
|
connectorName: connector.name,
|
|
1938
2185
|
channelId: msg.channelId,
|
|
2186
|
+
senderId: msg.senderId,
|
|
2187
|
+
senderName: msg.senderName,
|
|
1939
2188
|
replyToMessageId: msg.messageId,
|
|
1940
2189
|
threadId: msg.threadId,
|
|
1941
2190
|
}
|
|
1942
2191
|
if (fullText.trim()) {
|
|
1943
|
-
session
|
|
1944
|
-
session.lastActiveAt = Date.now()
|
|
2192
|
+
pushSessionMessage(session, 'assistant', fullText.trim(), { source: assistantSource })
|
|
1945
2193
|
persistSessionRecord(session)
|
|
1946
2194
|
notify(`messages:${session.id}`)
|
|
1947
2195
|
}
|
|
@@ -2021,6 +2269,8 @@ If media sending fails, report the exact error and retry with a corrected path/t
|
|
|
2021
2269
|
|
|
2022
2270
|
routeMessageHandlerRef.current = routeMessage
|
|
2023
2271
|
|
|
2272
|
+
export const routeConnectorMessageForTest = routeMessage
|
|
2273
|
+
|
|
2024
2274
|
/** Start a connector (serialized per ID to prevent concurrent start/stop races) */
|
|
2025
2275
|
export async function startConnector(connectorId: string): Promise<void> {
|
|
2026
2276
|
// Wait for any pending operation on this connector to finish (with timeout)
|
|
@@ -2439,6 +2689,30 @@ export async function performConnectorMessageAction(params: {
|
|
|
2439
2689
|
}
|
|
2440
2690
|
}
|
|
2441
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
|
+
|
|
2442
2716
|
/**
|
|
2443
2717
|
* Send an outbound message through a running connector.
|
|
2444
2718
|
* Intended for proactive agent notifications (e.g. WhatsApp updates).
|
|
@@ -2483,16 +2757,18 @@ export async function sendConnectorMessage(params: {
|
|
|
2483
2757
|
|
|
2484
2758
|
if (!connector || !connectorId) throw new Error('Connector resolution failed.')
|
|
2485
2759
|
|
|
2486
|
-
const
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2760
|
+
const {
|
|
2761
|
+
sanitizedText,
|
|
2762
|
+
suppressHiddenText,
|
|
2763
|
+
sanitizedCaptionText,
|
|
2764
|
+
sanitizedCaption,
|
|
2765
|
+
} = sanitizeConnectorOutboundContent({
|
|
2766
|
+
text: params.text,
|
|
2767
|
+
caption: params.caption,
|
|
2768
|
+
})
|
|
2493
2769
|
|
|
2494
2770
|
// Apply NO_MESSAGE filter at the delivery layer so all outbound paths respect it
|
|
2495
|
-
if (isNoMessage(
|
|
2771
|
+
if ((suppressHiddenText || isNoMessage(sanitizedText)) && !params.imageUrl && !params.fileUrl && !params.mediaPath) {
|
|
2496
2772
|
console.log(`[connector] sendConnectorMessage: NO_MESSAGE — suppressing outbound send`)
|
|
2497
2773
|
return { connectorId, platform: connector.platform, channelId: params.channelId }
|
|
2498
2774
|
}
|
|
@@ -2502,14 +2778,14 @@ export async function sendConnectorMessage(params: {
|
|
|
2502
2778
|
? normalizeWhatsappTarget(params.channelId)
|
|
2503
2779
|
: params.channelId
|
|
2504
2780
|
|
|
2505
|
-
let outboundText =
|
|
2781
|
+
let outboundText = sanitizedText
|
|
2506
2782
|
let outboundOptions: Parameters<NonNullable<ConnectorInstance['sendMessage']>>[2] | undefined = {
|
|
2507
2783
|
imageUrl: params.imageUrl,
|
|
2508
2784
|
fileUrl: params.fileUrl,
|
|
2509
2785
|
mediaPath: params.mediaPath,
|
|
2510
2786
|
mimeType: params.mimeType,
|
|
2511
2787
|
fileName: params.fileName,
|
|
2512
|
-
caption:
|
|
2788
|
+
caption: sanitizedCaption,
|
|
2513
2789
|
replyToMessageId: params.replyToMessageId,
|
|
2514
2790
|
threadId: params.threadId,
|
|
2515
2791
|
ptt: params.ptt,
|
|
@@ -2520,8 +2796,8 @@ export async function sendConnectorMessage(params: {
|
|
|
2520
2796
|
|| params.fileUrl
|
|
2521
2797
|
|| (params.mediaPath ? uploadApiUrlFromPath(params.mediaPath) : null)
|
|
2522
2798
|
const fallbackParts = [
|
|
2523
|
-
|
|
2524
|
-
|
|
2799
|
+
sanitizedText.trim(),
|
|
2800
|
+
sanitizedCaptionText,
|
|
2525
2801
|
mediaLink ? `Attachment: ${mediaLink}` : '',
|
|
2526
2802
|
!mediaLink && params.mediaPath ? `Attachment: ${path.basename(params.mediaPath)}` : '',
|
|
2527
2803
|
].filter(Boolean)
|
|
@@ -2529,7 +2805,29 @@ export async function sendConnectorMessage(params: {
|
|
|
2529
2805
|
outboundOptions = undefined
|
|
2530
2806
|
}
|
|
2531
2807
|
|
|
2532
|
-
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
|
+
|
|
2533
2831
|
if (params.sessionId) {
|
|
2534
2832
|
const sessions = loadSessions()
|
|
2535
2833
|
const session = sessions[params.sessionId]
|
|
@@ -2562,6 +2860,7 @@ export async function sendConnectorMessage(params: {
|
|
|
2562
2860
|
}
|
|
2563
2861
|
sessions[session.id] = session
|
|
2564
2862
|
saveSessions(sessions)
|
|
2863
|
+
notify('sessions')
|
|
2565
2864
|
notify(`messages:${session.id}`)
|
|
2566
2865
|
}
|
|
2567
2866
|
}
|