@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
@@ -47,6 +47,69 @@ describe('bridgeAutomation', () => {
47
47
  expect(result.prompt).toBe(prompt);
48
48
  });
49
49
  });
50
+ describe('bridgeAutomation env:NAME resolution', () => {
51
+ it('resolves env:NAME refs in delivery webhook URLs', () => {
52
+ process.env['TEST_WEBHOOK_URL'] = 'https://hooks.example.com/abc123';
53
+ try {
54
+ const result = bridgeAutomation(makeAutomation({
55
+ delivery: {
56
+ targets: [{ type: 'webhook', url: 'env:TEST_WEBHOOK_URL' }],
57
+ },
58
+ }));
59
+ expect(result.delivery?.targets[0]).toEqual({
60
+ type: 'webhook',
61
+ url: 'https://hooks.example.com/abc123',
62
+ });
63
+ }
64
+ finally {
65
+ delete process.env['TEST_WEBHOOK_URL'];
66
+ }
67
+ });
68
+ it('resolves env:NAME refs in failureAlert webhook URLs', () => {
69
+ process.env['TEST_ALERT_URL'] = 'https://alerts.example.com/oncall';
70
+ try {
71
+ const result = bridgeAutomation(makeAutomation({
72
+ failureAlert: {
73
+ targets: [{ type: 'webhook', url: 'env:TEST_ALERT_URL' }],
74
+ },
75
+ }));
76
+ expect(result.failureAlert?.targets[0]).toEqual({
77
+ type: 'webhook',
78
+ url: 'https://alerts.example.com/oncall',
79
+ });
80
+ }
81
+ finally {
82
+ delete process.env['TEST_ALERT_URL'];
83
+ }
84
+ });
85
+ it('leaves callback targets unchanged', () => {
86
+ const result = bridgeAutomation(makeAutomation({
87
+ delivery: {
88
+ targets: [{ type: 'callback', name: 'my-handler' }],
89
+ },
90
+ }));
91
+ expect(result.delivery?.targets[0]).toEqual({ type: 'callback', name: 'my-handler' });
92
+ });
93
+ it('leaves literal http URLs unchanged', () => {
94
+ const result = bridgeAutomation(makeAutomation({
95
+ delivery: {
96
+ targets: [{ type: 'webhook', url: 'https://hooks.example.com/fixed' }],
97
+ },
98
+ }));
99
+ expect(result.delivery?.targets[0]).toEqual({
100
+ type: 'webhook',
101
+ url: 'https://hooks.example.com/fixed',
102
+ });
103
+ });
104
+ it('throws at bridge time if env var is missing (fail fast at boot)', () => {
105
+ delete process.env['NONEXISTENT_WEBHOOK'];
106
+ expect(() => bridgeAutomation(makeAutomation({
107
+ delivery: {
108
+ targets: [{ type: 'webhook', url: 'env:NONEXISTENT_WEBHOOK' }],
109
+ },
110
+ }))).toThrow(/NONEXISTENT_WEBHOOK/);
111
+ });
112
+ });
50
113
  describe('bridgeAutomations', () => {
51
114
  it('converts an array of automations', () => {
52
115
  const automations = [
@@ -1 +1 @@
1
- {"version":3,"file":"automation-bridge.test.js","sourceRoot":"","sources":["../../../src/agent/automation-bridge.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"automation-bridge.test.js","sourceRoot":"","sources":["../../../src/agent/automation-bridge.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAC,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAC,MAAM,QAAQ,CAAC;AAE5C,OAAO,EAAC,gBAAgB,EAAE,iBAAiB,EAAC,MAAM,wBAAwB,CAAC;AAE3E,SAAS,cAAc,CAAC,YAAuC,EAAE;IAC/D,OAAO;QACL,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,iBAAiB;QACxB,OAAO,EAAE,MAAM;QACf,QAAQ,EAAE,aAAa;QACvB,MAAM,EAAE,qBAAqB;QAC7B,QAAQ,EAAE,OAAO;QACjB,GAAG,SAAS;KACb,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,MAAM,GAAG,gBAAgB,CAAC,cAAc,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAC7C,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;QAClD,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC5C,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACjC,MAAM,MAAM,GAAG,gBAAgB,CAAC,cAAc,CAAC;YAC7C,OAAO,EAAE,SAAS;YAClB,QAAQ,EAAE,SAAS;SACpB,CAAC,CAAC,CAAC;QACJ,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,aAAa,EAAE,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,MAAM,GAAG,gBAAgB,CAAC,cAAc,CAAC;YAC7C,OAAO,EAAE,QAAQ;YACjB,QAAQ,EAAE,SAAS;SACpB,CAAC,CAAC,CAAC;QACJ,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9C,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,aAAa,EAAE,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC9B,MAAM,MAAM,GAAG,kFAAkF,CAAC;QAClG,MAAM,MAAM,GAAG,gBAAgB,CAAC,cAAc,CAAC,EAAC,MAAM,EAAC,CAAC,CAAC,CAAC;QAC1D,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,sCAAsC,EAAE,GAAG,EAAE;IACpD,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,GAAG,kCAAkC,CAAC;QACrE,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,gBAAgB,CAAC,cAAc,CAAC;gBAC7C,QAAQ,EAAE;oBACR,OAAO,EAAE,CAAC,EAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,sBAAsB,EAAC,CAAC;iBAC1D;aACF,CAAC,CAAC,CAAC;YACJ,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;gBAC1C,IAAI,EAAE,SAAS;gBACf,GAAG,EAAE,kCAAkC;aACxC,CAAC,CAAC;QACL,CAAC;gBAAS,CAAC;YACT,OAAO,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QACzC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,GAAG,mCAAmC,CAAC;QACpE,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,gBAAgB,CAAC,cAAc,CAAC;gBAC7C,YAAY,EAAE;oBACZ,OAAO,EAAE,CAAC,EAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,oBAAoB,EAAC,CAAC;iBACxD;aACF,CAAC,CAAC,CAAC;YACJ,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;gBAC9C,IAAI,EAAE,SAAS;gBACf,GAAG,EAAE,mCAAmC;aACzC,CAAC,CAAC;QACL,CAAC;gBAAS,CAAC;YACT,OAAO,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QACvC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,MAAM,GAAG,gBAAgB,CAAC,cAAc,CAAC;YAC7C,QAAQ,EAAE;gBACR,OAAO,EAAE,CAAC,EAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAC,CAAC;aAClD;SACF,CAAC,CAAC,CAAC;QACJ,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAC,CAAC,CAAC;IACtF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,MAAM,GAAG,gBAAgB,CAAC,cAAc,CAAC;YAC7C,QAAQ,EAAE;gBACR,OAAO,EAAE,CAAC,EAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,iCAAiC,EAAC,CAAC;aACrE;SACF,CAAC,CAAC,CAAC;QACJ,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;YAC1C,IAAI,EAAE,SAAS;YACf,GAAG,EAAE,iCAAiC;SACvC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,OAAO,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;QAC1C,MAAM,CAAC,GAAG,EAAE,CAAC,gBAAgB,CAAC,cAAc,CAAC;YAC3C,QAAQ,EAAE;gBACR,OAAO,EAAE,CAAC,EAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,yBAAyB,EAAC,CAAC;aAC7D;SACF,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,WAAW,GAAG;YAClB,cAAc,CAAC,EAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAC,CAAC;YACjD,cAAc,CAAC,EAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAC,CAAC;YACzE,cAAc,CAAC,EAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAC,CAAC;SACzE,CAAC;QAEF,MAAM,OAAO,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;QAC/C,MAAM,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAChC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAClD,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjD,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -5,11 +5,4 @@
5
5
  */
6
6
  import type { LocalServerConfig } from './agent-types.js';
7
7
  import type { ServerInstance } from '../server.js';
8
- /**
9
- * Creates an Express server for repo-based agent mode.
10
- *
11
- * Loads the `.amodal/` config from `config.repoPath`, creates a
12
- * `StandaloneSessionManager`, mounts all routes, and optionally watches
13
- * for config changes (hot reload).
14
- */
15
8
  export declare function createLocalServer(config: LocalServerConfig): Promise<ServerInstance>;
@@ -18,11 +18,15 @@ import { existsSync } from 'node:fs';
18
18
  import path from 'node:path';
19
19
  import { loadRepo } from '@amodalai/core';
20
20
  import { StandaloneSessionManager } from '../session/manager.js';
21
- import { PGLiteSessionStore } from '../session/store.js';
21
+ import { selectSessionStore } from '../session/session-store-selector.js';
22
+ import { resolveEnvRef } from '../env-ref.js';
22
23
  import { buildSessionComponents } from '../session/session-builder.js';
23
24
  import { LocalToolExecutor } from './tool-executor-local.js';
24
25
  import { buildMcpConfigs } from './mcp-config.js';
25
26
  import { ConfigWatcher } from './config-watcher.js';
27
+ import { RuntimeEventBus } from '../events/event-bus.js';
28
+ import { createEventsRouter } from '../events/events-route.js';
29
+ import { wrapStoreBackendWithEvents } from '../events/store-event-wrapper.js';
26
30
  import { ProactiveRunner } from './proactive/proactive-runner.js';
27
31
  import { createChatStreamRouter } from '../routes/chat-stream.js';
28
32
  import { createChatRouter } from '../routes/chat.js';
@@ -37,14 +41,22 @@ import { createStoresRouter } from './routes/stores.js';
37
41
  import { createFilesRouter } from './routes/files.js';
38
42
  import { createEvalRouter } from './routes/evals.js';
39
43
  import { errorHandler } from '../middleware/error-handler.js';
44
+ import { asyncHandler } from '../routes/route-helpers.js';
40
45
  import { createPGLiteStoreBackend } from '../stores/pglite-store-backend.js';
41
- import { SessionStore } from './session-store.js';
42
46
  import { EvalStore } from './eval-store.js';
43
47
  import { buildPages } from './page-builder.js';
44
48
  import { LOCAL_APP_ID } from '../constants.js';
45
49
  import { log, createLogger } from '../logger.js';
50
+ import { bootstrapChannels } from '../channels/bootstrap.js';
51
+ import { DrizzleChannelSessionMapper } from '../channels/channel-session-mapper.js';
52
+ // Each check must use an endpoint that returns 200 on a valid key and
53
+ // a distinct auth-failure status (typically 401) on a bad key. Do NOT
54
+ // use endpoints with method guards that might return 405 before the
55
+ // auth check — `GET /v1/messages` on Anthropic does exactly that, and
56
+ // makes every key (good or bad) look invalid because Anthropic returns
57
+ // 405 for wrong-method regardless of whether the x-api-key is real.
46
58
  const PROVIDER_CHECKS = [
47
- { provider: 'anthropic', envVar: 'ANTHROPIC_API_KEY', url: 'https://api.anthropic.com/v1/messages', authHeader: (key) => ({ 'x-api-key': key, 'anthropic-version': '2023-06-01' }) },
59
+ { provider: 'anthropic', envVar: 'ANTHROPIC_API_KEY', url: 'https://api.anthropic.com/v1/models', authHeader: (key) => ({ 'x-api-key': key, 'anthropic-version': '2023-06-01' }) },
48
60
  { provider: 'openai', envVar: 'OPENAI_API_KEY', url: 'https://api.openai.com/v1/models', authHeader: (key) => ({ Authorization: `Bearer ${key}` }) },
49
61
  ];
50
62
  async function checkProviders() {
@@ -80,7 +92,33 @@ async function checkProviders() {
80
92
  * `StandaloneSessionManager`, mounts all routes, and optionally watches
81
93
  * for config changes (hot reload).
82
94
  */
95
+ /**
96
+ * Install a process-level unhandledRejection listener that logs instead
97
+ * of crashing. An escaped rejection is always a bug — we want loud logs,
98
+ * not silent outages. The previous behavior (default Node: print + crash)
99
+ * turned small bugs (one leaked promise) into whole-server downtime for
100
+ * every active session. Logging + continuing preserves service for all
101
+ * other sessions while still surfacing the issue to operators.
102
+ *
103
+ * Idempotent: only installs once per process (the local-server can be
104
+ * created and torn down repeatedly during tests).
105
+ */
106
+ let unhandledRejectionListenerInstalled = false;
107
+ function installUnhandledRejectionLogger() {
108
+ if (unhandledRejectionListenerInstalled)
109
+ return;
110
+ unhandledRejectionListenerInstalled = true;
111
+ process.on('unhandledRejection', (reason) => {
112
+ const err = reason instanceof Error ? reason : new Error(String(reason));
113
+ log.error('unhandled_rejection', {
114
+ name: err.name,
115
+ message: err.message,
116
+ stack: err.stack,
117
+ });
118
+ });
119
+ }
83
120
  export async function createLocalServer(config) {
121
+ installUnhandledRejectionLogger();
84
122
  let bundle = await loadRepo({ localPath: config.repoPath });
85
123
  // Check provider API keys in the background at startup
86
124
  let providerStatuses = PROVIDER_CHECKS.map((c) => ({
@@ -110,9 +148,7 @@ export async function createLocalServer(config) {
110
148
  const storeConfig = bundle.config.stores;
111
149
  const backend = storeConfig?.backend ?? 'pglite';
112
150
  if (backend === 'postgres' && storeConfig?.postgresUrl) {
113
- const connUrl = storeConfig.postgresUrl.startsWith('env:')
114
- ? process.env[storeConfig.postgresUrl.slice(4)] ?? ''
115
- : storeConfig.postgresUrl;
151
+ const connUrl = resolveEnvRef(storeConfig.postgresUrl) ?? '';
116
152
  if (!connUrl) {
117
153
  log.error('store_postgres_url_missing', { configured: storeConfig.postgresUrl });
118
154
  }
@@ -178,19 +214,41 @@ export async function createLocalServer(config) {
178
214
  }
179
215
  }
180
216
  // -------------------------------------------------------------------------
217
+ // Runtime event bus (powers /api/events SSE for live UI updates)
218
+ // -------------------------------------------------------------------------
219
+ const eventBus = new RuntimeEventBus({
220
+ onListenerError: (err, event) => {
221
+ log.warn('event_bus_listener_error', {
222
+ seq: event.seq,
223
+ type: event.type,
224
+ error: err instanceof Error ? err.message : String(err),
225
+ });
226
+ },
227
+ });
228
+ // Wrap the store backend so every write emits store_updated events.
229
+ // Covers every write path through one seam: tools, REST routes, admin
230
+ // file tools, task execution — they all go through this backend.
231
+ if (storeBackend) {
232
+ storeBackend = wrapStoreBackendWithEvents(storeBackend, eventBus);
233
+ }
234
+ // -------------------------------------------------------------------------
181
235
  // Session manager (new standalone stack)
182
236
  // -------------------------------------------------------------------------
183
237
  const sessionLogger = createLogger({ component: 'session-manager' });
184
- const sessionStore = new PGLiteSessionStore({ logger: sessionLogger });
185
- await sessionStore.initialize();
238
+ const sessionDataDir = `${config.repoPath}/.amodal/session-data`;
239
+ const sessionStore = await selectSessionStore({
240
+ backend: bundle.config.stores?.backend,
241
+ postgresUrl: resolveEnvRef(bundle.config.stores?.postgresUrl),
242
+ logger: sessionLogger,
243
+ dataDir: sessionDataDir,
244
+ });
186
245
  const sessionManager = new StandaloneSessionManager({
187
246
  logger: sessionLogger,
188
247
  store: sessionStore,
189
248
  ttlMs: config.sessionTtlMs,
249
+ eventBus,
190
250
  });
191
251
  sessionManager.start();
192
- // Legacy session store for UI history (file-based)
193
- const legacySessionStore = new SessionStore(config.repoPath);
194
252
  // -------------------------------------------------------------------------
195
253
  // MCP connections (shared across sessions)
196
254
  // -------------------------------------------------------------------------
@@ -235,13 +293,10 @@ export async function createLocalServer(config) {
235
293
  sessionType: 'automation',
236
294
  });
237
295
  const session = sessionManager.create({
238
- tenantId: 'local',
239
- userId: 'automation',
240
296
  provider: components.provider,
241
297
  toolRegistry: components.toolRegistry,
242
298
  permissionChecker: components.permissionChecker,
243
299
  systemPrompt: components.systemPrompt,
244
- userRoles: components.userRoles,
245
300
  toolContextFactory: components.toolContextFactory,
246
301
  appId: LOCAL_APP_ID,
247
302
  });
@@ -255,11 +310,50 @@ export async function createLocalServer(config) {
255
310
  createSessionComponents: createAutomationSessionComponents,
256
311
  logger: log,
257
312
  webhookSecret: config.webhookSecret,
258
- onSessionComplete: (session) => {
313
+ summarizeToolResult: config.summarizeToolResult,
314
+ onSessionComplete: (session, automationName) => {
315
+ // Tag the automation name onto metadata so the UI can filter
316
+ // sessions by automation via /sessions?automation=<name>.
317
+ session.metadata.automationName = automationName;
259
318
  void sessionManager.persist(session);
260
319
  },
320
+ eventBus,
321
+ onAutomationResult: config.onAutomationResult,
261
322
  });
262
323
  // -------------------------------------------------------------------------
324
+ // Channel plugins (messaging integrations)
325
+ // -------------------------------------------------------------------------
326
+ let channelsResult = null;
327
+ if (bundle.channels && bundle.channels.length > 0) {
328
+ // Both PGLite and Postgres factories return DrizzleSessionStore which
329
+ // exposes `db`. Cast through the concrete type safely.
330
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- narrowing to concrete DrizzleSessionStore
331
+ const storeDb = sessionStore.db;
332
+ const channelSessionMapper = new DrizzleChannelSessionMapper({
333
+ db: storeDb,
334
+ logger: log,
335
+ eventBus,
336
+ });
337
+ channelsResult = await bootstrapChannels({
338
+ channels: bundle.channels,
339
+ repoPath: config.repoPath,
340
+ packages: bundle.config.packages,
341
+ sessionMapper: channelSessionMapper,
342
+ sessionManager,
343
+ buildSessionComponents: () => buildSessionComponents({
344
+ bundle,
345
+ storeBackend: storeBackend ?? null,
346
+ mcpManager,
347
+ logger: log,
348
+ toolExecutor,
349
+ sessionType: 'chat',
350
+ }),
351
+ appId: LOCAL_APP_ID,
352
+ eventBus,
353
+ logger: log,
354
+ });
355
+ }
356
+ // -------------------------------------------------------------------------
263
357
  // Config watcher (hot reload)
264
358
  // -------------------------------------------------------------------------
265
359
  let watcher = null;
@@ -327,7 +421,11 @@ export async function createLocalServer(config) {
327
421
  // Resolve resume session ID
328
422
  let resumeSessionId = config.resumeSessionId;
329
423
  if (resumeSessionId === 'latest') {
330
- resumeSessionId = legacySessionStore.latest() ?? undefined;
424
+ const { sessions: recent } = await sessionStore.list({
425
+ limit: 1,
426
+ filter: { appId: LOCAL_APP_ID },
427
+ });
428
+ resumeSessionId = recent[0]?.id;
331
429
  }
332
430
  if (resumeSessionId) {
333
431
  log.debug('resume_session', { sessionId: resumeSessionId });
@@ -336,43 +434,48 @@ export async function createLocalServer(config) {
336
434
  app.get('/config', (_req, res) => {
337
435
  res.json({ resumeSessionId: resumeSessionId ?? null });
338
436
  });
339
- // Sessions endpoints (legacy file-based store for UI history)
340
- app.get('/sessions', (req, res) => {
437
+ // Sessions endpoints served directly from the DrizzleSessionStore.
438
+ //
439
+ // Dev-UI consumers (sidebar Recent list, Sessions page, Automation detail
440
+ // page) don't paginate — they render what they get and slice the top N.
441
+ // A 500-session ceiling keeps the response bounded without forcing a
442
+ // cursor API on the client today. If dev sessions regularly exceed this,
443
+ // the store already supports cursor pagination via SessionListOptions.
444
+ const SESSION_LIST_LIMIT = 500;
445
+ app.get('/sessions', asyncHandler(async (req, res) => {
341
446
  const automationFilter = typeof req.query?.['automation'] === 'string' ? String(req.query['automation']) : undefined;
342
- const all = legacySessionStore.list();
343
- const visible = all.filter((s) => s.appId !== 'eval-runner' && s.appId !== 'admin');
344
- const filtered = automationFilter ? visible.filter((s) => s.automationName === automationFilter) : visible;
345
- res.json({ sessions: filtered });
346
- });
347
- app.get('/session/:id', (req, res) => {
348
- const persisted = legacySessionStore.load(req.params['id'] ?? '');
447
+ // Automation filter uses metadata.automationName; otherwise restrict
448
+ // to chat sessions by metadata.appId (excludes eval-runner / admin).
449
+ const filter = automationFilter
450
+ ? { automationName: automationFilter }
451
+ : { appId: LOCAL_APP_ID };
452
+ const { sessions: rows } = await sessionStore.list({ limit: SESSION_LIST_LIMIT, filter });
453
+ const sessions = rows.map((s) => {
454
+ const title = typeof s.metadata['title'] === 'string' ? s.metadata['title'] : undefined;
455
+ const appId = typeof s.metadata['appId'] === 'string' ? s.metadata['appId'] : LOCAL_APP_ID;
456
+ const automationName = typeof s.metadata['automationName'] === 'string' ? s.metadata['automationName'] : undefined;
457
+ return {
458
+ id: s.id,
459
+ appId,
460
+ title,
461
+ summary: title ?? extractFirstUserText(s.messages) ?? 'Untitled',
462
+ createdAt: s.createdAt.getTime(),
463
+ lastAccessedAt: s.updatedAt.getTime(),
464
+ automationName,
465
+ };
466
+ });
467
+ res.json({ sessions });
468
+ }));
469
+ app.get('/session/:id', asyncHandler(async (req, res) => {
470
+ const persisted = await sessionStore.load(req.params['id'] ?? '');
349
471
  if (!persisted) {
350
472
  res.status(404).json({ error: 'Session not found' });
351
473
  return;
352
474
  }
353
- const messages = persisted.conversationHistory.map((msg) => {
354
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Persisted message
355
- const m = msg;
356
- if (m['type'] === 'user') {
357
- return { role: 'user', text: String(m['text'] ?? '') };
358
- }
359
- if (m['type'] === 'assistant_text') {
360
- return { role: 'assistant', text: String(m['text'] ?? ''), toolCalls: m['toolCalls'] };
361
- }
362
- if (m['role'] === 'user') {
363
- return { role: 'user', text: typeof m['content'] === 'string' ? m['content'] : '' };
364
- }
365
- if (m['role'] === 'assistant') {
366
- const blocks = Array.isArray(m['content']) ? m['content'] : [];
367
- const isTextBlock = (b) => typeof b === 'object' && b !== null && 'type' in b && b['type'] === 'text' && 'text' in b;
368
- const text = blocks.filter(isTextBlock).map((b) => b.text).join('');
369
- return { role: 'assistant', text };
370
- }
371
- return { role: String(m['role'] ?? m['type'] ?? 'unknown'), text: String(m['text'] ?? '') };
372
- });
475
+ const messages = persisted.messages.map(flattenModelMessage).filter((m) => m !== null);
373
476
  res.json({ session_id: persisted.id, messages });
374
- });
375
- app.patch('/session/:id', express.json(), (req, res) => {
477
+ }));
478
+ app.patch('/session/:id', express.json(), asyncHandler(async (req, res) => {
376
479
  const sessionId = req.params['id'] ?? '';
377
480
  const body = req.body;
378
481
  if (!body || typeof body !== 'object' || !('title' in body) || typeof body['title'] !== 'string') {
@@ -381,72 +484,67 @@ export async function createLocalServer(config) {
381
484
  }
382
485
  // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- validated above
383
486
  const title = body['title'];
384
- const updated = legacySessionStore.updateTitle(sessionId, title);
385
- if (!updated) {
386
- res.status(404).json({ error: 'Session not found' });
387
- return;
487
+ // Live session: mutate metadata on the shared object and persist so
488
+ // the next /sessions read reflects the new title. A concurrent
489
+ // runMessage may be mid-turn, but JSON.stringify runs atomically in
490
+ // JS's single-threaded event loop — no torn writes. The next
491
+ // end-of-turn persist will overwrite with the completed messages
492
+ // array; metadata.title stays because it's on the live session.
493
+ //
494
+ // Not-live: load → mutate → save. No race possible.
495
+ const live = sessionManager.get(sessionId);
496
+ if (live) {
497
+ live.metadata.title = title;
498
+ await sessionManager.persist(live);
388
499
  }
500
+ else {
501
+ const persisted = await sessionStore.load(sessionId);
502
+ if (!persisted) {
503
+ res.status(404).json({ error: 'Session not found' });
504
+ return;
505
+ }
506
+ persisted.metadata.title = title;
507
+ persisted.updatedAt = new Date();
508
+ await sessionStore.save(persisted);
509
+ }
510
+ // Emit session_updated so the sidebar picks up the new title live.
511
+ eventBus.emit({ type: 'session_updated', sessionId, appId: LOCAL_APP_ID, title });
389
512
  res.json({ ok: true });
390
- });
391
- app.delete('/session/:id', (req, res) => {
513
+ }));
514
+ app.delete('/session/:id', asyncHandler(async (req, res) => {
392
515
  const sessionId = req.params['id'] ?? '';
393
- void sessionManager.destroy(sessionId);
394
- const deleted = legacySessionStore.delete(sessionId);
516
+ await sessionManager.destroy(sessionId);
517
+ const deleted = await sessionStore.delete(sessionId);
395
518
  if (!deleted) {
396
519
  res.status(404).json({ error: 'Session not found' });
397
520
  return;
398
521
  }
522
+ eventBus.emit({ type: 'session_deleted', sessionId });
399
523
  res.json({ ok: true });
400
- });
524
+ }));
401
525
  // File browser/editor
402
526
  app.use(createFilesRouter({ repoPath: config.repoPath }));
527
+ // Event bus SSE stream (live UI updates)
528
+ app.use(createEventsRouter({ bus: eventBus, logger: log }));
403
529
  // Evals
404
530
  const evalStore = new EvalStore(config.repoPath);
405
531
  app.use(createEvalRouter({ getBundle, evalStore, repoPath: config.repoPath, getPort: () => config.port }));
406
532
  // Feedback
407
533
  const feedbackStore = new FeedbackStore(config.repoPath);
408
534
  app.use(createFeedbackRouter({ feedbackStore }));
409
- // Chat routes (new stack)
535
+ // Chat routes (new stack) — persistence is handled inside runMessage /
536
+ // route-helpers, so no explicit hooks are needed here.
410
537
  app.use(createChatStreamRouter({
411
538
  sessionManager,
412
539
  bundleResolver: { staticBundle: bundle },
413
540
  shared,
414
- createStreamHooks: () => ({
415
- onSessionPersist: (sessionId) => {
416
- const session = sessionManager.get(sessionId);
417
- if (session) {
418
- void sessionManager.persist(session);
419
- // Also mirror to the legacy file-based store so the /sessions
420
- // endpoints (read by the UI history panel) see chat sessions.
421
- // Without this, chat sessions only land in PGLite and the UI
422
- // only sees automation/task sessions. PGLite remains the source
423
- // of truth — a mirror failure is logged and swallowed (the hook
424
- // is invoked from fireDrainHooks after the response has drained,
425
- // so throwing here would break the route handler).
426
- try {
427
- legacySessionStore.save({
428
- id: session.id,
429
- appId: session.appId,
430
- title: session.metadata.title,
431
- messages: session.messages,
432
- createdAt: session.createdAt,
433
- lastAccessedAt: session.lastAccessedAt,
434
- });
435
- }
436
- catch (err) {
437
- log.warn('legacy_session_mirror_failed', {
438
- sessionId: session.id,
439
- error: err instanceof Error ? err.message : String(err),
440
- });
441
- }
442
- }
443
- },
444
- }),
541
+ summarizeToolResult: config.summarizeToolResult,
445
542
  }));
446
543
  app.use(createChatRouter({
447
544
  sessionManager,
448
545
  bundleResolver: { staticBundle: bundle },
449
546
  shared,
547
+ summarizeToolResult: config.summarizeToolResult,
450
548
  }));
451
549
  // Task runner
452
550
  app.use(createTaskRouter({ sessionManager, createTaskSession: createAutomationSessionComponents }));
@@ -462,6 +560,11 @@ export async function createLocalServer(config) {
462
560
  // Automations
463
561
  app.use(createAutomationRouter({ runner }));
464
562
  app.use(createWebhookRouter({ runner, webhookSecret: config.webhookSecret }));
563
+ // Messaging channels
564
+ if (channelsResult) {
565
+ app.use('/channels', channelsResult.router);
566
+ log.info('channels_router_mounted', { channels: [...channelsResult.adapters.keys()] });
567
+ }
465
568
  // Store REST API (if stores are defined)
466
569
  if (storeBackend) {
467
570
  app.use(createStoresRouter({ repo: bundle, storeBackend, appId: LOCAL_APP_ID }));
@@ -520,6 +623,8 @@ export async function createLocalServer(config) {
520
623
  // Shared resources and session components will pick up the new
521
624
  // bundle on next session creation via getBundle().
522
625
  log.info('config_reloaded', { name: newBundle.config.name });
626
+ eventBus.emit({ type: 'manifest_changed' });
627
+ eventBus.emit({ type: 'files_changed' });
523
628
  });
524
629
  watcher.start();
525
630
  }
@@ -539,6 +644,7 @@ export async function createLocalServer(config) {
539
644
  }
540
645
  if (server) {
541
646
  const s = server;
647
+ // Stop accepting new connections
542
648
  await new Promise((resolve, reject) => {
543
649
  s.close((err) => {
544
650
  if (err)
@@ -546,6 +652,9 @@ export async function createLocalServer(config) {
546
652
  else
547
653
  resolve();
548
654
  });
655
+ // Force-close existing connections (SSE streams, etc.) so
656
+ // close() doesn't hang waiting for them to drain.
657
+ s.closeAllConnections();
549
658
  });
550
659
  server = null;
551
660
  }
@@ -560,4 +669,82 @@ export async function createLocalServer(config) {
560
669
  },
561
670
  };
562
671
  }
672
+ // ---------------------------------------------------------------------------
673
+ // /sessions + /session/:id response helpers
674
+ // ---------------------------------------------------------------------------
675
+ /** Max length of the first-user-message excerpt shown in session lists. */
676
+ const SUMMARY_EXCERPT_MAX = 80;
677
+ function isRecord(x) {
678
+ return typeof x === 'object' && x !== null;
679
+ }
680
+ function isTextPart(part) {
681
+ return isRecord(part) && part['type'] === 'text' && typeof part['text'] === 'string';
682
+ }
683
+ function isToolCallPart(part) {
684
+ return (isRecord(part) &&
685
+ part['type'] === 'tool-call' &&
686
+ typeof part['toolCallId'] === 'string' &&
687
+ typeof part['toolName'] === 'string');
688
+ }
689
+ function getMessageRole(raw) {
690
+ if (!isRecord(raw))
691
+ return null;
692
+ const role = raw['role'];
693
+ return typeof role === 'string' ? role : null;
694
+ }
695
+ function getMessageContent(raw) {
696
+ if (!isRecord(raw))
697
+ return undefined;
698
+ return raw['content'];
699
+ }
700
+ /** Truncate with an ellipsis when the source exceeds the excerpt budget. */
701
+ function excerpt(s) {
702
+ return s.length > SUMMARY_EXCERPT_MAX ? `${s.slice(0, SUMMARY_EXCERPT_MAX)}…` : s;
703
+ }
704
+ /** Extract the first user-message text from a persisted message array for list summaries. */
705
+ function extractFirstUserText(messages) {
706
+ for (const raw of messages) {
707
+ if (getMessageRole(raw) !== 'user')
708
+ continue;
709
+ const content = getMessageContent(raw);
710
+ if (typeof content === 'string')
711
+ return excerpt(content);
712
+ if (Array.isArray(content)) {
713
+ const firstText = content.find(isTextPart);
714
+ if (firstText)
715
+ return excerpt(firstText.text);
716
+ }
717
+ }
718
+ return undefined;
719
+ }
720
+ /**
721
+ * Flatten a persisted `ModelMessage` (ai SDK v6) into the shape the web UI's
722
+ * /session/:id consumer expects: {role, text, toolCalls?}. Returns null for
723
+ * tool-result messages and for assistant turns with no renderable content
724
+ * (the history panel shows conversation + tool-call chips, not raw tool
725
+ * plumbing).
726
+ */
727
+ function flattenModelMessage(raw) {
728
+ const role = getMessageRole(raw);
729
+ if (role !== 'user' && role !== 'assistant')
730
+ return null;
731
+ const content = getMessageContent(raw);
732
+ if (typeof content === 'string') {
733
+ return { role, text: content };
734
+ }
735
+ if (Array.isArray(content)) {
736
+ const text = content.filter(isTextPart).map((p) => p.text).join('');
737
+ const toolCalls = role === 'assistant'
738
+ ? content.filter(isToolCallPart).map((p) => ({
739
+ toolId: p.toolCallId,
740
+ toolName: p.toolName,
741
+ parameters: isRecord(p.input) ? p.input : {},
742
+ }))
743
+ : [];
744
+ if (text.length === 0 && toolCalls.length === 0)
745
+ return null;
746
+ return toolCalls.length > 0 ? { role, text, toolCalls } : { role, text };
747
+ }
748
+ return null;
749
+ }
563
750
  //# sourceMappingURL=local-server.js.map