@relaycast/engine 6.1.0 → 6.2.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/delivery-maintenance.d.ts +1 -1
- package/dist/adapters/node/delivery-maintenance.d.ts.map +1 -1
- package/dist/adapters/node/delivery-maintenance.js +4 -4
- package/dist/adapters/node/delivery-maintenance.js.map +1 -1
- package/dist/adapters/node/index.js +2 -2
- package/dist/adapters/node/index.js.map +1 -1
- package/dist/adapters/node/realtime.d.ts.map +1 -1
- package/dist/adapters/node/realtime.js +45 -3
- package/dist/adapters/node/realtime.js.map +1 -1
- package/dist/agent-disconnect.d.ts +23 -1
- package/dist/agent-disconnect.d.ts.map +1 -1
- package/dist/agent-disconnect.js +20 -2
- package/dist/agent-disconnect.js.map +1 -1
- package/dist/db/migrations/0032_invocation_handler_unreachable.sql +5 -0
- package/dist/db/schema.d.ts +17 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +4 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/engine/action.d.ts +34 -13
- package/dist/engine/action.d.ts.map +1 -1
- package/dist/engine/action.js +323 -57
- package/dist/engine/action.js.map +1 -1
- package/dist/engine/delivery.d.ts +21 -1
- package/dist/engine/delivery.d.ts.map +1 -1
- package/dist/engine/delivery.js +79 -9
- package/dist/engine/delivery.js.map +1 -1
- package/dist/engine/invocationCompletion.d.ts +43 -0
- package/dist/engine/invocationCompletion.d.ts.map +1 -1
- package/dist/engine/invocationCompletion.js +108 -4
- package/dist/engine/invocationCompletion.js.map +1 -1
- package/dist/engine/node.d.ts +72 -6
- package/dist/engine/node.d.ts.map +1 -1
- package/dist/engine/node.js +152 -23
- package/dist/engine/node.js.map +1 -1
- package/dist/engine/nodeDeliver.d.ts.map +1 -1
- package/dist/engine/nodeDeliver.js +5 -0
- package/dist/engine/nodeDeliver.js.map +1 -1
- package/dist/engine/observerToken.js +1 -1
- package/dist/engine/observerToken.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/node-invocations.d.ts +1 -1
- package/dist/node-invocations.d.ts.map +1 -1
- package/dist/node-invocations.js +1 -1
- package/dist/node-invocations.js.map +1 -1
- package/dist/ports/realtime.d.ts +22 -1
- package/dist/ports/realtime.d.ts.map +1 -1
- package/dist/ports/realtime.js.map +1 -1
- package/dist/routes/action.js +11 -5
- package/dist/routes/action.js.map +1 -1
- package/dist/routes/deliveryRouting.d.ts +20 -1
- package/dist/routes/deliveryRouting.d.ts.map +1 -1
- package/dist/routes/deliveryRouting.js +114 -17
- package/dist/routes/deliveryRouting.js.map +1 -1
- package/dist/routes/node.js +3 -2
- package/dist/routes/node.js.map +1 -1
- package/dist/routes/presence.d.ts.map +1 -1
- package/dist/routes/presence.js +15 -4
- package/dist/routes/presence.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { and, eq } from 'drizzle-orm';
|
|
2
|
+
import { z } from 'zod';
|
|
1
3
|
import { transformForClient } from './wsTransform.js';
|
|
2
4
|
import { enqueueEvent } from './eventQueue.js';
|
|
5
|
+
import { actionInvocations } from '../db/schema.js';
|
|
3
6
|
import { sendNodeDeliveriesToAgents } from './nodeDeliver.js';
|
|
4
7
|
import { appendAndPublishWorkspaceEvent } from './workspaceEvents.js';
|
|
5
8
|
function buildEvent(type, workspaceId, data) {
|
|
@@ -40,22 +43,123 @@ export async function emitInvocationCompletionEffects(deps, workspaceId, result)
|
|
|
40
43
|
// Through the durable log so observers can backfill action results too.
|
|
41
44
|
fanoutTasks.push(appendAndPublishWorkspaceEvent({ db: deps.db, realtime: deps.realtime }, workspaceId, { type: eventType, payload }));
|
|
42
45
|
await Promise.allSettled(fanoutTasks);
|
|
46
|
+
await enqueueAndSendWebhook(deps, workspaceId, eventType, result);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Push one event through the durable outbox + external webhook queue. Both steps
|
|
50
|
+
* are best-effort — a caller's state transition must never fail because the
|
|
51
|
+
* outbox insert or queue send did.
|
|
52
|
+
*/
|
|
53
|
+
async function enqueueAndSendWebhook(deps, workspaceId, eventType, data) {
|
|
43
54
|
let outboxId;
|
|
44
55
|
try {
|
|
45
|
-
outboxId = await enqueueEvent(deps.db, workspaceId, eventType,
|
|
56
|
+
outboxId = await enqueueEvent(deps.db, workspaceId, eventType, data);
|
|
46
57
|
}
|
|
47
58
|
catch {
|
|
48
59
|
// Best-effort: if the durable outbox row can't be inserted, still try the queue send.
|
|
49
60
|
}
|
|
50
61
|
try {
|
|
51
|
-
await deps.webhookQueue.send(outboxId ? { type: eventType, workspaceId, data
|
|
62
|
+
await deps.webhookQueue.send(outboxId ? { type: eventType, workspaceId, data, outboxId } : {
|
|
52
63
|
type: eventType,
|
|
53
64
|
workspaceId,
|
|
54
|
-
data
|
|
65
|
+
data,
|
|
55
66
|
});
|
|
56
67
|
}
|
|
57
68
|
catch {
|
|
58
|
-
// Best-effort side effect;
|
|
69
|
+
// Best-effort side effect; the state transition must not fail because webhook delivery did.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Shape of the `metadata.fleet.invocation_id` spawn correlation. Unknown keys
|
|
73
|
+
* are stripped by default, so extra metadata fields are tolerated. */
|
|
74
|
+
const fleetInvocationSchema = z.object({
|
|
75
|
+
fleet: z.object({ invocation_id: z.string() }),
|
|
76
|
+
});
|
|
77
|
+
/**
|
|
78
|
+
* Extract the spawn invocation id an agent was registered under, if any. The
|
|
79
|
+
* `metadata.fleet.invocation_id` correlation is written by `registerAgentViaNode`
|
|
80
|
+
* on every via-node agent.
|
|
81
|
+
*/
|
|
82
|
+
export function fleetInvocationId(metadata) {
|
|
83
|
+
const parsed = fleetInvocationSchema.safeParse(metadata);
|
|
84
|
+
return parsed.success ? parsed.data.fleet.invocation_id : null;
|
|
85
|
+
}
|
|
86
|
+
/** Resolve the caller mailbox of a spawn invocation; best-effort (null on any error). */
|
|
87
|
+
async function lookupSpawnCaller(db, workspaceId, invocationId) {
|
|
88
|
+
try {
|
|
89
|
+
const [row] = await db
|
|
90
|
+
.select({ callerId: actionInvocations.callerId })
|
|
91
|
+
.from(actionInvocations)
|
|
92
|
+
.where(and(eq(actionInvocations.workspaceId, workspaceId), eq(actionInvocations.id, invocationId)));
|
|
93
|
+
return row?.callerId ?? null;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Fan out a durable `agent.exited` event across the same three channels as
|
|
101
|
+
* invocation completion: (1) the spawn caller's mailbox (when the exit
|
|
102
|
+
* correlates to a spawn invocation with a live caller), (2) the durable
|
|
103
|
+
* workspace event log + live observer stream, and (3) the webhook outbox.
|
|
104
|
+
*
|
|
105
|
+
* Best-effort by contract — every channel is isolated so the underlying agent
|
|
106
|
+
* state transition (deregister / inventory reconcile / release) never fails
|
|
107
|
+
* because an emission did.
|
|
108
|
+
*/
|
|
109
|
+
export async function emitAgentExitedEffects(deps, workspaceId, input) {
|
|
110
|
+
const eventType = 'agent.exited';
|
|
111
|
+
const eventData = {
|
|
112
|
+
agent_id: input.agentId,
|
|
113
|
+
agent_name: input.agentName,
|
|
114
|
+
node_id: input.nodeId,
|
|
115
|
+
invocation_id: input.invocationId,
|
|
116
|
+
reason: input.reason,
|
|
117
|
+
};
|
|
118
|
+
const event = buildEvent(eventType, workspaceId, eventData);
|
|
119
|
+
const payload = transformForClient(event);
|
|
120
|
+
const fanoutTasks = [];
|
|
121
|
+
// Notify the spawn caller's mailbox so a spawner learns its agent exited. The
|
|
122
|
+
// stable eventKey dedupes redundant emissions of the same logical exit.
|
|
123
|
+
const callerId = input.invocationId ? await lookupSpawnCaller(deps.db, workspaceId, input.invocationId) : null;
|
|
124
|
+
if (callerId) {
|
|
125
|
+
fanoutTasks.push(sendNodeDeliveriesToAgents({
|
|
126
|
+
db: deps.db,
|
|
127
|
+
nodeConnections: deps.nodeConnections,
|
|
128
|
+
workspaceId,
|
|
129
|
+
environment: deps.config?.environment,
|
|
130
|
+
httpPushProxy: deps.config?.httpPushProxy,
|
|
131
|
+
}, {
|
|
132
|
+
agentIds: [callerId],
|
|
133
|
+
event: eventType,
|
|
134
|
+
// Omit messageId so delivery falls back to this unique eventKey (agent +
|
|
135
|
+
// invocation + reason). A messageId keyed only on agentId would repeat
|
|
136
|
+
// across an agent's lifecycle sessions and a downstream dedup on it could
|
|
137
|
+
// drop later exit notifications.
|
|
138
|
+
eventKey: `${input.agentId}_${input.invocationId ?? 'none'}_${input.reason}`,
|
|
139
|
+
data: eventData,
|
|
140
|
+
}));
|
|
59
141
|
}
|
|
142
|
+
fanoutTasks.push(appendAndPublishWorkspaceEvent({ db: deps.db, realtime: deps.realtime }, workspaceId, { type: eventType, payload }));
|
|
143
|
+
await Promise.allSettled(fanoutTasks);
|
|
144
|
+
await enqueueAndSendWebhook(deps, workspaceId, eventType, eventData);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Fan out a durable `node.status.online` / `node.status.offline` event. Node
|
|
148
|
+
* death has no single caller mailbox, so only the durable workspace log + live
|
|
149
|
+
* stream and the webhook outbox apply. Best-effort by contract.
|
|
150
|
+
*/
|
|
151
|
+
export async function emitNodeStatusEffects(deps, workspaceId, input) {
|
|
152
|
+
const eventType = input.status === 'online' ? 'node.status.online' : 'node.status.offline';
|
|
153
|
+
const eventData = {
|
|
154
|
+
node_id: input.nodeId,
|
|
155
|
+
node_name: input.nodeName,
|
|
156
|
+
...(input.status === 'offline' && input.reason ? { reason: input.reason } : {}),
|
|
157
|
+
};
|
|
158
|
+
const event = buildEvent(eventType, workspaceId, eventData);
|
|
159
|
+
const payload = transformForClient(event);
|
|
160
|
+
await Promise.allSettled([
|
|
161
|
+
appendAndPublishWorkspaceEvent({ db: deps.db, realtime: deps.realtime }, workspaceId, { type: eventType, payload }),
|
|
162
|
+
]);
|
|
163
|
+
await enqueueAndSendWebhook(deps, workspaceId, eventType, eventData);
|
|
60
164
|
}
|
|
61
165
|
//# sourceMappingURL=invocationCompletion.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invocationCompletion.js","sourceRoot":"","sources":["../../src/engine/invocationCompletion.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"invocationCompletion.js","sourceRoot":"","sources":["../../src/engine/invocationCompletion.ts"],"names":[],"mappings":"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"}
|
package/dist/engine/node.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { getDb } from '../db/index.js';
|
|
|
3
3
|
import { agents, nodes } from '../db/schema.js';
|
|
4
4
|
import type { EngineDb } from '../ports/database.js';
|
|
5
5
|
import { type NodeConnectionRegistry } from '../ports/realtime.js';
|
|
6
|
-
import type { InvocationCompletionDeps } from './invocationCompletion.js';
|
|
6
|
+
import type { InvocationCompletionDeps, NodeOfflineReason } from './invocationCompletion.js';
|
|
7
7
|
type Db = ReturnType<typeof getDb>;
|
|
8
8
|
type NodeRow = typeof nodes.$inferSelect;
|
|
9
9
|
type AgentRow = typeof agents.$inferSelect;
|
|
@@ -148,6 +148,69 @@ export declare function getNodeByName(db: Db, workspaceId: string, name: string)
|
|
|
148
148
|
lastHeartbeatAt: Date | null;
|
|
149
149
|
createdAt: Date;
|
|
150
150
|
}>;
|
|
151
|
+
export declare function getNodeById(db: Db, workspaceId: string, nodeId: string): Promise<{
|
|
152
|
+
id: string;
|
|
153
|
+
workspaceId: string;
|
|
154
|
+
name: string;
|
|
155
|
+
tokenHash: string;
|
|
156
|
+
machineId: string | null;
|
|
157
|
+
kind: string;
|
|
158
|
+
role: string;
|
|
159
|
+
deliveryAdapter: string;
|
|
160
|
+
deliveryConfig: Record<string, unknown> | null;
|
|
161
|
+
capabilities: {
|
|
162
|
+
name: string;
|
|
163
|
+
kind?: string | undefined;
|
|
164
|
+
global?: boolean | undefined;
|
|
165
|
+
queue?: boolean | undefined;
|
|
166
|
+
metadata?: Record<string, import("@relaycast/types").FleetWireJsonValue> | undefined;
|
|
167
|
+
}[];
|
|
168
|
+
maxAgents: number;
|
|
169
|
+
activeAgents: number;
|
|
170
|
+
reservedAgents: number;
|
|
171
|
+
tags: string[];
|
|
172
|
+
version: string;
|
|
173
|
+
status: string;
|
|
174
|
+
handlersLive: boolean;
|
|
175
|
+
load: number;
|
|
176
|
+
lastHeartbeatAt: Date | null;
|
|
177
|
+
createdAt: Date;
|
|
178
|
+
}>;
|
|
179
|
+
/**
|
|
180
|
+
* Resolve the node an enroll (POST /v1/nodes) targets: by node_id when
|
|
181
|
+
* supplied, by name otherwise.
|
|
182
|
+
*/
|
|
183
|
+
export declare function resolveNodeForEnroll(db: Db, workspaceId: string, data: {
|
|
184
|
+
node_id?: string;
|
|
185
|
+
name: string;
|
|
186
|
+
}): Promise<{
|
|
187
|
+
id: string;
|
|
188
|
+
workspaceId: string;
|
|
189
|
+
name: string;
|
|
190
|
+
tokenHash: string;
|
|
191
|
+
machineId: string | null;
|
|
192
|
+
kind: string;
|
|
193
|
+
role: string;
|
|
194
|
+
deliveryAdapter: string;
|
|
195
|
+
deliveryConfig: Record<string, unknown> | null;
|
|
196
|
+
capabilities: {
|
|
197
|
+
name: string;
|
|
198
|
+
kind?: string | undefined;
|
|
199
|
+
global?: boolean | undefined;
|
|
200
|
+
queue?: boolean | undefined;
|
|
201
|
+
metadata?: Record<string, import("@relaycast/types").FleetWireJsonValue> | undefined;
|
|
202
|
+
}[];
|
|
203
|
+
maxAgents: number;
|
|
204
|
+
activeAgents: number;
|
|
205
|
+
reservedAgents: number;
|
|
206
|
+
tags: string[];
|
|
207
|
+
version: string;
|
|
208
|
+
status: string;
|
|
209
|
+
handlersLive: boolean;
|
|
210
|
+
load: number;
|
|
211
|
+
lastHeartbeatAt: Date | null;
|
|
212
|
+
createdAt: Date;
|
|
213
|
+
}>;
|
|
151
214
|
export interface RegisterNodeResult {
|
|
152
215
|
node: ReturnType<typeof publicNode>;
|
|
153
216
|
acceptance: FleetCapabilityAcceptance[];
|
|
@@ -179,20 +242,23 @@ export declare function heartbeatNode(db: Db, workspaceId: string, nodeId: strin
|
|
|
179
242
|
last_heartbeat_at: string | null;
|
|
180
243
|
created_at: string;
|
|
181
244
|
} | null>;
|
|
182
|
-
export declare function markNodeOffline(db: Db, registry: NodeConnectionRegistry, workspaceId: string, nodeId: string
|
|
245
|
+
export declare function markNodeOffline(db: Db, registry: NodeConnectionRegistry, workspaceId: string, nodeId: string, effects?: {
|
|
246
|
+
deps?: InvocationCompletionDeps;
|
|
247
|
+
reason?: NodeOfflineReason;
|
|
248
|
+
}): Promise<void>;
|
|
183
249
|
/**
|
|
184
250
|
* A provider's control connection dropped. Its capability set persists (marked
|
|
185
251
|
* offline) so the node still shows the manifest; agents it hosted go offline.
|
|
186
252
|
* When it was the node's last connection the whole node goes offline.
|
|
187
253
|
*/
|
|
188
|
-
export declare function handleProviderDisconnect(db: Db, registry: NodeConnectionRegistry, workspaceId: string, nodeId: string, providerName: string, hasRemainingConnections: boolean): Promise<void>;
|
|
254
|
+
export declare function handleProviderDisconnect(db: Db, registry: NodeConnectionRegistry, workspaceId: string, nodeId: string, providerName: string, hasRemainingConnections: boolean, deps?: InvocationCompletionDeps): Promise<void>;
|
|
189
255
|
/**
|
|
190
256
|
* Provider-scoped deregister: removes the provider's attachment, persisted
|
|
191
257
|
* capability set, and materialized actions. When it was the node's last
|
|
192
258
|
* provider the node goes fully offline.
|
|
193
259
|
*/
|
|
194
|
-
export declare function deregisterProvider(db: Db, registry: NodeConnectionRegistry, workspaceId: string, nodeId: string, providerName: string): Promise<void>;
|
|
195
|
-
export declare function sweepOfflineNodes(db: Db, registry: NodeConnectionRegistry): Promise<number>;
|
|
260
|
+
export declare function deregisterProvider(db: Db, registry: NodeConnectionRegistry, workspaceId: string, nodeId: string, providerName: string, deps?: InvocationCompletionDeps): Promise<void>;
|
|
261
|
+
export declare function sweepOfflineNodes(db: Db, registry: NodeConnectionRegistry, deps?: InvocationCompletionDeps): Promise<number>;
|
|
196
262
|
export declare function ensureDirectNodeForAgent(db: Db, workspaceId: string, agent: Pick<AgentRow, 'id' | 'name' | 'locationNodeId'>, opts?: {
|
|
197
263
|
force?: boolean;
|
|
198
264
|
online?: boolean;
|
|
@@ -261,7 +327,7 @@ export declare function registerAgentViaNode(db: Db, workspaceId: string, nodeId
|
|
|
261
327
|
export declare function deregisterAgentViaNode(db: Db, workspaceId: string, nodeId: string, message: {
|
|
262
328
|
agent_id?: string;
|
|
263
329
|
name?: string;
|
|
264
|
-
}): Promise<{
|
|
330
|
+
}, deps?: InvocationCompletionDeps): Promise<{
|
|
265
331
|
id: string;
|
|
266
332
|
name: string;
|
|
267
333
|
createdAt: Date;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/engine/node.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EAEzB,yBAAyB,EACzB,mBAAmB,EACnB,yBAAyB,EACzB,wBAAwB,EACxB,qBAAqB,EACrB,eAAe,EACf,sBAAsB,EACvB,MAAM,kBAAkB,CAAC;AAK1B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAqB,MAAM,EAA8D,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAI/H,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAgC,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAqBjG,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/engine/node.ts"],"names":[],"mappings":"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"}
|
package/dist/engine/node.js
CHANGED
|
@@ -10,7 +10,7 @@ import { isNodeLive, nodeHasCapability } from './placement.js';
|
|
|
10
10
|
import { DEFAULT_PROVIDER_NAME, capabilityKind, heartbeatProvider, markProviderOffline, materializeProviderActions, recomputeNodeAggregate, removeProvider, upsertProvider, } from './nodeProvider.js';
|
|
11
11
|
import { serializeNodeOp } from './nodeLock.js';
|
|
12
12
|
import { completeNodeInvocation, dispatchCapacitySpawn, rescheduleInvocationsForLostNode, rescheduleNodeInvocation, } from './action.js';
|
|
13
|
-
import { emitInvocationCompletionEffects } from './invocationCompletion.js';
|
|
13
|
+
import { emitInvocationCompletionEffects, emitAgentExitedEffects, emitNodeStatusEffects, fleetInvocationId } from './invocationCompletion.js';
|
|
14
14
|
import { ackDeliveriesUpToSeq, deliverPendingToNode } from './delivery.js';
|
|
15
15
|
function resolveProviderIdentity(message, connectionId) {
|
|
16
16
|
if (message.provider)
|
|
@@ -121,7 +121,24 @@ function normalizeLegacyAdapter(adapter) {
|
|
|
121
121
|
export async function createNodeToken(db, workspaceId, data) {
|
|
122
122
|
const token = `nt_live_${randomHex(24)}`;
|
|
123
123
|
const tokenHash = await sha256Hex(token);
|
|
124
|
-
const
|
|
124
|
+
const name = data.name.startsWith('#') ? data.name.slice(1) : data.name;
|
|
125
|
+
// Reject names that are empty or still #-prefixed after normalization: an
|
|
126
|
+
// empty name is unreachable via /v1/nodes/:name, and a residual "#" would
|
|
127
|
+
// dodge every name lookup (including the conflict check below).
|
|
128
|
+
if (!name || name.startsWith('#')) {
|
|
129
|
+
throw codedError('Node name must be non-empty and cannot begin with "#"', 'invalid_node_name', 400);
|
|
130
|
+
}
|
|
131
|
+
// Enrollment keys on node_id (the stable identity) when supplied; the name
|
|
132
|
+
// lookup is only a fallback for callers without a stable id. A name held by
|
|
133
|
+
// a *different* node id is a conflict — mirroring node.register — never a
|
|
134
|
+
// silent rewrite of the other node.
|
|
135
|
+
const existing = await resolveNodeForEnroll(db, workspaceId, data);
|
|
136
|
+
if (data.node_id !== undefined) {
|
|
137
|
+
const nameHolder = await getNodeByName(db, workspaceId, name);
|
|
138
|
+
if (nameHolder && nameHolder.id !== data.node_id) {
|
|
139
|
+
throw codedError(`Node name "${name}" is already enrolled by another node`, 'node_name_conflict', 409);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
125
142
|
const now = new Date();
|
|
126
143
|
const existingShape = existing ? normalizeLegacyNodeShape(existing.kind, existing.role) : null;
|
|
127
144
|
const normalized = normalizeLegacyNodeShape(data.kind ?? existing?.kind ?? 'ws', data.role ?? existing?.role ?? (data.max_agents !== undefined && data.max_agents > 1 ? 'broker' : undefined));
|
|
@@ -146,6 +163,7 @@ export async function createNodeToken(db, workspaceId, data) {
|
|
|
146
163
|
const [updated] = await db
|
|
147
164
|
.update(nodes)
|
|
148
165
|
.set({
|
|
166
|
+
name,
|
|
149
167
|
tokenHash,
|
|
150
168
|
kind,
|
|
151
169
|
role,
|
|
@@ -167,7 +185,7 @@ export async function createNodeToken(db, workspaceId, data) {
|
|
|
167
185
|
.values({
|
|
168
186
|
id: data.node_id ?? `node_${generateId()}`,
|
|
169
187
|
workspaceId,
|
|
170
|
-
name
|
|
188
|
+
name,
|
|
171
189
|
tokenHash,
|
|
172
190
|
kind,
|
|
173
191
|
role,
|
|
@@ -198,6 +216,23 @@ export async function getNodeByName(db, workspaceId, name) {
|
|
|
198
216
|
.where(and(eq(nodes.workspaceId, workspaceId), eq(nodes.name, normalized)));
|
|
199
217
|
return node ?? null;
|
|
200
218
|
}
|
|
219
|
+
export async function getNodeById(db, workspaceId, nodeId) {
|
|
220
|
+
const [node] = await db
|
|
221
|
+
.select()
|
|
222
|
+
.from(nodes)
|
|
223
|
+
.where(and(eq(nodes.workspaceId, workspaceId), eq(nodes.id, nodeId)));
|
|
224
|
+
return node ?? null;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Resolve the node an enroll (POST /v1/nodes) targets: by node_id when
|
|
228
|
+
* supplied, by name otherwise.
|
|
229
|
+
*/
|
|
230
|
+
export async function resolveNodeForEnroll(db, workspaceId, data) {
|
|
231
|
+
if (data.node_id !== undefined) {
|
|
232
|
+
return getNodeById(db, workspaceId, data.node_id);
|
|
233
|
+
}
|
|
234
|
+
return getNodeByName(db, workspaceId, data.name);
|
|
235
|
+
}
|
|
201
236
|
export async function registerNode(db, workspaceId, authenticatedNodeId, message, provider) {
|
|
202
237
|
if (message.node_id !== authenticatedNodeId) {
|
|
203
238
|
throw codedError('node_id does not match the authenticated node token', 'node_id_mismatch', 403);
|
|
@@ -346,7 +381,14 @@ export async function heartbeatNode(db, workspaceId, nodeId, providerName, messa
|
|
|
346
381
|
.where(and(eq(nodes.workspaceId, workspaceId), eq(nodes.id, nodeId)));
|
|
347
382
|
return updated ? publicNode(updated) : null;
|
|
348
383
|
}
|
|
349
|
-
export async function markNodeOffline(db, registry, workspaceId, nodeId) {
|
|
384
|
+
export async function markNodeOffline(db, registry, workspaceId, nodeId, effects) {
|
|
385
|
+
// Capture the node's name + prior status BEFORE flipping it so a durable
|
|
386
|
+
// node.status.offline is emitted only on a real online -> offline transition
|
|
387
|
+
// (never on a node that was already offline).
|
|
388
|
+
const [before] = await db
|
|
389
|
+
.select({ status: nodes.status, name: nodes.name })
|
|
390
|
+
.from(nodes)
|
|
391
|
+
.where(and(eq(nodes.workspaceId, workspaceId), eq(nodes.id, nodeId)));
|
|
350
392
|
await db
|
|
351
393
|
.update(nodes)
|
|
352
394
|
.set({
|
|
@@ -369,19 +411,27 @@ export async function markNodeOffline(db, registry, workspaceId, nodeId) {
|
|
|
369
411
|
.set({ status: 'offline', lastSeen: new Date() })
|
|
370
412
|
.where(and(eq(agents.workspaceId, workspaceId), eq(agents.locationType, 'via_node'), eq(agents.locationNodeId, nodeId)));
|
|
371
413
|
await rescheduleInvocationsForLostNode(db, registry, workspaceId, nodeId);
|
|
414
|
+
if (effects?.deps && before && before.status === 'online') {
|
|
415
|
+
await emitNodeStatusEffects(effects.deps, workspaceId, {
|
|
416
|
+
status: 'offline',
|
|
417
|
+
nodeId,
|
|
418
|
+
nodeName: before.name,
|
|
419
|
+
reason: effects.reason,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
372
422
|
}
|
|
373
423
|
/**
|
|
374
424
|
* A provider's control connection dropped. Its capability set persists (marked
|
|
375
425
|
* offline) so the node still shows the manifest; agents it hosted go offline.
|
|
376
426
|
* When it was the node's last connection the whole node goes offline.
|
|
377
427
|
*/
|
|
378
|
-
export async function handleProviderDisconnect(db, registry, workspaceId, nodeId, providerName, hasRemainingConnections) {
|
|
428
|
+
export async function handleProviderDisconnect(db, registry, workspaceId, nodeId, providerName, hasRemainingConnections, deps) {
|
|
379
429
|
// Serialized with node-control operations so a teardown can't race a
|
|
380
430
|
// concurrent register's provider upsert / aggregate recompute (or, on
|
|
381
431
|
// better-sqlite3, deadlock its isolated transaction).
|
|
382
432
|
return serializeNodeOp(workspaceId, nodeId, async () => {
|
|
383
433
|
if (!hasRemainingConnections) {
|
|
384
|
-
await markNodeOffline(db, registry, workspaceId, nodeId);
|
|
434
|
+
await markNodeOffline(db, registry, workspaceId, nodeId, { deps, reason: 'disconnected' });
|
|
385
435
|
return;
|
|
386
436
|
}
|
|
387
437
|
await markProviderOffline(db, workspaceId, nodeId, providerName);
|
|
@@ -397,7 +447,7 @@ export async function handleProviderDisconnect(db, registry, workspaceId, nodeId
|
|
|
397
447
|
* capability set, and materialized actions. When it was the node's last
|
|
398
448
|
* provider the node goes fully offline.
|
|
399
449
|
*/
|
|
400
|
-
export async function deregisterProvider(db, registry, workspaceId, nodeId, providerName) {
|
|
450
|
+
export async function deregisterProvider(db, registry, workspaceId, nodeId, providerName, deps) {
|
|
401
451
|
await removeProvider(db, workspaceId, nodeId, providerName);
|
|
402
452
|
registry.detachProvider(workspaceId, nodeId, providerName);
|
|
403
453
|
await db
|
|
@@ -409,17 +459,39 @@ export async function deregisterProvider(db, registry, workspaceId, nodeId, prov
|
|
|
409
459
|
.from(nodeProviders)
|
|
410
460
|
.where(and(eq(nodeProviders.workspaceId, workspaceId), eq(nodeProviders.nodeId, nodeId)));
|
|
411
461
|
if (!remaining || remaining.count === 0) {
|
|
412
|
-
await markNodeOffline(db, registry, workspaceId, nodeId);
|
|
462
|
+
await markNodeOffline(db, registry, workspaceId, nodeId, { deps, reason: 'deregistered' });
|
|
413
463
|
}
|
|
414
464
|
else {
|
|
415
|
-
// Other
|
|
416
|
-
// node-wide — that would disturb the surviving
|
|
417
|
-
// Work dispatched to the removed provider is
|
|
418
|
-
// sweep, matching the provider-disconnect path.
|
|
465
|
+
// Other provider rows remain (possibly all offline): recompute the node
|
|
466
|
+
// aggregate but don't reschedule node-wide — that would disturb the surviving
|
|
467
|
+
// providers' in-flight invokes. Work dispatched to the removed provider is
|
|
468
|
+
// caught by the dispatch-timeout sweep, matching the provider-disconnect path.
|
|
469
|
+
// Capture status across the recompute so that when the deregistered provider
|
|
470
|
+
// was the node's last *online* one, the resulting online -> offline flip still
|
|
471
|
+
// emits a durable node.status.offline — recomputeNodeAggregate flips the
|
|
472
|
+
// column but never emits.
|
|
473
|
+
const [before] = await db
|
|
474
|
+
.select({ status: nodes.status, name: nodes.name })
|
|
475
|
+
.from(nodes)
|
|
476
|
+
.where(and(eq(nodes.workspaceId, workspaceId), eq(nodes.id, nodeId)));
|
|
419
477
|
await recomputeNodeAggregate(db, workspaceId, nodeId);
|
|
478
|
+
if (deps && before?.status === 'online') {
|
|
479
|
+
const [after] = await db
|
|
480
|
+
.select({ status: nodes.status, name: nodes.name })
|
|
481
|
+
.from(nodes)
|
|
482
|
+
.where(and(eq(nodes.workspaceId, workspaceId), eq(nodes.id, nodeId)));
|
|
483
|
+
if (after && after.status !== 'online') {
|
|
484
|
+
await emitNodeStatusEffects(deps, workspaceId, {
|
|
485
|
+
status: 'offline',
|
|
486
|
+
nodeId,
|
|
487
|
+
nodeName: after.name ?? before.name,
|
|
488
|
+
reason: 'deregistered',
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
420
492
|
}
|
|
421
493
|
}
|
|
422
|
-
export async function sweepOfflineNodes(db, registry) {
|
|
494
|
+
export async function sweepOfflineNodes(db, registry, deps) {
|
|
423
495
|
const rows = await db.select().from(nodes).where(eq(nodes.status, 'online'));
|
|
424
496
|
const stale = rows.filter((node) => !isNodeLive(node));
|
|
425
497
|
let swept = 0;
|
|
@@ -433,7 +505,7 @@ export async function sweepOfflineNodes(db, registry) {
|
|
|
433
505
|
.where(and(eq(nodes.workspaceId, node.workspaceId), eq(nodes.id, node.id)));
|
|
434
506
|
if (!current || current.status !== 'online' || isNodeLive(current))
|
|
435
507
|
return false;
|
|
436
|
-
await markNodeOffline(db, registry, node.workspaceId, node.id);
|
|
508
|
+
await markNodeOffline(db, registry, node.workspaceId, node.id, { deps, reason: 'liveness_timeout' });
|
|
437
509
|
return true;
|
|
438
510
|
});
|
|
439
511
|
if (offlined)
|
|
@@ -874,7 +946,7 @@ export async function registerAgentViaNode(db, workspaceId, nodeId, providerName
|
|
|
874
946
|
};
|
|
875
947
|
});
|
|
876
948
|
}
|
|
877
|
-
export async function deregisterAgentViaNode(db, workspaceId, nodeId, message) {
|
|
949
|
+
export async function deregisterAgentViaNode(db, workspaceId, nodeId, message, deps) {
|
|
878
950
|
const conditions = [
|
|
879
951
|
eq(agents.workspaceId, workspaceId),
|
|
880
952
|
eq(agents.locationType, 'via_node'),
|
|
@@ -908,6 +980,15 @@ export async function deregisterAgentViaNode(db, workspaceId, nodeId, message) {
|
|
|
908
980
|
.where(and(eq(agents.workspaceId, workspaceId), eq(agents.id, updated.id)));
|
|
909
981
|
await markDirectNodeOfflineForAgent(db, workspaceId, updated.id);
|
|
910
982
|
await releaseNodeAgentSlots(db, workspaceId, activeNodeIds.filter((activeNodeId) => activeNodeId !== nodeId));
|
|
983
|
+
if (deps) {
|
|
984
|
+
await emitAgentExitedEffects(deps, workspaceId, {
|
|
985
|
+
agentId: updated.id,
|
|
986
|
+
agentName: updated.name,
|
|
987
|
+
nodeId,
|
|
988
|
+
invocationId: fleetInvocationId(updated.metadata),
|
|
989
|
+
reason: 'deregistered',
|
|
990
|
+
});
|
|
991
|
+
}
|
|
911
992
|
}
|
|
912
993
|
return updated ?? null;
|
|
913
994
|
}
|
|
@@ -1032,7 +1113,7 @@ export async function reconcileInventory(db, registry, workspaceId, nodeId, prov
|
|
|
1032
1113
|
invocation_id: item.invocation_id ?? null,
|
|
1033
1114
|
session_ref: item.session_ref ?? null,
|
|
1034
1115
|
},
|
|
1035
|
-
});
|
|
1116
|
+
}, completionDeps);
|
|
1036
1117
|
if (completed) {
|
|
1037
1118
|
completedInvocations++;
|
|
1038
1119
|
if (completionDeps) {
|
|
@@ -1041,15 +1122,27 @@ export async function reconcileInventory(db, registry, workspaceId, nodeId, prov
|
|
|
1041
1122
|
}
|
|
1042
1123
|
}
|
|
1043
1124
|
const nodeAgents = await db
|
|
1044
|
-
.select({ id: agents.id, name: agents.name })
|
|
1125
|
+
.select({ id: agents.id, name: agents.name, metadata: agents.metadata })
|
|
1045
1126
|
.from(agents)
|
|
1046
1127
|
.where(and(eq(agents.workspaceId, workspaceId), eq(agents.locationType, 'via_node'), eq(agents.locationNodeId, nodeId), eq(agents.providerName, providerName), eq(agents.status, 'active')));
|
|
1047
|
-
const
|
|
1128
|
+
const missingAgents = nodeAgents.filter((agent) => !names.has(agent.name));
|
|
1129
|
+
const missing = missingAgents.map((agent) => agent.id);
|
|
1048
1130
|
if (missing.length > 0) {
|
|
1049
1131
|
await db
|
|
1050
1132
|
.update(agents)
|
|
1051
1133
|
.set({ status: 'offline', lastSeen: new Date() })
|
|
1052
1134
|
.where(inArray(agents.id, missing));
|
|
1135
|
+
if (completionDeps) {
|
|
1136
|
+
for (const agent of missingAgents) {
|
|
1137
|
+
await emitAgentExitedEffects(completionDeps, workspaceId, {
|
|
1138
|
+
agentId: agent.id,
|
|
1139
|
+
agentName: agent.name,
|
|
1140
|
+
nodeId,
|
|
1141
|
+
invocationId: fleetInvocationId(agent.metadata),
|
|
1142
|
+
reason: 'missing_from_inventory',
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1053
1146
|
}
|
|
1054
1147
|
let rescheduledInvocations = 0;
|
|
1055
1148
|
for (const invocation of openInvocations) {
|
|
@@ -1095,6 +1188,29 @@ export async function getPublicNode(db, workspaceId, name) {
|
|
|
1095
1188
|
const node = await getNodeByName(db, workspaceId, name);
|
|
1096
1189
|
return node ? publicNode(node) : null;
|
|
1097
1190
|
}
|
|
1191
|
+
/** The node's persisted `status` column, or undefined when the node is gone. */
|
|
1192
|
+
async function readNodeStatus(db, workspaceId, nodeId) {
|
|
1193
|
+
const [row] = await db
|
|
1194
|
+
.select({ status: nodes.status })
|
|
1195
|
+
.from(nodes)
|
|
1196
|
+
.where(and(eq(nodes.workspaceId, workspaceId), eq(nodes.id, nodeId)));
|
|
1197
|
+
return row?.status;
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Emit a durable `node.status.online` only on a real offline -> online
|
|
1201
|
+
* transition: the node was not `online` before the register/heartbeat and the
|
|
1202
|
+
* resulting descriptor reports online. A steady heartbeat (already online) or a
|
|
1203
|
+
* register that leaves the node offline (no live provider) emits nothing.
|
|
1204
|
+
*/
|
|
1205
|
+
async function emitNodeOnlineTransition(deps, workspaceId, priorStatus, node) {
|
|
1206
|
+
if (!deps || !node || priorStatus === 'online' || node.status !== 'online')
|
|
1207
|
+
return;
|
|
1208
|
+
await emitNodeStatusEffects(deps, workspaceId, {
|
|
1209
|
+
status: 'online',
|
|
1210
|
+
nodeId: node.id,
|
|
1211
|
+
nodeName: node.name,
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1098
1214
|
function sendControl(socket, payload) {
|
|
1099
1215
|
if (!socket)
|
|
1100
1216
|
return false;
|
|
@@ -1155,6 +1271,7 @@ export async function handleNodeControlMessage(args) {
|
|
|
1155
1271
|
throw codedError(conflict.message, conflict.code, 409);
|
|
1156
1272
|
}
|
|
1157
1273
|
}
|
|
1274
|
+
const priorStatus = await readNodeStatus(args.db, args.workspaceId, args.nodeId);
|
|
1158
1275
|
const registered = await registerNode(args.db, args.workspaceId, args.nodeId, message, provider);
|
|
1159
1276
|
const readinessSupported = supportsProviderDeliveryReadiness(args.registry);
|
|
1160
1277
|
const acceptance = registered.acceptance.map((capability) => (capability.name === FLEET_DELIVERY_CURSOR_CAPABILITY && !readinessSupported
|
|
@@ -1176,6 +1293,11 @@ export async function handleNodeControlMessage(args) {
|
|
|
1176
1293
|
accepted_capabilities: acceptance,
|
|
1177
1294
|
},
|
|
1178
1295
|
});
|
|
1296
|
+
// The node row is already persisted online, so emit the durable
|
|
1297
|
+
// transition before draining: a drainNode rejection must not skip the
|
|
1298
|
+
// node.status.online event. Isolated so its own failure can't either.
|
|
1299
|
+
await emitNodeOnlineTransition(args.completionDeps, args.workspaceId, priorStatus, registered.node)
|
|
1300
|
+
.catch((err) => console.error('[node.status] online event emission failed', err));
|
|
1179
1301
|
// Node is now marked online: flush any queued action.invoke frames so
|
|
1180
1302
|
// spawns queued while it was offline can reserve capacity and dispatch.
|
|
1181
1303
|
await args.registry.drainNode(args.workspaceId, args.nodeId);
|
|
@@ -1191,14 +1313,21 @@ export async function handleNodeControlMessage(args) {
|
|
|
1191
1313
|
}
|
|
1192
1314
|
return;
|
|
1193
1315
|
}
|
|
1194
|
-
case 'node.heartbeat':
|
|
1195
|
-
await
|
|
1316
|
+
case 'node.heartbeat': {
|
|
1317
|
+
const priorStatus = await readNodeStatus(args.db, args.workspaceId, args.nodeId);
|
|
1318
|
+
const beat = await heartbeatNode(args.db, args.workspaceId, args.nodeId, frameProviderName, message);
|
|
1319
|
+
// The node row is already persisted, so emit the durable online
|
|
1320
|
+
// transition before draining: a drainNode rejection must not skip the
|
|
1321
|
+
// node.status.online event. Isolated so its own failure can't either.
|
|
1322
|
+
await emitNodeOnlineTransition(args.completionDeps, args.workspaceId, priorStatus, beat)
|
|
1323
|
+
.catch((err) => console.error('[node.status] online event emission failed', err));
|
|
1196
1324
|
// Heartbeat refreshes online/capacity state; re-drain as a backstop in
|
|
1197
1325
|
// case a queued spawn could not reserve capacity at register time.
|
|
1198
1326
|
await args.registry.drainNode(args.workspaceId, args.nodeId);
|
|
1199
1327
|
return;
|
|
1328
|
+
}
|
|
1200
1329
|
case 'node.deregister':
|
|
1201
|
-
await deregisterProvider(args.db, args.registry, args.workspaceId, args.nodeId, frameProviderName);
|
|
1330
|
+
await deregisterProvider(args.db, args.registry, args.workspaceId, args.nodeId, frameProviderName, args.completionDeps);
|
|
1202
1331
|
return;
|
|
1203
1332
|
case 'node.spawn': {
|
|
1204
1333
|
// Handler-context `ctx.spawnAgent`: capacity-direct delegation to this
|
|
@@ -1246,7 +1375,7 @@ export async function handleNodeControlMessage(args) {
|
|
|
1246
1375
|
return;
|
|
1247
1376
|
}
|
|
1248
1377
|
case 'agent.deregister':
|
|
1249
|
-
await deregisterAgentViaNode(args.db, args.workspaceId, args.nodeId, message);
|
|
1378
|
+
await deregisterAgentViaNode(args.db, args.workspaceId, args.nodeId, message, args.completionDeps);
|
|
1250
1379
|
return;
|
|
1251
1380
|
case 'inventory.sync': {
|
|
1252
1381
|
const result = await reconcileInventory(args.db, args.registry, args.workspaceId, args.nodeId, frameProviderName, message.agents, args.completionDeps);
|
|
@@ -1273,7 +1402,7 @@ export async function handleNodeControlMessage(args) {
|
|
|
1273
1402
|
const completed = await completeNodeInvocation(args.db, args.registry, args.workspaceId, args.nodeId, frameProviderName, message.invocation_id, {
|
|
1274
1403
|
...(Object.prototype.hasOwnProperty.call(message, 'output') ? { output: message.output } : {}),
|
|
1275
1404
|
...(message.error ? { error: message.error } : {}),
|
|
1276
|
-
});
|
|
1405
|
+
}, args.completionDeps);
|
|
1277
1406
|
if (completed && args.completionDeps) {
|
|
1278
1407
|
await emitInvocationCompletionEffects(args.completionDeps, args.workspaceId, completed);
|
|
1279
1408
|
}
|