@openscout/runtime 0.2.29 → 0.2.31

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.
@@ -6,7 +6,7 @@ import { createInMemoryControlRuntime } from "./broker.js";
6
6
  import { FileBackedBrokerJournal } from "./broker-journal.js";
7
7
  import { buildCollaborationInvocation } from "./collaboration-invocations.js";
8
8
  import { discoverMeshNodes } from "./mesh-discovery.js";
9
- import { buildMeshCollaborationEventBundle, buildMeshCollaborationRecordBundle, buildMeshInvocationBundle, buildMeshMessageBundle, forwardMeshCollaborationEvent, forwardMeshCollaborationRecord, forwardMeshInvocation, forwardMeshMessage, } from "./mesh-forwarding.js";
9
+ import { buildMeshCollaborationEventBundle, buildMeshCollaborationRecordBundle, buildMeshInvocationBundle, buildMeshMessageBundle, forwardMeshCollaborationEvent, forwardMeshCollaborationRecord, forwardMeshInvocation, fetchPeerAgents, forwardMeshMessage, } from "./mesh-forwarding.js";
10
10
  import { ensureLocalAgentBindingOnline, isLocalAgentEndpointAlive, isLocalAgentSessionAlive, invokeLocalAgentEndpoint, loadRegisteredLocalAgentBindings, shouldDisableGeneratedCodexEndpoint, } from "./local-agents.js";
11
11
  import { RecoverableSQLiteProjection } from "./sqlite-projection.js";
12
12
  import { ensureOpenScoutCleanSlateSync } from "./support-paths.js";
@@ -144,6 +144,31 @@ async function discoverPeers(seeds = []) {
144
144
  for (const node of result.discovered) {
145
145
  await upsertNodeDurably(node);
146
146
  }
147
+ // Sync agents from each discovered peer so local broker knows about remote agents.
148
+ // This enables @mention resolution and message forwarding across the mesh.
149
+ for (const node of result.discovered) {
150
+ if (!node.brokerUrl)
151
+ continue;
152
+ try {
153
+ const peerAgents = await fetchPeerAgents(node.brokerUrl);
154
+ for (const agent of peerAgents) {
155
+ if (agent.id === nodeId)
156
+ continue;
157
+ const remoteAgent = {
158
+ ...agent,
159
+ homeNodeId: agent.homeNodeId || node.id,
160
+ authorityNodeId: agent.authorityNodeId || node.id,
161
+ };
162
+ await upsertAgentDurably(remoteAgent);
163
+ }
164
+ if (peerAgents.length > 0) {
165
+ console.log(`[openscout-runtime] synced ${peerAgents.length} agent(s) from peer ${node.name || node.id}`);
166
+ }
167
+ }
168
+ catch {
169
+ // Best-effort: peer may be temporarily unreachable
170
+ }
171
+ }
147
172
  return result.discovered;
148
173
  }
149
174
  function currentLocalNode() {
@@ -58,6 +58,7 @@ export async function discoverMeshNodes(options) {
58
58
  const candidates = unique([...peerSeeds, ...tailscaleSeeds])
59
59
  .filter((seed) => seed && seed !== options.localBrokerUrl);
60
60
  const discovered = [];
61
+ const seen = new Set();
61
62
  for (const seed of candidates) {
62
63
  const node = await probeNode(seed, options.timeoutMs ?? 1500);
63
64
  if (!node)
@@ -66,6 +67,9 @@ export async function discoverMeshNodes(options) {
66
67
  continue;
67
68
  if (node.meshId !== options.meshId)
68
69
  continue;
70
+ if (seen.has(node.id))
71
+ continue;
72
+ seen.add(node.id);
69
73
  discovered.push({
70
74
  ...node,
71
75
  brokerUrl: node.brokerUrl ?? seed,
@@ -55,3 +55,4 @@ export declare function forwardMeshCollaborationEvent(brokerUrl: string, bundle:
55
55
  ok: true;
56
56
  duplicate?: boolean;
57
57
  }>;
58
+ export declare function fetchPeerAgents(brokerUrl: string): Promise<AgentDefinition[]>;
@@ -179,3 +179,14 @@ export async function forwardMeshCollaborationRecord(brokerUrl, bundle) {
179
179
  export async function forwardMeshCollaborationEvent(brokerUrl, bundle) {
180
180
  return postJson(`${brokerUrl.replace(/\/$/, "")}/v1/mesh/collaboration/events`, bundle);
181
181
  }
182
+ export async function fetchPeerAgents(brokerUrl) {
183
+ const url = `${brokerUrl.replace(/\/$/, "")}/v1/snapshot`;
184
+ const response = await fetch(url, {
185
+ headers: { accept: "application/json" },
186
+ signal: AbortSignal.timeout(5_000),
187
+ });
188
+ if (!response.ok)
189
+ return [];
190
+ const snapshot = await response.json();
191
+ return Object.values(snapshot.agents ?? {});
192
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openscout/runtime",
3
- "version": "0.2.29",
3
+ "version": "0.2.31",
4
4
  "description": "Local runtime foundation for the OpenScout control plane",
5
5
  "type": "module",
6
6
  "bin": {
@@ -86,7 +86,7 @@
86
86
  "postpack": "node ../../scripts/restore-publish-manifest.mjs ."
87
87
  },
88
88
  "dependencies": {
89
- "@openscout/protocol": "0.2.29",
89
+ "@openscout/protocol": "0.2.31",
90
90
  "smol-toml": "^1.6.1"
91
91
  },
92
92
  "publishConfig": {
@@ -34,6 +34,7 @@ import {
34
34
  forwardMeshCollaborationEvent,
35
35
  forwardMeshCollaborationRecord,
36
36
  forwardMeshInvocation,
37
+ fetchPeerAgents,
37
38
  forwardMeshMessage,
38
39
  type MeshCollaborationEventBundle,
39
40
  type MeshCollaborationRecordBundle,
@@ -220,6 +221,29 @@ async function discoverPeers(seeds: string[] = []): Promise<NodeDefinition[]> {
220
221
  await upsertNodeDurably(node);
221
222
  }
222
223
 
224
+ // Sync agents from each discovered peer so local broker knows about remote agents.
225
+ // This enables @mention resolution and message forwarding across the mesh.
226
+ for (const node of result.discovered) {
227
+ if (!node.brokerUrl) continue;
228
+ try {
229
+ const peerAgents = await fetchPeerAgents(node.brokerUrl);
230
+ for (const agent of peerAgents) {
231
+ if (agent.id === nodeId) continue;
232
+ const remoteAgent: AgentDefinition = {
233
+ ...agent,
234
+ homeNodeId: agent.homeNodeId || node.id,
235
+ authorityNodeId: agent.authorityNodeId || node.id,
236
+ };
237
+ await upsertAgentDurably(remoteAgent);
238
+ }
239
+ if (peerAgents.length > 0) {
240
+ console.log(`[openscout-runtime] synced ${peerAgents.length} agent(s) from peer ${node.name || node.id}`);
241
+ }
242
+ } catch {
243
+ // Best-effort: peer may be temporarily unreachable
244
+ }
245
+ }
246
+
223
247
  return result.discovered;
224
248
  }
225
249
 
@@ -82,12 +82,15 @@ export async function discoverMeshNodes(
82
82
  .filter((seed) => seed && seed !== options.localBrokerUrl);
83
83
 
84
84
  const discovered: NodeDefinition[] = [];
85
+ const seen = new Set<string>();
85
86
 
86
87
  for (const seed of candidates) {
87
88
  const node = await probeNode(seed, options.timeoutMs ?? 1500);
88
89
  if (!node) continue;
89
90
  if (node.id === options.localNodeId) continue;
90
91
  if (node.meshId !== options.meshId) continue;
92
+ if (seen.has(node.id)) continue;
93
+ seen.add(node.id);
91
94
 
92
95
  discovered.push({
93
96
  ...node,
@@ -297,3 +297,16 @@ export async function forwardMeshCollaborationEvent(
297
297
  ): Promise<{ ok: true; duplicate?: boolean }> {
298
298
  return postJson(`${brokerUrl.replace(/\/$/, "")}/v1/mesh/collaboration/events`, bundle);
299
299
  }
300
+
301
+ export async function fetchPeerAgents(
302
+ brokerUrl: string,
303
+ ): Promise<AgentDefinition[]> {
304
+ const url = `${brokerUrl.replace(/\/$/, "")}/v1/snapshot`;
305
+ const response = await fetch(url, {
306
+ headers: { accept: "application/json" },
307
+ signal: AbortSignal.timeout(5_000),
308
+ });
309
+ if (!response.ok) return [];
310
+ const snapshot = await response.json() as { agents?: Record<string, AgentDefinition> };
311
+ return Object.values(snapshot.agents ?? {});
312
+ }