@agentbean/daemon 0.1.35 → 0.2.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/dist/apps/daemon-next/src/bin.d.ts +2 -0
- package/dist/apps/daemon-next/src/bin.js +6 -0
- package/dist/apps/daemon-next/src/cli.d.ts +26 -0
- package/dist/apps/daemon-next/src/cli.js +124 -0
- package/dist/apps/daemon-next/src/executor.d.ts +6 -0
- package/dist/apps/daemon-next/src/executor.js +51 -0
- package/dist/apps/daemon-next/src/index.d.ts +60 -0
- package/dist/apps/daemon-next/src/index.js +87 -0
- package/dist/apps/daemon-next/src/scanner.d.ts +15 -0
- package/dist/apps/daemon-next/src/scanner.js +94 -0
- package/dist/packages/contracts/src/agent.d.ts +69 -0
- package/dist/packages/contracts/src/agent.js +4 -0
- package/dist/packages/contracts/src/auth.d.ts +20 -0
- package/dist/packages/contracts/src/auth.js +1 -0
- package/dist/packages/contracts/src/channel.d.ts +58 -0
- package/dist/packages/contracts/src/channel.js +1 -0
- package/dist/packages/contracts/src/common.d.ts +17 -0
- package/dist/packages/contracts/src/common.js +27 -0
- package/dist/packages/contracts/src/device.d.ts +27 -0
- package/dist/packages/contracts/src/device.js +1 -0
- package/dist/packages/contracts/src/dispatch.d.ts +46 -0
- package/dist/packages/contracts/src/dispatch.js +1 -0
- package/dist/packages/contracts/src/index.d.ts +9 -0
- package/dist/packages/contracts/src/index.js +9 -0
- package/dist/packages/contracts/src/message.d.ts +20 -0
- package/dist/packages/contracts/src/message.js +1 -0
- package/dist/packages/contracts/src/socket.d.ts +74 -0
- package/dist/packages/contracts/src/socket.js +74 -0
- package/dist/packages/contracts/src/team.d.ts +13 -0
- package/dist/packages/contracts/src/team.js +1 -0
- package/package.json +14 -25
- package/README.md +0 -158
- package/dist/adapters/adapter.js +0 -9
- package/dist/adapters/claude-code.js +0 -83
- package/dist/adapters/codex.js +0 -280
- package/dist/adapters/factory.js +0 -38
- package/dist/adapters/hermes.js +0 -178
- package/dist/adapters/openclaw.js +0 -129
- package/dist/agent-instance.js +0 -181
- package/dist/auth-store.js +0 -44
- package/dist/bin.js +0 -6
- package/dist/config.js +0 -148
- package/dist/connection.js +0 -211
- package/dist/device-daemon.js +0 -530
- package/dist/index.js +0 -368
- package/dist/log.js +0 -7
- package/dist/post-process.js +0 -177
- package/dist/profile-paths.js +0 -39
- package/dist/sandbox.js +0 -53
- package/dist/scanner.js +0 -423
- package/dist/uploader.js +0 -46
- package/dist/workspace-manager.js +0 -196
- package/dist/workspace-sync.js +0 -69
package/dist/device-daemon.js
DELETED
|
@@ -1,530 +0,0 @@
|
|
|
1
|
-
import { io } from 'socket.io-client';
|
|
2
|
-
import { execFile } from 'node:child_process';
|
|
3
|
-
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { basename, isAbsolute, dirname } from 'node:path';
|
|
5
|
-
import { promisify } from 'node:util';
|
|
6
|
-
import { logger } from './log.js';
|
|
7
|
-
import { AgentInstance } from './agent-instance.js';
|
|
8
|
-
import { pickAdapter } from './adapters/factory.js';
|
|
9
|
-
import { scanRuntimes, scanAgentOSAgents, scanLocalAgents, collectSystemInfo } from './scanner.js';
|
|
10
|
-
import { syncWorkspaceArtifacts } from './workspace-sync.js';
|
|
11
|
-
import { scanCacheFile } from './profile-paths.js';
|
|
12
|
-
const execFileAsync = promisify(execFile);
|
|
13
|
-
function errorMessage(err) {
|
|
14
|
-
if (err instanceof Error && err.message)
|
|
15
|
-
return err.message;
|
|
16
|
-
if (typeof err === 'string' && err.trim())
|
|
17
|
-
return err;
|
|
18
|
-
try {
|
|
19
|
-
const serialized = JSON.stringify(err);
|
|
20
|
-
if (serialized && serialized !== '{}')
|
|
21
|
-
return serialized;
|
|
22
|
-
}
|
|
23
|
-
catch { }
|
|
24
|
-
return 'unknown error';
|
|
25
|
-
}
|
|
26
|
-
function agentSlug(name) {
|
|
27
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
28
|
-
}
|
|
29
|
-
function scannedAgentId(deviceId, name) {
|
|
30
|
-
return `scan-${deviceId}-${agentSlug(name)}`;
|
|
31
|
-
}
|
|
32
|
-
function normalizeAdapterKind(kind) {
|
|
33
|
-
const normalized = (kind ?? '').trim().toLowerCase().replace(/[_\s]+/g, '-');
|
|
34
|
-
if (normalized === 'claude' || normalized === 'claude-code')
|
|
35
|
-
return 'claude-code';
|
|
36
|
-
if (normalized === 'codex' || normalized === 'codex-cli')
|
|
37
|
-
return 'codex';
|
|
38
|
-
if (normalized === 'kimi' || normalized === 'kimi-cli')
|
|
39
|
-
return 'kimi-cli';
|
|
40
|
-
return normalized;
|
|
41
|
-
}
|
|
42
|
-
function runtimeScoreForCustomAgent(runtime, custom) {
|
|
43
|
-
if (!runtime.installed || !runtime.command?.trim())
|
|
44
|
-
return 0;
|
|
45
|
-
const runtimeCommand = runtime.command.trim();
|
|
46
|
-
const customCommand = custom.command?.trim() ?? '';
|
|
47
|
-
if (runtimeCommand && customCommand && runtimeCommand === customCommand)
|
|
48
|
-
return 100;
|
|
49
|
-
const runtimeBase = basename(runtimeCommand).toLowerCase();
|
|
50
|
-
const customBase = customCommand ? basename(customCommand).toLowerCase() : '';
|
|
51
|
-
if (runtimeBase && customBase && runtimeBase === customBase)
|
|
52
|
-
return 90;
|
|
53
|
-
const runtimeKind = normalizeAdapterKind(runtime.adapterKind);
|
|
54
|
-
const customKind = normalizeAdapterKind(custom.adapterKind);
|
|
55
|
-
if (runtimeKind === 'kimi-cli' && customKind === 'codex' && customCommand.toLowerCase().includes('kimi'))
|
|
56
|
-
return 85;
|
|
57
|
-
if (runtimeKind && customKind && runtimeKind === customKind)
|
|
58
|
-
return 70;
|
|
59
|
-
return 0;
|
|
60
|
-
}
|
|
61
|
-
export function resolveCustomAgentRuntime(custom, runtimes) {
|
|
62
|
-
const configured = custom.command?.trim() ?? '';
|
|
63
|
-
const configuredAbsoluteExists = configured && isAbsolute(configured) && existsSync(configured);
|
|
64
|
-
const bestRuntime = [...runtimes]
|
|
65
|
-
.map((runtime) => ({ runtime, score: runtimeScoreForCustomAgent(runtime, custom) }))
|
|
66
|
-
.filter((candidate) => candidate.score > 0)
|
|
67
|
-
.sort((a, b) => b.score - a.score)[0]?.runtime;
|
|
68
|
-
if (configuredAbsoluteExists) {
|
|
69
|
-
return { command: configured, runtime: bestRuntime };
|
|
70
|
-
}
|
|
71
|
-
if (bestRuntime?.command?.trim()) {
|
|
72
|
-
return { command: bestRuntime.command.trim(), runtime: bestRuntime };
|
|
73
|
-
}
|
|
74
|
-
if (configured && isAbsolute(configured) && !configuredAbsoluteExists) {
|
|
75
|
-
const fallback = basename(configured).trim();
|
|
76
|
-
if (fallback)
|
|
77
|
-
return { command: fallback };
|
|
78
|
-
}
|
|
79
|
-
if (!configured && normalizeAdapterKind(custom.adapterKind) === 'codex') {
|
|
80
|
-
return { command: 'codex' };
|
|
81
|
-
}
|
|
82
|
-
return { command: configured };
|
|
83
|
-
}
|
|
84
|
-
export function nativeDirectoryPickerCommands(platform = process.platform) {
|
|
85
|
-
if (platform === 'darwin') {
|
|
86
|
-
return [{
|
|
87
|
-
command: 'osascript',
|
|
88
|
-
args: [
|
|
89
|
-
'-e',
|
|
90
|
-
'POSIX path of (choose folder with prompt "选择项目目录" default location (path to home folder))',
|
|
91
|
-
],
|
|
92
|
-
}];
|
|
93
|
-
}
|
|
94
|
-
if (platform === 'win32') {
|
|
95
|
-
return [{
|
|
96
|
-
command: 'powershell.exe',
|
|
97
|
-
args: [
|
|
98
|
-
'-NoProfile',
|
|
99
|
-
'-STA',
|
|
100
|
-
'-Command',
|
|
101
|
-
'Add-Type -AssemblyName System.Windows.Forms; $dialog = New-Object System.Windows.Forms.FolderBrowserDialog; if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $dialog.SelectedPath }',
|
|
102
|
-
],
|
|
103
|
-
}];
|
|
104
|
-
}
|
|
105
|
-
return [
|
|
106
|
-
{ command: 'zenity', args: ['--file-selection', '--directory', '--title=选择项目目录'] },
|
|
107
|
-
{ command: 'kdialog', args: ['--getexistingdirectory', '.', '选择项目目录'] },
|
|
108
|
-
];
|
|
109
|
-
}
|
|
110
|
-
function isMissingCommandError(err) {
|
|
111
|
-
return err?.code === 'ENOENT';
|
|
112
|
-
}
|
|
113
|
-
function isDirectoryPickerCancel(err) {
|
|
114
|
-
const message = `${err?.message ?? ''}\n${err?.stderr ?? ''}`;
|
|
115
|
-
return err?.code === 1 || /cancel|canceled|cancelled|User canceled|No file selected/i.test(message);
|
|
116
|
-
}
|
|
117
|
-
export async function selectNativeDirectory(commands = nativeDirectoryPickerCommands()) {
|
|
118
|
-
let lastError = null;
|
|
119
|
-
for (const cmd of commands) {
|
|
120
|
-
try {
|
|
121
|
-
const { stdout } = await execFileAsync(cmd.command, cmd.args, { timeout: 120_000 });
|
|
122
|
-
const selected = stdout.trim();
|
|
123
|
-
if (selected)
|
|
124
|
-
return selected;
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
catch (err) {
|
|
128
|
-
if (isMissingCommandError(err)) {
|
|
129
|
-
lastError = err;
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (isDirectoryPickerCancel(err))
|
|
133
|
-
return null;
|
|
134
|
-
throw err;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
throw new Error(lastError ? `directory picker command not available: ${errorMessage(lastError)}` : 'directory picker command not available');
|
|
138
|
-
}
|
|
139
|
-
function isRuntimeEntry(entry) {
|
|
140
|
-
return entry.category === 'executor-hosted' &&
|
|
141
|
-
['codex', 'claude-code', 'kimi-cli', 'Kimi-cli'].includes(entry.adapterKind);
|
|
142
|
-
}
|
|
143
|
-
function splitLegacyCache(entries) {
|
|
144
|
-
const agents = [];
|
|
145
|
-
const runtimes = [];
|
|
146
|
-
for (const entry of entries) {
|
|
147
|
-
if (isRuntimeEntry(entry)) {
|
|
148
|
-
runtimes.push({
|
|
149
|
-
name: entry.name,
|
|
150
|
-
adapterKind: entry.adapterKind,
|
|
151
|
-
command: entry.command,
|
|
152
|
-
installed: Boolean(entry.command),
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
agents.push(entry);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return { agents, runtimes };
|
|
160
|
-
}
|
|
161
|
-
function loadCache(profileId) {
|
|
162
|
-
try {
|
|
163
|
-
const cacheFile = scanCacheFile(profileId);
|
|
164
|
-
if (!existsSync(cacheFile))
|
|
165
|
-
return null;
|
|
166
|
-
const parsed = JSON.parse(readFileSync(cacheFile, 'utf-8'));
|
|
167
|
-
if (Array.isArray(parsed))
|
|
168
|
-
return splitLegacyCache(parsed);
|
|
169
|
-
return {
|
|
170
|
-
agents: parsed.agents ?? [],
|
|
171
|
-
runtimes: parsed.runtimes ?? [],
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
function saveCache(payload, profileId) {
|
|
179
|
-
try {
|
|
180
|
-
const cacheFile = scanCacheFile(profileId);
|
|
181
|
-
const cacheDir = dirname(cacheFile);
|
|
182
|
-
if (!existsSync(cacheDir))
|
|
183
|
-
mkdirSync(cacheDir, { recursive: true });
|
|
184
|
-
writeFileSync(cacheFile, JSON.stringify(payload, null, 2));
|
|
185
|
-
}
|
|
186
|
-
catch (err) {
|
|
187
|
-
logger.warn({ err: err?.message }, 'failed to save scan cache');
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
export function createDeviceSocketOptions(input) {
|
|
191
|
-
return {
|
|
192
|
-
auth: {
|
|
193
|
-
token: input.token,
|
|
194
|
-
deviceId: input.deviceId,
|
|
195
|
-
machineId: input.machineId,
|
|
196
|
-
profileId: input.profileId,
|
|
197
|
-
networkId: input.networkId,
|
|
198
|
-
agents: input.agents,
|
|
199
|
-
systemInfo: input.systemInfo,
|
|
200
|
-
daemonVersion: input.systemInfo.daemonVersion,
|
|
201
|
-
protocolVersion: 1,
|
|
202
|
-
capabilities: {
|
|
203
|
-
customAgentDispatch: true,
|
|
204
|
-
directoryPicker: true,
|
|
205
|
-
},
|
|
206
|
-
},
|
|
207
|
-
transports: ['websocket', 'polling'],
|
|
208
|
-
rememberUpgrade: true,
|
|
209
|
-
reconnection: true,
|
|
210
|
-
reconnectionDelay: 1_000,
|
|
211
|
-
reconnectionDelayMax: 10_000,
|
|
212
|
-
timeout: 20_000,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
async function scanAll() {
|
|
216
|
-
const [runtimes, agentos, local] = await Promise.all([
|
|
217
|
-
scanRuntimes(),
|
|
218
|
-
scanAgentOSAgents(),
|
|
219
|
-
scanLocalAgents(),
|
|
220
|
-
]);
|
|
221
|
-
const agents = [];
|
|
222
|
-
const runtimeResults = runtimes.filter((rt) => rt.installed);
|
|
223
|
-
// AgentOS + standalone (from gateway and filesystem scans)
|
|
224
|
-
const seen = new Set();
|
|
225
|
-
for (const ag of agentos) {
|
|
226
|
-
if (!seen.has(ag.command)) {
|
|
227
|
-
seen.add(ag.command);
|
|
228
|
-
agents.push({ ...ag, source: 'scanned' });
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
for (const ag of local) {
|
|
232
|
-
if (!seen.has(ag.command)) {
|
|
233
|
-
seen.add(ag.command);
|
|
234
|
-
agents.push({ ...ag, source: 'scanned' });
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return { agents, runtimes: runtimeResults };
|
|
238
|
-
}
|
|
239
|
-
export function createDeviceDaemon(cfg, agents) {
|
|
240
|
-
let socket = null;
|
|
241
|
-
let heartbeatTimer = null;
|
|
242
|
-
let rescanTimer = null;
|
|
243
|
-
let workspaceSyncTimer = null;
|
|
244
|
-
const RESCAN_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
245
|
-
const WORKSPACE_SYNC_INTERVAL_MS = 2 * 60 * 1000;
|
|
246
|
-
const queues = new Map();
|
|
247
|
-
const httpBase = cfg.server.url.replace(/\/agent$/, '');
|
|
248
|
-
let firstConnect = true;
|
|
249
|
-
const systemInfo = collectSystemInfo();
|
|
250
|
-
let latestRuntimes = [];
|
|
251
|
-
const publicAgents = Array.from(agents.values())
|
|
252
|
-
.filter((a) => a.visibility === 'public')
|
|
253
|
-
.map((a) => a.publicMeta);
|
|
254
|
-
function emitRegister(sock, payload) {
|
|
255
|
-
latestRuntimes = payload.runtimes.filter((runtime) => runtime.installed);
|
|
256
|
-
if (payload.runtimes.length > 0) {
|
|
257
|
-
sock.emit('device:register-runtimes', { runtimes: payload.runtimes }, (ack) => {
|
|
258
|
-
if (!ack?.ok)
|
|
259
|
-
logger.warn({ error: ack?.error }, 'failed to register runtimes');
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
if (payload.agents.length === 0)
|
|
263
|
-
return;
|
|
264
|
-
for (const ag of payload.agents) {
|
|
265
|
-
const id = scannedAgentId(cfg.deviceId, ag.name);
|
|
266
|
-
if (agents.has(id))
|
|
267
|
-
continue;
|
|
268
|
-
const entry = {
|
|
269
|
-
id,
|
|
270
|
-
name: ag.name,
|
|
271
|
-
role: ag.category === 'executor-hosted' ? 'executor-agent' : 'gateway-agent',
|
|
272
|
-
category: ag.category,
|
|
273
|
-
adapter: {
|
|
274
|
-
kind: ag.adapterKind,
|
|
275
|
-
command: ag.command,
|
|
276
|
-
args: ag.args ?? [],
|
|
277
|
-
cwd: ag.cwd,
|
|
278
|
-
},
|
|
279
|
-
visibility: 'public',
|
|
280
|
-
};
|
|
281
|
-
try {
|
|
282
|
-
agents.set(id, new AgentInstance(entry, pickAdapter(entry.adapter)));
|
|
283
|
-
logger.info({ id, kind: entry.adapter.kind }, 'scanned agent instance created');
|
|
284
|
-
}
|
|
285
|
-
catch (err) {
|
|
286
|
-
logger.warn({ id, err: errorMessage(err) }, 'failed to create scanned agent instance');
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
sock.emit('device:register-agents', { agents: payload.agents }, (ack) => {
|
|
290
|
-
if (ack?.ok) {
|
|
291
|
-
logger.info({ count: ack.agents?.length }, 'scanned agents registered');
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
logger.warn({ error: ack?.error }, 'failed to register scanned agents');
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
async function scanAndRegister(sock, useCache) {
|
|
299
|
-
if (useCache) {
|
|
300
|
-
const cached = loadCache(cfg.profileId);
|
|
301
|
-
if (cached) {
|
|
302
|
-
logger.info({ count: cached.agents.length + cached.runtimes.length }, 'using cached scan results');
|
|
303
|
-
emitRegister(sock, cached);
|
|
304
|
-
// Background refresh — only emit if results differ
|
|
305
|
-
scanAll().then((fresh) => {
|
|
306
|
-
saveCache(fresh, cfg.profileId);
|
|
307
|
-
const cachedKey = JSON.stringify([
|
|
308
|
-
...cached.agents.map((a) => a.command),
|
|
309
|
-
...cached.runtimes.map((rt) => rt.command),
|
|
310
|
-
].sort());
|
|
311
|
-
const freshKey = JSON.stringify([
|
|
312
|
-
...fresh.agents.map((a) => a.command),
|
|
313
|
-
...fresh.runtimes.map((rt) => rt.command),
|
|
314
|
-
].sort());
|
|
315
|
-
if (cachedKey !== freshKey) {
|
|
316
|
-
logger.info({ count: fresh.agents.length + fresh.runtimes.length }, 'scan results changed, updating');
|
|
317
|
-
emitRegister(sock, fresh);
|
|
318
|
-
}
|
|
319
|
-
}).catch((err) => {
|
|
320
|
-
logger.warn({ err: err?.message }, 'background scan failed');
|
|
321
|
-
});
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
// Full scan (no cache or cache miss)
|
|
326
|
-
try {
|
|
327
|
-
const scanned = await scanAll();
|
|
328
|
-
saveCache(scanned, cfg.profileId);
|
|
329
|
-
emitRegister(sock, scanned);
|
|
330
|
-
}
|
|
331
|
-
catch (err) {
|
|
332
|
-
logger.error({ err: err?.message }, 'scan failed');
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
return {
|
|
336
|
-
async start() {
|
|
337
|
-
const agentUrl = cfg.server.url.endsWith('/agent') ? cfg.server.url : cfg.server.url + '/agent';
|
|
338
|
-
socket = io(agentUrl, createDeviceSocketOptions({
|
|
339
|
-
token: cfg.server.token,
|
|
340
|
-
deviceId: cfg.deviceId,
|
|
341
|
-
machineId: cfg.machineId,
|
|
342
|
-
profileId: cfg.profileId,
|
|
343
|
-
networkId: cfg.networkId,
|
|
344
|
-
agents: publicAgents,
|
|
345
|
-
systemInfo,
|
|
346
|
-
}));
|
|
347
|
-
socket.on('connect', () => {
|
|
348
|
-
const reconnecting = !firstConnect;
|
|
349
|
-
firstConnect = false;
|
|
350
|
-
logger.info({ deviceId: cfg.deviceId, sid: socket.id, reconnecting }, 'device daemon connected');
|
|
351
|
-
socket.emit('register');
|
|
352
|
-
// Reconnect: skip scan entirely (server already has our agents)
|
|
353
|
-
// First connect: use cache if available, otherwise full scan
|
|
354
|
-
if (!reconnecting) {
|
|
355
|
-
scanAndRegister(socket, true);
|
|
356
|
-
}
|
|
357
|
-
if (heartbeatTimer)
|
|
358
|
-
clearInterval(heartbeatTimer);
|
|
359
|
-
heartbeatTimer = setInterval(() => {
|
|
360
|
-
socket?.emit('heartbeat');
|
|
361
|
-
}, cfg.heartbeatIntervalMs);
|
|
362
|
-
// Periodic re-scan to update agent availability
|
|
363
|
-
if (rescanTimer)
|
|
364
|
-
clearInterval(rescanTimer);
|
|
365
|
-
rescanTimer = setInterval(() => {
|
|
366
|
-
if (!socket?.connected)
|
|
367
|
-
return;
|
|
368
|
-
scanAndRegister(socket, false);
|
|
369
|
-
}, RESCAN_INTERVAL_MS);
|
|
370
|
-
syncWorkspaceArtifacts({ serverUrl: httpBase, token: cfg.server.token, networkId: cfg.networkId });
|
|
371
|
-
if (workspaceSyncTimer)
|
|
372
|
-
clearInterval(workspaceSyncTimer);
|
|
373
|
-
workspaceSyncTimer = setInterval(() => {
|
|
374
|
-
if (!socket?.connected)
|
|
375
|
-
return;
|
|
376
|
-
syncWorkspaceArtifacts({ serverUrl: httpBase, token: cfg.server.token, networkId: cfg.networkId });
|
|
377
|
-
}, WORKSPACE_SYNC_INTERVAL_MS);
|
|
378
|
-
});
|
|
379
|
-
socket.on('connect_error', (err) => {
|
|
380
|
-
logger.error({ err: err.message }, 'connect_error');
|
|
381
|
-
});
|
|
382
|
-
socket.io.on('reconnect_attempt', (attempt) => {
|
|
383
|
-
logger.info({ attempt }, 'device daemon reconnect attempt');
|
|
384
|
-
});
|
|
385
|
-
socket.io.on('reconnect', (attempt) => {
|
|
386
|
-
logger.info({ attempt }, 'device daemon reconnected');
|
|
387
|
-
});
|
|
388
|
-
socket.io.on('reconnect_error', (err) => {
|
|
389
|
-
logger.warn({ err: errorMessage(err) }, 'device daemon reconnect failed');
|
|
390
|
-
});
|
|
391
|
-
socket.on('dispatch', (req) => {
|
|
392
|
-
let agent = agents.get(req.agentId);
|
|
393
|
-
if (req.customAgent) {
|
|
394
|
-
const custom = req.customAgent;
|
|
395
|
-
const resolvedRuntime = resolveCustomAgentRuntime(custom, latestRuntimes);
|
|
396
|
-
const entry = {
|
|
397
|
-
id: custom.id,
|
|
398
|
-
name: custom.name,
|
|
399
|
-
role: custom.role ?? 'executor-agent',
|
|
400
|
-
category: 'executor-hosted',
|
|
401
|
-
adapter: {
|
|
402
|
-
kind: custom.adapterKind,
|
|
403
|
-
command: resolvedRuntime.command,
|
|
404
|
-
args: custom.args ?? [],
|
|
405
|
-
cwd: custom.cwd ?? undefined,
|
|
406
|
-
workspace: custom.cwd ?? undefined,
|
|
407
|
-
env: custom.env ?? undefined,
|
|
408
|
-
systemPrompt: custom.description ?? undefined,
|
|
409
|
-
},
|
|
410
|
-
visibility: 'public',
|
|
411
|
-
};
|
|
412
|
-
try {
|
|
413
|
-
agent = new AgentInstance(entry, pickAdapter(entry.adapter));
|
|
414
|
-
agents.set(req.agentId, agent);
|
|
415
|
-
logger.info({
|
|
416
|
-
agentId: req.agentId,
|
|
417
|
-
kind: entry.adapter.kind,
|
|
418
|
-
command: entry.adapter.command,
|
|
419
|
-
configuredCommand: custom.command,
|
|
420
|
-
runtimeCommand: resolvedRuntime.runtime?.command,
|
|
421
|
-
cwd: entry.adapter.cwd,
|
|
422
|
-
}, 'custom agent instance created for dispatch');
|
|
423
|
-
}
|
|
424
|
-
catch (err) {
|
|
425
|
-
logger.warn({ agentId: req.agentId, err: errorMessage(err) }, 'failed to create custom dispatch agent');
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
if (!agent) {
|
|
429
|
-
logger.warn({ agentId: req.agentId, requestId: req.requestId }, 'dispatch for unknown agent');
|
|
430
|
-
socket?.emit('error_event', {
|
|
431
|
-
agentId: req.agentId,
|
|
432
|
-
at: Date.now(),
|
|
433
|
-
message: `agent ${req.agentId} not found on this device`,
|
|
434
|
-
scope: 'dispatch',
|
|
435
|
-
requestId: req.requestId,
|
|
436
|
-
});
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
// Serialize dispatches per agent to avoid concurrent adapter usage
|
|
440
|
-
const currentSocket = socket;
|
|
441
|
-
if (!currentSocket) {
|
|
442
|
-
logger.warn({ agentId: req.agentId, requestId: req.requestId }, 'dispatch received but socket is null');
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
const prev = queues.get(req.agentId) ?? Promise.resolve();
|
|
446
|
-
const next = prev.then(async () => {
|
|
447
|
-
await agent.handleDispatch({
|
|
448
|
-
socket: currentSocket,
|
|
449
|
-
req,
|
|
450
|
-
serverUrl: httpBase,
|
|
451
|
-
token: cfg.server.token,
|
|
452
|
-
networkId: req.teamId ?? req.networkId ?? cfg.networkId,
|
|
453
|
-
deviceId: cfg.deviceId,
|
|
454
|
-
});
|
|
455
|
-
}).catch((err) => {
|
|
456
|
-
const message = errorMessage(err);
|
|
457
|
-
logger.error({ err: message, agentId: req.agentId }, 'dispatch queue error');
|
|
458
|
-
currentSocket.emit('error_event', {
|
|
459
|
-
agentId: req.agentId,
|
|
460
|
-
at: Date.now(),
|
|
461
|
-
message,
|
|
462
|
-
scope: 'reply',
|
|
463
|
-
requestId: req.requestId,
|
|
464
|
-
});
|
|
465
|
-
});
|
|
466
|
-
queues.set(req.agentId, next);
|
|
467
|
-
});
|
|
468
|
-
socket.on('dispatch:cancel', (payload) => {
|
|
469
|
-
const targets = payload.agentId ? [payload.agentId] : [...agents.keys()];
|
|
470
|
-
let cancelled = 0;
|
|
471
|
-
for (const agentId of targets) {
|
|
472
|
-
const agent = agents.get(agentId);
|
|
473
|
-
if (!agent)
|
|
474
|
-
continue;
|
|
475
|
-
cancelled += agent.cancelDispatch(payload.requestId);
|
|
476
|
-
}
|
|
477
|
-
logger.info({ agentId: payload.agentId, requestId: payload.requestId, cancelled, reason: payload.reason }, 'dispatch cancel requested');
|
|
478
|
-
});
|
|
479
|
-
socket.on('device:select-directory', async (_payload, ack) => {
|
|
480
|
-
try {
|
|
481
|
-
const selected = await selectNativeDirectory();
|
|
482
|
-
if (!selected) {
|
|
483
|
-
ack?.({ ok: false, error: 'CANCELLED' });
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
ack?.({ ok: true, path: selected });
|
|
487
|
-
}
|
|
488
|
-
catch (err) {
|
|
489
|
-
const message = errorMessage(err);
|
|
490
|
-
logger.warn({ err: message }, 'failed to select directory on device');
|
|
491
|
-
ack?.({ ok: false, error: message });
|
|
492
|
-
}
|
|
493
|
-
});
|
|
494
|
-
socket.on('agents:discover', async () => {
|
|
495
|
-
await scanAndRegister(socket, false);
|
|
496
|
-
});
|
|
497
|
-
socket.on('disconnect', (reason) => {
|
|
498
|
-
logger.warn({ reason }, 'device daemon disconnected');
|
|
499
|
-
if (heartbeatTimer) {
|
|
500
|
-
clearInterval(heartbeatTimer);
|
|
501
|
-
heartbeatTimer = null;
|
|
502
|
-
}
|
|
503
|
-
if (rescanTimer) {
|
|
504
|
-
clearInterval(rescanTimer);
|
|
505
|
-
rescanTimer = null;
|
|
506
|
-
}
|
|
507
|
-
if (workspaceSyncTimer) {
|
|
508
|
-
clearInterval(workspaceSyncTimer);
|
|
509
|
-
workspaceSyncTimer = null;
|
|
510
|
-
}
|
|
511
|
-
});
|
|
512
|
-
},
|
|
513
|
-
async stop() {
|
|
514
|
-
if (heartbeatTimer) {
|
|
515
|
-
clearInterval(heartbeatTimer);
|
|
516
|
-
heartbeatTimer = null;
|
|
517
|
-
}
|
|
518
|
-
if (rescanTimer) {
|
|
519
|
-
clearInterval(rescanTimer);
|
|
520
|
-
rescanTimer = null;
|
|
521
|
-
}
|
|
522
|
-
if (workspaceSyncTimer) {
|
|
523
|
-
clearInterval(workspaceSyncTimer);
|
|
524
|
-
workspaceSyncTimer = null;
|
|
525
|
-
}
|
|
526
|
-
socket?.close();
|
|
527
|
-
socket = null;
|
|
528
|
-
},
|
|
529
|
-
};
|
|
530
|
-
}
|