@data-club/ai-hub-server 0.0.1

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 (248) hide show
  1. package/dist/auth/argon2.d.ts +72 -0
  2. package/dist/auth/argon2.d.ts.map +1 -0
  3. package/dist/auth/argon2.js +145 -0
  4. package/dist/auth/argon2.js.map +1 -0
  5. package/dist/auth/encryption.d.ts +52 -0
  6. package/dist/auth/encryption.d.ts.map +1 -0
  7. package/dist/auth/encryption.js +98 -0
  8. package/dist/auth/encryption.js.map +1 -0
  9. package/dist/auth/jwt.d.ts +133 -0
  10. package/dist/auth/jwt.d.ts.map +1 -0
  11. package/dist/auth/jwt.js +147 -0
  12. package/dist/auth/jwt.js.map +1 -0
  13. package/dist/auth/middleware.d.ts +101 -0
  14. package/dist/auth/middleware.d.ts.map +1 -0
  15. package/dist/auth/middleware.js +161 -0
  16. package/dist/auth/middleware.js.map +1 -0
  17. package/dist/auth/passwordPolicy.d.ts +72 -0
  18. package/dist/auth/passwordPolicy.d.ts.map +1 -0
  19. package/dist/auth/passwordPolicy.js +88 -0
  20. package/dist/auth/passwordPolicy.js.map +1 -0
  21. package/dist/auth/roleRules.d.ts +200 -0
  22. package/dist/auth/roleRules.d.ts.map +1 -0
  23. package/dist/auth/roleRules.js +365 -0
  24. package/dist/auth/roleRules.js.map +1 -0
  25. package/dist/auth/userView.d.ts +55 -0
  26. package/dist/auth/userView.d.ts.map +1 -0
  27. package/dist/auth/userView.js +77 -0
  28. package/dist/auth/userView.js.map +1 -0
  29. package/dist/bootstrap.d.ts +104 -0
  30. package/dist/bootstrap.d.ts.map +1 -0
  31. package/dist/bootstrap.js +160 -0
  32. package/dist/bootstrap.js.map +1 -0
  33. package/dist/db/connection.d.ts +39 -0
  34. package/dist/db/connection.d.ts.map +1 -0
  35. package/dist/db/connection.js +62 -0
  36. package/dist/db/connection.js.map +1 -0
  37. package/dist/db/migrate.d.ts +31 -0
  38. package/dist/db/migrate.d.ts.map +1 -0
  39. package/dist/db/migrate.js +197 -0
  40. package/dist/db/migrate.js.map +1 -0
  41. package/dist/db/schema.d.ts +1902 -0
  42. package/dist/db/schema.d.ts.map +1 -0
  43. package/dist/db/schema.js +662 -0
  44. package/dist/db/schema.js.map +1 -0
  45. package/dist/db/seed.d.ts +92 -0
  46. package/dist/db/seed.d.ts.map +1 -0
  47. package/dist/db/seed.js +160 -0
  48. package/dist/db/seed.js.map +1 -0
  49. package/dist/handlers/fetch.d.ts +89 -0
  50. package/dist/handlers/fetch.d.ts.map +1 -0
  51. package/dist/handlers/fetch.js +173 -0
  52. package/dist/handlers/fetch.js.map +1 -0
  53. package/dist/handlers/nextjs.d.ts +40 -0
  54. package/dist/handlers/nextjs.d.ts.map +1 -0
  55. package/dist/handlers/nextjs.js +42 -0
  56. package/dist/handlers/nextjs.js.map +1 -0
  57. package/dist/handlers/responses.d.ts +46 -0
  58. package/dist/handlers/responses.d.ts.map +1 -0
  59. package/dist/handlers/responses.js +74 -0
  60. package/dist/handlers/responses.js.map +1 -0
  61. package/dist/handlers/routeTable.d.ts +21 -0
  62. package/dist/handlers/routeTable.d.ts.map +1 -0
  63. package/dist/handlers/routeTable.js +133 -0
  64. package/dist/handlers/routeTable.js.map +1 -0
  65. package/dist/handlers/router.d.ts +37 -0
  66. package/dist/handlers/router.d.ts.map +1 -0
  67. package/dist/handlers/router.js +82 -0
  68. package/dist/handlers/router.js.map +1 -0
  69. package/dist/handlers/sse.d.ts +38 -0
  70. package/dist/handlers/sse.d.ts.map +1 -0
  71. package/dist/handlers/sse.js +52 -0
  72. package/dist/handlers/sse.js.map +1 -0
  73. package/dist/handlers/types.d.ts +96 -0
  74. package/dist/handlers/types.d.ts.map +1 -0
  75. package/dist/handlers/types.js +14 -0
  76. package/dist/handlers/types.js.map +1 -0
  77. package/dist/index.d.ts +25 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +24 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/letta/client.d.ts +355 -0
  82. package/dist/letta/client.d.ts.map +1 -0
  83. package/dist/letta/client.js +529 -0
  84. package/dist/letta/client.js.map +1 -0
  85. package/dist/letta/eventTranslator.d.ts +166 -0
  86. package/dist/letta/eventTranslator.d.ts.map +1 -0
  87. package/dist/letta/eventTranslator.js +532 -0
  88. package/dist/letta/eventTranslator.js.map +1 -0
  89. package/dist/letta/index.d.ts +13 -0
  90. package/dist/letta/index.d.ts.map +1 -0
  91. package/dist/letta/index.js +12 -0
  92. package/dist/letta/index.js.map +1 -0
  93. package/dist/letta/modelSettings.d.ts +176 -0
  94. package/dist/letta/modelSettings.d.ts.map +1 -0
  95. package/dist/letta/modelSettings.js +314 -0
  96. package/dist/letta/modelSettings.js.map +1 -0
  97. package/dist/letta/promptWrapping.d.ts +141 -0
  98. package/dist/letta/promptWrapping.d.ts.map +1 -0
  99. package/dist/letta/promptWrapping.js +213 -0
  100. package/dist/letta/promptWrapping.js.map +1 -0
  101. package/dist/letta/sandbox.d.ts +100 -0
  102. package/dist/letta/sandbox.d.ts.map +1 -0
  103. package/dist/letta/sandbox.js +151 -0
  104. package/dist/letta/sandbox.js.map +1 -0
  105. package/dist/letta/settingsFileSeed.d.ts +146 -0
  106. package/dist/letta/settingsFileSeed.d.ts.map +1 -0
  107. package/dist/letta/settingsFileSeed.js +177 -0
  108. package/dist/letta/settingsFileSeed.js.map +1 -0
  109. package/dist/letta/subprocessThrottle.d.ts +151 -0
  110. package/dist/letta/subprocessThrottle.d.ts.map +1 -0
  111. package/dist/letta/subprocessThrottle.js +195 -0
  112. package/dist/letta/subprocessThrottle.js.map +1 -0
  113. package/dist/letta/tpmRecovery.d.ts +202 -0
  114. package/dist/letta/tpmRecovery.d.ts.map +1 -0
  115. package/dist/letta/tpmRecovery.js +314 -0
  116. package/dist/letta/tpmRecovery.js.map +1 -0
  117. package/dist/letta/types.d.ts +54 -0
  118. package/dist/letta/types.d.ts.map +1 -0
  119. package/dist/letta/types.js +16 -0
  120. package/dist/letta/types.js.map +1 -0
  121. package/dist/observability/logger.d.ts +48 -0
  122. package/dist/observability/logger.d.ts.map +1 -0
  123. package/dist/observability/logger.js +81 -0
  124. package/dist/observability/logger.js.map +1 -0
  125. package/dist/observability/metrics.d.ts +53 -0
  126. package/dist/observability/metrics.d.ts.map +1 -0
  127. package/dist/observability/metrics.js +92 -0
  128. package/dist/observability/metrics.js.map +1 -0
  129. package/dist/routes/admin/agents.d.ts +63 -0
  130. package/dist/routes/admin/agents.d.ts.map +1 -0
  131. package/dist/routes/admin/agents.js +461 -0
  132. package/dist/routes/admin/agents.js.map +1 -0
  133. package/dist/routes/admin/rotatingMessages.d.ts +51 -0
  134. package/dist/routes/admin/rotatingMessages.d.ts.map +1 -0
  135. package/dist/routes/admin/rotatingMessages.js +292 -0
  136. package/dist/routes/admin/rotatingMessages.js.map +1 -0
  137. package/dist/routes/admin/sessions.d.ts +61 -0
  138. package/dist/routes/admin/sessions.d.ts.map +1 -0
  139. package/dist/routes/admin/sessions.js +232 -0
  140. package/dist/routes/admin/sessions.js.map +1 -0
  141. package/dist/routes/admin/settings.d.ts +110 -0
  142. package/dist/routes/admin/settings.d.ts.map +1 -0
  143. package/dist/routes/admin/settings.js +401 -0
  144. package/dist/routes/admin/settings.js.map +1 -0
  145. package/dist/routes/admin/users.d.ts +115 -0
  146. package/dist/routes/admin/users.d.ts.map +1 -0
  147. package/dist/routes/admin/users.js +374 -0
  148. package/dist/routes/admin/users.js.map +1 -0
  149. package/dist/routes/agents.d.ts +30 -0
  150. package/dist/routes/agents.d.ts.map +1 -0
  151. package/dist/routes/agents.js +55 -0
  152. package/dist/routes/agents.js.map +1 -0
  153. package/dist/routes/auth.d.ts +121 -0
  154. package/dist/routes/auth.d.ts.map +1 -0
  155. package/dist/routes/auth.js +422 -0
  156. package/dist/routes/auth.js.map +1 -0
  157. package/dist/routes/chat.d.ts +97 -0
  158. package/dist/routes/chat.d.ts.map +1 -0
  159. package/dist/routes/chat.js +484 -0
  160. package/dist/routes/chat.js.map +1 -0
  161. package/dist/routes/featureToggles.d.ts +34 -0
  162. package/dist/routes/featureToggles.d.ts.map +1 -0
  163. package/dist/routes/featureToggles.js +47 -0
  164. package/dist/routes/featureToggles.js.map +1 -0
  165. package/dist/routes/health.d.ts +24 -0
  166. package/dist/routes/health.d.ts.map +1 -0
  167. package/dist/routes/health.js +37 -0
  168. package/dist/routes/health.js.map +1 -0
  169. package/dist/routes/metrics.d.ts +35 -0
  170. package/dist/routes/metrics.d.ts.map +1 -0
  171. package/dist/routes/metrics.js +106 -0
  172. package/dist/routes/metrics.js.map +1 -0
  173. package/dist/routes/sessions.d.ts +161 -0
  174. package/dist/routes/sessions.d.ts.map +1 -0
  175. package/dist/routes/sessions.js +1501 -0
  176. package/dist/routes/sessions.js.map +1 -0
  177. package/dist/routes/stubs.d.ts +25 -0
  178. package/dist/routes/stubs.d.ts.map +1 -0
  179. package/dist/routes/stubs.js +25 -0
  180. package/dist/routes/stubs.js.map +1 -0
  181. package/dist/state/cron.d.ts +58 -0
  182. package/dist/state/cron.d.ts.map +1 -0
  183. package/dist/state/cron.js +160 -0
  184. package/dist/state/cron.js.map +1 -0
  185. package/dist/state/rotationRunner.d.ts +96 -0
  186. package/dist/state/rotationRunner.d.ts.map +1 -0
  187. package/dist/state/rotationRunner.js +246 -0
  188. package/dist/state/rotationRunner.js.map +1 -0
  189. package/dist/state/rotationScheduler.d.ts +76 -0
  190. package/dist/state/rotationScheduler.d.ts.map +1 -0
  191. package/dist/state/rotationScheduler.js +124 -0
  192. package/dist/state/rotationScheduler.js.map +1 -0
  193. package/dist/state/turnRegistry.d.ts +95 -0
  194. package/dist/state/turnRegistry.d.ts.map +1 -0
  195. package/dist/state/turnRegistry.js +149 -0
  196. package/dist/state/turnRegistry.js.map +1 -0
  197. package/dist/state/turnRunner.d.ts +134 -0
  198. package/dist/state/turnRunner.d.ts.map +1 -0
  199. package/dist/state/turnRunner.js +476 -0
  200. package/dist/state/turnRunner.js.map +1 -0
  201. package/package.json +47 -0
  202. package/src/auth/argon2.ts +152 -0
  203. package/src/auth/encryption.ts +110 -0
  204. package/src/auth/jwt.ts +217 -0
  205. package/src/auth/middleware.ts +250 -0
  206. package/src/auth/passwordPolicy.ts +99 -0
  207. package/src/auth/roleRules.ts +497 -0
  208. package/src/auth/userView.ts +88 -0
  209. package/src/bootstrap.ts +254 -0
  210. package/src/db/connection.ts +75 -0
  211. package/src/db/migrate.ts +212 -0
  212. package/src/db/schema.ts +750 -0
  213. package/src/db/seed.ts +236 -0
  214. package/src/handlers/fetch.ts +251 -0
  215. package/src/handlers/nextjs.ts +49 -0
  216. package/src/handlers/responses.ts +95 -0
  217. package/src/handlers/routeTable.ts +188 -0
  218. package/src/handlers/router.ts +96 -0
  219. package/src/handlers/sse.ts +55 -0
  220. package/src/handlers/types.ts +104 -0
  221. package/src/index.ts +60 -0
  222. package/src/letta/client.ts +860 -0
  223. package/src/letta/eventTranslator.ts +667 -0
  224. package/src/letta/index.ts +46 -0
  225. package/src/letta/lettaCliShim.mjs +40 -0
  226. package/src/letta/sandbox.ts +167 -0
  227. package/src/letta/settingsFileSeed.ts +254 -0
  228. package/src/letta/subprocessThrottle.ts +251 -0
  229. package/src/letta/types.ts +65 -0
  230. package/src/observability/logger.ts +103 -0
  231. package/src/observability/metrics.ts +114 -0
  232. package/src/routes/admin/agents.ts +574 -0
  233. package/src/routes/admin/rotatingMessages.ts +351 -0
  234. package/src/routes/admin/sessions.ts +283 -0
  235. package/src/routes/admin/settings.ts +463 -0
  236. package/src/routes/admin/users.ts +455 -0
  237. package/src/routes/agents.ts +66 -0
  238. package/src/routes/auth.ts +513 -0
  239. package/src/routes/chat.ts +534 -0
  240. package/src/routes/featureToggles.ts +52 -0
  241. package/src/routes/health.ts +44 -0
  242. package/src/routes/metrics.ts +111 -0
  243. package/src/routes/sessions.ts +1878 -0
  244. package/src/state/cron.ts +179 -0
  245. package/src/state/rotationRunner.ts +326 -0
  246. package/src/state/rotationScheduler.ts +164 -0
  247. package/src/state/turnRegistry.ts +210 -0
  248. package/src/state/turnRunner.ts +588 -0
@@ -0,0 +1,484 @@
1
+ /**
2
+ * `/chat/*` — SSE chat, resumption, cancel (§6.3.2).
3
+ *
4
+ * Three endpoints:
5
+ *
6
+ * - `POST /chat` — body `{ session_id, user_message, effort?, idempotency_key? }`.
7
+ * Streams events per §9. Owner-scoped: the caller must own the
8
+ * referenced session.
9
+ * - `GET /chat/:turn_id/events?from_seq=N` — resumption: replays
10
+ * persisted events with `seq > N`, then live-attaches if the turn is
11
+ * still active.
12
+ * - `POST /chat/:turn_id/cancel` — aborts an in-flight turn. Idempotent
13
+ * when the turn already terminated.
14
+ *
15
+ * The runner (`state/turnRunner.ts`) owns the actual state machine; this
16
+ * file is the HTTP/SSE shell. The runner publishes events to a live
17
+ * channel + persists them; the `POST /chat` writer subscribes to the
18
+ * same channel and ships bytes to the client. The runner's `onEvent`
19
+ * callback can write directly; this file uses
20
+ * `subscribeToTurn` because it shares the same plumbing the resumption
21
+ * endpoint uses.
22
+ *
23
+ * @see SPECIFICATIONS.md §6.3.2 — chat (SSE)
24
+ * @see SPECIFICATIONS.md §6.6 — server-authoritative state
25
+ * @see SPECIFICATIONS.md §6.9 — cancellation
26
+ */
27
+ import { eq } from "drizzle-orm";
28
+ import { z } from "zod";
29
+ import { protectedRoute } from "../auth/middleware.js";
30
+ import { messageOverride, session, turn, turnEvent } from "../db/schema.js";
31
+ import { formatSseEvent, formatSseHeartbeat } from "../handlers/sse.js";
32
+ import { errorResponse, jsonResponse, notFound, validationError, } from "../handlers/responses.js";
33
+ import { markCancelled, replayPersistedEvents, runTurn, } from "../state/turnRunner.js";
34
+ import { isTurnLive, requestTurnCancellation, subscribeToTurn, } from "../state/turnRegistry.js";
35
+ /* ────────────────────────────────────────────────────────────────────────── */
36
+ /* POST /chat */
37
+ /* ────────────────────────────────────────────────────────────────────────── */
38
+ /** Per-message wire body. `effort` is optional with the §7.4 enum. */
39
+ const postChatSchema = z
40
+ .object({
41
+ session_id: z.string().uuid(),
42
+ user_message: z.string().min(1),
43
+ effort: z
44
+ .enum(["none", "minimal", "low", "medium", "high", "extra_high"])
45
+ .optional(),
46
+ idempotency_key: z.string().min(1).max(255).optional(),
47
+ /**
48
+ * Optional tier-3 customization: the human-facing replacement for
49
+ * `user_message` in the chat UI. When set, AI hub sends
50
+ * `user_message` to Letta verbatim (system view) but, after the turn
51
+ * terminates `done`, writes an override row at the position the
52
+ * persisted user message landed at so subsequent
53
+ * `GET /sessions/:id/messages` calls display this string instead.
54
+ *
55
+ * Persisted only on `done`. On `cancelled` / `failed`, Letta drops
56
+ * the user message and we drop the override — no orphan row.
57
+ */
58
+ user_override: z.string().optional(),
59
+ })
60
+ .strict();
61
+ /**
62
+ * `POST /chat` — opens an SSE stream for the new turn.
63
+ *
64
+ * Owner-scoped: returns 404 for sessions not owned by the caller. The
65
+ * `power_user`+ "all-sessions overview" is a different surface.
66
+ *
67
+ * The handler returns a `Response` whose body is a `ReadableStream`. The
68
+ * runner runs in the background, posts events to the registry's live
69
+ * channel, and the stream subscriber writes them out as `event:`/`data:`
70
+ * pairs. The runner returns after the stream's terminal event is
71
+ * persisted; the writer closes the stream shortly after.
72
+ *
73
+ * @see SPECIFICATIONS.md §6.3.2
74
+ * @see SPECIFICATIONS.md §9 — wire protocol
75
+ */
76
+ export const handlePostChat = protectedRoute({}, async (req, ctx, auth) => {
77
+ let raw;
78
+ try {
79
+ raw = await req.json();
80
+ }
81
+ catch {
82
+ return validationError("Request body must be valid JSON.");
83
+ }
84
+ const parsed = postChatSchema.safeParse(raw);
85
+ if (!parsed.success) {
86
+ const issue = parsed.error.issues[0];
87
+ return validationError(issue?.message ?? "Invalid request body.");
88
+ }
89
+ const body = parsed.data;
90
+ // Owner-scope check.
91
+ const row = ctx.db
92
+ .select()
93
+ .from(session)
94
+ .where(eq(session.id, body.session_id))
95
+ .get();
96
+ if (!row || row.userId !== auth.user.id)
97
+ return notFound();
98
+ if (row.isArchived) {
99
+ return errorResponse(409, "session_archived", "Cannot post to an archived session — unarchive it first.");
100
+ }
101
+ // Honor client disconnects: when the request's signal aborts, the
102
+ // runner's signal aborts too, which closes the upstream Letta call.
103
+ const requestSignal = req.signal;
104
+ // We need to know the turn id for the response headers. The runner
105
+ // assigns it at row-insert time; capture it from the `stream_start`
106
+ // event the runner emits first.
107
+ const stream = new ReadableStream({
108
+ async start(controller) {
109
+ let started = false;
110
+ const writeEvent = (event) => {
111
+ try {
112
+ controller.enqueue(formatSseEvent(event));
113
+ started = true;
114
+ }
115
+ catch {
116
+ /* writer was closed (client disconnected) — ignore */
117
+ }
118
+ };
119
+ // Periodic heartbeat (§9.1) — every 10 s. Keeps proxies from
120
+ // closing the connection during long idle phases (tool exec,
121
+ // 429 wait).
122
+ const heartbeat = setInterval(() => {
123
+ try {
124
+ controller.enqueue(formatSseHeartbeat());
125
+ }
126
+ catch {
127
+ /* writer was closed — clear and bail */
128
+ clearInterval(heartbeat);
129
+ }
130
+ }, 10_000);
131
+ const runnerOpts = {
132
+ db: ctx.db,
133
+ lettaClient: ctx.lettaClient,
134
+ logger: ctx.logger,
135
+ requestId: ctx.logger
136
+ .bindings?.()?.["request_id"] ?? "",
137
+ userId: auth.user.id,
138
+ sessionId: row.id,
139
+ lettaConversationId: row.lettaConversationId,
140
+ lettaAgentId: row.lettaAgentId,
141
+ sandboxDir: row.sandboxDir,
142
+ userMessage: body.user_message,
143
+ ...(body.effort !== undefined ? { effort: body.effort } : {}),
144
+ ...(body.idempotency_key !== undefined ? { idempotencyKey: body.idempotency_key } : {}),
145
+ onEvent: (event) => {
146
+ if (event === null) {
147
+ clearInterval(heartbeat);
148
+ try {
149
+ controller.close();
150
+ }
151
+ catch {
152
+ /* already closed */
153
+ }
154
+ return;
155
+ }
156
+ writeEvent(event);
157
+ },
158
+ signal: requestSignal,
159
+ };
160
+ // Run the turn. We don't await here — the runner pushes events
161
+ // via `onEvent`; we keep the stream open until it completes.
162
+ // But we DO need to catch errors so the stream doesn't dangle if
163
+ // the runner throws before emitting `stream_end`.
164
+ //
165
+ // The `.then` chain handles the tier-3 `user_override` parameter:
166
+ // after the turn terminates `done`, we list Letta's messages to
167
+ // find the position the persisted user message landed at, then
168
+ // upsert an override row so `GET /sessions/:id/messages` will
169
+ // display `user_override` in place of the wrapped Letta content.
170
+ // We deliberately don't write on `cancelled` / `failed` — in
171
+ // those cases Letta drops the user message and a persisted
172
+ // override would point at nothing.
173
+ runTurn(runnerOpts)
174
+ .then(async (result) => {
175
+ if (result.status !== "done")
176
+ return;
177
+ if (body.user_override === undefined)
178
+ return;
179
+ try {
180
+ const messages = await ctx.lettaClient.listMessages(row.lettaConversationId);
181
+ // Position = chronologically-sorted index of the latest
182
+ // user_message authored by this caller. Letta is the
183
+ // source of truth — we don't trust message ordering on
184
+ // any other axis.
185
+ const sorted = [...messages].sort((a, b) => {
186
+ const aT = Date.parse(a.date ?? "");
187
+ const bT = Date.parse(b.date ?? "");
188
+ if (Number.isNaN(aT) && Number.isNaN(bT))
189
+ return 0;
190
+ if (Number.isNaN(aT))
191
+ return 1;
192
+ if (Number.isNaN(bT))
193
+ return -1;
194
+ return aT - bT;
195
+ });
196
+ let position = -1;
197
+ for (let i = sorted.length - 1; i >= 0; i--) {
198
+ const msg = sorted[i];
199
+ if (msg.message_type === "user_message" && msg.sender_id === auth.user.id) {
200
+ position = i;
201
+ break;
202
+ }
203
+ }
204
+ if (position < 0) {
205
+ ctx.logger.warn({
206
+ event: "user_override_post_turn_no_user_message",
207
+ session_id: row.id,
208
+ }, "Turn ended `done` but no matching user_message was found in Letta — skipping override write.");
209
+ return;
210
+ }
211
+ const now = new Date().toISOString();
212
+ ctx.db
213
+ .insert(messageOverride)
214
+ .values({
215
+ conversationId: row.lettaConversationId,
216
+ ordinalPosition: position,
217
+ systemOverride: null,
218
+ userOverride: body.user_override,
219
+ createdAt: now,
220
+ updatedAt: now,
221
+ })
222
+ .onConflictDoUpdate({
223
+ target: [messageOverride.conversationId, messageOverride.ordinalPosition],
224
+ // Merge: only overwrite the user_override layer. Preserve
225
+ // any system_override an earlier PUT might have written.
226
+ set: { userOverride: body.user_override, updatedAt: now },
227
+ })
228
+ .run();
229
+ ctx.logger.info({
230
+ event: "user_override_post_turn_written",
231
+ session_id: row.id,
232
+ ordinal_position: position,
233
+ }, "Wrote user_override after successful turn.");
234
+ }
235
+ catch (err) {
236
+ // Non-fatal: the chat already succeeded. Log and move on.
237
+ ctx.logger.error({
238
+ event: "user_override_post_turn_failed",
239
+ session_id: row.id,
240
+ err: err instanceof Error ? err.message : String(err),
241
+ }, "Failed to write user_override post-turn.");
242
+ }
243
+ })
244
+ .catch((err) => {
245
+ ctx.logger.error({ event: "turn_runner_threw", err: err instanceof Error ? err.message : String(err) }, "Turn runner threw outside the normal lifecycle.");
246
+ clearInterval(heartbeat);
247
+ if (!started) {
248
+ // No stream_start emitted — close abruptly. The client will
249
+ // see a connection-error and may retry.
250
+ try {
251
+ controller.close();
252
+ }
253
+ catch {
254
+ /* ignored */
255
+ }
256
+ }
257
+ });
258
+ },
259
+ });
260
+ return new Response(stream, {
261
+ status: 200,
262
+ headers: {
263
+ "Content-Type": "text/event-stream; charset=utf-8",
264
+ "Cache-Control": "no-cache, no-transform",
265
+ "Connection": "keep-alive",
266
+ "X-Accel-Buffering": "no",
267
+ },
268
+ });
269
+ });
270
+ /* ────────────────────────────────────────────────────────────────────────── */
271
+ /* GET /chat/:turn_id/events?from_seq=N */
272
+ /* ────────────────────────────────────────────────────────────────────────── */
273
+ /**
274
+ * `GET /chat/:turn_id/events?from_seq=N` — resumption.
275
+ *
276
+ * Two-phase:
277
+ *
278
+ * 1. Replay persisted `turn_event` rows with `seq > N`, in order.
279
+ * 2. If the turn is still live, attach to the in-memory broadcaster so
280
+ * new events stream through. If the turn is terminal, the response
281
+ * closes after the replay.
282
+ *
283
+ * Owner-scoped: the caller must own the parent session (the admin
284
+ * "sessions overview" doesn't go through this endpoint — that's a
285
+ * separate JSON read).
286
+ *
287
+ * @see SPECIFICATIONS.md §6.3.2 — resumption
288
+ */
289
+ export const handleGetChatEvents = protectedRoute({}, async (req, ctx, auth, params) => {
290
+ const turnId = params.turn_id;
291
+ if (!turnId)
292
+ return notFound();
293
+ const turnRow = ctx.db.select().from(turn).where(eq(turn.id, turnId)).get();
294
+ if (!turnRow)
295
+ return notFound();
296
+ const sessionRow = ctx.db
297
+ .select()
298
+ .from(session)
299
+ .where(eq(session.id, turnRow.sessionId))
300
+ .get();
301
+ if (!sessionRow || sessionRow.userId !== auth.user.id)
302
+ return notFound();
303
+ const url = new URL(req.url);
304
+ const fromSeqRaw = url.searchParams.get("from_seq");
305
+ const fromSeq = fromSeqRaw === null ? -1 : Number.parseInt(fromSeqRaw, 10);
306
+ if (Number.isNaN(fromSeq)) {
307
+ return validationError("from_seq must be an integer.", "invalid_from_seq");
308
+ }
309
+ const requestSignal = req.signal;
310
+ const turnIsLive = isTurnLive(turnId);
311
+ const stream = new ReadableStream({
312
+ start(controller) {
313
+ let closed = false;
314
+ const safeWrite = (event) => {
315
+ if (closed)
316
+ return;
317
+ try {
318
+ controller.enqueue(formatSseEvent(event));
319
+ }
320
+ catch {
321
+ closed = true;
322
+ }
323
+ };
324
+ const safeClose = () => {
325
+ if (closed)
326
+ return;
327
+ closed = true;
328
+ try {
329
+ controller.close();
330
+ }
331
+ catch {
332
+ /* ignored */
333
+ }
334
+ };
335
+ // Phase 1 — replay every persisted event with seq > fromSeq.
336
+ const replayed = replayPersistedEvents(ctx.db, turnId, fromSeq);
337
+ let highestSeqWritten = fromSeq;
338
+ for (const event of replayed) {
339
+ safeWrite(event);
340
+ if (event.seq > highestSeqWritten)
341
+ highestSeqWritten = event.seq;
342
+ }
343
+ // Phase 2 — live-attach (only if the turn is still running).
344
+ if (!turnIsLive) {
345
+ safeClose();
346
+ return;
347
+ }
348
+ const unsubscribe = subscribeToTurn(turnId, (event) => {
349
+ // De-duplicate against the replay window. Events with seq <=
350
+ // highestSeqWritten were either already replayed or are stale
351
+ // re-broadcasts; the runner never re-emits, but we guard
352
+ // defensively.
353
+ if (event.seq <= highestSeqWritten)
354
+ return;
355
+ highestSeqWritten = event.seq;
356
+ safeWrite(event);
357
+ }, () => {
358
+ safeClose();
359
+ });
360
+ if (!unsubscribe) {
361
+ // Turn went terminal between our `isTurnLive` check and
362
+ // subscription. Re-read any events that arrived in the gap
363
+ // and close.
364
+ const tail = replayPersistedEvents(ctx.db, turnId, highestSeqWritten);
365
+ for (const event of tail)
366
+ safeWrite(event);
367
+ safeClose();
368
+ return;
369
+ }
370
+ const onAbort = () => {
371
+ unsubscribe();
372
+ safeClose();
373
+ };
374
+ if (requestSignal.aborted)
375
+ onAbort();
376
+ else
377
+ requestSignal.addEventListener("abort", onAbort, { once: true });
378
+ },
379
+ });
380
+ return new Response(stream, {
381
+ status: 200,
382
+ headers: {
383
+ "Content-Type": "text/event-stream; charset=utf-8",
384
+ "Cache-Control": "no-cache, no-transform",
385
+ "Connection": "keep-alive",
386
+ "X-Accel-Buffering": "no",
387
+ },
388
+ });
389
+ });
390
+ /* ────────────────────────────────────────────────────────────────────────── */
391
+ /* POST /chat/:turn_id/cancel */
392
+ /* ────────────────────────────────────────────────────────────────────────── */
393
+ /**
394
+ * `POST /chat/:turn_id/cancel` — aborts an in-flight turn authoritatively.
395
+ *
396
+ * Owner-scoped via the parent session. Idempotent — calling it on a
397
+ * terminal turn returns the same envelope as the first call (no error,
398
+ * the existing status is reported as `already_terminal`).
399
+ *
400
+ * Two cancellation paths fire here, in order:
401
+ *
402
+ * 1. **Letta-side cancel** — `client.agents.messages.cancel({ run_ids })`.
403
+ * This is the load-bearing one: it tells Letta to stop the run
404
+ * server-side and roll back the in-progress step from the
405
+ * conversation history (completed steps survive). **Without this,
406
+ * aborting only our SDK call's HTTP connection does nothing** —
407
+ * Letta keeps generating, persists the full output, and the user's
408
+ * refresh reveals it. Empirically verified.
409
+ *
410
+ * 2. **Local registry cancel** — `requestTurnCancellation(turnId)`. Fires
411
+ * our runner's AbortSignal so the in-process for-await loop exits
412
+ * cleanly + emits `stream_end{reason:"cancelled"}`. Also serves as
413
+ * the fallback when (1) fails (network blip, Letta-side error, run
414
+ * already terminal): the client still gets a clean terminal event.
415
+ *
416
+ * Race-free ordering: we fire (1) first so Letta has begun winding the
417
+ * run down by the time (2) fires; the few final events Letta delivers
418
+ * during that window flow through the runner's stream naturally before
419
+ * the abort lands.
420
+ *
421
+ * @see SPECIFICATIONS.md §6.3.2
422
+ * @see SPECIFICATIONS.md §6.9 — cancellation
423
+ */
424
+ export const handlePostChatCancel = protectedRoute({}, async (_req, ctx, auth, params) => {
425
+ const turnId = params.turn_id;
426
+ if (!turnId)
427
+ return notFound();
428
+ const turnRow = ctx.db.select().from(turn).where(eq(turn.id, turnId)).get();
429
+ if (!turnRow)
430
+ return notFound();
431
+ const sessionRow = ctx.db
432
+ .select()
433
+ .from(session)
434
+ .where(eq(session.id, turnRow.sessionId))
435
+ .get();
436
+ if (!sessionRow || sessionRow.userId !== auth.user.id)
437
+ return notFound();
438
+ if (turnRow.status !== "running") {
439
+ return jsonResponse(200, {
440
+ turn_id: turnId,
441
+ status: turnRow.status,
442
+ was_already_terminal: true,
443
+ });
444
+ }
445
+ // Cancel via the runner's `ChatTurnHandle.abort()` —
446
+ // `requestTurnCancellation` fires the registry's AbortController,
447
+ // the runner listens for it and translates into a Letta-Code-SDK
448
+ // `session.abort()` call (which sends `interrupt` to the spawned
449
+ // CLI subprocess). Pre-migration this also called Letta server's
450
+ // `agents.messages.cancel({run_ids})` directly via a captured
451
+ // `lettaRunId`; the SDK abstracts that now and we no longer
452
+ // thread run-ids through the runner. See migration §4.2.
453
+ //
454
+ // Behavioral caveat (migration §7 open question 5): if the spawned
455
+ // CLI is mid-tool-execution, the interrupt may be queued, not
456
+ // honored, and the tool runs to completion. The runner emits
457
+ // `stream_end{cancelled}` regardless once the upstream terminal
458
+ // result lands, so the React client gets a clean cancel event
459
+ // even when the run effectively completed.
460
+ const aborted = requestTurnCancellation(turnId);
461
+ let finalStatus = turnRow.status;
462
+ if (!aborted) {
463
+ const fresh = ctx.db.select().from(turn).where(eq(turn.id, turnId)).get();
464
+ finalStatus = fresh?.status ?? "cancelled";
465
+ }
466
+ else {
467
+ finalStatus = "cancelled";
468
+ markCancelled(ctx.db, turnId);
469
+ }
470
+ return jsonResponse(200, {
471
+ turn_id: turnId,
472
+ status: finalStatus,
473
+ was_already_terminal: false,
474
+ });
475
+ });
476
+ /* ────────────────────────────────────────────────────────────────────────── */
477
+ /* Re-exports */
478
+ /* ────────────────────────────────────────────────────────────────────────── */
479
+ // Re-exported here for the tests that exercise the persisted-event log
480
+ // directly without going through HTTP.
481
+ export { replayPersistedEvents };
482
+ // Used by integration tests that need to read turn_event rows directly.
483
+ export { turnEvent };
484
+ //# sourceMappingURL=chat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat.js","sourceRoot":"","sources":["../../src/routes/chat.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `GET /feature-toggles` — public read of the deployment-wide UI feature
3
+ * toggles set by `super_admin` (§8.4).
4
+ *
5
+ * Unlike `/admin/settings/feature-toggles`, this endpoint is **unauthenticated**
6
+ * — every page-load can fetch it before login completes, and every role can
7
+ * read it after login. The blob itself is non-sensitive: it controls which
8
+ * UI affordances render in the chat shell, not any data. Keeping the read
9
+ * public matches the precedence rule in §8.4 ("server toggle > React prop >
10
+ * built-in default") — the toggles need to apply at render time on every
11
+ * user's UI, not only super_admin's. Writes remain super_admin-only via
12
+ * `PATCH /admin/settings/feature-toggles`.
13
+ *
14
+ * Missing keys are returned as absent in the response (not coerced to false)
15
+ * so the client can distinguish "explicitly off" from "fall through to the
16
+ * React prop default."
17
+ *
18
+ * @see SPECIFICATIONS.md §8.4 — feature toggles
19
+ * @see ui-sketches/08-admin-settings.md — feature_toggles_section
20
+ */
21
+ import type { RouteHandler } from "../handlers/types.js";
22
+ /**
23
+ * `GET /feature-toggles` — returns `{ feature_toggles: {...} }`.
24
+ *
25
+ * No auth required. Every authenticated AND unauthenticated request gets
26
+ * the same response. CORS is governed by the framework wrapper; the handler
27
+ * itself only writes JSON.
28
+ *
29
+ * @param _req - the inbound request (unused).
30
+ * @param ctx - the server context.
31
+ * @returns 200 with the toggles blob, or 500 if the singleton row is missing.
32
+ */
33
+ export declare const handleGetPublicFeatureToggles: RouteHandler;
34
+ //# sourceMappingURL=featureToggles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"featureToggles.d.ts","sourceRoot":"","sources":["../../src/routes/featureToggles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,6BAA6B,EAAE,YAa3C,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * `GET /feature-toggles` — public read of the deployment-wide UI feature
3
+ * toggles set by `super_admin` (§8.4).
4
+ *
5
+ * Unlike `/admin/settings/feature-toggles`, this endpoint is **unauthenticated**
6
+ * — every page-load can fetch it before login completes, and every role can
7
+ * read it after login. The blob itself is non-sensitive: it controls which
8
+ * UI affordances render in the chat shell, not any data. Keeping the read
9
+ * public matches the precedence rule in §8.4 ("server toggle > React prop >
10
+ * built-in default") — the toggles need to apply at render time on every
11
+ * user's UI, not only super_admin's. Writes remain super_admin-only via
12
+ * `PATCH /admin/settings/feature-toggles`.
13
+ *
14
+ * Missing keys are returned as absent in the response (not coerced to false)
15
+ * so the client can distinguish "explicitly off" from "fall through to the
16
+ * React prop default."
17
+ *
18
+ * @see SPECIFICATIONS.md §8.4 — feature toggles
19
+ * @see ui-sketches/08-admin-settings.md — feature_toggles_section
20
+ */
21
+ import { eq } from "drizzle-orm";
22
+ import { adminSetting } from "../db/schema.js";
23
+ import { internalError, jsonResponse } from "../handlers/responses.js";
24
+ /**
25
+ * `GET /feature-toggles` — returns `{ feature_toggles: {...} }`.
26
+ *
27
+ * No auth required. Every authenticated AND unauthenticated request gets
28
+ * the same response. CORS is governed by the framework wrapper; the handler
29
+ * itself only writes JSON.
30
+ *
31
+ * @param _req - the inbound request (unused).
32
+ * @param ctx - the server context.
33
+ * @returns 200 with the toggles blob, or 500 if the singleton row is missing.
34
+ */
35
+ export const handleGetPublicFeatureToggles = async (_req, ctx) => {
36
+ const row = ctx.db
37
+ .select()
38
+ .from(adminSetting)
39
+ .where(eq(adminSetting.id, 1))
40
+ .get();
41
+ if (!row)
42
+ return internalError();
43
+ return jsonResponse(200, {
44
+ feature_toggles: row.featureToggles ?? {},
45
+ });
46
+ };
47
+ //# sourceMappingURL=featureToggles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"featureToggles.js","sourceRoot":"","sources":["../../src/routes/featureToggles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEjC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGvE;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAiB,KAAK,EAC9D,IAAI,EACJ,GAAG,EACH,EAAE;IACF,MAAM,GAAG,GAAG,GAAG,CAAC,EAAE;SACf,MAAM,EAAE;SACR,IAAI,CAAC,YAAY,CAAC;SAClB,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;SAC7B,GAAG,EAAE,CAAC;IACT,IAAI,CAAC,GAAG;QAAE,OAAO,aAAa,EAAE,CAAC;IACjC,OAAO,YAAY,CAAC,GAAG,EAAE;QACvB,eAAe,EAAE,GAAG,CAAC,cAAc,IAAI,EAAE;KAC1C,CAAC,CAAC;AACL,CAAC,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `/health` and `/health/ready` handlers.
3
+ *
4
+ * Step 2 ships the basic shapes:
5
+ *
6
+ * - `/health` — pure liveness, no checks. Returns 200 always (the process is
7
+ * up; that's the only signal a liveness probe needs).
8
+ * - `/health/ready` — readiness. Step 2 checks DB reachability via a trivial
9
+ * `SELECT 1`. **Letta reachability is intentionally NOT checked here** —
10
+ * the Letta SDK comes online in Step 5, which extends this handler.
11
+ *
12
+ * @see SPECIFICATIONS.md §6.3.5 — health endpoints
13
+ */
14
+ import type { RouteHandler } from "../handlers/types.js";
15
+ /** `GET /health` — returns 200 with a static body. */
16
+ export declare const handleHealth: RouteHandler;
17
+ /**
18
+ * `GET /health/ready` — runs each readiness probe in turn. Returns 503
19
+ * with the first failing probe's name on any failure.
20
+ *
21
+ * Step 2 checks `db`. Step 5 will append `letta`.
22
+ */
23
+ export declare const handleHealthReady: RouteHandler;
24
+ //# sourceMappingURL=health.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/routes/health.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,sDAAsD;AACtD,eAAO,MAAM,YAAY,EAAE,YACU,CAAC;AAEtC;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,YAW/B,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `/health` and `/health/ready` handlers.
3
+ *
4
+ * Step 2 ships the basic shapes:
5
+ *
6
+ * - `/health` — pure liveness, no checks. Returns 200 always (the process is
7
+ * up; that's the only signal a liveness probe needs).
8
+ * - `/health/ready` — readiness. Step 2 checks DB reachability via a trivial
9
+ * `SELECT 1`. **Letta reachability is intentionally NOT checked here** —
10
+ * the Letta SDK comes online in Step 5, which extends this handler.
11
+ *
12
+ * @see SPECIFICATIONS.md §6.3.5 — health endpoints
13
+ */
14
+ import { jsonResponse, errorResponse } from "../handlers/responses.js";
15
+ /** `GET /health` — returns 200 with a static body. */
16
+ export const handleHealth = async () => jsonResponse(200, { status: "ok" });
17
+ /**
18
+ * `GET /health/ready` — runs each readiness probe in turn. Returns 503
19
+ * with the first failing probe's name on any failure.
20
+ *
21
+ * Step 2 checks `db`. Step 5 will append `letta`.
22
+ */
23
+ export const handleHealthReady = async (_req, ctx) => {
24
+ try {
25
+ ctx.sqlite.prepare("SELECT 1 AS ok").get();
26
+ }
27
+ catch (err) {
28
+ ctx.logger.error({ component: "db", err: errMessage(err) }, "Readiness probe failed: db unreachable.");
29
+ return errorResponse(503, "not_ready", "Database unreachable");
30
+ }
31
+ return jsonResponse(200, { status: "ready", checks: { db: "ok" } });
32
+ };
33
+ /** Extracts a string for logging from any thrown value. */
34
+ function errMessage(err) {
35
+ return err instanceof Error ? err.message : String(err);
36
+ }
37
+ //# sourceMappingURL=health.js.map