@amodalai/runtime 0.2.0 → 0.2.2

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 (268) hide show
  1. package/dist/src/__fixtures__/README.md +4 -0
  2. package/dist/src/{agent/user-context-fetcher.test.d.ts → __fixtures__/e2e.test.d.ts} +1 -1
  3. package/dist/src/__fixtures__/e2e.test.js +211 -0
  4. package/dist/src/__fixtures__/e2e.test.js.map +1 -0
  5. package/dist/src/__fixtures__/smoke-agent/automations/delivery-callback-test.json +9 -0
  6. package/dist/src/__fixtures__/smoke-agent/connections/mock-mcp/spec.json +1 -1
  7. package/dist/src/__fixtures__/smoke.test.js +715 -29
  8. package/dist/src/__fixtures__/smoke.test.js.map +1 -1
  9. package/dist/src/__fixtures__/test-env.d.ts +27 -0
  10. package/dist/src/__fixtures__/test-env.js +64 -0
  11. package/dist/src/__fixtures__/test-env.js.map +1 -0
  12. package/dist/src/__fixtures__/test-helpers.d.ts +30 -0
  13. package/dist/src/__fixtures__/test-helpers.js +120 -0
  14. package/dist/src/__fixtures__/test-helpers.js.map +1 -0
  15. package/dist/src/agent/agent-types.d.ts +22 -0
  16. package/dist/src/agent/agent-types.js.map +1 -1
  17. package/dist/src/agent/automation-bridge.d.ts +9 -0
  18. package/dist/src/agent/automation-bridge.js +26 -0
  19. package/dist/src/agent/automation-bridge.js.map +1 -1
  20. package/dist/src/agent/automation-bridge.test.js +63 -0
  21. package/dist/src/agent/automation-bridge.test.js.map +1 -1
  22. package/dist/src/agent/local-server.d.ts +0 -7
  23. package/dist/src/agent/local-server.js +274 -87
  24. package/dist/src/agent/local-server.js.map +1 -1
  25. package/dist/src/agent/local-server.test.js +14 -11
  26. package/dist/src/agent/local-server.test.js.map +1 -1
  27. package/dist/src/agent/loop-types.d.ts +81 -7
  28. package/dist/src/agent/loop-types.js +4 -0
  29. package/dist/src/agent/loop-types.js.map +1 -1
  30. package/dist/src/agent/loop.js +16 -3
  31. package/dist/src/agent/loop.js.map +1 -1
  32. package/dist/src/agent/loop.test.js +572 -10
  33. package/dist/src/agent/loop.test.js.map +1 -1
  34. package/dist/src/agent/page-builder.js +20 -17
  35. package/dist/src/agent/page-builder.js.map +1 -1
  36. package/dist/src/agent/proactive/delivery-router.d.ts +68 -0
  37. package/dist/src/agent/proactive/delivery-router.js +337 -0
  38. package/dist/src/agent/proactive/delivery-router.js.map +1 -0
  39. package/dist/src/agent/proactive/delivery-router.test.d.ts +6 -0
  40. package/dist/src/agent/proactive/delivery-router.test.js +455 -0
  41. package/dist/src/agent/proactive/delivery-router.test.js.map +1 -0
  42. package/dist/src/agent/proactive/proactive-runner.d.ts +23 -1
  43. package/dist/src/agent/proactive/proactive-runner.js +42 -10
  44. package/dist/src/agent/proactive/proactive-runner.js.map +1 -1
  45. package/dist/src/agent/proactive/proactive-runner.test.js +0 -3
  46. package/dist/src/agent/proactive/proactive-runner.test.js.map +1 -1
  47. package/dist/src/agent/routes/admin-chat-abort.test.d.ts +6 -0
  48. package/dist/src/agent/routes/admin-chat-abort.test.js +206 -0
  49. package/dist/src/agent/routes/admin-chat-abort.test.js.map +1 -0
  50. package/dist/src/agent/routes/admin-chat.js +0 -3
  51. package/dist/src/agent/routes/admin-chat.js.map +1 -1
  52. package/dist/src/agent/routes/files.js +46 -52
  53. package/dist/src/agent/routes/files.js.map +1 -1
  54. package/dist/src/agent/routes/inspect.js +4 -6
  55. package/dist/src/agent/routes/inspect.js.map +1 -1
  56. package/dist/src/agent/routes/task.test.js +0 -3
  57. package/dist/src/agent/routes/task.test.js.map +1 -1
  58. package/dist/src/agent/snapshot-server.js +37 -3
  59. package/dist/src/agent/snapshot-server.js.map +1 -1
  60. package/dist/src/agent/states/compacting.js +5 -3
  61. package/dist/src/agent/states/compacting.js.map +1 -1
  62. package/dist/src/agent/states/confirming.js +3 -0
  63. package/dist/src/agent/states/confirming.js.map +1 -1
  64. package/dist/src/agent/states/dispatching.js +45 -2
  65. package/dist/src/agent/states/dispatching.js.map +1 -1
  66. package/dist/src/agent/states/executing.js +225 -81
  67. package/dist/src/agent/states/executing.js.map +1 -1
  68. package/dist/src/agent/states/streaming.js +14 -0
  69. package/dist/src/agent/states/streaming.js.map +1 -1
  70. package/dist/src/agent/states/thinking.d.ts +1 -1
  71. package/dist/src/agent/states/thinking.js +246 -29
  72. package/dist/src/agent/states/thinking.js.map +1 -1
  73. package/dist/src/agent/token-estimate.d.ts +20 -6
  74. package/dist/src/agent/token-estimate.js +24 -3
  75. package/dist/src/agent/token-estimate.js.map +1 -1
  76. package/dist/src/agent/token-estimate.test.d.ts +6 -0
  77. package/dist/src/agent/token-estimate.test.js +44 -0
  78. package/dist/src/agent/token-estimate.test.js.map +1 -0
  79. package/dist/src/agent/tool-executor-local.test.js +0 -1
  80. package/dist/src/agent/tool-executor-local.test.js.map +1 -1
  81. package/dist/src/agent/tool-harness-template.js +0 -1
  82. package/dist/src/agent/tool-harness-template.js.map +1 -1
  83. package/dist/src/api/create-agent.js +1 -5
  84. package/dist/src/api/create-agent.js.map +1 -1
  85. package/dist/src/api/types.d.ts +1 -5
  86. package/dist/src/channels/bootstrap.d.ts +59 -0
  87. package/dist/src/channels/bootstrap.js +84 -0
  88. package/dist/src/channels/bootstrap.js.map +1 -0
  89. package/dist/src/channels/channel-session-mapper.d.ts +42 -0
  90. package/dist/src/channels/channel-session-mapper.js +91 -0
  91. package/dist/src/channels/channel-session-mapper.js.map +1 -0
  92. package/dist/src/channels/dedup-cache.d.ts +17 -0
  93. package/dist/src/channels/dedup-cache.js +51 -0
  94. package/dist/src/channels/dedup-cache.js.map +1 -0
  95. package/dist/src/channels/dedup-cache.test.d.ts +6 -0
  96. package/dist/src/channels/dedup-cache.test.js +51 -0
  97. package/dist/src/channels/dedup-cache.test.js.map +1 -0
  98. package/dist/src/channels/errors.d.ts +28 -0
  99. package/dist/src/channels/errors.js +38 -0
  100. package/dist/src/channels/errors.js.map +1 -0
  101. package/dist/src/channels/in-memory-session-mapper.d.ts +34 -0
  102. package/dist/src/channels/in-memory-session-mapper.js +50 -0
  103. package/dist/src/channels/in-memory-session-mapper.js.map +1 -0
  104. package/dist/src/channels/plugin-loader.d.ts +20 -0
  105. package/dist/src/channels/plugin-loader.js +136 -0
  106. package/dist/src/channels/plugin-loader.js.map +1 -0
  107. package/dist/src/channels/plugin-loader.test.d.ts +6 -0
  108. package/dist/src/channels/plugin-loader.test.js +113 -0
  109. package/dist/src/channels/plugin-loader.test.js.map +1 -0
  110. package/dist/src/channels/routes.d.ts +29 -0
  111. package/dist/src/channels/routes.js +165 -0
  112. package/dist/src/channels/routes.js.map +1 -0
  113. package/dist/src/config.d.ts +0 -2
  114. package/dist/src/config.js +0 -1
  115. package/dist/src/config.js.map +1 -1
  116. package/dist/src/config.test.js +0 -2
  117. package/dist/src/config.test.js.map +1 -1
  118. package/dist/src/context/compiler.js +11 -34
  119. package/dist/src/context/compiler.js.map +1 -1
  120. package/dist/src/context/compiler.test.js +7 -60
  121. package/dist/src/context/compiler.test.js.map +1 -1
  122. package/dist/src/context/types.d.ts +0 -4
  123. package/dist/src/env-ref.d.ts +13 -0
  124. package/dist/src/env-ref.js +31 -0
  125. package/dist/src/env-ref.js.map +1 -0
  126. package/dist/src/env-ref.test.d.ts +6 -0
  127. package/dist/src/env-ref.test.js +34 -0
  128. package/dist/src/env-ref.test.js.map +1 -0
  129. package/dist/src/errors.d.ts +15 -0
  130. package/dist/src/errors.js +22 -0
  131. package/dist/src/errors.js.map +1 -1
  132. package/dist/src/errors.test.js +2 -2
  133. package/dist/src/errors.test.js.map +1 -1
  134. package/dist/src/events/event-bus.d.ts +54 -0
  135. package/dist/src/events/event-bus.js +84 -0
  136. package/dist/src/events/event-bus.js.map +1 -0
  137. package/dist/src/events/event-bus.test.d.ts +6 -0
  138. package/dist/src/events/event-bus.test.js +112 -0
  139. package/dist/src/events/event-bus.test.js.map +1 -0
  140. package/dist/src/events/events-route.d.ts +36 -0
  141. package/dist/src/events/events-route.js +80 -0
  142. package/dist/src/events/events-route.js.map +1 -0
  143. package/dist/src/events/events-route.test.d.ts +6 -0
  144. package/dist/src/events/events-route.test.js +134 -0
  145. package/dist/src/events/events-route.test.js.map +1 -0
  146. package/dist/src/events/store-event-wrapper.d.ts +19 -0
  147. package/dist/src/events/store-event-wrapper.js +57 -0
  148. package/dist/src/events/store-event-wrapper.js.map +1 -0
  149. package/dist/src/events/store-event-wrapper.test.d.ts +6 -0
  150. package/dist/src/events/store-event-wrapper.test.js +91 -0
  151. package/dist/src/events/store-event-wrapper.test.js.map +1 -0
  152. package/dist/src/index.d.ts +13 -0
  153. package/dist/src/index.js +10 -0
  154. package/dist/src/index.js.map +1 -1
  155. package/dist/src/middleware/auth.d.ts +0 -2
  156. package/dist/src/middleware/auth.js.map +1 -1
  157. package/dist/src/providers/search-provider.d.ts +64 -0
  158. package/dist/src/providers/search-provider.js +174 -0
  159. package/dist/src/providers/search-provider.js.map +1 -0
  160. package/dist/src/providers/types.d.ts +8 -0
  161. package/dist/src/routes/ai-stream.d.ts +18 -4
  162. package/dist/src/routes/ai-stream.js +10 -2
  163. package/dist/src/routes/ai-stream.js.map +1 -1
  164. package/dist/src/routes/chat-stream.d.ts +9 -1
  165. package/dist/src/routes/chat-stream.js +3 -1
  166. package/dist/src/routes/chat-stream.js.map +1 -1
  167. package/dist/src/routes/chat.d.ts +6 -0
  168. package/dist/src/routes/chat.js +2 -1
  169. package/dist/src/routes/chat.js.map +1 -1
  170. package/dist/src/routes/session-resolver.d.ts +15 -2
  171. package/dist/src/routes/session-resolver.js +22 -25
  172. package/dist/src/routes/session-resolver.js.map +1 -1
  173. package/dist/src/routes/session-resolver.test.js +117 -20
  174. package/dist/src/routes/session-resolver.test.js.map +1 -1
  175. package/dist/src/server.d.ts +35 -1
  176. package/dist/src/server.js +33 -0
  177. package/dist/src/server.js.map +1 -1
  178. package/dist/src/session/drizzle-session-store.d.ts +57 -0
  179. package/dist/src/session/drizzle-session-store.js +204 -0
  180. package/dist/src/session/drizzle-session-store.js.map +1 -0
  181. package/dist/src/session/manager.d.ts +6 -3
  182. package/dist/src/session/manager.js +46 -19
  183. package/dist/src/session/manager.js.map +1 -1
  184. package/dist/src/session/manager.test.js +12 -18
  185. package/dist/src/session/manager.test.js.map +1 -1
  186. package/dist/src/session/pglite-session-store.d.ts +23 -0
  187. package/dist/src/session/pglite-session-store.js +86 -0
  188. package/dist/src/session/pglite-session-store.js.map +1 -0
  189. package/dist/src/session/postgres-session-store.d.ts +44 -0
  190. package/dist/src/session/postgres-session-store.js +153 -0
  191. package/dist/src/session/postgres-session-store.js.map +1 -0
  192. package/dist/src/session/session-builder.d.ts +0 -5
  193. package/dist/src/session/session-builder.js +22 -6
  194. package/dist/src/session/session-builder.js.map +1 -1
  195. package/dist/src/session/session-builder.test.js +3 -8
  196. package/dist/src/session/session-builder.test.js.map +1 -1
  197. package/dist/src/session/session-store-selector.d.ts +49 -0
  198. package/dist/src/session/session-store-selector.js +60 -0
  199. package/dist/src/session/session-store-selector.js.map +1 -0
  200. package/dist/src/session/session-store-selector.test.d.ts +6 -0
  201. package/dist/src/session/session-store-selector.test.js +79 -0
  202. package/dist/src/session/session-store-selector.test.js.map +1 -0
  203. package/dist/src/session/store.d.ts +146 -32
  204. package/dist/src/session/store.js +126 -138
  205. package/dist/src/session/store.js.map +1 -1
  206. package/dist/src/session/store.test.js +385 -107
  207. package/dist/src/session/store.test.js.map +1 -1
  208. package/dist/src/session/tool-context-factory.d.ts +3 -7
  209. package/dist/src/session/tool-context-factory.js +1 -3
  210. package/dist/src/session/tool-context-factory.js.map +1 -1
  211. package/dist/src/session/tool-context-factory.test.js +1 -6
  212. package/dist/src/session/tool-context-factory.test.js.map +1 -1
  213. package/dist/src/session/types.d.ts +13 -10
  214. package/dist/src/stores/schema.d.ts +111 -34
  215. package/dist/src/stores/schema.js +21 -4
  216. package/dist/src/stores/schema.js.map +1 -1
  217. package/dist/src/tools/admin-file-tools.d.ts +29 -0
  218. package/dist/src/tools/admin-file-tools.js +527 -13
  219. package/dist/src/tools/admin-file-tools.js.map +1 -1
  220. package/dist/src/tools/admin-file-tools.test.js +380 -9
  221. package/dist/src/tools/admin-file-tools.test.js.map +1 -1
  222. package/dist/src/tools/custom-tool-adapter.js +0 -1
  223. package/dist/src/tools/custom-tool-adapter.js.map +1 -1
  224. package/dist/src/tools/custom-tool-adapter.test.js +0 -2
  225. package/dist/src/tools/custom-tool-adapter.test.js.map +1 -1
  226. package/dist/src/tools/dispatch-tool.d.ts +4 -4
  227. package/dist/src/tools/fetch-url-tool.d.ts +23 -0
  228. package/dist/src/tools/fetch-url-tool.js +333 -0
  229. package/dist/src/tools/fetch-url-tool.js.map +1 -0
  230. package/dist/src/tools/fetch-url-tool.test.d.ts +6 -0
  231. package/dist/src/tools/fetch-url-tool.test.js +227 -0
  232. package/dist/src/tools/fetch-url-tool.test.js.map +1 -0
  233. package/dist/src/tools/mcp-tool-adapter.test.js +0 -2
  234. package/dist/src/tools/mcp-tool-adapter.test.js.map +1 -1
  235. package/dist/src/tools/registry.test.js +0 -2
  236. package/dist/src/tools/registry.test.js.map +1 -1
  237. package/dist/src/tools/request-tool.test.js +0 -2
  238. package/dist/src/tools/request-tool.test.js.map +1 -1
  239. package/dist/src/tools/store-tools.test.js +0 -2
  240. package/dist/src/tools/store-tools.test.js.map +1 -1
  241. package/dist/src/tools/types.d.ts +20 -7
  242. package/dist/src/tools/web-search-tool.d.ts +31 -0
  243. package/dist/src/tools/web-search-tool.js +170 -0
  244. package/dist/src/tools/web-search-tool.js.map +1 -0
  245. package/dist/src/tools/web-search-tool.test.d.ts +6 -0
  246. package/dist/src/tools/web-search-tool.test.js +152 -0
  247. package/dist/src/tools/web-search-tool.test.js.map +1 -0
  248. package/dist/src/tools/web-tools-shared.d.ts +21 -0
  249. package/dist/src/tools/web-tools-shared.js +32 -0
  250. package/dist/src/tools/web-tools-shared.js.map +1 -0
  251. package/dist/src/types.d.ts +20 -4
  252. package/dist/src/types.js +13 -2
  253. package/dist/src/types.js.map +1 -1
  254. package/dist/src/types.test.js +0 -3
  255. package/dist/src/types.test.js.map +1 -1
  256. package/dist/tsconfig.tsbuildinfo +1 -1
  257. package/package.json +17 -3
  258. package/dist/src/agent/session-store.d.ts +0 -71
  259. package/dist/src/agent/session-store.js +0 -151
  260. package/dist/src/agent/session-store.js.map +0 -1
  261. package/dist/src/agent/user-context-fetcher.d.ts +0 -25
  262. package/dist/src/agent/user-context-fetcher.js +0 -79
  263. package/dist/src/agent/user-context-fetcher.js.map +0 -1
  264. package/dist/src/agent/user-context-fetcher.test.js +0 -121
  265. package/dist/src/agent/user-context-fetcher.test.js.map +0 -1
  266. package/dist/src/session/admin-file-tools.d.ts +0 -136
  267. package/dist/src/session/admin-file-tools.js +0 -240
  268. package/dist/src/session/admin-file-tools.js.map +0 -1
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Amodal Labs, Inc.
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ /**
7
+ * Runtime event bus.
8
+ *
9
+ * Centralized pub/sub for server-level state changes (session created,
10
+ * automation triggered, store updated, manifest reloaded). Clients
11
+ * subscribe via the `/api/events` SSE endpoint and use these events to
12
+ * drive live UI updates instead of polling.
13
+ *
14
+ * Each event gets a monotonic sequence number assigned at emit time.
15
+ * A ring buffer of recent events lets reconnecting clients catch up
16
+ * via the `Last-Event-ID` SSE header without missing any state changes.
17
+ */
18
+ import type { RuntimeEvent, RuntimeEventPayload } from '@amodalai/types';
19
+ export type RuntimeEventListener = (event: RuntimeEvent) => void;
20
+ export interface EventBusOptions {
21
+ /** Max events retained for reconnect-and-resume. Default 200. */
22
+ bufferSize?: number;
23
+ /**
24
+ * Called when a listener throws. The bus never rethrows listener errors
25
+ * (one bad subscriber shouldn't break the broadcast to others), but a
26
+ * throwing listener is a bug worth surfacing. Wire this to a logger.
27
+ */
28
+ onListenerError?: (err: unknown, event: RuntimeEvent) => void;
29
+ }
30
+ export declare class RuntimeEventBus {
31
+ private listeners;
32
+ private seq;
33
+ private readonly bufferSize;
34
+ private readonly onListenerError;
35
+ private buffer;
36
+ constructor(options?: EventBusOptions);
37
+ /**
38
+ * Emit an event. The bus assigns `seq` and `timestamp`.
39
+ * Returns the fully-populated event.
40
+ */
41
+ emit(payload: RuntimeEventPayload): RuntimeEvent;
42
+ /**
43
+ * Subscribe to events. Returns an unsubscribe function.
44
+ *
45
+ * If `sinceSeq` is provided, buffered events with `seq > sinceSeq`
46
+ * are delivered synchronously before the listener starts receiving
47
+ * live events. Used for reconnect-and-resume.
48
+ */
49
+ subscribe(listener: RuntimeEventListener, sinceSeq?: number): () => void;
50
+ /** Count of currently subscribed listeners (for observability) */
51
+ get listenerCount(): number;
52
+ /** Current sequence number (for tests + observability) */
53
+ get currentSeq(): number;
54
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Amodal Labs, Inc.
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ /** Maximum number of events kept in the replay buffer */
7
+ const DEFAULT_BUFFER_SIZE = 200;
8
+ export class RuntimeEventBus {
9
+ listeners = new Set();
10
+ seq = 0;
11
+ bufferSize;
12
+ onListenerError;
13
+ buffer = [];
14
+ constructor(options = {}) {
15
+ this.bufferSize = options.bufferSize ?? DEFAULT_BUFFER_SIZE;
16
+ this.onListenerError = options.onListenerError;
17
+ }
18
+ /**
19
+ * Emit an event. The bus assigns `seq` and `timestamp`.
20
+ * Returns the fully-populated event.
21
+ */
22
+ emit(payload) {
23
+ this.seq += 1;
24
+ // The payload's discriminant `type` field carries through; we only
25
+ // add `seq` and `timestamp`. Shape-preserving by construction.
26
+ const event = {
27
+ ...payload,
28
+ seq: this.seq,
29
+ timestamp: new Date().toISOString(),
30
+ };
31
+ // Append to ring buffer
32
+ this.buffer.push(event);
33
+ if (this.buffer.length > this.bufferSize) {
34
+ this.buffer.shift();
35
+ }
36
+ // Fan out to listeners. Errors in one listener must not break the
37
+ // broadcast to others — so we catch, surface via `onListenerError`
38
+ // (for observability), and continue. Not a silent swallow: the
39
+ // caller sees every failure via the injected callback.
40
+ for (const listener of this.listeners) {
41
+ try {
42
+ listener(event);
43
+ }
44
+ catch (err) {
45
+ this.onListenerError?.(err, event);
46
+ }
47
+ }
48
+ return event;
49
+ }
50
+ /**
51
+ * Subscribe to events. Returns an unsubscribe function.
52
+ *
53
+ * If `sinceSeq` is provided, buffered events with `seq > sinceSeq`
54
+ * are delivered synchronously before the listener starts receiving
55
+ * live events. Used for reconnect-and-resume.
56
+ */
57
+ subscribe(listener, sinceSeq) {
58
+ if (sinceSeq !== undefined) {
59
+ for (const event of this.buffer) {
60
+ if (event.seq > sinceSeq) {
61
+ try {
62
+ listener(event);
63
+ }
64
+ catch (err) {
65
+ this.onListenerError?.(err, event);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ this.listeners.add(listener);
71
+ return () => {
72
+ this.listeners.delete(listener);
73
+ };
74
+ }
75
+ /** Count of currently subscribed listeners (for observability) */
76
+ get listenerCount() {
77
+ return this.listeners.size;
78
+ }
79
+ /** Current sequence number (for tests + observability) */
80
+ get currentSeq() {
81
+ return this.seq;
82
+ }
83
+ }
84
+ //# sourceMappingURL=event-bus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-bus.js","sourceRoot":"","sources":["../../../src/events/event-bus.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAoBH,yDAAyD;AACzD,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAehC,MAAM,OAAO,eAAe;IAClB,SAAS,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC5C,GAAG,GAAG,CAAC,CAAC;IACC,UAAU,CAAS;IACnB,eAAe,CAAqC;IAC7D,MAAM,GAAmB,EAAE,CAAC;IAEpC,YAAY,UAA2B,EAAE;QACvC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,mBAAmB,CAAC;QAC5D,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;IACjD,CAAC;IAED;;;OAGG;IACH,IAAI,CAAC,OAA4B;QAC/B,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QACd,mEAAmE;QACnE,+DAA+D;QAC/D,MAAM,KAAK,GAAG;YACZ,GAAG,OAAO;YACV,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpB,CAAC;QAElB,wBAAwB;QACxB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YACzC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACtB,CAAC;QAED,kEAAkE;QAClE,mEAAmE;QACnE,+DAA+D;QAC/D,uDAAuD;QACvD,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACtC,IAAI,CAAC;gBACH,QAAQ,CAAC,KAAK,CAAC,CAAC;YAClB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,CAAC,eAAe,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;;;OAMG;IACH,SAAS,CAAC,QAA8B,EAAE,QAAiB;QACzD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBAChC,IAAI,KAAK,CAAC,GAAG,GAAG,QAAQ,EAAE,CAAC;oBACzB,IAAI,CAAC;wBACH,QAAQ,CAAC,KAAK,CAAC,CAAC;oBAClB,CAAC;oBAAC,OAAO,GAAG,EAAE,CAAC;wBACb,IAAI,CAAC,eAAe,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;oBACrC,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC,CAAC;IACJ,CAAC;IAED,kEAAkE;IAClE,IAAI,aAAa;QACf,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;IAC7B,CAAC;IAED,0DAA0D;IAC1D,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,GAAG,CAAC;IAClB,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Amodal Labs, Inc.
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ export {};
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Amodal Labs, Inc.
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { describe, it, expect, vi } from 'vitest';
7
+ import { RuntimeEventBus } from './event-bus.js';
8
+ describe('RuntimeEventBus', () => {
9
+ it('assigns monotonic sequence numbers starting at 1', () => {
10
+ const bus = new RuntimeEventBus();
11
+ const a = bus.emit({ type: 'session_created', sessionId: 's1', appId: 'local' });
12
+ const b = bus.emit({ type: 'session_updated', sessionId: 's1', appId: 'local' });
13
+ const c = bus.emit({ type: 'manifest_changed' });
14
+ expect(a.seq).toBe(1);
15
+ expect(b.seq).toBe(2);
16
+ expect(c.seq).toBe(3);
17
+ });
18
+ it('stamps ISO timestamps on every event', () => {
19
+ const bus = new RuntimeEventBus();
20
+ const event = bus.emit({ type: 'manifest_changed' });
21
+ expect(event.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
22
+ });
23
+ it('delivers events to all subscribed listeners', () => {
24
+ const bus = new RuntimeEventBus();
25
+ const listener1 = vi.fn();
26
+ const listener2 = vi.fn();
27
+ bus.subscribe(listener1);
28
+ bus.subscribe(listener2);
29
+ bus.emit({ type: 'session_created', sessionId: 's1', appId: 'local' });
30
+ expect(listener1).toHaveBeenCalledTimes(1);
31
+ expect(listener2).toHaveBeenCalledTimes(1);
32
+ });
33
+ it('unsubscribe stops delivery to that listener', () => {
34
+ const bus = new RuntimeEventBus();
35
+ const listener = vi.fn();
36
+ const unsub = bus.subscribe(listener);
37
+ bus.emit({ type: 'manifest_changed' });
38
+ unsub();
39
+ bus.emit({ type: 'manifest_changed' });
40
+ expect(listener).toHaveBeenCalledTimes(1);
41
+ });
42
+ it('replays buffered events when sinceSeq is provided', () => {
43
+ const bus = new RuntimeEventBus();
44
+ bus.emit({ type: 'session_created', sessionId: 's1', appId: 'local' }); // seq 1
45
+ bus.emit({ type: 'session_created', sessionId: 's2', appId: 'local' }); // seq 2
46
+ bus.emit({ type: 'session_created', sessionId: 's3', appId: 'local' }); // seq 3
47
+ const listener = vi.fn();
48
+ bus.subscribe(listener, 1);
49
+ expect(listener).toHaveBeenCalledTimes(2);
50
+ expect(listener.mock.calls[0]?.[0].seq).toBe(2);
51
+ expect(listener.mock.calls[1]?.[0].seq).toBe(3);
52
+ });
53
+ it('ring buffer evicts oldest events past capacity', () => {
54
+ const bus = new RuntimeEventBus({ bufferSize: 3 });
55
+ for (let i = 0; i < 5; i++) {
56
+ bus.emit({ type: 'manifest_changed' });
57
+ }
58
+ const listener = vi.fn();
59
+ bus.subscribe(listener, 0);
60
+ expect(listener).toHaveBeenCalledTimes(3);
61
+ expect(listener.mock.calls[0]?.[0].seq).toBe(3);
62
+ expect(listener.mock.calls[2]?.[0].seq).toBe(5);
63
+ });
64
+ it('a throwing listener does not block other listeners', () => {
65
+ const bus = new RuntimeEventBus();
66
+ const bad = vi.fn(() => { throw new Error('oops'); });
67
+ const good = vi.fn();
68
+ bus.subscribe(bad);
69
+ bus.subscribe(good);
70
+ bus.emit({ type: 'manifest_changed' });
71
+ expect(bad).toHaveBeenCalledTimes(1);
72
+ expect(good).toHaveBeenCalledTimes(1);
73
+ });
74
+ it('surfaces listener errors via onListenerError callback', () => {
75
+ const onListenerError = vi.fn();
76
+ const bus = new RuntimeEventBus({ onListenerError });
77
+ bus.subscribe(() => { throw new Error('oops'); });
78
+ bus.emit({ type: 'manifest_changed' });
79
+ expect(onListenerError).toHaveBeenCalledTimes(1);
80
+ const [err, event] = onListenerError.mock.calls[0] ?? [];
81
+ expect(err).toBeInstanceOf(Error);
82
+ expect(err.message).toBe('oops');
83
+ expect(event.type).toBe('manifest_changed');
84
+ });
85
+ it('listenerCount tracks active subscriptions', () => {
86
+ const bus = new RuntimeEventBus();
87
+ expect(bus.listenerCount).toBe(0);
88
+ const unsub1 = bus.subscribe(() => { });
89
+ const unsub2 = bus.subscribe(() => { });
90
+ expect(bus.listenerCount).toBe(2);
91
+ unsub1();
92
+ expect(bus.listenerCount).toBe(1);
93
+ unsub2();
94
+ expect(bus.listenerCount).toBe(0);
95
+ });
96
+ it('preserves event data fields through emit', () => {
97
+ const bus = new RuntimeEventBus();
98
+ const event = bus.emit({
99
+ type: 'store_updated',
100
+ storeName: 'leads',
101
+ operation: 'put',
102
+ count: 5,
103
+ });
104
+ expect(event.type).toBe('store_updated');
105
+ if (event.type === 'store_updated') {
106
+ expect(event.storeName).toBe('leads');
107
+ expect(event.operation).toBe('put');
108
+ expect(event.count).toBe(5);
109
+ }
110
+ });
111
+ });
112
+ //# sourceMappingURL=event-bus.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-bus.test.js","sourceRoot":"","sources":["../../../src/events/event-bus.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Amodal Labs, Inc.
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ /**
7
+ * SSE route for the runtime event bus.
8
+ *
9
+ * GET /api/events
10
+ *
11
+ * Clients open a single EventSource and receive every server-emitted
12
+ * runtime event (session_created, automation_triggered, store_updated,
13
+ * etc.) without polling.
14
+ *
15
+ * Reconnect-and-resume: clients may send a `Last-Event-ID` header
16
+ * (EventSource does this automatically on reconnect). The bus replays
17
+ * any buffered events with seq > Last-Event-ID before streaming live.
18
+ *
19
+ * Heartbeats: a comment line is sent every 15s so proxies don't close
20
+ * the connection during quiet periods.
21
+ */
22
+ import { Router } from 'express';
23
+ import type { RuntimeEventBus } from './event-bus.js';
24
+ /** Minimal logger surface — matches the runtime's Logger interface. */
25
+ interface EventsRouterLogger {
26
+ debug(event: string, data?: Record<string, unknown>): void;
27
+ }
28
+ export interface EventsRouterOptions {
29
+ bus: RuntimeEventBus;
30
+ /** Heartbeat interval in ms. Default 15000. */
31
+ heartbeatMs?: number;
32
+ /** Optional logger for connect/disconnect observability */
33
+ logger?: EventsRouterLogger;
34
+ }
35
+ export declare function createEventsRouter(options: EventsRouterOptions): Router;
36
+ export {};
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Amodal Labs, Inc.
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ /**
7
+ * SSE route for the runtime event bus.
8
+ *
9
+ * GET /api/events
10
+ *
11
+ * Clients open a single EventSource and receive every server-emitted
12
+ * runtime event (session_created, automation_triggered, store_updated,
13
+ * etc.) without polling.
14
+ *
15
+ * Reconnect-and-resume: clients may send a `Last-Event-ID` header
16
+ * (EventSource does this automatically on reconnect). The bus replays
17
+ * any buffered events with seq > Last-Event-ID before streaming live.
18
+ *
19
+ * Heartbeats: a comment line is sent every 15s so proxies don't close
20
+ * the connection during quiet periods.
21
+ */
22
+ import { Router } from 'express';
23
+ const HEARTBEAT_DEFAULT_MS = 15_000;
24
+ export function createEventsRouter(options) {
25
+ const router = Router();
26
+ const { bus, logger } = options;
27
+ const heartbeatMs = options.heartbeatMs ?? HEARTBEAT_DEFAULT_MS;
28
+ router.get('/api/events', (req, res) => {
29
+ // Parse optional Last-Event-ID for reconnect-and-resume
30
+ const lastIdHeader = req.header('Last-Event-ID');
31
+ let sinceSeq;
32
+ if (lastIdHeader) {
33
+ const parsed = Number.parseInt(lastIdHeader, 10);
34
+ if (Number.isFinite(parsed) && parsed >= 0) {
35
+ sinceSeq = parsed;
36
+ }
37
+ }
38
+ logger?.debug('events_client_connected', {
39
+ sinceSeq,
40
+ listenerCount: bus.listenerCount + 1,
41
+ });
42
+ // SSE response headers
43
+ res.setHeader('Content-Type', 'text/event-stream');
44
+ res.setHeader('Cache-Control', 'no-cache, no-transform');
45
+ res.setHeader('Connection', 'keep-alive');
46
+ // Disable nginx response buffering for SSE
47
+ res.setHeader('X-Accel-Buffering', 'no');
48
+ res.flushHeaders();
49
+ // Subscribe to the bus. `subscribe` replays buffered events > sinceSeq
50
+ // synchronously, then hands live events to the listener.
51
+ const unsubscribe = bus.subscribe((event) => {
52
+ // SSE frame: id + event + data. `id:` lets EventSource resume.
53
+ res.write(`id: ${String(event.seq)}\n`);
54
+ res.write(`event: ${event.type}\n`);
55
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
56
+ }, sinceSeq);
57
+ // Heartbeat: SSE comment line every N seconds. Keeps proxies from
58
+ // closing an idle connection and lets the client detect drops.
59
+ const heartbeat = setInterval(() => {
60
+ res.write(':\n\n');
61
+ }, heartbeatMs);
62
+ // Clean up on disconnect
63
+ let cleanedUp = false;
64
+ const cleanup = () => {
65
+ if (cleanedUp)
66
+ return;
67
+ cleanedUp = true;
68
+ clearInterval(heartbeat);
69
+ unsubscribe();
70
+ logger?.debug('events_client_disconnected', {
71
+ listenerCount: bus.listenerCount,
72
+ });
73
+ };
74
+ req.on('close', cleanup);
75
+ req.on('error', cleanup);
76
+ res.on('close', cleanup);
77
+ });
78
+ return router;
79
+ }
80
+ //# sourceMappingURL=events-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events-route.js","sourceRoot":"","sources":["../../../src/events/events-route.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAC,MAAM,EAAC,MAAM,SAAS,CAAC;AAiB/B,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAEpC,MAAM,UAAU,kBAAkB,CAAC,OAA4B;IAC7D,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,MAAM,EAAC,GAAG,EAAE,MAAM,EAAC,GAAG,OAAO,CAAC;IAC9B,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,oBAAoB,CAAC;IAEhE,MAAM,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE;QACxD,wDAAwD;QACxD,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;QACjD,IAAI,QAA4B,CAAC;QACjC,IAAI,YAAY,EAAE,CAAC;YACjB,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;YACjD,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;gBAC3C,QAAQ,GAAG,MAAM,CAAC;YACpB,CAAC;QACH,CAAC;QAED,MAAM,EAAE,KAAK,CAAC,yBAAyB,EAAE;YACvC,QAAQ;YACR,aAAa,EAAE,GAAG,CAAC,aAAa,GAAG,CAAC;SACrC,CAAC,CAAC;QAEH,uBAAuB;QACvB,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,mBAAmB,CAAC,CAAC;QACnD,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,wBAAwB,CAAC,CAAC;QACzD,GAAG,CAAC,SAAS,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;QAC1C,2CAA2C;QAC3C,GAAG,CAAC,SAAS,CAAC,mBAAmB,EAAE,IAAI,CAAC,CAAC;QACzC,GAAG,CAAC,YAAY,EAAE,CAAC;QAEnB,uEAAuE;QACvE,yDAAyD;QACzD,MAAM,WAAW,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;YAC1C,+DAA+D;YAC/D,GAAG,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACxC,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;YACpC,GAAG,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAClD,CAAC,EAAE,QAAQ,CAAC,CAAC;QAEb,kEAAkE;QAClE,+DAA+D;QAC/D,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE;YACjC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACrB,CAAC,EAAE,WAAW,CAAC,CAAC;QAEhB,yBAAyB;QACzB,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,MAAM,OAAO,GAAG,GAAS,EAAE;YACzB,IAAI,SAAS;gBAAE,OAAO;YACtB,SAAS,GAAG,IAAI,CAAC;YACjB,aAAa,CAAC,SAAS,CAAC,CAAC;YACzB,WAAW,EAAE,CAAC;YACd,MAAM,EAAE,KAAK,CAAC,4BAA4B,EAAE;gBAC1C,aAAa,EAAE,GAAG,CAAC,aAAa;aACjC,CAAC,CAAC;QACL,CAAC,CAAC;QAEF,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACzB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACzB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Amodal Labs, Inc.
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ export {};
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Amodal Labs, Inc.
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { describe, it, expect, afterEach } from 'vitest';
7
+ import express from 'express';
8
+ import { RuntimeEventBus } from './event-bus.js';
9
+ import { createEventsRouter } from './events-route.js';
10
+ function startServer(bus) {
11
+ const app = express();
12
+ app.use(createEventsRouter({ bus, heartbeatMs: 60_000 }));
13
+ return new Promise((resolve) => {
14
+ const server = app.listen(0, () => {
15
+ const address = server.address();
16
+ const port = address && typeof address === 'object' ? address.port : 0;
17
+ resolve({ port, server, bus });
18
+ });
19
+ });
20
+ }
21
+ function closeServer(server) {
22
+ return new Promise((resolve) => {
23
+ server.close(() => resolve());
24
+ });
25
+ }
26
+ async function collectEvents(url, expected, opts = {}) {
27
+ const headers = { Accept: 'text/event-stream' };
28
+ if (opts.lastEventId)
29
+ headers['Last-Event-ID'] = opts.lastEventId;
30
+ const controller = new AbortController();
31
+ const timeout = setTimeout(() => controller.abort(), opts.timeoutMs ?? 2000);
32
+ const res = await fetch(url, { headers, signal: controller.signal });
33
+ if (!res.body)
34
+ throw new Error('no response body');
35
+ const reader = res.body.getReader();
36
+ const decoder = new TextDecoder();
37
+ const collected = [];
38
+ let buffer = '';
39
+ try {
40
+ while (collected.length < expected) {
41
+ const { done, value } = await reader.read();
42
+ if (done)
43
+ break;
44
+ buffer += decoder.decode(value, { stream: true });
45
+ let idx;
46
+ while ((idx = buffer.indexOf('\n\n')) !== -1) {
47
+ const frame = buffer.slice(0, idx);
48
+ buffer = buffer.slice(idx + 2);
49
+ const dataLine = frame.split('\n').find((l) => l.startsWith('data:'));
50
+ if (dataLine) {
51
+ const json = dataLine.slice(5).trim();
52
+ collected.push(JSON.parse(json));
53
+ if (collected.length >= expected)
54
+ break;
55
+ }
56
+ }
57
+ }
58
+ }
59
+ finally {
60
+ clearTimeout(timeout);
61
+ controller.abort();
62
+ reader.cancel().catch(() => { });
63
+ }
64
+ return collected;
65
+ }
66
+ describe('events SSE route', () => {
67
+ let harness = null;
68
+ afterEach(async () => {
69
+ if (harness) {
70
+ await closeServer(harness.server);
71
+ harness = null;
72
+ }
73
+ });
74
+ it('streams emitted events to connected clients', async () => {
75
+ const bus = new RuntimeEventBus();
76
+ harness = await startServer(bus);
77
+ const url = `http://127.0.0.1:${String(harness.port)}/api/events`;
78
+ const eventsPromise = collectEvents(url, 2, { timeoutMs: 2000 });
79
+ setTimeout(() => {
80
+ bus.emit({ type: 'session_created', sessionId: 's1', appId: 'local' });
81
+ bus.emit({ type: 'manifest_changed' });
82
+ }, 50);
83
+ const events = await eventsPromise;
84
+ expect(events).toHaveLength(2);
85
+ expect(events[0]?.['type']).toBe('session_created');
86
+ expect(events[0]?.['seq']).toBe(1);
87
+ expect(events[1]?.['type']).toBe('manifest_changed');
88
+ expect(events[1]?.['seq']).toBe(2);
89
+ });
90
+ it('replays buffered events matching Last-Event-ID', async () => {
91
+ const bus = new RuntimeEventBus();
92
+ harness = await startServer(bus);
93
+ const url = `http://127.0.0.1:${String(harness.port)}/api/events`;
94
+ bus.emit({ type: 'session_created', sessionId: 's1', appId: 'local' });
95
+ bus.emit({ type: 'session_created', sessionId: 's2', appId: 'local' });
96
+ bus.emit({ type: 'session_created', sessionId: 's3', appId: 'local' });
97
+ const events = await collectEvents(url, 2, { lastEventId: '1', timeoutMs: 2000 });
98
+ expect(events).toHaveLength(2);
99
+ expect(events[0]?.['seq']).toBe(2);
100
+ expect(events[1]?.['seq']).toBe(3);
101
+ });
102
+ it('includes id and event headers in SSE frames', async () => {
103
+ const bus = new RuntimeEventBus();
104
+ harness = await startServer(bus);
105
+ const url = `http://127.0.0.1:${String(harness.port)}/api/events`;
106
+ const controller = new AbortController();
107
+ const timeout = setTimeout(() => controller.abort(), 2000);
108
+ const res = await fetch(url, {
109
+ headers: { Accept: 'text/event-stream' },
110
+ signal: controller.signal,
111
+ });
112
+ if (!res.body)
113
+ throw new Error('no response body');
114
+ const reader = res.body.getReader();
115
+ const decoder = new TextDecoder();
116
+ setTimeout(() => {
117
+ bus.emit({ type: 'manifest_changed' });
118
+ }, 50);
119
+ let raw = '';
120
+ while (!raw.includes('\n\n')) {
121
+ const { value, done } = await reader.read();
122
+ if (done)
123
+ break;
124
+ raw += decoder.decode(value, { stream: true });
125
+ }
126
+ clearTimeout(timeout);
127
+ controller.abort();
128
+ reader.cancel().catch(() => { });
129
+ expect(raw).toContain('id: 1');
130
+ expect(raw).toContain('event: manifest_changed');
131
+ expect(raw).toContain('data: {"type":"manifest_changed"');
132
+ });
133
+ });
134
+ //# sourceMappingURL=events-route.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events-route.test.js","sourceRoot":"","sources":["../../../src/events/events-route.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Amodal Labs, Inc.
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ /**
7
+ * Wraps a `StoreBackend` so every successful write emits a `store_updated`
8
+ * event to the runtime event bus. Read operations pass through unchanged.
9
+ *
10
+ * Instrumenting at the backend level (rather than each call site: tools,
11
+ * REST routes, admin file tools, task execution) means we cover every
12
+ * write path through one seam.
13
+ */
14
+ import type { StoreBackend, RuntimeEventPayload } from '@amodalai/types';
15
+ interface StoreEventSink {
16
+ emit: (payload: RuntimeEventPayload) => unknown;
17
+ }
18
+ export declare function wrapStoreBackendWithEvents(inner: StoreBackend, bus: StoreEventSink): StoreBackend;
19
+ export {};
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Amodal Labs, Inc.
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ export function wrapStoreBackendWithEvents(inner, bus) {
7
+ return {
8
+ initialize(stores) {
9
+ return inner.initialize(stores);
10
+ },
11
+ get(appId, store, key) {
12
+ return inner.get(appId, store, key);
13
+ },
14
+ list(appId, store, opts) {
15
+ return inner.list(appId, store, opts);
16
+ },
17
+ history(appId, store, key) {
18
+ return inner.history(appId, store, key);
19
+ },
20
+ async put(appId, store, key, payload, meta) {
21
+ const result = await inner.put(appId, store, key, payload, meta);
22
+ bus.emit({
23
+ type: 'store_updated',
24
+ storeName: store,
25
+ operation: 'put',
26
+ });
27
+ return result;
28
+ },
29
+ async delete(appId, store, key) {
30
+ const result = await inner.delete(appId, store, key);
31
+ if (result) {
32
+ bus.emit({
33
+ type: 'store_updated',
34
+ storeName: store,
35
+ operation: 'delete',
36
+ });
37
+ }
38
+ return result;
39
+ },
40
+ async purgeExpired(appId, store) {
41
+ const count = await inner.purgeExpired(appId, store);
42
+ if (count > 0) {
43
+ bus.emit({
44
+ type: 'store_updated',
45
+ storeName: store ?? '*',
46
+ operation: 'delete',
47
+ count,
48
+ });
49
+ }
50
+ return count;
51
+ },
52
+ close() {
53
+ return inner.close();
54
+ },
55
+ };
56
+ }
57
+ //# sourceMappingURL=store-event-wrapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store-event-wrapper.js","sourceRoot":"","sources":["../../../src/events/store-event-wrapper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA0BH,MAAM,UAAU,0BAA0B,CACxC,KAAmB,EACnB,GAAmB;IAEnB,OAAO;QACL,UAAU,CAAC,MAAqB;YAC9B,OAAO,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAClC,CAAC;QAED,GAAG,CAAC,KAAa,EAAE,KAAa,EAAE,GAAW;YAC3C,OAAO,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;QACtC,CAAC;QAED,IAAI,CAAC,KAAa,EAAE,KAAa,EAAE,IAAuB;YACxD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QACxC,CAAC;QAED,OAAO,CAAC,KAAa,EAAE,KAAa,EAAE,GAAW;YAC/C,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;QAC1C,CAAC;QAED,KAAK,CAAC,GAAG,CACP,KAAa,EACb,KAAa,EACb,GAAW,EACX,OAAgC,EAChC,IAAgC;YAEhC,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;YACjE,GAAG,CAAC,IAAI,CAAC;gBACP,IAAI,EAAE,eAAe;gBACrB,SAAS,EAAE,KAAK;gBAChB,SAAS,EAAE,KAAK;aACjB,CAAC,CAAC;YACH,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,KAAa,EAAE,KAAa,EAAE,GAAW;YACpD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;YACrD,IAAI,MAAM,EAAE,CAAC;gBACX,GAAG,CAAC,IAAI,CAAC;oBACP,IAAI,EAAE,eAAe;oBACrB,SAAS,EAAE,KAAK;oBAChB,SAAS,EAAE,QAAQ;iBACpB,CAAC,CAAC;YACL,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,KAAK,CAAC,YAAY,CAAC,KAAa,EAAE,KAAc;YAC9C,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YACrD,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;gBACd,GAAG,CAAC,IAAI,CAAC;oBACP,IAAI,EAAE,eAAe;oBACrB,SAAS,EAAE,KAAK,IAAI,GAAG;oBACvB,SAAS,EAAE,QAAQ;oBACnB,KAAK;iBACN,CAAC,CAAC;YACL,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,KAAK;YACH,OAAO,KAAK,CAAC,KAAK,EAAE,CAAC;QACvB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Amodal Labs, Inc.
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ export {};