@lawaty/lacode 1.1.0 → 1.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 +87 -1
- package/README.md +25 -12
- package/dist/agents.js +10 -3
- package/dist/commands.js +11 -5
- package/dist/index.js +1254 -99
- package/dist/lacode-voice.js +2351 -0
- package/dist/lacode.js +2 -0
- package/dist/pool.js +102 -42
- package/dist/tui.js +1282 -0
- package/dist/tunnel.js +2 -0
- package/docs/CONFIGURATION.md +17 -7
- package/docs/USER-STORIES.md +11 -11
- package/package.json +12 -4
- package/presets/README.md +17 -19
- package/presets/free-tier.json +11 -13
- package/scripts/score-models.mjs +353 -0
package/dist/index.js
CHANGED
|
@@ -25,9 +25,11 @@ 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 UNBOUND_CLAIM_MS = 2 * 60 * 1e3;
|
|
28
29
|
var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
|
|
29
30
|
var ORPHAN_TMP_MS = 60 * 1e3;
|
|
30
31
|
var CATALOG_TTL_MS = 10 * 60 * 1e3;
|
|
32
|
+
var PROVIDER_LIST_TTL_MS = 60 * 1e3;
|
|
31
33
|
var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
|
|
32
34
|
var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
33
35
|
var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
|
@@ -35,9 +37,9 @@ var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
|
35
37
|
var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
|
|
36
38
|
var UNAVAILABLE_REASON = "endpoint-unavailable";
|
|
37
39
|
var MAX_WEIGHT = 100;
|
|
38
|
-
var
|
|
39
|
-
var
|
|
40
|
-
var
|
|
40
|
+
var slotCeiling = (slot) => slot.weight;
|
|
41
|
+
var slotBucket = (slot) => slot.bucket ?? slot.model;
|
|
42
|
+
var SLOTS_WARN_AT = 64;
|
|
41
43
|
var COOLDOWN_BASE_MS = 60 * 1e3;
|
|
42
44
|
var COOLDOWN_MAX_MS = 15 * 60 * 1e3;
|
|
43
45
|
var QUOTA_COOLDOWN_BASE_MS = 15 * 60 * 1e3;
|
|
@@ -47,8 +49,12 @@ var STATED_COOLDOWN_MAX_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
|
47
49
|
var isQuotaReason = (reason) => /quota|limit|rate|too-many|overloaded|429/i.test(reason);
|
|
48
50
|
var MODEL_MISSING_REASON = "model-missing";
|
|
49
51
|
var MODEL_MISSING_COOLDOWN_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
52
|
+
var AUTH_FAILURE_REASON = "auth-failed";
|
|
53
|
+
var AUTH_FAILURE_COOLDOWN_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
50
54
|
var MODEL_MISSING_PATTERN = /model\s+(?:not\s+found|does\s+not\s+exist|is\s+not\s+available)|(?:unknown|no\s+such)\s+model/i;
|
|
51
55
|
var isModelMissingText = (text) => MODEL_MISSING_PATTERN.test(text);
|
|
56
|
+
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;
|
|
57
|
+
var isAuthFailureText = (text) => AUTH_FAILURE_PATTERN.test(text);
|
|
52
58
|
var BASES = ["explore", "implement", "context"];
|
|
53
59
|
var PRESETS_DIR = join2(PACKAGE_ROOT, "presets");
|
|
54
60
|
var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
@@ -71,15 +77,18 @@ function parseSlots(parsed, origin) {
|
|
|
71
77
|
entries.forEach((entry, i) => {
|
|
72
78
|
const model = typeof entry?.model === "string" ? entry.model.trim() : "";
|
|
73
79
|
const at = `slot ${i + 1}`;
|
|
74
|
-
if (slots.length >= MAX_SLOTS) {
|
|
75
|
-
problems.push(`${at} (${model || "?"}): the pool is capped at ${MAX_SLOTS} slots (one variant agent per base per slot is declared); entry ignored`);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
80
|
if (!model.includes("/")) return problems.push(`${at}: model must be written provider/model-id, got "${model}"`);
|
|
79
81
|
const weight = readWeight(entry, at, model, problems);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
const rawBucket = entry?.bucket;
|
|
83
|
+
const bucket = typeof rawBucket === "string" ? rawBucket.trim() : void 0;
|
|
84
|
+
if (rawBucket !== void 0 && !bucket) {
|
|
85
|
+
problems.push(`${at} (${model}): bucket must be a non-empty name; using the model id`);
|
|
86
|
+
}
|
|
87
|
+
slots.push({ index: slots.length + 1, model, weight, ...bucket ? { bucket } : {} });
|
|
82
88
|
});
|
|
89
|
+
if (slots.length > SLOTS_WARN_AT) {
|
|
90
|
+
problems.push(`${origin}: ${slots.length} slots is unusually large, but every entry is kept (the pool has no slot cap)`);
|
|
91
|
+
}
|
|
83
92
|
if (slots.length === 0) {
|
|
84
93
|
problems.push(`${origin} yielded no usable slot; using built-in defaults`);
|
|
85
94
|
return { slots: FALLBACK_SLOTS, problems };
|
|
@@ -91,8 +100,17 @@ function parseTextSlots(text, origin) {
|
|
|
91
100
|
for (const raw of text.split(/\r?\n/)) {
|
|
92
101
|
const line = raw.split("#")[0].trim();
|
|
93
102
|
if (!line) continue;
|
|
94
|
-
const [model,
|
|
95
|
-
|
|
103
|
+
const [model, ...rest] = line.split(/\s+/);
|
|
104
|
+
let weight;
|
|
105
|
+
let bucket;
|
|
106
|
+
for (const token of rest) {
|
|
107
|
+
if (token.startsWith("b:")) {
|
|
108
|
+
if (bucket === void 0) bucket = token.slice(2);
|
|
109
|
+
} else if (weight === void 0) {
|
|
110
|
+
weight = token;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
entries.push({ model, ...weight === void 0 ? {} : { weight }, ...bucket === void 0 ? {} : { bucket } });
|
|
96
114
|
}
|
|
97
115
|
return parseSlots({ slots: entries }, origin);
|
|
98
116
|
}
|
|
@@ -236,9 +254,17 @@ var retryAfterMs = (headers) => {
|
|
|
236
254
|
for (const [key, value] of Object.entries(headers)) {
|
|
237
255
|
if (key.toLowerCase() !== "retry-after") continue;
|
|
238
256
|
const seconds = Number(value);
|
|
239
|
-
if (Number.isFinite(seconds) && seconds > 0)
|
|
257
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
258
|
+
const ms = seconds * 1e3;
|
|
259
|
+
if (ms > STATED_COOLDOWN_MAX_MS) return void 0;
|
|
260
|
+
return ms;
|
|
261
|
+
}
|
|
240
262
|
const when = Date.parse(value);
|
|
241
|
-
if (Number.isFinite(when))
|
|
263
|
+
if (Number.isFinite(when)) {
|
|
264
|
+
const delay = when - Date.now();
|
|
265
|
+
if (delay > STATED_COOLDOWN_MAX_MS) return void 0;
|
|
266
|
+
return Math.max(delay, 1e3);
|
|
267
|
+
}
|
|
242
268
|
}
|
|
243
269
|
return void 0;
|
|
244
270
|
};
|
|
@@ -270,6 +296,11 @@ function state(opts = {}) {
|
|
|
270
296
|
reaping: false,
|
|
271
297
|
lastSweep: 0,
|
|
272
298
|
lastDirSweep: 0,
|
|
299
|
+
connected: /* @__PURE__ */ new Set(),
|
|
300
|
+
connectedAt: 0,
|
|
301
|
+
connectedReady: false,
|
|
302
|
+
credentialed: /* @__PURE__ */ new Set(),
|
|
303
|
+
configCreds: /* @__PURE__ */ new Set(),
|
|
273
304
|
id,
|
|
274
305
|
dir,
|
|
275
306
|
modelsFile,
|
|
@@ -288,7 +319,7 @@ function ensureSlots(s, onWarn) {
|
|
|
288
319
|
for (const problem of problems) onWarn(problem);
|
|
289
320
|
return slots;
|
|
290
321
|
}
|
|
291
|
-
var slotSignature = (origin, slots) => `${origin}\0${slots.map((slot) => `${slot.index}:${slot.model}:${slot.weight}`).join("|")}`;
|
|
322
|
+
var slotSignature = (origin, slots) => `${origin}\0${slots.map((slot) => `${slot.index}:${slot.model}:${slot.weight}:${slot.bucket ?? ""}`).join("|")}`;
|
|
292
323
|
function reloadSlots(s, onWarn) {
|
|
293
324
|
const { slots, problems, origin } = resolveModels({ models: s.inlineModels, modelsFile: s.modelsFile });
|
|
294
325
|
const changed = slotSignature(origin, slots) !== slotSignature(s.origin, s.slots);
|
|
@@ -377,9 +408,16 @@ function sweepStateDir(s) {
|
|
|
377
408
|
}
|
|
378
409
|
return removed;
|
|
379
410
|
}
|
|
411
|
+
var claimExpired = (claim, now) => now - (claim.hb ?? claim.t) > CLAIM_TTL_MS || !claim.g && now - claim.t > UNBOUND_CLAIM_MS;
|
|
380
412
|
function pruneOwn(s) {
|
|
381
413
|
const now = Date.now();
|
|
382
|
-
|
|
414
|
+
let removed = false;
|
|
415
|
+
for (const [key, claim] of s.own)
|
|
416
|
+
if (claimExpired(claim, now)) {
|
|
417
|
+
s.own.delete(key);
|
|
418
|
+
removed = true;
|
|
419
|
+
}
|
|
420
|
+
if (removed) publish(s);
|
|
383
421
|
}
|
|
384
422
|
function pruneActivity(s, now) {
|
|
385
423
|
if (s.activity.size === 0) return;
|
|
@@ -407,7 +445,7 @@ function publish(s) {
|
|
|
407
445
|
if (!s.dirReady) return;
|
|
408
446
|
const now = Date.now();
|
|
409
447
|
const payload = {};
|
|
410
|
-
for (const [key, claim] of s.own) if (
|
|
448
|
+
for (const [key, claim] of s.own) if (!claimExpired(claim, now)) payload[key] = claim;
|
|
411
449
|
try {
|
|
412
450
|
writeFileSync(join2(s.dir, `${selfFile(s.id)}.tmp`), JSON.stringify(payload));
|
|
413
451
|
renameSync(join2(s.dir, `${selfFile(s.id)}.tmp`), join2(s.dir, selfFile(s.id)));
|
|
@@ -466,7 +504,7 @@ function readSiblings(s) {
|
|
|
466
504
|
try {
|
|
467
505
|
const parsed = JSON.parse(readFileSync2(path5, "utf8"));
|
|
468
506
|
const claims = Object.values(parsed).filter(
|
|
469
|
-
(c) => c && typeof c.v === "string" && typeof c.m === "string" &&
|
|
507
|
+
(c) => c && typeof c.v === "string" && typeof c.m === "string" && typeof c.k === "number" && Number.isFinite(c.k) && !claimExpired(c, now)
|
|
470
508
|
);
|
|
471
509
|
siblings.push({ id: match[1], claims });
|
|
472
510
|
} catch {
|
|
@@ -516,20 +554,32 @@ function recordStrike(s, model, reason, stated) {
|
|
|
516
554
|
if (!s.dirReady) return;
|
|
517
555
|
if (reason === UNAVAILABLE_REASON) return coolUnavailable(s, model);
|
|
518
556
|
const now = Date.now();
|
|
519
|
-
const
|
|
557
|
+
const slotsForModel = s.slots.filter((candidate) => candidate.model === model);
|
|
558
|
+
const buckets = [...new Set(slotsForModel.map((slot) => slotBucket(slot)))];
|
|
559
|
+
const targets = buckets.length === 0 ? [model] : [...new Set(
|
|
560
|
+
s.slots.filter((candidate) => buckets.includes(slotBucket(candidate))).map((candidate) => candidate.model)
|
|
561
|
+
)];
|
|
562
|
+
if (targets.length === 0) targets.push(model);
|
|
563
|
+
const prior = targets.map((target) => s.strikes.get(target)).filter((candidate) => Boolean(candidate)).reduce((worst, candidate) => !worst || candidate.strikes > worst.strikes ? candidate : worst, void 0);
|
|
520
564
|
const strikes = prior && now - prior.last < STRIKE_DECAY_MS ? prior.strikes + 1 : 1;
|
|
521
565
|
const quota = isQuotaReason(reason);
|
|
522
566
|
const base = quota ? QUOTA_COOLDOWN_BASE_MS : COOLDOWN_BASE_MS;
|
|
523
567
|
const cap = quota ? QUOTA_COOLDOWN_MAX_MS : COOLDOWN_MAX_MS;
|
|
524
568
|
const backoff = Math.min(cap, base * 2 ** (strikes - 1));
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
569
|
+
const statedOk = typeof stated === "number" && Number.isFinite(stated) && stated > 0 && stated <= STATED_COOLDOWN_MAX_MS;
|
|
570
|
+
const until = now + (statedOk ? stated : backoff);
|
|
571
|
+
let strike;
|
|
572
|
+
for (const target of targets) {
|
|
573
|
+
const record = { model: target, until, strikes, reason, last: now };
|
|
574
|
+
s.cooling.set(target, record);
|
|
575
|
+
s.strikes.set(target, record);
|
|
576
|
+
const path5 = join2(s.dir, limitFile(target));
|
|
577
|
+
try {
|
|
578
|
+
writeFileSync(`${path5}.tmp`, JSON.stringify(record));
|
|
579
|
+
renameSync(`${path5}.tmp`, path5);
|
|
580
|
+
} catch {
|
|
581
|
+
}
|
|
582
|
+
if (target === model) strike = record;
|
|
533
583
|
}
|
|
534
584
|
return strike;
|
|
535
585
|
}
|
|
@@ -593,6 +643,12 @@ function clearCooldowns(s, models) {
|
|
|
593
643
|
var markProbe = (s, sessionID) => s.probes.add(sessionID);
|
|
594
644
|
var unmarkProbe = (s, sessionID) => s.probes.delete(sessionID);
|
|
595
645
|
var isProbe = (s, sessionID) => s.probes.has(sessionID);
|
|
646
|
+
function providerReady(s, model) {
|
|
647
|
+
if (!s.connectedReady) return true;
|
|
648
|
+
const provider = providerOf(model);
|
|
649
|
+
if (!s.connected.has(provider)) return false;
|
|
650
|
+
return s.credentialed.has(provider) || s.configCreds.has(provider) || provider === "opencode";
|
|
651
|
+
}
|
|
596
652
|
function refreshCatalog(s, serverUrl) {
|
|
597
653
|
if (!serverUrl || Date.now() - s.catalogAt < CATALOG_TTL_MS) return;
|
|
598
654
|
s.catalogAt = Date.now();
|
|
@@ -609,6 +665,7 @@ function refreshCatalog(s, serverUrl) {
|
|
|
609
665
|
})();
|
|
610
666
|
}
|
|
611
667
|
function isAvailable(s, model, cooling) {
|
|
668
|
+
if (!providerReady(s, model)) return false;
|
|
612
669
|
if (cooling.has(model)) return false;
|
|
613
670
|
const status = s.catalog.get(model);
|
|
614
671
|
return status === void 0 || status === "active" || status === "unknown";
|
|
@@ -619,30 +676,27 @@ function snapshot(s) {
|
|
|
619
676
|
const siblings = readSiblings(s);
|
|
620
677
|
const cooling = readLimits(s);
|
|
621
678
|
const counts = /* @__PURE__ */ new Map();
|
|
622
|
-
const bump = (
|
|
623
|
-
for (const claim of s.own.values()) bump(claim.
|
|
624
|
-
for (const sibling of siblings) for (const claim of sibling.claims) bump(claim.
|
|
679
|
+
const bump = (k) => counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
680
|
+
for (const claim of s.own.values()) bump(claim.k);
|
|
681
|
+
for (const sibling of siblings) for (const claim of sibling.claims) bump(claim.k);
|
|
625
682
|
return { counts, cooling, siblings, own: s.own, dirReady: s.dirReady };
|
|
626
683
|
}
|
|
627
|
-
var loadOf = (counts,
|
|
628
|
-
var underCeiling = (slot, counts) => loadOf(counts, slot.
|
|
684
|
+
var loadOf = (counts, slot) => counts.get(slot) ?? 0;
|
|
685
|
+
var underCeiling = (slot, counts) => loadOf(counts, slot.index) < slotCeiling(slot);
|
|
629
686
|
function pickSlot(s, snap) {
|
|
630
687
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
631
|
-
const
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
if (loadOf(snap.counts, slot.model) < slot.weight) return slot;
|
|
635
|
-
}
|
|
688
|
+
const configured = slots.filter((slot) => providerReady(s, slot.model));
|
|
689
|
+
const usable = configured.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
690
|
+
const pool = usable.length > 0 ? usable : configured.length > 0 ? configured : slots;
|
|
636
691
|
for (const slot of usable) {
|
|
637
|
-
if (loadOf(snap.counts, slot.
|
|
692
|
+
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
638
693
|
}
|
|
639
|
-
return pool.reduce((best, slot) => loadOf(snap.counts, slot.
|
|
694
|
+
return pool.reduce((best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best, pool[0]);
|
|
640
695
|
}
|
|
641
696
|
function poolPass(s, snap) {
|
|
642
697
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
643
698
|
const usable = slots.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
644
|
-
if (usable.some((slot) => loadOf(snap.counts, slot.
|
|
645
|
-
if (usable.some((slot) => loadOf(snap.counts, slot.model) < slotCeiling(slot))) return 2;
|
|
699
|
+
if (usable.some((slot) => loadOf(snap.counts, slot.index) < slot.weight)) return 1;
|
|
646
700
|
return "overflow";
|
|
647
701
|
}
|
|
648
702
|
function agentFor(base, slot) {
|
|
@@ -1677,12 +1731,12 @@ Report briefly what you changed and where. Do not edit any other file.`,
|
|
|
1677
1731
|
}
|
|
1678
1732
|
}
|
|
1679
1733
|
};
|
|
1680
|
-
function buildAgentSet() {
|
|
1734
|
+
function buildAgentSet(slotCount = FALLBACK_SLOTS.length) {
|
|
1681
1735
|
const set = { ...AGENTS };
|
|
1682
1736
|
for (const base of POOLED_BASES) {
|
|
1683
1737
|
const def = AGENTS[base];
|
|
1684
1738
|
if (!def) continue;
|
|
1685
|
-
for (let slot = 1; slot <=
|
|
1739
|
+
for (let slot = 1; slot <= Math.max(1, slotCount); slot++) {
|
|
1686
1740
|
const variant = {
|
|
1687
1741
|
...def,
|
|
1688
1742
|
description: `Hidden pool slot ${slot} for ${base}. Its model is NOT set here -- LaCode injects it from the resolved pool preset at config time, so the preset is the only place a pool model is written down. Spawn '${base}' and let the pool route.`,
|
|
@@ -1839,7 +1893,7 @@ Follow the rules for the file in your system prompt, then report what you change
|
|
|
1839
1893
|
Print one PASS/FAIL line per check below, with the exact fix for any FAIL.
|
|
1840
1894
|
|
|
1841
1895
|
1. Pool status \u2014 call the \`pool_status\` tool. Report the slot list, each
|
|
1842
|
-
slot's load against its
|
|
1896
|
+
slot's load against its budget (its weight), and which models
|
|
1843
1897
|
are cooling.
|
|
1844
1898
|
|
|
1845
1899
|
2. OmniRoute configured \u2014 inspect the \`@lawaty/lacode\` plugin options in the
|
|
@@ -1876,10 +1930,10 @@ The pool is ONE global slot table shared by every pooled base (explore, implemen
|
|
|
1876
1930
|
|
|
1877
1931
|
2. Resolve the instruction above into a target slot table. These are the rules the loader enforces \u2014 never write a file that breaks one:
|
|
1878
1932
|
- one slot per line: \`<provider>/<model-id> [weight]\`
|
|
1879
|
-
-
|
|
1880
|
-
- weight is optional (default 1) and must be a number in (0, ${MAX_WEIGHT}]. It is the slot's active-session budget
|
|
1933
|
+
- any number of slots; a model may appear on more than one line, and each line is its own slot with its own budget
|
|
1934
|
+
- weight is optional (default 1) and must be a number in (0, ${MAX_WEIGHT}]. It is the slot's active-session budget \u2014 how many concurrent sessions it may hold. The pool's total capacity is the sum of the weights. NOT a priority; pick order follows line order.
|
|
1881
1935
|
- \`#\` starts a comment (whole-line or trailing); blank lines are ignored.
|
|
1882
|
-
If the instruction names a model ambiguously,
|
|
1936
|
+
If the instruction names a model ambiguously, stop and ask rather than guessing.
|
|
1883
1937
|
|
|
1884
1938
|
3. Write the WHOLE file, not a patch. This file is an override, not an edit target: any slot you drop disappears, and slot numbers are line positions, so a partial write silently renumbers slots. Keep the existing comment header. If the write fails on a missing directory, create \`~/.config/lacode\` first and retry.
|
|
1885
1939
|
|
|
@@ -1905,6 +1959,11 @@ $ARGUMENTS
|
|
|
1905
1959
|
|
|
1906
1960
|
Report the tool's output as it came back, then add one short line naming what changed: re-admitted, still limited, or benched because the provider does not serve the model. If a model was benched as \`model-missing\`, say so and name the replacement in \`~/.config/lacode/pool.txt\` \u2014 but change nothing yourself.`
|
|
1907
1961
|
},
|
|
1962
|
+
"reprocess-timeline": {
|
|
1963
|
+
description: "Rebuild this session's timeline from its full transcript",
|
|
1964
|
+
agent: "implement",
|
|
1965
|
+
template: "Call the `timeline_reprocess` tool exactly once with no arguments. Then reply with only its output."
|
|
1966
|
+
},
|
|
1908
1967
|
"image-in-cb": {
|
|
1909
1968
|
description: "Attach the clipboard image to this message \u2014 macOS needs pngpaste, Linux needs wl-paste or xclip.",
|
|
1910
1969
|
template: "$ARGUMENTS"
|
|
@@ -2198,6 +2257,7 @@ import { tool } from "@opencode-ai/plugin";
|
|
|
2198
2257
|
var PROBE_SYSTEM = "You are a connection health check. Reply with the single word: ok";
|
|
2199
2258
|
var PROBE_PROMPT = "ok";
|
|
2200
2259
|
var PROBE_TIMEOUT_MS = 45e3;
|
|
2260
|
+
var REASON_UNRESPONSIVE = "no answer";
|
|
2201
2261
|
var failureText = (error) => {
|
|
2202
2262
|
if (error === void 0 || error === null) return "";
|
|
2203
2263
|
if (typeof error === "string") return error;
|
|
@@ -2210,32 +2270,35 @@ var failureText = (error) => {
|
|
|
2210
2270
|
var shortReason = (text) => {
|
|
2211
2271
|
if (!text) return "failed";
|
|
2212
2272
|
if (isModelMissingText(text)) return MODEL_MISSING_REASON;
|
|
2273
|
+
if (isAuthFailureText(text)) return AUTH_FAILURE_REASON;
|
|
2213
2274
|
return limitReason(text) ?? (isLimitText(text) ? "limit" : "failed");
|
|
2214
2275
|
};
|
|
2215
2276
|
var probeOne = async (client, opts, model, leftSeconds) => {
|
|
2216
2277
|
const { state: s, directory } = opts;
|
|
2217
2278
|
const { provider, id: modelID } = splitModel(model);
|
|
2218
|
-
const settle = (ok, reason, left) => ({
|
|
2279
|
+
const settle = (ok, reason, left, kind, strikes) => ({
|
|
2219
2280
|
model,
|
|
2220
2281
|
ok,
|
|
2221
2282
|
reason,
|
|
2222
2283
|
hadSeconds: leftSeconds,
|
|
2223
|
-
leftSeconds: left
|
|
2284
|
+
leftSeconds: left,
|
|
2285
|
+
kind,
|
|
2286
|
+
strikes
|
|
2224
2287
|
});
|
|
2225
2288
|
const created = await client.session.create({ body: { title: `pool probe (${model})` }, query: { directory } }).then((r) => r?.data?.id).catch(() => void 0);
|
|
2226
|
-
if (!created) return settle(false, "no probe session", leftSeconds);
|
|
2289
|
+
if (!created) return settle(false, "no probe session", leftSeconds, "cooling");
|
|
2227
2290
|
markProbe(s, created);
|
|
2228
2291
|
let timer;
|
|
2229
2292
|
try {
|
|
2230
2293
|
const timeoutMs = opts.timeoutMs ?? PROBE_TIMEOUT_MS;
|
|
2231
2294
|
const clock = opts.now ?? Date.now;
|
|
2232
2295
|
let timedOut = false;
|
|
2233
|
-
const deadline = new Promise((
|
|
2296
|
+
const deadline = new Promise((resolve5) => {
|
|
2234
2297
|
timer = setTimeout(() => {
|
|
2235
2298
|
timedOut = true;
|
|
2236
2299
|
void client.session.abort({ path: { id: created }, query: { directory } }).catch(() => {
|
|
2237
2300
|
});
|
|
2238
|
-
|
|
2301
|
+
resolve5(void 0);
|
|
2239
2302
|
}, timeoutMs);
|
|
2240
2303
|
});
|
|
2241
2304
|
const answer = client.session.prompt({
|
|
@@ -2254,20 +2317,37 @@ var probeOne = async (client, opts, model, leftSeconds) => {
|
|
|
2254
2317
|
const failure = result?.error ?? result?.data?.info?.error;
|
|
2255
2318
|
if (!timedOut && failure === void 0 && result?.data?.info !== void 0) {
|
|
2256
2319
|
clearCooldowns(s, [model]);
|
|
2257
|
-
return settle(true, "answered", 0);
|
|
2320
|
+
return settle(true, "answered", 0, "answered");
|
|
2258
2321
|
}
|
|
2259
2322
|
const text = failureText(failure);
|
|
2323
|
+
let kind = "cooling";
|
|
2324
|
+
let strikes;
|
|
2260
2325
|
if (isModelMissingText(text)) {
|
|
2261
|
-
recordStrike(s, model, MODEL_MISSING_REASON, MODEL_MISSING_COOLDOWN_MS);
|
|
2326
|
+
const recorded = recordStrike(s, model, MODEL_MISSING_REASON, MODEL_MISSING_COOLDOWN_MS);
|
|
2327
|
+
strikes = recorded?.strikes;
|
|
2328
|
+
kind = "benched";
|
|
2329
|
+
} else if (isAuthFailureText(text)) {
|
|
2330
|
+
const recorded = recordStrike(s, model, AUTH_FAILURE_REASON, AUTH_FAILURE_COOLDOWN_MS);
|
|
2331
|
+
strikes = recorded?.strikes;
|
|
2332
|
+
kind = "benched";
|
|
2333
|
+
} else if (timedOut || text === "") {
|
|
2334
|
+
const recorded = recordStrike(s, model, REASON_UNRESPONSIVE, 0);
|
|
2335
|
+
strikes = recorded?.strikes;
|
|
2336
|
+
kind = "benched";
|
|
2262
2337
|
} else {
|
|
2263
2338
|
const stated = statedCooldown(text);
|
|
2264
|
-
if (stated !== void 0)
|
|
2339
|
+
if (stated !== void 0) {
|
|
2340
|
+
const recorded = recordStrike(s, model, shortReason(text), stated);
|
|
2341
|
+
strikes = recorded?.strikes;
|
|
2342
|
+
}
|
|
2265
2343
|
}
|
|
2266
2344
|
const still = readLimits(s).get(model);
|
|
2267
2345
|
return settle(
|
|
2268
2346
|
false,
|
|
2269
2347
|
timedOut ? `no answer in ${Math.round(timeoutMs / 1e3)}s` : shortReason(text),
|
|
2270
|
-
still && still.until > clock() ? Math.ceil((still.until - clock()) / 1e3) : 0
|
|
2348
|
+
still && still.until > clock() ? Math.ceil((still.until - clock()) / 1e3) : 0,
|
|
2349
|
+
kind,
|
|
2350
|
+
strikes
|
|
2271
2351
|
);
|
|
2272
2352
|
} finally {
|
|
2273
2353
|
if (timer) clearTimeout(timer);
|
|
@@ -2352,6 +2432,24 @@ var SLOT_WATCH_MS = 1e3;
|
|
|
2352
2432
|
var SLOT_WATCH_DEBOUNCE_MS = 250;
|
|
2353
2433
|
var ABORT_TIMEOUT_MS = 1e4;
|
|
2354
2434
|
var formatWindow = (seconds) => seconds < 60 ? `${seconds}s` : `${Math.ceil(seconds / 60)}m`;
|
|
2435
|
+
var reportText = (report) => {
|
|
2436
|
+
if (report.outcomes.length === 0) return "no models are cooling down, so there was nothing to probe.";
|
|
2437
|
+
const answered = report.outcomes.filter((outcome) => outcome.ok).length;
|
|
2438
|
+
const benched = report.outcomes.filter((outcome) => outcome.kind === "benched").length;
|
|
2439
|
+
const cooling = report.outcomes.length - answered - benched;
|
|
2440
|
+
return [
|
|
2441
|
+
...report.outcomes.map((outcome) => {
|
|
2442
|
+
if (outcome.ok) return `${outcome.model}: answered, cooldown cleared`;
|
|
2443
|
+
if (outcome.kind === "benched") {
|
|
2444
|
+
const window = outcome.leftSeconds > 0 ? ` for ${Math.round(outcome.leftSeconds)}s` : "";
|
|
2445
|
+
const strike = outcome.strikes !== void 0 ? ` (strike ${outcome.strikes})` : "";
|
|
2446
|
+
return `${outcome.model}: ${outcome.reason} \u2014 benched${window}${strike}`;
|
|
2447
|
+
}
|
|
2448
|
+
return `${outcome.model}: still limited (${outcome.reason}) \u2014 cooldown kept${outcome.leftSeconds > 0 ? `, ${formatWindow(outcome.leftSeconds)} left` : ""}`;
|
|
2449
|
+
}),
|
|
2450
|
+
`summary: ${answered} answered, ${benched} benched, ${cooling} still cooling`
|
|
2451
|
+
].join("\n");
|
|
2452
|
+
};
|
|
2355
2453
|
var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
2356
2454
|
const opts = options ?? {};
|
|
2357
2455
|
const s = state(opts);
|
|
@@ -2363,6 +2461,63 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2363
2461
|
s.warned.add(message);
|
|
2364
2462
|
await log("warn", message);
|
|
2365
2463
|
};
|
|
2464
|
+
const refreshProviders = async (force = false) => {
|
|
2465
|
+
if (!force && Date.now() - s.connectedAt < PROVIDER_LIST_TTL_MS) return;
|
|
2466
|
+
s.connectedAt = Date.now();
|
|
2467
|
+
try {
|
|
2468
|
+
const providerApi = client.provider;
|
|
2469
|
+
if (!providerApi || typeof providerApi.list !== "function") return;
|
|
2470
|
+
const result = await providerApi.list({ query: { directory } });
|
|
2471
|
+
const data = result?.data;
|
|
2472
|
+
const connected = data?.connected;
|
|
2473
|
+
const all = data?.all;
|
|
2474
|
+
if (Array.isArray(connected) && Array.isArray(all)) {
|
|
2475
|
+
s.connected = new Set(connected.filter((id) => typeof id === "string"));
|
|
2476
|
+
const credentialed = /* @__PURE__ */ new Set();
|
|
2477
|
+
for (const entry of all) {
|
|
2478
|
+
if (!entry || typeof entry !== "object") continue;
|
|
2479
|
+
const rec = entry;
|
|
2480
|
+
const id = typeof rec.id === "string" ? rec.id : void 0;
|
|
2481
|
+
if (!id) continue;
|
|
2482
|
+
if (typeof rec.key === "string" && rec.key.trim() !== "") {
|
|
2483
|
+
credentialed.add(id);
|
|
2484
|
+
continue;
|
|
2485
|
+
}
|
|
2486
|
+
const options2 = rec.options;
|
|
2487
|
+
if (options2 && typeof options2 === "object") {
|
|
2488
|
+
const opts2 = options2;
|
|
2489
|
+
if (typeof opts2.apiKey === "string" && opts2.apiKey.trim() !== "") {
|
|
2490
|
+
credentialed.add(id);
|
|
2491
|
+
continue;
|
|
2492
|
+
}
|
|
2493
|
+
const baseURL = typeof opts2.baseURL === "string" ? opts2.baseURL.trim() : "";
|
|
2494
|
+
if (baseURL !== "") {
|
|
2495
|
+
const envArr2 = Array.isArray(rec.env) ? rec.env : [];
|
|
2496
|
+
const hasCredEnv = envArr2.some((e) => typeof e === "string" && /key|token|secret|password|credential|auth/i.test(e));
|
|
2497
|
+
if (!hasCredEnv) {
|
|
2498
|
+
credentialed.add(id);
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
const envArr = Array.isArray(rec.env) ? rec.env : [];
|
|
2504
|
+
for (const e of envArr) {
|
|
2505
|
+
if (typeof e === "string" && /key|token|secret|password|credential|auth/i.test(e)) {
|
|
2506
|
+
if (typeof process.env[e] === "string" && process.env[e].trim() !== "") {
|
|
2507
|
+
credentialed.add(id);
|
|
2508
|
+
break;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
s.credentialed = credentialed;
|
|
2514
|
+
s.connectedReady = true;
|
|
2515
|
+
} else if (Array.isArray(connected)) {
|
|
2516
|
+
s.connected = new Set(connected.filter((id) => typeof id === "string"));
|
|
2517
|
+
}
|
|
2518
|
+
} catch {
|
|
2519
|
+
}
|
|
2520
|
+
};
|
|
2366
2521
|
init(s, (m) => void warnOnce(m));
|
|
2367
2522
|
const sweepDirIfDue = () => {
|
|
2368
2523
|
if (Date.now() - s.lastDirSweep < STATE_SWEEP_INTERVAL_MS) return;
|
|
@@ -2389,6 +2544,73 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2389
2544
|
return { ok: false, why: String(error) };
|
|
2390
2545
|
}
|
|
2391
2546
|
};
|
|
2547
|
+
const abortReports = /* @__PURE__ */ new Map();
|
|
2548
|
+
const abortKind = (reason) => isQuotaReason(reason) || isLimitText(reason) ? "rate-limit" : "provider error";
|
|
2549
|
+
const abortLabel = (kind, elapsedMs) => {
|
|
2550
|
+
const seconds = Math.max(1, Math.round(elapsedMs / 1e3));
|
|
2551
|
+
if (kind === "no-answer") return `no answer in ${seconds}s`;
|
|
2552
|
+
if (kind === "rate-limit") return `rate limited after ${seconds}s`;
|
|
2553
|
+
if (kind === "empty response") return `empty response after ${seconds}s`;
|
|
2554
|
+
if (kind === "permission") return `permission denied after ${seconds}s`;
|
|
2555
|
+
return `provider error after ${seconds}s`;
|
|
2556
|
+
};
|
|
2557
|
+
const pendingCancelledParts = /* @__PURE__ */ new Map();
|
|
2558
|
+
const patchedCancelledParts = /* @__PURE__ */ new Set();
|
|
2559
|
+
const cancelledPrefix = "Task Cancelled by LaCode:";
|
|
2560
|
+
const patchCancelledPart = async (part, key, report) => {
|
|
2561
|
+
if (patchedCancelledParts.has(key)) return;
|
|
2562
|
+
patchedCancelledParts.add(key);
|
|
2563
|
+
if (patchedCancelledParts.size > 64) {
|
|
2564
|
+
const oldest = patchedCancelledParts.values().next().value;
|
|
2565
|
+
if (oldest !== void 0) patchedCancelledParts.delete(oldest);
|
|
2566
|
+
}
|
|
2567
|
+
const error = `${cancelledPrefix} ${report.message}`;
|
|
2568
|
+
try {
|
|
2569
|
+
const url = new URL(
|
|
2570
|
+
`/session/${encodeURIComponent(part.sessionID)}/message/${encodeURIComponent(part.messageID)}/part/${encodeURIComponent(part.id)}`,
|
|
2571
|
+
serverUrl
|
|
2572
|
+
);
|
|
2573
|
+
if (directory) url.searchParams.set("directory", directory);
|
|
2574
|
+
const headers = { "Content-Type": "application/json" };
|
|
2575
|
+
const password = process.env.OPENCODE_SERVER_PASSWORD;
|
|
2576
|
+
if (password) {
|
|
2577
|
+
const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode";
|
|
2578
|
+
headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
2579
|
+
}
|
|
2580
|
+
const response = await fetch(url, {
|
|
2581
|
+
method: "PATCH",
|
|
2582
|
+
headers,
|
|
2583
|
+
body: JSON.stringify({ ...part, state: { ...part.state, error } }),
|
|
2584
|
+
signal: AbortSignal.timeout(5e3)
|
|
2585
|
+
});
|
|
2586
|
+
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`.trim());
|
|
2587
|
+
abortReports.delete(key);
|
|
2588
|
+
} catch (cause) {
|
|
2589
|
+
await log("warn", `could not rewrite cancelled task ${part.callID}: ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
2590
|
+
session: part.sessionID,
|
|
2591
|
+
callID: part.callID
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
};
|
|
2595
|
+
const noteAbort = async (key, report) => {
|
|
2596
|
+
abortReports.set(key, report);
|
|
2597
|
+
if (abortReports.size > 32) {
|
|
2598
|
+
const oldest = abortReports.keys().next().value;
|
|
2599
|
+
if (oldest !== void 0) abortReports.delete(oldest);
|
|
2600
|
+
}
|
|
2601
|
+
await log("warn", report.message, {
|
|
2602
|
+
slot: report.slot,
|
|
2603
|
+
model: report.model,
|
|
2604
|
+
elapsedMs: report.elapsedMs,
|
|
2605
|
+
failure: report.kind,
|
|
2606
|
+
released: report.released
|
|
2607
|
+
});
|
|
2608
|
+
const pending = pendingCancelledParts.get(key);
|
|
2609
|
+
if (pending) {
|
|
2610
|
+
pendingCancelledParts.delete(key);
|
|
2611
|
+
await patchCancelledPart(pending, key, report);
|
|
2612
|
+
}
|
|
2613
|
+
};
|
|
2392
2614
|
const MAX_ABORT_ATTEMPTS = 3;
|
|
2393
2615
|
const coolForHang = async (claim, log2) => {
|
|
2394
2616
|
const now = Date.now();
|
|
@@ -2444,6 +2666,16 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2444
2666
|
const kill = await abortSession(sessionID);
|
|
2445
2667
|
if (kill.ok) {
|
|
2446
2668
|
release(s, key);
|
|
2669
|
+
const elapsedMs = Date.now() - claim.t;
|
|
2670
|
+
const kind = abortKind(reason);
|
|
2671
|
+
await noteAbort(key, {
|
|
2672
|
+
slot: claim.k,
|
|
2673
|
+
model: claim.m,
|
|
2674
|
+
elapsedMs,
|
|
2675
|
+
kind,
|
|
2676
|
+
released: true,
|
|
2677
|
+
message: `slot ${claim.k} ${claim.m} \u2014 ${abortLabel(kind, elapsedMs)}; claim released`
|
|
2678
|
+
});
|
|
2447
2679
|
await log(
|
|
2448
2680
|
"warn",
|
|
2449
2681
|
`limit (${reason}) on ${model}; aborted in-flight ${claim.v} session ${sessionID} to fail over now`,
|
|
@@ -2512,6 +2744,15 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2512
2744
|
reason === "overran" ? `overran ${claim.v} on ${claim.m} after ${age}s; aborted session ${claim.g}` : `hung ${claim.v} on ${claim.m} after ${age}s with ${since}; aborted session ${claim.g}`,
|
|
2513
2745
|
{ base: claim.v, model: claim.m, slot: claim.k, session: claim.g, aborted: true }
|
|
2514
2746
|
);
|
|
2747
|
+
const elapsedMs = Date.now() - claim.t;
|
|
2748
|
+
await noteAbort(key, {
|
|
2749
|
+
slot: claim.k,
|
|
2750
|
+
model: claim.m,
|
|
2751
|
+
elapsedMs,
|
|
2752
|
+
kind: "no-answer",
|
|
2753
|
+
released: true,
|
|
2754
|
+
message: `slot ${claim.k} ${claim.m} \u2014 ${reason === "overran" ? `overran after ${Math.max(1, Math.round(elapsedMs / 1e3))}s` : abortLabel("no-answer", elapsedMs)}; claim released`
|
|
2755
|
+
});
|
|
2515
2756
|
await coolForHang(claim, log);
|
|
2516
2757
|
continue;
|
|
2517
2758
|
}
|
|
@@ -2563,17 +2804,10 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2563
2804
|
if (report.outcomes.length === 0) return "nothing cooling to probe";
|
|
2564
2805
|
const parts = [`probed ${report.outcomes.length} model(s)`, `re-admitted ${report.cleared.length}`];
|
|
2565
2806
|
if (report.missing.length > 0) parts.push(`benched ${report.missing.length} missing`);
|
|
2807
|
+
const authBenched = report.outcomes.filter((outcome) => outcome.reason === AUTH_FAILURE_REASON).length;
|
|
2808
|
+
if (authBenched > 0) parts.push(`benched ${authBenched} auth-failed`);
|
|
2566
2809
|
return parts.join(", ");
|
|
2567
2810
|
};
|
|
2568
|
-
const reportText = (report) => {
|
|
2569
|
-
if (report.outcomes.length === 0) return "no models are cooling down, so there was nothing to probe.";
|
|
2570
|
-
return [
|
|
2571
|
-
...report.outcomes.map(
|
|
2572
|
-
(outcome) => outcome.ok ? `${outcome.model}: answered \u2014 cooldown cleared (it had ${formatWindow(outcome.hadSeconds)} left), back in the pool` : outcome.reason === MODEL_MISSING_REASON ? `${outcome.model}: not served by the provider (${outcome.reason}) \u2014 benched${outcome.leftSeconds > 0 ? `, ${formatWindow(outcome.leftSeconds)} left` : ""}` : `${outcome.model}: still limited (${outcome.reason}) \u2014 cooldown kept${outcome.leftSeconds > 0 ? `, ${formatWindow(outcome.leftSeconds)} left` : ""}`
|
|
2573
|
-
),
|
|
2574
|
-
report.cleared.length > 0 ? `re-admitted: ${report.cleared.join(", ")}. They route again on the next pooled spawn \u2014 nothing needs restarting or reloading.` : report.missing.length > 0 ? `benched: ${report.missing.join(", ")}. The provider does not serve them; replace them in the pool.` : report.outcomes.every((outcome) => outcome.ok) ? "every model that was asked answered; nothing needed re-admitting." : "nothing was re-admitted; the pool is short exactly as much as it was."
|
|
2575
|
-
].join("\n");
|
|
2576
|
-
};
|
|
2577
2811
|
const announce = async (report) => {
|
|
2578
2812
|
if (!report.changed) return;
|
|
2579
2813
|
const message = report.cleared.length > 0 ? `Re-admitted ${report.cleared.join(", ")} \u2014 they answer again.` : report.missing.length > 0 ? `Benched ${report.missing.join(", ")} \u2014 the provider does not serve them.` : `${report.kept.length} cooling model(s) re-checked, still limited.`;
|
|
@@ -2608,15 +2842,34 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2608
2842
|
const slots = ensureSlots(s, (m) => void warnOnce(m));
|
|
2609
2843
|
const agents = cfg.agent;
|
|
2610
2844
|
const providers = cfg.provider;
|
|
2845
|
+
const configCreds = /* @__PURE__ */ new Set();
|
|
2846
|
+
for (const [id, entry] of Object.entries(providers ?? {})) {
|
|
2847
|
+
const options2 = entry.options;
|
|
2848
|
+
if (typeof options2?.apiKey === "string" && options2.apiKey.trim() !== "") configCreds.add(id);
|
|
2849
|
+
}
|
|
2850
|
+
s.configCreds = configCreds;
|
|
2611
2851
|
const known = /* @__PURE__ */ new Set();
|
|
2612
2852
|
for (const [name, def] of Object.entries(cfg.agent ?? {})) if (def?.model) s.models.set(name, def.model);
|
|
2613
2853
|
for (const slot of slots) {
|
|
2614
2854
|
for (const base of BASES) {
|
|
2615
2855
|
const name = agentFor(base, slot.index);
|
|
2616
|
-
|
|
2856
|
+
let def = agents?.[name];
|
|
2617
2857
|
if (!def) {
|
|
2618
|
-
|
|
2619
|
-
|
|
2858
|
+
const baseDef = agents?.[base];
|
|
2859
|
+
if (!agents || !baseDef) {
|
|
2860
|
+
await warnOnce(`variant ${name} is missing from the config; spawns of ${base} that route to slot ${slot.index} would fail`);
|
|
2861
|
+
continue;
|
|
2862
|
+
}
|
|
2863
|
+
const created = {
|
|
2864
|
+
...baseDef,
|
|
2865
|
+
description: `Hidden pool slot ${slot.index} for ${base}. Its model is set by the pool at config time. Spawn '${base}' and let the pool route.`,
|
|
2866
|
+
hidden: true,
|
|
2867
|
+
mode: "primary"
|
|
2868
|
+
};
|
|
2869
|
+
delete created.model;
|
|
2870
|
+
agents[name] = created;
|
|
2871
|
+
def = created;
|
|
2872
|
+
await log("info", `declared variant ${name} for slot ${slot.index} (table grew after startup)`);
|
|
2620
2873
|
}
|
|
2621
2874
|
if (def.model && def.model !== slot.model) {
|
|
2622
2875
|
await warnOnce(`${name} still hard-codes ${def.model}; the resolved pool preset wins (${slot.model}). Delete the line to keep one source of truth.`);
|
|
@@ -2629,6 +2882,11 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2629
2882
|
const provider = slot.model.slice(0, slash);
|
|
2630
2883
|
const id = slot.model.slice(slash + 1);
|
|
2631
2884
|
const entry = providers?.[provider];
|
|
2885
|
+
if (!providerReady(s, slot.model)) {
|
|
2886
|
+
await warnOnce(
|
|
2887
|
+
`provider ${provider} is not connected in opencode; slot ${slot.index} (${slot.model}) is excluded from routing until it is authenticated (run \`opencode auth login ${provider}\` or remove the line)`
|
|
2888
|
+
);
|
|
2889
|
+
}
|
|
2632
2890
|
if (!entry) {
|
|
2633
2891
|
await warnOnce(`provider ${provider} is not configured in opencode.jsonc; slot ${slot.index} (${slot.model}) cannot resolve`);
|
|
2634
2892
|
continue;
|
|
@@ -2698,7 +2956,8 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2698
2956
|
const provider = typeof action?.provider === "string" ? action.provider : void 0;
|
|
2699
2957
|
const reason2 = action?.reason ?? "";
|
|
2700
2958
|
const message = typeof status2?.message === "string" ? status2.message : "";
|
|
2701
|
-
const
|
|
2959
|
+
const auth2 = isAuthFailureText(reason2) || isAuthFailureText(message);
|
|
2960
|
+
const limited2 = reason2 === "free_tier_limit" || isLimitText(reason2) || isLimitText(message) || auth2;
|
|
2702
2961
|
if (status2?.type === "retry" && limited2) {
|
|
2703
2962
|
if (sessionID && isProbe(s, sessionID)) {
|
|
2704
2963
|
await log("debug", `probe on provider ${provider ?? "?"} was limited (${reason2 || "rate-limited"}); not cooling the provider`);
|
|
@@ -2706,9 +2965,9 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2706
2965
|
}
|
|
2707
2966
|
const own = claimFor(sessionID);
|
|
2708
2967
|
const outage = isUnavailableText(message) && !isQuotaReason(reason2);
|
|
2709
|
-
const named = outage ? UNAVAILABLE_REASON : reason2 || limitReason(message) || "rate-limited";
|
|
2968
|
+
const named = auth2 ? AUTH_FAILURE_REASON : outage ? UNAVAILABLE_REASON : reason2 || limitReason(message) || "rate-limited";
|
|
2710
2969
|
if (provider) {
|
|
2711
|
-
const until = outage ? Date.now() + UNAVAILABLE_COOLDOWN_MS : typeof status2.next === "number" && status2.next > Date.now() ? status2.next : Date.now() + HANG_FALLBACK_MS;
|
|
2970
|
+
const until = auth2 ? Date.now() + AUTH_FAILURE_COOLDOWN_MS : outage ? Date.now() + UNAVAILABLE_COOLDOWN_MS : typeof status2.next === "number" && status2.next > Date.now() ? status2.next : Date.now() + HANG_FALLBACK_MS;
|
|
2712
2971
|
const cooled = coolProvider(s, provider, until, named);
|
|
2713
2972
|
if (cooled.length > 0) {
|
|
2714
2973
|
await log("warn", `limit on provider ${provider} (${named}); cooling ${cooled.length} slot(s) until ${new Date(until).toISOString().slice(11, 19)}Z`, {
|
|
@@ -2718,7 +2977,16 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2718
2977
|
});
|
|
2719
2978
|
}
|
|
2720
2979
|
} else if (own) {
|
|
2721
|
-
if (
|
|
2980
|
+
if (auth2) {
|
|
2981
|
+
const strike2 = recordStrike(s, own.claim.m, AUTH_FAILURE_REASON, AUTH_FAILURE_COOLDOWN_MS);
|
|
2982
|
+
if (strike2) {
|
|
2983
|
+
await log("warn", `limit on ${own.claim.m} (${AUTH_FAILURE_REASON}); cooling it for ${Math.round((strike2.until - Date.now()) / 1e3)}s`, {
|
|
2984
|
+
model: own.claim.m,
|
|
2985
|
+
slot: own.claim.k,
|
|
2986
|
+
session: sessionID
|
|
2987
|
+
});
|
|
2988
|
+
}
|
|
2989
|
+
} else if (outage) {
|
|
2722
2990
|
const transient = coolUnavailable(s, own.claim.m);
|
|
2723
2991
|
if (transient) await log("warn", `cooling ${own.claim.m} for ${Math.round((transient.until - Date.now()) / 1e3)}s (${transient.reason})`);
|
|
2724
2992
|
} else {
|
|
@@ -2750,15 +3018,63 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2750
3018
|
await log("debug", `probe session ${sessionID} ended with an error; not cooling anything`);
|
|
2751
3019
|
return;
|
|
2752
3020
|
}
|
|
3021
|
+
const own = claimFor(sessionID);
|
|
3022
|
+
const missing2 = isModelMissingText(haystack2);
|
|
2753
3023
|
const status2 = typeof error?.data?.statusCode === "number" ? error.data.statusCode : void 0;
|
|
3024
|
+
const auth2 = isAuthFailureText(haystack2) || status2 === 401 || status2 === 403;
|
|
2754
3025
|
const limited2 = isLimitText(haystack2) || status2 === 429;
|
|
2755
3026
|
const outage = isUnavailableText(haystack2);
|
|
2756
|
-
if (
|
|
2757
|
-
|
|
2758
|
-
|
|
3027
|
+
if (missing2) {
|
|
3028
|
+
const unbound = !own && sessionID ? [...s.own.entries()].find(([, claim]) => !claim.g && claim.s === sessionID && haystack2.includes(claim.m)) : void 0;
|
|
3029
|
+
const target = own ?? (unbound ? { key: unbound[0], claim: unbound[1] } : void 0);
|
|
3030
|
+
const model2 = target?.claim.m;
|
|
3031
|
+
if (model2) {
|
|
3032
|
+
const strike2 = recordStrike(s, model2, MODEL_MISSING_REASON, MODEL_MISSING_COOLDOWN_MS);
|
|
3033
|
+
if (strike2) {
|
|
3034
|
+
await log("warn", `bench ${model2} (${MODEL_MISSING_REASON}, strike ${strike2.strikes}) after a terminal model-not-found error`, {
|
|
3035
|
+
model: model2,
|
|
3036
|
+
slot: target?.claim.k,
|
|
3037
|
+
session: sessionID
|
|
3038
|
+
});
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
if (target) {
|
|
3042
|
+
release(s, target.key);
|
|
3043
|
+
await log("warn", `released ${target.claim.v} claim on ${target.claim.m} after a terminal model-not-found error`, {
|
|
3044
|
+
base: target.claim.v,
|
|
3045
|
+
model: target.claim.m,
|
|
3046
|
+
slot: target.claim.k,
|
|
3047
|
+
session: sessionID
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
if (!limited2 && !outage && !auth2) {
|
|
3053
|
+
if (own) {
|
|
3054
|
+
release(s, own.key);
|
|
3055
|
+
await log("warn", `released ${own.claim.v} claim on ${own.claim.m} after a terminal session error`, {
|
|
3056
|
+
base: own.claim.v,
|
|
3057
|
+
model: own.claim.m,
|
|
3058
|
+
slot: own.claim.k,
|
|
3059
|
+
session: sessionID
|
|
3060
|
+
});
|
|
3061
|
+
}
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
const named = auth2 ? AUTH_FAILURE_REASON : outage ? UNAVAILABLE_REASON : limitReason(haystack2) ?? "rate-limited";
|
|
2759
3065
|
let stated2;
|
|
2760
3066
|
if (own) {
|
|
2761
|
-
if (
|
|
3067
|
+
if (auth2) {
|
|
3068
|
+
stated2 = AUTH_FAILURE_COOLDOWN_MS;
|
|
3069
|
+
const strike2 = recordStrike(s, own.claim.m, AUTH_FAILURE_REASON, stated2);
|
|
3070
|
+
if (strike2) {
|
|
3071
|
+
await log("warn", `limit on ${own.claim.m} (${AUTH_FAILURE_REASON}); cooling it for ${Math.round((strike2.until - Date.now()) / 1e3)}s from a terminal session.error`, {
|
|
3072
|
+
model: own.claim.m,
|
|
3073
|
+
slot: own.claim.k,
|
|
3074
|
+
session: sessionID
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
} else if (outage) {
|
|
2762
3078
|
const transient = coolUnavailable(s, own.claim.m);
|
|
2763
3079
|
if (transient) {
|
|
2764
3080
|
await log("warn", `cooling ${own.claim.m} for ${Math.round((transient.until - Date.now()) / 1e3)}s (${transient.reason}) after a terminal error`);
|
|
@@ -2776,6 +3092,15 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2776
3092
|
}
|
|
2777
3093
|
}
|
|
2778
3094
|
await failOver(sessionID, own?.claim.m ?? "?", named, { stated: stated2, cool: !own });
|
|
3095
|
+
if (own && s.own.has(own.key)) {
|
|
3096
|
+
release(s, own.key);
|
|
3097
|
+
await log("warn", `released ${own.claim.v} claim on ${own.claim.m} after a terminal session error (abort did not confirm)`, {
|
|
3098
|
+
base: own.claim.v,
|
|
3099
|
+
model: own.claim.m,
|
|
3100
|
+
slot: own.claim.k,
|
|
3101
|
+
session: sessionID
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
2779
3104
|
return;
|
|
2780
3105
|
}
|
|
2781
3106
|
if (event.type === "session.created") {
|
|
@@ -2789,29 +3114,73 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2789
3114
|
const sessionID = info2?.sessionID ?? part?.sessionID;
|
|
2790
3115
|
if (sessionID) noteActivity(s, sessionID);
|
|
2791
3116
|
}
|
|
3117
|
+
if (event.type === "message.part.updated") {
|
|
3118
|
+
const part = props.part;
|
|
3119
|
+
if (part?.type === "tool" && part.tool === "task" && part.state?.status === "error" && part.state.error === "Task cancelled" && part.id && part.sessionID && part.messageID && part.callID) {
|
|
3120
|
+
const key = claimKey(part.sessionID, part.callID);
|
|
3121
|
+
const full = part;
|
|
3122
|
+
const report = abortReports.get(key);
|
|
3123
|
+
if (report) {
|
|
3124
|
+
await patchCancelledPart(full, key, report);
|
|
3125
|
+
} else {
|
|
3126
|
+
pendingCancelledParts.set(key, full);
|
|
3127
|
+
if (pendingCancelledParts.size > 32) {
|
|
3128
|
+
const oldest = pendingCancelledParts.keys().next().value;
|
|
3129
|
+
if (oldest !== void 0) pendingCancelledParts.delete(oldest);
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
2792
3135
|
if (event.type !== "message.updated") return;
|
|
2793
3136
|
const info = props.info;
|
|
2794
3137
|
if (!info || info.role !== "assistant" || !info.error) return;
|
|
2795
3138
|
const haystack = JSON.stringify(info.error) ?? "";
|
|
2796
3139
|
const status = typeof info.error.data?.statusCode === "number" ? info.error.data.statusCode : void 0;
|
|
3140
|
+
const auth = isAuthFailureText(haystack) || status === 401 || status === 403;
|
|
2797
3141
|
const limited = isLimitText(haystack) || status === 429;
|
|
2798
3142
|
const gateway5xx = status !== void 0 && status >= 500 && status <= 599;
|
|
2799
3143
|
const transientGateway = gateway5xx || isTransientGatewayText(haystack);
|
|
2800
|
-
|
|
3144
|
+
const missing = isModelMissingText(haystack);
|
|
3145
|
+
if (!limited && !transientGateway && !auth && !missing) return;
|
|
2801
3146
|
const model = `${info.providerID ?? "?"}/${info.modelID ?? "?"}`;
|
|
2802
3147
|
if (info.sessionID && isProbe(s, info.sessionID)) {
|
|
2803
3148
|
await log("debug", `probe of ${model} hit a limit (${isLimitText(haystack) ? "limit" : "transient"}); leaving the cooldown to the probe`);
|
|
2804
3149
|
return;
|
|
2805
3150
|
}
|
|
2806
3151
|
const ownMsg = claimFor(info.sessionID);
|
|
2807
|
-
if (
|
|
3152
|
+
if (missing) {
|
|
3153
|
+
const ownMsgMissing = ownMsg;
|
|
3154
|
+
const missingModel = ownMsgMissing?.claim.m ?? model;
|
|
3155
|
+
if (missingModel !== "?/?") {
|
|
3156
|
+
const strike2 = recordStrike(s, missingModel, MODEL_MISSING_REASON, MODEL_MISSING_COOLDOWN_MS);
|
|
3157
|
+
if (strike2) {
|
|
3158
|
+
await log("warn", `bench ${missingModel} (${MODEL_MISSING_REASON}, strike ${strike2.strikes}) after a model-not-found error`, {
|
|
3159
|
+
model: missingModel,
|
|
3160
|
+
slot: ownMsgMissing?.claim.k,
|
|
3161
|
+
session: info.sessionID
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
if (ownMsgMissing) {
|
|
3166
|
+
release(s, ownMsgMissing.key);
|
|
3167
|
+
await log("warn", `released ${ownMsgMissing.claim.v} claim on ${ownMsgMissing.claim.m} after a model-not-found error`, {
|
|
3168
|
+
base: ownMsgMissing.claim.v,
|
|
3169
|
+
model: ownMsgMissing.claim.m,
|
|
3170
|
+
slot: ownMsgMissing.claim.k,
|
|
3171
|
+
session: info.sessionID
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
3176
|
+
if (isUnavailableText(haystack) || !limited && !auth && transientGateway) {
|
|
2808
3177
|
const transient = coolUnavailable(s, model);
|
|
2809
3178
|
if (transient) await log("warn", `cooling ${model} for ${Math.round((transient.until - Date.now()) / 1e3)}s (${transient.reason})`);
|
|
2810
3179
|
await failOver(info.sessionID, model, transient?.reason ?? UNAVAILABLE_REASON, { cool: false });
|
|
2811
3180
|
return;
|
|
2812
3181
|
}
|
|
2813
|
-
const stated = statedCooldown(haystack) ?? retryAfterMs(info.error.data?.responseHeaders);
|
|
2814
|
-
const reason = limitReason(haystack);
|
|
3182
|
+
const stated = auth ? AUTH_FAILURE_COOLDOWN_MS : statedCooldown(haystack) ?? retryAfterMs(info.error.data?.responseHeaders);
|
|
3183
|
+
const reason = auth ? AUTH_FAILURE_REASON : limitReason(haystack);
|
|
2815
3184
|
const strike = recordStrike(s, model, reason ?? "rate-limited", stated);
|
|
2816
3185
|
if (strike) await log("warn", `cooling ${model} for ${Math.round((strike.until - Date.now()) / 1e3)}s (${reason ?? "rate-limited"}, strike ${strike.strikes})`);
|
|
2817
3186
|
await failOver(info.sessionID, model, reason ?? "rate-limited", { stated, cool: !ownMsg });
|
|
@@ -2838,6 +3207,7 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2838
3207
|
}
|
|
2839
3208
|
try {
|
|
2840
3209
|
await reapStuck();
|
|
3210
|
+
await refreshProviders();
|
|
2841
3211
|
refreshCatalog(s, serverUrl);
|
|
2842
3212
|
const snap = snapshot(s);
|
|
2843
3213
|
if (BASES.includes(requested)) {
|
|
@@ -2852,7 +3222,7 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2852
3222
|
weight: slot.weight,
|
|
2853
3223
|
model: slot.model,
|
|
2854
3224
|
ceiling: slotCeiling(slot),
|
|
2855
|
-
load: (snap.counts.get(slot.
|
|
3225
|
+
load: (snap.counts.get(slot.index) ?? 0) + 1
|
|
2856
3226
|
});
|
|
2857
3227
|
output.args.subagent_type = variant;
|
|
2858
3228
|
acquire(s, requested, slot, input.sessionID, claimKey(input.sessionID, input.callID));
|
|
@@ -2869,9 +3239,21 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2869
3239
|
await log("error", `routing failed, spawn unchanged: ${String(error)}`);
|
|
2870
3240
|
}
|
|
2871
3241
|
},
|
|
2872
|
-
"tool.execute.after": async (input) => {
|
|
3242
|
+
"tool.execute.after": async (input, output) => {
|
|
2873
3243
|
if (input.tool !== "task") return;
|
|
2874
|
-
|
|
3244
|
+
const key = claimKey(input.sessionID, input.callID);
|
|
3245
|
+
release(s, key);
|
|
3246
|
+
const report = abortReports.get(key);
|
|
3247
|
+
if (!report) return;
|
|
3248
|
+
abortReports.delete(key);
|
|
3249
|
+
const detail = [
|
|
3250
|
+
`Task Cancelled by LaCode: ${report.message}`,
|
|
3251
|
+
`slot ${report.slot}; model ${report.model}; elapsed ${report.elapsedMs}ms; failure ${report.kind}; claim ${report.released ? "released" : "held"}`
|
|
3252
|
+
].join("\n");
|
|
3253
|
+
const text = typeof output.output === "string" ? output.output : "";
|
|
3254
|
+
output.output = /task\s+cancel+ed/i.test(text) ? detail : text ? `${text}
|
|
3255
|
+
|
|
3256
|
+
${detail}` : detail;
|
|
2875
3257
|
},
|
|
2876
3258
|
tool: {
|
|
2877
3259
|
pool_recheck: tool({
|
|
@@ -2880,6 +3262,7 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2880
3262
|
models: tool.schema.array(tool.schema.string()).optional().describe("Model ids to validate, as provider/model-id. Omit to probe only the models that are cooling.")
|
|
2881
3263
|
},
|
|
2882
3264
|
async execute(args) {
|
|
3265
|
+
await refreshProviders(true);
|
|
2883
3266
|
const models = args?.models;
|
|
2884
3267
|
const report = models && models.length > 0 ? await runValidation(models) : await runProbe();
|
|
2885
3268
|
return { title: `agent-pool: ${reportTitle(report)}`, output: reportText(report) };
|
|
@@ -2889,28 +3272,30 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2889
3272
|
description: "Show live pool load across every opencode process, per free model and per agent type, each slot's load against its ceiling, the slot marked <- next that the next spawn would take, and which models are cooling after a rate limit or a stuck task; COOLING/UNAVAILABLE = not routable. Check it before launching many parallel subagents.",
|
|
2890
3273
|
args: {},
|
|
2891
3274
|
async execute() {
|
|
3275
|
+
await refreshProviders(true);
|
|
2892
3276
|
refreshCatalog(s, serverUrl);
|
|
2893
3277
|
const slots = ensureSlots(s, (m) => void warnOnce(m));
|
|
2894
3278
|
const snap = snapshot(s);
|
|
2895
3279
|
const pass = poolPass(s, snap);
|
|
2896
3280
|
const now = Date.now();
|
|
2897
|
-
const
|
|
2898
|
-
const add = (
|
|
2899
|
-
const entry =
|
|
3281
|
+
const bySlot = /* @__PURE__ */ new Map();
|
|
3282
|
+
const add = (k, base) => {
|
|
3283
|
+
const entry = bySlot.get(k) ?? { total: 0, bases: /* @__PURE__ */ new Map() };
|
|
2900
3284
|
entry.total++;
|
|
2901
3285
|
entry.bases.set(base, (entry.bases.get(base) ?? 0) + 1);
|
|
2902
|
-
|
|
3286
|
+
bySlot.set(k, entry);
|
|
2903
3287
|
};
|
|
2904
|
-
for (const claim of snap.own.values()) add(claim.
|
|
2905
|
-
for (const sibling of snap.siblings) for (const claim of sibling.claims) add(claim.
|
|
3288
|
+
for (const claim of snap.own.values()) add(claim.k, claim.v);
|
|
3289
|
+
for (const sibling of snap.siblings) for (const claim of sibling.claims) add(claim.k, claim.v);
|
|
2906
3290
|
const next = pickSlot(s, snap);
|
|
2907
3291
|
const line = (slot) => {
|
|
2908
|
-
const entry =
|
|
3292
|
+
const entry = bySlot.get(slot.index);
|
|
2909
3293
|
const users = entry && entry.bases.size > 0 ? [...entry.bases].map(([b, n]) => `${b}x${n}`).join(" ") : "idle";
|
|
2910
3294
|
const strike = snap.cooling.get(slot.model);
|
|
2911
3295
|
const status = s.catalog.get(slot.model);
|
|
2912
3296
|
const notes = [
|
|
2913
3297
|
strike ? `UNAVAILABLE (COOLING ${Math.ceil((strike.until - now) / 1e3)}s, ${strike.reason}, strike ${strike.strikes})` : "",
|
|
3298
|
+
!providerReady(s, slot.model) ? `UNAVAILABLE (provider-unconfigured: ${providerOf(slot.model)}; run \`opencode auth login ${providerOf(slot.model)}\`)` : "",
|
|
2914
3299
|
status && status !== "active" && status !== "unknown" ? `catalog:${status}` : ""
|
|
2915
3300
|
].filter(Boolean);
|
|
2916
3301
|
const marker = slot.index === next.index ? " <- next" : "";
|
|
@@ -2920,14 +3305,25 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2920
3305
|
};
|
|
2921
3306
|
const total = [...snap.counts.values()].reduce((sum, n) => sum + n, 0);
|
|
2922
3307
|
const coolingNow = [...snap.cooling.entries()].filter(([, st]) => st.until > now);
|
|
3308
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3309
|
+
for (const slot of slots) {
|
|
3310
|
+
const name = slotBucket(slot);
|
|
3311
|
+
const entry = buckets.get(name) ?? { seats: 0, slots: 0, cooling: false };
|
|
3312
|
+
entry.seats += slot.weight;
|
|
3313
|
+
entry.slots++;
|
|
3314
|
+
if (snap.cooling.has(slot.model)) entry.cooling = true;
|
|
3315
|
+
buckets.set(name, entry);
|
|
3316
|
+
}
|
|
3317
|
+
const sharedBuckets = [...buckets].filter(([, entry]) => entry.slots > 1);
|
|
2923
3318
|
const verdicts = [...snap.own.values()].map((claim) => condemnReason(s, claim, now));
|
|
2924
3319
|
const hung = verdicts.filter((verdict) => verdict === "hung").length;
|
|
2925
3320
|
const overran = verdicts.filter((verdict) => verdict === "overran").length;
|
|
2926
3321
|
return {
|
|
2927
3322
|
title: `agent-pool: ${total} task(s) in flight, ${coolingNow.length} model(s) cooling`,
|
|
2928
3323
|
output: [
|
|
2929
|
-
`shared pool over ${slots.length}
|
|
2930
|
-
|
|
3324
|
+
`shared pool over ${slots.length} slot(s) from ${s.origin}; state ${pass}; bases: ${BASES.join(", ")}
|
|
3325
|
+
one ordered pass in slot order: each slot fills to its weight, then overflow goes to the least-loaded slot. Capacity is the sum of the weights; repeat a model on another line to give it more (FULL = at weight).`,
|
|
3326
|
+
...sharedBuckets.length ? [`buckets: ${sharedBuckets.map(([name, entry]) => `${name} ${entry.seats} seat(s)${entry.cooling ? " COOLING" : ""}`).join("; ")}`] : [],
|
|
2931
3327
|
...slots.map(line),
|
|
2932
3328
|
"",
|
|
2933
3329
|
"by process:",
|
|
@@ -3780,7 +4176,7 @@ var retireSession = async (client, directory, sessionID, mode, log) => {
|
|
|
3780
4176
|
const gone = async () => await client.session.get({ path: { id: sessionID }, query: { directory } }).then((r) => r.data).catch(() => void 0) === void 0;
|
|
3781
4177
|
await client.session.delete({ path: { id: sessionID }, query: { directory } }).catch(() => void 0);
|
|
3782
4178
|
if (await gone()) return;
|
|
3783
|
-
await new Promise((
|
|
4179
|
+
await new Promise((resolve5) => setTimeout(resolve5, 500));
|
|
3784
4180
|
await client.session.delete({ path: { id: sessionID }, query: { directory } }).catch(() => void 0);
|
|
3785
4181
|
if (await gone()) return;
|
|
3786
4182
|
await log?.("warn", "the cartographer's session could not be removed from the session list; it stays as an empty entry", {
|
|
@@ -4439,7 +4835,15 @@ var INTERNAL_TITLE_PREFIXES = [
|
|
|
4439
4835
|
// context-autoupdate spawns the cartographer through the session API, with no
|
|
4440
4836
|
// parentID, so it looks exactly like a root session going idle. Notifying for
|
|
4441
4837
|
// it would announce the cartographer's own bookkeeping as a finished task.
|
|
4442
|
-
"context (auto"
|
|
4838
|
+
"context (auto",
|
|
4839
|
+
// timeline spawns its summarizer through the session API with the same shape,
|
|
4840
|
+
// so it would otherwise announce the summarizer's own bookkeeping as a task.
|
|
4841
|
+
"timeline (auto",
|
|
4842
|
+
// pool probe sessions are created as root sessions with no parentID and the
|
|
4843
|
+
// title `pool probe (<model>)` (`src/lib/probe.ts`), so notification for
|
|
4844
|
+
// their idle/error would announce Lacode's own health-check as finished user
|
|
4845
|
+
// work.
|
|
4846
|
+
"pool probe"
|
|
4443
4847
|
];
|
|
4444
4848
|
var NOTIFY_OUTCOMES = ["done", "question", "permission", "error"];
|
|
4445
4849
|
var notifyMessage = (outcome, detail) => {
|
|
@@ -4927,10 +5331,10 @@ var notifyHooks = (async ({ client, directory }, options, deps = {}) => {
|
|
|
4927
5331
|
const info = await client.session.get({ path: { id: sessionID }, query: { directory } }).then((result) => result.data).catch(() => void 0);
|
|
4928
5332
|
return info;
|
|
4929
5333
|
};
|
|
4930
|
-
const deliverViaHelper = (title, message, urgency, sound, requestID, sessionID) => new Promise((
|
|
5334
|
+
const deliverViaHelper = (title, message, urgency, sound, requestID, sessionID) => new Promise((resolve5) => {
|
|
4931
5335
|
if (inflight >= maxInflight) {
|
|
4932
5336
|
void log("warn", `notification dropped: ${inflight} helpers already in flight`, { title });
|
|
4933
|
-
|
|
5337
|
+
resolve5({ ok: false, detail: `too many notification helpers in flight (${inflight})` });
|
|
4934
5338
|
return;
|
|
4935
5339
|
}
|
|
4936
5340
|
const helper = findHelper();
|
|
@@ -4939,7 +5343,7 @@ var notifyHooks = (async ({ client, directory }, options, deps = {}) => {
|
|
|
4939
5343
|
warnedHelper = true;
|
|
4940
5344
|
void log("warn", "no oc-notify helper found; run 'bin/oc-sync' to install one");
|
|
4941
5345
|
}
|
|
4942
|
-
|
|
5346
|
+
resolve5({ ok: false, detail: "no oc-notify helper found" });
|
|
4943
5347
|
return;
|
|
4944
5348
|
}
|
|
4945
5349
|
const childEnv = { ...process.env, ...sessionID ? { [NOTIFY_SESSION_ENV]: sessionID } : {} };
|
|
@@ -4962,7 +5366,7 @@ var notifyHooks = (async ({ client, directory }, options, deps = {}) => {
|
|
|
4962
5366
|
settled = true;
|
|
4963
5367
|
inflight--;
|
|
4964
5368
|
if (timer) clearTimeout(timer);
|
|
4965
|
-
|
|
5369
|
+
resolve5(result);
|
|
4966
5370
|
};
|
|
4967
5371
|
timer = setTimeout(() => {
|
|
4968
5372
|
try {
|
|
@@ -5751,6 +6155,753 @@ async function selfUpdateHooks(input, options) {
|
|
|
5751
6155
|
return {};
|
|
5752
6156
|
}
|
|
5753
6157
|
|
|
6158
|
+
// src/plugins/timeline.ts
|
|
6159
|
+
import { writeFileSync as writeFileSync8, renameSync as renameSync6, rmSync as rmSync3 } from "node:fs";
|
|
6160
|
+
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
6161
|
+
|
|
6162
|
+
// src/lib/timeline.ts
|
|
6163
|
+
import { appendFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync10, rmSync as rmSync2 } from "node:fs";
|
|
6164
|
+
import { homedir as homedir7 } from "node:os";
|
|
6165
|
+
import { dirname as dirname5, join as join11, resolve as resolve4 } from "node:path";
|
|
6166
|
+
function timelineDir() {
|
|
6167
|
+
const env = process.env.LACODE_TIMELINE_DIR;
|
|
6168
|
+
if (env) return resolve4(env);
|
|
6169
|
+
return join11(homedir7(), ".local/share/opencode/lacode/timeline");
|
|
6170
|
+
}
|
|
6171
|
+
function timelineFile(sessionID) {
|
|
6172
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(sessionID)) throw new Error("invalid sessionID");
|
|
6173
|
+
return join11(timelineDir(), sessionID + ".jsonl");
|
|
6174
|
+
}
|
|
6175
|
+
function normalizeThrough(through) {
|
|
6176
|
+
if (!through || typeof through !== "object") return void 0;
|
|
6177
|
+
const out = {};
|
|
6178
|
+
if (typeof through.messageID === "string") out.messageID = through.messageID;
|
|
6179
|
+
if (typeof through.time === "number") out.time = through.time;
|
|
6180
|
+
if (out.messageID === void 0 && out.time === void 0) return void 0;
|
|
6181
|
+
return out;
|
|
6182
|
+
}
|
|
6183
|
+
function appendEntriesAt(file, texts, now, through) {
|
|
6184
|
+
mkdirSync7(dirname5(file), { recursive: true });
|
|
6185
|
+
const cleaned = [];
|
|
6186
|
+
for (const t of texts) {
|
|
6187
|
+
if (typeof t !== "string" || t.length === 0) continue;
|
|
6188
|
+
cleaned.push(t.slice(0, 320));
|
|
6189
|
+
}
|
|
6190
|
+
let recent = [];
|
|
6191
|
+
try {
|
|
6192
|
+
recent = readEntriesAt(file, 20);
|
|
6193
|
+
} catch {
|
|
6194
|
+
recent = [];
|
|
6195
|
+
}
|
|
6196
|
+
const recentSet = new Set(recent.map((e) => e.text.toLowerCase()));
|
|
6197
|
+
const batchSeen = /* @__PURE__ */ new Set();
|
|
6198
|
+
const toAppend = [];
|
|
6199
|
+
for (const t of cleaned) {
|
|
6200
|
+
const key = t.toLowerCase();
|
|
6201
|
+
if (recentSet.has(key)) continue;
|
|
6202
|
+
if (batchSeen.has(key)) continue;
|
|
6203
|
+
batchSeen.add(key);
|
|
6204
|
+
toAppend.push(t);
|
|
6205
|
+
}
|
|
6206
|
+
const validThrough = normalizeThrough(through);
|
|
6207
|
+
let count = 0;
|
|
6208
|
+
for (const t of toAppend) {
|
|
6209
|
+
const ts = now ?? Date.now();
|
|
6210
|
+
const line = { ts, text: t };
|
|
6211
|
+
if (validThrough) line.through = validThrough;
|
|
6212
|
+
appendFileSync(file, JSON.stringify(line) + "\n");
|
|
6213
|
+
count++;
|
|
6214
|
+
}
|
|
6215
|
+
return count;
|
|
6216
|
+
}
|
|
6217
|
+
function readEntriesAt(file, limit) {
|
|
6218
|
+
let raw = "";
|
|
6219
|
+
try {
|
|
6220
|
+
raw = readFileSync10(file, "utf8");
|
|
6221
|
+
} catch {
|
|
6222
|
+
return [];
|
|
6223
|
+
}
|
|
6224
|
+
const entries = [];
|
|
6225
|
+
for (const line of raw.split("\n")) {
|
|
6226
|
+
if (line.trim() === "") continue;
|
|
6227
|
+
try {
|
|
6228
|
+
const obj = JSON.parse(line);
|
|
6229
|
+
if (obj && typeof obj.ts === "number" && typeof obj.text === "string") {
|
|
6230
|
+
const entry = { ts: obj.ts, text: obj.text };
|
|
6231
|
+
const t = normalizeThrough(obj.through);
|
|
6232
|
+
if (t) entry.through = t;
|
|
6233
|
+
entries.push(entry);
|
|
6234
|
+
}
|
|
6235
|
+
} catch {
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6238
|
+
if (limit != null && limit <= 0) return [];
|
|
6239
|
+
const lim = limit == null ? 50 : limit;
|
|
6240
|
+
return entries.slice(-lim);
|
|
6241
|
+
}
|
|
6242
|
+
function appendEntries(sessionID, texts, now, through) {
|
|
6243
|
+
return appendEntriesAt(timelineFile(sessionID), texts, now, through);
|
|
6244
|
+
}
|
|
6245
|
+
function readEntries(sessionID, limit) {
|
|
6246
|
+
return readEntriesAt(timelineFile(sessionID), limit);
|
|
6247
|
+
}
|
|
6248
|
+
function removeEntries(sessionID) {
|
|
6249
|
+
try {
|
|
6250
|
+
rmSync2(timelineFile(sessionID), { force: true });
|
|
6251
|
+
} catch {
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
function collapse(s) {
|
|
6255
|
+
return s.replace(/\s+/g, " ").trim();
|
|
6256
|
+
}
|
|
6257
|
+
function trunc(s, n) {
|
|
6258
|
+
return s.length > n ? s.slice(0, n) : s;
|
|
6259
|
+
}
|
|
6260
|
+
function stripAnsiAndControls(s) {
|
|
6261
|
+
let out = "";
|
|
6262
|
+
for (let i = 0; i < s.length; i++) {
|
|
6263
|
+
const code = s.charCodeAt(i);
|
|
6264
|
+
if (code === 27) {
|
|
6265
|
+
if (i + 1 < s.length && s[i + 1] === "[") {
|
|
6266
|
+
i += 2;
|
|
6267
|
+
while (i < s.length) {
|
|
6268
|
+
const c = s.charCodeAt(i);
|
|
6269
|
+
if (c >= 64 && c <= 126) {
|
|
6270
|
+
break;
|
|
6271
|
+
}
|
|
6272
|
+
i++;
|
|
6273
|
+
}
|
|
6274
|
+
continue;
|
|
6275
|
+
}
|
|
6276
|
+
if (i + 1 < s.length && s[i + 1] === "]") {
|
|
6277
|
+
i += 2;
|
|
6278
|
+
while (i < s.length) {
|
|
6279
|
+
const c = s.charCodeAt(i);
|
|
6280
|
+
if (c === 7) break;
|
|
6281
|
+
if (c === 27 && i + 1 < s.length && s[i + 1] === "\\") {
|
|
6282
|
+
i += 1;
|
|
6283
|
+
break;
|
|
6284
|
+
}
|
|
6285
|
+
i++;
|
|
6286
|
+
}
|
|
6287
|
+
continue;
|
|
6288
|
+
}
|
|
6289
|
+
if (i + 1 < s.length) {
|
|
6290
|
+
i += 1;
|
|
6291
|
+
}
|
|
6292
|
+
continue;
|
|
6293
|
+
}
|
|
6294
|
+
if (code === 9 || code === 10) {
|
|
6295
|
+
out += s[i];
|
|
6296
|
+
continue;
|
|
6297
|
+
}
|
|
6298
|
+
if (code < 32 || code >= 127 && code <= 159) {
|
|
6299
|
+
continue;
|
|
6300
|
+
}
|
|
6301
|
+
out += s[i];
|
|
6302
|
+
}
|
|
6303
|
+
return out;
|
|
6304
|
+
}
|
|
6305
|
+
function inputSummary(input) {
|
|
6306
|
+
if (input === null || input === void 0) return "";
|
|
6307
|
+
if (typeof input !== "object") {
|
|
6308
|
+
try {
|
|
6309
|
+
return trunc(collapse(JSON.stringify(input)), 160);
|
|
6310
|
+
} catch {
|
|
6311
|
+
return "";
|
|
6312
|
+
}
|
|
6313
|
+
}
|
|
6314
|
+
const rec = input;
|
|
6315
|
+
const keys = ["filePath", "path", "command", "description", "pattern", "query", "url", "name", "title"];
|
|
6316
|
+
for (const k of keys) {
|
|
6317
|
+
if (Object.prototype.hasOwnProperty.call(rec, k) && rec[k] != null) {
|
|
6318
|
+
const v = rec[k];
|
|
6319
|
+
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
6320
|
+
return trunc(collapse(s), 160);
|
|
6321
|
+
}
|
|
6322
|
+
}
|
|
6323
|
+
try {
|
|
6324
|
+
return trunc(collapse(JSON.stringify(rec)), 160);
|
|
6325
|
+
} catch {
|
|
6326
|
+
return "";
|
|
6327
|
+
}
|
|
6328
|
+
}
|
|
6329
|
+
function summarizeToolResult(state2) {
|
|
6330
|
+
if (!state2) return "";
|
|
6331
|
+
const err = state2.error;
|
|
6332
|
+
if (typeof err === "string") {
|
|
6333
|
+
if (err.trim() === "") return "";
|
|
6334
|
+
return `error: ${trunc(err, 180)}`;
|
|
6335
|
+
}
|
|
6336
|
+
if (err && typeof err === "object") {
|
|
6337
|
+
try {
|
|
6338
|
+
const s = JSON.stringify(err);
|
|
6339
|
+
if (typeof s === "string" && s.trim() !== "") {
|
|
6340
|
+
return trunc(s, 180);
|
|
6341
|
+
}
|
|
6342
|
+
} catch {
|
|
6343
|
+
}
|
|
6344
|
+
return "";
|
|
6345
|
+
}
|
|
6346
|
+
if (typeof state2.output === "string") {
|
|
6347
|
+
for (const raw of state2.output.split("\n")) {
|
|
6348
|
+
const line = raw.trim();
|
|
6349
|
+
if (line.length > 0) {
|
|
6350
|
+
return trunc(line, 180);
|
|
6351
|
+
}
|
|
6352
|
+
}
|
|
6353
|
+
}
|
|
6354
|
+
if (typeof state2.title === "string" && state2.title.trim() !== "") {
|
|
6355
|
+
return trunc(state2.title, 180);
|
|
6356
|
+
}
|
|
6357
|
+
return "";
|
|
6358
|
+
}
|
|
6359
|
+
function renderMessage(m) {
|
|
6360
|
+
const role = m.info.role;
|
|
6361
|
+
const lines = [];
|
|
6362
|
+
if (role === "user") {
|
|
6363
|
+
for (const p of m.parts) {
|
|
6364
|
+
if (p.type === "text" && !p.synthetic && !p.ignored) {
|
|
6365
|
+
if ((p.text ?? "").includes("timeline_reprocess")) continue;
|
|
6366
|
+
const text = collapse(p.text ?? "");
|
|
6367
|
+
lines.push(`[user] ${trunc(text, 2e3)}`);
|
|
6368
|
+
}
|
|
6369
|
+
}
|
|
6370
|
+
} else if (role === "assistant") {
|
|
6371
|
+
for (const p of m.parts) {
|
|
6372
|
+
if (p.type === "text") {
|
|
6373
|
+
const text = collapse(p.text ?? "");
|
|
6374
|
+
lines.push(`[assistant] ${trunc(text, 1500)}`);
|
|
6375
|
+
} else if (p.type === "tool") {
|
|
6376
|
+
const tool3 = p.tool ?? "";
|
|
6377
|
+
if (tool3 === "timeline_reprocess") continue;
|
|
6378
|
+
const status = p.state?.status;
|
|
6379
|
+
const summary = inputSummary(p.state?.input);
|
|
6380
|
+
const result = summarizeToolResult(p.state);
|
|
6381
|
+
let line = `[tool] ${tool3}`;
|
|
6382
|
+
if (status) line += ` (${status})`;
|
|
6383
|
+
const inputPart = summary ? trunc(summary, 220) : "";
|
|
6384
|
+
let rest = "";
|
|
6385
|
+
if (inputPart) rest += ` ${inputPart}`;
|
|
6386
|
+
if (result) rest += ` -> ${result}`;
|
|
6387
|
+
line += rest;
|
|
6388
|
+
lines.push(line);
|
|
6389
|
+
}
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6392
|
+
return lines;
|
|
6393
|
+
}
|
|
6394
|
+
function buildExcerpt(messages, cursor, budget = 8e3) {
|
|
6395
|
+
let start = 0;
|
|
6396
|
+
if (cursor?.messageID) {
|
|
6397
|
+
const idx = messages.findIndex((m) => m.info.id === cursor.messageID);
|
|
6398
|
+
if (idx >= 0) {
|
|
6399
|
+
start = idx + 1;
|
|
6400
|
+
} else if (cursor.time != null) {
|
|
6401
|
+
const i = messages.findIndex((m) => (m.info.time?.created ?? -Infinity) > cursor.time);
|
|
6402
|
+
start = i < 0 ? messages.length : i;
|
|
6403
|
+
}
|
|
6404
|
+
} else if (cursor?.time != null) {
|
|
6405
|
+
const i = messages.findIndex((m) => (m.info.time?.created ?? -Infinity) > cursor.time);
|
|
6406
|
+
start = i < 0 ? messages.length : i;
|
|
6407
|
+
}
|
|
6408
|
+
const outLines = [];
|
|
6409
|
+
const included = [];
|
|
6410
|
+
for (let i = start; i < messages.length; i++) {
|
|
6411
|
+
const m = messages[i];
|
|
6412
|
+
const msgLines = renderMessage(m);
|
|
6413
|
+
if (msgLines.length === 0) continue;
|
|
6414
|
+
const candidateLines = outLines.length === 0 ? [...msgLines] : [...outLines, ...msgLines];
|
|
6415
|
+
const candidateText = candidateLines.join("\n");
|
|
6416
|
+
if (included.length > 0 && candidateText.length > budget) {
|
|
6417
|
+
break;
|
|
6418
|
+
}
|
|
6419
|
+
if (included.length === 0 && candidateText.length > budget) {
|
|
6420
|
+
const block = msgLines.join("\n");
|
|
6421
|
+
outLines.push(block.slice(0, budget));
|
|
6422
|
+
included.push(m);
|
|
6423
|
+
break;
|
|
6424
|
+
}
|
|
6425
|
+
outLines.push(...msgLines);
|
|
6426
|
+
included.push(m);
|
|
6427
|
+
}
|
|
6428
|
+
let model;
|
|
6429
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
6430
|
+
const info = messages[i].info;
|
|
6431
|
+
if (info.role !== "user") continue;
|
|
6432
|
+
if (info.model) {
|
|
6433
|
+
model = { providerID: info.model.providerID, modelID: info.model.modelID };
|
|
6434
|
+
break;
|
|
6435
|
+
}
|
|
6436
|
+
if (info.providerID && info.modelID) {
|
|
6437
|
+
model = { providerID: info.providerID, modelID: info.modelID };
|
|
6438
|
+
break;
|
|
6439
|
+
}
|
|
6440
|
+
}
|
|
6441
|
+
const lastIncluded = included[included.length - 1];
|
|
6442
|
+
const next = lastIncluded ? { messageID: lastIncluded.info.id, time: lastIncluded.info.time?.created } : void 0;
|
|
6443
|
+
return {
|
|
6444
|
+
text: outLines.join("\n"),
|
|
6445
|
+
next,
|
|
6446
|
+
count: included.length,
|
|
6447
|
+
model
|
|
6448
|
+
};
|
|
6449
|
+
}
|
|
6450
|
+
function parseBullets(text, max = 6) {
|
|
6451
|
+
const result = [];
|
|
6452
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6453
|
+
for (const raw of text.split("\n")) {
|
|
6454
|
+
const line = raw.trim();
|
|
6455
|
+
if (!line) continue;
|
|
6456
|
+
if (line.startsWith("```")) continue;
|
|
6457
|
+
if (line.length > 320) continue;
|
|
6458
|
+
const m = line.match(/^([-*•]|\d+[.)])\s+(.*)$/);
|
|
6459
|
+
if (!m) continue;
|
|
6460
|
+
const stripped = m[2].trim().replace(/`/g, "").replace(/\*\*/g, "").trim();
|
|
6461
|
+
if (!stripped) continue;
|
|
6462
|
+
const cleaned = stripAnsiAndControls(stripped).trim();
|
|
6463
|
+
if (!cleaned) continue;
|
|
6464
|
+
if (cleaned.length > 320) continue;
|
|
6465
|
+
const key = cleaned.toLowerCase();
|
|
6466
|
+
if (seen.has(key)) continue;
|
|
6467
|
+
seen.add(key);
|
|
6468
|
+
result.push(cleaned);
|
|
6469
|
+
if (result.length >= max) break;
|
|
6470
|
+
}
|
|
6471
|
+
return result;
|
|
6472
|
+
}
|
|
6473
|
+
function isInternalTitle(title) {
|
|
6474
|
+
const t = (title ?? "").trim();
|
|
6475
|
+
return t.startsWith("context (auto") || t.startsWith("timeline (auto") || t.startsWith("pool probe");
|
|
6476
|
+
}
|
|
6477
|
+
|
|
6478
|
+
// src/plugins/timeline.ts
|
|
6479
|
+
var DEFAULT_INTERVAL_MS = 3e5;
|
|
6480
|
+
var MIN_INTERVAL_MS = 1e4;
|
|
6481
|
+
var DEFAULT_MAX_ENTRIES = 8;
|
|
6482
|
+
var MAX_ENTRIES = 10;
|
|
6483
|
+
var PROMPT_TIMEOUT_MS = 9e4;
|
|
6484
|
+
var SYSTEM = `You are the session timeline summarizer. You receive a slice of a coding session transcript.
|
|
6485
|
+
Write 1-8 timeline entries describing the work as tasks, features, fixes, or stages.
|
|
6486
|
+
|
|
6487
|
+
Rules:
|
|
6488
|
+
- One entry per line, each starting with "- ".
|
|
6489
|
+
- Each entry is 6-16 words, past tense, plain text.
|
|
6490
|
+
- Cover each distinct task in the slice. Do not drop a provider, feature, fix, config, docs, or test change; merge only exact duplicates.
|
|
6491
|
+
- Describe the TASK or OUTCOME, not the implementation. "Pool: added bucket-based routing" is right; "added b:<name> to pool.ts and agent-pool.ts" is too detailed.
|
|
6492
|
+
- Never describe the mechanism: no flags, aliases, overrides, endpoints, probes, config keys, declarations, tags, or file changes. State what changed for the system: which capability now works, which bug is fixed, which behavior changed.
|
|
6493
|
+
- Never use these words in an entry: tag, alias, override, flag, probe, endpoint, config key, catalog, file, line, count.
|
|
6494
|
+
- For configuration plumbing, write the purpose: "Config: silenced warnings for unused providers", not "declared empty overrides".
|
|
6495
|
+
- Use a short area prefix when it clarifies: "Pool:", "Providers:", "Config:", "Docs:", "Tests:", "Fix:", "Feature:".
|
|
6496
|
+
- Name the capability, behavior, feature, bug, or decision. Do not name files, paths, functions, commands, environment variables, API keys, raw model IDs, test file names, or build artifacts. Provider and product names are allowed when they identify the task.
|
|
6497
|
+
- Do not include counts of slots, providers, tests, lines, or files.
|
|
6498
|
+
- Omit internal housekeeping: builds, dist rebuilds, test-file additions, formatting, and dependency churn. If the slice only contains housekeeping, output nothing.
|
|
6499
|
+
- Describe what was DONE or DECIDED. Never write "User asked...", "Assistant said...", "Discussed...", "Reviewed...", "Explained...", or "Considered...".
|
|
6500
|
+
- Merge related changes into one stage; do not repeat the same task in different words.
|
|
6501
|
+
- No markdown: no bold, italics, backticks, or code blocks.
|
|
6502
|
+
- If a technical detail is the essence of the task, name it at the capability level: "Added Cloudflare Workers AI as a provider", not its account-id variables.
|
|
6503
|
+
- If the slice contains no substantive work or decision, output nothing.
|
|
6504
|
+
- Output only the entries. No headings, no preamble, no closing sentence.
|
|
6505
|
+
|
|
6506
|
+
Example output:
|
|
6507
|
+
- Pool: added bucket-based routing for model groups.
|
|
6508
|
+
- Providers: added Gemini 3.5 Flash after the previous model timed out.
|
|
6509
|
+
- Providers: added Cloudflare Workers AI with a tool-compatible model.
|
|
6510
|
+
- Config: silenced warnings for unused providers.
|
|
6511
|
+
- Config: standardized provider key naming and added Groq and Google.
|
|
6512
|
+
- Docs: aligned the routing documentation with the implemented behavior.
|
|
6513
|
+
|
|
6514
|
+
Not this:
|
|
6515
|
+
- Config: declared empty OpenRouter overrides to silence warnings.
|
|
6516
|
+
- Providers: added a Google API key alias after Gemini 2.5-flash closure.
|
|
6517
|
+
- Config: standardized provider keys to _KEY-only naming.
|
|
6518
|
+
- Pool: reordered slots after tagging pool entries.
|
|
6519
|
+
- Pool: added bucket support and kept tags in pool.
|
|
6520
|
+
- Pool: standardized slots on opencode catalog.
|
|
6521
|
+
- Providers: added Gemini 3.5 Flash and a Google API key alias.
|
|
6522
|
+
|
|
6523
|
+
This:
|
|
6524
|
+
- Config: silenced warnings for unused providers.
|
|
6525
|
+
- Providers: replaced the retired Gemini 2.5 Flash with an available model.
|
|
6526
|
+
- Config: standardized provider key naming.
|
|
6527
|
+
- Pool: moved shared-window models to the end.
|
|
6528
|
+
- Pool: added named bucket support for model groups.
|
|
6529
|
+
- Pool: moved shared-window models to the end.
|
|
6530
|
+
- Providers: replaced the retired Gemini 2.5 Flash with Gemini 3.5 Flash.
|
|
6531
|
+
|
|
6532
|
+
Your entire response must consist only of lines beginning with "- ".`;
|
|
6533
|
+
function timelineHooks(input, options) {
|
|
6534
|
+
const { client, directory } = input;
|
|
6535
|
+
const opts = options ?? {};
|
|
6536
|
+
const config = typeof opts.timeline === "object" && opts.timeline !== null ? opts.timeline : void 0;
|
|
6537
|
+
const disabled = opts.timeline === false || config?.enabled === false || process.env.LACODE_TIMELINE === "0";
|
|
6538
|
+
const SERVICE5 = "timeline";
|
|
6539
|
+
const poolState = state(options);
|
|
6540
|
+
const log = (level, message, extra) => client.app.log({ body: { service: SERVICE5, level, message, extra } }).catch(() => {
|
|
6541
|
+
});
|
|
6542
|
+
const debug = (...args) => {
|
|
6543
|
+
if (process.env.LACODE_TIMELINE_DEBUG === "1") console.error("[timeline]", ...args);
|
|
6544
|
+
};
|
|
6545
|
+
const rawInterval = Number(process.env.LACODE_TIMELINE_INTERVAL_MS ?? config?.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
6546
|
+
const intervalMs = Number.isFinite(rawInterval) ? Math.max(MIN_INTERVAL_MS, rawInterval) : DEFAULT_INTERVAL_MS;
|
|
6547
|
+
const rawMaxEntries = Number(config?.maxEntries ?? DEFAULT_MAX_ENTRIES);
|
|
6548
|
+
const maxEntries = Number.isFinite(rawMaxEntries) ? Math.min(MAX_ENTRIES, Math.max(1, Math.trunc(rawMaxEntries))) : DEFAULT_MAX_ENTRIES;
|
|
6549
|
+
const shared = globalThis[/* @__PURE__ */ Symbol.for("lacode:timeline:state")] ??= {
|
|
6550
|
+
sessions: /* @__PURE__ */ new Map(),
|
|
6551
|
+
owned: /* @__PURE__ */ new Set(),
|
|
6552
|
+
reprocessing: /* @__PURE__ */ new Set()
|
|
6553
|
+
};
|
|
6554
|
+
const sessions = shared.sessions;
|
|
6555
|
+
const owned = shared.owned;
|
|
6556
|
+
const reprocessing = shared.reprocessing;
|
|
6557
|
+
const MAX_REPROCESS_SLICES = 100;
|
|
6558
|
+
let disposed = false;
|
|
6559
|
+
const promptModel = (raw) => {
|
|
6560
|
+
if (typeof raw !== "string" || !raw.includes("/")) return void 0;
|
|
6561
|
+
const { provider, id } = splitModel(raw);
|
|
6562
|
+
if (!id) return void 0;
|
|
6563
|
+
return { providerID: provider, modelID: id };
|
|
6564
|
+
};
|
|
6565
|
+
const stateFor = (id) => {
|
|
6566
|
+
let state2 = sessions.get(id);
|
|
6567
|
+
if (!state2) {
|
|
6568
|
+
state2 = {
|
|
6569
|
+
running: false,
|
|
6570
|
+
pending: false,
|
|
6571
|
+
failures: 0,
|
|
6572
|
+
cursor: readEntries(id, 1)[0]?.through
|
|
6573
|
+
};
|
|
6574
|
+
sessions.set(id, state2);
|
|
6575
|
+
while (sessions.size > 500) {
|
|
6576
|
+
let evicted = false;
|
|
6577
|
+
for (const [key, st] of sessions) {
|
|
6578
|
+
if (key === id) continue;
|
|
6579
|
+
if (!st.timer && !st.running) {
|
|
6580
|
+
sessions.delete(key);
|
|
6581
|
+
evicted = true;
|
|
6582
|
+
break;
|
|
6583
|
+
}
|
|
6584
|
+
}
|
|
6585
|
+
if (!evicted) break;
|
|
6586
|
+
}
|
|
6587
|
+
}
|
|
6588
|
+
return state2;
|
|
6589
|
+
};
|
|
6590
|
+
const note = (id) => {
|
|
6591
|
+
if (disabled || disposed) return;
|
|
6592
|
+
const state2 = stateFor(id);
|
|
6593
|
+
if (state2.root === false) return;
|
|
6594
|
+
state2.pending = true;
|
|
6595
|
+
if (state2.timer || state2.running) return;
|
|
6596
|
+
schedule(id, state2);
|
|
6597
|
+
};
|
|
6598
|
+
function schedule(id, state2) {
|
|
6599
|
+
if (state2.timer) clearTimeout(state2.timer);
|
|
6600
|
+
state2.timer = setTimeout(() => {
|
|
6601
|
+
state2.timer = void 0;
|
|
6602
|
+
void tick(id, state2);
|
|
6603
|
+
}, intervalMs);
|
|
6604
|
+
state2.timer.unref?.();
|
|
6605
|
+
}
|
|
6606
|
+
async function summarize(id, title, excerptText, prior, abort) {
|
|
6607
|
+
if (abort?.aborted) return { ok: false, bullets: [], error: "aborted" };
|
|
6608
|
+
let slot;
|
|
6609
|
+
try {
|
|
6610
|
+
init(poolState, (m) => log("warn", m));
|
|
6611
|
+
const snap = snapshot(poolState);
|
|
6612
|
+
slot = pickSlot(poolState, snap);
|
|
6613
|
+
} catch (error) {
|
|
6614
|
+
log("warn", String(error));
|
|
6615
|
+
return { ok: false, bullets: [], error: String(error) };
|
|
6616
|
+
}
|
|
6617
|
+
const model = promptModel(slot?.model);
|
|
6618
|
+
if (!model) {
|
|
6619
|
+
log("warn", "timeline: slot model cannot be split into provider/model", { model: slot?.model });
|
|
6620
|
+
return { ok: false, bullets: [], error: "slot model cannot be split into provider/model" };
|
|
6621
|
+
}
|
|
6622
|
+
const created = await client.session.create({
|
|
6623
|
+
body: { title: "timeline (auto)" },
|
|
6624
|
+
query: { directory }
|
|
6625
|
+
});
|
|
6626
|
+
const target = created?.data?.id;
|
|
6627
|
+
if (!target) {
|
|
6628
|
+
return { ok: false, bullets: [], error: "session create returned no id" };
|
|
6629
|
+
}
|
|
6630
|
+
const claimKey2 = claimKey(target, "timeline");
|
|
6631
|
+
let claimed = false;
|
|
6632
|
+
owned.add(target);
|
|
6633
|
+
let abortListener;
|
|
6634
|
+
try {
|
|
6635
|
+
claimed = true;
|
|
6636
|
+
acquire(poolState, "context", slot, target, claimKey2, target);
|
|
6637
|
+
const prompt = [
|
|
6638
|
+
`Session: ${title}`,
|
|
6639
|
+
...prior.length > 0 ? ["Existing timeline (do not repeat):", ...prior.map((entry) => `- ${entry.text}`)] : [],
|
|
6640
|
+
"Transcript slice:",
|
|
6641
|
+
excerptText
|
|
6642
|
+
].join("\n");
|
|
6643
|
+
if (abort) {
|
|
6644
|
+
abortListener = () => {
|
|
6645
|
+
void client.session.abort({ path: { id: target }, query: { directory } }).catch(() => {
|
|
6646
|
+
});
|
|
6647
|
+
};
|
|
6648
|
+
abort.addEventListener("abort", abortListener);
|
|
6649
|
+
}
|
|
6650
|
+
let timer;
|
|
6651
|
+
let timedOut = false;
|
|
6652
|
+
let result;
|
|
6653
|
+
try {
|
|
6654
|
+
const answer = client.session.prompt({
|
|
6655
|
+
path: { id: target },
|
|
6656
|
+
query: { directory },
|
|
6657
|
+
body: {
|
|
6658
|
+
model,
|
|
6659
|
+
tools: { "*": false },
|
|
6660
|
+
system: SYSTEM,
|
|
6661
|
+
parts: [{ type: "text", text: prompt }]
|
|
6662
|
+
}
|
|
6663
|
+
}).then(
|
|
6664
|
+
(value) => value,
|
|
6665
|
+
(error) => ({ error })
|
|
6666
|
+
);
|
|
6667
|
+
const deadline = new Promise((resolve5) => {
|
|
6668
|
+
timer = setTimeout(() => {
|
|
6669
|
+
timedOut = true;
|
|
6670
|
+
void client.session.abort({ path: { id: target }, query: { directory } }).catch(() => {
|
|
6671
|
+
});
|
|
6672
|
+
resolve5(void 0);
|
|
6673
|
+
}, PROMPT_TIMEOUT_MS);
|
|
6674
|
+
});
|
|
6675
|
+
result = await Promise.race([answer, deadline]);
|
|
6676
|
+
} finally {
|
|
6677
|
+
if (timer) clearTimeout(timer);
|
|
6678
|
+
}
|
|
6679
|
+
if (abort?.aborted) {
|
|
6680
|
+
return { ok: false, bullets: [], error: "aborted" };
|
|
6681
|
+
}
|
|
6682
|
+
const failure = result?.error ?? result?.data?.info?.error;
|
|
6683
|
+
if (timedOut || failure !== void 0) {
|
|
6684
|
+
return { ok: false, bullets: [], error: timedOut ? "timed out" : String(failure) };
|
|
6685
|
+
} else {
|
|
6686
|
+
const text = (result?.data?.parts ?? []).map((part) => part.type === "text" ? part.text ?? "" : "").join("\n");
|
|
6687
|
+
const bullets = parseBullets(text, maxEntries);
|
|
6688
|
+
if (bullets.length > 0) {
|
|
6689
|
+
return { ok: true, bullets };
|
|
6690
|
+
} else if (text.trim() === "") {
|
|
6691
|
+
return { ok: true, bullets: [] };
|
|
6692
|
+
} else {
|
|
6693
|
+
if (process.env.LACODE_TIMELINE_DEBUG === "1") {
|
|
6694
|
+
log("no bullet lines", { sessionID: id, text: text.slice(0, 300) });
|
|
6695
|
+
}
|
|
6696
|
+
return { ok: false, bullets: [], error: "no bullet lines" };
|
|
6697
|
+
}
|
|
6698
|
+
}
|
|
6699
|
+
} finally {
|
|
6700
|
+
if (abort && abortListener) abort.removeEventListener("abort", abortListener);
|
|
6701
|
+
if (claimed && claimKey2) release(poolState, claimKey2);
|
|
6702
|
+
await client.session.delete({ path: { id: target }, query: { directory } }).catch(() => {
|
|
6703
|
+
});
|
|
6704
|
+
owned.delete(target);
|
|
6705
|
+
}
|
|
6706
|
+
}
|
|
6707
|
+
async function tick(id, state2) {
|
|
6708
|
+
if (disposed) return;
|
|
6709
|
+
if (reprocessing.has(id)) return;
|
|
6710
|
+
try {
|
|
6711
|
+
state2.running = true;
|
|
6712
|
+
state2.pending = false;
|
|
6713
|
+
let title;
|
|
6714
|
+
let info = {};
|
|
6715
|
+
if (state2.root === void 0) {
|
|
6716
|
+
info = await client.session.get({ path: { id }, query: { directory } }).then((result2) => result2?.data).catch(() => void 0);
|
|
6717
|
+
if (!info) {
|
|
6718
|
+
sessions.delete(id);
|
|
6719
|
+
return;
|
|
6720
|
+
}
|
|
6721
|
+
title = info.title;
|
|
6722
|
+
state2.root = !info.parentID && !isInternalTitle(info.title);
|
|
6723
|
+
if (!state2.root) {
|
|
6724
|
+
sessions.delete(id);
|
|
6725
|
+
return;
|
|
6726
|
+
}
|
|
6727
|
+
}
|
|
6728
|
+
const response = await client.session.messages({ path: { id }, query: { directory } });
|
|
6729
|
+
const messages = response?.data ?? [];
|
|
6730
|
+
const excerpt = buildExcerpt(messages, state2.cursor);
|
|
6731
|
+
if (!excerpt.text) {
|
|
6732
|
+
return;
|
|
6733
|
+
}
|
|
6734
|
+
const result = await summarize(id, info.title ?? "untitled", excerpt.text, readEntries(id, 4));
|
|
6735
|
+
if (result.ok) {
|
|
6736
|
+
if (result.bullets.length) appendEntries(id, result.bullets, Date.now(), excerpt.next);
|
|
6737
|
+
state2.cursor = excerpt.next;
|
|
6738
|
+
state2.failures = 0;
|
|
6739
|
+
} else {
|
|
6740
|
+
state2.failures++;
|
|
6741
|
+
log("summarize failed", { sessionID: id, error: result.error });
|
|
6742
|
+
}
|
|
6743
|
+
} catch (error) {
|
|
6744
|
+
debug("tick failed", { sessionID: id, error });
|
|
6745
|
+
} finally {
|
|
6746
|
+
state2.running = false;
|
|
6747
|
+
if (sessions.get(id) === state2 && state2.pending && !disabled && !disposed && state2.root !== false) {
|
|
6748
|
+
schedule(id, state2);
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
6751
|
+
}
|
|
6752
|
+
async function resolveRootSession(id) {
|
|
6753
|
+
let current = id;
|
|
6754
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6755
|
+
while (true) {
|
|
6756
|
+
if (seen.has(current)) return current;
|
|
6757
|
+
seen.add(current);
|
|
6758
|
+
const info = await client.session.get({ path: { id: current }, query: { directory } }).then((result) => result?.data).catch(() => void 0);
|
|
6759
|
+
if (!info?.parentID) return current;
|
|
6760
|
+
current = info.parentID;
|
|
6761
|
+
}
|
|
6762
|
+
}
|
|
6763
|
+
async function reprocess(id, abort) {
|
|
6764
|
+
const rootId = await resolveRootSession(id);
|
|
6765
|
+
if (reprocessing.has(rootId)) return "A timeline reprocess is already running for this session.";
|
|
6766
|
+
if (sessions.get(rootId)?.running) return "The timeline is currently summarizing this session; try again in a moment.";
|
|
6767
|
+
reprocessing.add(rootId);
|
|
6768
|
+
const targetFile = timelineFile(rootId);
|
|
6769
|
+
const temp = targetFile + ".reprocess." + process.pid + "." + Date.now() + ".tmp";
|
|
6770
|
+
let cursor;
|
|
6771
|
+
let entries = 0;
|
|
6772
|
+
let calls = 0;
|
|
6773
|
+
try {
|
|
6774
|
+
const infoRes = await client.session.get({ path: { id: rootId }, query: { directory } });
|
|
6775
|
+
const info = infoRes?.data;
|
|
6776
|
+
if (!info) return "Session not found.";
|
|
6777
|
+
const msgRes = await client.session.messages({ path: { id: rootId }, query: { directory } });
|
|
6778
|
+
const messages = msgRes?.data ?? [];
|
|
6779
|
+
writeFileSync8(temp, "");
|
|
6780
|
+
for (let i = 0; i < MAX_REPROCESS_SLICES; i++) {
|
|
6781
|
+
if (abort?.aborted) {
|
|
6782
|
+
try {
|
|
6783
|
+
rmSync3(temp, { force: true });
|
|
6784
|
+
} catch {
|
|
6785
|
+
}
|
|
6786
|
+
return `Aborted after ${calls} summarizer calls; previous timeline kept.`;
|
|
6787
|
+
}
|
|
6788
|
+
const excerpt = buildExcerpt(messages, cursor);
|
|
6789
|
+
if (!excerpt.text) break;
|
|
6790
|
+
const prior = readEntriesAt(temp, 4);
|
|
6791
|
+
const result = await summarize(rootId, info.title ?? "untitled", excerpt.text, prior, abort);
|
|
6792
|
+
calls++;
|
|
6793
|
+
if (!result.ok) {
|
|
6794
|
+
try {
|
|
6795
|
+
rmSync3(temp, { force: true });
|
|
6796
|
+
} catch {
|
|
6797
|
+
}
|
|
6798
|
+
return `Stopped after ${calls} summarizer calls: ${result.error}. Previous timeline kept.`;
|
|
6799
|
+
}
|
|
6800
|
+
if (result.bullets.length) {
|
|
6801
|
+
entries += appendEntriesAt(temp, result.bullets, Date.now(), excerpt.next);
|
|
6802
|
+
}
|
|
6803
|
+
cursor = excerpt.next;
|
|
6804
|
+
}
|
|
6805
|
+
if (abort?.aborted) {
|
|
6806
|
+
try {
|
|
6807
|
+
rmSync3(temp, { force: true });
|
|
6808
|
+
} catch {
|
|
6809
|
+
}
|
|
6810
|
+
return `Aborted after ${calls} summarizer calls; previous timeline kept.`;
|
|
6811
|
+
}
|
|
6812
|
+
if (entries === 0) {
|
|
6813
|
+
try {
|
|
6814
|
+
rmSync3(temp, { force: true });
|
|
6815
|
+
} catch {
|
|
6816
|
+
}
|
|
6817
|
+
return "No entries generated; previous timeline kept.";
|
|
6818
|
+
}
|
|
6819
|
+
renameSync6(temp, targetFile);
|
|
6820
|
+
const state2 = sessions.get(rootId);
|
|
6821
|
+
if (state2) {
|
|
6822
|
+
state2.cursor = cursor;
|
|
6823
|
+
state2.failures = 0;
|
|
6824
|
+
state2.pending = false;
|
|
6825
|
+
}
|
|
6826
|
+
return `Rebuilt ${messages.length} messages into ${entries} entries with ${calls} summarizer calls.`;
|
|
6827
|
+
} catch (error) {
|
|
6828
|
+
try {
|
|
6829
|
+
rmSync3(temp, { force: true });
|
|
6830
|
+
} catch {
|
|
6831
|
+
}
|
|
6832
|
+
return `Reprocess failed: ${String(error)}. Previous timeline kept.`;
|
|
6833
|
+
} finally {
|
|
6834
|
+
reprocessing.delete(rootId);
|
|
6835
|
+
}
|
|
6836
|
+
}
|
|
6837
|
+
const timelineReprocess = tool2({
|
|
6838
|
+
description: "Rebuild the root session's entire timeline from its transcript, replacing the existing entries. Used by /reprocess-timeline.",
|
|
6839
|
+
args: {},
|
|
6840
|
+
execute: async (_args, ctx) => {
|
|
6841
|
+
const output = await reprocess(ctx.sessionID, ctx.abort);
|
|
6842
|
+
return { title: "Timeline reprocess", output };
|
|
6843
|
+
}
|
|
6844
|
+
});
|
|
6845
|
+
return {
|
|
6846
|
+
event: async ({ event }) => {
|
|
6847
|
+
try {
|
|
6848
|
+
switch (event.type) {
|
|
6849
|
+
case "message.updated": {
|
|
6850
|
+
const id = event.properties.info?.sessionID;
|
|
6851
|
+
if (!id) return;
|
|
6852
|
+
if (owned.has(id)) return;
|
|
6853
|
+
const role = event.properties.info?.role;
|
|
6854
|
+
if (role === "user" || role === "assistant") note(id);
|
|
6855
|
+
return;
|
|
6856
|
+
}
|
|
6857
|
+
case "message.part.updated": {
|
|
6858
|
+
const id = event.properties.part?.sessionID;
|
|
6859
|
+
if (!id) return;
|
|
6860
|
+
if (owned.has(id)) return;
|
|
6861
|
+
const type = event.properties.part?.type;
|
|
6862
|
+
if (type === "text" || type === "tool") note(id);
|
|
6863
|
+
return;
|
|
6864
|
+
}
|
|
6865
|
+
case "session.created": {
|
|
6866
|
+
const info = event.properties.info;
|
|
6867
|
+
if (!info) return;
|
|
6868
|
+
if (isInternalTitle(info.title)) owned.add(info.id);
|
|
6869
|
+
return;
|
|
6870
|
+
}
|
|
6871
|
+
case "session.deleted": {
|
|
6872
|
+
const id = event.properties.info?.id;
|
|
6873
|
+
if (!id) return;
|
|
6874
|
+
const state2 = sessions.get(id);
|
|
6875
|
+
if (state2?.timer) {
|
|
6876
|
+
clearTimeout(state2.timer);
|
|
6877
|
+
state2.timer = void 0;
|
|
6878
|
+
}
|
|
6879
|
+
sessions.delete(id);
|
|
6880
|
+
removeEntries(id);
|
|
6881
|
+
owned.delete(id);
|
|
6882
|
+
return;
|
|
6883
|
+
}
|
|
6884
|
+
default:
|
|
6885
|
+
return;
|
|
6886
|
+
}
|
|
6887
|
+
} catch (error) {
|
|
6888
|
+
debug("event failed", error);
|
|
6889
|
+
}
|
|
6890
|
+
},
|
|
6891
|
+
dispose: async () => {
|
|
6892
|
+
disposed = true;
|
|
6893
|
+
for (const state2 of sessions.values()) {
|
|
6894
|
+
if (state2.timer) {
|
|
6895
|
+
clearTimeout(state2.timer);
|
|
6896
|
+
state2.timer = void 0;
|
|
6897
|
+
}
|
|
6898
|
+
}
|
|
6899
|
+
reprocessing.clear();
|
|
6900
|
+
},
|
|
6901
|
+
tool: { timeline_reprocess: timelineReprocess }
|
|
6902
|
+
};
|
|
6903
|
+
}
|
|
6904
|
+
|
|
5754
6905
|
// src/index.ts
|
|
5755
6906
|
var inertHooks = {
|
|
5756
6907
|
config: async () => {
|
|
@@ -5786,7 +6937,7 @@ var LaCode = (async (input, options) => {
|
|
|
5786
6937
|
return void 0;
|
|
5787
6938
|
}
|
|
5788
6939
|
};
|
|
5789
|
-
const agentSet = buildAgentSet();
|
|
6940
|
+
const agentSet = buildAgentSet(resolveModels({ models: opts.models, modelsFile: opts.modelsFile }).slots.length);
|
|
5790
6941
|
const pool = await settled("agent-pool", () => agentPoolHooks(input, options)) ?? inertHooks;
|
|
5791
6942
|
const autoupdate = await settled("context-autoupdate", () => contextAutoUpdateHooks(input, options)) ?? inertHooks;
|
|
5792
6943
|
const notifier = await settled("notify", () => notifyHooks(input, options)) ?? inertHooks;
|
|
@@ -5794,6 +6945,7 @@ var LaCode = (async (input, options) => {
|
|
|
5794
6945
|
const feedback = await settled("feedback", () => feedbackHooks(input, options)) ?? inertHooks;
|
|
5795
6946
|
const omniroute = await settled("omniroute", () => omniRouteHooks(input, options)) ?? inertHooks;
|
|
5796
6947
|
const imagepaste = await settled("image-paste", () => imagePasteHooks(input, opts.imagePaste)) ?? inertHooks;
|
|
6948
|
+
const timeline = await settled("timeline", () => timelineHooks(input, options)) ?? inertHooks;
|
|
5797
6949
|
const config = async (cfg) => {
|
|
5798
6950
|
try {
|
|
5799
6951
|
const injected = applyAgents(cfg, agentSet);
|
|
@@ -5817,6 +6969,8 @@ var LaCode = (async (input, options) => {
|
|
|
5817
6969
|
autoupdate.event?.(payload).catch(() => {
|
|
5818
6970
|
}),
|
|
5819
6971
|
notifier.event?.(payload).catch(() => {
|
|
6972
|
+
}),
|
|
6973
|
+
timeline.event?.(payload).catch(() => {
|
|
5820
6974
|
})
|
|
5821
6975
|
]);
|
|
5822
6976
|
};
|
|
@@ -5827,7 +6981,7 @@ var LaCode = (async (input, options) => {
|
|
|
5827
6981
|
// only thing that routes task calls, so there is nothing to merge them with.
|
|
5828
6982
|
"tool.execute.before": pool["tool.execute.before"],
|
|
5829
6983
|
"tool.execute.after": pool["tool.execute.after"],
|
|
5830
|
-
tool: pool.tool,
|
|
6984
|
+
tool: { ...pool.tool ?? {}, ...timeline.tool ?? {} },
|
|
5831
6985
|
"command.execute.before": imagepaste["command.execute.before"],
|
|
5832
6986
|
dispose: async () => {
|
|
5833
6987
|
await pool.dispose?.();
|
|
@@ -5837,6 +6991,7 @@ var LaCode = (async (input, options) => {
|
|
|
5837
6991
|
await feedback.dispose?.();
|
|
5838
6992
|
await omniroute.dispose?.();
|
|
5839
6993
|
await imagepaste.dispose?.();
|
|
6994
|
+
await timeline.dispose?.();
|
|
5840
6995
|
},
|
|
5841
6996
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
5842
6997
|
if (!Array.isArray(output?.system)) return;
|