@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 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,662 @@
1
+ /**
2
+ * Drizzle schema for the ai-hub server's SQLite database.
3
+ *
4
+ * One row per real-world entity. v1 has no tenant scoping — a deployment is
5
+ * one customer organization, every row belongs to that customer.
6
+ *
7
+ * The shapes here are the canonical source of truth: the migration runner
8
+ * (`migrate.ts`) builds tables off these definitions, and every handler reads
9
+ * and writes through Drizzle's typed query builder.
10
+ *
11
+ * Forward-compat seams (intentionally non-obvious — see §6.7 "Future
12
+ * direction"):
13
+ *
14
+ * - `user.password_hash` is **nullable**. v1 always populates it (local login is
15
+ * the only flow that creates users). Future external IdPs leave it NULL.
16
+ * - `user.identity_source` defaults to `"local"`. v1 only ever writes
17
+ * `"local"`; future external IdPs add values like `"oidc:bankxyz"`.
18
+ * - `agent.is_archived` and `session.is_archived` are soft-hide flags.
19
+ * **Agents and sessions cannot be hard-deleted in v1** (§6.3.4 / §11) — the
20
+ * FK on `session.agent_id` is therefore always satisfiable and orphan
21
+ * sessions never happen.
22
+ *
23
+ * @see SPECIFICATIONS.md §11 — data model
24
+ * @see SPECIFICATIONS.md §6.7 — auth model + future IdPs
25
+ * @see SPECIFICATIONS.md §6.8 — persistence
26
+ */
27
+ import { sql } from "drizzle-orm";
28
+ import { blob, index, integer, primaryKey, sqliteTable, text, } from "drizzle-orm/sqlite-core";
29
+ /* ────────────────────────────────────────────────────────────────────────── */
30
+ /* user */
31
+ /* ────────────────────────────────────────────────────────────────────────── */
32
+ /**
33
+ * Every human in this deployment, including super admins. ai-hub owns auth.
34
+ *
35
+ * The PRD uses singular table names; we follow that exactly so SQL tooling
36
+ * matches what implementing agents see in the spec.
37
+ *
38
+ * @see SPECIFICATIONS.md §11
39
+ */
40
+ export const user = sqliteTable("user", {
41
+ /** Stable UUID PK assigned at insert time (server-generated). */
42
+ id: text("id").notNull().primaryKey(),
43
+ /** Login identifier — unique per deployment. */
44
+ username: text("username").notNull().unique(),
45
+ /** Optional human-readable name shown in the admin user list. */
46
+ displayName: text("display_name"),
47
+ /**
48
+ * Argon2id encoded hash (PHC string format: embeds m / t / p / salt /
49
+ * digest). Nullable for forward-compat with external IdPs (§6.7); v1
50
+ * always populates it.
51
+ */
52
+ passwordHash: text("password_hash"),
53
+ /**
54
+ * `"local"` in v1. Future external IdPs add values like `"oidc:bankxyz"`.
55
+ * Designed-for-extension seam per §6.7.
56
+ */
57
+ identitySource: text("identity_source").notNull().default("local"),
58
+ /**
59
+ * `true` = the user must change their password on the next successful
60
+ * login (§8.5). Set on the seeded super-admin and after every admin-driven
61
+ * password reset.
62
+ */
63
+ isInitialPassword: integer("is_initial_password", { mode: "boolean" })
64
+ .notNull()
65
+ .default(false),
66
+ /** One of the four roles in §8.1. */
67
+ role: text("role", {
68
+ enum: ["user", "power_user", "admin", "super_admin"],
69
+ })
70
+ .notNull()
71
+ .default("user"),
72
+ /**
73
+ * `"light"` / `"dark"` / NULL. NULL = follow `admin_setting.theme_default`.
74
+ * Set when the user picks via `AIHub.ThemeSwitcher`. Ignored as long as
75
+ * `theme_switcher_enabled = false` (§7.6).
76
+ */
77
+ themePreference: text("theme_preference", { enum: ["light", "dark"] }),
78
+ /**
79
+ * ISO-8601 timestamp of the last password mutation. Bumped on:
80
+ *
81
+ * - Admin password reset (`POST /admin/users/:id/reset-password`).
82
+ * - Voluntary password change (`POST /auth/change-password` with a
83
+ * session token + `current_password`).
84
+ * - Forced change after admin reset (`POST /auth/change-password`
85
+ * with a password-change-only token).
86
+ *
87
+ * The middleware compares `passwordChangedAt` against the JWT's
88
+ * `iat` claim — session tokens issued **before** the last password
89
+ * mutation are rejected as `401 invalid_credentials`. This is how
90
+ * an admin-driven reset signs the affected user out immediately
91
+ * without waiting for the 24h JWT expiry (Step 11 bugfix).
92
+ */
93
+ passwordChangedAt: text("password_changed_at")
94
+ .notNull()
95
+ .default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
96
+ /** ISO-8601-string seconds-precision timestamp (better-sqlite3 stores as TEXT). */
97
+ createdAt: text("created_at")
98
+ .notNull()
99
+ .default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
100
+ /** Bumped on every UPDATE — handlers SET this explicitly so it's deterministic. */
101
+ updatedAt: text("updated_at")
102
+ .notNull()
103
+ .default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
104
+ }, (t) => ({
105
+ /** Speeds up the role-matrix's "find any user with role super_admin" query. */
106
+ roleIdx: index("user_role_idx").on(t.role),
107
+ }));
108
+ /* ────────────────────────────────────────────────────────────────────────── */
109
+ /* agent */
110
+ /* ────────────────────────────────────────────────────────────────────────── */
111
+ /**
112
+ * `power_user`/`admin`/`super_admin`-curated catalog of ai-hub agents.
113
+ *
114
+ * Each row is a use-case (e.g. *Customer Support*) backed by exactly one Letta
115
+ * agent. Shared across every user in the deployment (§13.2).
116
+ *
117
+ * Cannot be hard-deleted in v1 — only archived. `is_archived = true` filters
118
+ * the row out of the user-facing `GET /agents` response so no new sessions can
119
+ * start against it; existing sessions remain readable + writable.
120
+ *
121
+ * @see SPECIFICATIONS.md §11 — `agent` table
122
+ * @see SPECIFICATIONS.md §6.5 — TPM recovery defaults
123
+ */
124
+ export const agent = sqliteTable("agent", {
125
+ /** Stable UUID PK. */
126
+ id: text("id").notNull().primaryKey(),
127
+ /** Operator-facing display name (no length cap in v1). */
128
+ displayName: text("display_name").notNull(),
129
+ /**
130
+ * The Letta-side agent's id. Validated against `client.agents.list` at
131
+ * write time (Step 5 wires the real check; Step 4 stubs it through).
132
+ */
133
+ lettaAgentId: text("letta_agent_id").notNull(),
134
+ /** Master toggle for the §6.5 recovery loop. Default `false`. */
135
+ tpmRecoveryEnabled: integer("tpm_recovery_enabled", { mode: "boolean" })
136
+ .notNull()
137
+ .default(false),
138
+ /** Effective ceiling on recovery attempts (§6.5 double-failure abort). */
139
+ maxRecoveryAttempts: integer("max_recovery_attempts").notNull().default(2),
140
+ /** Used when 429 doesn't include `Retry-After`. Default 65 (§6.5). */
141
+ defaultRetryAfterSeconds: integer("default_retry_after_seconds")
142
+ .notNull()
143
+ .default(65),
144
+ /**
145
+ * Inter-event idle watchdog for the chat stream. If no Letta event
146
+ * arrives within this many seconds while a turn is `running`, the
147
+ * runner aborts the turn cleanly: emits
148
+ * `error{code:"stream_idle"}` + `stream_end{reason:"error"}`, cancels
149
+ * the upstream Letta run via `client.agents.messages.cancel`, and
150
+ * transitions `turn.status -> failed`. The timer **resets on every
151
+ * received event** — well-behaved long-running turns (lots of small
152
+ * deltas) never trip it; truly wedged ones (provider hang, Letta
153
+ * deadlock) terminate predictably. Default 60s. Set higher for
154
+ * tool-heavy agents whose tools may legitimately take longer to run
155
+ * with no streaming output in between; set lower if you'd rather
156
+ * fail fast on a stalled provider.
157
+ */
158
+ streamIdleTimeoutSeconds: integer("stream_idle_timeout_seconds")
159
+ .notNull()
160
+ .default(60),
161
+ /**
162
+ * Step-12.5 specialized-agent lever #1 — per-session sandbox directories.
163
+ *
164
+ * Relative folder name under `{AI_HUB_FILES_DIR}/seeds/` whose contents
165
+ * the server recursively copies into `{AI_HUB_FILES_DIR}/sandboxes/{session_id}/`
166
+ * at session-create time. The Letta Code subprocess for the session then
167
+ * spawns with `cwd` pointed at the sandbox, and its bundled filesystem
168
+ * tools (`Read` / `Write` / `Edit` / `Bash` / `Grep` / `Glob`) are
169
+ * re-enabled so the agent can use the seeded files. NULL = no sandbox;
170
+ * sessions run with no `cwd` and the existing disallowed-tools list
171
+ * (today's stateless agent behavior). Validated `^[a-zA-Z0-9_-]+$` at
172
+ * admin-write time + checked to exist on disk.
173
+ *
174
+ * @see docs/server/session-sandboxes.md
175
+ */
176
+ sandboxSeedName: text("sandbox_seed_name"),
177
+ /**
178
+ * Step-12.5 specialized-agent lever #2 — base-conversation forking.
179
+ *
180
+ * Letta conversation id (e.g. `conv-…`) to fork from at `POST /sessions`
181
+ * time via `client.conversations.fork(...)`. The fork inherits the
182
+ * source's in-context messages + gets a freshly compiled system message.
183
+ * NULL = brand-new conversation via `client.conversations.create({ agent_id })`
184
+ * (today's behavior). Validated at admin-write time: must resolve via
185
+ * `client.conversations.retrieve` AND its `agent_id` must equal
186
+ * `letta_agent_id`.
187
+ *
188
+ * @see docs/server/session-sandboxes.md
189
+ */
190
+ forkSourceConversationId: text("fork_source_conversation_id"),
191
+ /** Soft-hide flag — see file-level comment. */
192
+ isArchived: integer("is_archived", { mode: "boolean" }).notNull().default(false),
193
+ /** ISO-8601 timestamp. */
194
+ createdAt: text("created_at")
195
+ .notNull()
196
+ .default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
197
+ /** ISO-8601 timestamp. */
198
+ updatedAt: text("updated_at")
199
+ .notNull()
200
+ .default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
201
+ });
202
+ /* ────────────────────────────────────────────────────────────────────────── */
203
+ /* session */
204
+ /* ────────────────────────────────────────────────────────────────────────── */
205
+ /**
206
+ * One row per ai-hub session — a user's chat against a given ai-hub agent.
207
+ *
208
+ * `letta_conversation_id` is server-internal: the React client sees only
209
+ * `id`. `letta_agent_id` is a snapshot of `agent.letta_agent_id` at session
210
+ * creation time (re-pointing the catalog row does not migrate existing
211
+ * conversations).
212
+ *
213
+ * @see SPECIFICATIONS.md §11 — `session` table
214
+ * @see SPECIFICATIONS.md §6.3.1 — sessions API
215
+ */
216
+ export const session = sqliteTable("session", {
217
+ /** Stable UUID PK. */
218
+ id: text("id").notNull().primaryKey(),
219
+ /** Owner — enforces (caller_id === user_id) on read/write (§13.2). */
220
+ userId: text("user_id")
221
+ .notNull()
222
+ .references(() => user.id, { onDelete: "cascade" }),
223
+ /**
224
+ * The ai-hub agent (use-case) this session is scoped to. RESTRICT-on-delete
225
+ * is moot in v1 because agents are never hard-deleted — only archived.
226
+ */
227
+ agentId: text("agent_id")
228
+ .notNull()
229
+ .references(() => agent.id, { onDelete: "restrict" }),
230
+ /** Identifier returned by Letta on `client.conversations.create`. */
231
+ lettaConversationId: text("letta_conversation_id").notNull(),
232
+ /**
233
+ * Snapshot of `agent.letta_agent_id` at create-time. Re-pointing the
234
+ * catalog entry does not move existing conversations.
235
+ */
236
+ lettaAgentId: text("letta_agent_id").notNull(),
237
+ /**
238
+ * Required at create-time (1..80 chars enforced by Zod at the handler
239
+ * layer, §6.3.1); editable via PATCH `/sessions/{id}`.
240
+ */
241
+ title: text("title").notNull(),
242
+ /**
243
+ * Soft-hide. Excluded from `GET /sessions` by default; `?include_archived=true`
244
+ * surfaces archived rows. There is no hard-delete (§6.3.1).
245
+ */
246
+ isArchived: integer("is_archived", { mode: "boolean" }).notNull().default(false),
247
+ /**
248
+ * Per-session reasoning effort, applied to the agent's `model_settings`
249
+ * before each turn. NULL = use agent's persisted default. The accepted
250
+ * values depend on the agent's model — Anthropic uses `low|medium|high|xhigh|max`,
251
+ * OpenAI uses `none|minimal|low|medium|high|xhigh`. The capability map
252
+ * (computed from the agent's model handle) drives UI rendering.
253
+ */
254
+ reasoningEffort: text("reasoning_effort"),
255
+ /**
256
+ * Per-session verbosity, OpenAI-family models only. NULL = default.
257
+ */
258
+ verbosity: text("verbosity"),
259
+ /**
260
+ * Per-session thinking budget for Google AI/Vertex models (numeric token
261
+ * budget). NULL = default.
262
+ */
263
+ thinkingBudget: integer("thinking_budget"),
264
+ /**
265
+ * Step-12.5 — absolute filesystem path of the per-session sandbox
266
+ * directory. Populated at create time **only when** the parent agent's
267
+ * `sandbox_seed_name` was non-null (the server recursively copied the
268
+ * seed folder into this path before responding with the new session).
269
+ * NULL = no sandbox; the Letta Code subprocess for this session spawns
270
+ * with no `cwd` and the existing disallowed-tools list.
271
+ *
272
+ * Server-internal — never exposed via wire (mirrors
273
+ * `letta_conversation_id`). Cleared to NULL by
274
+ * `POST /admin/sessions/:id/purge-sandbox`.
275
+ *
276
+ * @see docs/server/session-sandboxes.md
277
+ */
278
+ sandboxDir: text("sandbox_dir"),
279
+ /** ISO-8601 timestamp. */
280
+ createdAt: text("created_at")
281
+ .notNull()
282
+ .default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
283
+ /** ISO-8601 timestamp. */
284
+ updatedAt: text("updated_at")
285
+ .notNull()
286
+ .default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
287
+ }, (t) => ({
288
+ /** Drives the user-scoped session list + the per-caller session_count on /agents. */
289
+ userAgentIdx: index("session_user_agent_idx").on(t.userId, t.agentId),
290
+ }));
291
+ /* ────────────────────────────────────────────────────────────────────────── */
292
+ /* turn */
293
+ /* ────────────────────────────────────────────────────────────────────────── */
294
+ /**
295
+ * One row per AI turn (user message + assistant response cycle).
296
+ *
297
+ * Lives in the ai-hub DB for the *streaming session* (resumption + state
298
+ * transitions); message history itself is Letta-owned.
299
+ *
300
+ * @see SPECIFICATIONS.md §11
301
+ * @see SPECIFICATIONS.md §10 — state machine
302
+ */
303
+ export const turn = sqliteTable("turn", {
304
+ /** Stable UUID PK. */
305
+ id: text("id").notNull().primaryKey(),
306
+ /** Parent session. Cascades on delete — but sessions never delete in v1. */
307
+ sessionId: text("session_id")
308
+ .notNull()
309
+ .references(() => session.id, { onDelete: "cascade" }),
310
+ /** Top-level §10.1 turn status. `idle` is excluded — no row exists yet then. */
311
+ status: text("status", {
312
+ enum: ["running", "done", "cancelled", "failed"],
313
+ }).notNull(),
314
+ /** Sub-state per §10.1; only meaningful while `status = 'running'`. */
315
+ phase: text("phase", {
316
+ enum: [
317
+ "model_text",
318
+ "model_reasoning",
319
+ "model_tool_input",
320
+ "tool_executing",
321
+ "waiting_provider",
322
+ "awaiting_user",
323
+ ],
324
+ }),
325
+ /** Highest emitted `seq` (§9). 0 if no events yet. */
326
+ lastSeq: integer("last_seq").notNull().default(0),
327
+ /** Optional client-supplied key for resume-by-rerequest (§6.3.2). */
328
+ idempotencyKey: text("idempotency_key"),
329
+ /** ISO-8601. */
330
+ startedAt: text("started_at")
331
+ .notNull()
332
+ .default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
333
+ /** ISO-8601 — set when the turn enters a terminal state. */
334
+ finishedAt: text("finished_at"),
335
+ });
336
+ /* ────────────────────────────────────────────────────────────────────────── */
337
+ /* turn_event */
338
+ /* ────────────────────────────────────────────────────────────────────────── */
339
+ /**
340
+ * Append-only log of SSE events per turn for `/chat/{turn_id}/events`
341
+ * resumption. Composite PK = `(turn_id, seq)`.
342
+ *
343
+ * @see SPECIFICATIONS.md §11
344
+ * @see SPECIFICATIONS.md §6.6 — state machine and SSE
345
+ */
346
+ export const turnEvent = sqliteTable("turn_event", {
347
+ /** Parent turn. Cascade on parent delete (rare in practice — turns persist). */
348
+ turnId: text("turn_id")
349
+ .notNull()
350
+ .references(() => turn.id, { onDelete: "cascade" }),
351
+ /** Monotonic per-turn (§9). 0-based. */
352
+ seq: integer("seq").notNull(),
353
+ /** Discriminator from the §9 wire protocol. */
354
+ eventName: text("event_name").notNull(),
355
+ /** JSON-serialized payload. Written via `JSON.stringify` at the handler layer. */
356
+ data: text("data", { mode: "json" }).notNull(),
357
+ /** `(status, phase)` snapshot at emission. */
358
+ state: text("state", { mode: "json" }).notNull(),
359
+ /** ISO-8601 timestamp. */
360
+ createdAt: text("created_at")
361
+ .notNull()
362
+ .default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
363
+ }, (t) => ({
364
+ pk: primaryKey({ columns: [t.turnId, t.seq] }),
365
+ }));
366
+ /* ────────────────────────────────────────────────────────────────────────── */
367
+ /* admin_setting */
368
+ /* ────────────────────────────────────────────────────────────────────────── */
369
+ /**
370
+ * Singleton row holding deployment-wide settings.
371
+ *
372
+ * `id` is a fixed integer (= 1) so the singleton constraint is enforceable
373
+ * via PK. The encryption key (`AI_HUB_ENCRYPTION_KEY`) is **not** a column —
374
+ * it lives in env only (§6.8 / §12.1).
375
+ *
376
+ * Bootstrap-only env semantics for `service_token_signing_key` and
377
+ * `metrics_scrape_token` are documented in §6.7 / §6.10 / §12.1: on first
378
+ * boot, env values seed these columns; after that, env is no longer
379
+ * consulted. Rotation lives at `/admin/settings/rotate-{signing-key,
380
+ * metrics-token}`.
381
+ *
382
+ * @see SPECIFICATIONS.md §11
383
+ * @see SPECIFICATIONS.md §6.10 — `/metrics` bearer auth
384
+ * @see SPECIFICATIONS.md §6.7 — single signing key
385
+ * @see SPECIFICATIONS.md §12.1 — env vars
386
+ */
387
+ export const adminSetting = sqliteTable("admin_setting", {
388
+ /** Always 1 — singleton row. */
389
+ id: integer("id").notNull().primaryKey(),
390
+ /** AES-256 encrypted Letta API key. Encrypted under env `AI_HUB_ENCRYPTION_KEY`. */
391
+ lettaApiKeyEncrypted: blob("letta_api_key_encrypted", { mode: "buffer" }),
392
+ /** Letta server base URL (e.g. `http://localhost:8283/v1`). */
393
+ lettaBaseUrl: text("letta_base_url"),
394
+ /** Optional super-admin override for the "Open Letta ADE" link target (§6.3.4). */
395
+ lettaAdeUrl: text("letta_ade_url"),
396
+ /** Flat JSON blob of UI feature toggles per §8.4. Defaults to `{}`. */
397
+ featureToggles: text("feature_toggles", { mode: "json" }).notNull().default("{}"),
398
+ /** When false, the deployment is single-theme (§7.6). */
399
+ themeSwitcherEnabled: integer("theme_switcher_enabled", { mode: "boolean" })
400
+ .notNull()
401
+ .default(true),
402
+ /** `"light"` | `"dark"`. Initial theme for new users (§7.6). */
403
+ themeDefault: text("theme_default", { enum: ["light", "dark"] })
404
+ .notNull()
405
+ .default("light"),
406
+ /**
407
+ * JWT signing key. Bootstrap-seeded from env `SERVICE_TOKEN_SIGNING_KEY` on
408
+ * first boot if set, else random. Authoritative after first boot — rotate
409
+ * via `/admin/settings/rotate-signing-key` (§6.7).
410
+ */
411
+ serviceTokenSigningKey: blob("service_token_signing_key", { mode: "buffer" }),
412
+ /**
413
+ * 32-byte hex token used by `/metrics` (§6.10). Bootstrap-seeded from env
414
+ * `METRICS_SCRAPE_TOKEN` if set, else random + logged once on stdout.
415
+ */
416
+ metricsScrapeToken: text("metrics_scrape_token"),
417
+ /** Last rotation timestamp; surfaced in the metrics-token settings section. */
418
+ metricsScrapeTokenRotatedAt: text("metrics_scrape_token_rotated_at"),
419
+ /** Last rotation timestamp; surfaced in the signing-key settings section. */
420
+ signingKeyRotatedAt: text("signing_key_rotated_at"),
421
+ /** ISO-8601 timestamp. */
422
+ updatedAt: text("updated_at")
423
+ .notNull()
424
+ .default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
425
+ });
426
+ /* ────────────────────────────────────────────────────────────────────────── */
427
+ /* message_override */
428
+ /* ────────────────────────────────────────────────────────────────────────── */
429
+ /**
430
+ * Per-conversation, per-position **two-layer** content overrides for the
431
+ * messages rendered by `GET /sessions/:id/messages` and (forward-looking)
432
+ * the LLM-visible content at that position.
433
+ *
434
+ * **Why two layers.** Every message has two viewpoints that can legitimately
435
+ * diverge in tier-3 customizations:
436
+ *
437
+ * - **system view** (`system_override`) — what the LLM saw / should see at
438
+ * this position. Today this column is *stored but inert at read time* —
439
+ * we keep it because Letta owns its own conversation history and we
440
+ * can't actually rewrite past rows it persisted. It exists for two
441
+ * forward-looking purposes: audit/debug tooling that wants to know
442
+ * what the operator intends the LLM context to look like, and a future
443
+ * context-replay feature.
444
+ *
445
+ * - **user view** (`user_override`) — what the human sees in the chat
446
+ * transcript at this position. This is what the messages-list route
447
+ * substitutes for display.
448
+ *
449
+ * Both columns are nullable; at least one must be non-null per a CHECK
450
+ * constraint (enforced in the migration SQL). The "set just one column"
451
+ * shape lets one writer set the user-facing view without clobbering an
452
+ * earlier-set system-facing one and vice-versa.
453
+ *
454
+ * **Why position-keyed instead of id-keyed.** Letta rotates the
455
+ * `system_message.id` on every prompt recompile, so id-based keys would
456
+ * go stale. The chronologically-sorted index into
457
+ * `listMessages(conversation_id)` is stable across recompiles.
458
+ *
459
+ * **Lifecycle / writers.**
460
+ *
461
+ * - The `POST /chat` post-turn writer creates a row with `user_override`
462
+ * populated (and `system_override` left null) when the request body
463
+ * carried a `user_override` parameter AND the turn terminated `done`.
464
+ * It runs immediately after the turn-end event, at which point we know
465
+ * the user message persisted and no turn is active.
466
+ *
467
+ * - `PUT /sessions/:id/message-overrides/:position` is the manual writer
468
+ * for past messages — operator / SDK callers. Gated by two checks: the
469
+ * position must already exist in Letta's message list, and no turn for
470
+ * the session may be live. Merge semantics: only the columns the body
471
+ * provided are written; an existing column on the other layer is
472
+ * preserved.
473
+ *
474
+ * @see SPECIFICATIONS.md §6.3.1 — sessions API
475
+ * @see SPECIFICATIONS.md §6.4 — Letta integration boundary
476
+ */
477
+ export const messageOverride = sqliteTable("message_override", {
478
+ /** Letta conversation id this override applies within. */
479
+ conversationId: text("conversation_id").notNull(),
480
+ /**
481
+ * 0-based index into the chronologically-sorted message list returned
482
+ * by `LettaClient.listMessages(conversationId)`. Stable across
483
+ * system-prompt recompiles — only the `system_message` id rotates,
484
+ * not its position.
485
+ */
486
+ ordinalPosition: integer("ordinal_position").notNull(),
487
+ /**
488
+ * What the LLM saw / should see at this position. Stored but inert
489
+ * at display time today (Letta owns its own history). Tomorrow:
490
+ * audit/debug rendering, context-replay features. NULL when only the
491
+ * `user_override` layer was set.
492
+ */
493
+ systemOverride: text("system_override"),
494
+ /**
495
+ * What the human sees in the chat transcript at this position.
496
+ * Substituted verbatim into the wire `content` field by the
497
+ * messages-list route. Empty string is valid — it hides the message
498
+ * visually but keeps the bubble slot (use DELETE to remove the slot
499
+ * entirely). NULL when only the `system_override` layer was set.
500
+ */
501
+ userOverride: text("user_override"),
502
+ /** ISO-8601 timestamp. */
503
+ createdAt: text("created_at")
504
+ .notNull()
505
+ .default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
506
+ /** ISO-8601 timestamp. */
507
+ updatedAt: text("updated_at")
508
+ .notNull()
509
+ .default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
510
+ }, (t) => ({
511
+ pk: primaryKey({ columns: [t.conversationId, t.ordinalPosition] }),
512
+ }));
513
+ /* ────────────────────────────────────────────────────────────────────────── */
514
+ /* message_usage */
515
+ /* ────────────────────────────────────────────────────────────────────────── */
516
+ /**
517
+ * Provider-reported per-turn token usage, stamped on the LAST assistant
518
+ * message of each turn. Surfaced to the chat UI via a hover badge so
519
+ * users can see (a) total tokens consumed and (b) prompt-cache
520
+ * hit/miss split (Anthropic + OpenAI + Gemini all report
521
+ * `cached_input_tokens`; Anthropic also reports `cache_write_tokens`).
522
+ *
523
+ * The runner fetches Letta's `GET /v1/runs/{run_id}/messages` after
524
+ * the turn ends, finds the `usage_statistics`-typed message, and
525
+ * inserts one row here keyed by the last assistant message's Letta id.
526
+ *
527
+ * @see SPECIFICATIONS.md §9 — `usage` SSE event
528
+ * @see docs/server/chat-sse.md — wire protocol
529
+ */
530
+ export const messageUsage = sqliteTable("message_usage", {
531
+ /**
532
+ * Letta message id this usage belongs to — typically the LAST
533
+ * assistant message of the turn that produced it.
534
+ */
535
+ lettaMessageId: text("letta_message_id").notNull().primaryKey(),
536
+ /** Owning session; cascade-deletes when the session is hard-deleted. */
537
+ sessionId: text("session_id")
538
+ .notNull()
539
+ .references(() => session.id, { onDelete: "cascade" }),
540
+ /** Total non-cached prompt tokens (input). */
541
+ promptTokens: integer("prompt_tokens"),
542
+ /** Completion tokens (output). */
543
+ completionTokens: integer("completion_tokens"),
544
+ /** Sum reported by the provider — may differ from prompt+completion when reasoning is counted separately. */
545
+ totalTokens: integer("total_tokens"),
546
+ /** Prompt-cache HIT tokens. Anthropic / OpenAI / Gemini all report. */
547
+ cachedInputTokens: integer("cached_input_tokens"),
548
+ /** Prompt-cache WRITE tokens. Anthropic only — null elsewhere. */
549
+ cacheWriteTokens: integer("cache_write_tokens"),
550
+ /** Reasoning/thinking tokens. Anthropic + OpenAI o-series. */
551
+ reasoningTokens: integer("reasoning_tokens"),
552
+ /** Rolling context-window size estimate post-turn (Letta-side). */
553
+ contextTokens: integer("context_tokens"),
554
+ /** Optional Letta run id for traceback. */
555
+ runId: text("run_id"),
556
+ /** Stamped at insert time. */
557
+ createdAt: text("created_at")
558
+ .notNull()
559
+ .default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
560
+ }, (t) => ({
561
+ /** Per-session lookups when rendering /sessions/{id}/messages. */
562
+ sessionIdx: index("message_usage_session_idx").on(t.sessionId),
563
+ }));
564
+ /* ────────────────────────────────────────────────────────────────────────── */
565
+ /* rotating_message */
566
+ /* ────────────────────────────────────────────────────────────────────────── */
567
+ /**
568
+ * Admin-curated **rotating starting message** — a prompt that ai-hub runs
569
+ * against a Letta agent on a schedule, capturing the agent's reply as a
570
+ * *pre-prepared conversation* that refreshes automatically.
571
+ *
572
+ * This is an automated, self-refreshing evolution of `agent.fork_source_conversation_id`
573
+ * (static base-conversation forking). Instead of an operator hand-authoring a
574
+ * base conversation in the ADE and pasting its id, a rotation runner:
575
+ *
576
+ * 1. `createConversation(letta_agent_id)`,
577
+ * 2. sends `prompt` (drained to completion via the normal streaming path),
578
+ * 3. deletes the prompt's `user_message` so the conversation opens with the
579
+ * agent's reply,
580
+ * 4. stores the resulting conversation id in `current_conversation_id`.
581
+ *
582
+ * The row's `id` (a `rmsg-…` token) is the identifier callers reference
583
+ * anywhere a conversation is referenced. Today that's the agent's **Base
584
+ * conversation ID** field: an `agent.fork_source_conversation_id` of
585
+ * `rmsg-<id>` resolves (at `POST /sessions`) to this row's
586
+ * `current_conversation_id`, which is then forked like any static base.
587
+ *
588
+ * **Same-agent fork constraint.** Letta's fork API requires the new
589
+ * conversation to share the source's agent, so a rotating message can only
590
+ * back ai-hub agents whose `letta_agent_id` equals this row's
591
+ * `letta_agent_id`. The admin agents validator enforces this at write time
592
+ * (mirrors the static `fork_source_agent_mismatch` check).
593
+ *
594
+ * **Resilience.** A failed rotation never blanks a working
595
+ * `current_conversation_id` — the runner keeps the last-good value, records
596
+ * `last_status = 'error'` + `last_error`, and retries on the next interval.
597
+ *
598
+ * @see docs/server/rotating-messages.md
599
+ */
600
+ export const rotatingMessage = sqliteTable("rotating_message", {
601
+ /** Stable PK in `rmsg-<uuid>` form — also the public reference token. */
602
+ id: text("id").notNull().primaryKey(),
603
+ /** Operator-facing display name. */
604
+ displayName: text("display_name").notNull(),
605
+ /** The prompt sent to the agent each rotation. */
606
+ prompt: text("prompt").notNull(),
607
+ /**
608
+ * Raw Letta agent id the prompt runs against. Validated via
609
+ * `client.agents.retrieve` at admin-write time. Must equal the
610
+ * `letta_agent_id` of any ai-hub agent that references this row.
611
+ */
612
+ lettaAgentId: text("letta_agent_id").notNull(),
613
+ /**
614
+ * Schedule discriminator:
615
+ * - `"every"` — fixed interval; `interval_seconds` is authoritative.
616
+ * - `"daily"` — once per day at `daily_time` (`"HH:MM"`, server-local).
617
+ * - `"cron"` — `cron_expression` (5-field) parsed via `cron-parser`.
618
+ */
619
+ scheduleKind: text("schedule_kind", {
620
+ enum: ["every", "daily", "cron"],
621
+ }).notNull(),
622
+ /** Period in seconds for `schedule_kind = 'every'`. NULL otherwise. */
623
+ intervalSeconds: integer("interval_seconds"),
624
+ /** `"HH:MM"` (24h, server-local) for `schedule_kind = 'daily'`. NULL otherwise. */
625
+ dailyTime: text("daily_time"),
626
+ /** 5-field cron expression for `schedule_kind = 'cron'`. NULL otherwise. */
627
+ cronExpression: text("cron_expression"),
628
+ /**
629
+ * The current pre-prepared, forkable Letta conversation id. NULL until the
630
+ * first successful rotation; kept at its last-good value across failures.
631
+ */
632
+ currentConversationId: text("current_conversation_id"),
633
+ /** Outcome of the most recent rotation attempt. */
634
+ lastStatus: text("last_status", {
635
+ enum: ["pending", "ok", "error"],
636
+ })
637
+ .notNull()
638
+ .default("pending"),
639
+ /** Human-readable error from the last failed rotation. NULL when ok/pending. */
640
+ lastError: text("last_error"),
641
+ /** ISO-8601 timestamp of the last successful rotation. NULL until first success. */
642
+ lastRotatedAt: text("last_rotated_at"),
643
+ /**
644
+ * ISO-8601 timestamp the scheduler compares against `now` to decide when to
645
+ * rotate next. Set to "now" on create (so the first rotation fires on the
646
+ * next tick) and recomputed after every attempt.
647
+ */
648
+ nextRotationAt: text("next_rotation_at")
649
+ .notNull()
650
+ .default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
651
+ /** Soft-hide flag — archived rows are skipped by the scheduler + rejected as fork sources. */
652
+ isArchived: integer("is_archived", { mode: "boolean" }).notNull().default(false),
653
+ /** ISO-8601 timestamp. */
654
+ createdAt: text("created_at")
655
+ .notNull()
656
+ .default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
657
+ /** ISO-8601 timestamp. */
658
+ updatedAt: text("updated_at")
659
+ .notNull()
660
+ .default(sql `(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
661
+ });
662
+ //# sourceMappingURL=schema.js.map