@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,246 @@
1
+ /**
2
+ * Rotation runner for **rotating starting messages**.
3
+ *
4
+ * One rotation turns a configured prompt into a fresh, forkable "prepared
5
+ * conversation" (see `rotating_message` in `db/schema.ts`):
6
+ *
7
+ * 1. `createConversation(letta_agent_id)` — a clean conversation.
8
+ * 2. Send `prompt` via the normal streaming path, drained to the terminal
9
+ * `result`. Reuses `streamUserMessage`, so the rotation inherits the same
10
+ * subprocess throttle + toolset-none suppression + model routing as a
11
+ * real chat turn.
12
+ * 3. Find the prompt's `user_message` in the resulting history and delete it,
13
+ * so the conversation opens with the agent's reply.
14
+ * 4. Persist the new conversation id as `current_conversation_id`.
15
+ *
16
+ * **Resilience.** Any failure leaves `current_conversation_id` at its last-good
17
+ * value (sessions keep forking the previous good conversation), records
18
+ * `last_status = 'error'` + `last_error`, and still advances `next_rotation_at`
19
+ * so the next interval retries rather than the tick hammering a broken agent.
20
+ *
21
+ * `computeNextRotation` / `validateScheduleFields` are pure and unit-tested
22
+ * independently of any Letta interaction.
23
+ *
24
+ * @see docs/server/rotating-messages.md
25
+ */
26
+ import { eq } from "drizzle-orm";
27
+ import { rotatingMessage } from "../db/schema.js";
28
+ import { CronParseError, cronNextRun, parseCron } from "./cron.js";
29
+ /** Smallest allowed `every`-interval (guards against hammering Letta). */
30
+ export const MIN_INTERVAL_SECONDS = 60;
31
+ /** Largest allowed `every`-interval (366 days). */
32
+ export const MAX_INTERVAL_SECONDS = 366 * 24 * 3600;
33
+ /** `"HH:MM"` 24-hour validator for the `daily` preset (UTC). */
34
+ const DAILY_TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)$/;
35
+ /**
36
+ * Validates a schedule config (used by the admin create/patch handlers).
37
+ * Pure — the only "work" is parsing cron, which throws `CronParseError` on
38
+ * malformed input.
39
+ *
40
+ * @param s - the schedule fields to validate.
41
+ * @returns `{ ok: true }` or `{ ok: false, message }`.
42
+ */
43
+ export function validateScheduleFields(s) {
44
+ switch (s.scheduleKind) {
45
+ case "every": {
46
+ if (s.intervalSeconds === null ||
47
+ !Number.isInteger(s.intervalSeconds) ||
48
+ s.intervalSeconds < MIN_INTERVAL_SECONDS ||
49
+ s.intervalSeconds > MAX_INTERVAL_SECONDS) {
50
+ return {
51
+ ok: false,
52
+ message: `interval_seconds must be an integer between ${MIN_INTERVAL_SECONDS} and ${MAX_INTERVAL_SECONDS}.`,
53
+ };
54
+ }
55
+ return { ok: true };
56
+ }
57
+ case "daily": {
58
+ if (!s.dailyTime || !DAILY_TIME_RE.test(s.dailyTime)) {
59
+ return { ok: false, message: 'daily_time must be "HH:MM" (24-hour, UTC).' };
60
+ }
61
+ return { ok: true };
62
+ }
63
+ case "cron": {
64
+ if (!s.cronExpression || s.cronExpression.trim() === "") {
65
+ return { ok: false, message: "cron_expression is required for schedule_kind 'cron'." };
66
+ }
67
+ try {
68
+ parseCron(s.cronExpression);
69
+ }
70
+ catch (err) {
71
+ return {
72
+ ok: false,
73
+ message: err instanceof CronParseError ? err.message : "Invalid cron expression.",
74
+ };
75
+ }
76
+ return { ok: true };
77
+ }
78
+ }
79
+ }
80
+ /**
81
+ * Computes the next rotation instant (UTC ISO-8601) strictly after `from`.
82
+ *
83
+ * Assumes the schedule already passed `validateScheduleFields`. `daily` and
84
+ * `cron` are evaluated in UTC (see `cron.ts`).
85
+ *
86
+ * @param s - the schedule fields.
87
+ * @param from - the reference instant.
88
+ * @returns the next rotation time as a UTC ISO string.
89
+ */
90
+ export function computeNextRotation(s, from) {
91
+ switch (s.scheduleKind) {
92
+ case "every": {
93
+ const secs = s.intervalSeconds ?? MIN_INTERVAL_SECONDS;
94
+ return new Date(from.getTime() + secs * 1000).toISOString();
95
+ }
96
+ case "daily": {
97
+ const [hh, mm] = (s.dailyTime ?? "00:00").split(":").map(Number);
98
+ const next = new Date(from.getTime());
99
+ next.setUTCHours(hh, mm, 0, 0);
100
+ if (next.getTime() <= from.getTime()) {
101
+ next.setUTCDate(next.getUTCDate() + 1);
102
+ }
103
+ return next.toISOString();
104
+ }
105
+ case "cron":
106
+ return cronNextRun(s.cronExpression ?? "* * * * *", from);
107
+ }
108
+ }
109
+ /** `computeNextRotation` that never throws — falls back to +1h on a poison row. */
110
+ function safeComputeNextRotation(s, from, logger) {
111
+ try {
112
+ return computeNextRotation(s, from);
113
+ }
114
+ catch (err) {
115
+ logger.error({ err: err instanceof Error ? err.message : String(err), event: "rotation_next_compute_failed" }, "Failed to compute next rotation time; falling back to +1h.");
116
+ return new Date(from.getTime() + 3600 * 1000).toISOString();
117
+ }
118
+ }
119
+ /**
120
+ * Runs one rotation for `row` and persists the result. Never throws — the
121
+ * outcome is reported via the return value and the row's `last_status`.
122
+ *
123
+ * @param deps - db + Letta client + logger.
124
+ * @param row - the rotating-message row to rotate.
125
+ * @returns the rotation outcome.
126
+ */
127
+ export async function runRotation(deps, row) {
128
+ const now = (deps.now ?? (() => new Date()))();
129
+ const nowIso = now.toISOString();
130
+ const nextRotationAt = safeComputeNextRotation(row, now, deps.logger);
131
+ const previousConversationId = row.currentConversationId;
132
+ let createdConversationId = null;
133
+ try {
134
+ const conv = await deps.lettaClient.createConversation(row.lettaAgentId);
135
+ createdConversationId = conv.id;
136
+ await sendPromptAndDrain(deps.lettaClient, conv.id, row.lettaAgentId, row.prompt);
137
+ // Delete the prompt's user message so the conversation opens with the
138
+ // agent's reply. listMessages is oldest-first; the first user_message is
139
+ // the prompt we just sent.
140
+ const messages = await deps.lettaClient.listMessages(conv.id);
141
+ const firstUser = messages.find((m) => m.message_type === "user_message");
142
+ if (firstUser?.id) {
143
+ await deps.lettaClient.deleteMessage(row.lettaAgentId, firstUser.id);
144
+ }
145
+ else {
146
+ deps.logger.warn({ rotating_message_id: row.id, conversation_id: conv.id, event: "rotation_no_user_message" }, "Rotation produced no user_message to delete; storing conversation as-is.");
147
+ }
148
+ deps.db
149
+ .update(rotatingMessage)
150
+ .set({
151
+ currentConversationId: conv.id,
152
+ lastStatus: "ok",
153
+ lastError: null,
154
+ lastRotatedAt: nowIso,
155
+ nextRotationAt,
156
+ updatedAt: nowIso,
157
+ })
158
+ .where(eq(rotatingMessage.id, row.id))
159
+ .run();
160
+ // Best-effort cleanup of the now-superseded conversation.
161
+ if (previousConversationId && previousConversationId !== conv.id) {
162
+ try {
163
+ await deps.lettaClient.deleteConversation(previousConversationId);
164
+ }
165
+ catch (err) {
166
+ deps.logger.warn({
167
+ rotating_message_id: row.id,
168
+ conversation_id: previousConversationId,
169
+ err: err instanceof Error ? err.message : String(err),
170
+ event: "rotation_previous_cleanup_failed",
171
+ }, "Failed to delete the previous prepared conversation; leaving it orphaned.");
172
+ }
173
+ }
174
+ deps.logger.info({
175
+ rotating_message_id: row.id,
176
+ letta_agent_id: row.lettaAgentId,
177
+ conversation_id: conv.id,
178
+ next_rotation_at: nextRotationAt,
179
+ event: "rotation_ok",
180
+ }, "Rotating starting message refreshed.");
181
+ return { status: "ok", conversationId: conv.id };
182
+ }
183
+ catch (err) {
184
+ const message = err instanceof Error ? err.message : String(err);
185
+ // Best-effort drop of the half-built conversation so failures don't
186
+ // accumulate orphans. The last-good current_conversation_id is untouched.
187
+ if (createdConversationId) {
188
+ try {
189
+ await deps.lettaClient.deleteConversation(createdConversationId);
190
+ }
191
+ catch {
192
+ /* best-effort */
193
+ }
194
+ }
195
+ deps.db
196
+ .update(rotatingMessage)
197
+ .set({
198
+ lastStatus: "error",
199
+ lastError: message,
200
+ nextRotationAt,
201
+ updatedAt: nowIso,
202
+ })
203
+ .where(eq(rotatingMessage.id, row.id))
204
+ .run();
205
+ deps.logger.error({
206
+ rotating_message_id: row.id,
207
+ letta_agent_id: row.lettaAgentId,
208
+ err: message,
209
+ next_rotation_at: nextRotationAt,
210
+ event: "rotation_error",
211
+ }, "Rotation failed; keeping the previous prepared conversation.");
212
+ return { status: "error", error: message };
213
+ }
214
+ }
215
+ /**
216
+ * Sends `prompt` to `conversationId` and drains the stream to its terminal
217
+ * `result`. Throws when the run produced no terminal result or finished
218
+ * unsuccessfully. Always tears the subprocess down via `handle.close()`.
219
+ */
220
+ async function sendPromptAndDrain(client, conversationId, lettaAgentId, prompt) {
221
+ const handle = await client.streamUserMessage({
222
+ conversationId,
223
+ lettaAgentId,
224
+ userMessage: prompt,
225
+ });
226
+ let sawResult = false;
227
+ let success = false;
228
+ try {
229
+ for await (const msg of handle.events) {
230
+ if (msg.type === "result") {
231
+ sawResult = true;
232
+ success = msg.success;
233
+ }
234
+ }
235
+ }
236
+ finally {
237
+ handle.close();
238
+ }
239
+ if (!sawResult) {
240
+ throw new Error("Agent run produced no terminal result.");
241
+ }
242
+ if (!success) {
243
+ throw new Error("Agent run did not complete successfully.");
244
+ }
245
+ }
246
+ //# sourceMappingURL=rotationRunner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rotationRunner.js","sourceRoot":"","sources":["../../src/state/rotationRunner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * In-process scheduler for **rotating starting messages**.
3
+ *
4
+ * ai-hub deploys as one long-running Node process (in-process
5
+ * `better-sqlite3` + self-hosted Letta), so a simple tick loop is the right
6
+ * tool — no external cron, queue, or multi-instance coordination needed.
7
+ *
8
+ * Each tick selects non-archived `rotating_message` rows whose
9
+ * `next_rotation_at <= now` and runs them **serially** through `runRotation`.
10
+ * An in-memory `inFlight` set prevents a slow rotation from being re-picked by
11
+ * the next tick. `runRotation` advances `next_rotation_at` itself, so a row is
12
+ * naturally not due again until its interval elapses.
13
+ *
14
+ * The scheduler is started once per process from `createFetchHandler`
15
+ * (`ensureRotationScheduler`) — that's the single construction point both the
16
+ * dev `node:http` runner and the Next.js wrapper go through. It is **skipped
17
+ * under tests** (`VITEST` / `NODE_ENV=test`) and via
18
+ * `AI_HUB_DISABLE_ROTATION_SCHEDULER=1`, so the existing server-boot test
19
+ * suite is unaffected.
20
+ *
21
+ * @see docs/server/rotating-messages.md
22
+ */
23
+ import type { DrizzleDB } from "../db/connection.js";
24
+ import type { LettaClient } from "../letta/client.js";
25
+ import type { Logger } from "../observability/logger.js";
26
+ /** Dependency bundle for the scheduler. */
27
+ export interface RotationSchedulerDeps {
28
+ db: DrizzleDB;
29
+ lettaClient: LettaClient;
30
+ logger: Logger;
31
+ }
32
+ /** Handle for stopping a running scheduler. */
33
+ export interface RotationScheduler {
34
+ /** Stops the tick loop. Idempotent. */
35
+ stop(): void;
36
+ }
37
+ /** Options for `startRotationScheduler`. */
38
+ export interface StartRotationSchedulerOptions {
39
+ /** Tick interval in ms. Default 30000. Tests pass a small value. */
40
+ intervalMs?: number;
41
+ /** Skip the immediate kick-off tick (tests drive `tickOnce` directly). */
42
+ skipInitialTick?: boolean;
43
+ }
44
+ /**
45
+ * Starts a tick loop and returns a handle. Most callers want
46
+ * `ensureRotationScheduler` (process singleton); this is the testable core.
47
+ *
48
+ * @param deps - db + Letta client + logger.
49
+ * @param opts - tick cadence + initial-tick control.
50
+ * @returns a scheduler handle with `stop()`.
51
+ */
52
+ export declare function startRotationScheduler(deps: RotationSchedulerDeps, opts?: StartRotationSchedulerOptions): RotationScheduler;
53
+ /**
54
+ * Runs every due, non-archived rotation once, serially. Exported for tests
55
+ * (drive a single tick deterministically without a timer).
56
+ *
57
+ * @param deps - db + Letta client + logger.
58
+ * @param inFlight - shared set guarding against re-entrancy across ticks.
59
+ */
60
+ export declare function tickOnce(deps: RotationSchedulerDeps, inFlight?: Set<string>): Promise<void>;
61
+ /**
62
+ * Starts the rotation scheduler once per process and returns it (or `null`
63
+ * when intentionally disabled). Idempotent — subsequent calls return the
64
+ * already-running instance, so building multiple Fetch handlers in one process
65
+ * never spawns competing loops.
66
+ *
67
+ * Disabled (returns `null`, starts nothing) when:
68
+ * - `AI_HUB_DISABLE_ROTATION_SCHEDULER=1`, or
69
+ * - running under tests (`VITEST` set, or `NODE_ENV === "test"`).
70
+ *
71
+ * @param deps - db + Letta client + logger.
72
+ * @param env - env source (defaults to `process.env`).
73
+ * @returns the scheduler handle, or `null` when disabled.
74
+ */
75
+ export declare function ensureRotationScheduler(deps: RotationSchedulerDeps, env?: Record<string, string | undefined>): RotationScheduler | null;
76
+ //# sourceMappingURL=rotationScheduler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rotationScheduler.d.ts","sourceRoot":"","sources":["../../src/state/rotationScheduler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAOzD,2CAA2C;AAC3C,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,SAAS,CAAC;IACd,WAAW,EAAE,WAAW,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,+CAA+C;AAC/C,MAAM,WAAW,iBAAiB;IAChC,uCAAuC;IACvC,IAAI,IAAI,IAAI,CAAC;CACd;AAED,4CAA4C;AAC5C,MAAM,WAAW,6BAA6B;IAC5C,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,qBAAqB,EAC3B,IAAI,GAAE,6BAAkC,GACvC,iBAAiB,CAmCnB;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,qBAAqB,EAC3B,QAAQ,GAAE,GAAG,CAAC,MAAM,CAAa,GAChC,OAAO,CAAC,IAAI,CAAC,CAiBf;AAQD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,qBAAqB,EAC3B,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACpD,iBAAiB,GAAG,IAAI,CAO1B"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * In-process scheduler for **rotating starting messages**.
3
+ *
4
+ * ai-hub deploys as one long-running Node process (in-process
5
+ * `better-sqlite3` + self-hosted Letta), so a simple tick loop is the right
6
+ * tool — no external cron, queue, or multi-instance coordination needed.
7
+ *
8
+ * Each tick selects non-archived `rotating_message` rows whose
9
+ * `next_rotation_at <= now` and runs them **serially** through `runRotation`.
10
+ * An in-memory `inFlight` set prevents a slow rotation from being re-picked by
11
+ * the next tick. `runRotation` advances `next_rotation_at` itself, so a row is
12
+ * naturally not due again until its interval elapses.
13
+ *
14
+ * The scheduler is started once per process from `createFetchHandler`
15
+ * (`ensureRotationScheduler`) — that's the single construction point both the
16
+ * dev `node:http` runner and the Next.js wrapper go through. It is **skipped
17
+ * under tests** (`VITEST` / `NODE_ENV=test`) and via
18
+ * `AI_HUB_DISABLE_ROTATION_SCHEDULER=1`, so the existing server-boot test
19
+ * suite is unaffected.
20
+ *
21
+ * @see docs/server/rotating-messages.md
22
+ */
23
+ import { and, eq, lte } from "drizzle-orm";
24
+ import { rotatingMessage } from "../db/schema.js";
25
+ import { runRotation } from "./rotationRunner.js";
26
+ /** Default tick cadence. Rotations are hourly/daily, so 30s polling is ample. */
27
+ const DEFAULT_TICK_MS = 30_000;
28
+ /**
29
+ * Starts a tick loop and returns a handle. Most callers want
30
+ * `ensureRotationScheduler` (process singleton); this is the testable core.
31
+ *
32
+ * @param deps - db + Letta client + logger.
33
+ * @param opts - tick cadence + initial-tick control.
34
+ * @returns a scheduler handle with `stop()`.
35
+ */
36
+ export function startRotationScheduler(deps, opts = {}) {
37
+ const intervalMs = opts.intervalMs ?? DEFAULT_TICK_MS;
38
+ const inFlight = new Set();
39
+ let stopped = false;
40
+ let ticking = false;
41
+ const tick = async () => {
42
+ if (stopped || ticking)
43
+ return;
44
+ ticking = true;
45
+ try {
46
+ await tickOnce(deps, inFlight);
47
+ }
48
+ catch (err) {
49
+ deps.logger.error({ err: err instanceof Error ? err.message : String(err), event: "rotation_tick_failed" }, "Rotation scheduler tick failed.");
50
+ }
51
+ finally {
52
+ ticking = false;
53
+ }
54
+ };
55
+ const timer = setInterval(() => void tick(), intervalMs);
56
+ // Never let the scheduler alone keep the process alive.
57
+ if (typeof timer.unref === "function")
58
+ timer.unref();
59
+ if (!opts.skipInitialTick) {
60
+ void tick();
61
+ }
62
+ return {
63
+ stop() {
64
+ stopped = true;
65
+ clearInterval(timer);
66
+ },
67
+ };
68
+ }
69
+ /**
70
+ * Runs every due, non-archived rotation once, serially. Exported for tests
71
+ * (drive a single tick deterministically without a timer).
72
+ *
73
+ * @param deps - db + Letta client + logger.
74
+ * @param inFlight - shared set guarding against re-entrancy across ticks.
75
+ */
76
+ export async function tickOnce(deps, inFlight = new Set()) {
77
+ const nowIso = new Date().toISOString();
78
+ const due = deps.db
79
+ .select()
80
+ .from(rotatingMessage)
81
+ .where(and(eq(rotatingMessage.isArchived, false), lte(rotatingMessage.nextRotationAt, nowIso)))
82
+ .all();
83
+ for (const row of due) {
84
+ if (inFlight.has(row.id))
85
+ continue;
86
+ inFlight.add(row.id);
87
+ try {
88
+ await runRotation(deps, row);
89
+ }
90
+ finally {
91
+ inFlight.delete(row.id);
92
+ }
93
+ }
94
+ }
95
+ /* ────────────────────────────────────────────────────────────────────────── */
96
+ /* Process singleton */
97
+ /* ────────────────────────────────────────────────────────────────────────── */
98
+ let singleton = null;
99
+ /**
100
+ * Starts the rotation scheduler once per process and returns it (or `null`
101
+ * when intentionally disabled). Idempotent — subsequent calls return the
102
+ * already-running instance, so building multiple Fetch handlers in one process
103
+ * never spawns competing loops.
104
+ *
105
+ * Disabled (returns `null`, starts nothing) when:
106
+ * - `AI_HUB_DISABLE_ROTATION_SCHEDULER=1`, or
107
+ * - running under tests (`VITEST` set, or `NODE_ENV === "test"`).
108
+ *
109
+ * @param deps - db + Letta client + logger.
110
+ * @param env - env source (defaults to `process.env`).
111
+ * @returns the scheduler handle, or `null` when disabled.
112
+ */
113
+ export function ensureRotationScheduler(deps, env = process.env) {
114
+ if (singleton)
115
+ return singleton;
116
+ if (env.AI_HUB_DISABLE_ROTATION_SCHEDULER === "1")
117
+ return null;
118
+ if (env.VITEST || env.NODE_ENV === "test")
119
+ return null;
120
+ singleton = startRotationScheduler(deps);
121
+ deps.logger.info({ event: "rotation_scheduler_started" }, "Rotating-message scheduler started.");
122
+ return singleton;
123
+ }
124
+ //# sourceMappingURL=rotationScheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rotationScheduler.js","sourceRoot":"","sources":["../../src/state/rotationScheduler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * In-memory broadcaster for live turn streams.
3
+ *
4
+ * Every active turn has at most one writer (the runner) and zero-or-more
5
+ * subscribers (the original `POST /chat` connection + every
6
+ * `GET /chat/:turn_id/events?from_seq=N` resumption connection still
7
+ * running while the turn is live).
8
+ *
9
+ * Persistence lives in `turn_events` (§11) — the registry only handles the
10
+ * **live attach** part of resumption. The first half of a resumption read
11
+ * (replay from `from_seq`) is a plain DB query.
12
+ *
13
+ * The registry is process-local — this matches the single-customer
14
+ * deployment model (§1). When ai-hub scales to multiple replicas in a
15
+ * v1.x release, this gets a Redis-backed implementation behind the same
16
+ * interface; the wire protocol (§9) and the resumption contract (§6.3.2)
17
+ * stay unchanged.
18
+ *
19
+ * @see SPECIFICATIONS.md §6.3.2 — resumption endpoint
20
+ * @see SPECIFICATIONS.md §6.6 — server-authoritative state machine
21
+ * @see SPECIFICATIONS.md §11 — `turn_events` table
22
+ */
23
+ import type { AIHubEvent } from "@data-club/ai-hub";
24
+ /** Callback signature for a live subscriber. */
25
+ export type TurnEventListener = (event: AIHubEvent) => void;
26
+ /** Returned by `subscribe()` so the caller can unsubscribe. */
27
+ export type Unsubscribe = () => void;
28
+ /**
29
+ * Begins a new live turn channel.
30
+ *
31
+ * The runner calls this when it inserts the `turn` row, before it starts
32
+ * pumping events. Returns an opaque `TurnPublisher` the runner uses to
33
+ * publish + close.
34
+ *
35
+ * @param turnId - the freshly-created turn id.
36
+ * @returns the publisher handle.
37
+ */
38
+ export declare function openTurnChannel(turnId: string): TurnPublisher;
39
+ /**
40
+ * Subscribes to a live turn channel.
41
+ *
42
+ * Returns `null` when the turn is not active (either never opened or
43
+ * already closed) — the caller should fall back to replaying persisted
44
+ * events from `turn_events` instead.
45
+ *
46
+ * @param turnId - the turn id to attach to.
47
+ * @param listener - called once per event (with `seq` already assigned).
48
+ * @param onClose - called once when the channel closes (after final event).
49
+ * @returns an unsubscribe function, or `null` when the channel is closed.
50
+ */
51
+ export declare function subscribeToTurn(turnId: string, listener: TurnEventListener, onClose: () => void): Unsubscribe | null;
52
+ /**
53
+ * Returns `true` when the turn has an open channel right now.
54
+ *
55
+ * The chat handler uses this to disambiguate "turn is in flight" from
56
+ * "turn completed long ago" without having to query the DB twice.
57
+ */
58
+ export declare function isTurnLive(turnId: string): boolean;
59
+ /**
60
+ * Test-only utility — wipes the registry so tests can isolate from each
61
+ * other. Production code never calls this.
62
+ */
63
+ export declare function _resetTurnRegistryForTests(): void;
64
+ /**
65
+ * The runner's handle on a live channel. Opaque on purpose — the runner
66
+ * is the only writer.
67
+ */
68
+ export interface TurnPublisher {
69
+ /**
70
+ * AbortSignal that fires when a separate `POST /chat/:id/cancel`
71
+ * request calls `requestTurnCancellation(turnId)`. The runner listens
72
+ * to this in addition to the originating request's signal.
73
+ */
74
+ cancelSignal: AbortSignal;
75
+ /** Push one event to every current subscriber. */
76
+ publish(event: AIHubEvent): void;
77
+ /** Mark the channel closed. After this, `subscribeToTurn` returns null. */
78
+ close(): void;
79
+ }
80
+ /**
81
+ * Triggers cancellation of the named turn. The runner is expected to
82
+ * have wired its run to the publisher's `cancelSignal`; firing it from
83
+ * here aborts the upstream Letta call and short-circuits the loop.
84
+ *
85
+ * Idempotent: calling on a closed / unknown turn is a no-op. The
86
+ * `POST /chat/:id/cancel` route's user-facing 200 response covers
87
+ * race-conditions where the turn terminated between the request and
88
+ * this call.
89
+ *
90
+ * @param turnId - the turn to cancel.
91
+ * @returns `true` if the signal was fired, `false` if the channel is
92
+ * closed or unknown.
93
+ */
94
+ export declare function requestTurnCancellation(turnId: string): boolean;
95
+ //# sourceMappingURL=turnRegistry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turnRegistry.d.ts","sourceRoot":"","sources":["../../src/state/turnRegistry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,gDAAgD;AAChD,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AAE5D,+DAA+D;AAC/D,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AAyCrC;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CA0C7D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,EAAE,MAAM,IAAI,GAClB,WAAW,GAAG,IAAI,CAYpB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGlD;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,YAAY,EAAE,WAAW,CAAC;IAC1B,kDAAkD;IAClD,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACjC,2EAA2E;IAC3E,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAK/D"}