@electric-ax/agents-server 0.4.2 → 0.4.4
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/entrypoint.js +529 -248
- package/dist/index.cjs +1603 -1332
- package/dist/index.d.cts +274 -162
- package/dist/index.d.ts +274 -162
- package/dist/index.js +1601 -1332
- package/drizzle/0007_runner_diagnostics_and_principal.sql +22 -0
- package/drizzle/0008_runner_runtime_diagnostics.sql +50 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +6 -6
- package/src/db/schema.ts +33 -10
- package/src/electric-agents-types.ts +49 -3
- package/src/entity-registry.ts +136 -26
- package/src/host.ts +4 -5
- package/src/index.ts +5 -1
- package/src/principal.ts +23 -11
- package/src/routing/context.ts +1 -0
- package/src/routing/dispatch-policy.ts +123 -20
- package/src/routing/durable-streams-router.ts +286 -116
- package/src/routing/durable-streams-routing-adapter.ts +31 -64
- package/src/routing/electric-proxy-router.ts +1 -0
- package/src/routing/entities-router.ts +5 -5
- package/src/routing/hooks.ts +8 -1
- package/src/routing/internal-router.ts +6 -2
- package/src/routing/runners-router.ts +257 -19
- package/src/runtime.ts +4 -5
- package/src/server.ts +21 -15
- package/src/standalone-runtime.ts +4 -5
- package/src/stream-client.ts +18 -69
- package/src/utils/server-utils.ts +27 -8
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
UPDATE consumer_claims
|
|
2
|
+
SET status = 'expired', updated_at = NOW()
|
|
3
|
+
WHERE status = 'active' AND runner_id IS NOT NULL;
|
|
4
|
+
--> statement-breakpoint
|
|
5
|
+
UPDATE entity_dispatch_state
|
|
6
|
+
SET active_runner_id = NULL,
|
|
7
|
+
active_consumer_id = NULL,
|
|
8
|
+
active_epoch = NULL,
|
|
9
|
+
active_claimed_at = NULL,
|
|
10
|
+
active_lease_expires_at = NULL,
|
|
11
|
+
updated_at = NOW()
|
|
12
|
+
WHERE active_runner_id IS NOT NULL;
|
|
13
|
+
--> statement-breakpoint
|
|
14
|
+
DELETE FROM runners;
|
|
15
|
+
--> statement-breakpoint
|
|
16
|
+
ALTER TABLE runners RENAME COLUMN owner_user_id TO owner_principal;
|
|
17
|
+
--> statement-breakpoint
|
|
18
|
+
DROP INDEX IF EXISTS idx_runners_owner_user_id;
|
|
19
|
+
--> statement-breakpoint
|
|
20
|
+
CREATE INDEX idx_runners_owner_principal ON runners (tenant_id, owner_principal);
|
|
21
|
+
--> statement-breakpoint
|
|
22
|
+
ALTER TABLE runners ADD COLUMN diagnostics jsonb;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
CREATE TABLE runner_runtime_diagnostics (
|
|
2
|
+
tenant_id text NOT NULL DEFAULT 'default',
|
|
3
|
+
runner_id text NOT NULL,
|
|
4
|
+
owner_principal text NOT NULL,
|
|
5
|
+
wake_stream_offset text,
|
|
6
|
+
last_seen_at timestamp with time zone NOT NULL,
|
|
7
|
+
liveness_lease_expires_at timestamp with time zone NOT NULL,
|
|
8
|
+
diagnostics jsonb,
|
|
9
|
+
updated_at timestamp with time zone NOT NULL DEFAULT NOW(),
|
|
10
|
+
PRIMARY KEY (tenant_id, runner_id)
|
|
11
|
+
);
|
|
12
|
+
--> statement-breakpoint
|
|
13
|
+
CREATE INDEX idx_runner_runtime_diagnostics_owner
|
|
14
|
+
ON runner_runtime_diagnostics (tenant_id, owner_principal);
|
|
15
|
+
--> statement-breakpoint
|
|
16
|
+
CREATE INDEX idx_runner_runtime_diagnostics_liveness
|
|
17
|
+
ON runner_runtime_diagnostics (tenant_id, liveness_lease_expires_at);
|
|
18
|
+
--> statement-breakpoint
|
|
19
|
+
INSERT INTO runner_runtime_diagnostics (
|
|
20
|
+
tenant_id,
|
|
21
|
+
runner_id,
|
|
22
|
+
owner_principal,
|
|
23
|
+
wake_stream_offset,
|
|
24
|
+
last_seen_at,
|
|
25
|
+
liveness_lease_expires_at,
|
|
26
|
+
diagnostics,
|
|
27
|
+
updated_at
|
|
28
|
+
)
|
|
29
|
+
SELECT
|
|
30
|
+
tenant_id,
|
|
31
|
+
id,
|
|
32
|
+
owner_principal,
|
|
33
|
+
wake_stream_offset,
|
|
34
|
+
COALESCE(last_seen_at, updated_at),
|
|
35
|
+
COALESCE(liveness_lease_expires_at, updated_at),
|
|
36
|
+
diagnostics,
|
|
37
|
+
updated_at
|
|
38
|
+
FROM runners
|
|
39
|
+
WHERE last_seen_at IS NOT NULL
|
|
40
|
+
OR liveness_lease_expires_at IS NOT NULL
|
|
41
|
+
OR wake_stream_offset IS NOT NULL
|
|
42
|
+
OR diagnostics IS NOT NULL;
|
|
43
|
+
--> statement-breakpoint
|
|
44
|
+
ALTER TABLE runners DROP COLUMN diagnostics;
|
|
45
|
+
--> statement-breakpoint
|
|
46
|
+
ALTER TABLE runners DROP COLUMN wake_stream_offset;
|
|
47
|
+
--> statement-breakpoint
|
|
48
|
+
ALTER TABLE runners DROP COLUMN last_seen_at;
|
|
49
|
+
--> statement-breakpoint
|
|
50
|
+
ALTER TABLE runners DROP COLUMN liveness_lease_expires_at;
|
|
@@ -50,6 +50,20 @@
|
|
|
50
50
|
"when": 1776268800000,
|
|
51
51
|
"tag": "0006_principals",
|
|
52
52
|
"breakpoints": true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"idx": 7,
|
|
56
|
+
"version": "7",
|
|
57
|
+
"when": 1778899200000,
|
|
58
|
+
"tag": "0007_runner_diagnostics_and_principal",
|
|
59
|
+
"breakpoints": true
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"idx": 8,
|
|
63
|
+
"version": "7",
|
|
64
|
+
"when": 1778976000000,
|
|
65
|
+
"tag": "0008_runner_runtime_diagnostics",
|
|
66
|
+
"breakpoints": true
|
|
53
67
|
}
|
|
54
68
|
]
|
|
55
69
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-ax/agents-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Electric Agents entity runtime server",
|
|
5
5
|
"author": "Durable Stream contributors",
|
|
6
6
|
"bin": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@durable-streams/client": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/client@350",
|
|
40
40
|
"@durable-streams/server": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/server@350",
|
|
41
41
|
"@durable-streams/state": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/state@350",
|
|
42
|
-
"@electric-sql/client": "^1.5.
|
|
42
|
+
"@electric-sql/client": "^1.5.18",
|
|
43
43
|
"@mariozechner/pi-agent-core": "^0.70.2",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1",
|
|
45
45
|
"@sinclair/typebox": "^0.34.48",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"pino-pretty": "^13.0.0",
|
|
55
55
|
"postgres": "^3.4.0",
|
|
56
56
|
"undici": "^7.24.7",
|
|
57
|
-
"@electric-ax/agents-runtime": "0.2.
|
|
57
|
+
"@electric-ax/agents-runtime": "0.2.2"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^22.19.15",
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
"tsx": "^4.19.0",
|
|
66
66
|
"typescript": "^5.0.0",
|
|
67
67
|
"vitest": "^4.1.0",
|
|
68
|
-
"@electric-ax/agents": "0.4.
|
|
69
|
-
"@electric-ax/agents-server-conformance-tests": "0.1.
|
|
70
|
-
"@electric-ax/agents-server-ui": "0.4.
|
|
68
|
+
"@electric-ax/agents": "0.4.3",
|
|
69
|
+
"@electric-ax/agents-server-conformance-tests": "0.1.5",
|
|
70
|
+
"@electric-ax/agents-server-ui": "0.4.4"
|
|
71
71
|
},
|
|
72
72
|
"files": [
|
|
73
73
|
"dist",
|
package/src/db/schema.ts
CHANGED
|
@@ -106,16 +106,11 @@ export const runners = pgTable(
|
|
|
106
106
|
{
|
|
107
107
|
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
108
108
|
id: text(`id`).notNull(),
|
|
109
|
-
|
|
109
|
+
ownerPrincipal: text(`owner_principal`).notNull(),
|
|
110
110
|
label: text(`label`).notNull(),
|
|
111
111
|
kind: text(`kind`).notNull().default(`local`),
|
|
112
112
|
adminStatus: text(`admin_status`).notNull().default(`enabled`),
|
|
113
113
|
wakeStream: text(`wake_stream`).notNull(),
|
|
114
|
-
wakeStreamOffset: text(`wake_stream_offset`),
|
|
115
|
-
lastSeenAt: timestamp(`last_seen_at`, { withTimezone: true }),
|
|
116
|
-
livenessLeaseExpiresAt: timestamp(`liveness_lease_expires_at`, {
|
|
117
|
-
withTimezone: true,
|
|
118
|
-
}),
|
|
119
114
|
createdAt: timestamp(`created_at`, { withTimezone: true })
|
|
120
115
|
.notNull()
|
|
121
116
|
.defaultNow(),
|
|
@@ -126,12 +121,11 @@ export const runners = pgTable(
|
|
|
126
121
|
(table) => [
|
|
127
122
|
primaryKey({ columns: [table.tenantId, table.id] }),
|
|
128
123
|
unique(`uq_runners_wake_stream`).on(table.tenantId, table.wakeStream),
|
|
129
|
-
index(`
|
|
130
|
-
index(`idx_runners_admin_status`).on(table.tenantId, table.adminStatus),
|
|
131
|
-
index(`idx_runners_liveness_lease_expires_at`).on(
|
|
124
|
+
index(`idx_runners_owner_principal`).on(
|
|
132
125
|
table.tenantId,
|
|
133
|
-
table.
|
|
126
|
+
table.ownerPrincipal
|
|
134
127
|
),
|
|
128
|
+
index(`idx_runners_admin_status`).on(table.tenantId, table.adminStatus),
|
|
135
129
|
check(
|
|
136
130
|
`chk_runners_kind`,
|
|
137
131
|
sql`${table.kind} IN ('local', 'cloud-worker', 'sandbox', 'ci', 'server')`
|
|
@@ -143,6 +137,35 @@ export const runners = pgTable(
|
|
|
143
137
|
]
|
|
144
138
|
)
|
|
145
139
|
|
|
140
|
+
export const runnerRuntimeDiagnostics = pgTable(
|
|
141
|
+
`runner_runtime_diagnostics`,
|
|
142
|
+
{
|
|
143
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
144
|
+
runnerId: text(`runner_id`).notNull(),
|
|
145
|
+
ownerPrincipal: text(`owner_principal`).notNull(),
|
|
146
|
+
wakeStreamOffset: text(`wake_stream_offset`),
|
|
147
|
+
lastSeenAt: timestamp(`last_seen_at`, { withTimezone: true }).notNull(),
|
|
148
|
+
livenessLeaseExpiresAt: timestamp(`liveness_lease_expires_at`, {
|
|
149
|
+
withTimezone: true,
|
|
150
|
+
}).notNull(),
|
|
151
|
+
diagnostics: jsonb(`diagnostics`),
|
|
152
|
+
updatedAt: timestamp(`updated_at`, { withTimezone: true })
|
|
153
|
+
.notNull()
|
|
154
|
+
.defaultNow(),
|
|
155
|
+
},
|
|
156
|
+
(table) => [
|
|
157
|
+
primaryKey({ columns: [table.tenantId, table.runnerId] }),
|
|
158
|
+
index(`idx_runner_runtime_diagnostics_owner`).on(
|
|
159
|
+
table.tenantId,
|
|
160
|
+
table.ownerPrincipal
|
|
161
|
+
),
|
|
162
|
+
index(`idx_runner_runtime_diagnostics_liveness`).on(
|
|
163
|
+
table.tenantId,
|
|
164
|
+
table.livenessLeaseExpiresAt
|
|
165
|
+
),
|
|
166
|
+
]
|
|
167
|
+
)
|
|
168
|
+
|
|
146
169
|
export const entityDispatchState = pgTable(
|
|
147
170
|
`entity_dispatch_state`,
|
|
148
171
|
{
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
* Types for the Electric Agents entity runtime.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
PullWakeRunnerHealth,
|
|
7
|
+
WebhookNotification,
|
|
8
|
+
} from '@electric-ax/agents-runtime'
|
|
6
9
|
import type { Principal } from './principal.js'
|
|
7
10
|
|
|
8
11
|
type WakeNotification = WebhookNotification
|
|
@@ -105,7 +108,7 @@ export interface RunnerActiveClaim {
|
|
|
105
108
|
|
|
106
109
|
export interface ElectricAgentsRunner {
|
|
107
110
|
id: string
|
|
108
|
-
|
|
111
|
+
owner_principal: string
|
|
109
112
|
label: string
|
|
110
113
|
kind: RunnerKind
|
|
111
114
|
admin_status: RunnerAdminStatus
|
|
@@ -115,13 +118,14 @@ export interface ElectricAgentsRunner {
|
|
|
115
118
|
active_claims?: Array<RunnerActiveClaim>
|
|
116
119
|
wake_stream: string
|
|
117
120
|
wake_stream_offset?: string
|
|
121
|
+
diagnostics?: Record<string, unknown>
|
|
118
122
|
created_at: string
|
|
119
123
|
updated_at: string
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
export interface RegisterRunnerRequest {
|
|
123
127
|
id: string
|
|
124
|
-
|
|
128
|
+
owner_principal: string
|
|
125
129
|
label: string
|
|
126
130
|
kind?: RunnerKind
|
|
127
131
|
admin_status?: RunnerAdminStatus
|
|
@@ -133,6 +137,48 @@ export interface RunnerHeartbeatRequest {
|
|
|
133
137
|
wake_stream_offset?: string
|
|
134
138
|
wakeStreamOffset?: string
|
|
135
139
|
liveness_lease_expires_at?: string
|
|
140
|
+
diagnostics?: Record<string, unknown>
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export type RunnerHealthStatus = `healthy` | `degraded` | `unhealthy`
|
|
144
|
+
export type RunnerClientDiagnostics = Partial<
|
|
145
|
+
Omit<PullWakeRunnerHealth, `running` | `offset`>
|
|
146
|
+
>
|
|
147
|
+
|
|
148
|
+
export interface RunnerHealthResponse {
|
|
149
|
+
runner: {
|
|
150
|
+
id: string
|
|
151
|
+
admin_status: RunnerAdminStatus
|
|
152
|
+
liveness_status: RunnerLiveness | `expired`
|
|
153
|
+
lease_expires_at: string | null
|
|
154
|
+
lease_remaining_ms: number | null
|
|
155
|
+
wake_stream: string
|
|
156
|
+
wake_stream_offset: string | null
|
|
157
|
+
last_seen_at: string | null
|
|
158
|
+
created_at: string
|
|
159
|
+
}
|
|
160
|
+
client: RunnerClientDiagnostics | null
|
|
161
|
+
claims: {
|
|
162
|
+
active_count: number
|
|
163
|
+
active: Array<{
|
|
164
|
+
consumer_id: string
|
|
165
|
+
epoch: number
|
|
166
|
+
entity_url: string
|
|
167
|
+
stream_path: string
|
|
168
|
+
claimed_at: string
|
|
169
|
+
last_heartbeat_at: string | null
|
|
170
|
+
lease_expires_at: string | null
|
|
171
|
+
}>
|
|
172
|
+
}
|
|
173
|
+
dispatch: {
|
|
174
|
+
entities_with_active_claim: number
|
|
175
|
+
entities_with_outstanding_wake: number
|
|
176
|
+
entities_with_pending_work: number
|
|
177
|
+
}
|
|
178
|
+
health: {
|
|
179
|
+
status: RunnerHealthStatus
|
|
180
|
+
issues: Array<string>
|
|
181
|
+
}
|
|
136
182
|
}
|
|
137
183
|
|
|
138
184
|
export interface EntityDispatchState {
|
package/src/entity-registry.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
entityDispatchState,
|
|
8
8
|
entityManifestSources,
|
|
9
9
|
entityTypes,
|
|
10
|
+
runnerRuntimeDiagnostics,
|
|
10
11
|
runners,
|
|
11
12
|
tagStreamOutbox,
|
|
12
13
|
} from './db/schema.js'
|
|
@@ -73,7 +74,7 @@ export interface TagStreamOutboxRow {
|
|
|
73
74
|
|
|
74
75
|
export interface RegisterRunnerInput {
|
|
75
76
|
id: string
|
|
76
|
-
|
|
77
|
+
ownerPrincipal: string
|
|
77
78
|
label: string
|
|
78
79
|
kind?: RunnerKind
|
|
79
80
|
adminStatus?: RunnerAdminStatus
|
|
@@ -82,10 +83,22 @@ export interface RegisterRunnerInput {
|
|
|
82
83
|
|
|
83
84
|
export interface HeartbeatRunnerInput {
|
|
84
85
|
runnerId: string
|
|
86
|
+
ownerPrincipal: string
|
|
85
87
|
heartbeatAt?: Date
|
|
86
88
|
livenessLeaseExpiresAt?: Date
|
|
87
89
|
leaseMs?: number
|
|
88
90
|
wakeStreamOffset?: string
|
|
91
|
+
diagnostics?: Record<string, unknown>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface RunnerRuntimeDiagnostics {
|
|
95
|
+
runner_id: string
|
|
96
|
+
owner_principal: string
|
|
97
|
+
wake_stream_offset?: string
|
|
98
|
+
last_seen_at: string
|
|
99
|
+
liveness_lease_expires_at: string
|
|
100
|
+
diagnostics?: Record<string, unknown>
|
|
101
|
+
updated_at: string
|
|
89
102
|
}
|
|
90
103
|
|
|
91
104
|
export interface MaterializeActiveClaimInput {
|
|
@@ -140,7 +153,7 @@ export class PostgresRegistry {
|
|
|
140
153
|
.values({
|
|
141
154
|
tenantId: this.tenantId,
|
|
142
155
|
id: input.id,
|
|
143
|
-
|
|
156
|
+
ownerPrincipal: input.ownerPrincipal,
|
|
144
157
|
label: input.label,
|
|
145
158
|
kind: input.kind ?? `local`,
|
|
146
159
|
adminStatus: input.adminStatus ?? `enabled`,
|
|
@@ -150,7 +163,7 @@ export class PostgresRegistry {
|
|
|
150
163
|
.onConflictDoUpdate({
|
|
151
164
|
target: [runners.tenantId, runners.id],
|
|
152
165
|
set: {
|
|
153
|
-
|
|
166
|
+
ownerPrincipal: input.ownerPrincipal,
|
|
154
167
|
label: input.label,
|
|
155
168
|
kind: input.kind ?? `local`,
|
|
156
169
|
adminStatus: input.adminStatus ?? `enabled`,
|
|
@@ -176,11 +189,11 @@ export class PostgresRegistry {
|
|
|
176
189
|
}
|
|
177
190
|
|
|
178
191
|
async listRunners(filter?: {
|
|
179
|
-
|
|
192
|
+
ownerPrincipal?: string
|
|
180
193
|
}): Promise<Array<ElectricAgentsRunner>> {
|
|
181
194
|
const conditions = [eq(runners.tenantId, this.tenantId)]
|
|
182
|
-
if (filter?.
|
|
183
|
-
conditions.push(eq(runners.
|
|
195
|
+
if (filter?.ownerPrincipal) {
|
|
196
|
+
conditions.push(eq(runners.ownerPrincipal, filter.ownerPrincipal))
|
|
184
197
|
}
|
|
185
198
|
const rows = await this.db
|
|
186
199
|
.select()
|
|
@@ -198,22 +211,66 @@ export class PostgresRegistry {
|
|
|
198
211
|
input.livenessLeaseExpiresAt ??
|
|
199
212
|
new Date(now.getTime() + (input.leaseMs ?? DEFAULT_RUNNER_LEASE_MS))
|
|
200
213
|
|
|
201
|
-
|
|
202
|
-
.
|
|
203
|
-
.
|
|
214
|
+
await this.db
|
|
215
|
+
.insert(runnerRuntimeDiagnostics)
|
|
216
|
+
.values({
|
|
217
|
+
tenantId: this.tenantId,
|
|
218
|
+
runnerId: input.runnerId,
|
|
219
|
+
ownerPrincipal: input.ownerPrincipal,
|
|
204
220
|
lastSeenAt: now,
|
|
205
221
|
livenessLeaseExpiresAt: leaseExpiresAt,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
: {}),
|
|
222
|
+
wakeStreamOffset: input.wakeStreamOffset,
|
|
223
|
+
diagnostics: input.diagnostics,
|
|
209
224
|
updatedAt: now,
|
|
210
225
|
})
|
|
226
|
+
.onConflictDoUpdate({
|
|
227
|
+
target: [
|
|
228
|
+
runnerRuntimeDiagnostics.tenantId,
|
|
229
|
+
runnerRuntimeDiagnostics.runnerId,
|
|
230
|
+
],
|
|
231
|
+
set: {
|
|
232
|
+
lastSeenAt: now,
|
|
233
|
+
ownerPrincipal: input.ownerPrincipal,
|
|
234
|
+
livenessLeaseExpiresAt: leaseExpiresAt,
|
|
235
|
+
...(input.wakeStreamOffset !== undefined
|
|
236
|
+
? { wakeStreamOffset: input.wakeStreamOffset }
|
|
237
|
+
: {}),
|
|
238
|
+
...(input.diagnostics !== undefined
|
|
239
|
+
? { diagnostics: input.diagnostics }
|
|
240
|
+
: {}),
|
|
241
|
+
updatedAt: now,
|
|
242
|
+
},
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const runner = await this.getRunner(input.runnerId)
|
|
246
|
+
if (!runner) return null
|
|
247
|
+
return {
|
|
248
|
+
...runner,
|
|
249
|
+
last_seen_at: now.toISOString(),
|
|
250
|
+
liveness_lease_expires_at: leaseExpiresAt.toISOString(),
|
|
251
|
+
...(input.wakeStreamOffset !== undefined
|
|
252
|
+
? { wake_stream_offset: input.wakeStreamOffset }
|
|
253
|
+
: {}),
|
|
254
|
+
...(input.diagnostics !== undefined
|
|
255
|
+
? { diagnostics: input.diagnostics }
|
|
256
|
+
: {}),
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async getRunnerDiagnostics(
|
|
261
|
+
runnerId: string
|
|
262
|
+
): Promise<RunnerRuntimeDiagnostics | null> {
|
|
263
|
+
const rows = await this.db
|
|
264
|
+
.select()
|
|
265
|
+
.from(runnerRuntimeDiagnostics)
|
|
211
266
|
.where(
|
|
212
|
-
and(
|
|
267
|
+
and(
|
|
268
|
+
eq(runnerRuntimeDiagnostics.tenantId, this.tenantId),
|
|
269
|
+
eq(runnerRuntimeDiagnostics.runnerId, runnerId)
|
|
270
|
+
)
|
|
213
271
|
)
|
|
214
|
-
.
|
|
215
|
-
|
|
216
|
-
return rows[0] ? this.rowToRunner(rows[0]) : null
|
|
272
|
+
.limit(1)
|
|
273
|
+
return rows[0] ? this.rowToRunnerRuntimeDiagnostics(rows[0]) : null
|
|
217
274
|
}
|
|
218
275
|
|
|
219
276
|
async setRunnerAdminStatus(
|
|
@@ -366,6 +423,54 @@ export class PostgresRegistry {
|
|
|
366
423
|
return claim
|
|
367
424
|
}
|
|
368
425
|
|
|
426
|
+
async getActiveClaimsForRunner(
|
|
427
|
+
runnerId: string
|
|
428
|
+
): Promise<Array<ConsumerClaim>> {
|
|
429
|
+
const rows = await this.db
|
|
430
|
+
.select()
|
|
431
|
+
.from(consumerClaims)
|
|
432
|
+
.where(
|
|
433
|
+
and(
|
|
434
|
+
eq(consumerClaims.tenantId, this.tenantId),
|
|
435
|
+
eq(consumerClaims.runnerId, runnerId),
|
|
436
|
+
eq(consumerClaims.status, `active`)
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
return rows.map((row) => this.rowToConsumerClaim(row))
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async getDispatchStatsForRunner(runnerId: string): Promise<{
|
|
443
|
+
entities_with_active_claim: number
|
|
444
|
+
entities_with_outstanding_wake: number
|
|
445
|
+
entities_with_pending_work: number
|
|
446
|
+
}> {
|
|
447
|
+
const rows = await this.db
|
|
448
|
+
.select()
|
|
449
|
+
.from(entityDispatchState)
|
|
450
|
+
.where(
|
|
451
|
+
and(
|
|
452
|
+
eq(entityDispatchState.tenantId, this.tenantId),
|
|
453
|
+
eq(entityDispatchState.activeRunnerId, runnerId)
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
let activeClaim = 0
|
|
458
|
+
let outstandingWake = 0
|
|
459
|
+
let pendingWork = 0
|
|
460
|
+
for (const row of rows) {
|
|
461
|
+
if (row.activeConsumerId) activeClaim++
|
|
462
|
+
if (row.outstandingWakeId && !row.activeConsumerId) outstandingWake++
|
|
463
|
+
const pending = row.pendingSourceStreams as Array<unknown> | null
|
|
464
|
+
if (pending && pending.length > 0) pendingWork++
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
entities_with_active_claim: activeClaim,
|
|
469
|
+
entities_with_outstanding_wake: outstandingWake,
|
|
470
|
+
entities_with_pending_work: pendingWork,
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
369
474
|
private entityTypeWhere(name: string) {
|
|
370
475
|
return and(
|
|
371
476
|
eq(entityTypes.tenantId, this.tenantId),
|
|
@@ -1146,27 +1251,32 @@ export class PostgresRegistry {
|
|
|
1146
1251
|
}
|
|
1147
1252
|
|
|
1148
1253
|
private rowToRunner(row: typeof runners.$inferSelect): ElectricAgentsRunner {
|
|
1149
|
-
const now = Date.now()
|
|
1150
|
-
const livenessExpiry = row.livenessLeaseExpiresAt?.getTime()
|
|
1151
1254
|
return {
|
|
1152
1255
|
id: row.id,
|
|
1153
|
-
|
|
1256
|
+
owner_principal: row.ownerPrincipal,
|
|
1154
1257
|
label: row.label,
|
|
1155
1258
|
kind: assertRunnerKind(row.kind),
|
|
1156
1259
|
admin_status: assertRunnerAdminStatus(row.adminStatus),
|
|
1157
|
-
liveness:
|
|
1158
|
-
livenessExpiry !== undefined && livenessExpiry > now
|
|
1159
|
-
? `online`
|
|
1160
|
-
: `offline`,
|
|
1161
|
-
last_seen_at: row.lastSeenAt?.toISOString(),
|
|
1162
|
-
liveness_lease_expires_at: row.livenessLeaseExpiresAt?.toISOString(),
|
|
1163
1260
|
wake_stream: row.wakeStream,
|
|
1164
|
-
wake_stream_offset: row.wakeStreamOffset ?? undefined,
|
|
1165
1261
|
created_at: row.createdAt.toISOString(),
|
|
1166
1262
|
updated_at: row.updatedAt.toISOString(),
|
|
1167
1263
|
}
|
|
1168
1264
|
}
|
|
1169
1265
|
|
|
1266
|
+
private rowToRunnerRuntimeDiagnostics(
|
|
1267
|
+
row: typeof runnerRuntimeDiagnostics.$inferSelect
|
|
1268
|
+
): RunnerRuntimeDiagnostics {
|
|
1269
|
+
return {
|
|
1270
|
+
runner_id: row.runnerId,
|
|
1271
|
+
owner_principal: row.ownerPrincipal,
|
|
1272
|
+
wake_stream_offset: row.wakeStreamOffset ?? undefined,
|
|
1273
|
+
last_seen_at: row.lastSeenAt.toISOString(),
|
|
1274
|
+
liveness_lease_expires_at: row.livenessLeaseExpiresAt.toISOString(),
|
|
1275
|
+
diagnostics: (row.diagnostics as Record<string, unknown>) ?? undefined,
|
|
1276
|
+
updated_at: row.updatedAt.toISOString(),
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1170
1280
|
private rowToConsumerClaim(
|
|
1171
1281
|
row: typeof consumerClaims.$inferSelect
|
|
1172
1282
|
): ConsumerClaim {
|
package/src/host.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { PostgresRegistry } from './entity-registry.js'
|
|
|
3
3
|
import { EntityProjector } from './entity-projector.js'
|
|
4
4
|
import { ElectricAgentsTenantRuntime } from './runtime.js'
|
|
5
5
|
import { PostgresSchedulerClient, Scheduler } from './scheduler.js'
|
|
6
|
-
import { StreamClient
|
|
6
|
+
import { StreamClient } from './stream-client.js'
|
|
7
7
|
import { TagStreamOutboxDrainer } from './tag-stream-outbox-drainer.js'
|
|
8
8
|
import { DEFAULT_TENANT_ID, UnregisteredTenantError } from './tenant.js'
|
|
9
9
|
import { WakeRegistry } from './wake-registry.js'
|
|
@@ -313,10 +313,9 @@ export class AgentsHost {
|
|
|
313
313
|
private createStreamClient(config: AgentsHostTenantConfig): StreamClient {
|
|
314
314
|
if (config.streamClient) return config.streamClient
|
|
315
315
|
if (config.durableStreamsUrl) {
|
|
316
|
-
return new StreamClient(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
)
|
|
316
|
+
return new StreamClient(config.durableStreamsUrl, {
|
|
317
|
+
bearer: config.durableStreamsBearer,
|
|
318
|
+
})
|
|
320
319
|
}
|
|
321
320
|
throw new Error(
|
|
322
321
|
`AgentsHost tenant "${config.serviceId}" must provide a streamClient or durableStreamsUrl`
|
package/src/index.ts
CHANGED
|
@@ -37,7 +37,11 @@ export type { Principal, PrincipalKind } from './principal.js'
|
|
|
37
37
|
export { globalRouter } from './routing/global-router.js'
|
|
38
38
|
export type { GlobalRoutes } from './routing/global-router.js'
|
|
39
39
|
export type { TenantContext } from './routing/context.js'
|
|
40
|
-
export {
|
|
40
|
+
export {
|
|
41
|
+
streamRootDurableStreamsRoutingAdapter,
|
|
42
|
+
pathPrefixedSingleTenantDurableStreamsRoutingAdapter,
|
|
43
|
+
tenantRootDurableStreamsRoutingAdapter,
|
|
44
|
+
} from './routing/durable-streams-routing-adapter.js'
|
|
41
45
|
export type {
|
|
42
46
|
DurableStreamsRoutingAdapter,
|
|
43
47
|
DurableStreamsRoutingInput,
|
package/src/principal.ts
CHANGED
|
@@ -20,7 +20,7 @@ const PRINCIPAL_KINDS = new Set<PrincipalKind>([
|
|
|
20
20
|
|
|
21
21
|
export function parsePrincipalKey(input: string): Principal {
|
|
22
22
|
const colon = input.indexOf(`:`)
|
|
23
|
-
if (colon <= 0) throw new Error(`Invalid principal
|
|
23
|
+
if (colon <= 0) throw new Error(`Invalid principal identifier`)
|
|
24
24
|
const kind = input.slice(0, colon) as PrincipalKind
|
|
25
25
|
const id = input.slice(colon + 1)
|
|
26
26
|
if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`)
|
|
@@ -33,24 +33,38 @@ export function principalUrl(key: string): string {
|
|
|
33
33
|
return parsePrincipalKey(key).url
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export function
|
|
36
|
+
export function parsePrincipalUrl(url: string): Principal | null {
|
|
37
37
|
if (!url.startsWith(`/principal/`)) return null
|
|
38
38
|
const segment = url.slice(`/principal/`.length)
|
|
39
39
|
if (!segment || segment.includes(`/`)) return null
|
|
40
40
|
try {
|
|
41
|
-
const key = decodeURIComponent(segment)
|
|
42
41
|
// Principal URLs produced by parsePrincipalKey/principalUrl are canonical
|
|
43
42
|
// encoded single path segments, but accept legacy unencoded single-segment
|
|
44
43
|
// URLs here so callers can canonicalize them via parsePrincipalKey(key).url.
|
|
45
|
-
return parsePrincipalKey(
|
|
44
|
+
return parsePrincipalKey(decodeURIComponent(segment))
|
|
46
45
|
} catch {
|
|
47
46
|
return null
|
|
48
47
|
}
|
|
49
48
|
}
|
|
50
49
|
|
|
50
|
+
export function parsePrincipalInput(input: string): Principal | null {
|
|
51
|
+
const urlPrincipal = parsePrincipalUrl(input)
|
|
52
|
+
if (urlPrincipal) return urlPrincipal
|
|
53
|
+
try {
|
|
54
|
+
return parsePrincipalKey(input)
|
|
55
|
+
} catch {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isPrincipalUrl(url: string): boolean {
|
|
61
|
+
return parsePrincipalUrl(url) !== null
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
export function getPrincipalFromRequest(request: Request): Principal | null {
|
|
52
65
|
const value = request.headers.get(ELECTRIC_PRINCIPAL_HEADER)
|
|
53
|
-
|
|
66
|
+
if (!value) return null
|
|
67
|
+
return parsePrincipalInput(value)
|
|
54
68
|
}
|
|
55
69
|
|
|
56
70
|
export function getDevPrincipal(): Principal {
|
|
@@ -66,9 +80,8 @@ const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
|
|
|
66
80
|
export function isBuiltInSystemPrincipalUrl(url: string | undefined): boolean {
|
|
67
81
|
if (!url?.startsWith(`/principal/`)) return false
|
|
68
82
|
try {
|
|
69
|
-
const
|
|
70
|
-
if (!
|
|
71
|
-
const principal = parsePrincipalKey(key)
|
|
83
|
+
const principal = parsePrincipalUrl(url)
|
|
84
|
+
if (!principal) return false
|
|
72
85
|
return (
|
|
73
86
|
principal.kind === `system` &&
|
|
74
87
|
BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id)
|
|
@@ -84,9 +97,8 @@ export function principalFromCreatedBy(
|
|
|
84
97
|
| { url: string; key?: string | null; kind?: string; id?: string }
|
|
85
98
|
| undefined {
|
|
86
99
|
if (!createdBy) return undefined
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
89
|
-
const principal = parsePrincipalKey(key)
|
|
100
|
+
const principal = parsePrincipalUrl(createdBy)
|
|
101
|
+
if (!principal) return { url: createdBy, key: null }
|
|
90
102
|
return {
|
|
91
103
|
url: principal.url,
|
|
92
104
|
key: principal.key,
|
package/src/routing/context.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface TenantContext {
|
|
|
19
19
|
principal: Principal
|
|
20
20
|
publicUrl: string
|
|
21
21
|
localUrl?: string
|
|
22
|
+
/** Durable Streams backend URL prefix. Stream and control paths are appended as-is. */
|
|
22
23
|
durableStreamsUrl: string
|
|
23
24
|
durableStreamsBearer?: DurableStreamsBearerProvider
|
|
24
25
|
durableStreamsRouting?: DurableStreamsRoutingAdapter
|