synthos 0.7.1 → 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 +215 -65
- package/default-pages/application.json +1 -0
- package/default-pages/json_tools.json +1 -1
- package/default-pages/oregon_trail.html +321 -0
- package/default-pages/oregon_trail.json +12 -0
- package/default-pages/sidebar_page.json +1 -0
- package/default-pages/solar_explorer.html +10 -18
- package/default-pages/solar_explorer.json +2 -2
- package/default-pages/two-panel_page.json +1 -0
- package/default-pages/us_map.html +192 -0
- package/default-pages/us_map.json +12 -0
- package/default-pages/us_map_1850.html +325 -0
- package/default-pages/us_map_1850.json +12 -0
- package/default-pages/western_cities_1850.html +526 -0
- package/default-pages/western_cities_1850.json +12 -0
- package/default-themes/{nebula-dawn.css → nebula-dawn.v2.css} +24 -0
- package/default-themes/{nebula-dusk.css → nebula-dusk.v2.css} +24 -0
- package/dist/agents/a2a/a2aProvider.d.ts +3 -0
- package/dist/agents/a2a/a2aProvider.d.ts.map +1 -0
- package/dist/agents/a2a/a2aProvider.js +126 -0
- package/dist/agents/a2a/a2aProvider.js.map +1 -0
- package/dist/agents/discovery.d.ts +30 -0
- package/dist/agents/discovery.d.ts.map +1 -0
- package/dist/agents/discovery.js +52 -0
- package/dist/agents/discovery.js.map +1 -0
- package/dist/agents/index.d.ts +7 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +19 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/openclaw/gatewayManager.d.ts +113 -0
- package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -0
- package/dist/agents/openclaw/gatewayManager.js +470 -0
- package/dist/agents/openclaw/gatewayManager.js.map +1 -0
- package/dist/agents/openclaw/openclawProvider.d.ts +3 -0
- package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -0
- package/dist/agents/openclaw/openclawProvider.js +239 -0
- package/dist/agents/openclaw/openclawProvider.js.map +1 -0
- package/dist/agents/openclaw/sshTunnelManager.d.ts +23 -0
- package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -0
- package/dist/agents/openclaw/sshTunnelManager.js +340 -0
- package/dist/agents/openclaw/sshTunnelManager.js.map +1 -0
- package/dist/agents/types.d.ts +64 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +6 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/connectors/airtable/connector.json +27 -0
- package/dist/connectors/alpha-vantage/connector.json +26 -0
- package/dist/connectors/brave-search/connector.json +26 -0
- package/dist/connectors/cloudinary/connector.json +27 -0
- package/dist/connectors/deepl/connector.json +28 -0
- package/dist/connectors/elevenlabs/connector.json +30 -0
- package/dist/connectors/giphy/connector.json +27 -0
- package/dist/connectors/github/connector.json +29 -0
- package/dist/connectors/huggingface/connector.json +27 -0
- package/dist/connectors/imgur/connector.json +29 -0
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.d.ts.map +1 -1
- package/dist/connectors/instagram/connector.json +43 -0
- package/dist/connectors/jira/connector.json +28 -0
- package/dist/connectors/mapbox/connector.json +26 -0
- package/dist/connectors/nasa/connector.json +27 -0
- package/dist/connectors/newsapi/connector.json +27 -0
- package/dist/connectors/notion/connector.json +28 -0
- package/dist/connectors/open-exchange-rates/connector.json +27 -0
- package/dist/connectors/openweathermap/connector.json +26 -0
- package/dist/connectors/pexels/connector.json +27 -0
- package/dist/connectors/registry.d.ts.map +1 -1
- package/dist/connectors/registry.js +42 -96
- package/dist/connectors/registry.js.map +1 -1
- package/dist/connectors/resend/connector.json +29 -0
- package/dist/connectors/rss2json/connector.json +27 -0
- package/dist/connectors/sendgrid/connector.json +27 -0
- package/dist/connectors/spoonacular/connector.json +28 -0
- package/dist/connectors/stability-ai/connector.json +27 -0
- package/dist/connectors/twilio/connector.json +28 -0
- package/dist/connectors/types.d.ts +23 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/connectors/unsplash/connector.json +27 -0
- package/dist/connectors/wolfram-alpha/connector.json +26 -0
- package/dist/connectors/youtube-data/connector.json +30 -0
- package/dist/files.d.ts +1 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +16 -1
- package/dist/files.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +28 -0
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts +3 -2
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +122 -138
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +22 -0
- package/dist/models/anthropic.d.ts.map +1 -0
- package/dist/models/anthropic.js +76 -0
- package/dist/models/anthropic.js.map +1 -0
- package/dist/models/chainOfThought.d.ts +12 -0
- package/dist/models/chainOfThought.d.ts.map +1 -0
- package/dist/models/chainOfThought.js +45 -0
- package/dist/models/chainOfThought.js.map +1 -0
- package/dist/models/fireworksai.d.ts +30 -0
- package/dist/models/fireworksai.d.ts.map +1 -0
- package/dist/models/fireworksai.js +133 -0
- package/dist/models/fireworksai.js.map +1 -0
- package/dist/models/index.d.ts +7 -1
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +19 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/logCompletePrompt.d.ts +3 -0
- package/dist/models/logCompletePrompt.d.ts.map +1 -0
- package/dist/models/logCompletePrompt.js +23 -0
- package/dist/models/logCompletePrompt.js.map +1 -0
- package/dist/models/openai.d.ts +24 -0
- package/dist/models/openai.d.ts.map +1 -0
- package/dist/models/openai.js +80 -0
- package/dist/models/openai.js.map +1 -0
- package/dist/models/providers.d.ts +1 -0
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/providers.js +12 -4
- package/dist/models/providers.js.map +1 -1
- package/dist/models/types.d.ts +34 -2
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js +16 -0
- package/dist/models/types.js.map +1 -1
- package/dist/models/utils.d.ts +6 -0
- package/dist/models/utils.d.ts.map +1 -0
- package/dist/models/utils.js +21 -0
- package/dist/models/utils.js.map +1 -0
- package/dist/scripts.d.ts +2 -1
- package/dist/scripts.d.ts.map +1 -1
- package/dist/scripts.js +4 -3
- package/dist/scripts.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts +1 -1
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +9 -6
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/generateImage.d.ts +1 -1
- package/dist/service/generateImage.d.ts.map +1 -1
- package/dist/service/generateImage.js +3 -3
- package/dist/service/generateImage.js.map +1 -1
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +3 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/transformPage.d.ts +4 -2
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +74 -6
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useAgentRoutes.d.ts +4 -0
- package/dist/service/useAgentRoutes.d.ts.map +1 -0
- package/dist/service/useAgentRoutes.js +389 -0
- package/dist/service/useAgentRoutes.js.map +1 -0
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +157 -16
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts.map +1 -1
- package/dist/service/useConnectorRoutes.js +14 -3
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useGatewayRoutes.d.ts +4 -0
- package/dist/service/useGatewayRoutes.d.ts.map +1 -0
- package/dist/service/useGatewayRoutes.js +168 -0
- package/dist/service/useGatewayRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +16 -5
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/settings.d.ts +2 -1
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +4 -8
- package/dist/settings.js.map +1 -1
- package/dist/themes.d.ts +14 -0
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +86 -13
- package/dist/themes.js.map +1 -1
- package/package.json +10 -5
- package/page-scripts/helpers-v2.js +222 -0
- package/page-scripts/page-v2.js +656 -0
- package/required-pages/builder.html +1 -27
- package/required-pages/pages.html +745 -22
- package/required-pages/settings.html +819 -21
- package/required-pages/synthos_apis.html +56 -1
- package/src/agents/a2a/a2aProvider.ts +110 -0
- package/src/agents/discovery.ts +74 -0
- package/src/agents/index.ts +6 -0
- package/src/agents/openclaw/gatewayManager.ts +559 -0
- package/src/agents/openclaw/openclawProvider.ts +261 -0
- package/src/agents/openclaw/sshTunnelManager.ts +385 -0
- package/src/agents/types.ts +82 -0
- package/src/connectors/airtable/connector.json +27 -0
- package/src/connectors/alpha-vantage/connector.json +26 -0
- package/src/connectors/brave-search/connector.json +26 -0
- package/src/connectors/cloudinary/connector.json +27 -0
- package/src/connectors/deepl/connector.json +28 -0
- package/src/connectors/elevenlabs/connector.json +30 -0
- package/src/connectors/giphy/connector.json +27 -0
- package/src/connectors/github/connector.json +29 -0
- package/src/connectors/huggingface/connector.json +27 -0
- package/src/connectors/imgur/connector.json +29 -0
- package/src/connectors/index.ts +2 -0
- package/src/connectors/instagram/connector.json +43 -0
- package/src/connectors/jira/connector.json +28 -0
- package/src/connectors/mapbox/connector.json +26 -0
- package/src/connectors/nasa/connector.json +27 -0
- package/src/connectors/newsapi/connector.json +27 -0
- package/src/connectors/notion/connector.json +28 -0
- package/src/connectors/open-exchange-rates/connector.json +27 -0
- package/src/connectors/openweathermap/connector.json +26 -0
- package/src/connectors/pexels/connector.json +27 -0
- package/src/connectors/registry.ts +21 -97
- package/src/connectors/resend/connector.json +29 -0
- package/src/connectors/rss2json/connector.json +27 -0
- package/src/connectors/sendgrid/connector.json +27 -0
- package/src/connectors/spoonacular/connector.json +28 -0
- package/src/connectors/stability-ai/connector.json +27 -0
- package/src/connectors/twilio/connector.json +28 -0
- package/src/connectors/types.ts +25 -0
- package/src/connectors/unsplash/connector.json +27 -0
- package/src/connectors/wolfram-alpha/connector.json +26 -0
- package/src/connectors/youtube-data/connector.json +30 -0
- package/src/files.ts +14 -0
- package/src/init.ts +27 -0
- package/src/migrations.ts +121 -138
- package/src/models/anthropic.ts +89 -0
- package/src/models/chainOfThought.ts +56 -0
- package/src/models/fireworksai.ts +136 -0
- package/src/models/index.ts +7 -1
- package/src/models/logCompletePrompt.ts +25 -0
- package/src/models/openai.ts +90 -0
- package/src/models/providers.ts +12 -3
- package/src/models/types.ts +67 -2
- package/src/models/utils.ts +16 -0
- package/src/scripts.ts +2 -2
- package/src/service/createCompletePrompt.ts +3 -1
- package/src/service/generateImage.ts +2 -2
- package/src/service/server.ts +4 -0
- package/src/service/transformPage.ts +81 -8
- package/src/service/useAgentRoutes.ts +423 -0
- package/src/service/useApiRoutes.ts +173 -18
- package/src/service/useConnectorRoutes.ts +14 -3
- package/src/service/usePageRoutes.ts +20 -6
- package/src/settings.ts +6 -10
- package/src/themes.ts +84 -12
- package/tests/README.md +12 -0
- package/tests/anthropic.spec.ts +84 -0
- package/tests/chainOfThought.spec.ts +108 -0
- package/tests/ensureScripts.spec.ts +82 -0
- package/tests/files.spec.ts +233 -0
- package/tests/fireworksai.spec.ts +92 -0
- package/tests/logCompletePrompt.spec.ts +74 -0
- package/tests/migrations.spec.ts +169 -0
- package/tests/openai.spec.ts +71 -0
- package/tests/pages.spec.ts +328 -0
- package/tests/providers.spec.ts +144 -0
- package/tests/scripts.spec.ts +209 -0
- package/tests/transformPage.spec.ts +931 -0
- package/tests/types.spec.ts +23 -0
- package/default-pages/app_builder.json +0 -1
- package/default-pages/sidebar_builder.json +0 -1
- package/default-pages/two-panel_builder.json +0 -1
- package/images/home.png +0 -0
- package/images/page-management.png +0 -0
- package/images/settings.png +0 -0
- package/images/synthos-square.png +0 -0
- /package/default-pages/{app_builder.html → application.html} +0 -0
- /package/default-pages/{sidebar_builder.html → sidebar_page.html} +0 -0
- /package/default-pages/{two-panel_builder.html → two-panel_page.html} +0 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { AgentConfig, AgentResponse, AgentEvent, AgentProvider, Attachment, ChatMessage } from '../types';
|
|
3
|
+
import { connectAgent, GatewayConnection, request, onEvent, offEvent } from './gatewayManager';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OpenClaw provider that communicates over the gateway WebSocket.
|
|
7
|
+
*
|
|
8
|
+
* Chat flow:
|
|
9
|
+
* 1. Ensure WebSocket connection (connectAgent)
|
|
10
|
+
* 2. Resolve the main session key (sessions.resolve)
|
|
11
|
+
* 3. Send message via chat.send RPC → returns { runId, status: "started" }
|
|
12
|
+
* 4. Listen for "chat" events with matching runId for streamed response
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Get or create a WebSocket connection for an agent. */
|
|
16
|
+
async function getConnection(agent: AgentConfig): Promise<GatewayConnection> {
|
|
17
|
+
if (!agent.token) throw new Error(`Agent "${agent.name}" has no token configured`);
|
|
18
|
+
return connectAgent({ id: agent.id, name: agent.name, url: agent.url, token: agent.token });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Get the session key from agent config or resolve it. */
|
|
22
|
+
async function resolveSessionKey(agent: AgentConfig, conn: GatewayConnection): Promise<string> {
|
|
23
|
+
if (agent.sessionKey) return agent.sessionKey;
|
|
24
|
+
const result = await request(conn, 'sessions.resolve', {}) as Record<string, unknown>;
|
|
25
|
+
if (!result.key || typeof result.key !== 'string') {
|
|
26
|
+
throw new Error('Failed to resolve session key — set a Default Session Key in agent settings');
|
|
27
|
+
}
|
|
28
|
+
return result.key;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Filter out noise from chat history — heartbeats, tool output, media refs, etc.
|
|
33
|
+
* Returns true if the message should be excluded.
|
|
34
|
+
*/
|
|
35
|
+
function isNoiseMessage(role: string, content: string): boolean {
|
|
36
|
+
const trimmed = content.trim();
|
|
37
|
+
if (!trimmed) return true;
|
|
38
|
+
|
|
39
|
+
// Heartbeat prompts (user) and responses (assistant)
|
|
40
|
+
if (content.includes('"conversation_label"') && content.includes('heartbeat')) return true;
|
|
41
|
+
if (trimmed === 'HEARTBEAT_OK') return true;
|
|
42
|
+
|
|
43
|
+
// Raw JSON tool output (starts with { and looks like a JSON blob)
|
|
44
|
+
if (trimmed.startsWith('{') && (
|
|
45
|
+
trimmed.includes('"targetId"') ||
|
|
46
|
+
trimmed.includes('"ok"') ||
|
|
47
|
+
trimmed.includes('"error"') ||
|
|
48
|
+
trimmed.includes('"url"') ||
|
|
49
|
+
trimmed.includes('"format"')
|
|
50
|
+
)) return true;
|
|
51
|
+
|
|
52
|
+
// OpenClaw security notice / browser content injection
|
|
53
|
+
if (trimmed.startsWith('SECURITY NOTICE:')) return true;
|
|
54
|
+
|
|
55
|
+
// Media file references
|
|
56
|
+
if (trimmed.startsWith('MEDIA:')) return true;
|
|
57
|
+
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const openclawProvider: AgentProvider = {
|
|
62
|
+
async send(agent: AgentConfig, message: string, attachments?: Attachment[]): Promise<AgentResponse> {
|
|
63
|
+
const conn = await getConnection(agent);
|
|
64
|
+
const sessionKey = await resolveSessionKey(agent, conn);
|
|
65
|
+
|
|
66
|
+
// Collect response text from chat events
|
|
67
|
+
let responseText = '';
|
|
68
|
+
let done = false;
|
|
69
|
+
|
|
70
|
+
const chatListener = (payload: unknown) => {
|
|
71
|
+
const evt = payload as Record<string, unknown>;
|
|
72
|
+
const state = evt.state as string | undefined;
|
|
73
|
+
const msg = evt.message as Record<string, unknown> | undefined;
|
|
74
|
+
|
|
75
|
+
if (msg?.content && Array.isArray(msg.content)) {
|
|
76
|
+
for (const part of msg.content) {
|
|
77
|
+
const p = part as Record<string, unknown>;
|
|
78
|
+
if (p.type === 'text' && typeof p.text === 'string') {
|
|
79
|
+
responseText = p.text;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (state === 'final' || state === 'error') {
|
|
85
|
+
done = true;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
onEvent(conn, 'chat', chatListener);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Send the message — returns immediately with { runId, status: "started" }
|
|
93
|
+
await request(conn, 'chat.send', {
|
|
94
|
+
sessionKey,
|
|
95
|
+
message,
|
|
96
|
+
idempotencyKey: randomUUID(),
|
|
97
|
+
...(attachments?.length ? { attachments } : {}),
|
|
98
|
+
}, { expectFinal: false });
|
|
99
|
+
|
|
100
|
+
// Wait for the chat to complete (poll with timeout)
|
|
101
|
+
const timeout = 60_000;
|
|
102
|
+
const start = Date.now();
|
|
103
|
+
while (!done && Date.now() - start < timeout) {
|
|
104
|
+
await new Promise(r => setTimeout(r, 100));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
kind: 'message',
|
|
109
|
+
text: responseText || undefined,
|
|
110
|
+
raw: { sessionKey, text: responseText },
|
|
111
|
+
};
|
|
112
|
+
} finally {
|
|
113
|
+
offEvent(conn, 'chat', chatListener);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async *sendStream(agent: AgentConfig, message: string, attachments?: Attachment[]): AsyncIterable<AgentEvent> {
|
|
118
|
+
const conn = await getConnection(agent);
|
|
119
|
+
const sessionKey = await resolveSessionKey(agent, conn);
|
|
120
|
+
|
|
121
|
+
// Set up an event queue for streaming
|
|
122
|
+
const queue: AgentEvent[] = [];
|
|
123
|
+
let done = false;
|
|
124
|
+
let resolveWait: (() => void) | null = null;
|
|
125
|
+
|
|
126
|
+
// Use agent events for token-by-token streaming deltas,
|
|
127
|
+
// and chat events for final state detection.
|
|
128
|
+
const agentListener = (payload: unknown) => {
|
|
129
|
+
const evt = payload as Record<string, unknown>;
|
|
130
|
+
const stream = evt.stream as string | undefined;
|
|
131
|
+
const data = evt.data as Record<string, unknown> | undefined;
|
|
132
|
+
|
|
133
|
+
if (stream === 'assistant' && data?.delta && typeof data.delta === 'string') {
|
|
134
|
+
queue.push({ kind: 'text', data: data.delta });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (stream === 'lifecycle' && data?.phase === 'end') {
|
|
138
|
+
queue.push({ kind: 'done', data: null });
|
|
139
|
+
done = true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (resolveWait) { resolveWait(); resolveWait = null; }
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const chatListener = (payload: unknown) => {
|
|
146
|
+
const evt = payload as Record<string, unknown>;
|
|
147
|
+
const state = evt.state as string | undefined;
|
|
148
|
+
|
|
149
|
+
if (state === 'error') {
|
|
150
|
+
const errorMsg = (evt.error as Record<string, unknown>)?.message ?? 'Unknown error';
|
|
151
|
+
queue.push({ kind: 'error', data: errorMsg });
|
|
152
|
+
done = true;
|
|
153
|
+
if (resolveWait) { resolveWait(); resolveWait = null; }
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
onEvent(conn, 'agent', agentListener);
|
|
158
|
+
onEvent(conn, 'chat', chatListener);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
// Send the message
|
|
162
|
+
await request(conn, 'chat.send', {
|
|
163
|
+
sessionKey,
|
|
164
|
+
message,
|
|
165
|
+
idempotencyKey: randomUUID(),
|
|
166
|
+
...(attachments?.length ? { attachments } : {}),
|
|
167
|
+
}, { expectFinal: false });
|
|
168
|
+
|
|
169
|
+
// Yield events as they arrive
|
|
170
|
+
const timeout = 60_000;
|
|
171
|
+
const start = Date.now();
|
|
172
|
+
while (!done && Date.now() - start < timeout) {
|
|
173
|
+
if (queue.length === 0) {
|
|
174
|
+
await new Promise<void>(r => {
|
|
175
|
+
resolveWait = r;
|
|
176
|
+
setTimeout(r, 500);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
while (queue.length > 0) {
|
|
181
|
+
yield queue.shift()!;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!done) {
|
|
186
|
+
yield { kind: 'error', data: 'Chat response timed out' };
|
|
187
|
+
}
|
|
188
|
+
} finally {
|
|
189
|
+
offEvent(conn, 'agent', agentListener);
|
|
190
|
+
offEvent(conn, 'chat', chatListener);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
supportsStreaming(): boolean {
|
|
195
|
+
return true;
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
async getHistory(agent: AgentConfig): Promise<ChatMessage[]> {
|
|
199
|
+
const conn = await getConnection(agent);
|
|
200
|
+
const sessionKey = await resolveSessionKey(agent, conn);
|
|
201
|
+
const result = await request(conn, 'chat.history', { sessionKey });
|
|
202
|
+
// Discovery log removed — response is too large for routine logging
|
|
203
|
+
|
|
204
|
+
// Normalize: expect result to have a messages array (adjust once we see the real shape)
|
|
205
|
+
const raw = result as Record<string, unknown>;
|
|
206
|
+
const items = (raw.messages ?? raw.items ?? raw.history ?? []) as unknown[];
|
|
207
|
+
const messages: ChatMessage[] = [];
|
|
208
|
+
for (const item of items) {
|
|
209
|
+
const m = item as Record<string, unknown>;
|
|
210
|
+
const rawRole = m.role as string ?? 'assistant';
|
|
211
|
+
|
|
212
|
+
// Skip toolCall / toolResult pairs entirely — not user-visible
|
|
213
|
+
if (rawRole === 'toolResult' || rawRole === 'tool') continue;
|
|
214
|
+
if (rawRole === 'assistant' && Array.isArray(m.content)) {
|
|
215
|
+
const hasOnlyToolCalls = (m.content as Record<string, unknown>[]).every(
|
|
216
|
+
p => p.type === 'toolCall' || p.type === 'tool_use'
|
|
217
|
+
);
|
|
218
|
+
if (hasOnlyToolCalls) continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const role: 'user' | 'assistant' = rawRole === 'user' ? 'user' : 'assistant';
|
|
222
|
+
|
|
223
|
+
// Extract text from content array if present
|
|
224
|
+
let content = '';
|
|
225
|
+
if (typeof m.content === 'string') {
|
|
226
|
+
content = m.content;
|
|
227
|
+
} else if (Array.isArray(m.content)) {
|
|
228
|
+
for (const part of m.content) {
|
|
229
|
+
const p = part as Record<string, unknown>;
|
|
230
|
+
if (p.type === 'text' && typeof p.text === 'string') {
|
|
231
|
+
content += p.text;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (isNoiseMessage(role, content)) continue;
|
|
237
|
+
|
|
238
|
+
messages.push({
|
|
239
|
+
role,
|
|
240
|
+
content,
|
|
241
|
+
timestamp: m.timestamp as number | undefined,
|
|
242
|
+
raw: m,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return messages;
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
async abortChat(agent: AgentConfig): Promise<void> {
|
|
249
|
+
const conn = await getConnection(agent);
|
|
250
|
+
const sessionKey = await resolveSessionKey(agent, conn);
|
|
251
|
+
const result = await request(conn, 'chat.abort', { sessionKey });
|
|
252
|
+
console.log('[OpenClaw] ← raw chat.abort:', JSON.stringify(result, null, 2));
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
async clearSession(agent: AgentConfig): Promise<void> {
|
|
256
|
+
const conn = await getConnection(agent);
|
|
257
|
+
const sessionKey = await resolveSessionKey(agent, conn);
|
|
258
|
+
const result = await request(conn, 'sessions.reset', { sessionKey });
|
|
259
|
+
console.log('[OpenClaw] ← raw sessions.reset:', JSON.stringify(result, null, 2));
|
|
260
|
+
},
|
|
261
|
+
};
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
2
|
+
import * as net from 'net';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import { randomBytes } from 'crypto';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Types
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export interface SshTunnelConfig {
|
|
13
|
+
/** Full SSH command, e.g. "ssh -p 22 -N -L 18789:127.0.0.1:43901 root@1.2.3.4" */
|
|
14
|
+
command: string;
|
|
15
|
+
/** Password to pipe when prompted */
|
|
16
|
+
password: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface TunnelEntry {
|
|
20
|
+
agentId: string;
|
|
21
|
+
config: SshTunnelConfig;
|
|
22
|
+
process: ChildProcess | null;
|
|
23
|
+
running: boolean;
|
|
24
|
+
reconnecting: boolean;
|
|
25
|
+
intentionalStop: boolean;
|
|
26
|
+
reconnectTimer: ReturnType<typeof setTimeout> | null;
|
|
27
|
+
reconnectAttempts: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Constants
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const TUNNEL_READY_PROBE_DELAY_MS = 2_000;
|
|
35
|
+
const BASE_RECONNECT_DELAY_MS = 2_000;
|
|
36
|
+
const MAX_RECONNECT_DELAY_MS = 120_000;
|
|
37
|
+
|
|
38
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Shared askpass script — reads password from an env var, never touches disk
|
|
42
|
+
// with the actual secret. One script is created per process lifetime and
|
|
43
|
+
// reused by all tunnels. Each tunnel sets a unique env var name so concurrent
|
|
44
|
+
// tunnels don't collide.
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
let sharedAskpassPath: string | null = null;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Env-var-based askpass script. The script reads the env var whose name is
|
|
51
|
+
* passed via SYNTHOS_SSH_PASS_VAR and echoes its value.
|
|
52
|
+
*
|
|
53
|
+
* Windows .bat: echo %<varName>%
|
|
54
|
+
* Unix .sh: eval echo \$<varName>
|
|
55
|
+
*/
|
|
56
|
+
function getAskpassScript(): string {
|
|
57
|
+
if (sharedAskpassPath && fs.existsSync(sharedAskpassPath)) {
|
|
58
|
+
return sharedAskpassPath;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const tmpDir = os.tmpdir();
|
|
62
|
+
if (IS_WINDOWS) {
|
|
63
|
+
const p = path.join(tmpDir, 'synthos-askpass.bat');
|
|
64
|
+
// %SYNTHOS_SSH_PASS_VAR% holds the NAME of the var containing the password
|
|
65
|
+
// We use delayed expansion to read it indirectly
|
|
66
|
+
fs.writeFileSync(p,
|
|
67
|
+
'@echo off\r\n' +
|
|
68
|
+
'setlocal enabledelayedexpansion\r\n' +
|
|
69
|
+
'echo !%SYNTHOS_SSH_PASS_VAR%!\r\n',
|
|
70
|
+
{ mode: 0o700 }
|
|
71
|
+
);
|
|
72
|
+
sharedAskpassPath = p;
|
|
73
|
+
} else {
|
|
74
|
+
const p = path.join(tmpDir, 'synthos-askpass.sh');
|
|
75
|
+
fs.writeFileSync(p,
|
|
76
|
+
'#!/bin/sh\n' +
|
|
77
|
+
'eval echo \\$"$SYNTHOS_SSH_PASS_VAR"\n',
|
|
78
|
+
{ mode: 0o700 }
|
|
79
|
+
);
|
|
80
|
+
sharedAskpassPath = p;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return sharedAskpassPath;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Module-level tunnel pool
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
const tunnels = new Map<string, TunnelEntry>();
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Public API
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Start an SSH tunnel for the given agent. Resolves once the tunnel appears
|
|
98
|
+
* ready (local port probe succeeds or delay elapses).
|
|
99
|
+
*/
|
|
100
|
+
export async function startTunnel(agentId: string, config: SshTunnelConfig): Promise<void> {
|
|
101
|
+
// If already running, nothing to do
|
|
102
|
+
const existing = tunnels.get(agentId);
|
|
103
|
+
if (existing?.running) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const entry: TunnelEntry = existing ?? {
|
|
108
|
+
agentId,
|
|
109
|
+
config,
|
|
110
|
+
process: null,
|
|
111
|
+
running: false,
|
|
112
|
+
reconnecting: false,
|
|
113
|
+
intentionalStop: false,
|
|
114
|
+
reconnectTimer: null,
|
|
115
|
+
reconnectAttempts: 0,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
entry.config = config;
|
|
119
|
+
entry.intentionalStop = false;
|
|
120
|
+
tunnels.set(agentId, entry);
|
|
121
|
+
|
|
122
|
+
return spawnTunnel(entry);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Stop the SSH tunnel for the given agent.
|
|
127
|
+
*/
|
|
128
|
+
export function stopTunnel(agentId: string): void {
|
|
129
|
+
const entry = tunnels.get(agentId);
|
|
130
|
+
if (!entry) return;
|
|
131
|
+
|
|
132
|
+
entry.intentionalStop = true;
|
|
133
|
+
|
|
134
|
+
if (entry.reconnectTimer) {
|
|
135
|
+
clearTimeout(entry.reconnectTimer);
|
|
136
|
+
entry.reconnectTimer = null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (entry.process) {
|
|
140
|
+
entry.process.kill();
|
|
141
|
+
entry.process = null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
entry.running = false;
|
|
145
|
+
entry.reconnecting = false;
|
|
146
|
+
tunnels.delete(agentId);
|
|
147
|
+
console.log(`[SSH Tunnel] Stopped tunnel for agent "${agentId}"`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get tunnel status without starting it.
|
|
152
|
+
*/
|
|
153
|
+
export function getTunnelStatus(agentId: string): { running: boolean; reconnecting: boolean } {
|
|
154
|
+
const entry = tunnels.get(agentId);
|
|
155
|
+
return {
|
|
156
|
+
running: entry?.running ?? false,
|
|
157
|
+
reconnecting: entry?.reconnecting ?? false,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Internal helpers
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Try to extract the local port from the SSH command.
|
|
167
|
+
* Looks for -L <localPort>:<host>:<remotePort> pattern.
|
|
168
|
+
*/
|
|
169
|
+
function extractLocalPort(command: string): number | null {
|
|
170
|
+
const match = command.match(/-L\s+(\d+):/);
|
|
171
|
+
return match ? parseInt(match[1], 10) : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Probe a local TCP port to check if it's accepting connections.
|
|
176
|
+
*/
|
|
177
|
+
function probePort(port: number, timeoutMs: number = 1000): Promise<boolean> {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
const socket = new net.Socket();
|
|
180
|
+
const timer = setTimeout(() => {
|
|
181
|
+
socket.destroy();
|
|
182
|
+
resolve(false);
|
|
183
|
+
}, timeoutMs);
|
|
184
|
+
|
|
185
|
+
socket.connect(port, '127.0.0.1', () => {
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
socket.destroy();
|
|
188
|
+
resolve(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
socket.on('error', () => {
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
socket.destroy();
|
|
194
|
+
resolve(false);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Spawn the SSH process and wait for the tunnel to be ready.
|
|
201
|
+
*
|
|
202
|
+
* Password delivery: SSH does NOT read passwords from stdin — it opens the
|
|
203
|
+
* TTY directly. We use a shared askpass script that reads the password from
|
|
204
|
+
* a per-tunnel env var. The password only lives in process memory (the env
|
|
205
|
+
* of the child process), never on disk.
|
|
206
|
+
*/
|
|
207
|
+
function spawnTunnel(entry: TunnelEntry): Promise<void> {
|
|
208
|
+
return new Promise<void>((resolve, reject) => {
|
|
209
|
+
console.log(`[SSH Tunnel] Starting tunnel for agent "${entry.agentId}": ${entry.config.command}`);
|
|
210
|
+
|
|
211
|
+
let settled = false;
|
|
212
|
+
|
|
213
|
+
function settle(err?: Error): void {
|
|
214
|
+
if (settled) return;
|
|
215
|
+
settled = true;
|
|
216
|
+
if (err) reject(err);
|
|
217
|
+
else resolve();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const askpassPath = getAskpassScript();
|
|
222
|
+
|
|
223
|
+
// Per-tunnel env var name so concurrent tunnels don't collide
|
|
224
|
+
const passVarName = `SYNTHOS_SSHPW_${randomBytes(4).toString('hex')}`;
|
|
225
|
+
|
|
226
|
+
// Build args from the user's command, injecting StrictHostKeyChecking=no
|
|
227
|
+
// to avoid the interactive host key confirmation prompt
|
|
228
|
+
const parts = entry.config.command.trim().split(/\s+/);
|
|
229
|
+
const cmd = parts.shift()!;
|
|
230
|
+
const args = [
|
|
231
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
232
|
+
'-o', 'UserKnownHostsFile=' + (IS_WINDOWS ? 'NUL' : '/dev/null'),
|
|
233
|
+
...parts,
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
const env: Record<string, string> = {
|
|
237
|
+
...process.env as Record<string, string>,
|
|
238
|
+
SSH_ASKPASS: askpassPath,
|
|
239
|
+
SSH_ASKPASS_REQUIRE: 'force',
|
|
240
|
+
// The askpass script reads this var name, then echoes its value
|
|
241
|
+
SYNTHOS_SSH_PASS_VAR: passVarName,
|
|
242
|
+
[passVarName]: entry.config.password,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// On Unix, DISPLAY must be set for SSH_ASKPASS to be used (fallback for older SSH)
|
|
246
|
+
if (!IS_WINDOWS) {
|
|
247
|
+
env.DISPLAY = env.DISPLAY || 'dummy:0';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
console.log(`[SSH Tunnel] Spawning: ${cmd} ${args.join(' ')}`);
|
|
251
|
+
|
|
252
|
+
const child = spawn(cmd, args, {
|
|
253
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
254
|
+
env,
|
|
255
|
+
// Detach stdin from the parent's TTY so SSH uses SSH_ASKPASS
|
|
256
|
+
detached: !IS_WINDOWS,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
entry.process = child;
|
|
260
|
+
|
|
261
|
+
// Log all output for diagnostics
|
|
262
|
+
const handleOutput = (source: string) => (data: Buffer) => {
|
|
263
|
+
const text = data.toString().trim();
|
|
264
|
+
if (text) {
|
|
265
|
+
console.log(`[SSH Tunnel] [${entry.agentId}] ${source}: ${text}`);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
child.stdout?.on('data', handleOutput('stdout'));
|
|
270
|
+
child.stderr?.on('data', handleOutput('stderr'));
|
|
271
|
+
|
|
272
|
+
child.on('error', (err) => {
|
|
273
|
+
console.error(`[SSH Tunnel] Spawn error for agent "${entry.agentId}":`, err.message);
|
|
274
|
+
entry.running = false;
|
|
275
|
+
entry.process = null;
|
|
276
|
+
settle(err);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
child.on('exit', (code, signal) => {
|
|
280
|
+
const wasRunning = entry.running;
|
|
281
|
+
entry.running = false;
|
|
282
|
+
entry.process = null;
|
|
283
|
+
|
|
284
|
+
console.log(`[SSH Tunnel] Process exited for agent "${entry.agentId}" (code=${code}, signal=${signal})`);
|
|
285
|
+
|
|
286
|
+
// If we never settled (tunnel never came up), reject
|
|
287
|
+
settle(new Error(`SSH tunnel exited before becoming ready (code=${code})`));
|
|
288
|
+
|
|
289
|
+
// Auto-reconnect if the tunnel was running and this wasn't intentional
|
|
290
|
+
if (wasRunning && !entry.intentionalStop) {
|
|
291
|
+
scheduleReconnect(entry);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Wait for the tunnel to be ready by probing the local port
|
|
296
|
+
const localPort = extractLocalPort(entry.config.command);
|
|
297
|
+
if (localPort) {
|
|
298
|
+
waitForPort(entry, localPort, settle);
|
|
299
|
+
} else {
|
|
300
|
+
// No port to probe — use delay heuristic
|
|
301
|
+
setTimeout(() => {
|
|
302
|
+
if (entry.process && !entry.intentionalStop) {
|
|
303
|
+
entry.running = true;
|
|
304
|
+
entry.reconnecting = false;
|
|
305
|
+
entry.reconnectAttempts = 0;
|
|
306
|
+
console.log(`[SSH Tunnel] Tunnel ready (delay heuristic) for agent "${entry.agentId}"`);
|
|
307
|
+
settle();
|
|
308
|
+
}
|
|
309
|
+
}, TUNNEL_READY_PROBE_DELAY_MS);
|
|
310
|
+
}
|
|
311
|
+
} catch (err) {
|
|
312
|
+
entry.running = false;
|
|
313
|
+
settle(err instanceof Error ? err : new Error(String(err)));
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Poll for the local port to accept connections, then declare the tunnel ready.
|
|
320
|
+
*/
|
|
321
|
+
function waitForPort(entry: TunnelEntry, port: number, settle: (err?: Error) => void): void {
|
|
322
|
+
let attempts = 0;
|
|
323
|
+
const maxAttempts = 15; // 15 * 1000ms = 15s max (SSH + password can take a few seconds)
|
|
324
|
+
const intervalMs = 1000;
|
|
325
|
+
|
|
326
|
+
const timer = setInterval(async () => {
|
|
327
|
+
attempts++;
|
|
328
|
+
if (entry.intentionalStop || !entry.process) {
|
|
329
|
+
clearInterval(timer);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const ok = await probePort(port);
|
|
334
|
+
if (ok) {
|
|
335
|
+
clearInterval(timer);
|
|
336
|
+
entry.running = true;
|
|
337
|
+
entry.reconnecting = false;
|
|
338
|
+
entry.reconnectAttempts = 0;
|
|
339
|
+
console.log(`[SSH Tunnel] Tunnel ready (port ${port} open) for agent "${entry.agentId}"`);
|
|
340
|
+
settle();
|
|
341
|
+
} else if (attempts >= maxAttempts) {
|
|
342
|
+
clearInterval(timer);
|
|
343
|
+
// Declare ready anyway — the port might only open on first inbound connection
|
|
344
|
+
entry.running = true;
|
|
345
|
+
entry.reconnecting = false;
|
|
346
|
+
entry.reconnectAttempts = 0;
|
|
347
|
+
console.log(`[SSH Tunnel] Tunnel assumed ready (port probe timed out) for agent "${entry.agentId}"`);
|
|
348
|
+
settle();
|
|
349
|
+
}
|
|
350
|
+
}, intervalMs);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Schedule an automatic reconnect with exponential backoff.
|
|
355
|
+
*/
|
|
356
|
+
function scheduleReconnect(entry: TunnelEntry): void {
|
|
357
|
+
if (entry.reconnectTimer || entry.intentionalStop) return;
|
|
358
|
+
|
|
359
|
+
entry.reconnecting = true;
|
|
360
|
+
entry.reconnectAttempts++;
|
|
361
|
+
|
|
362
|
+
const delay = Math.min(
|
|
363
|
+
BASE_RECONNECT_DELAY_MS * Math.pow(2, entry.reconnectAttempts - 1),
|
|
364
|
+
MAX_RECONNECT_DELAY_MS
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
console.log(`[SSH Tunnel] Scheduling reconnect for agent "${entry.agentId}" in ${delay}ms (attempt ${entry.reconnectAttempts})`);
|
|
368
|
+
|
|
369
|
+
entry.reconnectTimer = setTimeout(async () => {
|
|
370
|
+
entry.reconnectTimer = null;
|
|
371
|
+
if (entry.intentionalStop) return;
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
await spawnTunnel(entry);
|
|
375
|
+
console.log(`[SSH Tunnel] Reconnected tunnel for agent "${entry.agentId}"`);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
console.error(`[SSH Tunnel] Reconnect failed for agent "${entry.agentId}":`,
|
|
378
|
+
err instanceof Error ? err.message : err);
|
|
379
|
+
// Schedule another attempt
|
|
380
|
+
if (!entry.intentionalStop) {
|
|
381
|
+
scheduleReconnect(entry);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}, delay);
|
|
385
|
+
}
|