@lawaty/lacode 2.2.0-beta.0 → 2.3.0-beta.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/CHANGELOG.md +45 -0
- package/README.md +14 -0
- package/dist/agents.js +4 -0
- package/dist/commands.js +4 -0
- package/dist/index.js +2531 -2127
- package/dist/lacode.js +4 -0
- package/dist/pool.js +186 -27
- package/dist/tui.js +153 -25
- package/dist/tunnel.js +4 -0
- package/docs/CONFIGURATION.md +12 -0
- package/docs/USER-STORIES.md +59 -8
- package/package.json +1 -1
- package/scripts/pool-selection.mjs +112 -15
- package/scripts/refresh-pool.mjs +72 -9
- package/scripts/score-models.mjs +168 -47
package/dist/lacode.js
CHANGED
|
@@ -53,6 +53,8 @@ var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
|
53
53
|
var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
|
54
54
|
var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
55
55
|
var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
|
|
56
|
+
var PROVIDER_DEGRADED_COOLDOWN_MS = 30 * 60 * 1e3;
|
|
57
|
+
var PROVIDER_DEGRADED_WINDOW_MS = 10 * 60 * 1e3;
|
|
56
58
|
var COOLDOWN_BASE_MS = 60 * 1e3;
|
|
57
59
|
var COOLDOWN_MAX_MS = 15 * 60 * 1e3;
|
|
58
60
|
var QUOTA_COOLDOWN_BASE_MS = 15 * 60 * 1e3;
|
|
@@ -63,6 +65,8 @@ var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
|
63
65
|
var USER_CONFIG_DIR = join2(homedir(), ".config", "lacode");
|
|
64
66
|
var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
|
|
65
67
|
var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
|
|
68
|
+
var CONTEXT_OVERFLOW_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
69
|
+
var BELOW_MIN_CONTEXT_COOLDOWN_MS = 6 * 60 * 60 * 1e3;
|
|
66
70
|
var STATE_SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
|
|
67
71
|
var CHILD_MEMORY_MS = 30 * 6e4;
|
|
68
72
|
|
package/dist/pool.js
CHANGED
|
@@ -25,6 +25,7 @@ var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
|
25
25
|
|
|
26
26
|
// src/lib/pool.ts
|
|
27
27
|
var CLAIM_TTL_MS = 10 * 60 * 1e3;
|
|
28
|
+
var HEARTBEAT_PUBLISH_MS = 3e4;
|
|
28
29
|
var UNBOUND_CLAIM_MS = 2 * 60 * 1e3;
|
|
29
30
|
var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
|
|
30
31
|
var ORPHAN_TMP_MS = 60 * 1e3;
|
|
@@ -36,7 +37,15 @@ var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
|
|
36
37
|
var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
37
38
|
var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
|
|
38
39
|
var UNAVAILABLE_REASON = "endpoint-unavailable";
|
|
40
|
+
var PROVIDER_DEGRADED_REASON = "provider-degraded";
|
|
41
|
+
var PROVIDER_DEGRADED_COOLDOWN_MS = 30 * 60 * 1e3;
|
|
42
|
+
var PROVIDER_DEGRADED_WINDOW_MS = 10 * 60 * 1e3;
|
|
43
|
+
var PROVIDER_DEGRADED_MIN_MODELS = 2;
|
|
44
|
+
var TRANSIENT_DEGRADATION_REASONS = /^(?:no answer|no-answer|hang|timeout|timed out|endpoint-unavailable|provider-degraded|empty response|network|server)$/i;
|
|
45
|
+
var FLAT_OUTAGE_REASONS = /* @__PURE__ */ new Set([UNAVAILABLE_REASON, PROVIDER_DEGRADED_REASON]);
|
|
39
46
|
var MAX_WEIGHT = 100;
|
|
47
|
+
var MIN_SLOT_CONTEXT = 1e5;
|
|
48
|
+
var MIN_SLOT_TPM = 2e4;
|
|
40
49
|
var slotCeiling = (slot) => slot.weight;
|
|
41
50
|
var slotBucket = (slot) => slot.bucket ?? slot.model;
|
|
42
51
|
var NOTIFY_DEFAULT_ON_PROVIDERS = /* @__PURE__ */ new Set(["opencode", "opencode-go"]);
|
|
@@ -67,6 +76,7 @@ var isModelMissingText = (text) => MODEL_MISSING_PATTERN.test(text);
|
|
|
67
76
|
var AUTH_FAILURE_PATTERN = /\b401\b|\b403\b|unauthori[sz]ed|not\s+authori[sz]ed|invalid[\s_-]+(?:api[\s_-]?(?:key|token)|token|credential|bearer)|api[\s_-]?key\s+(?:is\s+)?(?:invalid|missing|expired|not\s+valid)|authentication\s+(?:failed|error|required)|incorrect[\s_-]+api[\s_-]?key/i;
|
|
68
77
|
var isAuthFailureText = (text) => AUTH_FAILURE_PATTERN.test(text);
|
|
69
78
|
var BASES = ["explore", "implement", "context"];
|
|
79
|
+
var LEARNED_TPM_FILE = "learned-tpm.json";
|
|
70
80
|
var PRESETS_DIR = join2(PACKAGE_ROOT, "presets");
|
|
71
81
|
var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
72
82
|
var USER_CONFIG_DIR = join2(homedir(), ".config", "lacode");
|
|
@@ -116,7 +126,22 @@ function parseSlots(parsed, origin) {
|
|
|
116
126
|
if (Number.isFinite(parsedContext) && parsedContext > 0) context = parsedContext;
|
|
117
127
|
else problems.push(`${at} (${model}): context must be a positive number, got ${JSON.stringify(rawContext)}; ignoring it`);
|
|
118
128
|
}
|
|
119
|
-
|
|
129
|
+
if (context !== void 0 && context < MIN_SLOT_CONTEXT) {
|
|
130
|
+
problems.push(`${at} (${model}): context ${context} is below the ${MIN_SLOT_CONTEXT}-token minimum; dropping the slot`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const rawTpm = entry?.tpm;
|
|
134
|
+
let tpm;
|
|
135
|
+
if (rawTpm !== void 0) {
|
|
136
|
+
const parsedTpm = typeof rawTpm === "number" ? rawTpm : Number(rawTpm);
|
|
137
|
+
if (Number.isFinite(parsedTpm) && parsedTpm > 0) tpm = parsedTpm;
|
|
138
|
+
else problems.push(`${at} (${model}): tpm must be a positive number, got ${JSON.stringify(rawTpm)}; ignoring it`);
|
|
139
|
+
}
|
|
140
|
+
if (tpm !== void 0 && tpm < MIN_SLOT_TPM) {
|
|
141
|
+
problems.push(`${at} (${model}): tpm ${tpm} is below the ${MIN_SLOT_TPM}-token/minute minimum; dropping the slot`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
slots.push({ index: slots.length + 1, model, weight, ...bucket ? { bucket } : {}, notify, ...context === void 0 ? {} : { context }, ...tpm === void 0 ? {} : { tpm } });
|
|
120
145
|
});
|
|
121
146
|
if (slots.length > SLOTS_WARN_AT) {
|
|
122
147
|
problems.push(`${origin}: ${slots.length} slots is unusually large, but every entry is kept (the pool has no slot cap)`);
|
|
@@ -137,6 +162,7 @@ function parseTextSlots(text, origin) {
|
|
|
137
162
|
let bucket;
|
|
138
163
|
let notify;
|
|
139
164
|
let context;
|
|
165
|
+
let tpm;
|
|
140
166
|
for (const token of rest) {
|
|
141
167
|
if (token.startsWith("notify:")) {
|
|
142
168
|
if (notify === void 0) notify = token.slice("notify:".length);
|
|
@@ -149,13 +175,20 @@ function parseTextSlots(text, origin) {
|
|
|
149
175
|
}
|
|
150
176
|
continue;
|
|
151
177
|
}
|
|
178
|
+
if (token.startsWith("tpm:")) {
|
|
179
|
+
if (tpm === void 0) {
|
|
180
|
+
const parsedTpm = Number(token.slice(4));
|
|
181
|
+
if (Number.isFinite(parsedTpm) && parsedTpm > 0) tpm = parsedTpm;
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
152
185
|
if (token.startsWith("b:")) {
|
|
153
186
|
if (bucket === void 0) bucket = token.slice(2);
|
|
154
187
|
} else if (weight === void 0) {
|
|
155
188
|
weight = token;
|
|
156
189
|
}
|
|
157
190
|
}
|
|
158
|
-
entries.push({ model, ...weight === void 0 ? {} : { weight }, ...bucket === void 0 ? {} : { bucket }, ...notify === void 0 ? {} : { notify }, ...context === void 0 ? {} : { context } });
|
|
191
|
+
entries.push({ model, ...weight === void 0 ? {} : { weight }, ...bucket === void 0 ? {} : { bucket }, ...notify === void 0 ? {} : { notify }, ...context === void 0 ? {} : { context }, ...tpm === void 0 ? {} : { tpm } });
|
|
159
192
|
}
|
|
160
193
|
return parseSlots({ slots: entries }, origin);
|
|
161
194
|
}
|
|
@@ -275,6 +308,43 @@ var LIMIT_PATTERNS = [
|
|
|
275
308
|
];
|
|
276
309
|
var LIMIT_REACHED_PATTERN = /\blimits?[\s_-]?(?:reached|exceeded)\b|\breached\s+(?:your|the|its)\s+limit\b/i;
|
|
277
310
|
var CONTEXT_LIMIT_PATTERN = /(?:context|token|length|window|prompt|char|character|byte|size)\s*(?:limit|length|size|window)/i;
|
|
311
|
+
var CONTEXT_OVERFLOW_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
312
|
+
var CONTEXT_OVERFLOW_PATTERNS = [
|
|
313
|
+
/context[_\s-]*(?:length|window|limit|overflow|exceed)/i,
|
|
314
|
+
/input\s+too\s+long/i,
|
|
315
|
+
/prompt\s+is\s+too\s+long/i,
|
|
316
|
+
/maximum\s+(?:context|prompt|token)/i,
|
|
317
|
+
/reduce\s+the\s+length\s+of\s+(?:the\s+)?messages/i
|
|
318
|
+
];
|
|
319
|
+
var isContextOverflowText = (text) => CONTEXT_OVERFLOW_PATTERNS.some((re) => re.test(text));
|
|
320
|
+
var BELOW_MIN_CONTEXT_COOLDOWN_MS = 6 * 60 * 60 * 1e3;
|
|
321
|
+
var REQUEST_TOO_LARGE_PATTERNS = [
|
|
322
|
+
/request\s+too\s+large/i,
|
|
323
|
+
/please\s+reduce\s+your\s+message\s+size/i,
|
|
324
|
+
/limit\s+\d+\s*,\s*requested\s+\d+/i,
|
|
325
|
+
/\(tpm\)/i
|
|
326
|
+
];
|
|
327
|
+
var isRequestTooLargeText = (text) => REQUEST_TOO_LARGE_PATTERNS.some((re) => re.test(text));
|
|
328
|
+
var parseStatedTokenLimit = (text) => {
|
|
329
|
+
for (const re of [/limit\s+(\d+)\s*,\s*requested\s+\d+/i, /limit\s+is\s+(\d+)/i]) {
|
|
330
|
+
const match = text.match(re);
|
|
331
|
+
if (match) {
|
|
332
|
+
const value = Number(match[1]);
|
|
333
|
+
if (Number.isFinite(value) && value > 0) return value;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return void 0;
|
|
337
|
+
};
|
|
338
|
+
function parseRateLimitHeaders(headers) {
|
|
339
|
+
if (typeof headers !== "object" || headers === null || Array.isArray(headers)) return void 0;
|
|
340
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
341
|
+
if (key.toLowerCase() !== "x-ratelimit-limit-tokens") continue;
|
|
342
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value.trim()) : Number.NaN;
|
|
343
|
+
if (Number.isFinite(parsed) && parsed > 0 && Number.isInteger(parsed)) return parsed;
|
|
344
|
+
return void 0;
|
|
345
|
+
}
|
|
346
|
+
return void 0;
|
|
347
|
+
}
|
|
278
348
|
var LIMIT_REASON_PATTERN = /rate[\s_-]?limit|usage limit|too many requests|quota|freelimiterror|overloaded|limits?[\s_-]?(?:reached|exceeded)/i;
|
|
279
349
|
var isLimitText = (text) => LIMIT_PATTERNS.some((re) => re.test(text)) || LIMIT_REACHED_PATTERN.test(text) && !CONTEXT_LIMIT_PATTERN.test(text);
|
|
280
350
|
var limitReason = (text) => {
|
|
@@ -341,6 +411,7 @@ function state(opts = {}) {
|
|
|
341
411
|
reaping: false,
|
|
342
412
|
lastSweep: 0,
|
|
343
413
|
lastDirSweep: 0,
|
|
414
|
+
lastHeartbeatPublish: 0,
|
|
344
415
|
connected: /* @__PURE__ */ new Set(),
|
|
345
416
|
connectedAt: 0,
|
|
346
417
|
connectedReady: false,
|
|
@@ -359,26 +430,75 @@ function state(opts = {}) {
|
|
|
359
430
|
function ensureSlots(s, onWarn) {
|
|
360
431
|
if (s.slots.length > 0) return s.slots;
|
|
361
432
|
const { slots, problems, origin } = resolveModels({ models: s.inlineModels, modelsFile: s.modelsFile });
|
|
362
|
-
s.slots = slots;
|
|
433
|
+
s.slots = applyLearnedTpm(s, slots, onWarn);
|
|
363
434
|
s.origin = origin;
|
|
364
435
|
for (const problem of problems) onWarn(problem);
|
|
365
436
|
return slots;
|
|
366
437
|
}
|
|
367
|
-
var slotSignature = (origin, slots) => `${origin}\0${slots.map((slot) => `${slot.index}:${slot.model}:${slot.weight}:${slot.bucket ?? ""}:${slotNotify(slot) ? "1" : "0"}`).join("|")}`;
|
|
438
|
+
var slotSignature = (origin, slots) => `${origin}\0${slots.map((slot) => `${slot.index}:${slot.model}:${slot.weight}:${slot.bucket ?? ""}:${slotNotify(slot) ? "1" : "0"}:${slot.tpm ?? ""}`).join("|")}`;
|
|
368
439
|
function reloadSlots(s, onWarn) {
|
|
369
440
|
const { slots, problems, origin } = resolveModels({ models: s.inlineModels, modelsFile: s.modelsFile });
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
s.slots
|
|
441
|
+
const effective = applyLearnedTpm(s, slots, onWarn);
|
|
442
|
+
const changed = slotSignature(origin, effective) !== slotSignature(s.origin, s.slots);
|
|
443
|
+
if (!changed) return { changed, slots: s.slots, origin: s.origin };
|
|
444
|
+
s.slots = effective;
|
|
373
445
|
s.origin = origin;
|
|
374
446
|
for (const problem of problems) onWarn(problem);
|
|
375
|
-
return { changed, slots, origin };
|
|
447
|
+
return { changed, slots: effective, origin };
|
|
376
448
|
}
|
|
377
449
|
function slotSources(input = {}) {
|
|
378
450
|
if (input.models !== void 0) return [];
|
|
379
451
|
if (input.modelsFile) return [input.modelsFile];
|
|
380
452
|
return [USER_MODELS_TXT, USER_MODELS_PATH, DEFAULT_PRESET];
|
|
381
453
|
}
|
|
454
|
+
var learnedTpmPath = (s) => join2(s.dir, LEARNED_TPM_FILE);
|
|
455
|
+
function readLearnedTpm(s) {
|
|
456
|
+
try {
|
|
457
|
+
const raw = JSON.parse(readFileSync2(learnedTpmPath(s), "utf8"));
|
|
458
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
|
|
459
|
+
const out = {};
|
|
460
|
+
for (const [model, value] of Object.entries(raw)) {
|
|
461
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
462
|
+
if (Number.isFinite(n) && n > 0) out[model] = n;
|
|
463
|
+
}
|
|
464
|
+
return out;
|
|
465
|
+
} catch {
|
|
466
|
+
return {};
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function applyLearnedTpm(s, slots, onWarn) {
|
|
470
|
+
const learned = readLearnedTpm(s);
|
|
471
|
+
const out = [];
|
|
472
|
+
for (const slot of slots) {
|
|
473
|
+
const tpm = learned[slot.model];
|
|
474
|
+
if (tpm !== void 0 && tpm < MIN_SLOT_TPM) {
|
|
475
|
+
onWarn?.(`learned-tpm: dropping ${slot.model} (tpm ${tpm} < ${MIN_SLOT_TPM})`);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (tpm !== void 0) out.push({ ...slot, tpm: Math.min(slot.tpm ?? tpm, tpm) });
|
|
479
|
+
else out.push(slot);
|
|
480
|
+
}
|
|
481
|
+
return out.map((slot, i) => ({ ...slot, index: i + 1 }));
|
|
482
|
+
}
|
|
483
|
+
function noteTpm(s, model, tpm) {
|
|
484
|
+
if (!Number.isFinite(tpm) || tpm <= 0) return false;
|
|
485
|
+
const map = readLearnedTpm(s);
|
|
486
|
+
map[model] = tpm;
|
|
487
|
+
if (s.dirReady) {
|
|
488
|
+
try {
|
|
489
|
+
const path = learnedTpmPath(s);
|
|
490
|
+
writeFileSync(`${path}.tmp`, JSON.stringify(map));
|
|
491
|
+
renameSync(`${path}.tmp`, path);
|
|
492
|
+
} catch {
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (tpm < MIN_SLOT_TPM) {
|
|
496
|
+
s.slots = s.slots.filter((slot) => slot.model !== model).map((slot, i) => ({ ...slot, index: i + 1 }));
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
s.slots = s.slots.map((slot) => slot.model === model ? { ...slot, tpm: Math.min(slot.tpm ?? tpm, tpm) } : slot);
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
382
502
|
var claimKey = (sessionID, callID) => `${sessionID}:${callID}`;
|
|
383
503
|
var selfFile = (id) => `claims.${id}.json`;
|
|
384
504
|
var limitFile = (model) => `limit.${model.replace(/[^a-zA-Z0-9._-]/g, "__")}.json`;
|
|
@@ -597,7 +717,7 @@ function readLimits(s) {
|
|
|
597
717
|
}
|
|
598
718
|
function recordStrike(s, model, reason, stated) {
|
|
599
719
|
if (!s.dirReady) return;
|
|
600
|
-
if (reason
|
|
720
|
+
if (FLAT_OUTAGE_REASONS.has(reason)) return coolUnavailable(s, model, stated, reason);
|
|
601
721
|
const now = Date.now();
|
|
602
722
|
const slotsForModel = s.slots.filter((candidate) => candidate.model === model);
|
|
603
723
|
const buckets = [...new Set(slotsForModel.map((slot) => slotBucket(slot)))];
|
|
@@ -628,13 +748,14 @@ function recordStrike(s, model, reason, stated) {
|
|
|
628
748
|
}
|
|
629
749
|
return strike;
|
|
630
750
|
}
|
|
631
|
-
function coolUnavailable(s, model) {
|
|
751
|
+
function coolUnavailable(s, model, windowMs = UNAVAILABLE_COOLDOWN_MS, reason = UNAVAILABLE_REASON) {
|
|
632
752
|
if (!s.dirReady) return;
|
|
633
753
|
const now = Date.now();
|
|
754
|
+
const requested = Number.isFinite(windowMs) && windowMs > 0 ? Math.min(windowMs, STATED_COOLDOWN_MAX_MS) : UNAVAILABLE_COOLDOWN_MS;
|
|
634
755
|
const prior = s.cooling.get(model);
|
|
635
|
-
const until = now +
|
|
756
|
+
const until = now + requested;
|
|
636
757
|
if (prior && prior.until > until) return prior;
|
|
637
|
-
const strike = { model, until, strikes: 1, reason
|
|
758
|
+
const strike = { model, until, strikes: 1, reason, last: now };
|
|
638
759
|
s.cooling.set(model, strike);
|
|
639
760
|
const path = join2(s.dir, limitFile(model));
|
|
640
761
|
try {
|
|
@@ -730,24 +851,23 @@ var loadOf = (counts, slot) => counts.get(slot) ?? 0;
|
|
|
730
851
|
var underCeiling = (slot, counts) => loadOf(counts, slot.index) < slotCeiling(slot);
|
|
731
852
|
function pickSlot(s, snap, neededContext) {
|
|
732
853
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
const
|
|
854
|
+
const usable = slots.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
855
|
+
if (usable.length === 0) return void 0;
|
|
856
|
+
const effective = (slot) => Math.min(slot.context ?? Number.POSITIVE_INFINITY, slot.tpm ?? Number.POSITIVE_INFINITY);
|
|
736
857
|
if (neededContext === void 0) {
|
|
737
858
|
for (const slot of usable) {
|
|
738
859
|
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
739
860
|
}
|
|
740
|
-
return
|
|
861
|
+
return usable.reduce((best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best, usable[0]);
|
|
741
862
|
}
|
|
742
|
-
const adequate = usable.filter((slot) => slot
|
|
743
|
-
const adequateKnown = adequate.filter((slot) => slot
|
|
863
|
+
const adequate = usable.filter((slot) => effective(slot) >= neededContext);
|
|
864
|
+
const adequateKnown = adequate.filter((slot) => Number.isFinite(effective(slot)));
|
|
744
865
|
const candidates = adequate.length > 0 ? adequate : usable;
|
|
745
|
-
const fallbackPool = adequate.length > 0 ? adequate : pool;
|
|
746
866
|
if (adequateKnown.length === 0) {
|
|
747
|
-
const known = usable.filter((slot) => slot
|
|
867
|
+
const known = usable.filter((slot) => Number.isFinite(effective(slot)));
|
|
748
868
|
if (known.length > 0) {
|
|
749
|
-
const largest = Math.max(...known.map((slot) => slot
|
|
750
|
-
const largestSlots = known.filter((slot) => slot
|
|
869
|
+
const largest = Math.max(...known.map((slot) => effective(slot)));
|
|
870
|
+
const largestSlots = known.filter((slot) => effective(slot) === largest);
|
|
751
871
|
for (const slot of largestSlots) {
|
|
752
872
|
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
753
873
|
}
|
|
@@ -756,20 +876,22 @@ function pickSlot(s, snap, neededContext) {
|
|
|
756
876
|
for (const slot of candidates) {
|
|
757
877
|
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
758
878
|
}
|
|
759
|
-
return
|
|
879
|
+
return candidates.reduce((best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best, candidates[0]);
|
|
760
880
|
}
|
|
761
881
|
const underWeight = candidates.filter((slot) => loadOf(snap.counts, slot.index) < slot.weight);
|
|
762
882
|
if (underWeight.length > 0) {
|
|
763
|
-
const known = underWeight.filter((slot) => slot
|
|
883
|
+
const known = underWeight.filter((slot) => Number.isFinite(effective(slot)));
|
|
764
884
|
if (known.length === 0) return underWeight[0];
|
|
765
|
-
return known.reduce((best, slot) => slot
|
|
885
|
+
return known.reduce((best, slot) => effective(slot) < effective(best) ? slot : best, known[0]);
|
|
766
886
|
}
|
|
767
887
|
return candidates.reduce((best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best, candidates[0]);
|
|
768
888
|
}
|
|
769
889
|
function poolPass(s, snap, neededContext) {
|
|
770
890
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
771
891
|
const usable = slots.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
772
|
-
|
|
892
|
+
if (usable.length === 0) return "none";
|
|
893
|
+
const effective = (slot) => Math.min(slot.context ?? Number.POSITIVE_INFINITY, slot.tpm ?? Number.POSITIVE_INFINITY);
|
|
894
|
+
const candidates = neededContext === void 0 ? usable : usable.filter((slot) => effective(slot) >= neededContext);
|
|
773
895
|
const pool = candidates.length > 0 ? candidates : usable;
|
|
774
896
|
if (pool.some((slot) => loadOf(snap.counts, slot.index) < slot.weight)) return 1;
|
|
775
897
|
return "overflow";
|
|
@@ -831,7 +953,10 @@ function noteActivity(s, sessionID) {
|
|
|
831
953
|
s.activity.set(sessionID, now);
|
|
832
954
|
let touched = false;
|
|
833
955
|
for (const claim of s.own.values()) if (claim.g === sessionID) claim.hb = now, touched = true;
|
|
834
|
-
if (touched
|
|
956
|
+
if (touched && now - s.lastHeartbeatPublish >= HEARTBEAT_PUBLISH_MS) {
|
|
957
|
+
s.lastHeartbeatPublish = now;
|
|
958
|
+
publish(s);
|
|
959
|
+
}
|
|
835
960
|
}
|
|
836
961
|
function idleFor(s, claim, now) {
|
|
837
962
|
const target = claim.g;
|
|
@@ -877,6 +1002,24 @@ function coolProvider(s, provider, until, reason) {
|
|
|
877
1002
|
}
|
|
878
1003
|
return affected;
|
|
879
1004
|
}
|
|
1005
|
+
function noteProviderDegradation(s, model, reason) {
|
|
1006
|
+
if (!s.dirReady) return [];
|
|
1007
|
+
if (!TRANSIENT_DEGRADATION_REASONS.test(String(reason))) return [];
|
|
1008
|
+
const provider = providerOf(model);
|
|
1009
|
+
const slots = (s.slots.length > 0 ? s.slots : FALLBACK_SLOTS).filter((slot) => providerOf(slot.model) === provider);
|
|
1010
|
+
if (slots.length < PROVIDER_DEGRADED_MIN_MODELS) return [];
|
|
1011
|
+
const now = Date.now();
|
|
1012
|
+
const failing = /* @__PURE__ */ new Set();
|
|
1013
|
+
for (const slot of slots) {
|
|
1014
|
+
const record = s.cooling.get(slot.model);
|
|
1015
|
+
if (!record || record.until <= now) continue;
|
|
1016
|
+
if (now - record.last > PROVIDER_DEGRADED_WINDOW_MS) continue;
|
|
1017
|
+
if (!TRANSIENT_DEGRADATION_REASONS.test(String(record.reason))) continue;
|
|
1018
|
+
failing.add(slot.model);
|
|
1019
|
+
}
|
|
1020
|
+
if (failing.size < PROVIDER_DEGRADED_MIN_MODELS) return [];
|
|
1021
|
+
return coolProvider(s, provider, now + PROVIDER_DEGRADED_COOLDOWN_MS, PROVIDER_DEGRADED_REASON);
|
|
1022
|
+
}
|
|
880
1023
|
function recordFailure(s, f) {
|
|
881
1024
|
try {
|
|
882
1025
|
const path = join2(s.dir, "failures.jsonl");
|
|
@@ -931,19 +1074,27 @@ export {
|
|
|
931
1074
|
AUTH_FAILURE_COOLDOWN_MS,
|
|
932
1075
|
AUTH_FAILURE_REASON,
|
|
933
1076
|
BASES,
|
|
1077
|
+
BELOW_MIN_CONTEXT_COOLDOWN_MS,
|
|
934
1078
|
CATALOG_TTL_MS,
|
|
935
1079
|
CLAIM_TTL_MS,
|
|
1080
|
+
CONTEXT_OVERFLOW_COOLDOWN_MS,
|
|
936
1081
|
DEAD_FILE_MS,
|
|
937
1082
|
DEFAULT_PRESET,
|
|
938
1083
|
FALLBACK_SLOTS,
|
|
939
1084
|
HANG_COOLDOWN_MS,
|
|
1085
|
+
HEARTBEAT_PUBLISH_MS,
|
|
1086
|
+
LEARNED_TPM_FILE,
|
|
940
1087
|
MAX_CLAIM_AGE_MS,
|
|
941
1088
|
MAX_WEIGHT,
|
|
1089
|
+
MIN_SLOT_CONTEXT,
|
|
1090
|
+
MIN_SLOT_TPM,
|
|
942
1091
|
MODEL_MISSING_COOLDOWN_MS,
|
|
943
1092
|
MODEL_MISSING_REASON,
|
|
944
1093
|
ORPHAN_TMP_MS,
|
|
945
1094
|
PACKAGE_ROOT,
|
|
946
1095
|
PRESETS_DIR,
|
|
1096
|
+
PROVIDER_DEGRADED_COOLDOWN_MS,
|
|
1097
|
+
PROVIDER_DEGRADED_REASON,
|
|
947
1098
|
PROVIDER_LIST_TTL_MS,
|
|
948
1099
|
QUOTA_COOLDOWN_BASE_MS,
|
|
949
1100
|
QUOTA_COOLDOWN_MAX_MS,
|
|
@@ -961,6 +1112,7 @@ export {
|
|
|
961
1112
|
acquire,
|
|
962
1113
|
agentFor,
|
|
963
1114
|
announcesReset,
|
|
1115
|
+
applyLearnedTpm,
|
|
964
1116
|
backfillChild,
|
|
965
1117
|
bindChild,
|
|
966
1118
|
claimKey,
|
|
@@ -975,19 +1127,25 @@ export {
|
|
|
975
1127
|
init,
|
|
976
1128
|
isAuthFailureText,
|
|
977
1129
|
isAvailable,
|
|
1130
|
+
isContextOverflowText,
|
|
978
1131
|
isLimitText,
|
|
979
1132
|
isModelMissingText,
|
|
980
1133
|
isProbe,
|
|
981
1134
|
isQuotaReason,
|
|
1135
|
+
isRequestTooLargeText,
|
|
982
1136
|
isTransientGatewayText,
|
|
983
1137
|
isUnavailableText,
|
|
984
1138
|
limitReason,
|
|
985
1139
|
manualVariant,
|
|
986
1140
|
markProbe,
|
|
987
1141
|
noteActivity,
|
|
1142
|
+
noteProviderDegradation,
|
|
1143
|
+
noteTpm,
|
|
988
1144
|
notifyDefault,
|
|
1145
|
+
parseRateLimitHeaders,
|
|
989
1146
|
parseSlotText,
|
|
990
1147
|
parseSlots,
|
|
1148
|
+
parseStatedTokenLimit,
|
|
991
1149
|
parseTextSlots,
|
|
992
1150
|
pickSlot,
|
|
993
1151
|
poolPass,
|
|
@@ -997,6 +1155,7 @@ export {
|
|
|
997
1155
|
pruneSessions,
|
|
998
1156
|
publish,
|
|
999
1157
|
readFailures,
|
|
1158
|
+
readLearnedTpm,
|
|
1000
1159
|
readLimits,
|
|
1001
1160
|
readSiblings,
|
|
1002
1161
|
readSlotFile,
|
package/dist/tui.js
CHANGED
|
@@ -822,33 +822,12 @@ function createVoiceClient(options, deps) {
|
|
|
822
822
|
}
|
|
823
823
|
|
|
824
824
|
// src/tui/timeline.tsx
|
|
825
|
-
import {
|
|
825
|
+
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
826
826
|
import { homedir as homedir2 } from "node:os";
|
|
827
827
|
import { join as join3, resolve } from "node:path";
|
|
828
828
|
import { jsx, jsxs } from "@opentui/solid/jsx-runtime";
|
|
829
829
|
var registered = false;
|
|
830
830
|
var ENTRY_LINES = 14;
|
|
831
|
-
var readEntries = (id) => {
|
|
832
|
-
if (typeof id !== "string" || !/^[A-Za-z0-9_.-]+$/.test(id)) return [];
|
|
833
|
-
const rawDir = process.env.LACODE_TIMELINE_DIR;
|
|
834
|
-
const dir = rawDir ? resolve(rawDir) : join3(homedir2(), ".local/share/opencode/lacode/timeline");
|
|
835
|
-
try {
|
|
836
|
-
const raw = readFileSync3(join3(dir, id + ".jsonl"), "utf8");
|
|
837
|
-
const parsed = [];
|
|
838
|
-
for (const line of raw.split("\n")) {
|
|
839
|
-
const trimmed = line.trim();
|
|
840
|
-
if (!trimmed) continue;
|
|
841
|
-
try {
|
|
842
|
-
const obj = JSON.parse(trimmed);
|
|
843
|
-
if (obj && typeof obj.ts === "number" && typeof obj.text === "string") parsed.push(obj.text);
|
|
844
|
-
} catch {
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
return parsed.slice(-ENTRY_LINES).reverse();
|
|
848
|
-
} catch {
|
|
849
|
-
return [];
|
|
850
|
-
}
|
|
851
|
-
};
|
|
852
831
|
var tui = async (api) => {
|
|
853
832
|
if (registered) return;
|
|
854
833
|
registered = true;
|
|
@@ -862,15 +841,80 @@ var tui = async (api) => {
|
|
|
862
841
|
sidebar_content(ctx, props) {
|
|
863
842
|
const rows = [];
|
|
864
843
|
const muted = ctx.theme.current.textMuted;
|
|
844
|
+
const cache = { path: null, size: -1, mtimeMs: -1, ino: -1, entries: [], rendered: [] };
|
|
845
|
+
const readEntries = (id) => {
|
|
846
|
+
if (typeof id !== "string" || !/^[A-Za-z0-9_.-]+$/.test(id)) return [];
|
|
847
|
+
const rawDir = process.env.LACODE_TIMELINE_DIR;
|
|
848
|
+
const dir = rawDir ? resolve(rawDir) : join3(homedir2(), ".local/share/opencode/lacode/timeline");
|
|
849
|
+
const path = join3(dir, id + ".jsonl");
|
|
850
|
+
try {
|
|
851
|
+
const stat = statSync(path);
|
|
852
|
+
if (cache.path === path && cache.size === stat.size && cache.mtimeMs === stat.mtimeMs && cache.ino === stat.ino) {
|
|
853
|
+
return cache.entries;
|
|
854
|
+
}
|
|
855
|
+
const maxRead = 64 * 1024;
|
|
856
|
+
const start = Math.max(0, stat.size - maxRead);
|
|
857
|
+
const length = stat.size - start;
|
|
858
|
+
const fd = openSync(path, "r");
|
|
859
|
+
try {
|
|
860
|
+
const buf = Buffer.alloc(length);
|
|
861
|
+
let total = 0;
|
|
862
|
+
while (total < length) {
|
|
863
|
+
const n = readSync(fd, buf, total, length - total, start + total);
|
|
864
|
+
if (n <= 0) break;
|
|
865
|
+
total += n;
|
|
866
|
+
}
|
|
867
|
+
if (total < length) {
|
|
868
|
+
return [];
|
|
869
|
+
}
|
|
870
|
+
let raw = buf.toString("utf8");
|
|
871
|
+
if (start > 0) {
|
|
872
|
+
const firstNl = raw.indexOf("\n");
|
|
873
|
+
raw = firstNl === -1 ? "" : raw.slice(firstNl + 1);
|
|
874
|
+
}
|
|
875
|
+
const parsed = [];
|
|
876
|
+
for (const line of raw.split("\n")) {
|
|
877
|
+
const trimmed = line.trim();
|
|
878
|
+
if (!trimmed) continue;
|
|
879
|
+
try {
|
|
880
|
+
const obj = JSON.parse(trimmed);
|
|
881
|
+
if (obj && typeof obj.ts === "number" && typeof obj.text === "string") {
|
|
882
|
+
parsed.push({ ts: obj.ts, text: obj.text });
|
|
883
|
+
}
|
|
884
|
+
} catch {
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
const entries = parsed.slice(-ENTRY_LINES).reverse();
|
|
888
|
+
cache.path = path;
|
|
889
|
+
cache.size = stat.size;
|
|
890
|
+
cache.mtimeMs = stat.mtimeMs;
|
|
891
|
+
cache.ino = stat.ino;
|
|
892
|
+
cache.entries = entries;
|
|
893
|
+
return entries;
|
|
894
|
+
} finally {
|
|
895
|
+
closeSync(fd);
|
|
896
|
+
}
|
|
897
|
+
} catch {
|
|
898
|
+
return [];
|
|
899
|
+
}
|
|
900
|
+
};
|
|
865
901
|
const paint = () => {
|
|
866
902
|
const entries = readEntries(props.session_id);
|
|
903
|
+
const next = [];
|
|
904
|
+
for (let i = 0; i < ENTRY_LINES; i++) {
|
|
905
|
+
const text = entries[i]?.text;
|
|
906
|
+
next.push(text ? `\u2022 ${text}` : i === 0 && entries.length === 0 ? "No timeline yet" : "");
|
|
907
|
+
}
|
|
908
|
+
if (cache.rendered.length === ENTRY_LINES && next.every((value, i) => value === cache.rendered[i])) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
867
911
|
for (let i = 0; i < ENTRY_LINES; i++) {
|
|
868
912
|
const row = rows[i];
|
|
869
913
|
if (!row) continue;
|
|
870
|
-
|
|
871
|
-
row.content = text ? `\u2022 ${text}` : i === 0 && entries.length === 0 ? "No timeline yet" : "";
|
|
914
|
+
row.content = next[i];
|
|
872
915
|
row.requestRender?.();
|
|
873
916
|
}
|
|
917
|
+
cache.rendered = next;
|
|
874
918
|
};
|
|
875
919
|
const timer = setInterval(paint, 1e3);
|
|
876
920
|
timer.unref?.();
|
|
@@ -899,6 +943,47 @@ var plugin = {
|
|
|
899
943
|
};
|
|
900
944
|
var timeline_default = plugin;
|
|
901
945
|
|
|
946
|
+
// src/lib/modes.ts
|
|
947
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync } from "node:fs";
|
|
948
|
+
import { homedir as homedir3 } from "node:os";
|
|
949
|
+
import { dirname, join as join4 } from "node:path";
|
|
950
|
+
var MODES_FILE = process.env.LACODE_MODES_FILE ?? join4(homedir3(), ".config", "lacode", "modes.json");
|
|
951
|
+
var bool = (value, fallback) => typeof value === "boolean" ? value : fallback;
|
|
952
|
+
function readModes(defaults = {}) {
|
|
953
|
+
const fallback = { yolo: defaults.yolo === true, ykb: defaults.ykb === true };
|
|
954
|
+
try {
|
|
955
|
+
const raw = JSON.parse(readFileSync3(MODES_FILE, "utf8"));
|
|
956
|
+
return { yolo: bool(raw?.yolo, fallback.yolo), ykb: bool(raw?.ykb, fallback.ykb) };
|
|
957
|
+
} catch {
|
|
958
|
+
return fallback;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
function writeModes(modes) {
|
|
962
|
+
const path = MODES_FILE;
|
|
963
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
964
|
+
const tmp = `${path}.tmp`;
|
|
965
|
+
writeFileSync(tmp, `${JSON.stringify({ yolo: modes.yolo, ykb: modes.ykb }, null, 2)}
|
|
966
|
+
`);
|
|
967
|
+
renameSync(tmp, path);
|
|
968
|
+
}
|
|
969
|
+
function toggleMode(name, defaults = {}) {
|
|
970
|
+
const current = readModes(defaults);
|
|
971
|
+
const next = { ...current, [name]: !current[name] };
|
|
972
|
+
writeModes(next);
|
|
973
|
+
return next;
|
|
974
|
+
}
|
|
975
|
+
function recommendedAnswer(question) {
|
|
976
|
+
const q = question;
|
|
977
|
+
const options = (Array.isArray(q?.options) ? q.options : []).filter(
|
|
978
|
+
(option) => typeof option?.label === "string" && option.label.length > 0
|
|
979
|
+
);
|
|
980
|
+
if (options.length === 0) return [];
|
|
981
|
+
const recommended = options.find(
|
|
982
|
+
(option) => /\(recommended\)/i.test(option.label) || typeof option.description === "string" && /\(recommended\)/i.test(option.description)
|
|
983
|
+
);
|
|
984
|
+
return [(recommended ?? options[0]).label];
|
|
985
|
+
}
|
|
986
|
+
|
|
902
987
|
// src/tui/index.ts
|
|
903
988
|
var DEFAULT_MODEL = "whisper-large-v3-turbo";
|
|
904
989
|
var DEFAULT_LANGUAGE = "en";
|
|
@@ -1328,6 +1413,46 @@ var tui2 = async (api, options) => {
|
|
|
1328
1413
|
toast(api, { variant: "error", title: "lacode voice", message: readableError(err) });
|
|
1329
1414
|
}
|
|
1330
1415
|
};
|
|
1416
|
+
async function drainPendingMode(name) {
|
|
1417
|
+
try {
|
|
1418
|
+
const pending = await fetchPending(api);
|
|
1419
|
+
for (const item of pending) {
|
|
1420
|
+
if (name === "yolo" && item.kind === "permission") {
|
|
1421
|
+
await api.client.permission.reply({ requestID: item.request.id, reply: "once" });
|
|
1422
|
+
}
|
|
1423
|
+
if (name === "ykb" && item.kind === "question") {
|
|
1424
|
+
const answers = item.request.questions.map((question) => recommendedAnswer(question));
|
|
1425
|
+
if (answers.some((answer) => answer.length > 0)) {
|
|
1426
|
+
await api.client.question.reply({ requestID: item.request.id, answers });
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
} catch {
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
async function runToggleMode(name) {
|
|
1434
|
+
try {
|
|
1435
|
+
const next = toggleMode(name);
|
|
1436
|
+
toast(api, {
|
|
1437
|
+
variant: next[name] ? "success" : "info",
|
|
1438
|
+
message: `${name.toUpperCase()} mode ${next[name] ? "on" : "off"}`
|
|
1439
|
+
});
|
|
1440
|
+
if (next[name]) await drainPendingMode(name);
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
toast(api, { variant: "error", message: `Could not toggle ${name.toUpperCase()}: ${String(error)}` });
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
async function runModeStatus() {
|
|
1446
|
+
try {
|
|
1447
|
+
const modes = readModes();
|
|
1448
|
+
toast(api, {
|
|
1449
|
+
variant: "info",
|
|
1450
|
+
message: `YOLO ${modes.yolo ? "on" : "off"} \xB7 YKB ${modes.ykb ? "on" : "off"}`
|
|
1451
|
+
});
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
toast(api, { variant: "error", message: `Could not read mode state: ${String(error)}` });
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1331
1456
|
const commands = [
|
|
1332
1457
|
{
|
|
1333
1458
|
name: "lacode.voice.dictate",
|
|
@@ -1349,7 +1474,10 @@ var tui2 = async (api, options) => {
|
|
|
1349
1474
|
category: "Voice",
|
|
1350
1475
|
namespace: "palette",
|
|
1351
1476
|
run: async () => runCheck()
|
|
1352
|
-
}
|
|
1477
|
+
},
|
|
1478
|
+
{ name: "lacode.mode.yolo", title: "Mode: toggle YOLO (auto-approve permissions)", category: "Modes", namespace: "palette", slashName: "yolo", run: async () => runToggleMode("yolo") },
|
|
1479
|
+
{ name: "lacode.mode.ykb", title: "Mode: toggle YKB (no questions, decide itself)", category: "Modes", namespace: "palette", slashName: "ykb", run: async () => runToggleMode("ykb") },
|
|
1480
|
+
{ name: "lacode.mode.status", title: "Mode: show YOLO/YKB status", category: "Modes", namespace: "palette", slashName: "modes", run: async () => runModeStatus() }
|
|
1353
1481
|
];
|
|
1354
1482
|
const bindings = [];
|
|
1355
1483
|
if (opts.dictation) {
|
package/dist/tunnel.js
CHANGED
|
@@ -46,6 +46,8 @@ var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
|
46
46
|
var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
|
47
47
|
var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
48
48
|
var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
|
|
49
|
+
var PROVIDER_DEGRADED_COOLDOWN_MS = 30 * 60 * 1e3;
|
|
50
|
+
var PROVIDER_DEGRADED_WINDOW_MS = 10 * 60 * 1e3;
|
|
49
51
|
var COOLDOWN_BASE_MS = 60 * 1e3;
|
|
50
52
|
var COOLDOWN_MAX_MS = 15 * 60 * 1e3;
|
|
51
53
|
var QUOTA_COOLDOWN_BASE_MS = 15 * 60 * 1e3;
|
|
@@ -56,6 +58,8 @@ var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
|
56
58
|
var USER_CONFIG_DIR = join2(homedir(), ".config", "lacode");
|
|
57
59
|
var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
|
|
58
60
|
var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
|
|
61
|
+
var CONTEXT_OVERFLOW_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
62
|
+
var BELOW_MIN_CONTEXT_COOLDOWN_MS = 6 * 60 * 60 * 1e3;
|
|
59
63
|
var STATE_SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
|
|
60
64
|
var CHILD_MEMORY_MS = 30 * 6e4;
|
|
61
65
|
|