@openagents-org/agent-launcher 0.2.103 → 0.2.109
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/src/adapters/base.js +42 -12
- package/src/adapters/claude.js +1 -1
- package/src/adapters/cursor.js +1 -1
- package/src/adapters/llm-direct.js +1 -1
- package/src/adapters/nanoclaw.js +1 -1
- package/src/adapters/openclaw.js +1 -1
- package/src/adapters/opencode.js +1 -1
- package/src/adapters/utils.js +1 -1
- package/src/adapters/workspace-prompt.js +7 -3
- package/src/daemon.js +62 -0
- package/src/identity.js +1 -1
- package/src/mcp-server.js +4 -2
- package/src/workspace-client.js +60 -14
package/package.json
CHANGED
package/src/adapters/base.js
CHANGED
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Subclasses must implement _handleMessage(msg).
|
|
14
14
|
*
|
|
15
|
-
* Direct port of Python: src/openagents/adapters/base.py
|
|
15
|
+
* Direct port of Python: sdk/src/openagents/adapters/base.py
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
'use strict';
|
|
19
19
|
|
|
20
|
-
const { WorkspaceClient } = require('../workspace-client');
|
|
20
|
+
const { WorkspaceClient, SessionRevokedError } = require('../workspace-client');
|
|
21
21
|
const { generateSessionTitle, SESSION_DEFAULT_RE } = require('./utils');
|
|
22
22
|
|
|
23
23
|
const DEFAULT_ENDPOINT = 'https://workspace-endpoint.openagents.org';
|
|
@@ -43,6 +43,7 @@ class BaseAdapter {
|
|
|
43
43
|
this.client = new WorkspaceClient(this.endpoint);
|
|
44
44
|
this._lastEventId = null;
|
|
45
45
|
this._running = false;
|
|
46
|
+
this._sessionId = null; // issued by server on /v1/join; used to prove liveness
|
|
46
47
|
this._processedIds = new Set();
|
|
47
48
|
this._titledSessions = new Set();
|
|
48
49
|
this._mode = 'execute';
|
|
@@ -65,13 +66,14 @@ class BaseAdapter {
|
|
|
65
66
|
|
|
66
67
|
// Announce agent to workspace
|
|
67
68
|
try {
|
|
68
|
-
await this.client.joinNetwork(this.agentName, this.token, {
|
|
69
|
+
const joinResult = await this.client.joinNetwork(this.agentName, this.token, {
|
|
69
70
|
network: this.workspaceId,
|
|
70
71
|
agentType: this.agentType || 'agent',
|
|
71
72
|
serverHost: require('os').hostname(),
|
|
72
73
|
workingDir: this.workingDir || process.cwd(),
|
|
73
74
|
});
|
|
74
|
-
this.
|
|
75
|
+
this._sessionId = (joinResult && joinResult.session_id) || null;
|
|
76
|
+
this._log(`Joined workspace ${this.workspaceId}${this._sessionId ? ` (session ${this._sessionId.slice(0, 8)})` : ''}`);
|
|
75
77
|
} catch (e) {
|
|
76
78
|
this._log(`Warning: join failed: ${e.message}`);
|
|
77
79
|
}
|
|
@@ -130,8 +132,13 @@ class BaseAdapter {
|
|
|
130
132
|
|
|
131
133
|
async _heartbeat() {
|
|
132
134
|
try {
|
|
133
|
-
await this.client.heartbeat(this.workspaceId, this.agentName, this.token);
|
|
135
|
+
await this.client.heartbeat(this.workspaceId, this.agentName, this.token, this._sessionId);
|
|
134
136
|
} catch (e) {
|
|
137
|
+
if (e instanceof SessionRevokedError) {
|
|
138
|
+
this._log(`SESSION REVOKED: another client joined as '${this.agentName}'. Stopping adapter.`);
|
|
139
|
+
this._running = false;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
135
142
|
this._log(`Heartbeat failed: ${e.message}`);
|
|
136
143
|
}
|
|
137
144
|
}
|
|
@@ -312,8 +319,11 @@ class BaseAdapter {
|
|
|
312
319
|
senderName: this.agentName,
|
|
313
320
|
messageType: 'status',
|
|
314
321
|
metadata: { agent_mode: this._mode },
|
|
322
|
+
sessionId: this._sessionId,
|
|
315
323
|
});
|
|
316
|
-
} catch {
|
|
324
|
+
} catch (e) {
|
|
325
|
+
if (e instanceof SessionRevokedError) this._onSessionRevoked();
|
|
326
|
+
}
|
|
317
327
|
}
|
|
318
328
|
|
|
319
329
|
async sendThinking(channel, content) {
|
|
@@ -323,15 +333,27 @@ class BaseAdapter {
|
|
|
323
333
|
senderName: this.agentName,
|
|
324
334
|
messageType: 'thinking',
|
|
325
335
|
metadata: { agent_mode: this._mode },
|
|
336
|
+
sessionId: this._sessionId,
|
|
326
337
|
});
|
|
327
|
-
} catch {
|
|
338
|
+
} catch (e) {
|
|
339
|
+
if (e instanceof SessionRevokedError) this._onSessionRevoked();
|
|
340
|
+
}
|
|
328
341
|
}
|
|
329
342
|
|
|
330
343
|
async sendResponse(channel, content) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
344
|
+
try {
|
|
345
|
+
await this.client.sendMessage(this.workspaceId, channel, this.token, content, {
|
|
346
|
+
senderType: 'agent',
|
|
347
|
+
senderName: this.agentName,
|
|
348
|
+
sessionId: this._sessionId,
|
|
349
|
+
});
|
|
350
|
+
} catch (e) {
|
|
351
|
+
if (e instanceof SessionRevokedError) {
|
|
352
|
+
this._onSessionRevoked();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
throw e;
|
|
356
|
+
}
|
|
335
357
|
}
|
|
336
358
|
|
|
337
359
|
async sendError(channel, error) {
|
|
@@ -339,8 +361,16 @@ class BaseAdapter {
|
|
|
339
361
|
await this.client.sendMessage(this.workspaceId, channel, this.token, error, {
|
|
340
362
|
senderType: 'agent',
|
|
341
363
|
senderName: this.agentName,
|
|
364
|
+
sessionId: this._sessionId,
|
|
342
365
|
});
|
|
343
|
-
} catch {
|
|
366
|
+
} catch (e) {
|
|
367
|
+
if (e instanceof SessionRevokedError) this._onSessionRevoked();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
_onSessionRevoked() {
|
|
372
|
+
this._log(`SESSION REVOKED: another client joined as '${this.agentName}'. Stopping adapter.`);
|
|
373
|
+
this._running = false;
|
|
344
374
|
}
|
|
345
375
|
|
|
346
376
|
// ------------------------------------------------------------------
|
package/src/adapters/claude.js
CHANGED
package/src/adapters/cursor.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Calls OpenAI-compatible chat completions API directly with SSE streaming.
|
|
5
5
|
* No CLI binary needed — just OPENAI_API_KEY + OPENAI_BASE_URL.
|
|
6
6
|
*
|
|
7
|
-
* Port of Python: src/openagents/adapters/nanoclaw.py & cursor.py
|
|
7
|
+
* Port of Python: sdk/src/openagents/adapters/nanoclaw.py & cursor.py
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
'use strict';
|
package/src/adapters/nanoclaw.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* NanoClaw adapter — lightweight containerized coding agent.
|
|
3
3
|
*
|
|
4
4
|
* Uses direct LLM API mode (OpenAI-compatible chat completions).
|
|
5
|
-
* Port of Python: src/openagents/adapters/nanoclaw.py
|
|
5
|
+
* Port of Python: sdk/src/openagents/adapters/nanoclaw.py
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
'use strict';
|
package/src/adapters/openclaw.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - CLI mode: `openclaw agent --local --json` (preferred)
|
|
6
6
|
* - Workspace context injected via SKILL.md auto-discovery
|
|
7
7
|
*
|
|
8
|
-
* Direct port of Python: src/openagents/adapters/openclaw.py
|
|
8
|
+
* Direct port of Python: sdk/src/openagents/adapters/openclaw.py
|
|
9
9
|
* (CLI mode only — gateway WS and direct HTTP modes are not yet ported)
|
|
10
10
|
*/
|
|
11
11
|
|
package/src/adapters/opencode.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `opencode run --format json` as a subprocess. OpenCode handles its own
|
|
6
6
|
* model configuration, provider selection, and tool chain.
|
|
7
7
|
*
|
|
8
|
-
* Port of Python PR #316: src/openagents/adapters/opencode.py
|
|
8
|
+
* Port of Python PR #316: sdk/src/openagents/adapters/opencode.py
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
'use strict';
|
package/src/adapters/utils.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - Multi-agent collaboration (@mention delegation)
|
|
7
7
|
* - Workspace REST API skills (files, browser, tunnels)
|
|
8
8
|
*
|
|
9
|
-
* Direct port of Python: src/openagents/adapters/workspace_prompt.py
|
|
9
|
+
* Direct port of Python: sdk/src/openagents/adapters/workspace_prompt.py
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
'use strict';
|
|
@@ -21,8 +21,12 @@ function buildWorkspaceIdentity(agentName, workspaceId, channelName, mode = 'exe
|
|
|
21
21
|
'— just write your answer naturally.\n\n' +
|
|
22
22
|
'## Workspace Context\n' +
|
|
23
23
|
`- Workspace ID: ${workspaceId}\n` +
|
|
24
|
-
`- Channel: ${channelName}\n` +
|
|
25
|
-
`- Mode: ${mode}\n`
|
|
24
|
+
`- Channel: ${channelName} (this is the channel you are currently speaking in)\n` +
|
|
25
|
+
`- Mode: ${mode}\n\n` +
|
|
26
|
+
'When you need prior context, call `workspace_get_history` with ' +
|
|
27
|
+
`\`channel="${channelName}"\` (the current channel). Without the ` +
|
|
28
|
+
'channel argument the tool falls back to a default channel that may ' +
|
|
29
|
+
'be different from where you are.\n'
|
|
26
30
|
);
|
|
27
31
|
}
|
|
28
32
|
|
package/src/daemon.js
CHANGED
|
@@ -30,6 +30,7 @@ class Daemon {
|
|
|
30
30
|
this._shuttingDown = false;
|
|
31
31
|
this._statusInterval = null;
|
|
32
32
|
this._cmdInterval = null;
|
|
33
|
+
this._reloadInFlight = null; // serialize concurrent _reload() calls
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
@@ -182,6 +183,22 @@ class Daemon {
|
|
|
182
183
|
const bin = execPath || process.execPath;
|
|
183
184
|
|
|
184
185
|
fs.mkdirSync(configDir, { recursive: true });
|
|
186
|
+
|
|
187
|
+
// Refuse to start if an existing daemon is already running.
|
|
188
|
+
// Without this check, repeated `agn up` invocations would spawn
|
|
189
|
+
// multiple daemons that each process the same message → duplicate
|
|
190
|
+
// bot replies.
|
|
191
|
+
const existingPid = Daemon._readPid(pidFile);
|
|
192
|
+
if (existingPid && Daemon._isAlive(existingPid)) {
|
|
193
|
+
console.error(`Daemon already running (PID ${existingPid}).`);
|
|
194
|
+
console.error(`Run 'agn down' first, or 'agn status' to check.`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
// Stale pid file — clean up before spawning fresh
|
|
198
|
+
if (existingPid) {
|
|
199
|
+
try { fs.unlinkSync(pidFile); } catch {}
|
|
200
|
+
}
|
|
201
|
+
|
|
185
202
|
const logFd = fs.openSync(logFile, 'a');
|
|
186
203
|
|
|
187
204
|
// Build env with enhanced PATH (ensures node/npm are findable)
|
|
@@ -636,6 +653,27 @@ class Daemon {
|
|
|
636
653
|
}
|
|
637
654
|
|
|
638
655
|
async _reload() {
|
|
656
|
+
// Serialize reloads. fs.watch, the 'reload' command, and SIGHUP can
|
|
657
|
+
// all fire concurrently. Without a mutex, two _reload() calls in flight
|
|
658
|
+
// may both observe the same stale `this._adapters[name]` entry between
|
|
659
|
+
// stopAgent() and _launchAgent(), leaving a ghost adapter running
|
|
660
|
+
// alongside the new one → duplicate bot replies per message.
|
|
661
|
+
if (this._reloadInFlight) {
|
|
662
|
+
// Wait for the in-flight reload to finish, then run once more
|
|
663
|
+
// (the config may have changed again since it started).
|
|
664
|
+
this._reloadInFlight = this._reloadInFlight.then(
|
|
665
|
+
() => this._reloadUnsafe(),
|
|
666
|
+
() => this._reloadUnsafe(),
|
|
667
|
+
);
|
|
668
|
+
return this._reloadInFlight;
|
|
669
|
+
}
|
|
670
|
+
this._reloadInFlight = this._reloadUnsafe().finally(() => {
|
|
671
|
+
this._reloadInFlight = null;
|
|
672
|
+
});
|
|
673
|
+
return this._reloadInFlight;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async _reloadUnsafe() {
|
|
639
677
|
this._log('Reloading config...');
|
|
640
678
|
const oldNames = this._cachedAgentNames || new Set();
|
|
641
679
|
const oldConfigs = this._cachedAgentConfigs || {};
|
|
@@ -657,12 +695,14 @@ class Daemon {
|
|
|
657
695
|
// Start new agents or restart agents whose network changed
|
|
658
696
|
for (const agent of newAgents) {
|
|
659
697
|
if (!oldNames.has(agent.name)) {
|
|
698
|
+
await this._ensureAdapterCleared(agent.name);
|
|
660
699
|
this._launchAgent(agent);
|
|
661
700
|
this._log(`Reload: started new agent '${agent.name}'`);
|
|
662
701
|
} else if ((oldConfigs[agent.name] || '') !== (agent.network || '')) {
|
|
663
702
|
// Network config changed — restart agent
|
|
664
703
|
await this.stopAgent(agent.name);
|
|
665
704
|
this._stoppedAgents.delete(agent.name);
|
|
705
|
+
await this._ensureAdapterCleared(agent.name);
|
|
666
706
|
this._launchAgent(agent);
|
|
667
707
|
this._log(`Reload: restarted '${agent.name}' (network changed)`);
|
|
668
708
|
}
|
|
@@ -673,6 +713,28 @@ class Daemon {
|
|
|
673
713
|
this._writeStatus();
|
|
674
714
|
}
|
|
675
715
|
|
|
716
|
+
/**
|
|
717
|
+
* Wait until the old adapter (if any) has fully released its slot in
|
|
718
|
+
* this._adapters before relaunching. stopAgent already waits up to 5s,
|
|
719
|
+
* but on slow shutdowns that can be too short — and _launchAgent's
|
|
720
|
+
* duplicate-check would then silently skip the relaunch, leaving the
|
|
721
|
+
* OLD adapter running instead of starting the new one.
|
|
722
|
+
*/
|
|
723
|
+
async _ensureAdapterCleared(name) {
|
|
724
|
+
for (let i = 0; i < 20; i++) {
|
|
725
|
+
if (!this._adapters || !this._adapters[name]) return;
|
|
726
|
+
await this._sleep(500);
|
|
727
|
+
}
|
|
728
|
+
// Last resort: force-clear the slot so the new adapter can start.
|
|
729
|
+
// The old adapter will exit on its next poll iteration since its
|
|
730
|
+
// entry in _stoppedAgents triggers adapter.stop() via checkStop.
|
|
731
|
+
if (this._adapters && this._adapters[name]) {
|
|
732
|
+
this._log(`WARNING: adapter '${name}' did not clear after 10s — force-releasing slot to avoid duplicate`);
|
|
733
|
+
try { this._adapters[name].stop(); } catch {}
|
|
734
|
+
delete this._adapters[name];
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
676
738
|
_writePid() {
|
|
677
739
|
try {
|
|
678
740
|
fs.writeFileSync(this.config.pidFile, String(process.pid), 'utf-8');
|
package/src/identity.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Local identity management (~/.openagents/identity.json).
|
|
5
5
|
*
|
|
6
|
-
* Port of Python: src/openagents/client/workspace_client.py (identity section)
|
|
6
|
+
* Port of Python: sdk/src/openagents/client/workspace_client.py (identity section)
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
package/src/mcp-server.js
CHANGED
|
@@ -27,11 +27,12 @@ function buildToolDefs(disabledModules) {
|
|
|
27
27
|
// -- Workspace core (always enabled) --
|
|
28
28
|
{
|
|
29
29
|
name: 'workspace_get_history',
|
|
30
|
-
description: 'Read recent messages in the current
|
|
30
|
+
description: 'Read recent messages in a workspace channel. Defaults to the current channel; pass channel to query another.',
|
|
31
31
|
inputSchema: {
|
|
32
32
|
type: 'object',
|
|
33
33
|
properties: {
|
|
34
34
|
limit: { type: 'integer', description: 'Number of messages to return (default 20)', default: 20 },
|
|
35
|
+
channel: { type: 'string', description: 'Channel name (e.g. "channel-9bcd8e66"); omit to use the current channel.' },
|
|
35
36
|
},
|
|
36
37
|
},
|
|
37
38
|
},
|
|
@@ -341,7 +342,8 @@ class McpServer {
|
|
|
341
342
|
|
|
342
343
|
case 'workspace_get_history': {
|
|
343
344
|
const limit = args.limit || 20;
|
|
344
|
-
const
|
|
345
|
+
const channel = args.channel || this.channelName;
|
|
346
|
+
const data = await this.ws.pollMessages(this.workspaceId, channel, this.token, { limit });
|
|
345
347
|
const events = data.events || data || [];
|
|
346
348
|
if (!events.length) return text('No messages yet.');
|
|
347
349
|
const lines = events.map((e) => {
|
package/src/workspace-client.js
CHANGED
|
@@ -5,6 +5,19 @@ const http = require('http');
|
|
|
5
5
|
|
|
6
6
|
const DEFAULT_ENDPOINT = 'https://workspace-endpoint.openagents.org';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Thrown when the workspace rejects a request because our session_id has
|
|
10
|
+
* been revoked by a newer /v1/join as the same agent. Callers should
|
|
11
|
+
* stop the adapter rather than retry.
|
|
12
|
+
*/
|
|
13
|
+
class SessionRevokedError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'SessionRevokedError';
|
|
17
|
+
this.code = 'session_revoked';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
/**
|
|
9
22
|
* HTTP client for workspace API operations.
|
|
10
23
|
*
|
|
@@ -84,12 +97,18 @@ class WorkspaceClient {
|
|
|
84
97
|
|
|
85
98
|
/**
|
|
86
99
|
* Send heartbeat via POST /v1/heartbeat.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} [sessionId] - optional session id returned by /v1/join.
|
|
102
|
+
* If the server's current session for this agent differs, _post()
|
|
103
|
+
* throws SessionRevokedError and the caller should stop its adapter.
|
|
87
104
|
*/
|
|
88
|
-
async heartbeat(workspaceId, agentName, token) {
|
|
89
|
-
const
|
|
105
|
+
async heartbeat(workspaceId, agentName, token, sessionId) {
|
|
106
|
+
const body = {
|
|
90
107
|
agent_name: agentName,
|
|
91
108
|
network: workspaceId,
|
|
92
|
-
}
|
|
109
|
+
};
|
|
110
|
+
if (sessionId) body.session_id = sessionId;
|
|
111
|
+
const data = await this._post('/v1/heartbeat', body, this._wsHeaders(token));
|
|
93
112
|
return data.data || data;
|
|
94
113
|
}
|
|
95
114
|
|
|
@@ -107,9 +126,15 @@ class WorkspaceClient {
|
|
|
107
126
|
|
|
108
127
|
/**
|
|
109
128
|
* Post a raw event via POST /v1/events.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} [sessionId] - if given, embedded in event.metadata so
|
|
131
|
+
* the server can reject stale sessions with SessionRevokedError.
|
|
110
132
|
*/
|
|
111
|
-
async sendEvent(workspaceId, event, token) {
|
|
133
|
+
async sendEvent(workspaceId, event, token, sessionId) {
|
|
112
134
|
event.network = workspaceId;
|
|
135
|
+
if (sessionId) {
|
|
136
|
+
event.metadata = { ...(event.metadata || {}), session_id: sessionId };
|
|
137
|
+
}
|
|
113
138
|
const data = await this._post('/v1/events', event, this._wsHeaders(token));
|
|
114
139
|
return data.data || data;
|
|
115
140
|
}
|
|
@@ -118,7 +143,7 @@ class WorkspaceClient {
|
|
|
118
143
|
* Send a chat message to a workspace channel.
|
|
119
144
|
*/
|
|
120
145
|
async sendMessage(workspaceId, channelName, token, content, {
|
|
121
|
-
senderType = 'agent', senderName, messageType = 'chat', metadata, attachments,
|
|
146
|
+
senderType = 'agent', senderName, messageType = 'chat', metadata, attachments, sessionId,
|
|
122
147
|
} = {}) {
|
|
123
148
|
const sourcePrefix = senderType === 'agent' ? 'openagents' : 'human';
|
|
124
149
|
const source = senderName ? `${sourcePrefix}:${senderName}` : `${sourcePrefix}:unknown`;
|
|
@@ -132,7 +157,7 @@ class WorkspaceClient {
|
|
|
132
157
|
target: `channel/${channelName}`,
|
|
133
158
|
payload,
|
|
134
159
|
metadata: metadata || {},
|
|
135
|
-
}, token);
|
|
160
|
+
}, token, sessionId);
|
|
136
161
|
}
|
|
137
162
|
|
|
138
163
|
/**
|
|
@@ -175,24 +200,41 @@ class WorkspaceClient {
|
|
|
175
200
|
cursor = events[events.length - 1].id || null;
|
|
176
201
|
}
|
|
177
202
|
|
|
178
|
-
// Filter for messages targeted at this agent
|
|
203
|
+
// Filter for messages targeted at this agent.
|
|
204
|
+
//
|
|
205
|
+
// target_agents semantics:
|
|
206
|
+
// • absent → legacy server with no routing decision
|
|
207
|
+
// (broadcast for human messages, ignore for agents)
|
|
208
|
+
// • [...agentNames] → only listed agents should respond
|
|
209
|
+
// • ["__no_response__"]
|
|
210
|
+
// → routing happened and decided nobody
|
|
211
|
+
// should respond. Sentinel is used instead
|
|
212
|
+
// of [] because pre-0.2.106 clients treat
|
|
213
|
+
// empty array as "broadcast" and every
|
|
214
|
+
// agent would reply. The sentinel is
|
|
215
|
+
// non-empty and matches no real agent.
|
|
179
216
|
const messages = [];
|
|
180
217
|
for (const e of events) {
|
|
181
218
|
const source = e.source || '';
|
|
182
219
|
const meta = e.metadata || {};
|
|
183
|
-
const targetAgents = meta.target_agents
|
|
220
|
+
const targetAgents = meta.target_agents;
|
|
221
|
+
const hasTargetList = Array.isArray(targetAgents);
|
|
184
222
|
|
|
185
223
|
// Skip own messages
|
|
186
224
|
if (source === `openagents:${agentName}`) continue;
|
|
187
225
|
|
|
188
226
|
if (source.startsWith('human:')) {
|
|
189
|
-
|
|
190
|
-
|
|
227
|
+
if (hasTargetList) {
|
|
228
|
+
if (targetAgents.includes(agentName)) {
|
|
229
|
+
messages.push(this._eventToMessage(e));
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
// Legacy server (no target_agents): broadcast for compat
|
|
191
233
|
messages.push(this._eventToMessage(e));
|
|
192
234
|
}
|
|
193
235
|
} else if (source.startsWith('openagents:')) {
|
|
194
|
-
// Agent messages: only pick up if explicitly
|
|
195
|
-
if (targetAgents.includes(agentName)) {
|
|
236
|
+
// Agent messages: only pick up if explicitly listed
|
|
237
|
+
if (hasTargetList && targetAgents.includes(agentName)) {
|
|
196
238
|
messages.push(this._eventToMessage(e));
|
|
197
239
|
}
|
|
198
240
|
}
|
|
@@ -545,7 +587,11 @@ class WorkspaceClient {
|
|
|
545
587
|
const parsed = JSON.parse(data);
|
|
546
588
|
if (res.statusCode >= 400) {
|
|
547
589
|
const msg = parsed.message || `HTTP ${res.statusCode}`;
|
|
548
|
-
|
|
590
|
+
if (typeof msg === 'string' && msg.toLowerCase().includes('session_revoked')) {
|
|
591
|
+
reject(new SessionRevokedError(msg));
|
|
592
|
+
} else {
|
|
593
|
+
reject(new Error(msg));
|
|
594
|
+
}
|
|
549
595
|
} else {
|
|
550
596
|
resolve(parsed);
|
|
551
597
|
}
|
|
@@ -634,4 +680,4 @@ class WorkspaceClient {
|
|
|
634
680
|
}
|
|
635
681
|
}
|
|
636
682
|
|
|
637
|
-
module.exports = { WorkspaceClient };
|
|
683
|
+
module.exports = { WorkspaceClient, SessionRevokedError };
|