@pcircle/memesh 4.7.3 → 4.8.1

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.
Files changed (140) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +3 -1
  4. package/README.de.md +42 -16
  5. package/README.md +46 -19
  6. package/README.zh-TW.md +42 -17
  7. package/dashboard/dist/index.html +1 -1
  8. package/dist/core/agent-message-storage.d.ts +76 -0
  9. package/dist/core/agent-message-storage.d.ts.map +1 -0
  10. package/dist/core/agent-message-storage.js +359 -0
  11. package/dist/core/agent-message-storage.js.map +1 -0
  12. package/dist/core/agent-messaging.d.ts +224 -0
  13. package/dist/core/agent-messaging.d.ts.map +1 -0
  14. package/dist/core/agent-messaging.js +881 -0
  15. package/dist/core/agent-messaging.js.map +1 -0
  16. package/dist/core/agent-router.d.ts +194 -0
  17. package/dist/core/agent-router.d.ts.map +1 -0
  18. package/dist/core/agent-router.js +1138 -0
  19. package/dist/core/agent-router.js.map +1 -0
  20. package/dist/core/briefing.d.ts.map +1 -1
  21. package/dist/core/briefing.js +8 -1
  22. package/dist/core/briefing.js.map +1 -1
  23. package/dist/core/config.d.ts.map +1 -1
  24. package/dist/core/config.js.map +1 -1
  25. package/dist/core/doctor.d.ts +19 -0
  26. package/dist/core/doctor.d.ts.map +1 -1
  27. package/dist/core/doctor.js +176 -2
  28. package/dist/core/doctor.js.map +1 -1
  29. package/dist/core/dreamer.d.ts +8 -5
  30. package/dist/core/dreamer.d.ts.map +1 -1
  31. package/dist/core/dreamer.js +88 -3
  32. package/dist/core/dreamer.js.map +1 -1
  33. package/dist/core/product-improvements.d.ts +61 -0
  34. package/dist/core/product-improvements.d.ts.map +1 -0
  35. package/dist/core/product-improvements.js +216 -0
  36. package/dist/core/product-improvements.js.map +1 -0
  37. package/dist/core/repo-state.d.ts +11 -0
  38. package/dist/core/repo-state.d.ts.map +1 -0
  39. package/dist/core/repo-state.js +70 -0
  40. package/dist/core/repo-state.js.map +1 -0
  41. package/dist/core/schema-export.d.ts.map +1 -1
  42. package/dist/core/schema-export.js +70 -0
  43. package/dist/core/schema-export.js.map +1 -1
  44. package/dist/core/task-state.d.ts.map +1 -1
  45. package/dist/core/task-state.js +3 -3
  46. package/dist/core/task-state.js.map +1 -1
  47. package/dist/core/updater.d.ts +2 -0
  48. package/dist/core/updater.d.ts.map +1 -1
  49. package/dist/core/updater.js +11 -5
  50. package/dist/core/updater.js.map +1 -1
  51. package/dist/core/work-topology.d.ts.map +1 -1
  52. package/dist/core/work-topology.js +1 -0
  53. package/dist/core/work-topology.js.map +1 -1
  54. package/dist/host-adapters/acp-client.d.ts +147 -0
  55. package/dist/host-adapters/acp-client.d.ts.map +1 -0
  56. package/dist/host-adapters/acp-client.js +793 -0
  57. package/dist/host-adapters/acp-client.js.map +1 -0
  58. package/dist/host-adapters/claude-channel.d.ts +75 -0
  59. package/dist/host-adapters/claude-channel.d.ts.map +1 -0
  60. package/dist/host-adapters/claude-channel.js +255 -0
  61. package/dist/host-adapters/claude-channel.js.map +1 -0
  62. package/dist/host-adapters/codex-app-server.d.ts +83 -0
  63. package/dist/host-adapters/codex-app-server.d.ts.map +1 -0
  64. package/dist/host-adapters/codex-app-server.js +360 -0
  65. package/dist/host-adapters/codex-app-server.js.map +1 -0
  66. package/dist/host-adapters/codex-cli-queue.d.ts +17 -0
  67. package/dist/host-adapters/codex-cli-queue.d.ts.map +1 -0
  68. package/dist/host-adapters/codex-cli-queue.js +85 -0
  69. package/dist/host-adapters/codex-cli-queue.js.map +1 -0
  70. package/dist/host-runtime/acp.d.ts +57 -0
  71. package/dist/host-runtime/acp.d.ts.map +1 -0
  72. package/dist/host-runtime/acp.js +338 -0
  73. package/dist/host-runtime/acp.js.map +1 -0
  74. package/dist/host-runtime/claude.d.ts +41 -0
  75. package/dist/host-runtime/claude.d.ts.map +1 -0
  76. package/dist/host-runtime/claude.js +213 -0
  77. package/dist/host-runtime/claude.js.map +1 -0
  78. package/dist/host-runtime/codex-session.d.ts +24 -0
  79. package/dist/host-runtime/codex-session.d.ts.map +1 -0
  80. package/dist/host-runtime/codex-session.js +98 -0
  81. package/dist/host-runtime/codex-session.js.map +1 -0
  82. package/dist/host-runtime/codex.d.ts +33 -0
  83. package/dist/host-runtime/codex.d.ts.map +1 -0
  84. package/dist/host-runtime/codex.js +215 -0
  85. package/dist/host-runtime/codex.js.map +1 -0
  86. package/dist/host-runtime/config.d.ts +9 -0
  87. package/dist/host-runtime/config.d.ts.map +1 -0
  88. package/dist/host-runtime/config.js +91 -0
  89. package/dist/host-runtime/config.js.map +1 -0
  90. package/dist/host-runtime/router-client.d.ts +38 -0
  91. package/dist/host-runtime/router-client.d.ts.map +1 -0
  92. package/dist/host-runtime/router-client.js +463 -0
  93. package/dist/host-runtime/router-client.js.map +1 -0
  94. package/dist/host-runtime/router.d.ts +3 -0
  95. package/dist/host-runtime/router.d.ts.map +1 -0
  96. package/dist/host-runtime/router.js +43 -0
  97. package/dist/host-runtime/router.js.map +1 -0
  98. package/dist/mcp/server.js +2 -2
  99. package/dist/mcp/server.js.map +1 -1
  100. package/dist/skills-manifest.json +22 -17
  101. package/dist/storage/conflicts.d.ts.map +1 -1
  102. package/dist/storage/conflicts.js +8 -1
  103. package/dist/storage/conflicts.js.map +1 -1
  104. package/dist/storage/schema.d.ts +1 -1
  105. package/dist/storage/schema.d.ts.map +1 -1
  106. package/dist/storage/schema.js +255 -0
  107. package/dist/storage/schema.js.map +1 -1
  108. package/dist/transports/agent-messaging.d.ts +11 -0
  109. package/dist/transports/agent-messaging.d.ts.map +1 -0
  110. package/dist/transports/agent-messaging.js +273 -0
  111. package/dist/transports/agent-messaging.js.map +1 -0
  112. package/dist/transports/cli/cli.d.ts +6 -1
  113. package/dist/transports/cli/cli.d.ts.map +1 -1
  114. package/dist/transports/cli/cli.js +392 -17
  115. package/dist/transports/cli/cli.js.map +1 -1
  116. package/dist/transports/http/server.d.ts.map +1 -1
  117. package/dist/transports/http/server.js +31 -2
  118. package/dist/transports/http/server.js.map +1 -1
  119. package/dist/transports/mcp/handlers.d.ts +152 -1
  120. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  121. package/dist/transports/mcp/handlers.js +108 -3
  122. package/dist/transports/mcp/handlers.js.map +1 -1
  123. package/dist/transports/schemas.d.ts +106 -1
  124. package/dist/transports/schemas.d.ts.map +1 -1
  125. package/dist/transports/schemas.js +86 -0
  126. package/dist/transports/schemas.js.map +1 -1
  127. package/docs/platforms/README.md +167 -0
  128. package/docs/platforms/agent-messaging.md +290 -0
  129. package/hooks/hooks.json +11 -0
  130. package/llms-install.md +101 -25
  131. package/package.json +14 -5
  132. package/scripts/hooks/_generated/repo-state.js +77 -0
  133. package/scripts/hooks/_generated/schema.js +255 -0
  134. package/scripts/hooks/_generated/task-state.js +3 -3
  135. package/scripts/hooks/_generated/work-topology.js +1 -0
  136. package/scripts/hooks/_shared.js +77 -97
  137. package/scripts/hooks/auto-update-runner.mjs +243 -0
  138. package/scripts/hooks/session-start.js +12 -2
  139. package/scripts/hooks/session-summary.js +7 -6
  140. package/skills/memesh/SKILL.md +40 -1
@@ -0,0 +1,1138 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import net from 'node:net';
4
+ import path from 'node:path';
5
+ import { fetchAgentMessage, } from './agent-messaging.js';
6
+ export const AGENT_ROUTER_PROTOCOL_VERSION = 1;
7
+ export const AGENT_ROUTER_MAX_FRAME_BYTES = 64 * 1024;
8
+ export const AGENT_ROUTER_MAX_HOPS = 4;
9
+ const DEFAULT_RATE_LIMIT = 120;
10
+ const DEFAULT_RATE_WINDOW_MS = 60_000;
11
+ const DEFAULT_LEASE_MS = 60_000;
12
+ const MAX_LEASE_MS = 5 * 60_000;
13
+ const DEFAULT_DRAIN_LIMIT = 200;
14
+ const DEFAULT_CLIENT_TIMEOUT_MS = 2_000;
15
+ const STALE_SOCKET_PROBE_TIMEOUT_MS = 250;
16
+ const STARTUP_LOCK_WAIT_MS = 1_000;
17
+ const STARTUP_LOCK_STALE_MS = 5_000;
18
+ const MAX_FIELD_LENGTH = 200;
19
+ const MAX_ADAPTER_RECEIPT_BYTES = 16 * 1024;
20
+ export class AgentRouterError extends Error {
21
+ code;
22
+ constructor(code, message) {
23
+ super(message);
24
+ this.code = code;
25
+ }
26
+ }
27
+ export class AgentRouterStaleGenerationError extends AgentRouterError {
28
+ constructor() {
29
+ super('stale_generation', 'The session connection generation is stale or inactive.');
30
+ }
31
+ }
32
+ export class AgentRouterProtocolError extends AgentRouterError {
33
+ }
34
+ export class AgentRouter {
35
+ router_instance_id;
36
+ socket_path;
37
+ db;
38
+ adapters;
39
+ limits;
40
+ server = null;
41
+ socketIdentity = null;
42
+ sockets = new Set();
43
+ externalConnections = new Map();
44
+ inFlightDeliveries = new Map();
45
+ pendingExternal = new Map();
46
+ constructor(options) {
47
+ this.db = options.db;
48
+ this.socket_path = validateSocketPath(options.socket_path);
49
+ this.router_instance_id = validateField('router_instance_id', options.router_instance_id ?? randomUUID());
50
+ this.limits = normalizeLimits(options.limits);
51
+ this.adapters = new Map();
52
+ for (const adapter of options.adapters) {
53
+ const kind = validateField('adapter kind', adapter.kind);
54
+ if (this.adapters.has(kind))
55
+ throw new AgentRouterError('duplicate_adapter', `Duplicate adapter ${kind}.`);
56
+ this.adapters.set(kind, adapter);
57
+ }
58
+ }
59
+ async start() {
60
+ if (this.server)
61
+ throw new AgentRouterError('already_started', 'The agent router is already started.');
62
+ if (process.platform === 'win32') {
63
+ throw new AgentRouterError('unsupported_secure_host_runtime', 'The secure local host runtime is not supported on Windows.');
64
+ }
65
+ const directory = path.dirname(this.socket_path);
66
+ if (!fs.existsSync(directory))
67
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
68
+ const directoryMode = fs.statSync(directory).mode & 0o777;
69
+ if ((directoryMode & 0o077) !== 0) {
70
+ throw new AgentRouterError('insecure_socket_directory', 'Router socket directory must be private.');
71
+ }
72
+ const startupLock = await acquireStartupLock(this.socket_path);
73
+ const server = net.createServer((socket) => this.acceptSocket(socket));
74
+ this.server = server;
75
+ try {
76
+ for (;;) {
77
+ try {
78
+ await listenOnSocket(server, this.socket_path);
79
+ break;
80
+ }
81
+ catch (error) {
82
+ if (error.code !== 'EADDRINUSE')
83
+ throw error;
84
+ if (!await removeOrphanedSocket(this.socket_path)) {
85
+ throw new AgentRouterError('router_already_running', 'A live or indeterminate router already owns the socket.');
86
+ }
87
+ }
88
+ }
89
+ fs.chmodSync(this.socket_path, 0o600);
90
+ this.socketIdentity = readSocketIdentity(this.socket_path);
91
+ }
92
+ catch (error) {
93
+ this.server = null;
94
+ this.socketIdentity = null;
95
+ try {
96
+ server.close();
97
+ }
98
+ catch { }
99
+ throw error;
100
+ }
101
+ finally {
102
+ releaseStartupLock(startupLock);
103
+ }
104
+ }
105
+ async stop() {
106
+ const server = this.server;
107
+ if (!server)
108
+ return;
109
+ this.server = null;
110
+ const socketIdentity = this.socketIdentity;
111
+ this.socketIdentity = null;
112
+ for (const socket of [...this.sockets]) {
113
+ this.disconnectSocket(socket);
114
+ socket.destroy();
115
+ }
116
+ await new Promise((resolve) => server.close(() => resolve()));
117
+ if (socketIdentity)
118
+ unlinkSocketIfSame(this.socket_path, socketIdentity);
119
+ }
120
+ acceptSocket(socket) {
121
+ this.sockets.add(socket);
122
+ let pending = Buffer.alloc(0);
123
+ let windowStartedAt = Date.now();
124
+ let frameCount = 0;
125
+ socket.on('data', (chunk) => {
126
+ pending = Buffer.concat([pending, chunk]);
127
+ if (pending.length > this.limits.max_frame_bytes && !pending.includes(0x0a)) {
128
+ this.writeError(socket, '', 'frame_too_large', 'Router frame exceeds the byte limit.');
129
+ socket.destroy();
130
+ return;
131
+ }
132
+ for (;;) {
133
+ const newline = pending.indexOf(0x0a);
134
+ if (newline < 0)
135
+ break;
136
+ const frame = pending.subarray(0, newline);
137
+ pending = pending.subarray(newline + 1);
138
+ if (frame.length === 0)
139
+ continue;
140
+ if (frame.length > this.limits.max_frame_bytes) {
141
+ this.writeError(socket, '', 'frame_too_large', 'Router frame exceeds the byte limit.');
142
+ socket.destroy();
143
+ return;
144
+ }
145
+ const now = Date.now();
146
+ if (now - windowStartedAt >= this.limits.rate_window_ms) {
147
+ windowStartedAt = now;
148
+ frameCount = 0;
149
+ }
150
+ frameCount += 1;
151
+ if (frameCount > this.limits.max_frames_per_window) {
152
+ this.writeError(socket, '', 'rate_limited', 'Router frame rate exceeded.');
153
+ socket.destroy();
154
+ return;
155
+ }
156
+ void this.handleFrame(socket, frame);
157
+ }
158
+ });
159
+ socket.on('error', () => undefined);
160
+ socket.on('close', () => this.disconnectSocket(socket));
161
+ }
162
+ async handleFrame(socket, frame) {
163
+ let requestId = '';
164
+ try {
165
+ const request = parseRequest(frame, this.limits.max_hops);
166
+ requestId = request.request_id;
167
+ const result = await this.handleRequest(request, socket);
168
+ this.writeResponse(socket, { version: 1, request_id: requestId, ok: true, result });
169
+ }
170
+ catch (error) {
171
+ const routerError = toRouterError(error);
172
+ this.writeError(socket, requestId, routerError.code, routerError.message);
173
+ }
174
+ }
175
+ async handleRequest(request, socket) {
176
+ switch (request.type) {
177
+ case 'register':
178
+ return this.register(request, socket);
179
+ case 'notify': {
180
+ const delivered = await this.dispatchDelivery(request.delivery_id, request.project, request.hops + 1);
181
+ return { delivered };
182
+ }
183
+ case 'heartbeat': {
184
+ const connection = this.requireCurrentConnection(request);
185
+ this.db.transaction(() => {
186
+ const expiresAt = Date.now() + this.limits.lease_ms;
187
+ this.db.prepare(`
188
+ UPDATE agent_session_connections SET lease_expires_at_ms = ? WHERE connection_id = ?
189
+ `).run(expiresAt, connection.connection_id);
190
+ }).immediate();
191
+ return { generation: connection.generation, lease_ms: this.limits.lease_ms };
192
+ }
193
+ case 'disconnect': {
194
+ const connection = this.requireCurrentConnection(request);
195
+ this.db.transaction(() => {
196
+ this.db.prepare(`
197
+ UPDATE agent_session_connections
198
+ SET disconnected_at = CURRENT_TIMESTAMP, disconnect_reason = 'adapter_disconnect'
199
+ WHERE connection_id = ?
200
+ `).run(connection.connection_id);
201
+ this.insertPresenceFact(connection, 'disconnected', { reason: 'adapter_disconnect' });
202
+ }).immediate();
203
+ return { disconnected: true };
204
+ }
205
+ case 'host_accept':
206
+ case 'host_reject':
207
+ return this.handleHostOutcome(request, socket);
208
+ }
209
+ }
210
+ async register(request, socket) {
211
+ const adapter = this.adapters.get(request.adapter_kind);
212
+ if (!adapter)
213
+ throw new AgentRouterError('adapter_unavailable', 'The requested host adapter is unavailable.');
214
+ const registration = {
215
+ project: request.project,
216
+ principal_id: request.principal_id,
217
+ session_instance_id: request.session_instance_id,
218
+ adapter_kind: request.adapter_kind,
219
+ ...(request.auth_token === undefined ? {} : { auth_token: request.auth_token }),
220
+ };
221
+ if (!await adapter.authenticate(registration)) {
222
+ throw new AgentRouterError('authentication_failed', 'The host adapter did not authenticate this session.');
223
+ }
224
+ const connection = this.registerConnection(registration);
225
+ this.bindExternalConnection(connection, socket);
226
+ setImmediate(() => { void this.drainConnection(connection, request.hops + 1).catch(() => undefined); });
227
+ return {
228
+ connection_id: connection.connection_id,
229
+ generation: connection.generation,
230
+ lease_ms: this.limits.lease_ms,
231
+ drain_scheduled: true,
232
+ };
233
+ }
234
+ bindExternalConnection(connection, socket) {
235
+ for (const [connectionId, bound] of this.externalConnections) {
236
+ if (bound === socket)
237
+ this.externalConnections.delete(connectionId);
238
+ }
239
+ this.externalConnections.set(connection.connection_id, socket);
240
+ }
241
+ disconnectSocket(socket) {
242
+ this.sockets.delete(socket);
243
+ for (const [connectionId, bound] of this.externalConnections) {
244
+ if (bound !== socket)
245
+ continue;
246
+ this.externalConnections.delete(connectionId);
247
+ const row = this.db.prepare(`
248
+ SELECT connection_id, project, principal_id, session_instance_id, generation,
249
+ adapter_kind, router_instance_id, lease_expires_at_ms
250
+ FROM agent_session_connections WHERE connection_id = ? AND disconnected_at IS NULL
251
+ `).get(connectionId);
252
+ if (row) {
253
+ this.db.transaction(() => {
254
+ this.db.prepare(`UPDATE agent_session_connections
255
+ SET disconnected_at = CURRENT_TIMESTAMP, disconnect_reason = 'socket_closed'
256
+ WHERE connection_id = ? AND disconnected_at IS NULL`).run(connectionId);
257
+ this.insertPresenceFact(row, 'disconnected', { reason: 'socket_closed' });
258
+ }).immediate();
259
+ }
260
+ for (const [attemptId, pending] of this.pendingExternal) {
261
+ if (pending.connection.connection_id !== connectionId)
262
+ continue;
263
+ clearTimeout(pending.timer);
264
+ this.pendingExternal.delete(attemptId);
265
+ pending.resolve({ accepted: false, receipt: { failure_code: 'adapter_disconnected' } });
266
+ }
267
+ }
268
+ }
269
+ handleHostOutcome(request, socket) {
270
+ const pending = this.pendingExternal.get(request.attempt_id);
271
+ if (!pending) {
272
+ const duplicate = this.db.prepare(`SELECT 1 FROM agent_host_accepts
273
+ WHERE delivery_id = ? AND attempt_id = ?`).get(request.delivery_id, request.attempt_id);
274
+ return { duplicate: Boolean(duplicate) };
275
+ }
276
+ if (this.externalConnections.get(request.connection_id) !== socket
277
+ || pending.connection.connection_id !== request.connection_id
278
+ || pending.connection.generation !== request.generation
279
+ || pending.delivery_id !== request.delivery_id) {
280
+ throw new AgentRouterError('outcome_scope_mismatch', 'Host outcome does not match its delivery attempt.');
281
+ }
282
+ clearTimeout(pending.timer);
283
+ this.pendingExternal.delete(request.attempt_id);
284
+ pending.resolve(request.type === 'host_accept'
285
+ ? { accepted: true, receipt: validateReceipt(request.receipt) }
286
+ : { accepted: false, receipt: { failure_code: request.failure_code } });
287
+ return { correlated: true };
288
+ }
289
+ registerConnection(registration) {
290
+ return this.db.transaction(() => {
291
+ const existingPrincipal = this.db.prepare(`
292
+ SELECT activation_event_sequence FROM agent_principals
293
+ WHERE project = ? AND principal_id = ?
294
+ `).get(registration.project, registration.principal_id);
295
+ if (!existingPrincipal) {
296
+ const checkpoint = this.db.prepare(`
297
+ SELECT COALESCE(MAX(event_sequence), 0) AS sequence
298
+ FROM agent_message_events WHERE project = ?
299
+ `).get(registration.project).sequence;
300
+ this.db.prepare(`
301
+ INSERT INTO agent_principals (project, principal_id, activation_event_sequence)
302
+ VALUES (?, ?, ?)
303
+ `).run(registration.project, registration.principal_id, checkpoint);
304
+ }
305
+ const session = this.db.prepare(`
306
+ SELECT principal_id, adapter_kind, last_generation
307
+ FROM agent_session_instances WHERE project = ? AND session_instance_id = ?
308
+ `).get(registration.project, registration.session_instance_id);
309
+ if (session && (session.principal_id !== registration.principal_id || session.adapter_kind !== registration.adapter_kind)) {
310
+ throw new AgentRouterError('session_identity_conflict', 'The session instance is already bound to another principal or adapter.');
311
+ }
312
+ if (!session) {
313
+ this.db.prepare(`
314
+ INSERT INTO agent_session_instances (
315
+ project, session_instance_id, principal_id, adapter_kind, last_generation
316
+ ) VALUES (?, ?, ?, ?, 0)
317
+ `).run(registration.project, registration.session_instance_id, registration.principal_id, registration.adapter_kind);
318
+ }
319
+ const active = this.db.prepare(`
320
+ SELECT connection_id, project, principal_id, session_instance_id, generation,
321
+ adapter_kind, router_instance_id, lease_expires_at_ms
322
+ FROM agent_session_connections
323
+ WHERE project = ? AND session_instance_id = ? AND disconnected_at IS NULL
324
+ `).all(registration.project, registration.session_instance_id);
325
+ for (const previous of active) {
326
+ const previousSocket = this.externalConnections.get(previous.connection_id);
327
+ this.externalConnections.delete(previous.connection_id);
328
+ if (previousSocket && !previousSocket.destroyed) {
329
+ previousSocket.end(`${JSON.stringify({
330
+ version: 1,
331
+ type: 'session_superseded',
332
+ connection_id: previous.connection_id,
333
+ generation: previous.generation,
334
+ })}\n`);
335
+ }
336
+ for (const [attemptId, pending] of this.pendingExternal) {
337
+ if (pending.connection.connection_id !== previous.connection_id)
338
+ continue;
339
+ clearTimeout(pending.timer);
340
+ this.pendingExternal.delete(attemptId);
341
+ pending.resolve({ accepted: false, receipt: { failure_code: 'stale_generation' } });
342
+ }
343
+ this.db.prepare(`
344
+ UPDATE agent_session_connections
345
+ SET disconnected_at = CURRENT_TIMESTAMP, disconnect_reason = 'superseded'
346
+ WHERE connection_id = ?
347
+ `).run(previous.connection_id);
348
+ this.insertPresenceFact(previous, 'superseded', { replacement_router: this.router_instance_id });
349
+ }
350
+ const generation = (session?.last_generation ?? 0) + 1;
351
+ this.db.prepare(`
352
+ UPDATE agent_session_instances SET last_generation = ?
353
+ WHERE project = ? AND session_instance_id = ?
354
+ `).run(generation, registration.project, registration.session_instance_id);
355
+ const connection = {
356
+ connection_id: randomUUID(),
357
+ project: registration.project,
358
+ principal_id: registration.principal_id,
359
+ session_instance_id: registration.session_instance_id,
360
+ generation,
361
+ adapter_kind: registration.adapter_kind,
362
+ router_instance_id: this.router_instance_id,
363
+ lease_expires_at_ms: Date.now() + this.limits.lease_ms,
364
+ };
365
+ this.db.prepare(`
366
+ INSERT INTO agent_session_connections (
367
+ connection_id, project, principal_id, session_instance_id, generation,
368
+ adapter_kind, router_instance_id, lease_expires_at_ms
369
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
370
+ `).run(connection.connection_id, connection.project, connection.principal_id, connection.session_instance_id, connection.generation, connection.adapter_kind, connection.router_instance_id, connection.lease_expires_at_ms);
371
+ this.insertPresenceFact(connection, 'connected', {});
372
+ return connection;
373
+ }).immediate();
374
+ }
375
+ requireCurrentConnection(request) {
376
+ const row = this.db.prepare(`
377
+ SELECT connection_id, project, principal_id, session_instance_id, generation,
378
+ adapter_kind, router_instance_id, lease_expires_at_ms
379
+ FROM agent_session_connections
380
+ WHERE project = ? AND session_instance_id = ? AND connection_id = ? AND generation = ?
381
+ AND router_instance_id = ? AND disconnected_at IS NULL AND lease_expires_at_ms > ?
382
+ `).get(request.project, request.session_instance_id, request.connection_id, request.generation, this.router_instance_id, Date.now());
383
+ if (!row)
384
+ throw new AgentRouterStaleGenerationError();
385
+ return row;
386
+ }
387
+ insertPresenceFact(connection, kind, detail) {
388
+ this.db.prepare(`
389
+ INSERT INTO agent_presence_facts (
390
+ presence_fact_id, project, principal_id, session_instance_id,
391
+ connection_id, generation, presence_kind, detail_json
392
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
393
+ `).run(randomUUID(), connection.project, connection.principal_id, connection.session_instance_id, connection.connection_id, connection.generation, kind, JSON.stringify(detail));
394
+ }
395
+ async drainConnection(connection, hops) {
396
+ if (hops > this.limits.max_hops)
397
+ throw new AgentRouterError('hop_limit', 'Router hop limit exceeded.');
398
+ const checkpoint = this.db.prepare(`
399
+ SELECT activation_event_sequence FROM agent_principals
400
+ WHERE project = ? AND principal_id = ?
401
+ `).get(connection.project, connection.principal_id);
402
+ const upperBound = this.db.prepare(`
403
+ SELECT COALESCE(MAX(event_sequence), 0) AS event_sequence
404
+ FROM agent_message_events WHERE project = ?
405
+ `).get(connection.project).event_sequence;
406
+ const selectBatch = this.db.prepare(`
407
+ SELECT d.delivery_id, e.event_sequence
408
+ FROM agent_message_deliveries d
409
+ JOIN agent_message_events e ON e.delivery_id = d.delivery_id
410
+ LEFT JOIN agent_host_accepts h ON h.delivery_id = d.delivery_id
411
+ WHERE d.project = ? AND h.delivery_id IS NULL AND e.event_sequence <= ?
412
+ AND (e.event_sequence > ? OR (e.event_sequence = ? AND d.delivery_id > ?))
413
+ AND (
414
+ (d.target_kind = 'principal' AND d.recipient = ? AND e.event_sequence > ?)
415
+ OR (d.target_kind = 'session' AND d.recipient = ?)
416
+ )
417
+ ORDER BY e.event_sequence ASC, d.delivery_id ASC
418
+ LIMIT ?
419
+ `);
420
+ let drained = 0;
421
+ let cursorSequence = 0;
422
+ let cursorDeliveryId = '';
423
+ for (;;) {
424
+ const rows = selectBatch.all(connection.project, upperBound, cursorSequence, cursorSequence, cursorDeliveryId, connection.principal_id, checkpoint.activation_event_sequence, connection.session_instance_id, this.limits.drain_limit);
425
+ if (rows.length === 0)
426
+ break;
427
+ for (const row of rows) {
428
+ if (await this.dispatchDelivery(row.delivery_id, connection.project, hops, connection))
429
+ drained += 1;
430
+ }
431
+ const last = rows.at(-1);
432
+ cursorSequence = last.event_sequence;
433
+ cursorDeliveryId = last.delivery_id;
434
+ }
435
+ return drained;
436
+ }
437
+ async dispatchDelivery(deliveryId, project, hops, preferred) {
438
+ const inFlight = this.inFlightDeliveries.get(deliveryId);
439
+ if (inFlight)
440
+ return await inFlight.operation;
441
+ const entry = { operation: this.dispatchDeliveryOnce(deliveryId, project, hops, preferred) };
442
+ this.inFlightDeliveries.set(deliveryId, entry);
443
+ try {
444
+ return await entry.operation;
445
+ }
446
+ finally {
447
+ if (this.inFlightDeliveries.get(deliveryId) === entry) {
448
+ this.inFlightDeliveries.delete(deliveryId);
449
+ }
450
+ }
451
+ }
452
+ async dispatchDeliveryOnce(deliveryId, project, hops, preferred) {
453
+ if (hops > this.limits.max_hops)
454
+ throw new AgentRouterError('hop_limit', 'Router hop limit exceeded.');
455
+ if (this.db.prepare('SELECT 1 FROM agent_host_accepts WHERE delivery_id = ?').get(deliveryId))
456
+ return false;
457
+ const delivery = this.loadDelivery(deliveryId, project);
458
+ const connection = this.resolveConnection(delivery, preferred);
459
+ if (!connection)
460
+ return false;
461
+ const adapter = this.adapters.get(connection.adapter_kind);
462
+ const externalSocket = this.externalConnections.get(connection.connection_id);
463
+ const metadataDispatch = adapter?.dispatch_metadata_only;
464
+ const metadataOnly = Boolean(metadataDispatch);
465
+ if (metadataOnly && !externalSocket)
466
+ return false;
467
+ if (!metadataOnly && !externalSocket && !adapter?.dispatch)
468
+ return false;
469
+ const attempt = this.beginDispatchAttempt(delivery, connection);
470
+ let result;
471
+ try {
472
+ const common = {
473
+ dispatch_id: delivery.delivery_id,
474
+ attempt_id: attempt.attempt_id,
475
+ project: delivery.project,
476
+ principal_id: connection.principal_id,
477
+ session_instance_id: connection.session_instance_id,
478
+ connection_id: connection.connection_id,
479
+ generation: connection.generation,
480
+ hops,
481
+ };
482
+ if (metadataDispatch) {
483
+ result = await metadataDispatch({
484
+ ...common,
485
+ routing: {
486
+ project: delivery.project,
487
+ recipient: delivery.recipient,
488
+ target_kind: parseTargetKind(delivery.target_kind),
489
+ message_id: delivery.message_id,
490
+ delivery_id: delivery.delivery_id,
491
+ },
492
+ });
493
+ }
494
+ else {
495
+ const envelope = fetchAgentMessage(this.db, {
496
+ project: delivery.project,
497
+ recipient: delivery.recipient,
498
+ target_kind: parseTargetKind(delivery.target_kind),
499
+ message_id: delivery.message_id,
500
+ });
501
+ const input = {
502
+ ...common,
503
+ untrusted_payload: true,
504
+ envelope,
505
+ };
506
+ result = externalSocket
507
+ ? await this.dispatchExternal(externalSocket, connection, attempt.attempt_id, input)
508
+ : await adapter.dispatch(input);
509
+ }
510
+ }
511
+ catch {
512
+ this.finishAttempt(attempt.attempt_id, 'adapter_failed', 'adapter_error');
513
+ return false;
514
+ }
515
+ if (!result.accepted) {
516
+ this.finishAttempt(attempt.attempt_id, 'adapter_rejected', adapterFailureCode(result.receipt));
517
+ return false;
518
+ }
519
+ const receipt = validateReceipt(result.receipt ?? {});
520
+ try {
521
+ this.requireCurrentConnection(connection);
522
+ if (metadataOnly && (!externalSocket
523
+ || externalSocket.destroyed
524
+ || this.externalConnections.get(connection.connection_id) !== externalSocket))
525
+ throw new AgentRouterStaleGenerationError();
526
+ }
527
+ catch {
528
+ this.finishAttempt(attempt.attempt_id, 'stale_generation', 'stale_generation');
529
+ return false;
530
+ }
531
+ return this.db.transaction(() => {
532
+ const existing = this.db.prepare(`
533
+ SELECT 1 FROM agent_host_accepts WHERE delivery_id = ?
534
+ `).get(delivery.delivery_id);
535
+ if (existing)
536
+ return false;
537
+ this.db.prepare(`
538
+ INSERT INTO agent_host_accepts (
539
+ host_accept_id, attempt_id, delivery_id, adapter_kind, receipt_json
540
+ ) VALUES (?, ?, ?, ?, ?)
541
+ `).run(randomUUID(), attempt.attempt_id, delivery.delivery_id, connection.adapter_kind, JSON.stringify(receipt));
542
+ this.finishAttempt(attempt.attempt_id, 'adapter_returned', null);
543
+ return true;
544
+ }).immediate();
545
+ }
546
+ dispatchExternal(socket, connection, attemptId, input) {
547
+ const frame = Buffer.from(`${JSON.stringify({
548
+ version: 1,
549
+ type: 'deliver',
550
+ request_id: randomUUID(),
551
+ attempt_id: attemptId,
552
+ delivery_id: input.dispatch_id,
553
+ project: input.project,
554
+ principal_id: input.principal_id,
555
+ session_instance_id: input.session_instance_id,
556
+ connection_id: input.connection_id,
557
+ generation: input.generation,
558
+ hops: input.hops,
559
+ untrusted_payload: true,
560
+ envelope: input.envelope,
561
+ })}\n`, 'utf8');
562
+ if (frame.length > this.limits.max_frame_bytes) {
563
+ return Promise.resolve({ accepted: false, receipt: { failure_code: 'frame_too_large' } });
564
+ }
565
+ return new Promise((resolve) => {
566
+ const timer = setTimeout(() => {
567
+ this.pendingExternal.delete(attemptId);
568
+ resolve({ accepted: false, receipt: { failure_code: 'host_outcome_timeout' } });
569
+ }, this.limits.delivery_timeout_ms);
570
+ timer.unref();
571
+ this.pendingExternal.set(attemptId, {
572
+ connection,
573
+ delivery_id: input.dispatch_id,
574
+ resolve,
575
+ timer,
576
+ });
577
+ if (socket.destroyed) {
578
+ clearTimeout(timer);
579
+ this.pendingExternal.delete(attemptId);
580
+ resolve({ accepted: false, receipt: { failure_code: 'adapter_disconnected' } });
581
+ return;
582
+ }
583
+ socket.write(frame);
584
+ });
585
+ }
586
+ loadDelivery(deliveryId, project) {
587
+ const row = this.db.prepare(`
588
+ SELECT d.delivery_id, d.message_id, d.project, d.recipient, d.target_kind, e.event_sequence
589
+ FROM agent_message_deliveries d
590
+ JOIN agent_message_events e ON e.delivery_id = d.delivery_id
591
+ WHERE d.delivery_id = ? AND d.project = ?
592
+ `).get(validateField('delivery_id', deliveryId), validateField('project', project));
593
+ if (!row)
594
+ throw new AgentRouterError('delivery_not_found', 'The delivery does not exist in this project.');
595
+ parseTargetKind(row.target_kind);
596
+ return row;
597
+ }
598
+ resolveConnection(delivery, preferred) {
599
+ const targetKind = parseTargetKind(delivery.target_kind);
600
+ if (preferred && this.connectionMatchesDelivery(preferred, delivery, targetKind)) {
601
+ try {
602
+ return this.requireCurrentConnection(preferred);
603
+ }
604
+ catch {
605
+ return undefined;
606
+ }
607
+ }
608
+ if (targetKind === 'principal') {
609
+ const principal = this.db.prepare(`
610
+ SELECT activation_event_sequence FROM agent_principals
611
+ WHERE project = ? AND principal_id = ?
612
+ `).get(delivery.project, delivery.recipient);
613
+ if (!principal || delivery.event_sequence <= principal.activation_event_sequence)
614
+ return undefined;
615
+ return this.db.prepare(`
616
+ SELECT connection_id, project, principal_id, session_instance_id, generation,
617
+ adapter_kind, router_instance_id, lease_expires_at_ms
618
+ FROM agent_session_connections
619
+ WHERE project = ? AND principal_id = ? AND router_instance_id = ?
620
+ AND disconnected_at IS NULL AND lease_expires_at_ms > ?
621
+ ORDER BY connected_at DESC, rowid DESC
622
+ LIMIT 1
623
+ `).get(delivery.project, delivery.recipient, this.router_instance_id, Date.now());
624
+ }
625
+ return this.db.prepare(`
626
+ SELECT connection_id, project, principal_id, session_instance_id, generation,
627
+ adapter_kind, router_instance_id, lease_expires_at_ms
628
+ FROM agent_session_connections
629
+ WHERE project = ? AND session_instance_id = ? AND router_instance_id = ?
630
+ AND disconnected_at IS NULL AND lease_expires_at_ms > ?
631
+ ORDER BY generation DESC
632
+ LIMIT 1
633
+ `).get(delivery.project, delivery.recipient, this.router_instance_id, Date.now());
634
+ }
635
+ connectionMatchesDelivery(connection, delivery, targetKind) {
636
+ if (connection.project !== delivery.project)
637
+ return false;
638
+ return targetKind === 'principal'
639
+ ? connection.principal_id === delivery.recipient
640
+ : connection.session_instance_id === delivery.recipient;
641
+ }
642
+ beginDispatchAttempt(delivery, connection) {
643
+ return this.db.transaction(() => {
644
+ this.requireCurrentConnection(connection);
645
+ if (this.db.prepare('SELECT 1 FROM agent_host_accepts WHERE delivery_id = ?').get(delivery.delivery_id)) {
646
+ throw new AgentRouterError('already_accepted', 'The delivery already has a host acceptance.');
647
+ }
648
+ const attemptNumber = this.db.prepare(`
649
+ SELECT COALESCE(MAX(attempt_number), 0) + 1 AS next
650
+ FROM agent_dispatch_attempts WHERE delivery_id = ?
651
+ `).get(delivery.delivery_id).next;
652
+ const attemptId = randomUUID();
653
+ this.db.prepare(`
654
+ INSERT INTO agent_dispatch_attempts (
655
+ attempt_id, delivery_id, project, principal_id, session_instance_id,
656
+ connection_id, generation, router_instance_id, attempt_number
657
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
658
+ `).run(attemptId, delivery.delivery_id, delivery.project, connection.principal_id, connection.session_instance_id, connection.connection_id, connection.generation, connection.router_instance_id, attemptNumber);
659
+ return { attempt_id: attemptId, attempt_number: attemptNumber };
660
+ }).immediate();
661
+ }
662
+ finishAttempt(attemptId, result, failureCode) {
663
+ this.db.prepare(`
664
+ UPDATE agent_dispatch_attempts
665
+ SET result = ?, failure_code = ?, completed_at = CURRENT_TIMESTAMP
666
+ WHERE attempt_id = ?
667
+ `).run(result, failureCode, attemptId);
668
+ }
669
+ writeError(socket, requestId, code, message) {
670
+ this.writeResponse(socket, {
671
+ version: 1,
672
+ request_id: requestId,
673
+ ok: false,
674
+ error: { code, message },
675
+ });
676
+ }
677
+ writeResponse(socket, response) {
678
+ if (!socket.destroyed)
679
+ socket.write(`${JSON.stringify(response)}\n`);
680
+ }
681
+ }
682
+ export function createAgentRouterNotifier(socketPath) {
683
+ const validatedPath = validateSocketPath(socketPath);
684
+ return {
685
+ async notify(hint) {
686
+ await sendAgentRouterRequest(validatedPath, {
687
+ version: 1,
688
+ type: 'notify',
689
+ request_id: randomUUID(),
690
+ project: hint.project,
691
+ delivery_id: hint.delivery_id,
692
+ hops: 0,
693
+ });
694
+ },
695
+ };
696
+ }
697
+ export async function sendAgentRouterRequest(socketPath, request, timeoutMs = DEFAULT_CLIENT_TIMEOUT_MS) {
698
+ const frame = Buffer.from(`${JSON.stringify(request)}\n`, 'utf8');
699
+ if (frame.length > AGENT_ROUTER_MAX_FRAME_BYTES) {
700
+ throw new AgentRouterProtocolError('frame_too_large', 'Router request exceeds the byte limit.');
701
+ }
702
+ return new Promise((resolve, reject) => {
703
+ const socket = net.createConnection(validateSocketPath(socketPath));
704
+ let pending = Buffer.alloc(0);
705
+ let settled = false;
706
+ const timer = setTimeout(() => finish(new AgentRouterProtocolError('timeout', 'Router request timed out.')), timeoutMs);
707
+ const finish = (error, value) => {
708
+ if (settled)
709
+ return;
710
+ settled = true;
711
+ clearTimeout(timer);
712
+ socket.destroy();
713
+ if (error) {
714
+ reject(error);
715
+ }
716
+ else if (value === undefined) {
717
+ reject(new AgentRouterProtocolError('invalid_response', 'Router response omitted its result object.'));
718
+ }
719
+ else {
720
+ resolve(value);
721
+ }
722
+ };
723
+ socket.on('connect', () => socket.write(frame));
724
+ socket.on('data', (chunk) => {
725
+ pending = Buffer.concat([pending, chunk]);
726
+ if (pending.length > AGENT_ROUTER_MAX_FRAME_BYTES) {
727
+ finish(new AgentRouterProtocolError('frame_too_large', 'Router response exceeds the byte limit.'));
728
+ return;
729
+ }
730
+ const newline = pending.indexOf(0x0a);
731
+ if (newline < 0)
732
+ return;
733
+ try {
734
+ const response = JSON.parse(pending.subarray(0, newline).toString('utf8'));
735
+ if (response.version !== 1 || response.request_id !== request.request_id) {
736
+ throw new AgentRouterProtocolError('invalid_response', 'Router response identity does not match.');
737
+ }
738
+ if (!response.ok)
739
+ throw new AgentRouterProtocolError(response.error.code, response.error.message);
740
+ finish(undefined, validateRouterSuccessResult(request, response.result));
741
+ }
742
+ catch (error) {
743
+ finish(error instanceof Error ? error : new Error(String(error)));
744
+ }
745
+ });
746
+ socket.on('error', (error) => finish(error));
747
+ socket.on('end', () => {
748
+ if (!settled)
749
+ finish(new AgentRouterProtocolError('connection_closed', 'Router closed without a response.'));
750
+ });
751
+ });
752
+ }
753
+ function validateRouterSuccessResult(request, value) {
754
+ if (!isPlainObject(value)) {
755
+ throw new AgentRouterProtocolError('invalid_response', 'Router response result must be an object.');
756
+ }
757
+ const requireResultString = (field) => {
758
+ const candidate = value[field];
759
+ if (typeof candidate !== 'string' || candidate.length === 0 || candidate.length > MAX_FIELD_LENGTH) {
760
+ throw new AgentRouterProtocolError('invalid_response', `Router response omitted a valid ${field}.`);
761
+ }
762
+ return candidate;
763
+ };
764
+ const requireResultInteger = (field, minimum = 1) => {
765
+ const candidate = value[field];
766
+ if (!Number.isSafeInteger(candidate) || candidate < minimum) {
767
+ throw new AgentRouterProtocolError('invalid_response', `Router response omitted a valid ${field}.`);
768
+ }
769
+ return candidate;
770
+ };
771
+ const requireResultBoolean = (field) => {
772
+ const candidate = value[field];
773
+ if (typeof candidate !== 'boolean') {
774
+ throw new AgentRouterProtocolError('invalid_response', `Router response omitted a valid ${field}.`);
775
+ }
776
+ return candidate;
777
+ };
778
+ switch (request.type) {
779
+ case 'register':
780
+ requireResultString('connection_id');
781
+ requireResultInteger('generation');
782
+ requireResultInteger('lease_ms');
783
+ requireResultBoolean('drain_scheduled');
784
+ break;
785
+ case 'notify':
786
+ requireResultBoolean('delivered');
787
+ break;
788
+ case 'heartbeat':
789
+ requireResultInteger('generation');
790
+ requireResultInteger('lease_ms');
791
+ break;
792
+ case 'disconnect':
793
+ if (value.disconnected !== true) {
794
+ throw new AgentRouterProtocolError('invalid_response', 'Router response did not confirm disconnect.');
795
+ }
796
+ break;
797
+ case 'host_accept':
798
+ case 'host_reject':
799
+ if (value.correlated !== true && typeof value.duplicate !== 'boolean') {
800
+ throw new AgentRouterProtocolError('invalid_response', 'Router response omitted its host-outcome correlation.');
801
+ }
802
+ break;
803
+ }
804
+ return value;
805
+ }
806
+ function parseRequest(frame, maxHops) {
807
+ let value;
808
+ try {
809
+ value = JSON.parse(frame.toString('utf8'));
810
+ }
811
+ catch {
812
+ throw new AgentRouterProtocolError('invalid_json', 'Router frames must contain one JSON object.');
813
+ }
814
+ if (!isPlainObject(value))
815
+ throw new AgentRouterProtocolError('invalid_frame', 'Router frame must be an object.');
816
+ if (value.version !== 1)
817
+ throw new AgentRouterProtocolError('unsupported_version', 'Unsupported router protocol version.');
818
+ const type = value.type;
819
+ const common = ['version', 'type', 'request_id', 'hops'];
820
+ const requestId = validateField('request_id', value.request_id);
821
+ const hops = validateInteger('hops', value.hops, 0, maxHops);
822
+ switch (type) {
823
+ case 'register':
824
+ assertAllowedKeys(value, [
825
+ ...common, 'project', 'principal_id', 'session_instance_id', 'adapter_kind', 'auth_token',
826
+ ]);
827
+ return {
828
+ version: 1,
829
+ type,
830
+ request_id: requestId,
831
+ project: validateField('project', value.project),
832
+ principal_id: validateField('principal_id', value.principal_id),
833
+ session_instance_id: validateField('session_instance_id', value.session_instance_id),
834
+ adapter_kind: validateField('adapter_kind', value.adapter_kind),
835
+ ...(value.auth_token === undefined ? {} : { auth_token: validateField('auth_token', value.auth_token) }),
836
+ hops,
837
+ };
838
+ case 'notify':
839
+ assertAllowedKeys(value, [...common, 'project', 'delivery_id']);
840
+ return {
841
+ version: 1,
842
+ type,
843
+ request_id: requestId,
844
+ project: validateField('project', value.project),
845
+ delivery_id: validateField('delivery_id', value.delivery_id),
846
+ hops,
847
+ };
848
+ case 'heartbeat':
849
+ case 'disconnect':
850
+ assertAllowedKeys(value, [
851
+ ...common, 'project', 'session_instance_id', 'connection_id', 'generation',
852
+ ]);
853
+ return {
854
+ version: 1,
855
+ type,
856
+ request_id: requestId,
857
+ project: validateField('project', value.project),
858
+ session_instance_id: validateField('session_instance_id', value.session_instance_id),
859
+ connection_id: validateField('connection_id', value.connection_id),
860
+ generation: validateInteger('generation', value.generation, 1, Number.MAX_SAFE_INTEGER),
861
+ hops,
862
+ };
863
+ case 'host_accept':
864
+ assertAllowedKeys(value, [
865
+ ...common, 'attempt_id', 'delivery_id', 'connection_id', 'generation', 'receipt',
866
+ ]);
867
+ if (!isPlainObject(value.receipt)) {
868
+ throw new AgentRouterProtocolError('invalid_field', 'receipt must be an object.');
869
+ }
870
+ return {
871
+ version: 1,
872
+ type,
873
+ request_id: requestId,
874
+ attempt_id: validateField('attempt_id', value.attempt_id),
875
+ delivery_id: validateField('delivery_id', value.delivery_id),
876
+ connection_id: validateField('connection_id', value.connection_id),
877
+ generation: validateInteger('generation', value.generation, 1, Number.MAX_SAFE_INTEGER),
878
+ receipt: validateReceipt(value.receipt),
879
+ hops,
880
+ };
881
+ case 'host_reject':
882
+ assertAllowedKeys(value, [
883
+ ...common, 'attempt_id', 'delivery_id', 'connection_id', 'generation', 'failure_code',
884
+ ]);
885
+ return {
886
+ version: 1,
887
+ type,
888
+ request_id: requestId,
889
+ attempt_id: validateField('attempt_id', value.attempt_id),
890
+ delivery_id: validateField('delivery_id', value.delivery_id),
891
+ connection_id: validateField('connection_id', value.connection_id),
892
+ generation: validateInteger('generation', value.generation, 1, Number.MAX_SAFE_INTEGER),
893
+ failure_code: validateField('failure_code', value.failure_code),
894
+ hops,
895
+ };
896
+ default:
897
+ throw new AgentRouterProtocolError('unsupported_type', 'Unsupported router frame type.');
898
+ }
899
+ }
900
+ function assertAllowedKeys(value, allowed) {
901
+ const allowedSet = new Set(allowed);
902
+ const unexpected = Object.keys(value).find((key) => !allowedSet.has(key));
903
+ if (unexpected) {
904
+ throw new AgentRouterProtocolError('unexpected_field', `Router frame contains unsupported field ${unexpected}.`);
905
+ }
906
+ }
907
+ function validateField(label, value) {
908
+ if (typeof value !== 'string')
909
+ throw new AgentRouterProtocolError('invalid_field', `${label} must be a string.`);
910
+ const normalized = value.trim();
911
+ if (!normalized || normalized.length > MAX_FIELD_LENGTH) {
912
+ throw new AgentRouterProtocolError('invalid_field', `${label} must contain 1-${MAX_FIELD_LENGTH} characters.`);
913
+ }
914
+ return normalized;
915
+ }
916
+ function validateInteger(label, value, min, max) {
917
+ if (!Number.isInteger(value) || value < min || value > max) {
918
+ throw new AgentRouterProtocolError('invalid_field', `${label} is outside the allowed integer range.`);
919
+ }
920
+ return value;
921
+ }
922
+ function listenOnSocket(server, socketPath) {
923
+ return new Promise((resolve, reject) => {
924
+ const onError = (error) => {
925
+ server.off('listening', onListening);
926
+ reject(error);
927
+ };
928
+ const onListening = () => {
929
+ server.off('error', onError);
930
+ resolve();
931
+ };
932
+ server.once('error', onError);
933
+ server.once('listening', onListening);
934
+ server.listen(socketPath);
935
+ });
936
+ }
937
+ async function removeOrphanedSocket(socketPath) {
938
+ const initial = lstatIfPresent(socketPath);
939
+ if (!initial)
940
+ return true;
941
+ assertSafeStaleSocketCandidate(initial);
942
+ if (await probeSocket(socketPath) !== 'refused')
943
+ return false;
944
+ const afterProbe = lstatIfPresent(socketPath);
945
+ if (!afterProbe || !sameSocketIdentity(initial, afterProbe))
946
+ return true;
947
+ if (await probeSocket(socketPath) !== 'refused')
948
+ return false;
949
+ const beforeUnlink = lstatIfPresent(socketPath);
950
+ if (!beforeUnlink || !sameSocketIdentity(initial, beforeUnlink))
951
+ return true;
952
+ fs.unlinkSync(socketPath);
953
+ return true;
954
+ }
955
+ async function acquireStartupLock(socketPath) {
956
+ const lockPath = `${socketPath}.startup.lock`;
957
+ const deadline = Date.now() + STARTUP_LOCK_WAIT_MS;
958
+ for (;;) {
959
+ try {
960
+ const fd = fs.openSync(lockPath, 'wx', 0o600);
961
+ const stat = fs.fstatSync(fd);
962
+ const lock = { fd, path: lockPath, identity: { dev: stat.dev, ino: stat.ino } };
963
+ try {
964
+ fs.writeFileSync(fd, `${process.pid}\n`, 'utf8');
965
+ return lock;
966
+ }
967
+ catch (error) {
968
+ fs.closeSync(fd);
969
+ unlinkPathIfSame(lockPath, lock.identity);
970
+ throw error;
971
+ }
972
+ }
973
+ catch (error) {
974
+ if (error.code !== 'EEXIST')
975
+ throw error;
976
+ const existing = readPrivateLockIdentity(lockPath);
977
+ if (startupLockIsStale(lockPath, existing)) {
978
+ unlinkPathIfSame(lockPath, existing);
979
+ continue;
980
+ }
981
+ if (Date.now() >= deadline) {
982
+ throw new AgentRouterError('router_start_in_progress', 'Another router process is starting.');
983
+ }
984
+ await new Promise(resolve => setTimeout(resolve, 10));
985
+ }
986
+ }
987
+ }
988
+ function readPrivateLockIdentity(lockPath) {
989
+ const stat = fs.lstatSync(lockPath);
990
+ if (!stat.isFile() || (stat.mode & 0o077) !== 0) {
991
+ throw new AgentRouterError('insecure_startup_lock', 'Router startup lock must be an owner-private regular file.');
992
+ }
993
+ if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
994
+ throw new AgentRouterError('foreign_startup_lock', 'Router startup lock is owned by another user.');
995
+ }
996
+ return { dev: stat.dev, ino: stat.ino, mtimeMs: stat.mtimeMs };
997
+ }
998
+ function startupLockIsStale(lockPath, identity) {
999
+ if (Date.now() - identity.mtimeMs >= STARTUP_LOCK_STALE_MS)
1000
+ return true;
1001
+ try {
1002
+ const raw = fs.readFileSync(lockPath, 'utf8').trim();
1003
+ if (!/^[1-9][0-9]*$/.test(raw))
1004
+ return false;
1005
+ process.kill(Number(raw), 0);
1006
+ return false;
1007
+ }
1008
+ catch (error) {
1009
+ return error.code === 'ESRCH';
1010
+ }
1011
+ }
1012
+ function releaseStartupLock(lock) {
1013
+ try {
1014
+ fs.closeSync(lock.fd);
1015
+ }
1016
+ finally {
1017
+ unlinkPathIfSame(lock.path, lock.identity);
1018
+ }
1019
+ }
1020
+ function probeSocket(socketPath) {
1021
+ return new Promise((resolve) => {
1022
+ const socket = net.createConnection(socketPath);
1023
+ let settled = false;
1024
+ const timer = setTimeout(() => finish('indeterminate'), STALE_SOCKET_PROBE_TIMEOUT_MS);
1025
+ timer.unref();
1026
+ const finish = (result) => {
1027
+ if (settled)
1028
+ return;
1029
+ settled = true;
1030
+ clearTimeout(timer);
1031
+ socket.destroy();
1032
+ resolve(result);
1033
+ };
1034
+ socket.once('connect', () => finish('connected'));
1035
+ socket.once('error', (error) => {
1036
+ if (error.code === 'ECONNREFUSED')
1037
+ finish('refused');
1038
+ else if (error.code === 'ENOENT')
1039
+ finish('missing');
1040
+ else
1041
+ finish('indeterminate');
1042
+ });
1043
+ });
1044
+ }
1045
+ function assertSafeStaleSocketCandidate(stat) {
1046
+ if (!stat.isSocket()) {
1047
+ throw new AgentRouterError('socket_path_occupied', 'Router socket path is occupied by a non-socket file.');
1048
+ }
1049
+ if ((stat.mode & 0o077) !== 0) {
1050
+ throw new AgentRouterError('insecure_existing_socket', 'Existing router socket is not owner-private.');
1051
+ }
1052
+ if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
1053
+ throw new AgentRouterError('foreign_existing_socket', 'Existing router socket is owned by another user.');
1054
+ }
1055
+ }
1056
+ function lstatIfPresent(socketPath) {
1057
+ try {
1058
+ return fs.lstatSync(socketPath);
1059
+ }
1060
+ catch (error) {
1061
+ if (error.code === 'ENOENT')
1062
+ return undefined;
1063
+ throw error;
1064
+ }
1065
+ }
1066
+ function readSocketIdentity(socketPath) {
1067
+ const stat = fs.lstatSync(socketPath);
1068
+ if (!stat.isSocket())
1069
+ throw new AgentRouterError('invalid_socket', 'Router did not create a Unix socket.');
1070
+ return { dev: stat.dev, ino: stat.ino };
1071
+ }
1072
+ function sameSocketIdentity(expected, actual) {
1073
+ return expected.dev === actual.dev && expected.ino === actual.ino;
1074
+ }
1075
+ function unlinkSocketIfSame(socketPath, expected) {
1076
+ const current = lstatIfPresent(socketPath);
1077
+ if (!current || !current.isSocket() || !sameSocketIdentity(expected, current))
1078
+ return;
1079
+ fs.unlinkSync(socketPath);
1080
+ }
1081
+ function unlinkPathIfSame(targetPath, expected) {
1082
+ const current = lstatIfPresent(targetPath);
1083
+ if (!current || !sameSocketIdentity(expected, current))
1084
+ return;
1085
+ fs.unlinkSync(targetPath);
1086
+ }
1087
+ function validateSocketPath(socketPath) {
1088
+ if (typeof socketPath !== 'string' || !path.isAbsolute(socketPath) || Buffer.byteLength(socketPath) > 103) {
1089
+ throw new AgentRouterProtocolError('invalid_socket_path', 'Router socket path must be absolute and at most 103 bytes.');
1090
+ }
1091
+ return socketPath;
1092
+ }
1093
+ function normalizeLimits(limits = {}) {
1094
+ return {
1095
+ max_frame_bytes: validateInteger('max_frame_bytes', limits.max_frame_bytes ?? AGENT_ROUTER_MAX_FRAME_BYTES, 256, AGENT_ROUTER_MAX_FRAME_BYTES),
1096
+ max_hops: validateInteger('max_hops', limits.max_hops ?? AGENT_ROUTER_MAX_HOPS, 1, 16),
1097
+ max_frames_per_window: validateInteger('max_frames_per_window', limits.max_frames_per_window ?? DEFAULT_RATE_LIMIT, 1, 10_000),
1098
+ rate_window_ms: validateInteger('rate_window_ms', limits.rate_window_ms ?? DEFAULT_RATE_WINDOW_MS, 10, 60 * 60_000),
1099
+ lease_ms: validateInteger('lease_ms', limits.lease_ms ?? DEFAULT_LEASE_MS, 100, MAX_LEASE_MS),
1100
+ drain_limit: validateInteger('drain_limit', limits.drain_limit ?? DEFAULT_DRAIN_LIMIT, 1, 10_000),
1101
+ delivery_timeout_ms: validateInteger('delivery_timeout_ms', limits.delivery_timeout_ms ?? 10_000, 100, 10 * 60_000),
1102
+ };
1103
+ }
1104
+ function validateReceipt(value) {
1105
+ if (!isPlainObject(value))
1106
+ throw new AgentRouterError('invalid_adapter_receipt', 'Adapter receipt must be an object.');
1107
+ let json;
1108
+ try {
1109
+ json = JSON.stringify(value);
1110
+ }
1111
+ catch {
1112
+ throw new AgentRouterError('invalid_adapter_receipt', 'Adapter receipt must be JSON serializable.');
1113
+ }
1114
+ if (Buffer.byteLength(json, 'utf8') > MAX_ADAPTER_RECEIPT_BYTES) {
1115
+ throw new AgentRouterError('invalid_adapter_receipt', 'Adapter receipt exceeds the byte limit.');
1116
+ }
1117
+ return value;
1118
+ }
1119
+ function parseTargetKind(value) {
1120
+ if (value === 'principal' || value === 'session')
1121
+ return value;
1122
+ throw new AgentRouterError('invalid_target_kind', 'Stored delivery target kind is invalid.');
1123
+ }
1124
+ function adapterFailureCode(receipt) {
1125
+ const value = receipt?.failure_code;
1126
+ return typeof value === 'string' && /^[a-z0-9_]{1,64}$/.test(value)
1127
+ ? value
1128
+ : 'adapter_rejected';
1129
+ }
1130
+ function isPlainObject(value) {
1131
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
1132
+ }
1133
+ function toRouterError(error) {
1134
+ if (error instanceof AgentRouterError)
1135
+ return error;
1136
+ return new AgentRouterError('internal_error', 'The router could not process the request.');
1137
+ }
1138
+ //# sourceMappingURL=agent-router.js.map