@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,177 @@
1
+ /**
2
+ * Seeds Letta Code's global agent-settings file (`~/.letta/settings.json`)
3
+ * with `{toolset: "none"}` for each ai-hub-registered Letta agent.
4
+ *
5
+ * **Why this matters.** Letta Code's bundled CLI auto-attaches a "toolset"
6
+ * (a set of file-edit / shell / search tools — `Read`, `Write`, `Edit`,
7
+ * `Bash`, etc.) to every agent at session start, based on the model handle.
8
+ * ai-hub doesn't want those tools — we want only the tools the ADE admin
9
+ * explicitly attached to the agent. The CLI honors a per-agent
10
+ * `toolset: "none"` preference stored in its global settings file, which
11
+ * suppresses the entire auto-toolset.
12
+ *
13
+ * **Why global, not project-local.** Letta Code's `SettingsManager` reads
14
+ * the toolset preference from `~/.letta/settings.json` (the global file),
15
+ * not `<cwd>/.letta/settings.local.json` (the project file). The global
16
+ * settings carry agent records keyed by `(agentId, normalizedBaseUrl)`
17
+ * where `normalizedBaseUrl` is the value of `LETTA_BASE_URL` with the
18
+ * scheme and trailing slash stripped, or `undefined` when the agent lives
19
+ * on Letta Cloud. Self-hosted Letta deployments always have a non-default
20
+ * `LETTA_BASE_URL` so the agent record always has the `baseUrl` field set.
21
+ *
22
+ * The `LETTA_CLI_PATH` shim (which injects `--tools ""`) is the primary
23
+ * mechanism that forces an empty tool registry; this settings-file seed
24
+ * is the defense-in-depth fallback the TUI also honors.
25
+ */
26
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
27
+ import { homedir } from "node:os";
28
+ import { dirname, join } from "node:path";
29
+ /* ────────────────────────────────────────────────────────────────────────── */
30
+ /* Path resolution */
31
+ /* ────────────────────────────────────────────────────────────────────────── */
32
+ /**
33
+ * Returns the absolute path to Letta Code's global settings file. Tests
34
+ * override `HOME` to point at a tmp dir.
35
+ *
36
+ * @returns the path. (Doesn't check existence — caller does.)
37
+ */
38
+ export function resolveGlobalSettingsPath() {
39
+ return join(homedir(), ".letta", "settings.json");
40
+ }
41
+ /**
42
+ * Normalizes `LETTA_BASE_URL` to match the key Letta Code's CLI uses to
43
+ * look up agent settings AFTER ai-hub-server's `bootstrap.ts` runs its
44
+ * own `/v1` stripping. The CLI itself only strips scheme + trailing
45
+ * slash (so `http://host:p/v1` → `host:p/v1`), but bootstrap.ts mutates
46
+ * `process.env.LETTA_BASE_URL` at boot to drop the `/v1` (because the
47
+ * Letta Client SDK that the CLI uses internally appends `/v1` itself —
48
+ * leaving it in the env produces double-`/v1` requests that 404).
49
+ *
50
+ * So by the time the spawned CLI looks up the agent settings, its
51
+ * `getCurrentServerKey` reads `http://host:p` from env and produces
52
+ * `host:p`. The settings file entry must use the same key — hence the
53
+ * extra `/v1` strip here. If `bootstrap.ts` changes its policy, this
54
+ * helper has to change in lockstep.
55
+ *
56
+ * Returns `undefined` for the Letta Cloud default URL (so the agent
57
+ * record omits the `baseUrl` field, matching Letta Code's behavior on
58
+ * Cloud).
59
+ *
60
+ * @param baseUrl - raw `LETTA_BASE_URL` value.
61
+ * @returns the normalized form, or `undefined` for Letta Cloud.
62
+ */
63
+ export function normalizeLettaBaseUrl(baseUrl) {
64
+ if (!baseUrl)
65
+ return undefined;
66
+ const stripped = baseUrl
67
+ .replace(/^https?:\/\//, "")
68
+ .replace(/\/v1\/?$/i, "")
69
+ .replace(/\/$/, "");
70
+ if (stripped === "api.letta.com")
71
+ return undefined;
72
+ return stripped;
73
+ }
74
+ /* ────────────────────────────────────────────────────────────────────────── */
75
+ /* Read + write */
76
+ /* ────────────────────────────────────────────────────────────────────────── */
77
+ /**
78
+ * Reads the settings file from disk. Returns an empty object if the file
79
+ * doesn't exist or is unparseable — both cases are recoverable by an
80
+ * idempotent upsert that recreates the file.
81
+ *
82
+ * @param path - the resolved settings path.
83
+ * @returns the parsed settings (or `{}`).
84
+ */
85
+ export function readSettingsFile(path) {
86
+ if (!existsSync(path))
87
+ return {};
88
+ try {
89
+ const raw = readFileSync(path, "utf8");
90
+ if (!raw.trim())
91
+ return {};
92
+ const parsed = JSON.parse(raw);
93
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
94
+ return parsed;
95
+ }
96
+ return {};
97
+ }
98
+ catch {
99
+ // Malformed file (concurrent write, disk corruption) — treat as empty
100
+ // so the seed proceeds. Worst case the operator's other settings (if
101
+ // any) are clobbered, which is acceptable when the file was already
102
+ // broken.
103
+ return {};
104
+ }
105
+ }
106
+ /**
107
+ * Writes the settings file atomically: write to `<path>.tmp` then rename.
108
+ * The rename is atomic on every filesystem the ai-hub-server supports
109
+ * (ext4, xfs, btrfs, apfs). Creates `~/.letta/` if missing.
110
+ *
111
+ * @param path - the resolved settings path.
112
+ * @param settings - the new contents.
113
+ */
114
+ export function writeSettingsFile(path, settings) {
115
+ mkdirSync(dirname(path), { recursive: true });
116
+ const tmp = `${path}.tmp`;
117
+ writeFileSync(tmp, JSON.stringify(settings, null, 2), { encoding: "utf8", mode: 0o600 });
118
+ renameSync(tmp, path);
119
+ }
120
+ /**
121
+ * Idempotent upsert of `{toolset: "none"}` for a single agent in the
122
+ * global settings file.
123
+ *
124
+ * Matching is by `(agentId, baseUrl)` — same composite key Letta Code
125
+ * uses. If the agent record exists with a different toolset (`"auto"`,
126
+ * `"codex"`, etc.), it's replaced. If it exists already with `"none"`,
127
+ * we no-op (don't churn the file's mtime).
128
+ *
129
+ * @param opts.agentId - the Letta agent id (e.g. `agent-xxx`).
130
+ * @param opts.lettaBaseUrl - the raw `LETTA_BASE_URL` value (so the
131
+ * helper can normalize it the same way Letta Code does). Pass
132
+ * `process.env.LETTA_BASE_URL` from the caller.
133
+ * @param opts.settingsPath - override the settings-file path. Defaults to
134
+ * `~/.letta/settings.json`. Tests inject a tmp path.
135
+ * @param opts.logger - optional pino logger for observability lines.
136
+ * @returns the outcome (created / updated / noop) so the caller can log.
137
+ */
138
+ export function seedAgentToolsetNone(opts) {
139
+ const path = opts.settingsPath ?? resolveGlobalSettingsPath();
140
+ const normalizedBaseUrl = normalizeLettaBaseUrl(opts.lettaBaseUrl);
141
+ const settings = readSettingsFile(path);
142
+ const agents = Array.isArray(settings.agents) ? [...settings.agents] : [];
143
+ const matchIdx = agents.findIndex((a) => a.agentId === opts.agentId && (a.baseUrl ?? undefined) === normalizedBaseUrl);
144
+ let outcome;
145
+ if (matchIdx >= 0) {
146
+ const existing = agents[matchIdx];
147
+ const prev = existing.toolset;
148
+ if (prev === "none") {
149
+ outcome = { kind: "noop", previousToolset: "none" };
150
+ opts.logger?.debug({ agentId: opts.agentId, baseUrl: normalizedBaseUrl }, "letta_code_settings_seed_noop");
151
+ return outcome;
152
+ }
153
+ agents[matchIdx] = { ...existing, toolset: "none" };
154
+ outcome = { kind: "updated", previousToolset: prev };
155
+ }
156
+ else {
157
+ const entry = {
158
+ agentId: opts.agentId,
159
+ toolset: "none",
160
+ };
161
+ if (normalizedBaseUrl !== undefined)
162
+ entry.baseUrl = normalizedBaseUrl;
163
+ agents.push(entry);
164
+ outcome = { kind: "created", previousToolset: undefined };
165
+ }
166
+ const next = { ...settings, agents };
167
+ writeSettingsFile(path, next);
168
+ opts.logger?.info({
169
+ agentId: opts.agentId,
170
+ baseUrl: normalizedBaseUrl,
171
+ outcome: outcome.kind,
172
+ previousToolset: outcome.previousToolset,
173
+ settingsPath: path,
174
+ }, "letta_code_settings_seed");
175
+ return outcome;
176
+ }
177
+ //# sourceMappingURL=settingsFileSeed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settingsFileSeed.js","sourceRoot":"","sources":["../../src/letta/settingsFileSeed.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Dynamic RAM-based concurrent-spawn throttle for Letta Code subprocesses.
3
+ *
4
+ * Every time we're about to spawn a Letta Code CLI subprocess (which happens
5
+ * once per chat turn — see `letta/client.ts` → `streamUserMessage`), we call
6
+ * `acquire()` first. If the host's *available* RAM has dropped below a
7
+ * configured threshold OR we've already got the configured hard-cap of live
8
+ * subprocesses, `acquire()` throws `ServerOverloadedError`. The chat route
9
+ * catches that and returns HTTP 503 with the `server_overloaded` SSE error
10
+ * event so the React client can surface a "server is busy" banner.
11
+ *
12
+ * **Why MemAvailable, not MemFree.** Linux uses unused RAM as page cache;
13
+ * `os.freemem()` / `/proc/meminfo`'s `MemFree` line reports memory that's
14
+ * *literally* unallocated, which on any healthy server is misleadingly low
15
+ * (often ~5% even when ~50% is actually allocatable without swapping).
16
+ * `/proc/meminfo`'s `MemAvailable` line is the kernel's own estimate of
17
+ * "memory that could be allocated without entering swap" — that's what we
18
+ * compare against the threshold.
19
+ *
20
+ * **Why two gates and not one.** The RAM gate is the primary throttle and
21
+ * adapts to whatever EC2 instance the deployment runs on. The hard-cap is a
22
+ * circuit breaker: if our `/proc/meminfo` math is ever wrong (kernel reports
23
+ * stale numbers under load, container cgroup limits trip earlier than host
24
+ * MemAvailable suggests, …), an unbounded subprocess pool would still be
25
+ * able to OOM-kill the box. The hard-cap is the floor on how bad it can get.
26
+ *
27
+ * @see ../../docs/operations/env-vars.md
28
+ */
29
+ /** Default minimum percentage of total RAM that must be available before we'll
30
+ * spawn a new subprocess. Overridable via env. */
31
+ export declare const DEFAULT_RAM_THRESHOLD_PCT = 20;
32
+ /** Default hard cap on concurrent live subprocesses. Overridable via env. */
33
+ export declare const DEFAULT_HARD_CAP = 100;
34
+ /**
35
+ * Resolved throttle config. Pulled from env at construction time; tests pass
36
+ * an explicit one to bypass env reading.
37
+ */
38
+ export interface ThrottleConfig {
39
+ /** Minimum % of total RAM that must remain available to accept a spawn. */
40
+ ramThresholdPct: number;
41
+ /** Maximum concurrent live subprocesses, regardless of RAM. */
42
+ hardCap: number;
43
+ }
44
+ /**
45
+ * Reads throttle config from the process environment, with defaults.
46
+ *
47
+ * Env vars:
48
+ * - `AI_HUB_SUBPROCESS_RAM_THRESHOLD_PCT` — integer 0..100. Defaults to 20.
49
+ * A value <= 0 disables the RAM gate (every acquire passes the RAM check).
50
+ * A value >= 100 means every acquire fails the RAM check (useful for
51
+ * testing the overload path).
52
+ * - `AI_HUB_SUBPROCESS_HARD_CAP` — positive integer. Defaults to 100.
53
+ *
54
+ * @returns the parsed config bundle.
55
+ */
56
+ export declare function readThrottleConfigFromEnv(): ThrottleConfig;
57
+ /**
58
+ * Reader for the host's MemAvailable + MemTotal. Returns kilobytes (matches
59
+ * `/proc/meminfo` units). Tests inject a fake reader so they don't depend on
60
+ * the host's actual memory state.
61
+ */
62
+ export interface MemReader {
63
+ /** @returns `{availableKb, totalKb}` snapshot at the moment of call. */
64
+ read(): {
65
+ availableKb: number;
66
+ totalKb: number;
67
+ };
68
+ }
69
+ /**
70
+ * Default reader — parses `/proc/meminfo` directly. Falls back to
71
+ * `os.totalmem()` for `totalKb` when /proc is unreadable (non-Linux), and
72
+ * returns `MemTotal` as `availableKb` in that case so the RAM gate is
73
+ * effectively disabled there (the hard-cap still applies).
74
+ *
75
+ * This file lives at a stable kernel-exposed path on every Linux host
76
+ * (including in Docker without any special mounts) so no privileges are
77
+ * needed.
78
+ *
79
+ * @see https://www.kernel.org/doc/Documentation/filesystems/proc.txt — the
80
+ * meminfo section documents MemAvailable's semantics ("estimate of how
81
+ * much memory is available for starting new applications, without
82
+ * swapping").
83
+ */
84
+ export declare const procMeminfoReader: MemReader;
85
+ /**
86
+ * Typed error thrown by `SubprocessThrottle.acquire()` when the host is too
87
+ * loaded to accept another subprocess. The chat route catches this and
88
+ * returns HTTP 503 with an `error {server_overloaded}` SSE event.
89
+ */
90
+ export declare class ServerOverloadedError extends Error {
91
+ /** Stable error code for the SSE wire protocol's `error.code` field. */
92
+ readonly code: "server_overloaded";
93
+ /** Which gate tripped — useful for logs + the eventual operator playbook. */
94
+ readonly gate: "ram" | "hard_cap";
95
+ /** Snapshot of the trigger at the moment we rejected. */
96
+ readonly detail: {
97
+ /** MemAvailable as a fraction of MemTotal at the time of rejection. */
98
+ availablePct: number;
99
+ /** Live subprocess count at the time of rejection. */
100
+ liveCount: number;
101
+ /** Configured RAM threshold (percent). */
102
+ ramThresholdPct: number;
103
+ /** Configured hard cap. */
104
+ hardCap: number;
105
+ };
106
+ constructor(gate: "ram" | "hard_cap", detail: ServerOverloadedError["detail"]);
107
+ }
108
+ /**
109
+ * A leased slot in the throttle's live-subprocess counter. Returned by
110
+ * `acquire()`; callers must call `release()` exactly once (use try/finally).
111
+ */
112
+ export interface ThrottleLease {
113
+ /** Releases the slot back to the throttle. Idempotent — calling twice
114
+ * is a no-op. */
115
+ release(): void;
116
+ }
117
+ /**
118
+ * The throttle.
119
+ *
120
+ * Construct one per ai-hub-server process (it's stateful — the live-count
121
+ * counter is in-memory). Production wires this into `AIHubServerContext` via
122
+ * `createFetchHandler`; tests pass a fake `MemReader` to drive the RAM gate
123
+ * deterministically.
124
+ */
125
+ export declare class SubprocessThrottle {
126
+ readonly config: ThrottleConfig;
127
+ private readonly memReader;
128
+ private liveCount;
129
+ /**
130
+ * @param config - resolved throttle config (use `readThrottleConfigFromEnv`).
131
+ * @param memReader - mem reader (defaults to /proc/meminfo).
132
+ */
133
+ constructor(config: ThrottleConfig, memReader?: MemReader);
134
+ /**
135
+ * Attempts to acquire a slot. Throws `ServerOverloadedError` synchronously
136
+ * if either gate trips; otherwise increments the live counter and returns
137
+ * a lease the caller must release when the subprocess exits.
138
+ *
139
+ * The acquire-then-spawn dance has a small race: between the RAM check
140
+ * here and the actual spawn, another concurrent acquire could land. That's
141
+ * acceptable for v1 — the next acquire will see the updated MemAvailable
142
+ * and the hard cap is the absolute backstop.
143
+ *
144
+ * @returns a lease the caller must `.release()` in finally.
145
+ * @throws `ServerOverloadedError` if either gate trips.
146
+ */
147
+ acquire(): ThrottleLease;
148
+ /** @returns the current live-subprocess count (for observability + tests). */
149
+ get currentLiveCount(): number;
150
+ }
151
+ //# sourceMappingURL=subprocessThrottle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subprocessThrottle.d.ts","sourceRoot":"","sources":["../../src/letta/subprocessThrottle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH;mDACmD;AACnD,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAC5C,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,eAAe,EAAE,MAAM,CAAC;IACxB,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,IAAI,cAAc,CAO1D;AAaD;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,wEAAwE;IACxE,IAAI,IAAI;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAClD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,EAAE,SAe/B,CAAC;AAMF;;;;GAIG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAG,mBAAmB,CAAU;IAC7C,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,UAAU,CAAC;IAClC,yDAAyD;IACzD,QAAQ,CAAC,MAAM,EAAE;QACf,uEAAuE;QACvE,YAAY,EAAE,MAAM,CAAC;QACrB,sDAAsD;QACtD,SAAS,EAAE,MAAM,CAAC;QAClB,0CAA0C;QAC1C,eAAe,EAAE,MAAM,CAAC;QACxB,2BAA2B;QAC3B,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;gBAEU,IAAI,EAAE,KAAK,GAAG,UAAU,EAAE,MAAM,EAAE,qBAAqB,CAAC,QAAQ,CAAC;CAU9E;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B;sBACkB;IAClB,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,qBAAa,kBAAkB;IAC7B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,SAAS,CAAK;IAEtB;;;OAGG;gBACS,MAAM,EAAE,cAAc,EAAE,SAAS,GAAE,SAA6B;IAK5E;;;;;;;;;;;;OAYG;IACH,OAAO,IAAI,aAAa;IAoCxB,8EAA8E;IAC9E,IAAI,gBAAgB,IAAI,MAAM,CAE7B;CACF"}
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Dynamic RAM-based concurrent-spawn throttle for Letta Code subprocesses.
3
+ *
4
+ * Every time we're about to spawn a Letta Code CLI subprocess (which happens
5
+ * once per chat turn — see `letta/client.ts` → `streamUserMessage`), we call
6
+ * `acquire()` first. If the host's *available* RAM has dropped below a
7
+ * configured threshold OR we've already got the configured hard-cap of live
8
+ * subprocesses, `acquire()` throws `ServerOverloadedError`. The chat route
9
+ * catches that and returns HTTP 503 with the `server_overloaded` SSE error
10
+ * event so the React client can surface a "server is busy" banner.
11
+ *
12
+ * **Why MemAvailable, not MemFree.** Linux uses unused RAM as page cache;
13
+ * `os.freemem()` / `/proc/meminfo`'s `MemFree` line reports memory that's
14
+ * *literally* unallocated, which on any healthy server is misleadingly low
15
+ * (often ~5% even when ~50% is actually allocatable without swapping).
16
+ * `/proc/meminfo`'s `MemAvailable` line is the kernel's own estimate of
17
+ * "memory that could be allocated without entering swap" — that's what we
18
+ * compare against the threshold.
19
+ *
20
+ * **Why two gates and not one.** The RAM gate is the primary throttle and
21
+ * adapts to whatever EC2 instance the deployment runs on. The hard-cap is a
22
+ * circuit breaker: if our `/proc/meminfo` math is ever wrong (kernel reports
23
+ * stale numbers under load, container cgroup limits trip earlier than host
24
+ * MemAvailable suggests, …), an unbounded subprocess pool would still be
25
+ * able to OOM-kill the box. The hard-cap is the floor on how bad it can get.
26
+ *
27
+ * @see ../../docs/operations/env-vars.md
28
+ */
29
+ import { readFileSync } from "node:fs";
30
+ import { totalmem } from "node:os";
31
+ /* ────────────────────────────────────────────────────────────────────────── */
32
+ /* Config */
33
+ /* ────────────────────────────────────────────────────────────────────────── */
34
+ /** Default minimum percentage of total RAM that must be available before we'll
35
+ * spawn a new subprocess. Overridable via env. */
36
+ export const DEFAULT_RAM_THRESHOLD_PCT = 20;
37
+ /** Default hard cap on concurrent live subprocesses. Overridable via env. */
38
+ export const DEFAULT_HARD_CAP = 100;
39
+ /**
40
+ * Reads throttle config from the process environment, with defaults.
41
+ *
42
+ * Env vars:
43
+ * - `AI_HUB_SUBPROCESS_RAM_THRESHOLD_PCT` — integer 0..100. Defaults to 20.
44
+ * A value <= 0 disables the RAM gate (every acquire passes the RAM check).
45
+ * A value >= 100 means every acquire fails the RAM check (useful for
46
+ * testing the overload path).
47
+ * - `AI_HUB_SUBPROCESS_HARD_CAP` — positive integer. Defaults to 100.
48
+ *
49
+ * @returns the parsed config bundle.
50
+ */
51
+ export function readThrottleConfigFromEnv() {
52
+ const rawPct = process.env.AI_HUB_SUBPROCESS_RAM_THRESHOLD_PCT;
53
+ const rawCap = process.env.AI_HUB_SUBPROCESS_HARD_CAP;
54
+ return {
55
+ ramThresholdPct: parseIntInRange(rawPct, 0, 100, DEFAULT_RAM_THRESHOLD_PCT),
56
+ hardCap: parseIntInRange(rawCap, 1, 10_000, DEFAULT_HARD_CAP),
57
+ };
58
+ }
59
+ function parseIntInRange(raw, min, max, fallback) {
60
+ if (raw === undefined || raw === "")
61
+ return fallback;
62
+ const parsed = Number.parseInt(raw, 10);
63
+ if (!Number.isFinite(parsed) || parsed < min || parsed > max)
64
+ return fallback;
65
+ return parsed;
66
+ }
67
+ /**
68
+ * Default reader — parses `/proc/meminfo` directly. Falls back to
69
+ * `os.totalmem()` for `totalKb` when /proc is unreadable (non-Linux), and
70
+ * returns `MemTotal` as `availableKb` in that case so the RAM gate is
71
+ * effectively disabled there (the hard-cap still applies).
72
+ *
73
+ * This file lives at a stable kernel-exposed path on every Linux host
74
+ * (including in Docker without any special mounts) so no privileges are
75
+ * needed.
76
+ *
77
+ * @see https://www.kernel.org/doc/Documentation/filesystems/proc.txt — the
78
+ * meminfo section documents MemAvailable's semantics ("estimate of how
79
+ * much memory is available for starting new applications, without
80
+ * swapping").
81
+ */
82
+ export const procMeminfoReader = {
83
+ read() {
84
+ try {
85
+ const raw = readFileSync("/proc/meminfo", "utf8");
86
+ const total = raw.match(/^MemTotal:\s+(\d+)\s+kB/m);
87
+ const available = raw.match(/^MemAvailable:\s+(\d+)\s+kB/m);
88
+ if (total && available) {
89
+ return { totalKb: Number(total[1]), availableKb: Number(available[1]) };
90
+ }
91
+ }
92
+ catch {
93
+ // Fall through to the os.totalmem fallback.
94
+ }
95
+ const totalKb = Math.round(totalmem() / 1024);
96
+ return { totalKb, availableKb: totalKb };
97
+ },
98
+ };
99
+ /* ────────────────────────────────────────────────────────────────────────── */
100
+ /* Throttle */
101
+ /* ────────────────────────────────────────────────────────────────────────── */
102
+ /**
103
+ * Typed error thrown by `SubprocessThrottle.acquire()` when the host is too
104
+ * loaded to accept another subprocess. The chat route catches this and
105
+ * returns HTTP 503 with an `error {server_overloaded}` SSE event.
106
+ */
107
+ export class ServerOverloadedError extends Error {
108
+ /** Stable error code for the SSE wire protocol's `error.code` field. */
109
+ code = "server_overloaded";
110
+ /** Which gate tripped — useful for logs + the eventual operator playbook. */
111
+ gate;
112
+ /** Snapshot of the trigger at the moment we rejected. */
113
+ detail;
114
+ constructor(gate, detail) {
115
+ super(gate === "ram"
116
+ ? `Server overloaded: only ${detail.availablePct.toFixed(1)}% RAM available (< ${detail.ramThresholdPct}% required).`
117
+ : `Server overloaded: ${detail.liveCount} live subprocesses (>= ${detail.hardCap} hard cap).`);
118
+ this.name = "ServerOverloadedError";
119
+ this.gate = gate;
120
+ this.detail = detail;
121
+ }
122
+ }
123
+ /**
124
+ * The throttle.
125
+ *
126
+ * Construct one per ai-hub-server process (it's stateful — the live-count
127
+ * counter is in-memory). Production wires this into `AIHubServerContext` via
128
+ * `createFetchHandler`; tests pass a fake `MemReader` to drive the RAM gate
129
+ * deterministically.
130
+ */
131
+ export class SubprocessThrottle {
132
+ config;
133
+ memReader;
134
+ liveCount = 0;
135
+ /**
136
+ * @param config - resolved throttle config (use `readThrottleConfigFromEnv`).
137
+ * @param memReader - mem reader (defaults to /proc/meminfo).
138
+ */
139
+ constructor(config, memReader = procMeminfoReader) {
140
+ this.config = config;
141
+ this.memReader = memReader;
142
+ }
143
+ /**
144
+ * Attempts to acquire a slot. Throws `ServerOverloadedError` synchronously
145
+ * if either gate trips; otherwise increments the live counter and returns
146
+ * a lease the caller must release when the subprocess exits.
147
+ *
148
+ * The acquire-then-spawn dance has a small race: between the RAM check
149
+ * here and the actual spawn, another concurrent acquire could land. That's
150
+ * acceptable for v1 — the next acquire will see the updated MemAvailable
151
+ * and the hard cap is the absolute backstop.
152
+ *
153
+ * @returns a lease the caller must `.release()` in finally.
154
+ * @throws `ServerOverloadedError` if either gate trips.
155
+ */
156
+ acquire() {
157
+ // Hard cap check first — it's cheap and the more deterministic gate.
158
+ if (this.liveCount >= this.config.hardCap) {
159
+ throw new ServerOverloadedError("hard_cap", {
160
+ availablePct: 100, // unknown but irrelevant; the cap gate fired
161
+ liveCount: this.liveCount,
162
+ ramThresholdPct: this.config.ramThresholdPct,
163
+ hardCap: this.config.hardCap,
164
+ });
165
+ }
166
+ // RAM gate. ramThresholdPct=0 disables this gate entirely.
167
+ if (this.config.ramThresholdPct > 0) {
168
+ const { availableKb, totalKb } = this.memReader.read();
169
+ const availablePct = totalKb > 0 ? (availableKb / totalKb) * 100 : 100;
170
+ if (availablePct < this.config.ramThresholdPct) {
171
+ throw new ServerOverloadedError("ram", {
172
+ availablePct,
173
+ liveCount: this.liveCount,
174
+ ramThresholdPct: this.config.ramThresholdPct,
175
+ hardCap: this.config.hardCap,
176
+ });
177
+ }
178
+ }
179
+ this.liveCount++;
180
+ let released = false;
181
+ return {
182
+ release: () => {
183
+ if (released)
184
+ return;
185
+ released = true;
186
+ this.liveCount--;
187
+ },
188
+ };
189
+ }
190
+ /** @returns the current live-subprocess count (for observability + tests). */
191
+ get currentLiveCount() {
192
+ return this.liveCount;
193
+ }
194
+ }
195
+ //# sourceMappingURL=subprocessThrottle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subprocessThrottle.js","sourceRoot":"","sources":["../../src/letta/subprocessThrottle.ts"],"names":[],"mappings":"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"}