@lawaty/lacode 0.0.0-stage → 0.1.0

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/lacode.js ADDED
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/lacode
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ // src/tunnel/index.ts
7
+ import { spawnSync } from "node:child_process";
8
+ import { homedir } from "node:os";
9
+ import { join } from "node:path";
10
+ var ICONS = [
11
+ "utilities-terminal",
12
+ "dialog-information",
13
+ "dialog-error",
14
+ "dialog-warning",
15
+ "system-run",
16
+ "emblem-default"
17
+ ];
18
+ function resolveTunnel(env = process.env) {
19
+ const port = Number(env.OC_TUNNEL_PORT);
20
+ return {
21
+ user: env.OC_LOCAL_USER ?? "",
22
+ port: Number.isFinite(port) && port > 0 ? port : 10022,
23
+ key: env.OC_NOTIFY_KEY ?? join(homedir(), ".ssh", "lacode_notify"),
24
+ connectTimeoutMs: Number(env.OC_CONNECT_TIMEOUT_MS) || 5e3
25
+ };
26
+ }
27
+ function isRemote(env = process.env) {
28
+ return Boolean(env.SSH_CLIENT || env.SSH_TTY);
29
+ }
30
+ function normalizeUrgency(raw) {
31
+ return raw === "low" || raw === "critical" || raw === "normal" ? raw : "normal";
32
+ }
33
+ function normalizeIcon(raw) {
34
+ return ICONS.includes(raw) ? raw : "utilities-terminal";
35
+ }
36
+ var safeText = (raw, fallback, max) => {
37
+ const clean = raw.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
38
+ if (!clean) return fallback;
39
+ return clean.length > max ? `${clean.slice(0, max - 1)}\u2026` : clean;
40
+ };
41
+ var safeTitle = (raw) => safeText(raw ?? "", "LaCode", 120);
42
+ var safeMessage = (raw) => safeText(raw ?? "", "", 400);
43
+ var FIELD_SEPARATOR = "\n";
44
+ var encodePayload = (request) => [
45
+ safeTitle(request.title),
46
+ safeMessage(request.message),
47
+ normalizeUrgency(request.urgency),
48
+ normalizeIcon(request.icon)
49
+ ].join(FIELD_SEPARATOR);
50
+ var SSH_OPTS = [
51
+ "-o",
52
+ "BatchMode=yes",
53
+ "-o",
54
+ "IdentitiesOnly=yes",
55
+ "-o",
56
+ "ClearAllForwardings=yes",
57
+ "-o",
58
+ "LogLevel=ERROR",
59
+ "-o",
60
+ "StrictHostKeyChecking=accept-new"
61
+ ];
62
+ var run = (command2, args, timeout) => {
63
+ try {
64
+ const result = spawnSync(command2, args, { timeout, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
65
+ return {
66
+ ok: result.status === 0,
67
+ stdout: result.stdout ?? "",
68
+ stderr: (result.stderr ?? "").trim(),
69
+ // spawnSync reports a timeout as a null status and an ETIMEDOUT error; both
70
+ // are "did not arrive", not "crashed", and neither may throw.
71
+ detail: result.error ? String(result.error.code ?? result.error.message) : void 0
72
+ };
73
+ } catch (error) {
74
+ return { ok: false, stdout: "", stderr: "", detail: String(error) };
75
+ }
76
+ };
77
+ var have = (command2) => {
78
+ try {
79
+ return spawnSync("sh", ["-c", `command -v ${command2}`], { encoding: "utf8" }).status === 0;
80
+ } catch {
81
+ return false;
82
+ }
83
+ };
84
+ function sendLocal(request) {
85
+ if (!have("notify-send")) return { ok: false, detail: "notify-send is not installed", transport: "local" };
86
+ const args = [
87
+ safeTitle(request.title),
88
+ safeMessage(request.message),
89
+ `--urgency=${normalizeUrgency(request.urgency)}`,
90
+ `--icon=${normalizeIcon(request.icon)}`
91
+ ];
92
+ const result = run("notify-send", args, 5e3);
93
+ return { ok: result.ok, detail: result.ok ? void 0 : result.stderr || result.detail, transport: "local" };
94
+ }
95
+ function sendRemote(request, tunnel) {
96
+ if (!tunnel.user) return { ok: false, detail: "OC_LOCAL_USER is unset; the desktop user for the tunnel is unknown", transport: "remote" };
97
+ const args = [
98
+ ...SSH_OPTS,
99
+ "-o",
100
+ `ConnectTimeout=${Math.max(1, Math.round(tunnel.connectTimeoutMs / 1e3))}`,
101
+ "-i",
102
+ tunnel.key,
103
+ "-p",
104
+ String(tunnel.port),
105
+ `${tunnel.user}@localhost`,
106
+ "--",
107
+ encodePayload(request)
108
+ ];
109
+ const result = run("ssh", args, tunnel.connectTimeoutMs + 2e3);
110
+ return { ok: result.ok, detail: result.ok ? void 0 : result.stderr || result.detail, transport: "remote" };
111
+ }
112
+ function notify(request, env = process.env) {
113
+ try {
114
+ return isRemote(env) ? sendRemote(request, resolveTunnel(env)) : sendLocal(request);
115
+ } catch (error) {
116
+ return { ok: false, detail: String(error), transport: "none" };
117
+ }
118
+ }
119
+ function forward(host, argv2, options = {}) {
120
+ const env = options.env ?? process.env;
121
+ const timeoutMs = options.timeoutMs ?? (Number(env.OC_SSH_TIMEOUT_MS) || 3e4);
122
+ const result = run("ssh", [...SSH_OPTS, "-o", `ConnectTimeout=${Math.max(1, Math.round(timeoutMs / 1e3))}`, host, "--", ...argv2], timeoutMs);
123
+ return {
124
+ ok: result.ok,
125
+ detail: result.ok ? void 0 : result.stderr || result.detail,
126
+ transport: "remote",
127
+ stdout: result.stdout
128
+ };
129
+ }
130
+ function describe(env = process.env) {
131
+ const tunnel = resolveTunnel(env);
132
+ return {
133
+ running: isRemote(env) ? "remote (over ssh)" : "local",
134
+ user: tunnel.user || "(unset)",
135
+ port: tunnel.port,
136
+ key: tunnel.key,
137
+ notifySend: have("notify-send")
138
+ };
139
+ }
140
+
141
+ // bin/lacode
142
+ if (!process.features?.typescript) {
143
+ if (process.env.LACODE_STRIP_TYPECHECKED === "1") {
144
+ console.error("lacode: this node cannot load TypeScript sources; node >= 22.18 is required.");
145
+ process.exit(1);
146
+ }
147
+ const { spawnSync: spawnSync2 } = await import("node:child_process");
148
+ const result = spawnSync2(
149
+ process.execPath,
150
+ ["--experimental-strip-types", "--no-warnings", fileURLToPath(import.meta.url), ...process.argv.slice(2)],
151
+ { stdio: "inherit", env: { ...process.env, LACODE_STRIP_TYPECHECKED: "1" } }
152
+ );
153
+ process.exit(result.status ?? 1);
154
+ }
155
+ var USAGE = `lacode \u2014 portable notifications and remote exec for LaCode
156
+
157
+ lacode notify TITLE [MESSAGE] [URGENCY] [ICON]
158
+ Send one desktop notification. Local host: straight to the desktop
159
+ session. Over ssh: handed to the desktop through the reverse forward.
160
+ URGENCY is low | normal | critical. Fire-and-forget: always exits 0.
161
+
162
+ lacode forward HOST -- COMMAND [ARG...]
163
+ Run COMMAND on HOST over ssh. Exits with the remote's status.
164
+
165
+ lacode doctor
166
+ Print what the environment resolves to. Diagnostics only.
167
+
168
+ Environment:
169
+ OC_LOCAL_USER desktop user the tunnel authenticates as
170
+ OC_TUNNEL_PORT reverse-forward listener port (default 10022)
171
+ OC_NOTIFY_KEY dedicated key (default ~/.ssh/lacode_notify)
172
+ OC_CONNECT_TIMEOUT_MS / OC_SSH_TIMEOUT_MS`;
173
+ var argv = process.argv.slice(2);
174
+ var command = argv.shift();
175
+ switch (command) {
176
+ case void 0:
177
+ case "--help":
178
+ case "-h":
179
+ case "help":
180
+ console.log(USAGE);
181
+ process.exit(0);
182
+ break;
183
+ case "notify": {
184
+ const [rawTitle, message, urgency, icon] = argv;
185
+ if (!rawTitle) {
186
+ console.error("lacode notify: TITLE is required");
187
+ console.error(USAGE);
188
+ process.exit(2);
189
+ }
190
+ const result = notify({ title: rawTitle, message, urgency, icon });
191
+ if (!result.ok) console.error(`lacode: notification not delivered via ${result.transport}: ${result.detail ?? "unknown reason"}`);
192
+ process.exit(0);
193
+ break;
194
+ }
195
+ case "forward": {
196
+ const host = argv.shift();
197
+ const separator = argv.indexOf("--");
198
+ if (!host || separator < 0 || separator === argv.length - 1) {
199
+ console.error("lacode forward: usage: lacode forward HOST -- COMMAND [ARG...]");
200
+ process.exit(2);
201
+ }
202
+ const command2 = argv.slice(separator + 1);
203
+ const result = forward(host, command2);
204
+ if (result.stdout) process.stdout.write(result.stdout);
205
+ if (!result.ok) console.error(`lacode: ${host}: ${result.detail ?? "remote command failed"}`);
206
+ process.exit(result.ok ? 0 : 1);
207
+ break;
208
+ }
209
+ case "doctor": {
210
+ const info = describe();
211
+ for (const [key, value] of Object.entries(info)) console.log(`${key.padEnd(11)} ${String(value)}`);
212
+ if (info.running.startsWith("remote")) {
213
+ if (info.user === "(unset)") console.log("\nOC_LOCAL_USER is unset: the hop cannot name the desktop user, and guessing with `id -un` is wrong on most remote accounts.");
214
+ console.log("Set OC_LOCAL_USER, OC_TUNNEL_PORT and OC_NOTIFY_KEY, or point OC_NOTIFY_HELPER at a helper that knows them.");
215
+ }
216
+ process.exit(0);
217
+ break;
218
+ }
219
+ default:
220
+ console.error(`lacode: unknown command ${JSON.stringify(safeTitle(command))}`);
221
+ console.error(USAGE);
222
+ process.exit(2);
223
+ }
package/dist/pool.js ADDED
@@ -0,0 +1,487 @@
1
+ // src/lib/pool.ts
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ var CLAIM_TTL_MS = 10 * 60 * 1e3;
7
+ var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
8
+ var ORPHAN_TMP_MS = 60 * 1e3;
9
+ var CATALOG_TTL_MS = 10 * 60 * 1e3;
10
+ var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
11
+ var STUCK_IDLE_MS = 3 * 60 * 1e3;
12
+ var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
13
+ var MAX_WEIGHT = 100;
14
+ var MAX_SLOTS = 4;
15
+ var COOLDOWN_BASE_MS = 60 * 1e3;
16
+ var COOLDOWN_MAX_MS = 15 * 60 * 1e3;
17
+ var STRIKE_DECAY_MS = 30 * 60 * 1e3;
18
+ var BASES = ["explore-fast", "implement-fast", "context-manager"];
19
+ var findPackageRoot = (start) => {
20
+ let dir = start;
21
+ for (let up = 0; up < 6; up++) {
22
+ try {
23
+ if (JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).name === "@lawaty/lacode") return dir;
24
+ } catch {
25
+ }
26
+ const parent = dirname(dir);
27
+ if (parent === dir) break;
28
+ dir = parent;
29
+ }
30
+ return join(start, "..", "..");
31
+ };
32
+ var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
33
+ var PRESETS_DIR = join(PACKAGE_ROOT, "presets");
34
+ var DEFAULT_PRESET = join(PRESETS_DIR, "free-tier.json");
35
+ var USER_CONFIG_DIR = join(homedir(), ".config", "lacode");
36
+ var USER_MODELS_PATH = join(USER_CONFIG_DIR, "pool.json");
37
+ var FALLBACK_SLOTS = [
38
+ { index: 1, model: "opencode/space-bunny-free", weight: 3 },
39
+ { index: 2, model: "openrouter/nvidia/nemotron-3-ultra-550b-a55b:free", weight: 2 },
40
+ { index: 3, model: "opencode/big-pickle", weight: 1 },
41
+ { index: 4, model: "openrouter/thinkingmachines/inkling:free", weight: 1 }
42
+ ];
43
+ function parseSlots(parsed, origin) {
44
+ const problems = [];
45
+ const entries = parsed?.slots;
46
+ if (!Array.isArray(entries) || entries.length === 0) {
47
+ return { slots: FALLBACK_SLOTS, problems: [`${origin} has no non-empty "slots" array; using built-in defaults`] };
48
+ }
49
+ const slots = [];
50
+ entries.forEach((entry, i) => {
51
+ const model = typeof entry?.model === "string" ? entry.model.trim() : "";
52
+ const at = `slot ${i + 1}`;
53
+ if (slots.length >= MAX_SLOTS) {
54
+ problems.push(`${at} (${model || "?"}): the pool is capped at ${MAX_SLOTS} slots (one variant agent per base per slot is declared); entry ignored`);
55
+ return;
56
+ }
57
+ if (!model.includes("/")) return problems.push(`${at}: model must be written provider/model-id, got "${model}"`);
58
+ const weight = readWeight(entry, at, model, problems);
59
+ if (slots.some((s) => s.model === model)) return problems.push(`${at}: ${model} is already used by another slot; slots must be distinct models`);
60
+ slots.push({ index: slots.length + 1, model, weight });
61
+ });
62
+ if (slots.length === 0) {
63
+ problems.push(`${origin} yielded no usable slot; using built-in defaults`);
64
+ return { slots: FALLBACK_SLOTS, problems };
65
+ }
66
+ return { slots, problems };
67
+ }
68
+ var readJSON = (path) => {
69
+ try {
70
+ return { ok: true, value: JSON.parse(readFileSync(path, "utf8")) };
71
+ } catch (error) {
72
+ return { ok: false, error: String(error) };
73
+ }
74
+ };
75
+ function readOptional(path, origin, problems) {
76
+ const result = readJSON(path);
77
+ if (!result.ok) {
78
+ if (existsSync(path)) problems.push(`${origin} unreadable (${result.error}); trying the next source`);
79
+ return void 0;
80
+ }
81
+ return result.value;
82
+ }
83
+ function resolveModels(input = {}) {
84
+ const problems = [];
85
+ if (input.models !== void 0) {
86
+ const origin = "plugin options";
87
+ if (typeof input.models !== "object" || input.models === null || Array.isArray(input.models)) {
88
+ return { slots: FALLBACK_SLOTS, problems: [`${origin}: "models" must be an object like {"slots":[...]}`], origin };
89
+ }
90
+ const parsed = parseSlots(input.models, origin);
91
+ return { slots: parsed.slots, problems: [...problems, ...parsed.problems], origin };
92
+ }
93
+ if (input.modelsFile) {
94
+ const result = readJSON(input.modelsFile);
95
+ if (!result.ok) return { slots: FALLBACK_SLOTS, problems: [`${input.modelsFile} unreadable (${result.error}); using built-in defaults`], origin: input.modelsFile };
96
+ const parsed = parseSlots(result.value, input.modelsFile);
97
+ return { slots: parsed.slots, problems: parsed.problems, origin: input.modelsFile };
98
+ }
99
+ const user = readOptional(USER_MODELS_PATH, USER_MODELS_PATH, problems);
100
+ if (user !== void 0) {
101
+ const parsed = parseSlots(user, USER_MODELS_PATH);
102
+ if (parsed.slots !== FALLBACK_SLOTS) return { slots: parsed.slots, problems, origin: USER_MODELS_PATH };
103
+ problems.push(...parsed.problems);
104
+ }
105
+ const preset = readOptional(DEFAULT_PRESET, DEFAULT_PRESET, problems);
106
+ if (preset !== void 0) {
107
+ const parsed = parseSlots(preset, "presets/free-tier.json");
108
+ return { slots: parsed.slots, problems: [...problems, ...parsed.problems], origin: "presets/free-tier.json" };
109
+ }
110
+ return { slots: FALLBACK_SLOTS, problems: [...problems, `no pool source found (${USER_MODELS_PATH}, ${DEFAULT_PRESET}); using built-in defaults`], origin: "built-in defaults" };
111
+ }
112
+ function readSlotFile(path) {
113
+ const result = readJSON(path);
114
+ if (!result.ok) return { slots: FALLBACK_SLOTS, problems: [`${path} unreadable (${result.error}); using built-in defaults`] };
115
+ return parseSlots(result.value, path);
116
+ }
117
+ function ensureUserConfigDir() {
118
+ try {
119
+ mkdirSync(USER_CONFIG_DIR, { recursive: true });
120
+ return true;
121
+ } catch {
122
+ return false;
123
+ }
124
+ }
125
+ function readWeight(entry, at, model, problems) {
126
+ const raw = entry?.weight;
127
+ if (raw === void 0) {
128
+ const legacy = entry?.tier;
129
+ if (legacy !== void 0) {
130
+ problems.push(
131
+ `weight: "tier" is deprecated; write "weight" instead (primary is weight 2, overflow is weight 1), got ${JSON.stringify(legacy)}`
132
+ );
133
+ if (legacy === "primary" || legacy === "overflow") return legacy === "primary" ? 2 : 1;
134
+ }
135
+ return 1;
136
+ }
137
+ const weight = typeof raw === "number" ? raw : Number(raw);
138
+ if (!Number.isFinite(weight) || weight <= 0 || weight > MAX_WEIGHT) {
139
+ problems.push(`${at} (${model}): weight must be a number in (0, ${MAX_WEIGHT}], got ${JSON.stringify(raw)}; using weight 1`);
140
+ return 1;
141
+ }
142
+ return weight;
143
+ }
144
+ var LIMIT_PATTERNS = [
145
+ /rate[\s_-]?limit/i,
146
+ /usage limit/i,
147
+ /too many requests/i,
148
+ /\b429\b/,
149
+ /quota/i,
150
+ /freelimiterror/i,
151
+ /overloaded/i
152
+ ];
153
+ var isLimitText = (text) => LIMIT_PATTERNS.some((re) => re.test(text));
154
+ var statedCooldown = (text) => {
155
+ const hm = /resets?\s+in\s+(\d+)\s*(?:h|hr|hour)s?\s*(\d+)?\s*(?:m|min|minute)?s?/i.exec(text);
156
+ if (hm) return (Number(hm[1]) * 60 + (hm[2] ? Number(hm[2]) : 0)) * 60 * 1e3;
157
+ const m = /resets?\s+in\s+(\d+)\s*(?:m|min|minute)s?/i.exec(text);
158
+ if (m) return Number(m[1]) * 60 * 1e3;
159
+ const s = /retry[\s-]?in\s+(\d+)\s*(?:s|sec|second)s?/i.exec(text);
160
+ if (s) return Number(s[1]) * 1e3;
161
+ return void 0;
162
+ };
163
+ var retryAfterMs = (headers) => {
164
+ if (!headers) return void 0;
165
+ for (const [key, value] of Object.entries(headers)) {
166
+ if (key.toLowerCase() !== "retry-after") continue;
167
+ const seconds = Number(value);
168
+ if (Number.isFinite(seconds) && seconds > 0) return Math.min(seconds * 1e3, COOLDOWN_MAX_MS);
169
+ const when = Date.parse(value);
170
+ if (Number.isFinite(when)) return Math.min(Math.max(when - Date.now(), 1e3), COOLDOWN_MAX_MS);
171
+ }
172
+ return void 0;
173
+ };
174
+ function state(opts = {}) {
175
+ const id = opts.id ?? String(process.pid);
176
+ const dir = opts.dir ?? join(homedir(), ".local", "share", "opencode", "agent-pool");
177
+ const modelsFile = opts.modelsFile;
178
+ const models = opts.models;
179
+ const key = /* @__PURE__ */ Symbol.for(`agent-pool:state:${id}:${dir}:${modelsFile ?? ""}:${models === void 0 ? "" : "inline"}`);
180
+ const g = globalThis;
181
+ if (!g[key]) {
182
+ g[key] = {
183
+ own: /* @__PURE__ */ new Map(),
184
+ models: /* @__PURE__ */ new Map(),
185
+ known: /* @__PURE__ */ new Set(),
186
+ warned: /* @__PURE__ */ new Set(),
187
+ initialized: false,
188
+ dirReady: false,
189
+ catalog: /* @__PURE__ */ new Map(),
190
+ catalogAt: 0,
191
+ cooling: /* @__PURE__ */ new Map(),
192
+ activity: /* @__PURE__ */ new Map(),
193
+ reaped: 0,
194
+ reaping: false,
195
+ lastSweep: 0,
196
+ id,
197
+ dir,
198
+ modelsFile,
199
+ inlineModels: models,
200
+ origin: "unresolved",
201
+ slots: []
202
+ };
203
+ }
204
+ return g[key];
205
+ }
206
+ function ensureSlots(s, onWarn) {
207
+ if (s.slots.length > 0) return s.slots;
208
+ const { slots, problems, origin } = resolveModels({ models: s.inlineModels, modelsFile: s.modelsFile });
209
+ s.slots = slots;
210
+ s.origin = origin;
211
+ for (const problem of problems) onWarn(problem);
212
+ return slots;
213
+ }
214
+ var claimKey = (sessionID, callID) => `${sessionID}:${callID}`;
215
+ var selfFile = (id) => `claims.${id}.json`;
216
+ var limitFile = (model) => `limit.${model.replace(/[^a-zA-Z0-9._-]/g, "__")}.json`;
217
+ function init(s, onWarn) {
218
+ ensureSlots(s, onWarn);
219
+ if (s.initialized) return;
220
+ s.initialized = true;
221
+ try {
222
+ mkdirSync(s.dir, { recursive: true });
223
+ s.dirReady = true;
224
+ try {
225
+ unlinkSync(join(s.dir, selfFile(s.id)));
226
+ } catch {
227
+ }
228
+ } catch {
229
+ onWarn(`claim dir ${s.dir} unusable; pool load tracked in memory only`);
230
+ }
231
+ }
232
+ function pruneOwn(s) {
233
+ const now = Date.now();
234
+ for (const [key, claim] of s.own) if (now - claim.t > CLAIM_TTL_MS) s.own.delete(key);
235
+ }
236
+ function publish(s) {
237
+ if (!s.dirReady) return;
238
+ const now = Date.now();
239
+ const payload = {};
240
+ for (const [key, claim] of s.own) if (now - claim.t <= CLAIM_TTL_MS) payload[key] = claim;
241
+ try {
242
+ writeFileSync(join(s.dir, `${selfFile(s.id)}.tmp`), JSON.stringify(payload));
243
+ renameSync(join(s.dir, `${selfFile(s.id)}.tmp`), join(s.dir, selfFile(s.id)));
244
+ } catch {
245
+ s.dirReady = false;
246
+ }
247
+ }
248
+ function readSiblings(s) {
249
+ const siblings = [];
250
+ let entries;
251
+ try {
252
+ entries = readdirSync(s.dir);
253
+ } catch {
254
+ return siblings;
255
+ }
256
+ const now = Date.now();
257
+ for (const entry of entries) {
258
+ const path = join(s.dir, entry);
259
+ let age;
260
+ try {
261
+ age = now - statSync(path).mtimeMs;
262
+ } catch {
263
+ continue;
264
+ }
265
+ if (entry.endsWith(".tmp")) {
266
+ if (age > ORPHAN_TMP_MS) try {
267
+ unlinkSync(path);
268
+ } catch {
269
+ }
270
+ continue;
271
+ }
272
+ const match = /^claims\.(.+)\.json$/.exec(entry);
273
+ if (!match || match[1] === s.id) continue;
274
+ if (age > DEAD_FILE_MS) {
275
+ try {
276
+ unlinkSync(path);
277
+ } catch {
278
+ }
279
+ continue;
280
+ }
281
+ try {
282
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
283
+ const claims = Object.values(parsed).filter(
284
+ (c) => c && typeof c.v === "string" && typeof c.m === "string" && now - c.t <= CLAIM_TTL_MS
285
+ );
286
+ siblings.push({ id: match[1], claims });
287
+ } catch {
288
+ }
289
+ }
290
+ return siblings;
291
+ }
292
+ function readLimits(s) {
293
+ const cooling = /* @__PURE__ */ new Map();
294
+ let entries;
295
+ try {
296
+ entries = readdirSync(s.dir);
297
+ } catch {
298
+ return cooling;
299
+ }
300
+ const now = Date.now();
301
+ for (const entry of entries) {
302
+ if (!entry.startsWith("limit.") || !entry.endsWith(".json")) continue;
303
+ const path = join(s.dir, entry);
304
+ try {
305
+ const strike = JSON.parse(readFileSync(path, "utf8"));
306
+ if (typeof strike.until !== "number" || typeof strike.model !== "string") continue;
307
+ if (strike.until > now) cooling.set(strike.model, strike);
308
+ else unlinkSync(path);
309
+ } catch {
310
+ try {
311
+ unlinkSync(path);
312
+ } catch {
313
+ }
314
+ }
315
+ }
316
+ s.cooling = cooling;
317
+ return cooling;
318
+ }
319
+ function recordStrike(s, model, reason, stated) {
320
+ if (!s.dirReady) return;
321
+ const now = Date.now();
322
+ const prior = s.cooling.get(model);
323
+ const strikes = prior && now - prior.last < STRIKE_DECAY_MS ? prior.strikes + 1 : 1;
324
+ const backoff = Math.min(COOLDOWN_MAX_MS, COOLDOWN_BASE_MS * 2 ** (strikes - 1));
325
+ const strike = { model, until: now + (stated ?? backoff), strikes, reason, last: now };
326
+ s.cooling.set(model, strike);
327
+ const path = join(s.dir, limitFile(model));
328
+ try {
329
+ writeFileSync(`${path}.tmp`, JSON.stringify(strike));
330
+ renameSync(`${path}.tmp`, path);
331
+ } catch {
332
+ }
333
+ return strike;
334
+ }
335
+ function refreshCatalog(s, serverUrl) {
336
+ if (!serverUrl || Date.now() - s.catalogAt < CATALOG_TTL_MS) return;
337
+ s.catalogAt = Date.now();
338
+ void (async () => {
339
+ try {
340
+ const response = await fetch(new URL("/api/model", serverUrl).toString());
341
+ if (!response.ok) return;
342
+ const payload = await response.json();
343
+ const catalog = /* @__PURE__ */ new Map();
344
+ for (const m of payload.data ?? []) catalog.set(`${m.providerID}/${m.id}`, m.status ?? "unknown");
345
+ if (catalog.size > 0) s.catalog = catalog;
346
+ } catch {
347
+ }
348
+ })();
349
+ }
350
+ function isAvailable(s, model, cooling) {
351
+ if (cooling.has(model)) return false;
352
+ const status = s.catalog.get(model);
353
+ return status === void 0 || status === "active" || status === "unknown";
354
+ }
355
+ function snapshot(s) {
356
+ pruneOwn(s);
357
+ const siblings = readSiblings(s);
358
+ const cooling = readLimits(s);
359
+ const counts = /* @__PURE__ */ new Map();
360
+ const bump = (m) => counts.set(m, (counts.get(m) ?? 0) + 1);
361
+ for (const claim of s.own.values()) bump(claim.m);
362
+ for (const sibling of siblings) for (const claim of sibling.claims) bump(claim.m);
363
+ return { counts, cooling, siblings, own: s.own, dirReady: s.dirReady };
364
+ }
365
+ var loadOf = (counts, model) => counts.get(model) ?? 0;
366
+ var underCeiling = (slot, counts) => loadOf(counts, slot.model) < slot.weight;
367
+ function pickSlot(s, snap) {
368
+ const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
369
+ const usable = slots.filter((slot) => isAvailable(s, slot.model, snap.cooling));
370
+ const pool = usable.length > 0 ? usable : slots;
371
+ const open = pool.filter((slot) => underCeiling(slot, snap.counts));
372
+ if (open.length > 0) return open.reduce((best, slot) => slot.weight > best.weight ? slot : best, open[0]);
373
+ return pool.reduce((best, slot) => loadOf(snap.counts, slot.model) < loadOf(snap.counts, best.model) ? slot : best, pool[0]);
374
+ }
375
+ function agentFor(base, slot) {
376
+ return `${base}-${slot}`;
377
+ }
378
+ function acquire(s, base, slot, ownerSession, key, target) {
379
+ s.own.set(key, { v: base, k: slot.index, m: slot.model, t: Date.now(), s: ownerSession, g: target });
380
+ if (target) s.activity.set(target, Date.now());
381
+ publish(s);
382
+ }
383
+ function release(s, key) {
384
+ if (s.own.delete(key)) publish(s);
385
+ }
386
+ function bindChild(s, parentID, childID) {
387
+ const candidates = [...s.own.entries()].filter(([, c]) => c.s === parentID && !c.g).sort((a, b) => a[1].t - b[1].t);
388
+ if (candidates.length === 0) return false;
389
+ candidates[0][1].g = childID;
390
+ s.activity.set(childID, Date.now());
391
+ publish(s);
392
+ return true;
393
+ }
394
+ function noteActivity(s, sessionID) {
395
+ s.activity.set(sessionID, Date.now());
396
+ }
397
+ function idleFor(s, claim, now) {
398
+ const target = claim.g;
399
+ if (!target) return Infinity;
400
+ const seen = s.activity.get(target);
401
+ return seen ? now - seen : Infinity;
402
+ }
403
+ function findStuck(s, now = Date.now()) {
404
+ const stuck = [];
405
+ for (const [key, claim] of s.own) {
406
+ if (now - claim.t <= STUCK_MIN_AGE_MS) continue;
407
+ if (idleFor(s, claim, now) < STUCK_IDLE_MS) continue;
408
+ stuck.push({ key, claim });
409
+ }
410
+ return stuck;
411
+ }
412
+ function releaseStuck(s, keys) {
413
+ for (const key of keys) s.own.delete(key);
414
+ if (keys.length > 0) publish(s);
415
+ }
416
+ function slotFor(s, model) {
417
+ return s.slots.find((slot) => slot.model === model);
418
+ }
419
+ function splitModel(model) {
420
+ const slash = model.indexOf("/");
421
+ if (slash < 0) return { provider: model, id: "" };
422
+ return { provider: model.slice(0, slash), id: model.slice(slash + 1) };
423
+ }
424
+ var providerOf = (model) => splitModel(model).provider;
425
+ function coolProvider(s, provider, until, reason) {
426
+ const affected = [];
427
+ for (const slot of s.slots.length > 0 ? s.slots : FALLBACK_SLOTS) {
428
+ if (providerOf(slot.model) !== provider) continue;
429
+ const existing = s.cooling.get(slot.model);
430
+ const untilMs = Math.max(until, existing?.until ?? 0);
431
+ const strike = recordStrike(s, slot.model, reason, untilMs - Date.now());
432
+ if (strike) affected.push(strike);
433
+ }
434
+ return affected;
435
+ }
436
+ export {
437
+ BASES,
438
+ CATALOG_TTL_MS,
439
+ CLAIM_TTL_MS,
440
+ DEAD_FILE_MS,
441
+ DEFAULT_PRESET,
442
+ FALLBACK_SLOTS,
443
+ HANG_COOLDOWN_MS,
444
+ MAX_SLOTS,
445
+ MAX_WEIGHT,
446
+ ORPHAN_TMP_MS,
447
+ PACKAGE_ROOT,
448
+ PRESETS_DIR,
449
+ STUCK_IDLE_MS,
450
+ STUCK_MIN_AGE_MS,
451
+ USER_CONFIG_DIR,
452
+ USER_MODELS_PATH,
453
+ acquire,
454
+ agentFor,
455
+ bindChild,
456
+ claimKey,
457
+ coolProvider,
458
+ ensureSlots,
459
+ ensureUserConfigDir,
460
+ findStuck,
461
+ idleFor,
462
+ init,
463
+ isAvailable,
464
+ isLimitText,
465
+ noteActivity,
466
+ parseSlots,
467
+ pickSlot,
468
+ providerOf,
469
+ pruneOwn,
470
+ publish,
471
+ readLimits,
472
+ readSiblings,
473
+ readSlotFile,
474
+ recordStrike,
475
+ refreshCatalog,
476
+ release,
477
+ releaseStuck,
478
+ resolveModels,
479
+ retryAfterMs,
480
+ selfFile,
481
+ slotFor,
482
+ snapshot,
483
+ splitModel,
484
+ state,
485
+ statedCooldown,
486
+ underCeiling
487
+ };