@relaycast/engine 6.1.0 → 6.3.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.
Files changed (69) hide show
  1. package/dist/adapters/node/delivery-maintenance.d.ts +1 -1
  2. package/dist/adapters/node/delivery-maintenance.d.ts.map +1 -1
  3. package/dist/adapters/node/delivery-maintenance.js +4 -4
  4. package/dist/adapters/node/delivery-maintenance.js.map +1 -1
  5. package/dist/adapters/node/index.js +2 -2
  6. package/dist/adapters/node/index.js.map +1 -1
  7. package/dist/adapters/node/realtime.d.ts.map +1 -1
  8. package/dist/adapters/node/realtime.js +45 -3
  9. package/dist/adapters/node/realtime.js.map +1 -1
  10. package/dist/agent-disconnect.d.ts +23 -1
  11. package/dist/agent-disconnect.d.ts.map +1 -1
  12. package/dist/agent-disconnect.js +20 -2
  13. package/dist/agent-disconnect.js.map +1 -1
  14. package/dist/db/migrations/0032_invocation_handler_unreachable.sql +5 -0
  15. package/dist/db/schema.d.ts +17 -0
  16. package/dist/db/schema.d.ts.map +1 -1
  17. package/dist/db/schema.js +4 -0
  18. package/dist/db/schema.js.map +1 -1
  19. package/dist/engine/action.d.ts +34 -13
  20. package/dist/engine/action.d.ts.map +1 -1
  21. package/dist/engine/action.js +323 -57
  22. package/dist/engine/action.js.map +1 -1
  23. package/dist/engine/delivery.d.ts +21 -1
  24. package/dist/engine/delivery.d.ts.map +1 -1
  25. package/dist/engine/delivery.js +79 -9
  26. package/dist/engine/delivery.js.map +1 -1
  27. package/dist/engine/invocationCompletion.d.ts +43 -0
  28. package/dist/engine/invocationCompletion.d.ts.map +1 -1
  29. package/dist/engine/invocationCompletion.js +108 -4
  30. package/dist/engine/invocationCompletion.js.map +1 -1
  31. package/dist/engine/node.d.ts +72 -6
  32. package/dist/engine/node.d.ts.map +1 -1
  33. package/dist/engine/node.js +152 -23
  34. package/dist/engine/node.js.map +1 -1
  35. package/dist/engine/nodeDeliver.d.ts.map +1 -1
  36. package/dist/engine/nodeDeliver.js +5 -0
  37. package/dist/engine/nodeDeliver.js.map +1 -1
  38. package/dist/engine/observerToken.js +1 -1
  39. package/dist/engine/observerToken.js.map +1 -1
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +7 -5
  43. package/dist/index.js.map +1 -1
  44. package/dist/lib/origin.d.ts +44 -0
  45. package/dist/lib/origin.d.ts.map +1 -1
  46. package/dist/lib/origin.js +63 -3
  47. package/dist/lib/origin.js.map +1 -1
  48. package/dist/lib/serverTelemetry.d.ts.map +1 -1
  49. package/dist/lib/serverTelemetry.js +10 -2
  50. package/dist/lib/serverTelemetry.js.map +1 -1
  51. package/dist/node-invocations.d.ts +1 -1
  52. package/dist/node-invocations.d.ts.map +1 -1
  53. package/dist/node-invocations.js +1 -1
  54. package/dist/node-invocations.js.map +1 -1
  55. package/dist/ports/realtime.d.ts +22 -1
  56. package/dist/ports/realtime.d.ts.map +1 -1
  57. package/dist/ports/realtime.js.map +1 -1
  58. package/dist/routes/action.js +11 -5
  59. package/dist/routes/action.js.map +1 -1
  60. package/dist/routes/deliveryRouting.d.ts +20 -1
  61. package/dist/routes/deliveryRouting.d.ts.map +1 -1
  62. package/dist/routes/deliveryRouting.js +116 -17
  63. package/dist/routes/deliveryRouting.js.map +1 -1
  64. package/dist/routes/node.js +3 -2
  65. package/dist/routes/node.js.map +1 -1
  66. package/dist/routes/presence.d.ts.map +1 -1
  67. package/dist/routes/presence.js +15 -4
  68. package/dist/routes/presence.js.map +1 -1
  69. 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, result);
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: result, outboxId } : {
62
+ await deps.webhookQueue.send(outboxId ? { type: eventType, workspaceId, data, outboxId } : {
52
63
  type: eventType,
53
64
  workspaceId,
54
- data: result,
65
+ data,
55
66
  });
56
67
  }
57
68
  catch {
58
- // Best-effort side effect; completion must not fail because webhook delivery did.
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;AAE/C,OAAO,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,8BAA8B,EAAE,MAAM,sBAAsB,CAAC;AAatE,SAAS,UAAU,CAAC,IAAY,EAAE,WAAmB,EAAE,IAA6B;IAClF,OAAO;QACL,IAAI;QACJ,YAAY,EAAE,WAAW;QACzB,IAAI;QACJ,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,+BAA+B,CACnD,IAA8B,EAC9B,WAAmB,EACnB,MAAwB;IAExB,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,kBAAkB,CAAC;IACpF,MAAM,YAAY,GAAG;QACnB,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAC;IACF,MAAM,KAAK,GAAG,UAAU,CAAC,SAAS,EAAE,WAAW,EAAE,YAAY,CAAC,CAAC;IAC/D,MAAM,OAAO,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAE1C,MAAM,WAAW,GAAuB,EAAE,CAAC;IAC3C,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,WAAW,CAAC,IAAI,CACd,0BAA0B,CAAC;YACzB,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,WAAW;YACX,WAAW,EAAE,IAAI,CAAC,MAAM,EAAE,WAAW;YACrC,aAAa,EAAE,IAAI,CAAC,MAAM,EAAE,aAAa;SAC1C,EAAE;YACD,QAAQ,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC;YAC5B,KAAK,EAAE,SAAS;YAChB,QAAQ,EAAE,MAAM,CAAC,aAAa;YAC9B,IAAI,EAAE,YAAY;YAClB,SAAS,EAAE,MAAM,CAAC,aAAa;SAChC,CAAC,CACH,CAAC;IACJ,CAAC;IACD,wEAAwE;IACxE,WAAW,CAAC,IAAI,CACd,8BAA8B,CAC5B,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,EACxC,WAAW,EACX,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAC7B,CACF,CAAC;IACF,MAAM,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;IAEtC,IAAI,QAA4B,CAAC;IACjC,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE,MAAiC,CAAC,CAAC;IACpG,CAAC;IAAC,MAAM,CAAC;QACP,sFAAsF;IACxF,CAAC;IAED,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;YACjG,IAAI,EAAE,SAAS;YACf,WAAW;YACX,IAAI,EAAE,MAAM;SACb,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,kFAAkF;IACpF,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"invocationCompletion.js","sourceRoot":"","sources":["../../src/engine/invocationCompletion.ts"],"names":[],"mappings":"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"}
@@ -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): Promise<void>;
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":"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"}
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/engine/node.ts"],"names":[],"mappings":"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"}
@@ -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 existing = await getNodeByName(db, workspaceId, data.name);
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: data.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 providers remain: recompute the node aggregate but don't reschedule
416
- // node-wide — that would disturb the surviving providers' in-flight invokes.
417
- // Work dispatched to the removed provider is caught by the dispatch-timeout
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 missing = nodeAgents.filter((agent) => !names.has(agent.name)).map((agent) => agent.id);
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 heartbeatNode(args.db, args.workspaceId, args.nodeId, frameProviderName, message);
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
  }