@relaycast/engine 12.0.0 → 12.1.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 (112) hide show
  1. package/dist/adapters/node/index.d.ts.map +1 -1
  2. package/dist/adapters/node/index.js +1 -0
  3. package/dist/adapters/node/index.js.map +1 -1
  4. package/dist/adapters/node/presence.d.ts +18 -0
  5. package/dist/adapters/node/presence.d.ts.map +1 -1
  6. package/dist/adapters/node/presence.js +82 -0
  7. package/dist/adapters/node/presence.js.map +1 -1
  8. package/dist/adapters/node/realtime.d.ts +4 -0
  9. package/dist/adapters/node/realtime.d.ts.map +1 -1
  10. package/dist/adapters/node/realtime.js +32 -5
  11. package/dist/adapters/node/realtime.js.map +1 -1
  12. package/dist/db/schema.d.ts +2 -2
  13. package/dist/engine/a2a.d.ts +3 -0
  14. package/dist/engine/a2a.d.ts.map +1 -1
  15. package/dist/engine/a2a.js +1 -0
  16. package/dist/engine/a2a.js.map +1 -1
  17. package/dist/engine/action.d.ts +14 -9
  18. package/dist/engine/action.d.ts.map +1 -1
  19. package/dist/engine/action.js +335 -74
  20. package/dist/engine/action.js.map +1 -1
  21. package/dist/engine/agent.d.ts +9 -3
  22. package/dist/engine/agent.d.ts.map +1 -1
  23. package/dist/engine/agent.js +21 -3
  24. package/dist/engine/agent.js.map +1 -1
  25. package/dist/engine/delivery.d.ts.map +1 -1
  26. package/dist/engine/delivery.js +4 -0
  27. package/dist/engine/delivery.js.map +1 -1
  28. package/dist/engine/eventDelivery.d.ts +1 -1
  29. package/dist/engine/inboundWebhook.d.ts.map +1 -1
  30. package/dist/engine/inboundWebhook.js +12 -3
  31. package/dist/engine/inboundWebhook.js.map +1 -1
  32. package/dist/engine/inbox.d.ts.map +1 -1
  33. package/dist/engine/inbox.js +17 -0
  34. package/dist/engine/inbox.js.map +1 -1
  35. package/dist/engine/invocationClaim.d.ts +32 -0
  36. package/dist/engine/invocationClaim.d.ts.map +1 -0
  37. package/dist/engine/invocationClaim.js +42 -0
  38. package/dist/engine/invocationClaim.js.map +1 -0
  39. package/dist/engine/messageMetadata.d.ts +2 -0
  40. package/dist/engine/messageMetadata.d.ts.map +1 -1
  41. package/dist/engine/messageMetadata.js +3 -2
  42. package/dist/engine/messageMetadata.js.map +1 -1
  43. package/dist/engine/node.d.ts +12 -2
  44. package/dist/engine/node.d.ts.map +1 -1
  45. package/dist/engine/node.js +83 -4
  46. package/dist/engine/node.js.map +1 -1
  47. package/dist/engine/presence.d.ts +173 -1
  48. package/dist/engine/presence.d.ts.map +1 -1
  49. package/dist/engine/presence.js +81 -4
  50. package/dist/engine/presence.js.map +1 -1
  51. package/dist/engine/registrationAdmission.d.ts +40 -0
  52. package/dist/engine/registrationAdmission.d.ts.map +1 -0
  53. package/dist/engine/registrationAdmission.js +78 -0
  54. package/dist/engine/registrationAdmission.js.map +1 -0
  55. package/dist/engine/taskInvocation.d.ts +1 -1
  56. package/dist/engine/workspace.d.ts +26 -1
  57. package/dist/engine/workspace.d.ts.map +1 -1
  58. package/dist/engine/workspace.js +108 -7
  59. package/dist/engine/workspace.js.map +1 -1
  60. package/dist/engine/workspaceDrain.d.ts +38 -0
  61. package/dist/engine/workspaceDrain.d.ts.map +1 -0
  62. package/dist/engine/workspaceDrain.js +202 -0
  63. package/dist/engine/workspaceDrain.js.map +1 -0
  64. package/dist/engine.d.ts.map +1 -1
  65. package/dist/engine.js +28 -0
  66. package/dist/engine.js.map +1 -1
  67. package/dist/index.d.ts +4 -2
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +5 -1
  70. package/dist/index.js.map +1 -1
  71. package/dist/lib/engineDiagnostics.d.ts +26 -0
  72. package/dist/lib/engineDiagnostics.d.ts.map +1 -0
  73. package/dist/lib/engineDiagnostics.js +100 -0
  74. package/dist/lib/engineDiagnostics.js.map +1 -0
  75. package/dist/lib/httpError.d.ts.map +1 -1
  76. package/dist/lib/httpError.js +5 -1
  77. package/dist/lib/httpError.js.map +1 -1
  78. package/dist/middleware/admission.d.ts +7 -0
  79. package/dist/middleware/admission.d.ts.map +1 -1
  80. package/dist/middleware/admission.js +19 -1
  81. package/dist/middleware/admission.js.map +1 -1
  82. package/dist/middleware/rateLimit.d.ts.map +1 -1
  83. package/dist/middleware/rateLimit.js +8 -1
  84. package/dist/middleware/rateLimit.js.map +1 -1
  85. package/dist/ports/admission.d.ts +24 -3
  86. package/dist/ports/admission.d.ts.map +1 -1
  87. package/dist/ports/diagnostics.d.ts +42 -0
  88. package/dist/ports/diagnostics.d.ts.map +1 -0
  89. package/dist/ports/diagnostics.js +13 -0
  90. package/dist/ports/diagnostics.js.map +1 -0
  91. package/dist/ports/index.d.ts +9 -1
  92. package/dist/ports/index.d.ts.map +1 -1
  93. package/dist/ports/index.js.map +1 -1
  94. package/dist/ports/presence.d.ts +14 -0
  95. package/dist/ports/presence.d.ts.map +1 -1
  96. package/dist/ports/realtime.d.ts +18 -0
  97. package/dist/ports/realtime.d.ts.map +1 -1
  98. package/dist/ports/realtime.js.map +1 -1
  99. package/dist/routes/a2a.d.ts.map +1 -1
  100. package/dist/routes/a2a.js +2 -0
  101. package/dist/routes/a2a.js.map +1 -1
  102. package/dist/routes/action.js +3 -0
  103. package/dist/routes/action.js.map +1 -1
  104. package/dist/routes/agent.d.ts.map +1 -1
  105. package/dist/routes/agent.js +5 -1
  106. package/dist/routes/agent.js.map +1 -1
  107. package/dist/routes/node.d.ts.map +1 -1
  108. package/dist/routes/node.js +2 -1
  109. package/dist/routes/node.js.map +1 -1
  110. package/dist/routes/workspace.js +10 -2
  111. package/dist/routes/workspace.js.map +1 -1
  112. package/package.json +3 -3
@@ -1,12 +1,184 @@
1
1
  import type { getDb } from '../db/index.js';
2
2
  import type { PresenceTracker } from '../ports/presence.js';
3
+ type Db = ReturnType<typeof getDb>;
4
+ /**
5
+ * Active bindings on nodes that might pass {@link isNodeLive}.
6
+ *
7
+ * One join for the whole roster: `idx_nodes_status_heartbeat` ranges the live
8
+ * heartbeat window, then `idx_agent_node_bindings_node` seeks each node's
9
+ * active bindings. The agent seek is its primary key, and the provider seek is
10
+ * `node_providers_node_name_unique` for `agents.provider_name`. A nested
11
+ * `EXISTS` on `idx_node_providers_node` tells a provider-less direct node from
12
+ * a broker whose provider row is simply missing. SQLite stores heartbeats as
13
+ * whole seconds, so the lower bound is up to 999ms wider than the millisecond
14
+ * TTL; {@link isNodeLive} and {@link isProviderLive} are the decisions and drop
15
+ * that extra second. The read does not write.
16
+ */
17
+ export declare function nodeHostedPresenceQuery(db: Db, workspaceId: string, now: number): Omit<import("drizzle-orm/sqlite-core").SQLiteSelectBase<undefined, "async", unknown, {
18
+ agentId: import("drizzle-orm").SQL<string>;
19
+ nodeLastHeartbeatAt: import("drizzle-orm").SQL<Date>;
20
+ nodeHasProviders: import("drizzle-orm").SQL<number>;
21
+ providerStatus: import("drizzle-orm/sqlite-core").SQLiteColumn<{
22
+ name: "status";
23
+ tableName: "node_providers";
24
+ dataType: "string";
25
+ columnType: "SQLiteText";
26
+ data: string;
27
+ driverParam: string;
28
+ notNull: true;
29
+ hasDefault: true;
30
+ isPrimaryKey: false;
31
+ isAutoincrement: false;
32
+ hasRuntimeDefault: false;
33
+ enumValues: [string, ...string[]];
34
+ baseColumn: never;
35
+ identity: undefined;
36
+ generated: undefined;
37
+ }, {}, {
38
+ length: number | undefined;
39
+ }>;
40
+ providerHandlersLive: import("drizzle-orm/sqlite-core").SQLiteColumn<{
41
+ name: "handlers_live";
42
+ tableName: "node_providers";
43
+ dataType: "boolean";
44
+ columnType: "SQLiteBoolean";
45
+ data: boolean;
46
+ driverParam: number;
47
+ notNull: true;
48
+ hasDefault: true;
49
+ isPrimaryKey: false;
50
+ isAutoincrement: false;
51
+ hasRuntimeDefault: false;
52
+ enumValues: undefined;
53
+ baseColumn: never;
54
+ identity: undefined;
55
+ generated: undefined;
56
+ }, {}, {}>;
57
+ providerLastHeartbeatAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
58
+ name: "last_heartbeat_at";
59
+ tableName: "node_providers";
60
+ dataType: "date";
61
+ columnType: "SQLiteTimestamp";
62
+ data: Date;
63
+ driverParam: number;
64
+ notNull: false;
65
+ hasDefault: false;
66
+ isPrimaryKey: false;
67
+ isAutoincrement: false;
68
+ hasRuntimeDefault: false;
69
+ enumValues: undefined;
70
+ baseColumn: never;
71
+ identity: undefined;
72
+ generated: undefined;
73
+ }, {}, {}>;
74
+ }, "partial", {
75
+ agents: "not-null";
76
+ } & {
77
+ node_providers: "nullable";
78
+ }, false, "where", {
79
+ agentId: string;
80
+ nodeLastHeartbeatAt: Date;
81
+ nodeHasProviders: number;
82
+ providerStatus: string | null;
83
+ providerHandlersLive: boolean | null;
84
+ providerLastHeartbeatAt: Date | null;
85
+ }[], {
86
+ agentId: import("drizzle-orm").DrizzleTypeError<"You cannot reference this field without assigning it an alias first - use `.as(<alias>)`">;
87
+ nodeLastHeartbeatAt: import("drizzle-orm").DrizzleTypeError<"You cannot reference this field without assigning it an alias first - use `.as(<alias>)`">;
88
+ nodeHasProviders: import("drizzle-orm").DrizzleTypeError<"You cannot reference this field without assigning it an alias first - use `.as(<alias>)`">;
89
+ providerStatus: import("drizzle-orm").Column<import("drizzle-orm").UpdateColConfig<import("drizzle-orm").ColumnTypeConfig<{
90
+ name: "status";
91
+ tableName: "node_providers";
92
+ dataType: "string";
93
+ columnType: "SQLiteText";
94
+ data: string;
95
+ driverParam: string;
96
+ notNull: true;
97
+ hasDefault: true;
98
+ isPrimaryKey: false;
99
+ isAutoincrement: false;
100
+ hasRuntimeDefault: false;
101
+ enumValues: [string, ...string[]];
102
+ baseColumn: never;
103
+ identity: undefined;
104
+ generated: undefined;
105
+ }, {
106
+ length: number | undefined;
107
+ } & {
108
+ dialect: "sqlite";
109
+ }>, {
110
+ notNull: false;
111
+ }>, object, object>;
112
+ providerHandlersLive: import("drizzle-orm").Column<import("drizzle-orm").UpdateColConfig<import("drizzle-orm").ColumnTypeConfig<{
113
+ name: "handlers_live";
114
+ tableName: "node_providers";
115
+ dataType: "boolean";
116
+ columnType: "SQLiteBoolean";
117
+ data: boolean;
118
+ driverParam: number;
119
+ notNull: true;
120
+ hasDefault: true;
121
+ isPrimaryKey: false;
122
+ isAutoincrement: false;
123
+ hasRuntimeDefault: false;
124
+ enumValues: undefined;
125
+ baseColumn: never;
126
+ identity: undefined;
127
+ generated: undefined;
128
+ }, {} & {
129
+ dialect: "sqlite";
130
+ }>, {
131
+ notNull: false;
132
+ }>, object, object>;
133
+ providerLastHeartbeatAt: import("drizzle-orm").Column<import("drizzle-orm").UpdateColConfig<import("drizzle-orm").ColumnTypeConfig<{
134
+ name: "last_heartbeat_at";
135
+ tableName: "node_providers";
136
+ dataType: "date";
137
+ columnType: "SQLiteTimestamp";
138
+ data: Date;
139
+ driverParam: number;
140
+ notNull: false;
141
+ hasDefault: false;
142
+ isPrimaryKey: false;
143
+ isAutoincrement: false;
144
+ hasRuntimeDefault: false;
145
+ enumValues: undefined;
146
+ baseColumn: never;
147
+ identity: undefined;
148
+ generated: undefined;
149
+ }, {} & {
150
+ dialect: "sqlite";
151
+ }>, {
152
+ notNull: false;
153
+ }>, object, object>;
154
+ }>, "where">;
155
+ type NodeHostedPresenceRow = {
156
+ agentId: string;
157
+ nodeLastHeartbeatAt: Date | null;
158
+ nodeHasProviders: number | boolean;
159
+ providerStatus: string | null;
160
+ providerHandlersLive: boolean | null;
161
+ providerLastHeartbeatAt: Date | null;
162
+ };
163
+ /**
164
+ * A bound agent is online when its node is live and, on a node that has
165
+ * provider rows, the provider named by `agents.provider_name` is live too.
166
+ * Implicit direct nodes have no provider row; node liveness is the whole
167
+ * signal. A sibling provider's heartbeat must not count an agent whose own
168
+ * provider is disconnected or absent.
169
+ */
170
+ export declare function nodeHostedAgentOnline(row: NodeHostedPresenceRow, now: number): boolean;
3
171
  /**
4
172
  * Get presence status for all agents in a workspace.
5
173
  * Queries the presence tracker for online agents and merges with the DB roster.
174
+ * An agent located on a live node, with an active binding there, is online even
175
+ * when it has never heartbeated over HTTP — unless it explicitly disconnected,
176
+ * or its own provider (when the node has any) is not live.
6
177
  */
7
- export declare function getPresence(db: ReturnType<typeof getDb>, presence: PresenceTracker, workspaceId: string): Promise<Array<{
178
+ export declare function getPresence(db: Db, presence: PresenceTracker, workspaceId: string, now?: number): Promise<Array<{
8
179
  agent_id: string;
9
180
  agent_name: string;
10
181
  status: 'online' | 'offline';
11
182
  }>>;
183
+ export {};
12
184
  //# sourceMappingURL=presence.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../../src/engine/presence.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5D;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,EAC5B,QAAQ,EAAE,eAAe,EACzB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,CAAA;CAAE,CAAC,CAAC,CAoBxF"}
1
+ {"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../../src/engine/presence.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAK5D,KAAK,EAAE,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;AAEnC;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aA4C/E;AAED,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,mBAAmB,EAAE,IAAI,GAAG,IAAI,CAAC;IACjC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC;IACnC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,oBAAoB,EAAE,OAAO,GAAG,IAAI,CAAC;IACrC,uBAAuB,EAAE,IAAI,GAAG,IAAI,CAAC;CACtC,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAYtF;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,EAAE,EACN,QAAQ,EAAE,eAAe,EACzB,WAAW,EAAE,MAAM,EACnB,GAAG,SAAa,GACf,OAAO,CAAC,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,CAAA;CAAE,CAAC,CAAC,CA2BxF"}
@@ -1,12 +1,80 @@
1
- import { and, eq, ne } from 'drizzle-orm';
2
- import { agents } from '../db/schema.js';
1
+ import { and, eq, ne, sql } from 'drizzle-orm';
2
+ import { agentNodeBindings, agents, nodeProviders, nodes } from '../db/schema.js';
3
3
  import { RELEASED_AGENT_STATUS } from './agent.js';
4
+ import { isProviderLive } from './nodeProvider.js';
5
+ import { isNodeLive, NODE_LIVENESS_TTL_MS } from './placement.js';
6
+ /**
7
+ * Active bindings on nodes that might pass {@link isNodeLive}.
8
+ *
9
+ * One join for the whole roster: `idx_nodes_status_heartbeat` ranges the live
10
+ * heartbeat window, then `idx_agent_node_bindings_node` seeks each node's
11
+ * active bindings. The agent seek is its primary key, and the provider seek is
12
+ * `node_providers_node_name_unique` for `agents.provider_name`. A nested
13
+ * `EXISTS` on `idx_node_providers_node` tells a provider-less direct node from
14
+ * a broker whose provider row is simply missing. SQLite stores heartbeats as
15
+ * whole seconds, so the lower bound is up to 999ms wider than the millisecond
16
+ * TTL; {@link isNodeLive} and {@link isProviderLive} are the decisions and drop
17
+ * that extra second. The read does not write.
18
+ */
19
+ export function nodeHostedPresenceQuery(db, workspaceId, now) {
20
+ const newestSecond = Math.floor(now / 1000);
21
+ const oldestSecond = Math.floor((now - NODE_LIVENESS_TTL_MS) / 1000);
22
+ // CROSS JOIN is not reordered by SQLite, so the live-node index stays the
23
+ // outer loop and each node's bindings are a seek. INNER JOIN would let the
24
+ // planner start at every active binding in the workspace.
25
+ return db
26
+ .select({
27
+ // `nodes` and `agent_node_bindings` are indexed-by fragments, so their
28
+ // columns are not join-tracked Column fields. mapWith keeps the driver
29
+ // codecs (dates, text) the table columns already declare.
30
+ agentId: sql `${agentNodeBindings.agentId}`.mapWith(agentNodeBindings.agentId),
31
+ nodeLastHeartbeatAt: sql `${nodes.lastHeartbeatAt}`.mapWith(nodes.lastHeartbeatAt),
32
+ nodeHasProviders: sql `EXISTS (
33
+ SELECT 1 FROM node_providers AS np
34
+ WHERE np.workspace_id = ${nodes.workspaceId}
35
+ AND np.node_id = ${nodes.id}
36
+ )`.mapWith(Number),
37
+ providerStatus: nodeProviders.status,
38
+ providerHandlersLive: nodeProviders.handlersLive,
39
+ providerLastHeartbeatAt: nodeProviders.lastHeartbeatAt,
40
+ })
41
+ .from(sql `${nodes} INDEXED BY idx_nodes_status_heartbeat`)
42
+ .crossJoin(sql `${agentNodeBindings} INDEXED BY idx_agent_node_bindings_node`)
43
+ .crossJoin(agents)
44
+ .leftJoin(nodeProviders, and(eq(nodeProviders.workspaceId, workspaceId), eq(nodeProviders.nodeId, nodes.id), eq(nodeProviders.name, agents.providerName)))
45
+ .where(and(eq(nodes.workspaceId, workspaceId), eq(nodes.status, 'online'), sql `${nodes.lastHeartbeatAt} >= ${oldestSecond}`, sql `${nodes.lastHeartbeatAt} <= ${newestSecond}`, eq(agentNodeBindings.workspaceId, workspaceId), eq(agentNodeBindings.nodeId, nodes.id), eq(agentNodeBindings.status, 'active'), eq(agents.workspaceId, workspaceId), eq(agents.id, agentNodeBindings.agentId), eq(agents.locationType, 'via_node'), eq(agents.locationNodeId, nodes.id), ne(agents.status, RELEASED_AGENT_STATUS)));
46
+ }
47
+ /**
48
+ * A bound agent is online when its node is live and, on a node that has
49
+ * provider rows, the provider named by `agents.provider_name` is live too.
50
+ * Implicit direct nodes have no provider row; node liveness is the whole
51
+ * signal. A sibling provider's heartbeat must not count an agent whose own
52
+ * provider is disconnected or absent.
53
+ */
54
+ export function nodeHostedAgentOnline(row, now) {
55
+ if (!isNodeLive({ status: 'online', lastHeartbeatAt: row.nodeLastHeartbeatAt }, now))
56
+ return false;
57
+ const hasProviders = row.nodeHasProviders === true || Number(row.nodeHasProviders) === 1;
58
+ if (!hasProviders)
59
+ return true;
60
+ if (row.providerStatus == null || row.providerHandlersLive == null || row.providerLastHeartbeatAt == null) {
61
+ return false;
62
+ }
63
+ return isProviderLive({
64
+ status: row.providerStatus,
65
+ handlersLive: row.providerHandlersLive,
66
+ lastHeartbeatAt: row.providerLastHeartbeatAt,
67
+ }, now);
68
+ }
4
69
  /**
5
70
  * Get presence status for all agents in a workspace.
6
71
  * Queries the presence tracker for online agents and merges with the DB roster.
72
+ * An agent located on a live node, with an active binding there, is online even
73
+ * when it has never heartbeated over HTTP — unless it explicitly disconnected,
74
+ * or its own provider (when the node has any) is not live.
7
75
  */
8
- export async function getPresence(db, presence, workspaceId) {
9
- const [allAgents, onlineIds] = await Promise.all([
76
+ export async function getPresence(db, presence, workspaceId, now = Date.now()) {
77
+ const [allAgents, onlineIds, nodeHosted, suppressedIds] = await Promise.all([
10
78
  db
11
79
  .select({ id: agents.id, name: agents.name })
12
80
  .from(agents)
@@ -14,10 +82,19 @@ export async function getPresence(db, presence, workspaceId) {
14
82
  // They are not roster members and must not appear as presence entries.
15
83
  .where(and(eq(agents.workspaceId, workspaceId), ne(agents.status, RELEASED_AGENT_STATUS))),
16
84
  presence.getOnline(workspaceId),
85
+ nodeHostedPresenceQuery(db, workspaceId, now),
86
+ presence.explicitlyDisconnected?.(workspaceId) ?? Promise.resolve([]),
17
87
  ]);
18
88
  if (allAgents.length === 0)
19
89
  return [];
20
90
  const onlineSet = new Set(onlineIds);
91
+ const suppressed = new Set(suppressedIds);
92
+ for (const row of nodeHosted) {
93
+ if (suppressed.has(row.agentId))
94
+ continue;
95
+ if (nodeHostedAgentOnline(row, now))
96
+ onlineSet.add(row.agentId);
97
+ }
21
98
  return allAgents.map((agent) => ({
22
99
  agent_id: agent.id,
23
100
  agent_name: agent.name,
@@ -1 +1 @@
1
- {"version":3,"file":"presence.js","sourceRoot":"","sources":["../../src/engine/presence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAGzC,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEnD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,EAA4B,EAC5B,QAAyB,EACzB,WAAmB;IAEnB,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC/C,EAAE;aACC,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;aAC5C,IAAI,CAAC,MAAM,CAAC;YACb,wEAAwE;YACxE,uEAAuE;aACtE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC;QAC5F,QAAQ,CAAC,SAAS,CAAC,WAAW,CAAC;KAChC,CAAC,CAAC;IAEH,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEtC,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;IAErC,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC/B,QAAQ,EAAE,KAAK,CAAC,EAAE;QAClB,UAAU,EAAE,KAAK,CAAC,IAAI;QACtB,MAAM,EAAE,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,QAAkB,CAAC,CAAC,CAAE,SAAmB;KAC7E,CAAC,CAAC,CAAC;AACN,CAAC"}
1
+ {"version":3,"file":"presence.js","sourceRoot":"","sources":["../../src/engine/presence.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import type { EngineDb } from '../ports/database.js';
2
+ import type { AdmissionHooks, RegistrationAdmissionAction } from '../ports/admission.js';
3
+ import type { Workspace } from '../ports/auth.js';
4
+ /**
5
+ * How many of `action`'s kind the workspace currently has registered — the
6
+ * `registered` count a registration is admitted against.
7
+ *
8
+ * - `register_device`: every node a host enrolled, whatever its role or
9
+ * transport. Every agent also owns an implicit direct node
10
+ * (`node_direct_<agent id>`) for its own WebSocket; those are not devices.
11
+ * The prefix is reserved at enrollment (`createNodeToken`), so a host cannot
12
+ * enroll into it.
13
+ * - `register_agent`: non-human identities that have not been released, except
14
+ * the engine-provisioned inbound-webhook identity ({@link WEBHOOK_AGENT_NAME}).
15
+ * `createWebhook` provisions it on a workspace's first webhook; no caller
16
+ * registers it, and its name is reserved at registration
17
+ * (`assertRegistrableAgentName`), so it never holds a slot.
18
+ * - `register_human`: `type = 'human'` identities that have not been released.
19
+ */
20
+ export declare function countRegistrations(db: EngineDb, workspaceId: string, action: RegistrationAdmissionAction): Promise<number>;
21
+ /**
22
+ * Admits a NEW registration through the host's `admit` hook, throwing
23
+ * {@link AdmissionDeniedError} on a denial. Callers invoke it only on the
24
+ * create path — re-registering an existing node or agent is never new.
25
+ */
26
+ export type RegistrationAdmission = (action: RegistrationAdmissionAction) => Promise<void>;
27
+ export interface RegistrationAdmissionArgs {
28
+ db: EngineDb;
29
+ admission: AdmissionHooks | undefined;
30
+ /** The authenticated workspace row, or its id when the caller only has that. */
31
+ workspace: Workspace | string;
32
+ }
33
+ /**
34
+ * Build the admission check the registration paths run before inserting a
35
+ * node or agent, or `undefined` when the host has no `admit` hook (nothing to
36
+ * count for). A hook or count that throws admits the registration, like every
37
+ * other admission: a host's metering outage must not block registration.
38
+ */
39
+ export declare function createRegistrationAdmission(args: RegistrationAdmissionArgs): RegistrationAdmission | undefined;
40
+ //# sourceMappingURL=registrationAdmission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registrationAdmission.d.ts","sourceRoot":"","sources":["../../src/engine/registrationAdmission.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,2BAA2B,EAAE,MAAM,uBAAuB,CAAC;AACzF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAKlD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,QAAQ,EACZ,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,MAAM,CAAC,CAwBjB;AAED;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,MAAM,EAAE,2BAA2B,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE3F,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,QAAQ,CAAC;IACb,SAAS,EAAE,cAAc,GAAG,SAAS,CAAC;IACtC,gFAAgF;IAChF,SAAS,EAAE,SAAS,GAAG,MAAM,CAAC;CAC/B;AAQD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,yBAAyB,GAAG,qBAAqB,GAAG,SAAS,CAoB9G"}
@@ -0,0 +1,78 @@
1
+ import { and, eq, ne, not, sql } from 'drizzle-orm';
2
+ import { agents, nodes, workspaces } from '../db/schema.js';
3
+ import { AdmissionDeniedError } from '../lib/admissionError.js';
4
+ import { WEBHOOK_AGENT_NAME } from './messageMetadata.js';
5
+ /**
6
+ * How many of `action`'s kind the workspace currently has registered — the
7
+ * `registered` count a registration is admitted against.
8
+ *
9
+ * - `register_device`: every node a host enrolled, whatever its role or
10
+ * transport. Every agent also owns an implicit direct node
11
+ * (`node_direct_<agent id>`) for its own WebSocket; those are not devices.
12
+ * The prefix is reserved at enrollment (`createNodeToken`), so a host cannot
13
+ * enroll into it.
14
+ * - `register_agent`: non-human identities that have not been released, except
15
+ * the engine-provisioned inbound-webhook identity ({@link WEBHOOK_AGENT_NAME}).
16
+ * `createWebhook` provisions it on a workspace's first webhook; no caller
17
+ * registers it, and its name is reserved at registration
18
+ * (`assertRegistrableAgentName`), so it never holds a slot.
19
+ * - `register_human`: `type = 'human'` identities that have not been released.
20
+ */
21
+ export async function countRegistrations(db, workspaceId, action) {
22
+ const count = sql `count(*)`;
23
+ const rows = action === 'register_device'
24
+ ? await db
25
+ .select({ count })
26
+ .from(nodes)
27
+ .where(and(eq(nodes.workspaceId, workspaceId),
28
+ // GLOB, not LIKE: in LIKE `_` is a wildcard, so `nodeXdirectX…` would
29
+ // slip out of the count while passing the literal reserved-prefix check.
30
+ not(and(eq(nodes.role, 'direct'), sql `${nodes.id} GLOB 'node_direct_*'`))))
31
+ : await db
32
+ .select({ count })
33
+ .from(agents)
34
+ .where(and(eq(agents.workspaceId, workspaceId),
35
+ // Tombstoned identities (`RELEASED_AGENT_STATUS` in agent.ts) hold no slot.
36
+ ne(agents.status, 'released'), action === 'register_human'
37
+ ? eq(agents.type, 'human')
38
+ : and(ne(agents.type, 'human'), ne(agents.name, WEBHOOK_AGENT_NAME))));
39
+ return Number(rows[0]?.count ?? 0);
40
+ }
41
+ async function resolveWorkspace(db, workspace) {
42
+ if (typeof workspace !== 'string')
43
+ return workspace;
44
+ const [row] = await db.select().from(workspaces).where(eq(workspaces.id, workspace));
45
+ return row ?? null;
46
+ }
47
+ /**
48
+ * Build the admission check the registration paths run before inserting a
49
+ * node or agent, or `undefined` when the host has no `admit` hook (nothing to
50
+ * count for). A hook or count that throws admits the registration, like every
51
+ * other admission: a host's metering outage must not block registration.
52
+ */
53
+ export function createRegistrationAdmission(args) {
54
+ const admit = args.admission?.admit;
55
+ if (!admit)
56
+ return undefined;
57
+ return async (action) => {
58
+ let denial;
59
+ try {
60
+ const workspace = await resolveWorkspace(args.db, args.workspace);
61
+ if (!workspace)
62
+ return;
63
+ const registered = await countRegistrations(args.db, workspace.id, action);
64
+ denial = await admit.call(args.admission, { workspace, action, registered });
65
+ }
66
+ catch (err) {
67
+ console.warn('[admission] registration admission failed; admitting', {
68
+ action,
69
+ workspaceId: typeof args.workspace === 'string' ? args.workspace : args.workspace.id,
70
+ error: err instanceof Error ? err.message : String(err),
71
+ });
72
+ return;
73
+ }
74
+ if (denial)
75
+ throw new AdmissionDeniedError(denial);
76
+ };
77
+ }
78
+ //# sourceMappingURL=registrationAdmission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registrationAdmission.js","sourceRoot":"","sources":["../../src/engine/registrationAdmission.ts"],"names":[],"mappings":"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"}
@@ -77,11 +77,11 @@ export declare function completeTaskInvocation(db: EngineDb, workspaceId: string
77
77
  };
78
78
  completed: {
79
79
  error: string | null;
80
+ workspaceId: string;
80
81
  id: string;
81
82
  createdAt: Date;
82
83
  output: unknown;
83
84
  input: unknown;
84
- workspaceId: string;
85
85
  status: string;
86
86
  nodeLivenessRevision: number;
87
87
  handlerAgentId: string | null;
@@ -1,6 +1,7 @@
1
1
  import type { getDb } from '../db/index.js';
2
2
  import type { FileStorage } from '../ports/files.js';
3
3
  import type { WorkspaceProvenanceRecord } from '../db/schema.js';
4
+ import { type WorkspaceDrainOptions } from './workspaceDrain.js';
4
5
  type Db = ReturnType<typeof getDb>;
5
6
  type CreateWorkspaceOptions = string | {
6
7
  ownerApiKey?: string;
@@ -186,7 +187,28 @@ export declare function drainFileCleanup(db: Db, storage: FileStorage, options?:
186
187
  now?: Date;
187
188
  limit?: number;
188
189
  }): Promise<number>;
189
- export declare function deleteWorkspace(db: Db, storage: FileStorage, workspaceId: string): Promise<void>;
190
+ /**
191
+ * Delete a workspace.
192
+ *
193
+ * A workspace small enough to delete in one transaction keeps the atomic
194
+ * contract: everything is removed (or nothing, on failure) before this
195
+ * returns. A larger one would hold the shared database for the whole cascade,
196
+ * so it is tombstoned instead: the existing expiry deadline is pulled forward
197
+ * to now, which every credential path already rejects (`workspace_expired`),
198
+ * and its data is removed by bounded drains (`reapWorkspace`, the reaper).
199
+ * Returns `deferred: true` when data removal continues in the background.
200
+ */
201
+ export declare function deleteWorkspace(db: Db, storage: FileStorage, workspaceId: string, options?: {
202
+ now?: Date;
203
+ }): Promise<{
204
+ deferred: boolean;
205
+ }>;
206
+ /**
207
+ * Drain one workspace within a time budget (only when it is too large to
208
+ * delete atomically), then delete it once its bulk rows are gone. Returns true when the workspace row was deleted. Safe to call
209
+ * repeatedly and concurrently with the reaper: every step is idempotent.
210
+ */
211
+ export declare function reapWorkspace(db: Db, storage: FileStorage, workspaceId: string, options?: WorkspaceDrainOptions): Promise<boolean>;
190
212
  /**
191
213
  * Delete a bounded batch of workspaces whose callers explicitly opted into an
192
214
  * expiry deadline at creation. No inferred signal (name, age, agents, or
@@ -195,6 +217,9 @@ export declare function deleteWorkspace(db: Db, storage: FileStorage, workspaceI
195
217
  export declare function reapExpiredWorkspaces(db: Db, storage: FileStorage, options?: {
196
218
  now?: Date;
197
219
  limit?: number;
220
+ } & Omit<WorkspaceDrainOptions, 'deadline' | 'now'> & {
221
+ /** Wall clock for the drain budget (ms); `now` above is only the expiry cutoff. */
222
+ clock?: () => number;
198
223
  }): Promise<string[]>;
199
224
  export {};
200
225
  //# sourceMappingURL=workspace.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/engine/workspace.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/engine/workspace.ts"],"names":[],"mappings":"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"}