@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.
@@ -0,0 +1,3 @@
1
+ ALTER TABLE "entities" DROP CONSTRAINT "chk_entities_status";
2
+ --> statement-breakpoint
3
+ ALTER TABLE "entities" ADD CONSTRAINT "chk_entities_status" CHECK ("entities"."status" IN ('spawning', 'running', 'idle', 'paused', 'stopping', 'stopped', 'killed'));
@@ -64,6 +64,13 @@
64
64
  "when": 1778976000000,
65
65
  "tag": "0008_runner_runtime_diagnostics",
66
66
  "breakpoints": true
67
+ },
68
+ {
69
+ "idx": 9,
70
+ "version": "7",
71
+ "when": 1778540000000,
72
+ "tag": "0009_entity_signal_statuses",
73
+ "breakpoints": true
67
74
  }
68
75
  ]
69
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electric-ax/agents-server",
3
- "version": "0.4.5",
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@5d5c217",
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.0"
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-ui": "0.4.5",
69
- "@electric-ax/agents-server-conformance-tests": "0.1.6",
70
- "@electric-ax/agents": "0.4.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 = `spawning` | `running` | `idle` | `stopped`
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`
@@ -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) => entity.status === `stopped`)
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 stopped entity "${stopped.url}"`,
975
+ `Cannot fork terminal entity "${stopped.url}"`,
936
976
  409
937
977
  )
938
978
  }
939
979
 
940
- let active = subtree.filter((entity) => entity.status !== `idle`)
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 === `stopped`) {
1729
- throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409)
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 === `stopped`) {
1767
- throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409)
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 === `stopped`) {
1800
- throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409)
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 === `stopped`) {
1846
- throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409)
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
- // Kill
2421
+ // Signals
2358
2422
  // ==========================================================================
2359
2423
 
2360
- async kill(entityUrl: string): Promise<{ txid: number }> {
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
- await this.wakeRegistry.unregisterBySubscriber(entityUrl, this.tenantId)
2367
- await this.wakeRegistry.unregisterBySource(entityUrl, this.tenantId)
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 txid = await this.registry.updateStatusWithTxid(entityUrl, `stopped`)
2370
- if (this.entityBridgeManager) {
2371
- await this.entityBridgeManager.onEntityChanged(entityUrl)
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
- // Append entity_stopped to main/error streams and close them.
2375
- const stoppedEvent = entityStateSchema.entityStopped.insert({
2376
- key: `stopped`,
2377
- value: {
2378
- timestamp: new Date().toISOString(),
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
- } as any)
2381
- const eofData = this.encodeChangeEvent(
2382
- stoppedEvent as Record<string, unknown>
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 [entity.streams.main, entity.streams.error]) {
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, eofData, { close: true })
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 === `stopped`) {
2631
- throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409)
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) {