@relaycast/engine 4.1.6 → 5.0.0
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/dist/adapters/node/index.d.ts.map +1 -1
- package/dist/adapters/node/index.js +23 -1
- package/dist/adapters/node/index.js.map +1 -1
- package/dist/adapters/node/presence.d.ts +7 -3
- package/dist/adapters/node/presence.d.ts.map +1 -1
- package/dist/adapters/node/presence.js +10 -8
- package/dist/adapters/node/presence.js.map +1 -1
- package/dist/adapters/node/realtime.d.ts.map +1 -1
- package/dist/adapters/node/realtime.js +8 -3
- package/dist/adapters/node/realtime.js.map +1 -1
- package/dist/bin/serve.js +2 -4
- package/dist/bin/serve.js.map +1 -1
- package/dist/db/migrations/0021_node_delivery_contracts.sql +70 -0
- package/dist/db/migrations/0022_http_push_redrive.sql +13 -0
- package/dist/db/migrations/0023_direct_ws_nodes.sql +124 -0
- package/dist/db/migrations/0024_binding_workspace_constraints.sql +109 -0
- package/dist/db/schema.d.ts +339 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +48 -1
- package/dist/db/schema.js.map +1 -1
- package/dist/engine/action.d.ts +0 -7
- package/dist/engine/action.d.ts.map +1 -1
- package/dist/engine/action.js +0 -7
- package/dist/engine/action.js.map +1 -1
- package/dist/engine/agent.d.ts.map +1 -1
- package/dist/engine/agent.js +7 -1
- package/dist/engine/agent.js.map +1 -1
- package/dist/engine/delivery.d.ts +24 -0
- package/dist/engine/delivery.d.ts.map +1 -1
- package/dist/engine/delivery.js +190 -74
- package/dist/engine/delivery.js.map +1 -1
- package/dist/engine/deliveryWrites.d.ts +5 -0
- package/dist/engine/deliveryWrites.d.ts.map +1 -1
- package/dist/engine/deliveryWrites.js +51 -11
- package/dist/engine/deliveryWrites.js.map +1 -1
- package/dist/engine/invocationCompletion.d.ts +1 -1
- package/dist/engine/invocationCompletion.d.ts.map +1 -1
- package/dist/engine/invocationCompletion.js +1 -16
- package/dist/engine/invocationCompletion.js.map +1 -1
- package/dist/engine/message.d.ts.map +1 -1
- package/dist/engine/message.js +1 -0
- package/dist/engine/message.js.map +1 -1
- package/dist/engine/node.d.ts +85 -0
- package/dist/engine/node.d.ts.map +1 -1
- package/dist/engine/node.js +473 -14
- package/dist/engine/node.js.map +1 -1
- package/dist/engine/nodeContext.d.ts +22 -0
- package/dist/engine/nodeContext.d.ts.map +1 -0
- package/dist/engine/nodeContext.js +104 -0
- package/dist/engine/nodeContext.js.map +1 -0
- package/dist/engine/thread.d.ts.map +1 -1
- package/dist/engine/thread.js +37 -17
- package/dist/engine/thread.js.map +1 -1
- package/dist/engine/wsAuth.d.ts +36 -0
- package/dist/engine/wsAuth.d.ts.map +1 -0
- package/dist/engine/wsAuth.js +57 -0
- package/dist/engine/wsAuth.js.map +1 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +46 -74
- package/dist/engine.js.map +1 -1
- package/dist/entrypoints/node.d.ts.map +1 -1
- package/dist/entrypoints/node.js +27 -51
- package/dist/entrypoints/node.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/httpError.d.ts +5 -1
- package/dist/lib/httpError.d.ts.map +1 -1
- package/dist/lib/httpError.js +9 -5
- package/dist/lib/httpError.js.map +1 -1
- package/dist/lib/httpQuery.d.ts +28 -0
- package/dist/lib/httpQuery.d.ts.map +1 -0
- package/dist/lib/httpQuery.js +32 -0
- package/dist/lib/httpQuery.js.map +1 -0
- package/dist/lib/httpResponse.d.ts +116 -0
- package/dist/lib/httpResponse.d.ts.map +1 -0
- package/dist/lib/httpResponse.js +65 -0
- package/dist/lib/httpResponse.js.map +1 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +3 -2
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/idempotency.d.ts +30 -0
- package/dist/middleware/idempotency.d.ts.map +1 -1
- package/dist/middleware/idempotency.js +14 -0
- package/dist/middleware/idempotency.js.map +1 -1
- package/dist/middleware/planLimits.d.ts.map +1 -1
- package/dist/middleware/planLimits.js +2 -7
- package/dist/middleware/planLimits.js.map +1 -1
- package/dist/middleware/rateLimit.d.ts.map +1 -1
- package/dist/middleware/rateLimit.js +2 -7
- package/dist/middleware/rateLimit.js.map +1 -1
- package/dist/ports/index.d.ts +0 -14
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/routes/a2a.d.ts.map +1 -1
- package/dist/routes/a2a.js +41 -83
- package/dist/routes/a2a.js.map +1 -1
- package/dist/routes/action.d.ts.map +1 -1
- package/dist/routes/action.js +32 -37
- package/dist/routes/action.js.map +1 -1
- package/dist/routes/agent.d.ts.map +1 -1
- package/dist/routes/agent.js +69 -73
- package/dist/routes/agent.js.map +1 -1
- package/dist/routes/certify.d.ts.map +1 -1
- package/dist/routes/certify.js +21 -35
- package/dist/routes/certify.js.map +1 -1
- package/dist/routes/channel.d.ts.map +1 -1
- package/dist/routes/channel.js +33 -71
- package/dist/routes/channel.js.map +1 -1
- package/dist/routes/console.d.ts.map +1 -1
- package/dist/routes/console.js +22 -32
- package/dist/routes/console.js.map +1 -1
- package/dist/routes/delivery.d.ts.map +1 -1
- package/dist/routes/delivery.js +22 -54
- package/dist/routes/delivery.js.map +1 -1
- package/dist/routes/deliveryRouting.d.ts +6 -0
- package/dist/routes/deliveryRouting.d.ts.map +1 -1
- package/dist/routes/deliveryRouting.js +289 -35
- package/dist/routes/deliveryRouting.js.map +1 -1
- package/dist/routes/directory.d.ts.map +1 -1
- package/dist/routes/directory.js +47 -63
- package/dist/routes/directory.js.map +1 -1
- package/dist/routes/dm.d.ts.map +1 -1
- package/dist/routes/dm.js +21 -42
- package/dist/routes/dm.js.map +1 -1
- package/dist/routes/eventSubscription.d.ts.map +1 -1
- package/dist/routes/eventSubscription.js +15 -21
- package/dist/routes/eventSubscription.js.map +1 -1
- package/dist/routes/fanout.d.ts +2 -1
- package/dist/routes/fanout.d.ts.map +1 -1
- package/dist/routes/fanout.js +25 -15
- package/dist/routes/fanout.js.map +1 -1
- package/dist/routes/file.d.ts.map +1 -1
- package/dist/routes/file.js +34 -40
- package/dist/routes/file.js.map +1 -1
- package/dist/routes/groupDm.d.ts.map +1 -1
- package/dist/routes/groupDm.js +23 -48
- package/dist/routes/groupDm.js.map +1 -1
- package/dist/routes/inboundWebhook.d.ts.map +1 -1
- package/dist/routes/inboundWebhook.js +17 -35
- package/dist/routes/inboundWebhook.js.map +1 -1
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/inbox.js +2 -1
- package/dist/routes/inbox.js.map +1 -1
- package/dist/routes/message.d.ts.map +1 -1
- package/dist/routes/message.js +39 -50
- package/dist/routes/message.js.map +1 -1
- package/dist/routes/node.d.ts.map +1 -1
- package/dist/routes/node.js +125 -12
- package/dist/routes/node.js.map +1 -1
- package/dist/routes/presence.d.ts.map +1 -1
- package/dist/routes/presence.js +4 -3
- package/dist/routes/presence.js.map +1 -1
- package/dist/routes/reaction.d.ts.map +1 -1
- package/dist/routes/reaction.js +10 -21
- package/dist/routes/reaction.js.map +1 -1
- package/dist/routes/receipt.d.ts.map +1 -1
- package/dist/routes/receipt.js +7 -15
- package/dist/routes/receipt.js.map +1 -1
- package/dist/routes/routing.d.ts.map +1 -1
- package/dist/routes/routing.js +38 -51
- package/dist/routes/routing.js.map +1 -1
- package/dist/routes/search.d.ts.map +1 -1
- package/dist/routes/search.js +15 -12
- package/dist/routes/search.js.map +1 -1
- package/dist/routes/systemPrompt.d.ts.map +1 -1
- package/dist/routes/systemPrompt.js +8 -13
- package/dist/routes/systemPrompt.js.map +1 -1
- package/dist/routes/thread.d.ts.map +1 -1
- package/dist/routes/thread.js +17 -35
- package/dist/routes/thread.js.map +1 -1
- package/dist/routes/trigger.d.ts.map +1 -1
- package/dist/routes/trigger.js +15 -14
- package/dist/routes/trigger.js.map +1 -1
- package/dist/routes/workspace.d.ts.map +1 -1
- package/dist/routes/workspace.js +39 -220
- package/dist/routes/workspace.js.map +1 -1
- package/package.json +3 -3
- package/dist/lib/fleetNodes.d.ts +0 -9
- package/dist/lib/fleetNodes.d.ts.map +0 -1
- package/dist/lib/fleetNodes.js +0 -66
- package/dist/lib/fleetNodes.js.map +0 -1
- package/dist/lib/workspaceStream.d.ts +0 -9
- package/dist/lib/workspaceStream.d.ts.map +0 -1
- package/dist/lib/workspaceStream.js +0 -53
- package/dist/lib/workspaceStream.js.map +0 -1
- package/dist/middleware/fleetNodes.d.ts +0 -10
- package/dist/middleware/fleetNodes.d.ts.map +0 -1
- package/dist/middleware/fleetNodes.js +0 -18
- package/dist/middleware/fleetNodes.js.map +0 -1
package/dist/engine/node.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { and, eq, inArray, ne, or, sql } from 'drizzle-orm';
|
|
2
2
|
import { parseFleetBrokerToRelaycastMessage } from '@relaycast/types';
|
|
3
|
-
import { actionInvocations, actions, agents, channelMembers, channels, nodes } from '../db/schema.js';
|
|
3
|
+
import { actionInvocations, actions, agents, agentNodeBindings, channelMembers, channels, nodes } from '../db/schema.js';
|
|
4
4
|
import { randomHex, sha256Hex } from '../lib/crypto.js';
|
|
5
5
|
import { codedError } from '../lib/httpError.js';
|
|
6
6
|
import { runAtomic } from '../ports/database.js';
|
|
@@ -9,6 +9,12 @@ import { isNodeLive, nodeHasCapability } from './placement.js';
|
|
|
9
9
|
import { completeNodeInvocation, rescheduleInvocationsForLostNode, rescheduleNodeInvocation } from './action.js';
|
|
10
10
|
import { emitInvocationCompletionEffects } from './invocationCompletion.js';
|
|
11
11
|
import { ackDeliveriesUpToSeq, deliverPendingToNode } from './delivery.js';
|
|
12
|
+
export function directNodeIdForAgent(agentId) {
|
|
13
|
+
return `node_direct_${agentId}`;
|
|
14
|
+
}
|
|
15
|
+
function directNodeNameForAgent(agentId) {
|
|
16
|
+
return `direct-${agentId}`;
|
|
17
|
+
}
|
|
12
18
|
function capabilityName(capability) {
|
|
13
19
|
if (typeof capability === 'string')
|
|
14
20
|
return capability;
|
|
@@ -25,6 +31,9 @@ function publicNode(row) {
|
|
|
25
31
|
return {
|
|
26
32
|
id: row.id,
|
|
27
33
|
name: row.name,
|
|
34
|
+
kind: row.kind,
|
|
35
|
+
delivery_adapter: row.deliveryAdapter,
|
|
36
|
+
delivery: redactDeliveryConfig(row.deliveryConfig),
|
|
28
37
|
capabilities: row.capabilities,
|
|
29
38
|
tags: row.tags,
|
|
30
39
|
version: row.version,
|
|
@@ -38,6 +47,49 @@ function publicNode(row) {
|
|
|
38
47
|
created_at: row.createdAt.toISOString(),
|
|
39
48
|
};
|
|
40
49
|
}
|
|
50
|
+
const SENSITIVE_DELIVERY_KEY = /api[_-]?key|authorization|cookie|credential|headers|password|private[_-]?key|secret|token/i;
|
|
51
|
+
function redactDeliveryValue(key, value) {
|
|
52
|
+
if (key && /^headers$/i.test(key) && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
53
|
+
return Object.fromEntries(Object.keys(value).map((name) => [name, '[redacted]']));
|
|
54
|
+
}
|
|
55
|
+
if (key && SENSITIVE_DELIVERY_KEY.test(key)) {
|
|
56
|
+
return '[redacted]';
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
return value.map((item) => redactDeliveryValue(null, item));
|
|
60
|
+
}
|
|
61
|
+
if (value && typeof value === 'object') {
|
|
62
|
+
return Object.fromEntries(Object.entries(value).map(([childKey, childValue]) => [
|
|
63
|
+
childKey,
|
|
64
|
+
redactDeliveryValue(childKey, childValue),
|
|
65
|
+
]));
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
function redactDeliveryConfig(config) {
|
|
70
|
+
if (!config)
|
|
71
|
+
return null;
|
|
72
|
+
return redactDeliveryValue(null, config);
|
|
73
|
+
}
|
|
74
|
+
function defaultAdapter(kind, deliveryConfig) {
|
|
75
|
+
if (kind === 'fleet_ws')
|
|
76
|
+
return 'fleet.ws.v1';
|
|
77
|
+
if (kind === 'direct_ws')
|
|
78
|
+
return 'direct.ws.v1';
|
|
79
|
+
if (kind === 'poll')
|
|
80
|
+
return 'poll.v1';
|
|
81
|
+
const auth = deliveryConfig?.auth;
|
|
82
|
+
const authType = auth && typeof auth === 'object' && !Array.isArray(auth)
|
|
83
|
+
? auth.type
|
|
84
|
+
: undefined;
|
|
85
|
+
if (authType === 'hmac_sha256')
|
|
86
|
+
return 'http.hmac.v1';
|
|
87
|
+
if (authType === 'bearer')
|
|
88
|
+
return 'http.bearer.v1';
|
|
89
|
+
if (authType === 'static_headers')
|
|
90
|
+
return 'http.static_headers.v1';
|
|
91
|
+
return 'http.basic.v1';
|
|
92
|
+
}
|
|
41
93
|
async function ensureCapabilityActions(db, workspaceId, nodeId, capabilities) {
|
|
42
94
|
for (const capability of capabilities) {
|
|
43
95
|
const name = capabilityName(capability);
|
|
@@ -73,13 +125,22 @@ export async function createNodeToken(db, workspaceId, data) {
|
|
|
73
125
|
const tokenHash = await sha256Hex(token);
|
|
74
126
|
const existing = await getNodeByName(db, workspaceId, data.name);
|
|
75
127
|
const now = new Date();
|
|
128
|
+
const kind = data.kind ?? existing?.kind ?? 'fleet_ws';
|
|
129
|
+
const deliveryConfig = data.delivery === undefined ? existing?.deliveryConfig ?? null : data.delivery;
|
|
130
|
+
const deliveryAdapter = data.delivery_adapter
|
|
131
|
+
?? (data.delivery === undefined ? existing?.deliveryAdapter : undefined)
|
|
132
|
+
?? defaultAdapter(kind, deliveryConfig);
|
|
133
|
+
const maxAgents = data.max_agents ?? existing?.maxAgents ?? (kind === 'http_push' ? 1 : 0);
|
|
76
134
|
if (existing) {
|
|
77
135
|
const [updated] = await db
|
|
78
136
|
.update(nodes)
|
|
79
137
|
.set({
|
|
80
138
|
tokenHash,
|
|
139
|
+
kind,
|
|
140
|
+
deliveryAdapter,
|
|
141
|
+
deliveryConfig,
|
|
81
142
|
capabilities: normalizeCapabilities(data.capabilities ?? existing.capabilities),
|
|
82
|
-
maxAgents
|
|
143
|
+
maxAgents,
|
|
83
144
|
tags: data.tags ?? existing.tags,
|
|
84
145
|
version: data.version ?? existing.version,
|
|
85
146
|
status: 'offline',
|
|
@@ -96,8 +157,11 @@ export async function createNodeToken(db, workspaceId, data) {
|
|
|
96
157
|
workspaceId,
|
|
97
158
|
name: data.name,
|
|
98
159
|
tokenHash,
|
|
160
|
+
kind,
|
|
161
|
+
deliveryAdapter,
|
|
162
|
+
deliveryConfig,
|
|
99
163
|
capabilities: normalizeCapabilities(data.capabilities ?? []),
|
|
100
|
-
maxAgents
|
|
164
|
+
maxAgents,
|
|
101
165
|
tags: data.tags ?? [],
|
|
102
166
|
version: data.version ?? 'unknown',
|
|
103
167
|
status: 'offline',
|
|
@@ -138,6 +202,9 @@ export async function registerNode(db, workspaceId, authenticatedNodeId, message
|
|
|
138
202
|
.set({
|
|
139
203
|
name: message.name,
|
|
140
204
|
capabilities: message.capabilities,
|
|
205
|
+
kind: 'fleet_ws',
|
|
206
|
+
deliveryAdapter: 'fleet.ws.v1',
|
|
207
|
+
deliveryConfig: null,
|
|
141
208
|
maxAgents: message.max_agents,
|
|
142
209
|
tags: message.tags,
|
|
143
210
|
version: message.version,
|
|
@@ -232,8 +299,339 @@ async function autoJoinGeneral(db, workspaceId, agentId) {
|
|
|
232
299
|
}).onConflictDoNothing();
|
|
233
300
|
}
|
|
234
301
|
}
|
|
302
|
+
async function upsertAgentNodeBinding(db, workspaceId, agent, nodeId, opts = {}) {
|
|
303
|
+
if (opts.deactivateExisting ?? true) {
|
|
304
|
+
await db
|
|
305
|
+
.update(agentNodeBindings)
|
|
306
|
+
.set({ status: 'inactive', updatedAt: new Date() })
|
|
307
|
+
.where(and(eq(agentNodeBindings.workspaceId, workspaceId), eq(agentNodeBindings.agentId, agent.id), eq(agentNodeBindings.status, 'active'), ne(agentNodeBindings.nodeId, nodeId)));
|
|
308
|
+
}
|
|
309
|
+
await db
|
|
310
|
+
.insert(agentNodeBindings)
|
|
311
|
+
.values({
|
|
312
|
+
id: `anb_${generateId()}`,
|
|
313
|
+
workspaceId,
|
|
314
|
+
agentId: agent.id,
|
|
315
|
+
nodeId,
|
|
316
|
+
status: 'active',
|
|
317
|
+
sessionRef: opts.sessionRef ?? null,
|
|
318
|
+
priority: opts.priority ?? 0,
|
|
319
|
+
updatedAt: new Date(),
|
|
320
|
+
})
|
|
321
|
+
.onConflictDoUpdate({
|
|
322
|
+
target: [agentNodeBindings.agentId, agentNodeBindings.nodeId],
|
|
323
|
+
set: {
|
|
324
|
+
status: 'active',
|
|
325
|
+
sessionRef: opts.sessionRef ?? null,
|
|
326
|
+
priority: opts.priority ?? 0,
|
|
327
|
+
updatedAt: new Date(),
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
await db
|
|
331
|
+
.update(agents)
|
|
332
|
+
.set({
|
|
333
|
+
locationType: 'via_node',
|
|
334
|
+
locationNodeId: nodeId,
|
|
335
|
+
sessionRef: opts.sessionRef ?? undefined,
|
|
336
|
+
status: 'active',
|
|
337
|
+
lastSeen: new Date(),
|
|
338
|
+
})
|
|
339
|
+
.where(and(eq(agents.workspaceId, workspaceId), eq(agents.id, agent.id)));
|
|
340
|
+
}
|
|
341
|
+
async function activeBindingNodeIdsForAgent(db, workspaceId, agentId) {
|
|
342
|
+
const rows = await db
|
|
343
|
+
.select({ nodeId: agentNodeBindings.nodeId })
|
|
344
|
+
.from(agentNodeBindings)
|
|
345
|
+
.where(and(eq(agentNodeBindings.workspaceId, workspaceId), eq(agentNodeBindings.agentId, agentId), eq(agentNodeBindings.status, 'active')));
|
|
346
|
+
return rows.map((row) => row.nodeId);
|
|
347
|
+
}
|
|
348
|
+
async function reserveNodeAgentSlot(db, workspaceId, node) {
|
|
349
|
+
const [reserved] = await db
|
|
350
|
+
.update(nodes)
|
|
351
|
+
.set({
|
|
352
|
+
activeAgents: sql `${nodes.activeAgents} + 1`,
|
|
353
|
+
})
|
|
354
|
+
.where(and(eq(nodes.workspaceId, workspaceId), eq(nodes.id, node.id), or(eq(nodes.maxAgents, 0), sql `${nodes.activeAgents} < ${nodes.maxAgents}`)))
|
|
355
|
+
.returning({ id: nodes.id });
|
|
356
|
+
if (!reserved) {
|
|
357
|
+
throw codedError(`Node "${node.name}" is at capacity`, 'node_capacity_exceeded', 409);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async function releaseNodeAgentSlots(db, workspaceId, nodeIds) {
|
|
361
|
+
const uniqueNodeIds = [...new Set(nodeIds)];
|
|
362
|
+
if (uniqueNodeIds.length === 0)
|
|
363
|
+
return;
|
|
364
|
+
await db
|
|
365
|
+
.update(nodes)
|
|
366
|
+
.set({
|
|
367
|
+
activeAgents: sql `CASE WHEN ${nodes.activeAgents} > 0 THEN ${nodes.activeAgents} - 1 ELSE 0 END`,
|
|
368
|
+
})
|
|
369
|
+
.where(and(eq(nodes.workspaceId, workspaceId), inArray(nodes.id, uniqueNodeIds)));
|
|
370
|
+
}
|
|
371
|
+
export async function ensureDirectNodeForAgent(db, workspaceId, agent, opts = {}) {
|
|
372
|
+
return runAtomic(db, (tx) => ensureDirectNodeForAgentInTx(tx, workspaceId, agent, opts));
|
|
373
|
+
}
|
|
374
|
+
async function ensureDirectNodeForAgentInTx(db, workspaceId, agent, opts = {}) {
|
|
375
|
+
const activeNodeIds = await activeBindingNodeIdsForAgent(db, workspaceId, agent.id);
|
|
376
|
+
const nodeId = directNodeIdForAgent(agent.id);
|
|
377
|
+
const alreadyDirect = activeNodeIds.includes(nodeId);
|
|
378
|
+
const explicitNodeIds = activeNodeIds.filter((activeNodeId) => activeNodeId !== nodeId);
|
|
379
|
+
if (!opts.force && explicitNodeIds.length > 0) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const now = new Date();
|
|
383
|
+
const [existingDirect] = await db
|
|
384
|
+
.select()
|
|
385
|
+
.from(nodes)
|
|
386
|
+
.where(and(eq(nodes.workspaceId, workspaceId), eq(nodes.id, nodeId)));
|
|
387
|
+
if (!opts.force && alreadyDirect && existingDirect && agent.locationNodeId === nodeId) {
|
|
388
|
+
const update = {
|
|
389
|
+
name: directNodeNameForAgent(agent.id),
|
|
390
|
+
kind: 'direct_ws',
|
|
391
|
+
deliveryAdapter: 'direct.ws.v1',
|
|
392
|
+
deliveryConfig: {
|
|
393
|
+
implicit: true,
|
|
394
|
+
agent_id: agent.id,
|
|
395
|
+
agent_name: agent.name,
|
|
396
|
+
},
|
|
397
|
+
capabilities: [],
|
|
398
|
+
maxAgents: 1,
|
|
399
|
+
activeAgents: 1,
|
|
400
|
+
tags: ['implicit', 'direct_ws'],
|
|
401
|
+
version: 'implicit',
|
|
402
|
+
handlersLive: false,
|
|
403
|
+
load: 0,
|
|
404
|
+
};
|
|
405
|
+
if (opts.online) {
|
|
406
|
+
update.status = 'online';
|
|
407
|
+
update.lastHeartbeatAt = now;
|
|
408
|
+
}
|
|
409
|
+
const [updatedDirect] = await db
|
|
410
|
+
.update(nodes)
|
|
411
|
+
.set(update)
|
|
412
|
+
.where(and(eq(nodes.workspaceId, workspaceId), eq(nodes.id, nodeId)))
|
|
413
|
+
.returning();
|
|
414
|
+
return publicNode(updatedDirect ?? {
|
|
415
|
+
...existingDirect,
|
|
416
|
+
...update,
|
|
417
|
+
activeAgents: 1,
|
|
418
|
+
status: opts.online ? 'online' : existingDirect.status,
|
|
419
|
+
lastHeartbeatAt: opts.online ? now : existingDirect.lastHeartbeatAt,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
let directNode = existingDirect;
|
|
423
|
+
if (!directNode) {
|
|
424
|
+
const tokenHash = await sha256Hex(`implicit_direct:${workspaceId}:${agent.id}:${randomHex(16)}`);
|
|
425
|
+
[directNode] = await db
|
|
426
|
+
.insert(nodes)
|
|
427
|
+
.values({
|
|
428
|
+
id: nodeId,
|
|
429
|
+
workspaceId,
|
|
430
|
+
name: directNodeNameForAgent(agent.id),
|
|
431
|
+
tokenHash,
|
|
432
|
+
kind: 'direct_ws',
|
|
433
|
+
deliveryAdapter: 'direct.ws.v1',
|
|
434
|
+
deliveryConfig: {
|
|
435
|
+
implicit: true,
|
|
436
|
+
agent_id: agent.id,
|
|
437
|
+
agent_name: agent.name,
|
|
438
|
+
},
|
|
439
|
+
capabilities: [],
|
|
440
|
+
maxAgents: 1,
|
|
441
|
+
activeAgents: 0,
|
|
442
|
+
tags: ['implicit', 'direct_ws'],
|
|
443
|
+
version: 'implicit',
|
|
444
|
+
status: opts.online ? 'online' : 'offline',
|
|
445
|
+
handlersLive: false,
|
|
446
|
+
load: 0,
|
|
447
|
+
lastHeartbeatAt: opts.online ? now : null,
|
|
448
|
+
createdAt: now,
|
|
449
|
+
})
|
|
450
|
+
.returning();
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
const update = {
|
|
454
|
+
name: directNodeNameForAgent(agent.id),
|
|
455
|
+
kind: 'direct_ws',
|
|
456
|
+
deliveryAdapter: 'direct.ws.v1',
|
|
457
|
+
deliveryConfig: {
|
|
458
|
+
implicit: true,
|
|
459
|
+
agent_id: agent.id,
|
|
460
|
+
agent_name: agent.name,
|
|
461
|
+
},
|
|
462
|
+
capabilities: [],
|
|
463
|
+
maxAgents: 1,
|
|
464
|
+
tags: ['implicit', 'direct_ws'],
|
|
465
|
+
version: 'implicit',
|
|
466
|
+
handlersLive: false,
|
|
467
|
+
load: 0,
|
|
468
|
+
};
|
|
469
|
+
if (opts.online) {
|
|
470
|
+
update.status = 'online';
|
|
471
|
+
update.lastHeartbeatAt = now;
|
|
472
|
+
}
|
|
473
|
+
[directNode] = await db
|
|
474
|
+
.update(nodes)
|
|
475
|
+
.set(update)
|
|
476
|
+
.where(and(eq(nodes.workspaceId, workspaceId), eq(nodes.id, nodeId)))
|
|
477
|
+
.returning();
|
|
478
|
+
}
|
|
479
|
+
await upsertAgentNodeBinding(db, workspaceId, agent, nodeId, {
|
|
480
|
+
sessionRef: opts.sessionRef ?? null,
|
|
481
|
+
deactivateExisting: true,
|
|
482
|
+
});
|
|
483
|
+
await releaseNodeAgentSlots(db, workspaceId, explicitNodeIds);
|
|
484
|
+
await db
|
|
485
|
+
.update(nodes)
|
|
486
|
+
.set({
|
|
487
|
+
activeAgents: 1,
|
|
488
|
+
status: opts.online ? 'online' : directNode.status,
|
|
489
|
+
lastHeartbeatAt: opts.online ? now : directNode.lastHeartbeatAt,
|
|
490
|
+
})
|
|
491
|
+
.where(and(eq(nodes.workspaceId, workspaceId), eq(nodes.id, nodeId)));
|
|
492
|
+
return publicNode({
|
|
493
|
+
...directNode,
|
|
494
|
+
activeAgents: 1,
|
|
495
|
+
status: opts.online ? 'online' : directNode.status,
|
|
496
|
+
lastHeartbeatAt: opts.online ? now : directNode.lastHeartbeatAt,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
export async function markDirectNodeOfflineForAgent(db, workspaceId, agentId) {
|
|
500
|
+
const nodeId = directNodeIdForAgent(agentId);
|
|
501
|
+
await db
|
|
502
|
+
.update(nodes)
|
|
503
|
+
.set({
|
|
504
|
+
status: 'offline',
|
|
505
|
+
handlersLive: false,
|
|
506
|
+
load: 0,
|
|
507
|
+
lastHeartbeatAt: new Date(),
|
|
508
|
+
})
|
|
509
|
+
.where(and(eq(nodes.workspaceId, workspaceId), eq(nodes.id, nodeId)));
|
|
510
|
+
}
|
|
511
|
+
function serializeBinding(row) {
|
|
512
|
+
return {
|
|
513
|
+
id: row.id,
|
|
514
|
+
agent_id: row.agentId,
|
|
515
|
+
agent_name: row.agentName,
|
|
516
|
+
node_id: row.nodeId,
|
|
517
|
+
node_name: row.nodeName,
|
|
518
|
+
node_kind: row.nodeKind,
|
|
519
|
+
status: row.status,
|
|
520
|
+
session_ref: row.sessionRef,
|
|
521
|
+
priority: row.priority,
|
|
522
|
+
created_at: row.createdAt.toISOString(),
|
|
523
|
+
updated_at: row.updatedAt?.toISOString() ?? null,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
export async function bindAgentToNode(db, workspaceId, nodeName, agentName, opts = {}) {
|
|
527
|
+
return runAtomic(db, async (tx) => {
|
|
528
|
+
const node = await getNodeByName(tx, workspaceId, nodeName);
|
|
529
|
+
if (!node)
|
|
530
|
+
throw codedError(`Node "${nodeName}" not found`, 'node_not_found', 404);
|
|
531
|
+
const [agent] = await tx
|
|
532
|
+
.select()
|
|
533
|
+
.from(agents)
|
|
534
|
+
.where(and(eq(agents.workspaceId, workspaceId), eq(agents.name, agentName)));
|
|
535
|
+
if (!agent)
|
|
536
|
+
throw codedError(`Agent "${agentName}" not found`, 'agent_not_found', 404);
|
|
537
|
+
const activeNodeIds = await activeBindingNodeIdsForAgent(tx, workspaceId, agent.id);
|
|
538
|
+
const targetWasActive = activeNodeIds.includes(node.id);
|
|
539
|
+
let reservedTargetSlot = false;
|
|
540
|
+
try {
|
|
541
|
+
if (!targetWasActive) {
|
|
542
|
+
await reserveNodeAgentSlot(tx, workspaceId, node);
|
|
543
|
+
reservedTargetSlot = true;
|
|
544
|
+
}
|
|
545
|
+
await upsertAgentNodeBinding(tx, workspaceId, agent, node.id, {
|
|
546
|
+
sessionRef: opts.session_ref ?? null,
|
|
547
|
+
priority: opts.priority ?? 0,
|
|
548
|
+
});
|
|
549
|
+
await releaseNodeAgentSlots(tx, workspaceId, activeNodeIds.filter((nodeId) => nodeId !== node.id));
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
if (reservedTargetSlot) {
|
|
553
|
+
await releaseNodeAgentSlots(tx, workspaceId, [node.id]);
|
|
554
|
+
}
|
|
555
|
+
throw err;
|
|
556
|
+
}
|
|
557
|
+
const [binding] = await tx
|
|
558
|
+
.select({
|
|
559
|
+
id: agentNodeBindings.id,
|
|
560
|
+
agentId: agentNodeBindings.agentId,
|
|
561
|
+
agentName: agents.name,
|
|
562
|
+
nodeId: agentNodeBindings.nodeId,
|
|
563
|
+
nodeName: nodes.name,
|
|
564
|
+
nodeKind: nodes.kind,
|
|
565
|
+
status: agentNodeBindings.status,
|
|
566
|
+
sessionRef: agentNodeBindings.sessionRef,
|
|
567
|
+
priority: agentNodeBindings.priority,
|
|
568
|
+
createdAt: agentNodeBindings.createdAt,
|
|
569
|
+
updatedAt: agentNodeBindings.updatedAt,
|
|
570
|
+
})
|
|
571
|
+
.from(agentNodeBindings)
|
|
572
|
+
.innerJoin(agents, eq(agentNodeBindings.agentId, agents.id))
|
|
573
|
+
.innerJoin(nodes, eq(agentNodeBindings.nodeId, nodes.id))
|
|
574
|
+
.where(and(eq(agentNodeBindings.workspaceId, workspaceId), eq(agentNodeBindings.agentId, agent.id), eq(agentNodeBindings.nodeId, node.id)));
|
|
575
|
+
return serializeBinding(binding);
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
export async function unbindAgentFromNode(db, workspaceId, nodeName, agentName) {
|
|
579
|
+
return runAtomic(db, async (tx) => {
|
|
580
|
+
const node = await getNodeByName(tx, workspaceId, nodeName);
|
|
581
|
+
if (!node)
|
|
582
|
+
return false;
|
|
583
|
+
const [agent] = await tx
|
|
584
|
+
.select()
|
|
585
|
+
.from(agents)
|
|
586
|
+
.where(and(eq(agents.workspaceId, workspaceId), eq(agents.name, agentName)));
|
|
587
|
+
if (!agent)
|
|
588
|
+
return false;
|
|
589
|
+
const updated = await tx
|
|
590
|
+
.update(agentNodeBindings)
|
|
591
|
+
.set({ status: 'inactive', updatedAt: new Date() })
|
|
592
|
+
.where(and(eq(agentNodeBindings.workspaceId, workspaceId), eq(agentNodeBindings.nodeId, node.id), eq(agentNodeBindings.agentId, agent.id), eq(agentNodeBindings.status, 'active')))
|
|
593
|
+
.returning({ id: agentNodeBindings.id });
|
|
594
|
+
if (updated.length === 0)
|
|
595
|
+
return false;
|
|
596
|
+
await releaseNodeAgentSlots(tx, workspaceId, [node.id]);
|
|
597
|
+
if (agent.locationNodeId === node.id) {
|
|
598
|
+
await ensureDirectNodeForAgentInTx(tx, workspaceId, agent, { force: true });
|
|
599
|
+
}
|
|
600
|
+
return true;
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
export async function listNodeAgents(db, workspaceId, nodeName) {
|
|
604
|
+
const node = await getNodeByName(db, workspaceId, nodeName);
|
|
605
|
+
if (!node)
|
|
606
|
+
return null;
|
|
607
|
+
const rows = await db
|
|
608
|
+
.select({
|
|
609
|
+
id: agentNodeBindings.id,
|
|
610
|
+
agentId: agentNodeBindings.agentId,
|
|
611
|
+
agentName: agents.name,
|
|
612
|
+
nodeId: agentNodeBindings.nodeId,
|
|
613
|
+
nodeName: nodes.name,
|
|
614
|
+
nodeKind: nodes.kind,
|
|
615
|
+
status: agentNodeBindings.status,
|
|
616
|
+
sessionRef: agentNodeBindings.sessionRef,
|
|
617
|
+
priority: agentNodeBindings.priority,
|
|
618
|
+
createdAt: agentNodeBindings.createdAt,
|
|
619
|
+
updatedAt: agentNodeBindings.updatedAt,
|
|
620
|
+
})
|
|
621
|
+
.from(agentNodeBindings)
|
|
622
|
+
.innerJoin(agents, eq(agentNodeBindings.agentId, agents.id))
|
|
623
|
+
.innerJoin(nodes, eq(agentNodeBindings.nodeId, nodes.id))
|
|
624
|
+
.where(and(eq(agentNodeBindings.workspaceId, workspaceId), eq(agentNodeBindings.nodeId, node.id), eq(agentNodeBindings.status, 'active')));
|
|
625
|
+
return rows.map(serializeBinding);
|
|
626
|
+
}
|
|
235
627
|
export async function registerAgentViaNode(db, workspaceId, nodeId, message) {
|
|
236
628
|
return runAtomic(db, async (tx) => {
|
|
629
|
+
const [node] = await tx
|
|
630
|
+
.select()
|
|
631
|
+
.from(nodes)
|
|
632
|
+
.where(and(eq(nodes.workspaceId, workspaceId), eq(nodes.id, nodeId)));
|
|
633
|
+
if (!node)
|
|
634
|
+
throw codedError(`Node "${nodeId}" not found`, 'node_not_found', 404);
|
|
237
635
|
const token = `at_live_${randomHex(16)}`;
|
|
238
636
|
const tokenHash = await sha256Hex(token);
|
|
239
637
|
const now = new Date().toISOString();
|
|
@@ -280,6 +678,26 @@ export async function registerAgentViaNode(db, workspaceId, nodeId, message) {
|
|
|
280
678
|
throw codedError(`Agent "${message.name}" already has an active location`, 'agent_location_conflict', 409);
|
|
281
679
|
}
|
|
282
680
|
await autoJoinGeneral(tx, workspaceId, result.id);
|
|
681
|
+
const activeNodeIds = await activeBindingNodeIdsForAgent(tx, workspaceId, result.id);
|
|
682
|
+
const targetWasActive = activeNodeIds.includes(nodeId);
|
|
683
|
+
let reservedTargetSlot = false;
|
|
684
|
+
try {
|
|
685
|
+
if (!targetWasActive) {
|
|
686
|
+
await reserveNodeAgentSlot(tx, workspaceId, node);
|
|
687
|
+
reservedTargetSlot = true;
|
|
688
|
+
}
|
|
689
|
+
await upsertAgentNodeBinding(tx, workspaceId, result, nodeId, {
|
|
690
|
+
sessionRef: message.session_ref ?? null,
|
|
691
|
+
deactivateExisting: true,
|
|
692
|
+
});
|
|
693
|
+
await releaseNodeAgentSlots(tx, workspaceId, activeNodeIds.filter((activeNodeId) => activeNodeId !== nodeId));
|
|
694
|
+
}
|
|
695
|
+
catch (err) {
|
|
696
|
+
if (reservedTargetSlot) {
|
|
697
|
+
await releaseNodeAgentSlots(tx, workspaceId, [nodeId]);
|
|
698
|
+
}
|
|
699
|
+
throw err;
|
|
700
|
+
}
|
|
283
701
|
return {
|
|
284
702
|
agent_id: result.id,
|
|
285
703
|
name: result.name,
|
|
@@ -307,12 +725,33 @@ export async function deregisterAgentViaNode(db, workspaceId, nodeId, message) {
|
|
|
307
725
|
.set({ status: 'offline', lastSeen: new Date() })
|
|
308
726
|
.where(and(...conditions))
|
|
309
727
|
.returning();
|
|
728
|
+
if (updated) {
|
|
729
|
+
const activeNodeIds = await activeBindingNodeIdsForAgent(db, workspaceId, updated.id);
|
|
730
|
+
await db
|
|
731
|
+
.update(agentNodeBindings)
|
|
732
|
+
.set({ status: 'inactive', updatedAt: new Date() })
|
|
733
|
+
.where(and(eq(agentNodeBindings.workspaceId, workspaceId), eq(agentNodeBindings.nodeId, nodeId), eq(agentNodeBindings.agentId, updated.id), eq(agentNodeBindings.status, 'active')));
|
|
734
|
+
await releaseNodeAgentSlots(db, workspaceId, [nodeId]);
|
|
735
|
+
await ensureDirectNodeForAgent(db, workspaceId, updated, { force: true });
|
|
736
|
+
await db
|
|
737
|
+
.update(agents)
|
|
738
|
+
.set({ status: 'offline', lastSeen: new Date() })
|
|
739
|
+
.where(and(eq(agents.workspaceId, workspaceId), eq(agents.id, updated.id)));
|
|
740
|
+
await markDirectNodeOfflineForAgent(db, workspaceId, updated.id);
|
|
741
|
+
await releaseNodeAgentSlots(db, workspaceId, activeNodeIds.filter((activeNodeId) => activeNodeId !== nodeId));
|
|
742
|
+
}
|
|
310
743
|
return updated ?? null;
|
|
311
744
|
}
|
|
312
745
|
export async function reconcileInventory(db, registry, workspaceId, nodeId, inventoryAgents, completionDeps) {
|
|
313
746
|
const names = new Set(inventoryAgents.map((agent) => agent.name));
|
|
314
747
|
const liveInvocationIds = new Set(inventoryAgents.flatMap((agent) => (agent.invocation_id ? [agent.invocation_id] : [])));
|
|
315
748
|
let completedInvocations = 0;
|
|
749
|
+
const [node] = await db
|
|
750
|
+
.select()
|
|
751
|
+
.from(nodes)
|
|
752
|
+
.where(and(eq(nodes.workspaceId, workspaceId), eq(nodes.id, nodeId)));
|
|
753
|
+
if (!node)
|
|
754
|
+
throw codedError(`Node "${nodeId}" not found`, 'node_not_found', 404);
|
|
316
755
|
// Pre-validate every item against the current state BEFORE mutating anything,
|
|
317
756
|
// so a conflict on a later item can't leave earlier items partially
|
|
318
757
|
// reconciled (the control handler turns a throw into an error reply). Existing
|
|
@@ -349,17 +788,37 @@ export async function reconcileInventory(db, registry, workspaceId, nodeId, inve
|
|
|
349
788
|
for (const item of inventoryAgents) {
|
|
350
789
|
const existing = existingByName.get(item.name);
|
|
351
790
|
if (existing) {
|
|
352
|
-
await db
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
791
|
+
const activeNodeIds = await activeBindingNodeIdsForAgent(db, workspaceId, existing.id);
|
|
792
|
+
const targetWasActive = activeNodeIds.includes(nodeId);
|
|
793
|
+
let reservedTargetSlot = false;
|
|
794
|
+
try {
|
|
795
|
+
if (!targetWasActive) {
|
|
796
|
+
await reserveNodeAgentSlot(db, workspaceId, node);
|
|
797
|
+
reservedTargetSlot = true;
|
|
798
|
+
}
|
|
799
|
+
await db
|
|
800
|
+
.update(agents)
|
|
801
|
+
.set({
|
|
802
|
+
status: 'active',
|
|
803
|
+
lastSeen: new Date(),
|
|
804
|
+
locationType: 'via_node',
|
|
805
|
+
locationNodeId: nodeId,
|
|
806
|
+
originNodeId: existing.originNodeId ?? nodeId,
|
|
807
|
+
sessionRef: item.session_ref ?? existing.sessionRef,
|
|
808
|
+
})
|
|
809
|
+
.where(eq(agents.id, existing.id));
|
|
810
|
+
await upsertAgentNodeBinding(db, workspaceId, existing, nodeId, {
|
|
811
|
+
sessionRef: item.session_ref ?? existing.sessionRef,
|
|
812
|
+
deactivateExisting: true,
|
|
813
|
+
});
|
|
814
|
+
await releaseNodeAgentSlots(db, workspaceId, activeNodeIds.filter((activeNodeId) => activeNodeId !== nodeId));
|
|
815
|
+
}
|
|
816
|
+
catch (err) {
|
|
817
|
+
if (reservedTargetSlot) {
|
|
818
|
+
await releaseNodeAgentSlots(db, workspaceId, [nodeId]);
|
|
819
|
+
}
|
|
820
|
+
throw err;
|
|
821
|
+
}
|
|
363
822
|
}
|
|
364
823
|
if (!item.invocation_id)
|
|
365
824
|
continue;
|