@openagents-org/agent-launcher 0.2.143 → 0.2.146
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/package.json +1 -1
- package/registry.json +274 -66
- package/src/adapters/aider.js +634 -0
- package/src/adapters/amp.js +477 -0
- package/src/adapters/base.js +112 -115
- package/src/adapters/claude.js +36 -34
- package/src/adapters/cline-stream.js +632 -0
- package/src/adapters/cline.js +914 -0
- package/src/adapters/codex.js +14 -16
- package/src/adapters/copilot-stream-parser.js +380 -0
- package/src/adapters/copilot.js +796 -0
- package/src/adapters/cursor.js +9 -15
- package/src/adapters/gemini.js +44 -12
- package/src/adapters/goose-stream.js +268 -0
- package/src/adapters/goose.js +649 -0
- package/src/adapters/health-status.js +166 -0
- package/src/adapters/hermes.js +8 -17
- package/src/adapters/index.js +16 -1
- package/src/adapters/nanoclaw-bridge.js +264 -0
- package/src/adapters/nanoclaw-channel-install.js +411 -0
- package/src/adapters/nanoclaw-control.js +491 -0
- package/src/adapters/nanoclaw-delivery-store.js +0 -0
- package/src/adapters/nanoclaw-protocol.js +349 -0
- package/src/adapters/nanoclaw.js +661 -9
- package/src/adapters/opencode.js +541 -75
- package/src/adapters/workspace-prompt.js +0 -79
- package/src/agent-rows.js +13 -1
- package/src/auth-guidance.js +98 -0
- package/src/cli.js +36 -7
- package/src/config.js +8 -2
- package/src/daemon.js +165 -51
- package/src/installer.js +572 -10
- package/src/paths.js +125 -2
- package/src/tui.js +29 -2
- package/src/update-check.js +26 -14
- package/src/workspace-client.js +31 -29
package/src/paths.js
CHANGED
|
@@ -50,6 +50,12 @@ function getExtraBinDirs() {
|
|
|
50
50
|
// Common: ~/.local/bin (pipx, user installs)
|
|
51
51
|
_push(dirs, path.join(HOME, '.local', 'bin'));
|
|
52
52
|
|
|
53
|
+
// Aider (uv tool install) — its executable honors XDG_BIN_HOME /
|
|
54
|
+
// XDG_DATA_HOME/../bin / ~/.local/bin and also always lands in the uv tools
|
|
55
|
+
// venv. A GUI/daemon process won't see the installer's PATH edit, so add the
|
|
56
|
+
// real install dirs explicitly (filtered to existing dirs by the caller).
|
|
57
|
+
for (const d of aiderBinDirs()) _push(dirs, d);
|
|
58
|
+
|
|
53
59
|
// Also add the directory containing the current node binary
|
|
54
60
|
try {
|
|
55
61
|
const nodeDir = path.dirname(process.execPath);
|
|
@@ -142,6 +148,14 @@ function whichBinary(name) {
|
|
|
142
148
|
return cached.value;
|
|
143
149
|
}
|
|
144
150
|
|
|
151
|
+
// On a non-English Windows the console OUTPUT codepage is OEM (e.g. 936/GBK on
|
|
152
|
+
// zh-CN), so `where` prints a path whose non-ASCII bytes don't match the utf-8
|
|
153
|
+
// decoding execSync does — a Chinese username comes back mangled (e.g.
|
|
154
|
+
// `C:\Users\??.?[\…`) and yields ENOENT downstream. We can't reliably re-encode
|
|
155
|
+
// it (and forcing `chcp` is unsafe under windowsHide's console-less cmd), so we
|
|
156
|
+
// existence-check every hit and only return one that's real; a mangled path
|
|
157
|
+
// fails and the caller falls through to its Node-derived tiers (built from
|
|
158
|
+
// process.env / os.homedir, which the OS hands us as correct UTF-16).
|
|
145
159
|
const cmd = IS_WINDOWS ? `where ${name}` : `which ${name}`;
|
|
146
160
|
try {
|
|
147
161
|
const result = execSync(cmd, {
|
|
@@ -150,8 +164,12 @@ function whichBinary(name) {
|
|
|
150
164
|
env: { ...process.env, PATH: getEnhancedPATH() },
|
|
151
165
|
timeout: 5000,
|
|
152
166
|
windowsHide: true,
|
|
153
|
-
})
|
|
154
|
-
|
|
167
|
+
});
|
|
168
|
+
let value = null;
|
|
169
|
+
for (const line of result.split(/\r?\n/)) {
|
|
170
|
+
const hit = line.trim();
|
|
171
|
+
if (hit && fs.existsSync(hit)) { value = hit; break; }
|
|
172
|
+
}
|
|
155
173
|
whichBinaryCache.set(cacheKey, { value, at: Date.now() });
|
|
156
174
|
return value;
|
|
157
175
|
} catch {
|
|
@@ -160,6 +178,64 @@ function whichBinary(name) {
|
|
|
160
178
|
}
|
|
161
179
|
}
|
|
162
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Codepage-safe `where`/`which` lookup, shared by every CLI adapter.
|
|
183
|
+
*
|
|
184
|
+
* Hazard: on a non-English Windows the console output codepage is OEM (e.g.
|
|
185
|
+
* 936/GBK on zh-CN), so execSync decodes `where` stdout with the wrong codepage
|
|
186
|
+
* and mangles a non-ASCII (e.g. Chinese) username in the path — yielding ENOENT
|
|
187
|
+
* downstream (the `C:\Users\??.?[\…\claude.cmd` failure). Two safe defenses,
|
|
188
|
+
* no `chcp` (which is unreliable under windowsHide's console-less cmd):
|
|
189
|
+
* 1. (Windows) check the npm-global default `%APPDATA%\npm\<name>.cmd` FIRST.
|
|
190
|
+
* APPDATA comes from the OS as UTF-16 via Node, so this path is always
|
|
191
|
+
* correctly encoded — and it's where the npm-installed CLIs actually live.
|
|
192
|
+
* 2. Fall back to `where`/`which`, but existence-check every hit so a mangled
|
|
193
|
+
* path is skipped and the caller drops to its own Node-derived tiers.
|
|
194
|
+
*
|
|
195
|
+
* @param {string|string[]} names base name(s), e.g. 'claude' or ['cursor-agent','agent'].
|
|
196
|
+
* On Windows each is tried as <name>.cmd, <name>.exe, <name>.
|
|
197
|
+
* @param {object} [env] env for the lookup (defaults to getEnhancedEnv()).
|
|
198
|
+
* @returns {string|null} an existing absolute path, or null.
|
|
199
|
+
*/
|
|
200
|
+
function whereBinary(names, env) {
|
|
201
|
+
const bases = Array.isArray(names) ? names : [names];
|
|
202
|
+
|
|
203
|
+
// 1. npm-global default, derived from Node's env (correct encoding even when
|
|
204
|
+
// the username is non-ASCII). This is where `npm i -g <cli>` drops the shim.
|
|
205
|
+
if (IS_WINDOWS && process.env.APPDATA) {
|
|
206
|
+
for (const b of bases) {
|
|
207
|
+
const c = path.join(process.env.APPDATA, 'npm', `${b}.cmd`);
|
|
208
|
+
try { if (fs.existsSync(c)) return c; } catch {}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 2. PATH lookup, existence-guarded.
|
|
213
|
+
let cmd;
|
|
214
|
+
if (IS_WINDOWS) {
|
|
215
|
+
const parts = [];
|
|
216
|
+
for (const b of bases) {
|
|
217
|
+
parts.push(`where ${b}.cmd 2>nul`, `where ${b}.exe 2>nul`, `where ${b} 2>nul`);
|
|
218
|
+
}
|
|
219
|
+
cmd = parts.join(' || ');
|
|
220
|
+
} else {
|
|
221
|
+
cmd = bases.map((b) => `which ${b}`).join(' || ');
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
const out = execSync(cmd, {
|
|
225
|
+
encoding: 'utf-8',
|
|
226
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
227
|
+
timeout: 5000,
|
|
228
|
+
windowsHide: true,
|
|
229
|
+
env: env || getEnhancedEnv(),
|
|
230
|
+
});
|
|
231
|
+
for (const line of out.split(/\r?\n/)) {
|
|
232
|
+
const hit = line.trim();
|
|
233
|
+
if (hit && fs.existsSync(hit)) return hit;
|
|
234
|
+
}
|
|
235
|
+
} catch {}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
163
239
|
// ---- Windows paths ----
|
|
164
240
|
|
|
165
241
|
function _addWindowsPaths(dirs) {
|
|
@@ -197,6 +273,12 @@ function _addWindowsPaths(dirs) {
|
|
|
197
273
|
if (localAppData) _push(dirs, path.join(localAppData, 'cursor-agent'));
|
|
198
274
|
_push(dirs, path.join(HOME, '.local', 'bin'));
|
|
199
275
|
|
|
276
|
+
// Amp CLI (irm https://ampcode.com/install.ps1 | iex) — same registry-PATH
|
|
277
|
+
// staleness as Cursor; add the canonical install dir(s) so an already-running
|
|
278
|
+
// daemon resolves amp without a reboot.
|
|
279
|
+
_push(dirs, path.join(HOME, '.amp', 'bin'));
|
|
280
|
+
if (localAppData) _push(dirs, path.join(localAppData, 'amp'));
|
|
281
|
+
|
|
200
282
|
// Hermes CLI native (no-WSL) installer drops hermes.exe in the portable
|
|
201
283
|
// venv's Scripts dir and the uv shim in %LOCALAPPDATA%\hermes\bin. Same
|
|
202
284
|
// registry-PATH staleness as Cursor — add explicitly so an already-running
|
|
@@ -291,6 +373,12 @@ function _addUnixPaths(dirs) {
|
|
|
291
373
|
|
|
292
374
|
// Cursor CLI native installer (curl https://cursor.com/install | bash)
|
|
293
375
|
_push(dirs, path.join(HOME, '.cursor', 'bin'));
|
|
376
|
+
|
|
377
|
+
// Amp CLI native installer (curl https://ampcode.com/install.sh | bash)
|
|
378
|
+
// drops the binary in ~/.amp/bin and only symlinks into ~/.local/bin when
|
|
379
|
+
// that dir is already on PATH — so a GUI- or daemon-spawned process never
|
|
380
|
+
// sees it unless the canonical dir is listed here explicitly.
|
|
381
|
+
_push(dirs, path.join(HOME, '.amp', 'bin'));
|
|
294
382
|
}
|
|
295
383
|
|
|
296
384
|
// ---- macOS-specific ----
|
|
@@ -396,15 +484,50 @@ function clearBinaryLookupCache() {
|
|
|
396
484
|
whichBinaryCache.clear();
|
|
397
485
|
}
|
|
398
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Directories where the Aider CLI executable can land, in the SAME priority the
|
|
489
|
+
* official installer (aider.chat/install.{sh,ps1} → `uv tool install`) uses, so
|
|
490
|
+
* detection matches reality on every platform:
|
|
491
|
+
* 1. $XDG_BIN_HOME
|
|
492
|
+
* 2. $XDG_DATA_HOME/../bin
|
|
493
|
+
* 3. ~/.local/bin (the default uv-tool / pipx / pip --user bin)
|
|
494
|
+
* 4. the uv tools venv for aider-chat (Scripts on Windows, bin on Unix) — the
|
|
495
|
+
* executable always lands here on a successful `uv tool install`, even if
|
|
496
|
+
* the bin-dir copy/PATH edit didn't happen.
|
|
497
|
+
* Uses os.homedir()/live env so it reflects the current process (test-friendly).
|
|
498
|
+
* Returns directories only; callers join the platform binary names.
|
|
499
|
+
*/
|
|
500
|
+
function aiderBinDirs() {
|
|
501
|
+
const home = os.homedir();
|
|
502
|
+
const dirs = [];
|
|
503
|
+
if (process.env.XDG_BIN_HOME) dirs.push(process.env.XDG_BIN_HOME);
|
|
504
|
+
if (process.env.XDG_DATA_HOME) dirs.push(path.join(process.env.XDG_DATA_HOME, '..', 'bin'));
|
|
505
|
+
dirs.push(path.join(home, '.local', 'bin'));
|
|
506
|
+
if (IS_WINDOWS) {
|
|
507
|
+
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
508
|
+
const uvTools = process.env.UV_TOOL_DIR || path.join(appData, 'uv', 'tools');
|
|
509
|
+
dirs.push(path.join(uvTools, 'aider-chat', 'Scripts'));
|
|
510
|
+
dirs.push(path.join(home, 'bin'));
|
|
511
|
+
} else {
|
|
512
|
+
const uvTools = process.env.UV_TOOL_DIR
|
|
513
|
+
|| path.join(home, '.local', 'share', 'uv', 'tools');
|
|
514
|
+
dirs.push(path.join(uvTools, 'aider-chat', 'bin'));
|
|
515
|
+
dirs.push(path.join(home, 'bin'), '/usr/local/bin', '/opt/homebrew/bin');
|
|
516
|
+
}
|
|
517
|
+
return dirs;
|
|
518
|
+
}
|
|
519
|
+
|
|
399
520
|
module.exports = {
|
|
400
521
|
getExtraBinDirs,
|
|
401
522
|
getEnhancedPATH,
|
|
402
523
|
getEnhancedEnv,
|
|
403
524
|
whichBinary,
|
|
525
|
+
whereBinary,
|
|
404
526
|
clearBinaryLookupCache,
|
|
405
527
|
getRuntimePrefix,
|
|
406
528
|
getCorePrefix,
|
|
407
529
|
defaultAgentWorkdir,
|
|
530
|
+
aiderBinDirs,
|
|
408
531
|
IS_WINDOWS,
|
|
409
532
|
IS_MACOS,
|
|
410
533
|
SEP,
|
package/src/tui.js
CHANGED
|
@@ -13,6 +13,7 @@ const path = require('path');
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const { spawn } = require('child_process');
|
|
15
15
|
const { getExtraBinDirs } = require('./paths');
|
|
16
|
+
const { hasCredentialMetadata, formatAuthGuidance } = require('./auth-guidance');
|
|
16
17
|
|
|
17
18
|
const IS_WINDOWS = process.platform === 'win32';
|
|
18
19
|
|
|
@@ -67,7 +68,14 @@ function getConnector() {
|
|
|
67
68
|
|
|
68
69
|
function describeHealth(health) {
|
|
69
70
|
if (!health) return '';
|
|
70
|
-
if (!health.ready)
|
|
71
|
+
if (!health.ready) {
|
|
72
|
+
// "Not installed" is reserved for a genuinely missing executable; an
|
|
73
|
+
// installed-but-signed-out agent shows its login/config message instead.
|
|
74
|
+
if (health.reason === 'not_installed' || health.installed === false) {
|
|
75
|
+
return 'Not installed';
|
|
76
|
+
}
|
|
77
|
+
return health.message || 'Not configured';
|
|
78
|
+
}
|
|
71
79
|
const parts = ['Ready'];
|
|
72
80
|
if (health.auth_mode === 'api_key') parts.push('API key');
|
|
73
81
|
else if (health.auth_mode === 'cli_login') parts.push('CLI login');
|
|
@@ -283,11 +291,14 @@ function createTUI() {
|
|
|
283
291
|
// Local-only agents (no workspace) show a dimmed "(local)" marker.
|
|
284
292
|
const ws = r.workspace || `{gray-fg}${r.workspaceLabel}{/gray-fg}`;
|
|
285
293
|
items.push(` ${r.name.padEnd(22)} ${r.type.padEnd(14)} ${state.padEnd(30)} ${ws}`);
|
|
286
|
-
// Detail row: working dir + config warning
|
|
294
|
+
// Detail row: working dir + config warning + the REAL runtime error
|
|
295
|
+
// (spawn/join/heartbeat) when the daemon reported one, so a failure shows
|
|
296
|
+
// its actual cause instead of a swallowed/"Not installed" guess.
|
|
287
297
|
const details = [];
|
|
288
298
|
details.push(r.path || process.env.HOME || '~');
|
|
289
299
|
if (r.health) details.push(`{cyan-fg}${describeHealth(r.health)}{/cyan-fg}`);
|
|
290
300
|
if (r.notReadyMsg) details.push(`{yellow-fg}⚠ ${r.notReadyMsg}{/yellow-fg}`);
|
|
301
|
+
if (r.lastError) details.push(`{red-fg}✗ ${r.lastError}{/red-fg}`);
|
|
291
302
|
items.push(` {gray-fg} ${details.join(' | ')}{/gray-fg}`);
|
|
292
303
|
}
|
|
293
304
|
agentList.setItems(items);
|
|
@@ -754,6 +765,22 @@ function createTUI() {
|
|
|
754
765
|
currentView = 'configure';
|
|
755
766
|
const envFields = connector.registry.getEnvFields(agent.type);
|
|
756
767
|
if (!envFields || envFields.length === 0) {
|
|
768
|
+
// Some agents declare no env_config yet still REQUIRE a sign-in/credential
|
|
769
|
+
// (e.g. Gemini's OAuth login). Detect that via the registry's check_ready
|
|
770
|
+
// and show readiness + login guidance instead of a misleading "no
|
|
771
|
+
// configuration required". Agents without any credential metadata keep the
|
|
772
|
+
// original message — this is gated, so other agents are untouched.
|
|
773
|
+
const entry = connector.registry.getEntry(agent.type);
|
|
774
|
+
const checkReady = entry && entry.check_ready;
|
|
775
|
+
if (hasCredentialMetadata(checkReady)) {
|
|
776
|
+
let health = null;
|
|
777
|
+
try { health = connector.healthCheck(agent.type); } catch {}
|
|
778
|
+
const g = formatAuthGuidance(entry, health);
|
|
779
|
+
const tag = g.ready ? 'green-fg' : 'yellow-fg';
|
|
780
|
+
log(`{bold}{${tag}}${entry.label || agent.type} authentication: ${g.ready ? 'Ready' : 'Not ready'}{/}{/bold}`);
|
|
781
|
+
for (const line of g.lines) log(`{gray-fg}${line}{/gray-fg}`);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
757
784
|
log('{gray-fg}No configuration required for this agent type.{/gray-fg}');
|
|
758
785
|
return;
|
|
759
786
|
}
|
package/src/update-check.js
CHANGED
|
@@ -88,20 +88,31 @@ async function checkForUpdate() {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
91
|
+
* Derive the npm `--prefix` for a global install from the npm binary location.
|
|
92
|
+
*
|
|
93
|
+
* npm resolves `-g` installs relative to a PREFIX, and the layout differs by
|
|
94
|
+
* platform:
|
|
95
|
+
* Unix: packages → {prefix}/lib/node_modules (npm bin at {prefix}/bin)
|
|
96
|
+
* Windows: packages → {prefix}/node_modules (npm bin at {prefix})
|
|
97
|
+
*
|
|
98
|
+
* So the prefix is the PARENT of npm's bin dir on Unix, and the bin dir itself
|
|
99
|
+
* on Windows. The previous implementation walked up to the enclosing
|
|
100
|
+
* node_modules and returned the directory containing it — which on Unix is
|
|
101
|
+
* `{prefix}/lib`. Passing THAT as `--prefix` made npm append `lib/node_modules`
|
|
102
|
+
* again, installing into `{prefix}/lib/lib/node_modules` while the running `agn`
|
|
103
|
+
* (loaded from `{prefix}/lib/node_modules`) never changed: `agn update` reported
|
|
104
|
+
* success but silently no-op'd on every Unix host.
|
|
93
105
|
*/
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return null;
|
|
106
|
+
function npmPrefixFromBin(npmBin, platform = process.platform) {
|
|
107
|
+
if (!npmBin) return null;
|
|
108
|
+
// Use the target platform's path semantics so a Windows path resolves
|
|
109
|
+
// correctly even when this runs on a POSIX host (and vice versa).
|
|
110
|
+
const p = platform === 'win32' ? path.win32 : path.posix;
|
|
111
|
+
const binDir = p.dirname(npmBin);
|
|
112
|
+
// A bare name (e.g. 'npm' from the PATH hard-fallback) has no usable
|
|
113
|
+
// directory — let npm resolve its own default global prefix instead.
|
|
114
|
+
if (binDir === '.' || binDir === '') return null;
|
|
115
|
+
return platform === 'win32' ? binDir : p.dirname(binDir);
|
|
105
116
|
}
|
|
106
117
|
|
|
107
118
|
/**
|
|
@@ -139,7 +150,7 @@ function runUpdate(opts = {}) {
|
|
|
139
150
|
const platform = opts.platform || process.platform;
|
|
140
151
|
const spawn = opts.spawn || spawnSync;
|
|
141
152
|
const npmBin = opts.npmBin || findNpmBin({ platform });
|
|
142
|
-
const prefix = opts.prefix !== undefined ? opts.prefix :
|
|
153
|
+
const prefix = opts.prefix !== undefined ? opts.prefix : npmPrefixFromBin(npmBin, platform);
|
|
143
154
|
// Use a GLOBAL install (-g). A LOCAL install (`npm install <pkg> --prefix
|
|
144
155
|
// <dir>`) treats <dir> as a project root and prunes every node_modules entry
|
|
145
156
|
// not reachable from its package.json. When the prefix is the portable node
|
|
@@ -228,5 +239,6 @@ module.exports = {
|
|
|
228
239
|
notifyAndMaybeUpdate,
|
|
229
240
|
runUpdate,
|
|
230
241
|
findNpmBin,
|
|
242
|
+
npmPrefixFromBin,
|
|
231
243
|
currentVersion,
|
|
232
244
|
};
|
package/src/workspace-client.js
CHANGED
|
@@ -156,15 +156,12 @@ class WorkspaceClient {
|
|
|
156
156
|
*/
|
|
157
157
|
async sendMessage(workspaceId, channelName, token, content, {
|
|
158
158
|
senderType = 'agent', senderName, messageType = 'chat', metadata, attachments, sessionId,
|
|
159
|
-
spec, specToolCallId,
|
|
160
159
|
} = {}) {
|
|
161
160
|
const sourcePrefix = senderType === 'agent' ? 'openagents' : 'human';
|
|
162
161
|
const source = senderName ? `${sourcePrefix}:${senderName}` : `${sourcePrefix}:unknown`;
|
|
163
162
|
|
|
164
163
|
const payload = { content, message_type: messageType };
|
|
165
164
|
if (attachments && attachments.length) payload.attachments = attachments;
|
|
166
|
-
if (spec) payload.spec = spec;
|
|
167
|
-
if (specToolCallId) payload.spec_tool_call_id = specToolCallId;
|
|
168
165
|
|
|
169
166
|
return this.sendEvent(workspaceId, {
|
|
170
167
|
type: 'workspace.message.posted',
|
|
@@ -175,30 +172,6 @@ class WorkspaceClient {
|
|
|
175
172
|
}, token, sessionId);
|
|
176
173
|
}
|
|
177
174
|
|
|
178
|
-
/**
|
|
179
|
-
* Poll workspace.tool_result events — the client's response to an agent's
|
|
180
|
-
* render_ui invocation. Returns events directly (no message normalization)
|
|
181
|
-
* because the caller treats them differently than chat messages.
|
|
182
|
-
*/
|
|
183
|
-
async pollToolResults(workspaceId, token, { after, limit = 50 } = {}) {
|
|
184
|
-
const params = new URLSearchParams({
|
|
185
|
-
network: workspaceId,
|
|
186
|
-
type: 'workspace.tool_result',
|
|
187
|
-
limit: String(limit),
|
|
188
|
-
});
|
|
189
|
-
if (after) params.set('after', after);
|
|
190
|
-
|
|
191
|
-
const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token));
|
|
192
|
-
const result = data.data || data;
|
|
193
|
-
const events = (result && result.events) || [];
|
|
194
|
-
|
|
195
|
-
let cursor = null;
|
|
196
|
-
if (events.length > 0) {
|
|
197
|
-
cursor = events[events.length - 1].id || null;
|
|
198
|
-
}
|
|
199
|
-
return { events, cursor };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
175
|
/**
|
|
203
176
|
* Poll messages in a channel via GET /v1/events.
|
|
204
177
|
* @returns {Array} message-compatible objects
|
|
@@ -275,6 +248,11 @@ class WorkspaceClient {
|
|
|
275
248
|
const params = new URLSearchParams({
|
|
276
249
|
network: workspaceId,
|
|
277
250
|
type: 'workspace.message.posted',
|
|
251
|
+
// Server-side pre-filter: only return events routed to this agent (plus
|
|
252
|
+
// untargeted ones) instead of the whole network's traffic. Backward
|
|
253
|
+
// compatible — older backends ignore the unknown param and return
|
|
254
|
+
// everything, so the client-side filter below stays as the safety net.
|
|
255
|
+
target_agents: agentName,
|
|
278
256
|
limit: String(limit),
|
|
279
257
|
});
|
|
280
258
|
if (after) params.set('after', after);
|
|
@@ -283,12 +261,23 @@ class WorkspaceClient {
|
|
|
283
261
|
const result = data.data || data;
|
|
284
262
|
const events = (result && result.events) || [];
|
|
285
263
|
|
|
264
|
+
// Prefer the server's next_cursor: with server-side target filtering the
|
|
265
|
+
// last returned event lags the stream tip, so advancing by it alone would
|
|
266
|
+
// re-scan the same range every poll. next_cursor jumps past other agents'
|
|
267
|
+
// events once ours are drained. Older backends don't send it — fall back
|
|
268
|
+
// to the last returned event id (they return the full stream, so that IS
|
|
269
|
+
// the tip).
|
|
286
270
|
let cursor = null;
|
|
287
|
-
if (
|
|
271
|
+
if (result && result.next_cursor) {
|
|
272
|
+
cursor = result.next_cursor;
|
|
273
|
+
} else if (events.length > 0) {
|
|
288
274
|
cursor = events[events.length - 1].id || null;
|
|
289
275
|
}
|
|
290
276
|
|
|
291
|
-
// Filter for messages targeted at this agent.
|
|
277
|
+
// Filter for messages targeted at this agent. With a targeting-aware
|
|
278
|
+
// backend this is mostly a no-op (server already narrowed the set); it
|
|
279
|
+
// remains authoritative for older backends and for the untargeted events
|
|
280
|
+
// the server includes as a safe superset.
|
|
292
281
|
//
|
|
293
282
|
// target_agents semantics:
|
|
294
283
|
// • absent → legacy server with no routing decision
|
|
@@ -802,6 +791,14 @@ class WorkspaceClient {
|
|
|
802
791
|
method: 'GET',
|
|
803
792
|
headers,
|
|
804
793
|
timeout,
|
|
794
|
+
// Hard end-to-end deadline. The `timeout` option above is only a SOCKET
|
|
795
|
+
// inactivity timeout — it is NOT armed until a socket exists, so it does
|
|
796
|
+
// not bound DNS resolution or TCP connect. During a network partition a
|
|
797
|
+
// request stuck in getaddrinfo/connect never fires 'timeout', so the
|
|
798
|
+
// await never settles and the caller's single poll loop wedges forever
|
|
799
|
+
// (the agent stays "online" via its separate heartbeat but stops
|
|
800
|
+
// consuming messages). AbortSignal.timeout fires in ANY phase.
|
|
801
|
+
signal: AbortSignal.timeout(timeout),
|
|
805
802
|
}, (res) => {
|
|
806
803
|
let data = '';
|
|
807
804
|
res.on('data', (chunk) => { data += chunk; });
|
|
@@ -836,6 +833,7 @@ class WorkspaceClient {
|
|
|
836
833
|
method: 'GET',
|
|
837
834
|
headers,
|
|
838
835
|
timeout,
|
|
836
|
+
signal: AbortSignal.timeout(timeout), // hard deadline (covers DNS/connect); see _get
|
|
839
837
|
}, (res) => {
|
|
840
838
|
const chunks = [];
|
|
841
839
|
res.on('data', (chunk) => { chunks.push(chunk); });
|
|
@@ -868,6 +866,7 @@ class WorkspaceClient {
|
|
|
868
866
|
method: 'POST',
|
|
869
867
|
headers: { ...headers, 'Content-Length': Buffer.byteLength(jsonBody) },
|
|
870
868
|
timeout,
|
|
869
|
+
signal: AbortSignal.timeout(timeout), // hard deadline (covers DNS/connect); see _get
|
|
871
870
|
}, (res) => {
|
|
872
871
|
let data = '';
|
|
873
872
|
res.on('data', (chunk) => { data += chunk; });
|
|
@@ -910,6 +909,7 @@ class WorkspaceClient {
|
|
|
910
909
|
method: 'PUT',
|
|
911
910
|
headers: { ...headers, 'Content-Length': Buffer.byteLength(jsonBody) },
|
|
912
911
|
timeout,
|
|
912
|
+
signal: AbortSignal.timeout(timeout), // hard deadline (covers DNS/connect); see _get
|
|
913
913
|
}, (res) => {
|
|
914
914
|
let data = '';
|
|
915
915
|
res.on('data', (chunk) => { data += chunk; });
|
|
@@ -952,6 +952,7 @@ class WorkspaceClient {
|
|
|
952
952
|
method: 'PATCH',
|
|
953
953
|
headers: { ...headers, 'Content-Length': Buffer.byteLength(jsonBody) },
|
|
954
954
|
timeout,
|
|
955
|
+
signal: AbortSignal.timeout(timeout), // hard deadline (covers DNS/connect); see _get
|
|
955
956
|
}, (res) => {
|
|
956
957
|
let data = '';
|
|
957
958
|
res.on('data', (chunk) => { data += chunk; });
|
|
@@ -987,6 +988,7 @@ class WorkspaceClient {
|
|
|
987
988
|
method: 'DELETE',
|
|
988
989
|
headers,
|
|
989
990
|
timeout: 15000,
|
|
991
|
+
signal: AbortSignal.timeout(15000), // hard deadline (covers DNS/connect); see _get
|
|
990
992
|
}, (res) => {
|
|
991
993
|
let data = '';
|
|
992
994
|
res.on('data', (chunk) => { data += chunk; });
|