@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.
- package/dist/auth/argon2.d.ts +72 -0
- package/dist/auth/argon2.d.ts.map +1 -0
- package/dist/auth/argon2.js +145 -0
- package/dist/auth/argon2.js.map +1 -0
- package/dist/auth/encryption.d.ts +52 -0
- package/dist/auth/encryption.d.ts.map +1 -0
- package/dist/auth/encryption.js +98 -0
- package/dist/auth/encryption.js.map +1 -0
- package/dist/auth/jwt.d.ts +133 -0
- package/dist/auth/jwt.d.ts.map +1 -0
- package/dist/auth/jwt.js +147 -0
- package/dist/auth/jwt.js.map +1 -0
- package/dist/auth/middleware.d.ts +101 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +161 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/passwordPolicy.d.ts +72 -0
- package/dist/auth/passwordPolicy.d.ts.map +1 -0
- package/dist/auth/passwordPolicy.js +88 -0
- package/dist/auth/passwordPolicy.js.map +1 -0
- package/dist/auth/roleRules.d.ts +200 -0
- package/dist/auth/roleRules.d.ts.map +1 -0
- package/dist/auth/roleRules.js +365 -0
- package/dist/auth/roleRules.js.map +1 -0
- package/dist/auth/userView.d.ts +55 -0
- package/dist/auth/userView.d.ts.map +1 -0
- package/dist/auth/userView.js +77 -0
- package/dist/auth/userView.js.map +1 -0
- package/dist/bootstrap.d.ts +104 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +160 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/db/connection.d.ts +39 -0
- package/dist/db/connection.d.ts.map +1 -0
- package/dist/db/connection.js +62 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/migrate.d.ts +31 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +197 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/schema.d.ts +1902 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +662 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/seed.d.ts +92 -0
- package/dist/db/seed.d.ts.map +1 -0
- package/dist/db/seed.js +160 -0
- package/dist/db/seed.js.map +1 -0
- package/dist/handlers/fetch.d.ts +89 -0
- package/dist/handlers/fetch.d.ts.map +1 -0
- package/dist/handlers/fetch.js +173 -0
- package/dist/handlers/fetch.js.map +1 -0
- package/dist/handlers/nextjs.d.ts +40 -0
- package/dist/handlers/nextjs.d.ts.map +1 -0
- package/dist/handlers/nextjs.js +42 -0
- package/dist/handlers/nextjs.js.map +1 -0
- package/dist/handlers/responses.d.ts +46 -0
- package/dist/handlers/responses.d.ts.map +1 -0
- package/dist/handlers/responses.js +74 -0
- package/dist/handlers/responses.js.map +1 -0
- package/dist/handlers/routeTable.d.ts +21 -0
- package/dist/handlers/routeTable.d.ts.map +1 -0
- package/dist/handlers/routeTable.js +133 -0
- package/dist/handlers/routeTable.js.map +1 -0
- package/dist/handlers/router.d.ts +37 -0
- package/dist/handlers/router.d.ts.map +1 -0
- package/dist/handlers/router.js +82 -0
- package/dist/handlers/router.js.map +1 -0
- package/dist/handlers/sse.d.ts +38 -0
- package/dist/handlers/sse.d.ts.map +1 -0
- package/dist/handlers/sse.js +52 -0
- package/dist/handlers/sse.js.map +1 -0
- package/dist/handlers/types.d.ts +96 -0
- package/dist/handlers/types.d.ts.map +1 -0
- package/dist/handlers/types.js +14 -0
- package/dist/handlers/types.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/letta/client.d.ts +355 -0
- package/dist/letta/client.d.ts.map +1 -0
- package/dist/letta/client.js +529 -0
- package/dist/letta/client.js.map +1 -0
- package/dist/letta/eventTranslator.d.ts +166 -0
- package/dist/letta/eventTranslator.d.ts.map +1 -0
- package/dist/letta/eventTranslator.js +532 -0
- package/dist/letta/eventTranslator.js.map +1 -0
- package/dist/letta/index.d.ts +13 -0
- package/dist/letta/index.d.ts.map +1 -0
- package/dist/letta/index.js +12 -0
- package/dist/letta/index.js.map +1 -0
- package/dist/letta/modelSettings.d.ts +176 -0
- package/dist/letta/modelSettings.d.ts.map +1 -0
- package/dist/letta/modelSettings.js +314 -0
- package/dist/letta/modelSettings.js.map +1 -0
- package/dist/letta/promptWrapping.d.ts +141 -0
- package/dist/letta/promptWrapping.d.ts.map +1 -0
- package/dist/letta/promptWrapping.js +213 -0
- package/dist/letta/promptWrapping.js.map +1 -0
- package/dist/letta/sandbox.d.ts +100 -0
- package/dist/letta/sandbox.d.ts.map +1 -0
- package/dist/letta/sandbox.js +151 -0
- package/dist/letta/sandbox.js.map +1 -0
- package/dist/letta/settingsFileSeed.d.ts +146 -0
- package/dist/letta/settingsFileSeed.d.ts.map +1 -0
- package/dist/letta/settingsFileSeed.js +177 -0
- package/dist/letta/settingsFileSeed.js.map +1 -0
- package/dist/letta/subprocessThrottle.d.ts +151 -0
- package/dist/letta/subprocessThrottle.d.ts.map +1 -0
- package/dist/letta/subprocessThrottle.js +195 -0
- package/dist/letta/subprocessThrottle.js.map +1 -0
- package/dist/letta/tpmRecovery.d.ts +202 -0
- package/dist/letta/tpmRecovery.d.ts.map +1 -0
- package/dist/letta/tpmRecovery.js +314 -0
- package/dist/letta/tpmRecovery.js.map +1 -0
- package/dist/letta/types.d.ts +54 -0
- package/dist/letta/types.d.ts.map +1 -0
- package/dist/letta/types.js +16 -0
- package/dist/letta/types.js.map +1 -0
- package/dist/observability/logger.d.ts +48 -0
- package/dist/observability/logger.d.ts.map +1 -0
- package/dist/observability/logger.js +81 -0
- package/dist/observability/logger.js.map +1 -0
- package/dist/observability/metrics.d.ts +53 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +92 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/routes/admin/agents.d.ts +63 -0
- package/dist/routes/admin/agents.d.ts.map +1 -0
- package/dist/routes/admin/agents.js +461 -0
- package/dist/routes/admin/agents.js.map +1 -0
- package/dist/routes/admin/rotatingMessages.d.ts +51 -0
- package/dist/routes/admin/rotatingMessages.d.ts.map +1 -0
- package/dist/routes/admin/rotatingMessages.js +292 -0
- package/dist/routes/admin/rotatingMessages.js.map +1 -0
- package/dist/routes/admin/sessions.d.ts +61 -0
- package/dist/routes/admin/sessions.d.ts.map +1 -0
- package/dist/routes/admin/sessions.js +232 -0
- package/dist/routes/admin/sessions.js.map +1 -0
- package/dist/routes/admin/settings.d.ts +110 -0
- package/dist/routes/admin/settings.d.ts.map +1 -0
- package/dist/routes/admin/settings.js +401 -0
- package/dist/routes/admin/settings.js.map +1 -0
- package/dist/routes/admin/users.d.ts +115 -0
- package/dist/routes/admin/users.d.ts.map +1 -0
- package/dist/routes/admin/users.js +374 -0
- package/dist/routes/admin/users.js.map +1 -0
- package/dist/routes/agents.d.ts +30 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +55 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/auth.d.ts +121 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +422 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/chat.d.ts +97 -0
- package/dist/routes/chat.d.ts.map +1 -0
- package/dist/routes/chat.js +484 -0
- package/dist/routes/chat.js.map +1 -0
- package/dist/routes/featureToggles.d.ts +34 -0
- package/dist/routes/featureToggles.d.ts.map +1 -0
- package/dist/routes/featureToggles.js +47 -0
- package/dist/routes/featureToggles.js.map +1 -0
- package/dist/routes/health.d.ts +24 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +37 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/metrics.d.ts +35 -0
- package/dist/routes/metrics.d.ts.map +1 -0
- package/dist/routes/metrics.js +106 -0
- package/dist/routes/metrics.js.map +1 -0
- package/dist/routes/sessions.d.ts +161 -0
- package/dist/routes/sessions.d.ts.map +1 -0
- package/dist/routes/sessions.js +1501 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/routes/stubs.d.ts +25 -0
- package/dist/routes/stubs.d.ts.map +1 -0
- package/dist/routes/stubs.js +25 -0
- package/dist/routes/stubs.js.map +1 -0
- package/dist/state/cron.d.ts +58 -0
- package/dist/state/cron.d.ts.map +1 -0
- package/dist/state/cron.js +160 -0
- package/dist/state/cron.js.map +1 -0
- package/dist/state/rotationRunner.d.ts +96 -0
- package/dist/state/rotationRunner.d.ts.map +1 -0
- package/dist/state/rotationRunner.js +246 -0
- package/dist/state/rotationRunner.js.map +1 -0
- package/dist/state/rotationScheduler.d.ts +76 -0
- package/dist/state/rotationScheduler.d.ts.map +1 -0
- package/dist/state/rotationScheduler.js +124 -0
- package/dist/state/rotationScheduler.js.map +1 -0
- package/dist/state/turnRegistry.d.ts +95 -0
- package/dist/state/turnRegistry.d.ts.map +1 -0
- package/dist/state/turnRegistry.js +149 -0
- package/dist/state/turnRegistry.js.map +1 -0
- package/dist/state/turnRunner.d.ts +134 -0
- package/dist/state/turnRunner.d.ts.map +1 -0
- package/dist/state/turnRunner.js +476 -0
- package/dist/state/turnRunner.js.map +1 -0
- package/package.json +47 -0
- package/src/auth/argon2.ts +152 -0
- package/src/auth/encryption.ts +110 -0
- package/src/auth/jwt.ts +217 -0
- package/src/auth/middleware.ts +250 -0
- package/src/auth/passwordPolicy.ts +99 -0
- package/src/auth/roleRules.ts +497 -0
- package/src/auth/userView.ts +88 -0
- package/src/bootstrap.ts +254 -0
- package/src/db/connection.ts +75 -0
- package/src/db/migrate.ts +212 -0
- package/src/db/schema.ts +750 -0
- package/src/db/seed.ts +236 -0
- package/src/handlers/fetch.ts +251 -0
- package/src/handlers/nextjs.ts +49 -0
- package/src/handlers/responses.ts +95 -0
- package/src/handlers/routeTable.ts +188 -0
- package/src/handlers/router.ts +96 -0
- package/src/handlers/sse.ts +55 -0
- package/src/handlers/types.ts +104 -0
- package/src/index.ts +60 -0
- package/src/letta/client.ts +860 -0
- package/src/letta/eventTranslator.ts +667 -0
- package/src/letta/index.ts +46 -0
- package/src/letta/lettaCliShim.mjs +40 -0
- package/src/letta/sandbox.ts +167 -0
- package/src/letta/settingsFileSeed.ts +254 -0
- package/src/letta/subprocessThrottle.ts +251 -0
- package/src/letta/types.ts +65 -0
- package/src/observability/logger.ts +103 -0
- package/src/observability/metrics.ts +114 -0
- package/src/routes/admin/agents.ts +574 -0
- package/src/routes/admin/rotatingMessages.ts +351 -0
- package/src/routes/admin/sessions.ts +283 -0
- package/src/routes/admin/settings.ts +463 -0
- package/src/routes/admin/users.ts +455 -0
- package/src/routes/agents.ts +66 -0
- package/src/routes/auth.ts +513 -0
- package/src/routes/chat.ts +534 -0
- package/src/routes/featureToggles.ts +52 -0
- package/src/routes/health.ts +44 -0
- package/src/routes/metrics.ts +111 -0
- package/src/routes/sessions.ts +1878 -0
- package/src/state/cron.ts +179 -0
- package/src/state/rotationRunner.ts +326 -0
- package/src/state/rotationScheduler.ts +164 -0
- package/src/state/turnRegistry.ts +210 -0
- 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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"}
|