@electric-ax/agents-server 0.4.5 → 0.4.7
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 +404 -68
- package/dist/index.cjs +421 -67
- package/dist/index.d.cts +97 -11
- package/dist/index.d.ts +97 -11
- package/dist/index.js +414 -69
- package/drizzle/0009_entity_signal_statuses.sql +3 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/db/schema.ts +1 -1
- package/src/electric-agents-types.ts +76 -1
- package/src/entity-manager.ts +256 -33
- package/src/entity-registry.ts +57 -20
- package/src/entrypoint-lib.ts +5 -0
- package/src/index.ts +33 -0
- package/src/routing/context.ts +6 -0
- package/src/routing/dispatch-policy.ts +6 -0
- package/src/routing/durable-streams-router.ts +62 -13
- package/src/routing/entities-router.ts +57 -1
- package/src/routing/internal-router.ts +147 -23
- package/src/routing/runners-router.ts +1 -1
- package/src/runtime.ts +5 -1
- package/src/server.ts +18 -0
- package/src/stream-client.ts +10 -4
- package/src/webhook-signing.ts +173 -0
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.7",
|
|
4
4
|
"description": "Electric Agents entity runtime server",
|
|
5
5
|
"author": "Durable Stream contributors",
|
|
6
6
|
"bin": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
39
39
|
"@durable-streams/client": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/client@5d5c217",
|
|
40
|
-
"@durable-streams/server": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/server@
|
|
40
|
+
"@durable-streams/server": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/server@eac712f",
|
|
41
41
|
"@durable-streams/state": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/state@5d5c217",
|
|
42
42
|
"@electric-sql/client": "^1.5.18",
|
|
43
43
|
"@mariozechner/pi-agent-core": "^0.70.2",
|
|
@@ -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.3.
|
|
57
|
+
"@electric-ax/agents-runtime": "0.3.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-server-
|
|
69
|
-
"@electric-ax/agents
|
|
70
|
-
"@electric-ax/agents": "0.4.
|
|
68
|
+
"@electric-ax/agents-server-conformance-tests": "0.1.7",
|
|
69
|
+
"@electric-ax/agents": "0.4.6",
|
|
70
|
+
"@electric-ax/agents-server-ui": "0.4.7"
|
|
71
71
|
},
|
|
72
72
|
"files": [
|
|
73
73
|
"dist",
|
package/src/db/schema.ts
CHANGED
|
@@ -66,7 +66,7 @@ export const entities = pgTable(
|
|
|
66
66
|
index(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
|
|
67
67
|
check(
|
|
68
68
|
`chk_entities_status`,
|
|
69
|
-
sql`${table.status} IN ('spawning', 'running', 'idle', 'stopped')`
|
|
69
|
+
sql`${table.status} IN ('spawning', 'running', 'idle', 'paused', 'stopping', 'stopped', 'killed')`
|
|
70
70
|
),
|
|
71
71
|
]
|
|
72
72
|
)
|
|
@@ -15,15 +15,39 @@ export type AuthenticateRequest = (
|
|
|
15
15
|
request: Request
|
|
16
16
|
) => Promise<Principal | null> | Principal | null
|
|
17
17
|
|
|
18
|
-
export type EntityStatus =
|
|
18
|
+
export type EntityStatus =
|
|
19
|
+
| `spawning`
|
|
20
|
+
| `running`
|
|
21
|
+
| `idle`
|
|
22
|
+
| `paused`
|
|
23
|
+
| `stopping`
|
|
24
|
+
| `stopped`
|
|
25
|
+
| `killed`
|
|
26
|
+
|
|
27
|
+
export const ENTITY_SIGNALS = [
|
|
28
|
+
`SIGINT`,
|
|
29
|
+
`SIGHUP`,
|
|
30
|
+
`SIGTERM`,
|
|
31
|
+
`SIGKILL`,
|
|
32
|
+
`SIGSTOP`,
|
|
33
|
+
`SIGCONT`,
|
|
34
|
+
`SIGUSR`,
|
|
35
|
+
] as const
|
|
36
|
+
|
|
37
|
+
export type EntitySignal = (typeof ENTITY_SIGNALS)[number]
|
|
19
38
|
|
|
20
39
|
const VALID_ENTITY_STATUSES = new Set<string>([
|
|
21
40
|
`spawning`,
|
|
22
41
|
`running`,
|
|
23
42
|
`idle`,
|
|
43
|
+
`paused`,
|
|
44
|
+
`stopping`,
|
|
24
45
|
`stopped`,
|
|
46
|
+
`killed`,
|
|
25
47
|
])
|
|
26
48
|
|
|
49
|
+
const VALID_ENTITY_SIGNALS = new Set<string>(ENTITY_SIGNALS)
|
|
50
|
+
|
|
27
51
|
export function assertEntityStatus(s: string): EntityStatus {
|
|
28
52
|
if (!VALID_ENTITY_STATUSES.has(s)) {
|
|
29
53
|
throw new Error(`Invalid entity status: "${s}"`)
|
|
@@ -236,6 +260,41 @@ export interface ConsumerClaim {
|
|
|
236
260
|
updated_at: string
|
|
237
261
|
}
|
|
238
262
|
|
|
263
|
+
export function assertEntitySignal(s: string): EntitySignal {
|
|
264
|
+
if (!VALID_ENTITY_SIGNALS.has(s)) {
|
|
265
|
+
throw new Error(`Invalid entity signal: "${s}"`)
|
|
266
|
+
}
|
|
267
|
+
return s as EntitySignal
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function isTerminalEntityStatus(status: EntityStatus): boolean {
|
|
271
|
+
return status === `stopped` || status === `killed`
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function rejectsNormalWrites(status: EntityStatus): boolean {
|
|
275
|
+
return status === `stopping` || isTerminalEntityStatus(status)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function expectedSignalStatus(
|
|
279
|
+
status: EntityStatus,
|
|
280
|
+
signal: EntitySignal
|
|
281
|
+
): EntityStatus {
|
|
282
|
+
switch (signal) {
|
|
283
|
+
case `SIGKILL`:
|
|
284
|
+
return `killed`
|
|
285
|
+
case `SIGTERM`:
|
|
286
|
+
return status === `idle` ? `stopped` : `stopping`
|
|
287
|
+
case `SIGSTOP`:
|
|
288
|
+
return status === `idle` ? `paused` : status
|
|
289
|
+
case `SIGCONT`:
|
|
290
|
+
return status === `paused` ? `idle` : status
|
|
291
|
+
case `SIGINT`:
|
|
292
|
+
case `SIGHUP`:
|
|
293
|
+
case `SIGUSR`:
|
|
294
|
+
return status
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
239
298
|
export interface ElectricAgentsEntity {
|
|
240
299
|
url: string
|
|
241
300
|
type: string
|
|
@@ -353,6 +412,21 @@ export interface SendRequest {
|
|
|
353
412
|
position?: string
|
|
354
413
|
}
|
|
355
414
|
|
|
415
|
+
export interface SignalRequest {
|
|
416
|
+
signal: EntitySignal
|
|
417
|
+
reason?: string
|
|
418
|
+
payload?: unknown
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export interface SignalResponse {
|
|
422
|
+
url: string
|
|
423
|
+
signal: EntitySignal
|
|
424
|
+
previous_state: EntityStatus
|
|
425
|
+
new_state: EntityStatus
|
|
426
|
+
created_at: number
|
|
427
|
+
txid: number
|
|
428
|
+
}
|
|
429
|
+
|
|
356
430
|
export interface SetTagRequest {
|
|
357
431
|
value: string
|
|
358
432
|
}
|
|
@@ -369,6 +443,7 @@ export const ErrCodeNoSubscription = `NO_SUBSCRIPTION`
|
|
|
369
443
|
export const ErrCodeNotFound = `NOT_FOUND`
|
|
370
444
|
export const ErrCodeNotRunning = `NOT_RUNNING`
|
|
371
445
|
export const ErrCodeInvalidRequest = `INVALID_REQUEST`
|
|
446
|
+
export const ErrCodeInvalidSignal = `INVALID_SIGNAL`
|
|
372
447
|
export const ErrCodeUnknownEntityType = `UNKNOWN_ENTITY_TYPE`
|
|
373
448
|
export const ErrCodeSchemaValidationFailed = `SCHEMA_VALIDATION_FAILED`
|
|
374
449
|
export const ErrCodeUnknownMessageType = `UNKNOWN_MESSAGE_TYPE`
|
package/src/entity-manager.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
ErrCodeEntityPersistFailed,
|
|
17
17
|
ErrCodeForkInProgress,
|
|
18
18
|
ErrCodeForkWaitTimeout,
|
|
19
|
+
ErrCodeInvalidSignal,
|
|
19
20
|
ErrCodeInvalidRequest,
|
|
20
21
|
ErrCodeNotFound,
|
|
21
22
|
ErrCodeNotRunning,
|
|
@@ -25,6 +26,8 @@ import {
|
|
|
25
26
|
ErrCodeUnknownEntityType,
|
|
26
27
|
ErrCodeUnknownEventType,
|
|
27
28
|
ErrCodeUnknownMessageType,
|
|
29
|
+
isTerminalEntityStatus,
|
|
30
|
+
rejectsNormalWrites,
|
|
28
31
|
} from './electric-agents-types.js'
|
|
29
32
|
import { parseDispatchPolicy } from './dispatch-policy-schema.js'
|
|
30
33
|
import { applyTypeDefaultSubscriptionScope } from './routing/dispatch-policy.js'
|
|
@@ -54,9 +57,12 @@ import type {
|
|
|
54
57
|
DispatchPolicy,
|
|
55
58
|
ElectricAgentsEntity,
|
|
56
59
|
ElectricAgentsEntityType,
|
|
60
|
+
EntitySignal,
|
|
57
61
|
RegisterEntityTypeRequest,
|
|
58
62
|
SendRequest,
|
|
59
63
|
SetTagRequest,
|
|
64
|
+
SignalRequest,
|
|
65
|
+
SignalResponse,
|
|
60
66
|
TypedSpawnRequest,
|
|
61
67
|
} from './electric-agents-types.js'
|
|
62
68
|
import type { EntityBridgeCoordinator } from './entity-bridge-manager.js'
|
|
@@ -72,6 +78,36 @@ type WriteTokenValidator = (
|
|
|
72
78
|
entity: ElectricAgentsEntity,
|
|
73
79
|
token: string
|
|
74
80
|
) => boolean
|
|
81
|
+
type ServerSignalOutcome = `transitioned` | `ignored`
|
|
82
|
+
type ServerSignalHandling = {
|
|
83
|
+
status: ElectricAgentsEntity[`status`]
|
|
84
|
+
handled: boolean
|
|
85
|
+
outcome: ServerSignalOutcome
|
|
86
|
+
unregisterWakes: boolean
|
|
87
|
+
}
|
|
88
|
+
type ServerSignalValue = {
|
|
89
|
+
signal: EntitySignal
|
|
90
|
+
status: `handled` | `unhandled`
|
|
91
|
+
sender: typeof SERVER_SIGNAL_SENDER
|
|
92
|
+
timestamp: string
|
|
93
|
+
reason?: string
|
|
94
|
+
payload?: unknown
|
|
95
|
+
handled_at?: string
|
|
96
|
+
handled_by?: typeof SERVER_SIGNAL_SENDER
|
|
97
|
+
outcome?: ServerSignalOutcome
|
|
98
|
+
previous_state?: ElectricAgentsEntity[`status`]
|
|
99
|
+
new_state?: ElectricAgentsEntity[`status`]
|
|
100
|
+
}
|
|
101
|
+
type ServerSignalEvent = {
|
|
102
|
+
type: `signal`
|
|
103
|
+
key: string
|
|
104
|
+
value: ServerSignalValue
|
|
105
|
+
headers: {
|
|
106
|
+
operation: `insert`
|
|
107
|
+
timestamp: string
|
|
108
|
+
txid: string
|
|
109
|
+
}
|
|
110
|
+
}
|
|
75
111
|
|
|
76
112
|
function createInitialQueuePosition(date: Date): string {
|
|
77
113
|
return `${String(date.getTime()).padStart(16, `0`)}:a0`
|
|
@@ -103,6 +139,8 @@ type ForkResult = {
|
|
|
103
139
|
const DEFAULT_FORK_WAIT_TIMEOUT_MS = 120_000
|
|
104
140
|
const DEFAULT_FORK_WAIT_POLL_MS = 250
|
|
105
141
|
|
|
142
|
+
const SERVER_SIGNAL_SENDER = `/_electric/server`
|
|
143
|
+
|
|
106
144
|
function sleep(ms: number): Promise<void> {
|
|
107
145
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
108
146
|
}
|
|
@@ -928,16 +966,20 @@ export class EntityManager {
|
|
|
928
966
|
}
|
|
929
967
|
|
|
930
968
|
const subtree = await this.listEntitySubtree(root)
|
|
931
|
-
const stopped = subtree.find((entity) =>
|
|
969
|
+
const stopped = subtree.find((entity) =>
|
|
970
|
+
isTerminalEntityStatus(entity.status)
|
|
971
|
+
)
|
|
932
972
|
if (stopped) {
|
|
933
973
|
throw new ElectricAgentsError(
|
|
934
974
|
ErrCodeNotRunning,
|
|
935
|
-
`Cannot fork
|
|
975
|
+
`Cannot fork terminal entity "${stopped.url}"`,
|
|
936
976
|
409
|
|
937
977
|
)
|
|
938
978
|
}
|
|
939
979
|
|
|
940
|
-
let active = subtree.filter(
|
|
980
|
+
let active = subtree.filter(
|
|
981
|
+
(entity) => entity.status !== `idle` && entity.status !== `paused`
|
|
982
|
+
)
|
|
941
983
|
if (active.length === 0) {
|
|
942
984
|
this.addForkLocks(
|
|
943
985
|
this.forkWorkLockedEntities,
|
|
@@ -959,7 +1001,7 @@ export class EntityManager {
|
|
|
959
1001
|
workLocks
|
|
960
1002
|
)
|
|
961
1003
|
const lockedActive = lockedSubtree.filter(
|
|
962
|
-
(entity) => entity.status !== `idle`
|
|
1004
|
+
(entity) => entity.status !== `idle` && entity.status !== `paused`
|
|
963
1005
|
)
|
|
964
1006
|
if (lockedActive.length === 0) {
|
|
965
1007
|
return lockedSubtree
|
|
@@ -1673,6 +1715,12 @@ export class EntityManager {
|
|
|
1673
1715
|
value.processed_at = now
|
|
1674
1716
|
}
|
|
1675
1717
|
|
|
1718
|
+
const wakePausedEntity = entity.status === `paused` && req.mode !== `paused`
|
|
1719
|
+
if (wakePausedEntity) {
|
|
1720
|
+
await this.registry.updateStatus(entityUrl, `idle`)
|
|
1721
|
+
await this.entityBridgeManager?.onEntityChanged(entityUrl)
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1676
1724
|
const envelope = entityStateSchema.inbox.insert({
|
|
1677
1725
|
key,
|
|
1678
1726
|
value,
|
|
@@ -1725,8 +1773,12 @@ export class EntityManager {
|
|
|
1725
1773
|
if (!entity) {
|
|
1726
1774
|
throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404)
|
|
1727
1775
|
}
|
|
1728
|
-
if (entity.status
|
|
1729
|
-
throw new ElectricAgentsError(
|
|
1776
|
+
if (rejectsNormalWrites(entity.status)) {
|
|
1777
|
+
throw new ElectricAgentsError(
|
|
1778
|
+
ErrCodeNotRunning,
|
|
1779
|
+
`Entity is not accepting writes`,
|
|
1780
|
+
409
|
|
1781
|
+
)
|
|
1730
1782
|
}
|
|
1731
1783
|
|
|
1732
1784
|
const now = new Date().toISOString()
|
|
@@ -1763,8 +1815,12 @@ export class EntityManager {
|
|
|
1763
1815
|
if (!entity) {
|
|
1764
1816
|
throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404)
|
|
1765
1817
|
}
|
|
1766
|
-
if (entity.status
|
|
1767
|
-
throw new ElectricAgentsError(
|
|
1818
|
+
if (rejectsNormalWrites(entity.status)) {
|
|
1819
|
+
throw new ElectricAgentsError(
|
|
1820
|
+
ErrCodeNotRunning,
|
|
1821
|
+
`Entity is not accepting writes`,
|
|
1822
|
+
409
|
|
1823
|
+
)
|
|
1768
1824
|
}
|
|
1769
1825
|
|
|
1770
1826
|
const envelope = entityStateSchema.inbox.delete({ key } as any)
|
|
@@ -1796,8 +1852,12 @@ export class EntityManager {
|
|
|
1796
1852
|
401
|
|
1797
1853
|
)
|
|
1798
1854
|
}
|
|
1799
|
-
if (entity.status
|
|
1800
|
-
throw new ElectricAgentsError(
|
|
1855
|
+
if (rejectsNormalWrites(entity.status)) {
|
|
1856
|
+
throw new ElectricAgentsError(
|
|
1857
|
+
ErrCodeNotRunning,
|
|
1858
|
+
`Entity is not accepting writes`,
|
|
1859
|
+
409
|
|
1860
|
+
)
|
|
1801
1861
|
}
|
|
1802
1862
|
|
|
1803
1863
|
if (typeof req.value !== `string`) {
|
|
@@ -1842,8 +1902,12 @@ export class EntityManager {
|
|
|
1842
1902
|
401
|
|
1843
1903
|
)
|
|
1844
1904
|
}
|
|
1845
|
-
if (entity.status
|
|
1846
|
-
throw new ElectricAgentsError(
|
|
1905
|
+
if (rejectsNormalWrites(entity.status)) {
|
|
1906
|
+
throw new ElectricAgentsError(
|
|
1907
|
+
ErrCodeNotRunning,
|
|
1908
|
+
`Entity is not accepting writes`,
|
|
1909
|
+
409
|
|
1910
|
+
)
|
|
1847
1911
|
}
|
|
1848
1912
|
|
|
1849
1913
|
const result = await this.registry.removeEntityTag(entityUrl, key)
|
|
@@ -2354,37 +2418,194 @@ export class EntityManager {
|
|
|
2354
2418
|
}
|
|
2355
2419
|
|
|
2356
2420
|
// ==========================================================================
|
|
2357
|
-
//
|
|
2421
|
+
// Signals
|
|
2358
2422
|
// ==========================================================================
|
|
2359
2423
|
|
|
2360
|
-
async
|
|
2424
|
+
async signal(entityUrl: string, req: SignalRequest): Promise<SignalResponse> {
|
|
2361
2425
|
const entity = await this.registry.getEntity(entityUrl)
|
|
2362
2426
|
if (!entity) {
|
|
2363
2427
|
throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404)
|
|
2364
2428
|
}
|
|
2365
2429
|
|
|
2366
|
-
|
|
2367
|
-
|
|
2430
|
+
if (isTerminalEntityStatus(entity.status)) {
|
|
2431
|
+
throw new ElectricAgentsError(
|
|
2432
|
+
ErrCodeInvalidSignal,
|
|
2433
|
+
`Cannot signal a ${entity.status} entity`,
|
|
2434
|
+
409
|
|
2435
|
+
)
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
const now = new Date()
|
|
2439
|
+
const previousState = entity.status
|
|
2440
|
+
const handling = this.serverHandlingForSignal(previousState, req.signal)
|
|
2441
|
+
const txid =
|
|
2442
|
+
handling.status === previousState
|
|
2443
|
+
? await this.registry.touchEntityWithTxid(entityUrl)
|
|
2444
|
+
: await this.registry.updateStatusWithTxid(entityUrl, handling.status)
|
|
2445
|
+
if (txid === null) {
|
|
2446
|
+
throw new ElectricAgentsError(
|
|
2447
|
+
ErrCodeInvalidSignal,
|
|
2448
|
+
`Cannot signal entity because it is already terminal`,
|
|
2449
|
+
409
|
|
2450
|
+
)
|
|
2451
|
+
}
|
|
2368
2452
|
|
|
2369
|
-
const
|
|
2370
|
-
|
|
2371
|
-
|
|
2453
|
+
const key = `sig-${now.getTime()}-${randomUUID().slice(0, 8)}`
|
|
2454
|
+
const signalValue: ServerSignalValue = {
|
|
2455
|
+
signal: req.signal,
|
|
2456
|
+
status: handling.handled ? `handled` : `unhandled`,
|
|
2457
|
+
sender: SERVER_SIGNAL_SENDER,
|
|
2458
|
+
timestamp: now.toISOString(),
|
|
2459
|
+
}
|
|
2460
|
+
if (req.reason !== undefined) signalValue.reason = req.reason
|
|
2461
|
+
if (req.payload !== undefined) signalValue.payload = req.payload
|
|
2462
|
+
if (handling.handled) {
|
|
2463
|
+
signalValue.handled_at = now.toISOString()
|
|
2464
|
+
signalValue.handled_by = SERVER_SIGNAL_SENDER
|
|
2465
|
+
signalValue.outcome = handling.outcome
|
|
2466
|
+
signalValue.previous_state = previousState
|
|
2467
|
+
signalValue.new_state = handling.status
|
|
2372
2468
|
}
|
|
2373
2469
|
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
key
|
|
2377
|
-
value:
|
|
2378
|
-
|
|
2470
|
+
const signalEvent: ServerSignalEvent = {
|
|
2471
|
+
type: `signal`,
|
|
2472
|
+
key,
|
|
2473
|
+
value: signalValue,
|
|
2474
|
+
headers: {
|
|
2475
|
+
operation: `insert`,
|
|
2476
|
+
timestamp: now.toISOString(),
|
|
2477
|
+
txid: String(txid),
|
|
2379
2478
|
},
|
|
2380
|
-
}
|
|
2381
|
-
|
|
2382
|
-
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
const shouldCloseStreams = isTerminalEntityStatus(handling.status)
|
|
2482
|
+
await this.appendSignalEvent(entity, signalEvent, shouldCloseStreams)
|
|
2483
|
+
if (!shouldCloseStreams) {
|
|
2484
|
+
await this.evaluateWakes(
|
|
2485
|
+
entityUrl,
|
|
2486
|
+
signalEvent as unknown as Record<string, unknown>
|
|
2487
|
+
)
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
if (handling.unregisterWakes) {
|
|
2491
|
+
await this.wakeRegistry.unregisterBySubscriber(entityUrl, this.tenantId)
|
|
2492
|
+
await this.wakeRegistry.unregisterBySource(entityUrl, this.tenantId)
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
if (handling.status !== previousState && this.entityBridgeManager) {
|
|
2496
|
+
await this.entityBridgeManager.onEntityChanged(entityUrl)
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
return {
|
|
2500
|
+
url: entityUrl,
|
|
2501
|
+
signal: req.signal,
|
|
2502
|
+
previous_state: previousState,
|
|
2503
|
+
new_state: handling.status,
|
|
2504
|
+
created_at: now.getTime(),
|
|
2505
|
+
txid,
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
async kill(entityUrl: string): Promise<{ txid: number }> {
|
|
2510
|
+
const response = await this.signal(entityUrl, {
|
|
2511
|
+
signal: `SIGKILL`,
|
|
2512
|
+
reason: `Legacy kill command`,
|
|
2513
|
+
})
|
|
2514
|
+
return { txid: response.txid }
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
private serverHandlingForSignal(
|
|
2518
|
+
status: ElectricAgentsEntity[`status`],
|
|
2519
|
+
signal: EntitySignal
|
|
2520
|
+
): ServerSignalHandling {
|
|
2521
|
+
if (signal === `SIGKILL`) {
|
|
2522
|
+
return {
|
|
2523
|
+
status: `killed`,
|
|
2524
|
+
handled: true,
|
|
2525
|
+
outcome: `transitioned`,
|
|
2526
|
+
unregisterWakes: true,
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
if (signal === `SIGTERM`) {
|
|
2530
|
+
if (status === `idle` || status === `paused`) {
|
|
2531
|
+
return {
|
|
2532
|
+
status: `stopped`,
|
|
2533
|
+
handled: true,
|
|
2534
|
+
outcome: `transitioned`,
|
|
2535
|
+
unregisterWakes: true,
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
if (status === `running`) {
|
|
2539
|
+
return {
|
|
2540
|
+
status: `stopping`,
|
|
2541
|
+
handled: false,
|
|
2542
|
+
outcome: `transitioned`,
|
|
2543
|
+
unregisterWakes: false,
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
if (status === `paused` && signal !== `SIGCONT`) {
|
|
2548
|
+
return {
|
|
2549
|
+
status,
|
|
2550
|
+
handled: true,
|
|
2551
|
+
outcome: `ignored`,
|
|
2552
|
+
unregisterWakes: false,
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
if (signal === `SIGSTOP` && (status === `idle` || status === `running`)) {
|
|
2556
|
+
return {
|
|
2557
|
+
status: `paused`,
|
|
2558
|
+
handled: status === `idle`,
|
|
2559
|
+
outcome: `transitioned`,
|
|
2560
|
+
unregisterWakes: false,
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
if (signal === `SIGCONT` && status === `paused`) {
|
|
2564
|
+
return {
|
|
2565
|
+
status: `idle`,
|
|
2566
|
+
handled: false,
|
|
2567
|
+
outcome: `transitioned`,
|
|
2568
|
+
unregisterWakes: false,
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
return {
|
|
2573
|
+
status,
|
|
2574
|
+
handled: false,
|
|
2575
|
+
outcome: `ignored`,
|
|
2576
|
+
unregisterWakes: false,
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
private async appendSignalEvent(
|
|
2581
|
+
entity: ElectricAgentsEntity,
|
|
2582
|
+
signalEvent: ServerSignalEvent,
|
|
2583
|
+
closeStreams: boolean
|
|
2584
|
+
): Promise<void> {
|
|
2585
|
+
const signalData = this.encodeChangeEvent(
|
|
2586
|
+
signalEvent as unknown as Record<string, unknown>
|
|
2587
|
+
)
|
|
2588
|
+
if (!closeStreams) {
|
|
2589
|
+
await this.streamClient.append(entity.streams.main, signalData)
|
|
2590
|
+
return
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
const errorCloseEvent = {
|
|
2594
|
+
type: `signal`,
|
|
2595
|
+
key: signalEvent.key,
|
|
2596
|
+
value: signalEvent.value,
|
|
2597
|
+
headers: signalEvent.headers,
|
|
2598
|
+
}
|
|
2599
|
+
const errorSignalData = this.encodeChangeEvent(
|
|
2600
|
+
errorCloseEvent as unknown as Record<string, unknown>
|
|
2383
2601
|
)
|
|
2384
2602
|
|
|
2385
|
-
for (const streamPath of [
|
|
2603
|
+
for (const [streamPath, data] of [
|
|
2604
|
+
[entity.streams.main, signalData],
|
|
2605
|
+
[entity.streams.error, errorSignalData],
|
|
2606
|
+
] as const) {
|
|
2386
2607
|
try {
|
|
2387
|
-
await this.streamClient.append(streamPath,
|
|
2608
|
+
await this.streamClient.append(streamPath, data, { close: true })
|
|
2388
2609
|
} catch (err) {
|
|
2389
2610
|
const message = err instanceof Error ? err.message : String(err)
|
|
2390
2611
|
if (
|
|
@@ -2398,8 +2619,6 @@ export class EntityManager {
|
|
|
2398
2619
|
throw err
|
|
2399
2620
|
}
|
|
2400
2621
|
}
|
|
2401
|
-
|
|
2402
|
-
return { txid }
|
|
2403
2622
|
}
|
|
2404
2623
|
|
|
2405
2624
|
// ==========================================================================
|
|
@@ -2627,8 +2846,12 @@ export class EntityManager {
|
|
|
2627
2846
|
if (!entity) {
|
|
2628
2847
|
throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404)
|
|
2629
2848
|
}
|
|
2630
|
-
if (entity.status
|
|
2631
|
-
throw new ElectricAgentsError(
|
|
2849
|
+
if (rejectsNormalWrites(entity.status)) {
|
|
2850
|
+
throw new ElectricAgentsError(
|
|
2851
|
+
ErrCodeNotRunning,
|
|
2852
|
+
`Entity is not accepting writes`,
|
|
2853
|
+
409
|
|
2854
|
+
)
|
|
2632
2855
|
}
|
|
2633
2856
|
|
|
2634
2857
|
if (req.type && entity.type) {
|