@openagents-org/agent-launcher 0.2.192 → 0.2.194
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 +32 -2
- package/package.json +1 -1
- package/registry.json +9 -3
- package/src/adapters/antigravity-stream.js +65 -5
- package/src/adapters/base.js +332 -35
- package/src/adapters/claude.js +152 -21
- package/src/adapters/codex.js +37 -3
- package/src/adapters/cursor-node-entry.js +64 -0
- package/src/adapters/cursor.js +31 -1
- package/src/adapters/failure-streak.js +89 -0
- package/src/adapters/hermes.js +354 -58
- package/src/adapters/opencode.js +25 -3
- package/src/adapters/response-artifacts.js +72 -0
- package/src/adapters/thread-history.js +49 -0
- package/src/adapters/workspace-prompt.js +102 -12
- package/src/cli-models.js +244 -0
- package/src/cli.js +69 -13
- package/src/config.js +13 -3
- package/src/daemon.js +183 -50
- package/src/index.js +10 -1
- package/src/installer.js +413 -16
- package/src/log-writer.js +351 -0
- package/src/mcp-server.js +138 -9
- package/src/node-config.js +6 -4
- package/src/postinstall-stub.js +69 -0
- package/src/tools-v11/brief.js +97 -0
- package/src/tools-v11/index.js +28 -0
- package/src/tools-v11/presence-handoff.js +199 -0
- package/src/workspace-client.js +274 -32
package/src/daemon.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
5
6
|
const { execSync, execFileSync } = require('child_process');
|
|
6
7
|
// spawn() here is the WSL bridge from ./wsl: same signature as
|
|
7
8
|
// child_process.spawn, and a straight pass-through unless the resolved CLI
|
|
@@ -10,8 +11,19 @@ const { spawn } = require('./wsl');
|
|
|
10
11
|
const os = require('os');
|
|
11
12
|
const { WorkspaceClient } = require('./workspace-client');
|
|
12
13
|
const { listEndpointModels } = require('./model-list');
|
|
13
|
-
const {
|
|
14
|
+
const { supportsCliModels } = require('./cli-models');
|
|
15
|
+
const { AUTH_MODE_KEY, isCliLogin, modelKeys, stripForCliLogin, typeEnvFor } = require('./env');
|
|
14
16
|
const { getEnhancedEnv, whichBinary, IS_WINDOWS, defaultAgentWorkdir } = require('./paths');
|
|
17
|
+
const { LogWriter } = require('./log-writer');
|
|
18
|
+
|
|
19
|
+
// A signed-in agent's model list is asked for again after this long. What a
|
|
20
|
+
// CLI is offered changes when the CLI is upgraded (a vendor holds new models
|
|
21
|
+
// back from old versions), so the upgrade should show within the hour.
|
|
22
|
+
const CLI_MODELS_MAX_AGE_MS = 3600 * 1000;
|
|
23
|
+
// A sign-in whose CLI has never answered is asked again sooner: the first ask
|
|
24
|
+
// may have failed for a passing reason (a slow start, an update mid-way). A
|
|
25
|
+
// little under the 10-minute sweep, so the next pass retries it.
|
|
26
|
+
const CLI_MODELS_RETRY_MS = 9 * 60 * 1000;
|
|
15
27
|
|
|
16
28
|
/**
|
|
17
29
|
* Mask an API key for display (same shape the workspace backend uses for
|
|
@@ -110,7 +122,7 @@ class Daemon {
|
|
|
110
122
|
this._cmdInterval = null;
|
|
111
123
|
this._nodeHeartbeatInterval = null; // device-level heartbeat (connect-a-node)
|
|
112
124
|
this._displayNameInterval = null; // pulls workspace-side renames back
|
|
113
|
-
this._nodeClients = new Map(); //
|
|
125
|
+
this._nodeClients = new Map(); // node_id -> WorkspaceClient, one per private connection
|
|
114
126
|
this._runningCommands = new Set(); // node-command ids currently executing
|
|
115
127
|
this._runtimes = []; // detected agent runtimes for the node view
|
|
116
128
|
this._runtimesInterval = null;
|
|
@@ -118,6 +130,10 @@ class Daemon {
|
|
|
118
130
|
this._probeInterval = null;
|
|
119
131
|
this._probeStartupTimer = null;
|
|
120
132
|
this._probeInFlight = new Set(); // types being probed right now
|
|
133
|
+
this._cliModels = {}; // sign-in key → { models, askedAt } from the agents' own CLI
|
|
134
|
+
this._cliModelsInFlight = new Set(); // sign-in keys whose CLI is being asked right now
|
|
135
|
+
this._cliModelsStartupTimer = null;
|
|
136
|
+
this._cliModelsInterval = null;
|
|
121
137
|
this._reloadInFlight = null; // serialize concurrent _reload() calls
|
|
122
138
|
}
|
|
123
139
|
|
|
@@ -198,6 +214,98 @@ class Daemon {
|
|
|
198
214
|
this._nodeHeartbeat();
|
|
199
215
|
}
|
|
200
216
|
|
|
217
|
+
// ── Models a signed-in agent's account is offered ──────────────────────
|
|
218
|
+
// An agent on a subscription sign-in has no endpoint whose models can be
|
|
219
|
+
// listed, so its own CLI is asked (see cli-models.js). The answer rides the
|
|
220
|
+
// heartbeat's roster as agents[].cliModels, which puts it in front of the
|
|
221
|
+
// workspace's model picker without a command round trip. Kept in memory
|
|
222
|
+
// only: asking again after a restart costs a CLI start, not a model call.
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Whether this agent runs on its CLI's own sign-in: a type whose CLI can
|
|
226
|
+
* say what the account is offered, calling that CLI's default endpoint,
|
|
227
|
+
* with no key the CLI would be handed instead.
|
|
228
|
+
*/
|
|
229
|
+
_onCliSignIn(a) {
|
|
230
|
+
if (!supportsCliModels(a.type)) return false;
|
|
231
|
+
const endpoint = this._agentEndpoint(a);
|
|
232
|
+
return !endpoint.baseUrl && !endpoint.apiKey;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* One key per sign-in. Agents of a type run one CLI on one account unless
|
|
237
|
+
* their own env says otherwise (a token or a config dir of their own), so
|
|
238
|
+
* those with the same env share an answer: a device with nine Claude agents
|
|
239
|
+
* starts the CLI once, not nine times. The model each runs is left out, it
|
|
240
|
+
* does not change what the account is offered. Hashed, since that env can
|
|
241
|
+
* hold a token.
|
|
242
|
+
*/
|
|
243
|
+
_cliModelsKey(a) {
|
|
244
|
+
const skip = modelKeys(a.type, this.registry);
|
|
245
|
+
const own = Object.entries(a.env || {})
|
|
246
|
+
.filter(([k]) => !skip.has(k))
|
|
247
|
+
.sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0));
|
|
248
|
+
return crypto.createHash('sha256').update(JSON.stringify([a.type, own])).digest('hex');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Ask an agent's CLI for its models via a child `agn models <name> --json`
|
|
253
|
+
* (off the event loop, and on that agent's own env, like a probe).
|
|
254
|
+
* Resolves true when the list it keeps for that sign-in changed.
|
|
255
|
+
*/
|
|
256
|
+
async _listCliModels(a) {
|
|
257
|
+
const key = this._cliModelsKey(a);
|
|
258
|
+
if (this._cliModelsInFlight.has(key)) return false;
|
|
259
|
+
this._cliModelsInFlight.add(key);
|
|
260
|
+
try {
|
|
261
|
+
const r = await this._runAgn(['models', a.name, '--json']);
|
|
262
|
+
let parsed = null;
|
|
263
|
+
try { parsed = JSON.parse(r.stdout.trim()); } catch {}
|
|
264
|
+
const models = parsed && parsed.ok && Array.isArray(parsed.models) ? parsed.models : [];
|
|
265
|
+
const last = this._cliModels[key];
|
|
266
|
+
// A CLI that would not answer keeps the list it last gave: that is
|
|
267
|
+
// still the best answer there is.
|
|
268
|
+
const kept = models.length ? models : ((last && last.models) || null);
|
|
269
|
+
this._cliModels[key] = { models: kept, askedAt: Date.now() };
|
|
270
|
+
return JSON.stringify(kept) !== JSON.stringify((last && last.models) || null);
|
|
271
|
+
} finally {
|
|
272
|
+
this._cliModelsInFlight.delete(key);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Ask in the background for an agent that was just created or reconfigured. */
|
|
277
|
+
_askCliModels(name) {
|
|
278
|
+
let agent = null;
|
|
279
|
+
try { agent = this.config.getAgent(name); } catch {}
|
|
280
|
+
if (!agent || !this._onCliSignIn(agent)) return;
|
|
281
|
+
// A changed list goes out on a heartbeat now, not on the next tick.
|
|
282
|
+
this._listCliModels(agent)
|
|
283
|
+
.then((changed) => (changed ? this._nodeHeartbeat() : undefined))
|
|
284
|
+
.catch(() => {});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Periodic sweep: ask for every sign-in whose list is missing or has aged
|
|
289
|
+
* out, and forget the ones no agent is on any more. Sequential, like the
|
|
290
|
+
* probe sweep: there is no hurry.
|
|
291
|
+
*/
|
|
292
|
+
async _refreshCliModels() {
|
|
293
|
+
const signIns = new Set();
|
|
294
|
+
for (const a of this.config.getAgents()) {
|
|
295
|
+
if (!a.network || !this._onCliSignIn(a)) continue;
|
|
296
|
+
const key = this._cliModelsKey(a);
|
|
297
|
+
if (signIns.has(key)) continue;
|
|
298
|
+
signIns.add(key);
|
|
299
|
+
const last = this._cliModels[key];
|
|
300
|
+
const maxAge = last && last.models ? CLI_MODELS_MAX_AGE_MS : CLI_MODELS_RETRY_MS;
|
|
301
|
+
if (last && Date.now() - last.askedAt < maxAge) continue;
|
|
302
|
+
try { await this._listCliModels(a); } catch {}
|
|
303
|
+
}
|
|
304
|
+
for (const key of Object.keys(this._cliModels)) {
|
|
305
|
+
if (!signIns.has(key)) delete this._cliModels[key];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
201
309
|
/**
|
|
202
310
|
* Heartbeat this device (node) to EVERY workspace it is paired to,
|
|
203
311
|
* independent of any agent, so each workspace's "connected devices" view
|
|
@@ -239,7 +347,7 @@ class Daemon {
|
|
|
239
347
|
if (!info || !info.networkRef || info._credRestartPending) continue;
|
|
240
348
|
if (!this._adapters || !this._adapters[name]) continue;
|
|
241
349
|
let fresh = null;
|
|
242
|
-
try { fresh = this._resolveAgentNetwork(info.networkRef); } catch { /* keep running on the current token */ }
|
|
350
|
+
try { fresh = this._resolveAgentNetwork(info.networkRef, info.connectionId); } catch { /* keep running on the current token */ }
|
|
243
351
|
if (!fresh || !fresh.token || fresh.token === info.credentialToken) continue;
|
|
244
352
|
// Flag on the CURRENT info object: restartAgent replaces it with a fresh
|
|
245
353
|
// one on relaunch, so the flag cannot wedge the watch permanently.
|
|
@@ -247,17 +355,17 @@ class Daemon {
|
|
|
247
355
|
this._log(`${name}: workspace credential for '${info.networkRef}' rotated (device re-paired) — restarting to pick up the new token`);
|
|
248
356
|
this.restartAgent(name).catch((e) => {
|
|
249
357
|
info._credRestartPending = false;
|
|
250
|
-
this._log(`${name}: credential-rotation restart failed: ${e.message}
|
|
358
|
+
this._log(`${name}: credential-rotation restart failed: ${e.message}`, 'warn');
|
|
251
359
|
});
|
|
252
360
|
}
|
|
253
361
|
}
|
|
254
362
|
|
|
255
363
|
/** The WorkspaceClient for one pairing, created on first use and reused. */
|
|
256
364
|
_nodeClientFor(n) {
|
|
257
|
-
let client = this._nodeClients.get(n.
|
|
365
|
+
let client = this._nodeClients.get(n.node_id);
|
|
258
366
|
if (!client) {
|
|
259
367
|
client = new WorkspaceClient(n.endpoint);
|
|
260
|
-
this._nodeClients.set(n.
|
|
368
|
+
this._nodeClients.set(n.node_id, client);
|
|
261
369
|
}
|
|
262
370
|
return client;
|
|
263
371
|
}
|
|
@@ -281,8 +389,8 @@ class Daemon {
|
|
|
281
389
|
// is unreachable, and taking that as the workspace's word unpaired every
|
|
282
390
|
// workspace on the device at once.
|
|
283
391
|
if (_nodeGone(err)) {
|
|
284
|
-
try { nodeCfg.clearPairing(n.workspace_id); } catch {}
|
|
285
|
-
this._nodeClients.delete(n.
|
|
392
|
+
try { nodeCfg.clearPairing(n.workspace_id, n.node_id); } catch {}
|
|
393
|
+
this._nodeClients.delete(n.node_id);
|
|
286
394
|
this._log(`node ${n.node_id} no longer recognized by workspace ${n.workspace_slug || n.workspace_id} — pairing cleared`);
|
|
287
395
|
}
|
|
288
396
|
return; // nothing more to do for this pairing this tick
|
|
@@ -351,7 +459,13 @@ class Daemon {
|
|
|
351
459
|
*/
|
|
352
460
|
_agentOnNodeWorkspace(a, node) {
|
|
353
461
|
if (!a || !a.network || !node) return false;
|
|
354
|
-
|
|
462
|
+
if (a.network !== node.workspace_slug && a.network !== node.workspace_id) return false;
|
|
463
|
+
if (a.node_id) return a.node_id === node.node_id;
|
|
464
|
+
// Legacy agents may bind automatically only when the workspace is unambiguous.
|
|
465
|
+
const candidates = require('./node-config').listPairings().filter(
|
|
466
|
+
(p) => p.workspace_id === node.workspace_id && p.workspace_id,
|
|
467
|
+
);
|
|
468
|
+
return candidates.length <= 1;
|
|
355
469
|
}
|
|
356
470
|
|
|
357
471
|
/**
|
|
@@ -381,6 +495,9 @@ class Daemon {
|
|
|
381
495
|
try { typeEnv = typeEnvFor(a.type, this.envManager.load(a.type) || {}, this.registry, a.env); } catch {}
|
|
382
496
|
const model = (a.env && a.env.LLM_MODEL) || typeEnv.LLM_MODEL || null;
|
|
383
497
|
const apiKey = (a.env && a.env.LLM_API_KEY) || typeEnv.LLM_API_KEY || null;
|
|
498
|
+
// What the agent's own CLI says its signed-in account can run. Null
|
|
499
|
+
// for an agent on a key or a relay, and until its CLI has answered.
|
|
500
|
+
const listed = supportsCliModels(a.type) ? this._cliModels[this._cliModelsKey(a)] : null;
|
|
384
501
|
roster.push({
|
|
385
502
|
name: a.name,
|
|
386
503
|
type: a.type || 'unknown',
|
|
@@ -389,6 +506,7 @@ class Daemon {
|
|
|
389
506
|
workingDir: a.path || null,
|
|
390
507
|
apiKeyMasked: apiKey ? maskApiKey(apiKey) : null,
|
|
391
508
|
baseUrlHost: endpointHost(this._configuredBaseUrl(a, typeEnv)),
|
|
509
|
+
cliModels: listed && listed.models && this._onCliSignIn(a) ? listed.models : null,
|
|
392
510
|
probe: this._probes[a.name] || null,
|
|
393
511
|
});
|
|
394
512
|
}
|
|
@@ -583,7 +701,7 @@ class Daemon {
|
|
|
583
701
|
}
|
|
584
702
|
for (const [key, value] of Object.entries(config)) {
|
|
585
703
|
if (!allowed.has(key)) {
|
|
586
|
-
this._log(`config for '${type}': refused non-env_config key '${key}'
|
|
704
|
+
this._log(`config for '${type}': refused non-env_config key '${key}'`, 'warn');
|
|
587
705
|
continue;
|
|
588
706
|
}
|
|
589
707
|
if (value === null || value === undefined) continue;
|
|
@@ -634,7 +752,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
634
752
|
// pass and no /v1/token/resolve round-trip to fail (the outage class
|
|
635
753
|
// that used to break remote installs).
|
|
636
754
|
const r2 = await this._runAgn([
|
|
637
|
-
'connect', name, '--workspace', n.workspace_slug || n.workspace_id,
|
|
755
|
+
'connect', name, '--workspace', n.workspace_slug || n.workspace_id, '--connection', n.node_id,
|
|
638
756
|
]);
|
|
639
757
|
if (r2.code !== 0) throw new Error(r2.stderr || r2.stdout || 'connect failed');
|
|
640
758
|
ok = true;
|
|
@@ -643,6 +761,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
643
761
|
// full timeout — never hold the create result hostage). The outcome
|
|
644
762
|
// reaches the workspace via the heartbeat's agents[].probe.
|
|
645
763
|
this._probeAgent(name).then(() => this._nodeHeartbeat()).catch(() => {});
|
|
764
|
+
this._askCliModels(name);
|
|
646
765
|
} else if (action === 'start_agent') {
|
|
647
766
|
const r = await this._runAgn(['start', name]);
|
|
648
767
|
ok = r.code === 0;
|
|
@@ -673,7 +792,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
673
792
|
if (rc.code !== 0) throw new Error(rc.stderr || rc.stdout || 'recreate failed');
|
|
674
793
|
this._saveNodeAgentEnv(name, type, args);
|
|
675
794
|
const rc2 = await this._runAgn([
|
|
676
|
-
'connect', name, '--workspace', n.workspace_slug || n.workspace_id,
|
|
795
|
+
'connect', name, '--workspace', n.workspace_slug || n.workspace_id, '--connection', n.node_id,
|
|
677
796
|
]);
|
|
678
797
|
if (rc2.code !== 0) throw new Error(rc2.stderr || rc2.stdout || 'connect failed');
|
|
679
798
|
} else {
|
|
@@ -689,6 +808,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
689
808
|
// A reconfigure changes credentials/model — re-verify in the
|
|
690
809
|
// background so the workspace sees the new state without a manual test.
|
|
691
810
|
this._probeAgent(name).then(() => this._nodeHeartbeat()).catch(() => {});
|
|
811
|
+
this._askCliModels(name);
|
|
692
812
|
} else if (action === 'probe_agent') {
|
|
693
813
|
// Smoke-test on demand ("Re-test" in the workspace). Probes belong to
|
|
694
814
|
// configured agents (by name); the result rides agents[].probe.
|
|
@@ -805,11 +925,11 @@ async _runNodeCommand(n, cmd) {
|
|
|
805
925
|
this._crashGuardsInstalled = true;
|
|
806
926
|
process.on('unhandledRejection', (reason) => {
|
|
807
927
|
const msg = reason && reason.stack ? reason.stack : String(reason);
|
|
808
|
-
this._log(`UNHANDLED REJECTION (daemon kept alive): ${msg}
|
|
928
|
+
this._log(`UNHANDLED REJECTION (daemon kept alive): ${msg}`, 'error');
|
|
809
929
|
});
|
|
810
930
|
process.on('uncaughtException', (err) => {
|
|
811
931
|
const msg = err && err.stack ? err.stack : String(err);
|
|
812
|
-
this._log(`UNCAUGHT EXCEPTION (daemon kept alive): ${msg}
|
|
932
|
+
this._log(`UNCAUGHT EXCEPTION (daemon kept alive): ${msg}`, 'error');
|
|
813
933
|
});
|
|
814
934
|
}
|
|
815
935
|
|
|
@@ -877,6 +997,19 @@ async _runNodeCommand(n, cmd) {
|
|
|
877
997
|
3600 * 1000,
|
|
878
998
|
);
|
|
879
999
|
|
|
1000
|
+
// What each signed-in agent's account is offered, for the workspace's
|
|
1001
|
+
// model picker. Asking costs a CLI start and no model call. The pass is
|
|
1002
|
+
// frequent so an agent added on the device is picked up soon; it only
|
|
1003
|
+
// asks for a list that is missing or an hour old.
|
|
1004
|
+
this._cliModelsStartupTimer = setTimeout(
|
|
1005
|
+
() => this._refreshCliModels().catch(() => {}),
|
|
1006
|
+
20 * 1000,
|
|
1007
|
+
);
|
|
1008
|
+
this._cliModelsInterval = setInterval(
|
|
1009
|
+
() => this._refreshCliModels().catch(() => {}),
|
|
1010
|
+
10 * 60 * 1000,
|
|
1011
|
+
);
|
|
1012
|
+
|
|
880
1013
|
// Watch config file for hot-reload
|
|
881
1014
|
this._watchConfig();
|
|
882
1015
|
|
|
@@ -908,6 +1041,8 @@ async _runNodeCommand(n, cmd) {
|
|
|
908
1041
|
if (this._displayNameInterval) clearInterval(this._displayNameInterval);
|
|
909
1042
|
if (this._probeStartupTimer) clearTimeout(this._probeStartupTimer);
|
|
910
1043
|
if (this._probeInterval) clearInterval(this._probeInterval);
|
|
1044
|
+
if (this._cliModelsStartupTimer) clearTimeout(this._cliModelsStartupTimer);
|
|
1045
|
+
if (this._cliModelsInterval) clearInterval(this._cliModelsInterval);
|
|
911
1046
|
if (this._configWatcher) { try { this._configWatcher.close(); } catch {} }
|
|
912
1047
|
|
|
913
1048
|
// Kill all child processes
|
|
@@ -919,6 +1054,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
919
1054
|
this._writeStatus();
|
|
920
1055
|
this._cleanupPid();
|
|
921
1056
|
this._log('Daemon stopped');
|
|
1057
|
+
if (this._logWriter) await this._logWriter.flush();
|
|
922
1058
|
|
|
923
1059
|
if (this._shutdownResolve) this._shutdownResolve();
|
|
924
1060
|
}
|
|
@@ -947,7 +1083,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
947
1083
|
// a hung adapter.run() promise prevents the slot from ever being
|
|
948
1084
|
// released, and subsequent start/restart commands see "already running".
|
|
949
1085
|
if (this._adapters && this._adapters[agentName]) {
|
|
950
|
-
this._log(`WARNING: ${agentName} adapter did not exit after stop — force-releasing slot
|
|
1086
|
+
this._log(`WARNING: ${agentName} adapter did not exit after stop — force-releasing slot`, 'warn');
|
|
951
1087
|
try { this._adapters[agentName].stop(); } catch {}
|
|
952
1088
|
delete this._adapters[agentName];
|
|
953
1089
|
}
|
|
@@ -1162,15 +1298,18 @@ async _runNodeCommand(n, cmd) {
|
|
|
1162
1298
|
* fallback. Fields from the network entry win for display (name), the
|
|
1163
1299
|
* pairing wins for credentials (token/endpoint).
|
|
1164
1300
|
*/
|
|
1165
|
-
_resolveAgentNetwork(ref) {
|
|
1301
|
+
_resolveAgentNetwork(ref, nodeId) {
|
|
1166
1302
|
const network =
|
|
1167
1303
|
this.config.getNetworks().find((n) => n.slug === ref || n.id === ref) ||
|
|
1168
1304
|
null;
|
|
1169
1305
|
let pairing = null;
|
|
1170
1306
|
try {
|
|
1171
|
-
|
|
1172
|
-
.
|
|
1173
|
-
|
|
1307
|
+
const candidates = require('./node-config').listPairings()
|
|
1308
|
+
.filter((p) => p.workspace_slug === ref || p.workspace_id === ref);
|
|
1309
|
+
pairing = candidates.find((p) => !nodeId || p.node_id === nodeId) || null;
|
|
1310
|
+
if ((nodeId && !pairing) || (!nodeId && candidates.length > 1)) {
|
|
1311
|
+
return { ...(network || {}), token: null };
|
|
1312
|
+
}
|
|
1174
1313
|
} catch {}
|
|
1175
1314
|
if (!network && !pairing) return null;
|
|
1176
1315
|
return {
|
|
@@ -1214,7 +1353,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
1214
1353
|
// Workspace-connected agents use the adapter loop (poll + CLI per message).
|
|
1215
1354
|
// Local-only agents use the spawn loop (long-running child process).
|
|
1216
1355
|
const network = agentCfg.network
|
|
1217
|
-
? this._resolveAgentNetwork(agentCfg.network)
|
|
1356
|
+
? this._resolveAgentNetwork(agentCfg.network, agentCfg.node_id)
|
|
1218
1357
|
: null;
|
|
1219
1358
|
|
|
1220
1359
|
if (network && !network.token) {
|
|
@@ -1233,6 +1372,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
1233
1372
|
// Remember which credential this adapter runs with, so the rotation
|
|
1234
1373
|
// watch can restart it when a re-pair hands the workspace a new token.
|
|
1235
1374
|
info.networkRef = agentCfg.network;
|
|
1375
|
+
info.connectionId = agentCfg.node_id;
|
|
1236
1376
|
info.credentialToken = network.token;
|
|
1237
1377
|
this._adapterLoop(name, agentCfg, info, network);
|
|
1238
1378
|
} else {
|
|
@@ -1282,7 +1422,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
1282
1422
|
const exitCode = await new Promise((resolve) => {
|
|
1283
1423
|
proc.on('exit', (code) => resolve(code));
|
|
1284
1424
|
proc.on('error', (err) => {
|
|
1285
|
-
this._log(`${name} spawn error: ${err.message}
|
|
1425
|
+
this._log(`${name} spawn error: ${err.message}`, 'error');
|
|
1286
1426
|
resolve(1);
|
|
1287
1427
|
});
|
|
1288
1428
|
});
|
|
@@ -1309,13 +1449,13 @@ async _runNodeCommand(n, cmd) {
|
|
|
1309
1449
|
this._writeStatus();
|
|
1310
1450
|
|
|
1311
1451
|
if (info.restarts >= 10) {
|
|
1312
|
-
this._log(`${name} crashed ${info.restarts} times, giving up. Fix the issue and restart manually
|
|
1452
|
+
this._log(`${name} crashed ${info.restarts} times, giving up. Fix the issue and restart manually.`, 'error');
|
|
1313
1453
|
info.state = 'stopped';
|
|
1314
1454
|
this._writeStatus();
|
|
1315
1455
|
break;
|
|
1316
1456
|
}
|
|
1317
1457
|
|
|
1318
|
-
this._log(`${name} crashed: ${info.lastError}, restarting in ${info._backoff}s (attempt ${info.restarts})
|
|
1458
|
+
this._log(`${name} crashed: ${info.lastError}, restarting in ${info._backoff}s (attempt ${info.restarts})`, 'warn');
|
|
1319
1459
|
await this._sleep(info._backoff * 1000);
|
|
1320
1460
|
info._backoff = Math.min(info._backoff * 2, 60);
|
|
1321
1461
|
}
|
|
@@ -1384,6 +1524,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
1384
1524
|
toolMode: agentCfg.tool_mode || 'skills',
|
|
1385
1525
|
// Live runtime/connectivity status → daemon.status.json (Agents list/TUI).
|
|
1386
1526
|
onStatus: (update) => this._applyAdapterStatus(name, info, update),
|
|
1527
|
+
logSink: (level, msg) => this._logAs(`adapter [${name}]`, level, msg),
|
|
1387
1528
|
});
|
|
1388
1529
|
} catch (e) {
|
|
1389
1530
|
// A construction failure (e.g. unknown agent type in this core) is a hard
|
|
@@ -1391,7 +1532,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
1391
1532
|
info.state = 'error';
|
|
1392
1533
|
info.errorReason = 'adapter_crashed';
|
|
1393
1534
|
info.lastError = redactDiagnostic(e.message || String(e));
|
|
1394
|
-
this._log(`${name} failed to create ${agentType} adapter: ${info.lastError}
|
|
1535
|
+
this._log(`${name} failed to create ${agentType} adapter: ${info.lastError}`, 'error');
|
|
1395
1536
|
this._writeStatus();
|
|
1396
1537
|
return;
|
|
1397
1538
|
}
|
|
@@ -1408,11 +1549,11 @@ async _runNodeCommand(n, cmd) {
|
|
|
1408
1549
|
info.errorReason = pf.reason || 'runtime_missing';
|
|
1409
1550
|
info.lastError = redactDiagnostic(pf.message || 'runtime not available');
|
|
1410
1551
|
this._writeStatus();
|
|
1411
|
-
this._log(`${name} preflight failed (${info.errorReason}): ${info.lastError}
|
|
1552
|
+
this._log(`${name} preflight failed (${info.errorReason}): ${info.lastError}`, 'warn');
|
|
1412
1553
|
return;
|
|
1413
1554
|
}
|
|
1414
1555
|
} catch (e) {
|
|
1415
|
-
this._log(`${name} preflight error (ignored, continuing): ${e.message}
|
|
1556
|
+
this._log(`${name} preflight error (ignored, continuing): ${e.message}`, 'warn');
|
|
1416
1557
|
}
|
|
1417
1558
|
|
|
1418
1559
|
// Store adapter reference for stop and duplicate detection
|
|
@@ -1440,7 +1581,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
1440
1581
|
} catch (e) {
|
|
1441
1582
|
info.errorReason = info.errorReason || 'adapter_crashed';
|
|
1442
1583
|
info.lastError = redactDiagnostic((e.message || String(e)).slice(0, 200));
|
|
1443
|
-
this._log(`${name} adapter error: ${info.lastError}
|
|
1584
|
+
this._log(`${name} adapter error: ${info.lastError}`, 'error');
|
|
1444
1585
|
}
|
|
1445
1586
|
|
|
1446
1587
|
delete this._adapters[name];
|
|
@@ -1756,7 +1897,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
1756
1897
|
// The old adapter will exit on its next poll iteration since its
|
|
1757
1898
|
// entry in _stoppedAgents triggers adapter.stop() via checkStop.
|
|
1758
1899
|
if (this._adapters && this._adapters[name]) {
|
|
1759
|
-
this._log(`WARNING: adapter '${name}' did not clear after 10s — force-releasing slot to avoid duplicate
|
|
1900
|
+
this._log(`WARNING: adapter '${name}' did not clear after 10s — force-releasing slot to avoid duplicate`, 'warn');
|
|
1760
1901
|
try { this._adapters[name].stop(); } catch {}
|
|
1761
1902
|
delete this._adapters[name];
|
|
1762
1903
|
}
|
|
@@ -1773,33 +1914,25 @@ async _runNodeCommand(n, cmd) {
|
|
|
1773
1914
|
try { fs.unlinkSync(this.config.statusFile); } catch {}
|
|
1774
1915
|
}
|
|
1775
1916
|
|
|
1776
|
-
_log(msg) {
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1917
|
+
_log(msg, level = 'info') {
|
|
1918
|
+
this._logAs('daemon', level, msg);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
/**
|
|
1922
|
+
* One entry in daemon.log. Adapters log through here too (see the `logSink`
|
|
1923
|
+
* passed in _adapterLoop), so batching, repeat suppression and rotation cover
|
|
1924
|
+
* their lines as well — they used to bypass all of it via console.log.
|
|
1925
|
+
*/
|
|
1926
|
+
_logAs(scope, level, msg) {
|
|
1927
|
+
if (!this._logWriter) this._logWriter = new LogWriter(this.config.logFile);
|
|
1928
|
+
const written = this._logWriter.write(level, scope, msg);
|
|
1783
1929
|
// Only log to console if stdout is a TTY (not redirected to log file)
|
|
1784
1930
|
// to avoid duplicate lines when daemonized
|
|
1785
|
-
if (!this._shuttingDown && process.stdout.isTTY) {
|
|
1786
|
-
console.log(
|
|
1931
|
+
if (written && !this._shuttingDown && process.stdout.isTTY) {
|
|
1932
|
+
console.log(`${new Date().toISOString()} ${String(level).toUpperCase()} ${scope}: ${msg}`);
|
|
1787
1933
|
}
|
|
1788
1934
|
}
|
|
1789
1935
|
|
|
1790
|
-
_maybeRotateLog() {
|
|
1791
|
-
// Rotate at 10MB, keep 1 backup
|
|
1792
|
-
const MAX_SIZE = 10 * 1024 * 1024;
|
|
1793
|
-
try {
|
|
1794
|
-
const stat = fs.statSync(this.config.logFile);
|
|
1795
|
-
if (stat.size > MAX_SIZE) {
|
|
1796
|
-
const backup = this.config.logFile + '.1';
|
|
1797
|
-
try { fs.unlinkSync(backup); } catch {}
|
|
1798
|
-
fs.renameSync(this.config.logFile, backup);
|
|
1799
|
-
}
|
|
1800
|
-
} catch {}
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
1936
|
_sleep(ms) {
|
|
1804
1937
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1805
1938
|
}
|
package/src/index.js
CHANGED
|
@@ -121,7 +121,7 @@ class AgentConnector {
|
|
|
121
121
|
if (!network || !network.id) return null;
|
|
122
122
|
const endpoint = network.endpoint || (this.workspace && this.workspace.endpoint);
|
|
123
123
|
const { WorkspaceClient } = require('./workspace-client');
|
|
124
|
-
return { client: new WorkspaceClient(endpoint), network };
|
|
124
|
+
return { client: new WorkspaceClient(endpoint, { agentName }), network };
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/**
|
|
@@ -431,6 +431,15 @@ class AgentConnector {
|
|
|
431
431
|
const { probeAgentType } = require('./probe');
|
|
432
432
|
return probeAgentType(this, agentType, opts);
|
|
433
433
|
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Ask an agent type's own CLI which models the account it is signed in to
|
|
437
|
+
* can run. See cli-models.js.
|
|
438
|
+
*/
|
|
439
|
+
async listCliModels(agentType, opts) {
|
|
440
|
+
const { listCliModels } = require('./cli-models');
|
|
441
|
+
return listCliModels(this, agentType, opts);
|
|
442
|
+
}
|
|
434
443
|
}
|
|
435
444
|
|
|
436
445
|
const adapters = require('./adapters');
|