@lawaty/lacode 2.2.0-beta.0 → 2.3.0-beta.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/CHANGELOG.md +29 -0
- package/README.md +14 -0
- package/dist/agents.js +2 -0
- package/dist/commands.js +2 -0
- package/dist/index.js +2472 -2118
- package/dist/lacode.js +2 -0
- package/dist/pool.js +154 -23
- package/dist/tui.js +153 -25
- package/dist/tunnel.js +2 -0
- package/docs/CONFIGURATION.md +12 -0
- package/docs/USER-STORIES.md +56 -8
- package/package.json +1 -1
- package/scripts/pool-selection.mjs +109 -15
- package/scripts/refresh-pool.mjs +72 -9
- package/scripts/score-models.mjs +151 -47
package/dist/lacode.js
CHANGED
|
@@ -63,6 +63,8 @@ var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
|
63
63
|
var USER_CONFIG_DIR = join2(homedir(), ".config", "lacode");
|
|
64
64
|
var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
|
|
65
65
|
var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
|
|
66
|
+
var CONTEXT_OVERFLOW_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
67
|
+
var BELOW_MIN_CONTEXT_COOLDOWN_MS = 6 * 60 * 60 * 1e3;
|
|
66
68
|
var STATE_SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
|
|
67
69
|
var CHILD_MEMORY_MS = 30 * 6e4;
|
|
68
70
|
|
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;
|
|
@@ -37,6 +38,8 @@ var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
|
37
38
|
var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
|
|
38
39
|
var UNAVAILABLE_REASON = "endpoint-unavailable";
|
|
39
40
|
var MAX_WEIGHT = 100;
|
|
41
|
+
var MIN_SLOT_CONTEXT = 1e5;
|
|
42
|
+
var MIN_SLOT_TPM = 2e4;
|
|
40
43
|
var slotCeiling = (slot) => slot.weight;
|
|
41
44
|
var slotBucket = (slot) => slot.bucket ?? slot.model;
|
|
42
45
|
var NOTIFY_DEFAULT_ON_PROVIDERS = /* @__PURE__ */ new Set(["opencode", "opencode-go"]);
|
|
@@ -67,6 +70,7 @@ var isModelMissingText = (text) => MODEL_MISSING_PATTERN.test(text);
|
|
|
67
70
|
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
71
|
var isAuthFailureText = (text) => AUTH_FAILURE_PATTERN.test(text);
|
|
69
72
|
var BASES = ["explore", "implement", "context"];
|
|
73
|
+
var LEARNED_TPM_FILE = "learned-tpm.json";
|
|
70
74
|
var PRESETS_DIR = join2(PACKAGE_ROOT, "presets");
|
|
71
75
|
var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
72
76
|
var USER_CONFIG_DIR = join2(homedir(), ".config", "lacode");
|
|
@@ -116,7 +120,22 @@ function parseSlots(parsed, origin) {
|
|
|
116
120
|
if (Number.isFinite(parsedContext) && parsedContext > 0) context = parsedContext;
|
|
117
121
|
else problems.push(`${at} (${model}): context must be a positive number, got ${JSON.stringify(rawContext)}; ignoring it`);
|
|
118
122
|
}
|
|
119
|
-
|
|
123
|
+
if (context !== void 0 && context < MIN_SLOT_CONTEXT) {
|
|
124
|
+
problems.push(`${at} (${model}): context ${context} is below the ${MIN_SLOT_CONTEXT}-token minimum; dropping the slot`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const rawTpm = entry?.tpm;
|
|
128
|
+
let tpm;
|
|
129
|
+
if (rawTpm !== void 0) {
|
|
130
|
+
const parsedTpm = typeof rawTpm === "number" ? rawTpm : Number(rawTpm);
|
|
131
|
+
if (Number.isFinite(parsedTpm) && parsedTpm > 0) tpm = parsedTpm;
|
|
132
|
+
else problems.push(`${at} (${model}): tpm must be a positive number, got ${JSON.stringify(rawTpm)}; ignoring it`);
|
|
133
|
+
}
|
|
134
|
+
if (tpm !== void 0 && tpm < MIN_SLOT_TPM) {
|
|
135
|
+
problems.push(`${at} (${model}): tpm ${tpm} is below the ${MIN_SLOT_TPM}-token/minute minimum; dropping the slot`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
slots.push({ index: slots.length + 1, model, weight, ...bucket ? { bucket } : {}, notify, ...context === void 0 ? {} : { context }, ...tpm === void 0 ? {} : { tpm } });
|
|
120
139
|
});
|
|
121
140
|
if (slots.length > SLOTS_WARN_AT) {
|
|
122
141
|
problems.push(`${origin}: ${slots.length} slots is unusually large, but every entry is kept (the pool has no slot cap)`);
|
|
@@ -137,6 +156,7 @@ function parseTextSlots(text, origin) {
|
|
|
137
156
|
let bucket;
|
|
138
157
|
let notify;
|
|
139
158
|
let context;
|
|
159
|
+
let tpm;
|
|
140
160
|
for (const token of rest) {
|
|
141
161
|
if (token.startsWith("notify:")) {
|
|
142
162
|
if (notify === void 0) notify = token.slice("notify:".length);
|
|
@@ -149,13 +169,20 @@ function parseTextSlots(text, origin) {
|
|
|
149
169
|
}
|
|
150
170
|
continue;
|
|
151
171
|
}
|
|
172
|
+
if (token.startsWith("tpm:")) {
|
|
173
|
+
if (tpm === void 0) {
|
|
174
|
+
const parsedTpm = Number(token.slice(4));
|
|
175
|
+
if (Number.isFinite(parsedTpm) && parsedTpm > 0) tpm = parsedTpm;
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
152
179
|
if (token.startsWith("b:")) {
|
|
153
180
|
if (bucket === void 0) bucket = token.slice(2);
|
|
154
181
|
} else if (weight === void 0) {
|
|
155
182
|
weight = token;
|
|
156
183
|
}
|
|
157
184
|
}
|
|
158
|
-
entries.push({ model, ...weight === void 0 ? {} : { weight }, ...bucket === void 0 ? {} : { bucket }, ...notify === void 0 ? {} : { notify }, ...context === void 0 ? {} : { context } });
|
|
185
|
+
entries.push({ model, ...weight === void 0 ? {} : { weight }, ...bucket === void 0 ? {} : { bucket }, ...notify === void 0 ? {} : { notify }, ...context === void 0 ? {} : { context }, ...tpm === void 0 ? {} : { tpm } });
|
|
159
186
|
}
|
|
160
187
|
return parseSlots({ slots: entries }, origin);
|
|
161
188
|
}
|
|
@@ -275,6 +302,43 @@ var LIMIT_PATTERNS = [
|
|
|
275
302
|
];
|
|
276
303
|
var LIMIT_REACHED_PATTERN = /\blimits?[\s_-]?(?:reached|exceeded)\b|\breached\s+(?:your|the|its)\s+limit\b/i;
|
|
277
304
|
var CONTEXT_LIMIT_PATTERN = /(?:context|token|length|window|prompt|char|character|byte|size)\s*(?:limit|length|size|window)/i;
|
|
305
|
+
var CONTEXT_OVERFLOW_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
306
|
+
var CONTEXT_OVERFLOW_PATTERNS = [
|
|
307
|
+
/context[_\s-]*(?:length|window|limit|overflow|exceed)/i,
|
|
308
|
+
/input\s+too\s+long/i,
|
|
309
|
+
/prompt\s+is\s+too\s+long/i,
|
|
310
|
+
/maximum\s+(?:context|prompt|token)/i,
|
|
311
|
+
/reduce\s+the\s+length\s+of\s+(?:the\s+)?messages/i
|
|
312
|
+
];
|
|
313
|
+
var isContextOverflowText = (text) => CONTEXT_OVERFLOW_PATTERNS.some((re) => re.test(text));
|
|
314
|
+
var BELOW_MIN_CONTEXT_COOLDOWN_MS = 6 * 60 * 60 * 1e3;
|
|
315
|
+
var REQUEST_TOO_LARGE_PATTERNS = [
|
|
316
|
+
/request\s+too\s+large/i,
|
|
317
|
+
/please\s+reduce\s+your\s+message\s+size/i,
|
|
318
|
+
/limit\s+\d+\s*,\s*requested\s+\d+/i,
|
|
319
|
+
/\(tpm\)/i
|
|
320
|
+
];
|
|
321
|
+
var isRequestTooLargeText = (text) => REQUEST_TOO_LARGE_PATTERNS.some((re) => re.test(text));
|
|
322
|
+
var parseStatedTokenLimit = (text) => {
|
|
323
|
+
for (const re of [/limit\s+(\d+)\s*,\s*requested\s+\d+/i, /limit\s+is\s+(\d+)/i]) {
|
|
324
|
+
const match = text.match(re);
|
|
325
|
+
if (match) {
|
|
326
|
+
const value = Number(match[1]);
|
|
327
|
+
if (Number.isFinite(value) && value > 0) return value;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return void 0;
|
|
331
|
+
};
|
|
332
|
+
function parseRateLimitHeaders(headers) {
|
|
333
|
+
if (typeof headers !== "object" || headers === null || Array.isArray(headers)) return void 0;
|
|
334
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
335
|
+
if (key.toLowerCase() !== "x-ratelimit-limit-tokens") continue;
|
|
336
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value.trim()) : Number.NaN;
|
|
337
|
+
if (Number.isFinite(parsed) && parsed > 0 && Number.isInteger(parsed)) return parsed;
|
|
338
|
+
return void 0;
|
|
339
|
+
}
|
|
340
|
+
return void 0;
|
|
341
|
+
}
|
|
278
342
|
var LIMIT_REASON_PATTERN = /rate[\s_-]?limit|usage limit|too many requests|quota|freelimiterror|overloaded|limits?[\s_-]?(?:reached|exceeded)/i;
|
|
279
343
|
var isLimitText = (text) => LIMIT_PATTERNS.some((re) => re.test(text)) || LIMIT_REACHED_PATTERN.test(text) && !CONTEXT_LIMIT_PATTERN.test(text);
|
|
280
344
|
var limitReason = (text) => {
|
|
@@ -341,6 +405,7 @@ function state(opts = {}) {
|
|
|
341
405
|
reaping: false,
|
|
342
406
|
lastSweep: 0,
|
|
343
407
|
lastDirSweep: 0,
|
|
408
|
+
lastHeartbeatPublish: 0,
|
|
344
409
|
connected: /* @__PURE__ */ new Set(),
|
|
345
410
|
connectedAt: 0,
|
|
346
411
|
connectedReady: false,
|
|
@@ -359,26 +424,75 @@ function state(opts = {}) {
|
|
|
359
424
|
function ensureSlots(s, onWarn) {
|
|
360
425
|
if (s.slots.length > 0) return s.slots;
|
|
361
426
|
const { slots, problems, origin } = resolveModels({ models: s.inlineModels, modelsFile: s.modelsFile });
|
|
362
|
-
s.slots = slots;
|
|
427
|
+
s.slots = applyLearnedTpm(s, slots, onWarn);
|
|
363
428
|
s.origin = origin;
|
|
364
429
|
for (const problem of problems) onWarn(problem);
|
|
365
430
|
return slots;
|
|
366
431
|
}
|
|
367
|
-
var slotSignature = (origin, slots) => `${origin}\0${slots.map((slot) => `${slot.index}:${slot.model}:${slot.weight}:${slot.bucket ?? ""}:${slotNotify(slot) ? "1" : "0"}`).join("|")}`;
|
|
432
|
+
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
433
|
function reloadSlots(s, onWarn) {
|
|
369
434
|
const { slots, problems, origin } = resolveModels({ models: s.inlineModels, modelsFile: s.modelsFile });
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
s.slots
|
|
435
|
+
const effective = applyLearnedTpm(s, slots, onWarn);
|
|
436
|
+
const changed = slotSignature(origin, effective) !== slotSignature(s.origin, s.slots);
|
|
437
|
+
if (!changed) return { changed, slots: s.slots, origin: s.origin };
|
|
438
|
+
s.slots = effective;
|
|
373
439
|
s.origin = origin;
|
|
374
440
|
for (const problem of problems) onWarn(problem);
|
|
375
|
-
return { changed, slots, origin };
|
|
441
|
+
return { changed, slots: effective, origin };
|
|
376
442
|
}
|
|
377
443
|
function slotSources(input = {}) {
|
|
378
444
|
if (input.models !== void 0) return [];
|
|
379
445
|
if (input.modelsFile) return [input.modelsFile];
|
|
380
446
|
return [USER_MODELS_TXT, USER_MODELS_PATH, DEFAULT_PRESET];
|
|
381
447
|
}
|
|
448
|
+
var learnedTpmPath = (s) => join2(s.dir, LEARNED_TPM_FILE);
|
|
449
|
+
function readLearnedTpm(s) {
|
|
450
|
+
try {
|
|
451
|
+
const raw = JSON.parse(readFileSync2(learnedTpmPath(s), "utf8"));
|
|
452
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
|
|
453
|
+
const out = {};
|
|
454
|
+
for (const [model, value] of Object.entries(raw)) {
|
|
455
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
456
|
+
if (Number.isFinite(n) && n > 0) out[model] = n;
|
|
457
|
+
}
|
|
458
|
+
return out;
|
|
459
|
+
} catch {
|
|
460
|
+
return {};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function applyLearnedTpm(s, slots, onWarn) {
|
|
464
|
+
const learned = readLearnedTpm(s);
|
|
465
|
+
const out = [];
|
|
466
|
+
for (const slot of slots) {
|
|
467
|
+
const tpm = learned[slot.model];
|
|
468
|
+
if (tpm !== void 0 && tpm < MIN_SLOT_TPM) {
|
|
469
|
+
onWarn?.(`learned-tpm: dropping ${slot.model} (tpm ${tpm} < ${MIN_SLOT_TPM})`);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (tpm !== void 0) out.push({ ...slot, tpm: Math.min(slot.tpm ?? tpm, tpm) });
|
|
473
|
+
else out.push(slot);
|
|
474
|
+
}
|
|
475
|
+
return out.map((slot, i) => ({ ...slot, index: i + 1 }));
|
|
476
|
+
}
|
|
477
|
+
function noteTpm(s, model, tpm) {
|
|
478
|
+
if (!Number.isFinite(tpm) || tpm <= 0) return false;
|
|
479
|
+
const map = readLearnedTpm(s);
|
|
480
|
+
map[model] = tpm;
|
|
481
|
+
if (s.dirReady) {
|
|
482
|
+
try {
|
|
483
|
+
const path = learnedTpmPath(s);
|
|
484
|
+
writeFileSync(`${path}.tmp`, JSON.stringify(map));
|
|
485
|
+
renameSync(`${path}.tmp`, path);
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (tpm < MIN_SLOT_TPM) {
|
|
490
|
+
s.slots = s.slots.filter((slot) => slot.model !== model).map((slot, i) => ({ ...slot, index: i + 1 }));
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
s.slots = s.slots.map((slot) => slot.model === model ? { ...slot, tpm: Math.min(slot.tpm ?? tpm, tpm) } : slot);
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
382
496
|
var claimKey = (sessionID, callID) => `${sessionID}:${callID}`;
|
|
383
497
|
var selfFile = (id) => `claims.${id}.json`;
|
|
384
498
|
var limitFile = (model) => `limit.${model.replace(/[^a-zA-Z0-9._-]/g, "__")}.json`;
|
|
@@ -730,24 +844,23 @@ var loadOf = (counts, slot) => counts.get(slot) ?? 0;
|
|
|
730
844
|
var underCeiling = (slot, counts) => loadOf(counts, slot.index) < slotCeiling(slot);
|
|
731
845
|
function pickSlot(s, snap, neededContext) {
|
|
732
846
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
const
|
|
847
|
+
const usable = slots.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
848
|
+
if (usable.length === 0) return void 0;
|
|
849
|
+
const effective = (slot) => Math.min(slot.context ?? Number.POSITIVE_INFINITY, slot.tpm ?? Number.POSITIVE_INFINITY);
|
|
736
850
|
if (neededContext === void 0) {
|
|
737
851
|
for (const slot of usable) {
|
|
738
852
|
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
739
853
|
}
|
|
740
|
-
return
|
|
854
|
+
return usable.reduce((best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best, usable[0]);
|
|
741
855
|
}
|
|
742
|
-
const adequate = usable.filter((slot) => slot
|
|
743
|
-
const adequateKnown = adequate.filter((slot) => slot
|
|
856
|
+
const adequate = usable.filter((slot) => effective(slot) >= neededContext);
|
|
857
|
+
const adequateKnown = adequate.filter((slot) => Number.isFinite(effective(slot)));
|
|
744
858
|
const candidates = adequate.length > 0 ? adequate : usable;
|
|
745
|
-
const fallbackPool = adequate.length > 0 ? adequate : pool;
|
|
746
859
|
if (adequateKnown.length === 0) {
|
|
747
|
-
const known = usable.filter((slot) => slot
|
|
860
|
+
const known = usable.filter((slot) => Number.isFinite(effective(slot)));
|
|
748
861
|
if (known.length > 0) {
|
|
749
|
-
const largest = Math.max(...known.map((slot) => slot
|
|
750
|
-
const largestSlots = known.filter((slot) => slot
|
|
862
|
+
const largest = Math.max(...known.map((slot) => effective(slot)));
|
|
863
|
+
const largestSlots = known.filter((slot) => effective(slot) === largest);
|
|
751
864
|
for (const slot of largestSlots) {
|
|
752
865
|
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
753
866
|
}
|
|
@@ -756,20 +869,22 @@ function pickSlot(s, snap, neededContext) {
|
|
|
756
869
|
for (const slot of candidates) {
|
|
757
870
|
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
758
871
|
}
|
|
759
|
-
return
|
|
872
|
+
return candidates.reduce((best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best, candidates[0]);
|
|
760
873
|
}
|
|
761
874
|
const underWeight = candidates.filter((slot) => loadOf(snap.counts, slot.index) < slot.weight);
|
|
762
875
|
if (underWeight.length > 0) {
|
|
763
|
-
const known = underWeight.filter((slot) => slot
|
|
876
|
+
const known = underWeight.filter((slot) => Number.isFinite(effective(slot)));
|
|
764
877
|
if (known.length === 0) return underWeight[0];
|
|
765
|
-
return known.reduce((best, slot) => slot
|
|
878
|
+
return known.reduce((best, slot) => effective(slot) < effective(best) ? slot : best, known[0]);
|
|
766
879
|
}
|
|
767
880
|
return candidates.reduce((best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best, candidates[0]);
|
|
768
881
|
}
|
|
769
882
|
function poolPass(s, snap, neededContext) {
|
|
770
883
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
771
884
|
const usable = slots.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
772
|
-
|
|
885
|
+
if (usable.length === 0) return "none";
|
|
886
|
+
const effective = (slot) => Math.min(slot.context ?? Number.POSITIVE_INFINITY, slot.tpm ?? Number.POSITIVE_INFINITY);
|
|
887
|
+
const candidates = neededContext === void 0 ? usable : usable.filter((slot) => effective(slot) >= neededContext);
|
|
773
888
|
const pool = candidates.length > 0 ? candidates : usable;
|
|
774
889
|
if (pool.some((slot) => loadOf(snap.counts, slot.index) < slot.weight)) return 1;
|
|
775
890
|
return "overflow";
|
|
@@ -831,7 +946,10 @@ function noteActivity(s, sessionID) {
|
|
|
831
946
|
s.activity.set(sessionID, now);
|
|
832
947
|
let touched = false;
|
|
833
948
|
for (const claim of s.own.values()) if (claim.g === sessionID) claim.hb = now, touched = true;
|
|
834
|
-
if (touched
|
|
949
|
+
if (touched && now - s.lastHeartbeatPublish >= HEARTBEAT_PUBLISH_MS) {
|
|
950
|
+
s.lastHeartbeatPublish = now;
|
|
951
|
+
publish(s);
|
|
952
|
+
}
|
|
835
953
|
}
|
|
836
954
|
function idleFor(s, claim, now) {
|
|
837
955
|
const target = claim.g;
|
|
@@ -931,14 +1049,20 @@ export {
|
|
|
931
1049
|
AUTH_FAILURE_COOLDOWN_MS,
|
|
932
1050
|
AUTH_FAILURE_REASON,
|
|
933
1051
|
BASES,
|
|
1052
|
+
BELOW_MIN_CONTEXT_COOLDOWN_MS,
|
|
934
1053
|
CATALOG_TTL_MS,
|
|
935
1054
|
CLAIM_TTL_MS,
|
|
1055
|
+
CONTEXT_OVERFLOW_COOLDOWN_MS,
|
|
936
1056
|
DEAD_FILE_MS,
|
|
937
1057
|
DEFAULT_PRESET,
|
|
938
1058
|
FALLBACK_SLOTS,
|
|
939
1059
|
HANG_COOLDOWN_MS,
|
|
1060
|
+
HEARTBEAT_PUBLISH_MS,
|
|
1061
|
+
LEARNED_TPM_FILE,
|
|
940
1062
|
MAX_CLAIM_AGE_MS,
|
|
941
1063
|
MAX_WEIGHT,
|
|
1064
|
+
MIN_SLOT_CONTEXT,
|
|
1065
|
+
MIN_SLOT_TPM,
|
|
942
1066
|
MODEL_MISSING_COOLDOWN_MS,
|
|
943
1067
|
MODEL_MISSING_REASON,
|
|
944
1068
|
ORPHAN_TMP_MS,
|
|
@@ -961,6 +1085,7 @@ export {
|
|
|
961
1085
|
acquire,
|
|
962
1086
|
agentFor,
|
|
963
1087
|
announcesReset,
|
|
1088
|
+
applyLearnedTpm,
|
|
964
1089
|
backfillChild,
|
|
965
1090
|
bindChild,
|
|
966
1091
|
claimKey,
|
|
@@ -975,19 +1100,24 @@ export {
|
|
|
975
1100
|
init,
|
|
976
1101
|
isAuthFailureText,
|
|
977
1102
|
isAvailable,
|
|
1103
|
+
isContextOverflowText,
|
|
978
1104
|
isLimitText,
|
|
979
1105
|
isModelMissingText,
|
|
980
1106
|
isProbe,
|
|
981
1107
|
isQuotaReason,
|
|
1108
|
+
isRequestTooLargeText,
|
|
982
1109
|
isTransientGatewayText,
|
|
983
1110
|
isUnavailableText,
|
|
984
1111
|
limitReason,
|
|
985
1112
|
manualVariant,
|
|
986
1113
|
markProbe,
|
|
987
1114
|
noteActivity,
|
|
1115
|
+
noteTpm,
|
|
988
1116
|
notifyDefault,
|
|
1117
|
+
parseRateLimitHeaders,
|
|
989
1118
|
parseSlotText,
|
|
990
1119
|
parseSlots,
|
|
1120
|
+
parseStatedTokenLimit,
|
|
991
1121
|
parseTextSlots,
|
|
992
1122
|
pickSlot,
|
|
993
1123
|
poolPass,
|
|
@@ -997,6 +1127,7 @@ export {
|
|
|
997
1127
|
pruneSessions,
|
|
998
1128
|
publish,
|
|
999
1129
|
readFailures,
|
|
1130
|
+
readLearnedTpm,
|
|
1000
1131
|
readLimits,
|
|
1001
1132
|
readSiblings,
|
|
1002
1133
|
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
|
@@ -56,6 +56,8 @@ var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
|
56
56
|
var USER_CONFIG_DIR = join2(homedir(), ".config", "lacode");
|
|
57
57
|
var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
|
|
58
58
|
var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
|
|
59
|
+
var CONTEXT_OVERFLOW_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
60
|
+
var BELOW_MIN_CONTEXT_COOLDOWN_MS = 6 * 60 * 60 * 1e3;
|
|
59
61
|
var STATE_SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
|
|
60
62
|
var CHILD_MEMORY_MS = 30 * 6e4;
|
|
61
63
|
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -50,6 +50,18 @@ warning. The flag gates only the desktop reset notice, not pool routing or
|
|
|
50
50
|
status, and pool files are hot-reloaded so changes take effect without a
|
|
51
51
|
restart.
|
|
52
52
|
|
|
53
|
+
## Modes
|
|
54
|
+
|
|
55
|
+
Two runtime modes can be seeded from plugin options and toggled from the TUI:
|
|
56
|
+
|
|
57
|
+
| Command | Effect |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `/yolo` | Toggle YOLO. Every permission request that would `ask` is approved automatically; an explicit `deny` still wins. |
|
|
60
|
+
| `/ykb` | Toggle YKB (You Know Better). The agent is told not to ask questions and to continue with its recommendation. A question that still appears is answered with its `(Recommended)` option, or its first option when none is marked. |
|
|
61
|
+
| `/modes` | Show both switches. |
|
|
62
|
+
|
|
63
|
+
The toggles write `~/.config/lacode/modes.json` (override with `LACODE_MODES_FILE`) and the server picks the change up on the next decision — no restart. Turning a mode on also answers requests that are already pending. The `yolo` / `ykb` plugin options only seed the file on first run; after that, the file wins. A free-text question with no options is left for the user, since there is no option to choose.
|
|
64
|
+
|
|
53
65
|
|
|
54
66
|
## Which models run which agent
|
|
55
67
|
|