@lawaty/lacode 1.1.0 → 1.2.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 +47 -1
- package/README.md +22 -9
- package/dist/agents.js +8 -3
- package/dist/commands.js +9 -5
- package/dist/index.js +1017 -94
- package/dist/lacode-voice.js +2351 -0
- package/dist/pool.js +73 -38
- package/dist/tui.js +1282 -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
|
@@ -35,9 +35,9 @@ var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
|
35
35
|
var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
|
|
36
36
|
var UNAVAILABLE_REASON = "endpoint-unavailable";
|
|
37
37
|
var MAX_WEIGHT = 100;
|
|
38
|
-
var
|
|
39
|
-
var
|
|
40
|
-
var
|
|
38
|
+
var slotCeiling = (slot) => slot.weight;
|
|
39
|
+
var slotBucket = (slot) => slot.bucket ?? slot.model;
|
|
40
|
+
var SLOTS_WARN_AT = 64;
|
|
41
41
|
var COOLDOWN_BASE_MS = 60 * 1e3;
|
|
42
42
|
var COOLDOWN_MAX_MS = 15 * 60 * 1e3;
|
|
43
43
|
var QUOTA_COOLDOWN_BASE_MS = 15 * 60 * 1e3;
|
|
@@ -47,8 +47,12 @@ var STATED_COOLDOWN_MAX_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
|
47
47
|
var isQuotaReason = (reason) => /quota|limit|rate|too-many|overloaded|429/i.test(reason);
|
|
48
48
|
var MODEL_MISSING_REASON = "model-missing";
|
|
49
49
|
var MODEL_MISSING_COOLDOWN_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
50
|
+
var AUTH_FAILURE_REASON = "auth-failed";
|
|
51
|
+
var AUTH_FAILURE_COOLDOWN_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
50
52
|
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
53
|
var isModelMissingText = (text) => MODEL_MISSING_PATTERN.test(text);
|
|
54
|
+
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;
|
|
55
|
+
var isAuthFailureText = (text) => AUTH_FAILURE_PATTERN.test(text);
|
|
52
56
|
var BASES = ["explore", "implement", "context"];
|
|
53
57
|
var PRESETS_DIR = join2(PACKAGE_ROOT, "presets");
|
|
54
58
|
var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
@@ -71,15 +75,18 @@ function parseSlots(parsed, origin) {
|
|
|
71
75
|
entries.forEach((entry, i) => {
|
|
72
76
|
const model = typeof entry?.model === "string" ? entry.model.trim() : "";
|
|
73
77
|
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
78
|
if (!model.includes("/")) return problems.push(`${at}: model must be written provider/model-id, got "${model}"`);
|
|
79
79
|
const weight = readWeight(entry, at, model, problems);
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
const rawBucket = entry?.bucket;
|
|
81
|
+
const bucket = typeof rawBucket === "string" ? rawBucket.trim() : void 0;
|
|
82
|
+
if (rawBucket !== void 0 && !bucket) {
|
|
83
|
+
problems.push(`${at} (${model}): bucket must be a non-empty name; using the model id`);
|
|
84
|
+
}
|
|
85
|
+
slots.push({ index: slots.length + 1, model, weight, ...bucket ? { bucket } : {} });
|
|
82
86
|
});
|
|
87
|
+
if (slots.length > SLOTS_WARN_AT) {
|
|
88
|
+
problems.push(`${origin}: ${slots.length} slots is unusually large, but every entry is kept (the pool has no slot cap)`);
|
|
89
|
+
}
|
|
83
90
|
if (slots.length === 0) {
|
|
84
91
|
problems.push(`${origin} yielded no usable slot; using built-in defaults`);
|
|
85
92
|
return { slots: FALLBACK_SLOTS, problems };
|
|
@@ -91,8 +98,17 @@ function parseTextSlots(text, origin) {
|
|
|
91
98
|
for (const raw of text.split(/\r?\n/)) {
|
|
92
99
|
const line = raw.split("#")[0].trim();
|
|
93
100
|
if (!line) continue;
|
|
94
|
-
const [model,
|
|
95
|
-
|
|
101
|
+
const [model, ...rest] = line.split(/\s+/);
|
|
102
|
+
let weight;
|
|
103
|
+
let bucket;
|
|
104
|
+
for (const token of rest) {
|
|
105
|
+
if (token.startsWith("b:")) {
|
|
106
|
+
if (bucket === void 0) bucket = token.slice(2);
|
|
107
|
+
} else if (weight === void 0) {
|
|
108
|
+
weight = token;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
entries.push({ model, ...weight === void 0 ? {} : { weight }, ...bucket === void 0 ? {} : { bucket } });
|
|
96
112
|
}
|
|
97
113
|
return parseSlots({ slots: entries }, origin);
|
|
98
114
|
}
|
|
@@ -236,9 +252,17 @@ var retryAfterMs = (headers) => {
|
|
|
236
252
|
for (const [key, value] of Object.entries(headers)) {
|
|
237
253
|
if (key.toLowerCase() !== "retry-after") continue;
|
|
238
254
|
const seconds = Number(value);
|
|
239
|
-
if (Number.isFinite(seconds) && seconds > 0)
|
|
255
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
256
|
+
const ms = seconds * 1e3;
|
|
257
|
+
if (ms > STATED_COOLDOWN_MAX_MS) return void 0;
|
|
258
|
+
return ms;
|
|
259
|
+
}
|
|
240
260
|
const when = Date.parse(value);
|
|
241
|
-
if (Number.isFinite(when))
|
|
261
|
+
if (Number.isFinite(when)) {
|
|
262
|
+
const delay = when - Date.now();
|
|
263
|
+
if (delay > STATED_COOLDOWN_MAX_MS) return void 0;
|
|
264
|
+
return Math.max(delay, 1e3);
|
|
265
|
+
}
|
|
242
266
|
}
|
|
243
267
|
return void 0;
|
|
244
268
|
};
|
|
@@ -288,7 +312,7 @@ function ensureSlots(s, onWarn) {
|
|
|
288
312
|
for (const problem of problems) onWarn(problem);
|
|
289
313
|
return slots;
|
|
290
314
|
}
|
|
291
|
-
var slotSignature = (origin, slots) => `${origin}\0${slots.map((slot) => `${slot.index}:${slot.model}:${slot.weight}`).join("|")}`;
|
|
315
|
+
var slotSignature = (origin, slots) => `${origin}\0${slots.map((slot) => `${slot.index}:${slot.model}:${slot.weight}:${slot.bucket ?? ""}`).join("|")}`;
|
|
292
316
|
function reloadSlots(s, onWarn) {
|
|
293
317
|
const { slots, problems, origin } = resolveModels({ models: s.inlineModels, modelsFile: s.modelsFile });
|
|
294
318
|
const changed = slotSignature(origin, slots) !== slotSignature(s.origin, s.slots);
|
|
@@ -466,7 +490,7 @@ function readSiblings(s) {
|
|
|
466
490
|
try {
|
|
467
491
|
const parsed = JSON.parse(readFileSync2(path5, "utf8"));
|
|
468
492
|
const claims = Object.values(parsed).filter(
|
|
469
|
-
(c) => c && typeof c.v === "string" && typeof c.m === "string" && now - (c.hb ?? c.t) <= CLAIM_TTL_MS
|
|
493
|
+
(c) => c && typeof c.v === "string" && typeof c.m === "string" && typeof c.k === "number" && Number.isFinite(c.k) && now - (c.hb ?? c.t) <= CLAIM_TTL_MS
|
|
470
494
|
);
|
|
471
495
|
siblings.push({ id: match[1], claims });
|
|
472
496
|
} catch {
|
|
@@ -516,20 +540,32 @@ function recordStrike(s, model, reason, stated) {
|
|
|
516
540
|
if (!s.dirReady) return;
|
|
517
541
|
if (reason === UNAVAILABLE_REASON) return coolUnavailable(s, model);
|
|
518
542
|
const now = Date.now();
|
|
519
|
-
const
|
|
543
|
+
const slotsForModel = s.slots.filter((candidate) => candidate.model === model);
|
|
544
|
+
const buckets = [...new Set(slotsForModel.map((slot) => slotBucket(slot)))];
|
|
545
|
+
const targets = buckets.length === 0 ? [model] : [...new Set(
|
|
546
|
+
s.slots.filter((candidate) => buckets.includes(slotBucket(candidate))).map((candidate) => candidate.model)
|
|
547
|
+
)];
|
|
548
|
+
if (targets.length === 0) targets.push(model);
|
|
549
|
+
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
550
|
const strikes = prior && now - prior.last < STRIKE_DECAY_MS ? prior.strikes + 1 : 1;
|
|
521
551
|
const quota = isQuotaReason(reason);
|
|
522
552
|
const base = quota ? QUOTA_COOLDOWN_BASE_MS : COOLDOWN_BASE_MS;
|
|
523
553
|
const cap = quota ? QUOTA_COOLDOWN_MAX_MS : COOLDOWN_MAX_MS;
|
|
524
554
|
const backoff = Math.min(cap, base * 2 ** (strikes - 1));
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
555
|
+
const statedOk = typeof stated === "number" && Number.isFinite(stated) && stated > 0 && stated <= STATED_COOLDOWN_MAX_MS;
|
|
556
|
+
const until = now + (statedOk ? stated : backoff);
|
|
557
|
+
let strike;
|
|
558
|
+
for (const target of targets) {
|
|
559
|
+
const record = { model: target, until, strikes, reason, last: now };
|
|
560
|
+
s.cooling.set(target, record);
|
|
561
|
+
s.strikes.set(target, record);
|
|
562
|
+
const path5 = join2(s.dir, limitFile(target));
|
|
563
|
+
try {
|
|
564
|
+
writeFileSync(`${path5}.tmp`, JSON.stringify(record));
|
|
565
|
+
renameSync(`${path5}.tmp`, path5);
|
|
566
|
+
} catch {
|
|
567
|
+
}
|
|
568
|
+
if (target === model) strike = record;
|
|
533
569
|
}
|
|
534
570
|
return strike;
|
|
535
571
|
}
|
|
@@ -619,30 +655,26 @@ function snapshot(s) {
|
|
|
619
655
|
const siblings = readSiblings(s);
|
|
620
656
|
const cooling = readLimits(s);
|
|
621
657
|
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.
|
|
658
|
+
const bump = (k) => counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
659
|
+
for (const claim of s.own.values()) bump(claim.k);
|
|
660
|
+
for (const sibling of siblings) for (const claim of sibling.claims) bump(claim.k);
|
|
625
661
|
return { counts, cooling, siblings, own: s.own, dirReady: s.dirReady };
|
|
626
662
|
}
|
|
627
|
-
var loadOf = (counts,
|
|
628
|
-
var underCeiling = (slot, counts) => loadOf(counts, slot.
|
|
663
|
+
var loadOf = (counts, slot) => counts.get(slot) ?? 0;
|
|
664
|
+
var underCeiling = (slot, counts) => loadOf(counts, slot.index) < slotCeiling(slot);
|
|
629
665
|
function pickSlot(s, snap) {
|
|
630
666
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
631
667
|
const usable = slots.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
632
668
|
const pool = usable.length > 0 ? usable : slots;
|
|
633
669
|
for (const slot of usable) {
|
|
634
|
-
if (loadOf(snap.counts, slot.
|
|
635
|
-
}
|
|
636
|
-
for (const slot of usable) {
|
|
637
|
-
if (loadOf(snap.counts, slot.model) < slotCeiling(slot)) return slot;
|
|
670
|
+
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
638
671
|
}
|
|
639
|
-
return pool.reduce((best, slot) => loadOf(snap.counts, slot.
|
|
672
|
+
return pool.reduce((best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best, pool[0]);
|
|
640
673
|
}
|
|
641
674
|
function poolPass(s, snap) {
|
|
642
675
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
643
676
|
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;
|
|
677
|
+
if (usable.some((slot) => loadOf(snap.counts, slot.index) < slot.weight)) return 1;
|
|
646
678
|
return "overflow";
|
|
647
679
|
}
|
|
648
680
|
function agentFor(base, slot) {
|
|
@@ -1677,12 +1709,12 @@ Report briefly what you changed and where. Do not edit any other file.`,
|
|
|
1677
1709
|
}
|
|
1678
1710
|
}
|
|
1679
1711
|
};
|
|
1680
|
-
function buildAgentSet() {
|
|
1712
|
+
function buildAgentSet(slotCount = FALLBACK_SLOTS.length) {
|
|
1681
1713
|
const set = { ...AGENTS };
|
|
1682
1714
|
for (const base of POOLED_BASES) {
|
|
1683
1715
|
const def = AGENTS[base];
|
|
1684
1716
|
if (!def) continue;
|
|
1685
|
-
for (let slot = 1; slot <=
|
|
1717
|
+
for (let slot = 1; slot <= Math.max(1, slotCount); slot++) {
|
|
1686
1718
|
const variant = {
|
|
1687
1719
|
...def,
|
|
1688
1720
|
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 +1871,7 @@ Follow the rules for the file in your system prompt, then report what you change
|
|
|
1839
1871
|
Print one PASS/FAIL line per check below, with the exact fix for any FAIL.
|
|
1840
1872
|
|
|
1841
1873
|
1. Pool status \u2014 call the \`pool_status\` tool. Report the slot list, each
|
|
1842
|
-
slot's load against its
|
|
1874
|
+
slot's load against its budget (its weight), and which models
|
|
1843
1875
|
are cooling.
|
|
1844
1876
|
|
|
1845
1877
|
2. OmniRoute configured \u2014 inspect the \`@lawaty/lacode\` plugin options in the
|
|
@@ -1876,10 +1908,10 @@ The pool is ONE global slot table shared by every pooled base (explore, implemen
|
|
|
1876
1908
|
|
|
1877
1909
|
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
1910
|
- 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
|
|
1911
|
+
- any number of slots; a model may appear on more than one line, and each line is its own slot with its own budget
|
|
1912
|
+
- 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
1913
|
- \`#\` starts a comment (whole-line or trailing); blank lines are ignored.
|
|
1882
|
-
If the instruction names a model ambiguously,
|
|
1914
|
+
If the instruction names a model ambiguously, stop and ask rather than guessing.
|
|
1883
1915
|
|
|
1884
1916
|
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
1917
|
|
|
@@ -1905,6 +1937,11 @@ $ARGUMENTS
|
|
|
1905
1937
|
|
|
1906
1938
|
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
1939
|
},
|
|
1940
|
+
"reprocess-timeline": {
|
|
1941
|
+
description: "Rebuild this session's timeline from its full transcript",
|
|
1942
|
+
agent: "implement",
|
|
1943
|
+
template: "Call the `timeline_reprocess` tool exactly once with no arguments. Then reply with only its output."
|
|
1944
|
+
},
|
|
1908
1945
|
"image-in-cb": {
|
|
1909
1946
|
description: "Attach the clipboard image to this message \u2014 macOS needs pngpaste, Linux needs wl-paste or xclip.",
|
|
1910
1947
|
template: "$ARGUMENTS"
|
|
@@ -2198,6 +2235,7 @@ import { tool } from "@opencode-ai/plugin";
|
|
|
2198
2235
|
var PROBE_SYSTEM = "You are a connection health check. Reply with the single word: ok";
|
|
2199
2236
|
var PROBE_PROMPT = "ok";
|
|
2200
2237
|
var PROBE_TIMEOUT_MS = 45e3;
|
|
2238
|
+
var REASON_UNRESPONSIVE = "no answer";
|
|
2201
2239
|
var failureText = (error) => {
|
|
2202
2240
|
if (error === void 0 || error === null) return "";
|
|
2203
2241
|
if (typeof error === "string") return error;
|
|
@@ -2210,32 +2248,35 @@ var failureText = (error) => {
|
|
|
2210
2248
|
var shortReason = (text) => {
|
|
2211
2249
|
if (!text) return "failed";
|
|
2212
2250
|
if (isModelMissingText(text)) return MODEL_MISSING_REASON;
|
|
2251
|
+
if (isAuthFailureText(text)) return AUTH_FAILURE_REASON;
|
|
2213
2252
|
return limitReason(text) ?? (isLimitText(text) ? "limit" : "failed");
|
|
2214
2253
|
};
|
|
2215
2254
|
var probeOne = async (client, opts, model, leftSeconds) => {
|
|
2216
2255
|
const { state: s, directory } = opts;
|
|
2217
2256
|
const { provider, id: modelID } = splitModel(model);
|
|
2218
|
-
const settle = (ok, reason, left) => ({
|
|
2257
|
+
const settle = (ok, reason, left, kind, strikes) => ({
|
|
2219
2258
|
model,
|
|
2220
2259
|
ok,
|
|
2221
2260
|
reason,
|
|
2222
2261
|
hadSeconds: leftSeconds,
|
|
2223
|
-
leftSeconds: left
|
|
2262
|
+
leftSeconds: left,
|
|
2263
|
+
kind,
|
|
2264
|
+
strikes
|
|
2224
2265
|
});
|
|
2225
2266
|
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);
|
|
2267
|
+
if (!created) return settle(false, "no probe session", leftSeconds, "cooling");
|
|
2227
2268
|
markProbe(s, created);
|
|
2228
2269
|
let timer;
|
|
2229
2270
|
try {
|
|
2230
2271
|
const timeoutMs = opts.timeoutMs ?? PROBE_TIMEOUT_MS;
|
|
2231
2272
|
const clock = opts.now ?? Date.now;
|
|
2232
2273
|
let timedOut = false;
|
|
2233
|
-
const deadline = new Promise((
|
|
2274
|
+
const deadline = new Promise((resolve5) => {
|
|
2234
2275
|
timer = setTimeout(() => {
|
|
2235
2276
|
timedOut = true;
|
|
2236
2277
|
void client.session.abort({ path: { id: created }, query: { directory } }).catch(() => {
|
|
2237
2278
|
});
|
|
2238
|
-
|
|
2279
|
+
resolve5(void 0);
|
|
2239
2280
|
}, timeoutMs);
|
|
2240
2281
|
});
|
|
2241
2282
|
const answer = client.session.prompt({
|
|
@@ -2254,20 +2295,37 @@ var probeOne = async (client, opts, model, leftSeconds) => {
|
|
|
2254
2295
|
const failure = result?.error ?? result?.data?.info?.error;
|
|
2255
2296
|
if (!timedOut && failure === void 0 && result?.data?.info !== void 0) {
|
|
2256
2297
|
clearCooldowns(s, [model]);
|
|
2257
|
-
return settle(true, "answered", 0);
|
|
2298
|
+
return settle(true, "answered", 0, "answered");
|
|
2258
2299
|
}
|
|
2259
2300
|
const text = failureText(failure);
|
|
2301
|
+
let kind = "cooling";
|
|
2302
|
+
let strikes;
|
|
2260
2303
|
if (isModelMissingText(text)) {
|
|
2261
|
-
recordStrike(s, model, MODEL_MISSING_REASON, MODEL_MISSING_COOLDOWN_MS);
|
|
2304
|
+
const recorded = recordStrike(s, model, MODEL_MISSING_REASON, MODEL_MISSING_COOLDOWN_MS);
|
|
2305
|
+
strikes = recorded?.strikes;
|
|
2306
|
+
kind = "benched";
|
|
2307
|
+
} else if (isAuthFailureText(text)) {
|
|
2308
|
+
const recorded = recordStrike(s, model, AUTH_FAILURE_REASON, AUTH_FAILURE_COOLDOWN_MS);
|
|
2309
|
+
strikes = recorded?.strikes;
|
|
2310
|
+
kind = "benched";
|
|
2311
|
+
} else if (timedOut || text === "") {
|
|
2312
|
+
const recorded = recordStrike(s, model, REASON_UNRESPONSIVE, 0);
|
|
2313
|
+
strikes = recorded?.strikes;
|
|
2314
|
+
kind = "benched";
|
|
2262
2315
|
} else {
|
|
2263
2316
|
const stated = statedCooldown(text);
|
|
2264
|
-
if (stated !== void 0)
|
|
2317
|
+
if (stated !== void 0) {
|
|
2318
|
+
const recorded = recordStrike(s, model, shortReason(text), stated);
|
|
2319
|
+
strikes = recorded?.strikes;
|
|
2320
|
+
}
|
|
2265
2321
|
}
|
|
2266
2322
|
const still = readLimits(s).get(model);
|
|
2267
2323
|
return settle(
|
|
2268
2324
|
false,
|
|
2269
2325
|
timedOut ? `no answer in ${Math.round(timeoutMs / 1e3)}s` : shortReason(text),
|
|
2270
|
-
still && still.until > clock() ? Math.ceil((still.until - clock()) / 1e3) : 0
|
|
2326
|
+
still && still.until > clock() ? Math.ceil((still.until - clock()) / 1e3) : 0,
|
|
2327
|
+
kind,
|
|
2328
|
+
strikes
|
|
2271
2329
|
);
|
|
2272
2330
|
} finally {
|
|
2273
2331
|
if (timer) clearTimeout(timer);
|
|
@@ -2352,6 +2410,24 @@ var SLOT_WATCH_MS = 1e3;
|
|
|
2352
2410
|
var SLOT_WATCH_DEBOUNCE_MS = 250;
|
|
2353
2411
|
var ABORT_TIMEOUT_MS = 1e4;
|
|
2354
2412
|
var formatWindow = (seconds) => seconds < 60 ? `${seconds}s` : `${Math.ceil(seconds / 60)}m`;
|
|
2413
|
+
var reportText = (report) => {
|
|
2414
|
+
if (report.outcomes.length === 0) return "no models are cooling down, so there was nothing to probe.";
|
|
2415
|
+
const answered = report.outcomes.filter((outcome) => outcome.ok).length;
|
|
2416
|
+
const benched = report.outcomes.filter((outcome) => outcome.kind === "benched").length;
|
|
2417
|
+
const cooling = report.outcomes.length - answered - benched;
|
|
2418
|
+
return [
|
|
2419
|
+
...report.outcomes.map((outcome) => {
|
|
2420
|
+
if (outcome.ok) return `${outcome.model}: answered, cooldown cleared`;
|
|
2421
|
+
if (outcome.kind === "benched") {
|
|
2422
|
+
const window = outcome.leftSeconds > 0 ? ` for ${Math.round(outcome.leftSeconds)}s` : "";
|
|
2423
|
+
const strike = outcome.strikes !== void 0 ? ` (strike ${outcome.strikes})` : "";
|
|
2424
|
+
return `${outcome.model}: ${outcome.reason} \u2014 benched${window}${strike}`;
|
|
2425
|
+
}
|
|
2426
|
+
return `${outcome.model}: still limited (${outcome.reason}) \u2014 cooldown kept${outcome.leftSeconds > 0 ? `, ${formatWindow(outcome.leftSeconds)} left` : ""}`;
|
|
2427
|
+
}),
|
|
2428
|
+
`summary: ${answered} answered, ${benched} benched, ${cooling} still cooling`
|
|
2429
|
+
].join("\n");
|
|
2430
|
+
};
|
|
2355
2431
|
var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
2356
2432
|
const opts = options ?? {};
|
|
2357
2433
|
const s = state(opts);
|
|
@@ -2389,6 +2465,36 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2389
2465
|
return { ok: false, why: String(error) };
|
|
2390
2466
|
}
|
|
2391
2467
|
};
|
|
2468
|
+
const abortReports = /* @__PURE__ */ new Map();
|
|
2469
|
+
const abortKind = (reason) => isQuotaReason(reason) || isLimitText(reason) ? "rate-limit" : "provider error";
|
|
2470
|
+
const abortLabel = (kind, elapsedMs) => {
|
|
2471
|
+
const seconds = Math.max(1, Math.round(elapsedMs / 1e3));
|
|
2472
|
+
if (kind === "no-answer") return `no answer in ${seconds}s`;
|
|
2473
|
+
if (kind === "rate-limit") return `rate limited after ${seconds}s`;
|
|
2474
|
+
if (kind === "empty response") return `empty response after ${seconds}s`;
|
|
2475
|
+
if (kind === "permission") return `permission denied after ${seconds}s`;
|
|
2476
|
+
return `provider error after ${seconds}s`;
|
|
2477
|
+
};
|
|
2478
|
+
const noteAbort = async (key, report) => {
|
|
2479
|
+
abortReports.set(key, report);
|
|
2480
|
+
if (abortReports.size > 32) {
|
|
2481
|
+
const oldest = abortReports.keys().next().value;
|
|
2482
|
+
if (oldest !== void 0) abortReports.delete(oldest);
|
|
2483
|
+
}
|
|
2484
|
+
await log("warn", report.message, {
|
|
2485
|
+
slot: report.slot,
|
|
2486
|
+
model: report.model,
|
|
2487
|
+
elapsedMs: report.elapsedMs,
|
|
2488
|
+
failure: report.kind,
|
|
2489
|
+
released: report.released
|
|
2490
|
+
});
|
|
2491
|
+
try {
|
|
2492
|
+
await client.tui?.showToast?.({
|
|
2493
|
+
body: { title: "pool spawn", message: report.message, variant: "warning", duration: 8e3 }
|
|
2494
|
+
});
|
|
2495
|
+
} catch {
|
|
2496
|
+
}
|
|
2497
|
+
};
|
|
2392
2498
|
const MAX_ABORT_ATTEMPTS = 3;
|
|
2393
2499
|
const coolForHang = async (claim, log2) => {
|
|
2394
2500
|
const now = Date.now();
|
|
@@ -2444,6 +2550,16 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2444
2550
|
const kill = await abortSession(sessionID);
|
|
2445
2551
|
if (kill.ok) {
|
|
2446
2552
|
release(s, key);
|
|
2553
|
+
const elapsedMs = Date.now() - claim.t;
|
|
2554
|
+
const kind = abortKind(reason);
|
|
2555
|
+
await noteAbort(key, {
|
|
2556
|
+
slot: claim.k,
|
|
2557
|
+
model: claim.m,
|
|
2558
|
+
elapsedMs,
|
|
2559
|
+
kind,
|
|
2560
|
+
released: true,
|
|
2561
|
+
message: `slot ${claim.k} ${claim.m} \u2014 ${abortLabel(kind, elapsedMs)}; claim released`
|
|
2562
|
+
});
|
|
2447
2563
|
await log(
|
|
2448
2564
|
"warn",
|
|
2449
2565
|
`limit (${reason}) on ${model}; aborted in-flight ${claim.v} session ${sessionID} to fail over now`,
|
|
@@ -2512,6 +2628,15 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2512
2628
|
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
2629
|
{ base: claim.v, model: claim.m, slot: claim.k, session: claim.g, aborted: true }
|
|
2514
2630
|
);
|
|
2631
|
+
const elapsedMs = Date.now() - claim.t;
|
|
2632
|
+
await noteAbort(key, {
|
|
2633
|
+
slot: claim.k,
|
|
2634
|
+
model: claim.m,
|
|
2635
|
+
elapsedMs,
|
|
2636
|
+
kind: "no-answer",
|
|
2637
|
+
released: true,
|
|
2638
|
+
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`
|
|
2639
|
+
});
|
|
2515
2640
|
await coolForHang(claim, log);
|
|
2516
2641
|
continue;
|
|
2517
2642
|
}
|
|
@@ -2563,17 +2688,10 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2563
2688
|
if (report.outcomes.length === 0) return "nothing cooling to probe";
|
|
2564
2689
|
const parts = [`probed ${report.outcomes.length} model(s)`, `re-admitted ${report.cleared.length}`];
|
|
2565
2690
|
if (report.missing.length > 0) parts.push(`benched ${report.missing.length} missing`);
|
|
2691
|
+
const authBenched = report.outcomes.filter((outcome) => outcome.reason === AUTH_FAILURE_REASON).length;
|
|
2692
|
+
if (authBenched > 0) parts.push(`benched ${authBenched} auth-failed`);
|
|
2566
2693
|
return parts.join(", ");
|
|
2567
2694
|
};
|
|
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
2695
|
const announce = async (report) => {
|
|
2578
2696
|
if (!report.changed) return;
|
|
2579
2697
|
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.`;
|
|
@@ -2613,10 +2731,23 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2613
2731
|
for (const slot of slots) {
|
|
2614
2732
|
for (const base of BASES) {
|
|
2615
2733
|
const name = agentFor(base, slot.index);
|
|
2616
|
-
|
|
2734
|
+
let def = agents?.[name];
|
|
2617
2735
|
if (!def) {
|
|
2618
|
-
|
|
2619
|
-
|
|
2736
|
+
const baseDef = agents?.[base];
|
|
2737
|
+
if (!agents || !baseDef) {
|
|
2738
|
+
await warnOnce(`variant ${name} is missing from the config; spawns of ${base} that route to slot ${slot.index} would fail`);
|
|
2739
|
+
continue;
|
|
2740
|
+
}
|
|
2741
|
+
const created = {
|
|
2742
|
+
...baseDef,
|
|
2743
|
+
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.`,
|
|
2744
|
+
hidden: true,
|
|
2745
|
+
mode: "primary"
|
|
2746
|
+
};
|
|
2747
|
+
delete created.model;
|
|
2748
|
+
agents[name] = created;
|
|
2749
|
+
def = created;
|
|
2750
|
+
await log("info", `declared variant ${name} for slot ${slot.index} (table grew after startup)`);
|
|
2620
2751
|
}
|
|
2621
2752
|
if (def.model && def.model !== slot.model) {
|
|
2622
2753
|
await warnOnce(`${name} still hard-codes ${def.model}; the resolved pool preset wins (${slot.model}). Delete the line to keep one source of truth.`);
|
|
@@ -2698,7 +2829,8 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2698
2829
|
const provider = typeof action?.provider === "string" ? action.provider : void 0;
|
|
2699
2830
|
const reason2 = action?.reason ?? "";
|
|
2700
2831
|
const message = typeof status2?.message === "string" ? status2.message : "";
|
|
2701
|
-
const
|
|
2832
|
+
const auth2 = isAuthFailureText(reason2) || isAuthFailureText(message);
|
|
2833
|
+
const limited2 = reason2 === "free_tier_limit" || isLimitText(reason2) || isLimitText(message) || auth2;
|
|
2702
2834
|
if (status2?.type === "retry" && limited2) {
|
|
2703
2835
|
if (sessionID && isProbe(s, sessionID)) {
|
|
2704
2836
|
await log("debug", `probe on provider ${provider ?? "?"} was limited (${reason2 || "rate-limited"}); not cooling the provider`);
|
|
@@ -2706,9 +2838,9 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2706
2838
|
}
|
|
2707
2839
|
const own = claimFor(sessionID);
|
|
2708
2840
|
const outage = isUnavailableText(message) && !isQuotaReason(reason2);
|
|
2709
|
-
const named = outage ? UNAVAILABLE_REASON : reason2 || limitReason(message) || "rate-limited";
|
|
2841
|
+
const named = auth2 ? AUTH_FAILURE_REASON : outage ? UNAVAILABLE_REASON : reason2 || limitReason(message) || "rate-limited";
|
|
2710
2842
|
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;
|
|
2843
|
+
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
2844
|
const cooled = coolProvider(s, provider, until, named);
|
|
2713
2845
|
if (cooled.length > 0) {
|
|
2714
2846
|
await log("warn", `limit on provider ${provider} (${named}); cooling ${cooled.length} slot(s) until ${new Date(until).toISOString().slice(11, 19)}Z`, {
|
|
@@ -2718,7 +2850,16 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2718
2850
|
});
|
|
2719
2851
|
}
|
|
2720
2852
|
} else if (own) {
|
|
2721
|
-
if (
|
|
2853
|
+
if (auth2) {
|
|
2854
|
+
const strike2 = recordStrike(s, own.claim.m, AUTH_FAILURE_REASON, AUTH_FAILURE_COOLDOWN_MS);
|
|
2855
|
+
if (strike2) {
|
|
2856
|
+
await log("warn", `limit on ${own.claim.m} (${AUTH_FAILURE_REASON}); cooling it for ${Math.round((strike2.until - Date.now()) / 1e3)}s`, {
|
|
2857
|
+
model: own.claim.m,
|
|
2858
|
+
slot: own.claim.k,
|
|
2859
|
+
session: sessionID
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
} else if (outage) {
|
|
2722
2863
|
const transient = coolUnavailable(s, own.claim.m);
|
|
2723
2864
|
if (transient) await log("warn", `cooling ${own.claim.m} for ${Math.round((transient.until - Date.now()) / 1e3)}s (${transient.reason})`);
|
|
2724
2865
|
} else {
|
|
@@ -2751,14 +2892,25 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2751
2892
|
return;
|
|
2752
2893
|
}
|
|
2753
2894
|
const status2 = typeof error?.data?.statusCode === "number" ? error.data.statusCode : void 0;
|
|
2895
|
+
const auth2 = isAuthFailureText(haystack2) || status2 === 401 || status2 === 403;
|
|
2754
2896
|
const limited2 = isLimitText(haystack2) || status2 === 429;
|
|
2755
2897
|
const outage = isUnavailableText(haystack2);
|
|
2756
|
-
if (!limited2 && !outage) return;
|
|
2898
|
+
if (!limited2 && !outage && !auth2) return;
|
|
2757
2899
|
const own = claimFor(sessionID);
|
|
2758
|
-
const named = outage ? UNAVAILABLE_REASON : limitReason(haystack2) ?? "rate-limited";
|
|
2900
|
+
const named = auth2 ? AUTH_FAILURE_REASON : outage ? UNAVAILABLE_REASON : limitReason(haystack2) ?? "rate-limited";
|
|
2759
2901
|
let stated2;
|
|
2760
2902
|
if (own) {
|
|
2761
|
-
if (
|
|
2903
|
+
if (auth2) {
|
|
2904
|
+
stated2 = AUTH_FAILURE_COOLDOWN_MS;
|
|
2905
|
+
const strike2 = recordStrike(s, own.claim.m, AUTH_FAILURE_REASON, stated2);
|
|
2906
|
+
if (strike2) {
|
|
2907
|
+
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`, {
|
|
2908
|
+
model: own.claim.m,
|
|
2909
|
+
slot: own.claim.k,
|
|
2910
|
+
session: sessionID
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
} else if (outage) {
|
|
2762
2914
|
const transient = coolUnavailable(s, own.claim.m);
|
|
2763
2915
|
if (transient) {
|
|
2764
2916
|
await log("warn", `cooling ${own.claim.m} for ${Math.round((transient.until - Date.now()) / 1e3)}s (${transient.reason}) after a terminal error`);
|
|
@@ -2794,24 +2946,25 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2794
2946
|
if (!info || info.role !== "assistant" || !info.error) return;
|
|
2795
2947
|
const haystack = JSON.stringify(info.error) ?? "";
|
|
2796
2948
|
const status = typeof info.error.data?.statusCode === "number" ? info.error.data.statusCode : void 0;
|
|
2949
|
+
const auth = isAuthFailureText(haystack) || status === 401 || status === 403;
|
|
2797
2950
|
const limited = isLimitText(haystack) || status === 429;
|
|
2798
2951
|
const gateway5xx = status !== void 0 && status >= 500 && status <= 599;
|
|
2799
2952
|
const transientGateway = gateway5xx || isTransientGatewayText(haystack);
|
|
2800
|
-
if (!limited && !transientGateway) return;
|
|
2953
|
+
if (!limited && !transientGateway && !auth) return;
|
|
2801
2954
|
const model = `${info.providerID ?? "?"}/${info.modelID ?? "?"}`;
|
|
2802
2955
|
if (info.sessionID && isProbe(s, info.sessionID)) {
|
|
2803
2956
|
await log("debug", `probe of ${model} hit a limit (${isLimitText(haystack) ? "limit" : "transient"}); leaving the cooldown to the probe`);
|
|
2804
2957
|
return;
|
|
2805
2958
|
}
|
|
2806
2959
|
const ownMsg = claimFor(info.sessionID);
|
|
2807
|
-
if (isUnavailableText(haystack) || !limited && transientGateway) {
|
|
2960
|
+
if (isUnavailableText(haystack) || !limited && !auth && transientGateway) {
|
|
2808
2961
|
const transient = coolUnavailable(s, model);
|
|
2809
2962
|
if (transient) await log("warn", `cooling ${model} for ${Math.round((transient.until - Date.now()) / 1e3)}s (${transient.reason})`);
|
|
2810
2963
|
await failOver(info.sessionID, model, transient?.reason ?? UNAVAILABLE_REASON, { cool: false });
|
|
2811
2964
|
return;
|
|
2812
2965
|
}
|
|
2813
|
-
const stated = statedCooldown(haystack) ?? retryAfterMs(info.error.data?.responseHeaders);
|
|
2814
|
-
const reason = limitReason(haystack);
|
|
2966
|
+
const stated = auth ? AUTH_FAILURE_COOLDOWN_MS : statedCooldown(haystack) ?? retryAfterMs(info.error.data?.responseHeaders);
|
|
2967
|
+
const reason = auth ? AUTH_FAILURE_REASON : limitReason(haystack);
|
|
2815
2968
|
const strike = recordStrike(s, model, reason ?? "rate-limited", stated);
|
|
2816
2969
|
if (strike) await log("warn", `cooling ${model} for ${Math.round((strike.until - Date.now()) / 1e3)}s (${reason ?? "rate-limited"}, strike ${strike.strikes})`);
|
|
2817
2970
|
await failOver(info.sessionID, model, reason ?? "rate-limited", { stated, cool: !ownMsg });
|
|
@@ -2852,7 +3005,7 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2852
3005
|
weight: slot.weight,
|
|
2853
3006
|
model: slot.model,
|
|
2854
3007
|
ceiling: slotCeiling(slot),
|
|
2855
|
-
load: (snap.counts.get(slot.
|
|
3008
|
+
load: (snap.counts.get(slot.index) ?? 0) + 1
|
|
2856
3009
|
});
|
|
2857
3010
|
output.args.subagent_type = variant;
|
|
2858
3011
|
acquire(s, requested, slot, input.sessionID, claimKey(input.sessionID, input.callID));
|
|
@@ -2869,9 +3022,21 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2869
3022
|
await log("error", `routing failed, spawn unchanged: ${String(error)}`);
|
|
2870
3023
|
}
|
|
2871
3024
|
},
|
|
2872
|
-
"tool.execute.after": async (input) => {
|
|
3025
|
+
"tool.execute.after": async (input, output) => {
|
|
2873
3026
|
if (input.tool !== "task") return;
|
|
2874
|
-
|
|
3027
|
+
const key = claimKey(input.sessionID, input.callID);
|
|
3028
|
+
release(s, key);
|
|
3029
|
+
const report = abortReports.get(key);
|
|
3030
|
+
if (!report) return;
|
|
3031
|
+
abortReports.delete(key);
|
|
3032
|
+
const detail = [
|
|
3033
|
+
`pool spawn: ${report.message}`,
|
|
3034
|
+
`slot ${report.slot}; model ${report.model}; elapsed ${report.elapsedMs}ms; failure ${report.kind}; claim ${report.released ? "released" : "held"}`
|
|
3035
|
+
].join("\n");
|
|
3036
|
+
const text = typeof output.output === "string" ? output.output : "";
|
|
3037
|
+
output.output = /task\s+cancel+ed/i.test(text) ? detail : text ? `${text}
|
|
3038
|
+
|
|
3039
|
+
${detail}` : detail;
|
|
2875
3040
|
},
|
|
2876
3041
|
tool: {
|
|
2877
3042
|
pool_recheck: tool({
|
|
@@ -2894,18 +3059,18 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2894
3059
|
const snap = snapshot(s);
|
|
2895
3060
|
const pass = poolPass(s, snap);
|
|
2896
3061
|
const now = Date.now();
|
|
2897
|
-
const
|
|
2898
|
-
const add = (
|
|
2899
|
-
const entry =
|
|
3062
|
+
const bySlot = /* @__PURE__ */ new Map();
|
|
3063
|
+
const add = (k, base) => {
|
|
3064
|
+
const entry = bySlot.get(k) ?? { total: 0, bases: /* @__PURE__ */ new Map() };
|
|
2900
3065
|
entry.total++;
|
|
2901
3066
|
entry.bases.set(base, (entry.bases.get(base) ?? 0) + 1);
|
|
2902
|
-
|
|
3067
|
+
bySlot.set(k, entry);
|
|
2903
3068
|
};
|
|
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.
|
|
3069
|
+
for (const claim of snap.own.values()) add(claim.k, claim.v);
|
|
3070
|
+
for (const sibling of snap.siblings) for (const claim of sibling.claims) add(claim.k, claim.v);
|
|
2906
3071
|
const next = pickSlot(s, snap);
|
|
2907
3072
|
const line = (slot) => {
|
|
2908
|
-
const entry =
|
|
3073
|
+
const entry = bySlot.get(slot.index);
|
|
2909
3074
|
const users = entry && entry.bases.size > 0 ? [...entry.bases].map(([b, n]) => `${b}x${n}`).join(" ") : "idle";
|
|
2910
3075
|
const strike = snap.cooling.get(slot.model);
|
|
2911
3076
|
const status = s.catalog.get(slot.model);
|
|
@@ -2920,14 +3085,25 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2920
3085
|
};
|
|
2921
3086
|
const total = [...snap.counts.values()].reduce((sum, n) => sum + n, 0);
|
|
2922
3087
|
const coolingNow = [...snap.cooling.entries()].filter(([, st]) => st.until > now);
|
|
3088
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3089
|
+
for (const slot of slots) {
|
|
3090
|
+
const name = slotBucket(slot);
|
|
3091
|
+
const entry = buckets.get(name) ?? { seats: 0, slots: 0, cooling: false };
|
|
3092
|
+
entry.seats += slot.weight;
|
|
3093
|
+
entry.slots++;
|
|
3094
|
+
if (snap.cooling.has(slot.model)) entry.cooling = true;
|
|
3095
|
+
buckets.set(name, entry);
|
|
3096
|
+
}
|
|
3097
|
+
const sharedBuckets = [...buckets].filter(([, entry]) => entry.slots > 1);
|
|
2923
3098
|
const verdicts = [...snap.own.values()].map((claim) => condemnReason(s, claim, now));
|
|
2924
3099
|
const hung = verdicts.filter((verdict) => verdict === "hung").length;
|
|
2925
3100
|
const overran = verdicts.filter((verdict) => verdict === "overran").length;
|
|
2926
3101
|
return {
|
|
2927
3102
|
title: `agent-pool: ${total} task(s) in flight, ${coolingNow.length} model(s) cooling`,
|
|
2928
3103
|
output: [
|
|
2929
|
-
`shared pool over ${slots.length}
|
|
2930
|
-
|
|
3104
|
+
`shared pool over ${slots.length} slot(s) from ${s.origin}; state ${pass}; bases: ${BASES.join(", ")}
|
|
3105
|
+
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).`,
|
|
3106
|
+
...sharedBuckets.length ? [`buckets: ${sharedBuckets.map(([name, entry]) => `${name} ${entry.seats} seat(s)${entry.cooling ? " COOLING" : ""}`).join("; ")}`] : [],
|
|
2931
3107
|
...slots.map(line),
|
|
2932
3108
|
"",
|
|
2933
3109
|
"by process:",
|
|
@@ -3780,7 +3956,7 @@ var retireSession = async (client, directory, sessionID, mode, log) => {
|
|
|
3780
3956
|
const gone = async () => await client.session.get({ path: { id: sessionID }, query: { directory } }).then((r) => r.data).catch(() => void 0) === void 0;
|
|
3781
3957
|
await client.session.delete({ path: { id: sessionID }, query: { directory } }).catch(() => void 0);
|
|
3782
3958
|
if (await gone()) return;
|
|
3783
|
-
await new Promise((
|
|
3959
|
+
await new Promise((resolve5) => setTimeout(resolve5, 500));
|
|
3784
3960
|
await client.session.delete({ path: { id: sessionID }, query: { directory } }).catch(() => void 0);
|
|
3785
3961
|
if (await gone()) return;
|
|
3786
3962
|
await log?.("warn", "the cartographer's session could not be removed from the session list; it stays as an empty entry", {
|
|
@@ -4439,7 +4615,15 @@ var INTERNAL_TITLE_PREFIXES = [
|
|
|
4439
4615
|
// context-autoupdate spawns the cartographer through the session API, with no
|
|
4440
4616
|
// parentID, so it looks exactly like a root session going idle. Notifying for
|
|
4441
4617
|
// it would announce the cartographer's own bookkeeping as a finished task.
|
|
4442
|
-
"context (auto"
|
|
4618
|
+
"context (auto",
|
|
4619
|
+
// timeline spawns its summarizer through the session API with the same shape,
|
|
4620
|
+
// so it would otherwise announce the summarizer's own bookkeeping as a task.
|
|
4621
|
+
"timeline (auto",
|
|
4622
|
+
// pool probe sessions are created as root sessions with no parentID and the
|
|
4623
|
+
// title `pool probe (<model>)` (`src/lib/probe.ts`), so notification for
|
|
4624
|
+
// their idle/error would announce Lacode's own health-check as finished user
|
|
4625
|
+
// work.
|
|
4626
|
+
"pool probe"
|
|
4443
4627
|
];
|
|
4444
4628
|
var NOTIFY_OUTCOMES = ["done", "question", "permission", "error"];
|
|
4445
4629
|
var notifyMessage = (outcome, detail) => {
|
|
@@ -4927,10 +5111,10 @@ var notifyHooks = (async ({ client, directory }, options, deps = {}) => {
|
|
|
4927
5111
|
const info = await client.session.get({ path: { id: sessionID }, query: { directory } }).then((result) => result.data).catch(() => void 0);
|
|
4928
5112
|
return info;
|
|
4929
5113
|
};
|
|
4930
|
-
const deliverViaHelper = (title, message, urgency, sound, requestID, sessionID) => new Promise((
|
|
5114
|
+
const deliverViaHelper = (title, message, urgency, sound, requestID, sessionID) => new Promise((resolve5) => {
|
|
4931
5115
|
if (inflight >= maxInflight) {
|
|
4932
5116
|
void log("warn", `notification dropped: ${inflight} helpers already in flight`, { title });
|
|
4933
|
-
|
|
5117
|
+
resolve5({ ok: false, detail: `too many notification helpers in flight (${inflight})` });
|
|
4934
5118
|
return;
|
|
4935
5119
|
}
|
|
4936
5120
|
const helper = findHelper();
|
|
@@ -4939,7 +5123,7 @@ var notifyHooks = (async ({ client, directory }, options, deps = {}) => {
|
|
|
4939
5123
|
warnedHelper = true;
|
|
4940
5124
|
void log("warn", "no oc-notify helper found; run 'bin/oc-sync' to install one");
|
|
4941
5125
|
}
|
|
4942
|
-
|
|
5126
|
+
resolve5({ ok: false, detail: "no oc-notify helper found" });
|
|
4943
5127
|
return;
|
|
4944
5128
|
}
|
|
4945
5129
|
const childEnv = { ...process.env, ...sessionID ? { [NOTIFY_SESSION_ENV]: sessionID } : {} };
|
|
@@ -4962,7 +5146,7 @@ var notifyHooks = (async ({ client, directory }, options, deps = {}) => {
|
|
|
4962
5146
|
settled = true;
|
|
4963
5147
|
inflight--;
|
|
4964
5148
|
if (timer) clearTimeout(timer);
|
|
4965
|
-
|
|
5149
|
+
resolve5(result);
|
|
4966
5150
|
};
|
|
4967
5151
|
timer = setTimeout(() => {
|
|
4968
5152
|
try {
|
|
@@ -5751,6 +5935,741 @@ async function selfUpdateHooks(input, options) {
|
|
|
5751
5935
|
return {};
|
|
5752
5936
|
}
|
|
5753
5937
|
|
|
5938
|
+
// src/plugins/timeline.ts
|
|
5939
|
+
import { writeFileSync as writeFileSync8, renameSync as renameSync6, rmSync as rmSync3 } from "node:fs";
|
|
5940
|
+
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
5941
|
+
|
|
5942
|
+
// src/lib/timeline.ts
|
|
5943
|
+
import { appendFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync10, rmSync as rmSync2 } from "node:fs";
|
|
5944
|
+
import { homedir as homedir7 } from "node:os";
|
|
5945
|
+
import { dirname as dirname5, join as join11, resolve as resolve4 } from "node:path";
|
|
5946
|
+
function timelineDir() {
|
|
5947
|
+
const env = process.env.LACODE_TIMELINE_DIR;
|
|
5948
|
+
if (env) return resolve4(env);
|
|
5949
|
+
return join11(homedir7(), ".local/share/opencode/lacode/timeline");
|
|
5950
|
+
}
|
|
5951
|
+
function timelineFile(sessionID) {
|
|
5952
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(sessionID)) throw new Error("invalid sessionID");
|
|
5953
|
+
return join11(timelineDir(), sessionID + ".jsonl");
|
|
5954
|
+
}
|
|
5955
|
+
function normalizeThrough(through) {
|
|
5956
|
+
if (!through || typeof through !== "object") return void 0;
|
|
5957
|
+
const out = {};
|
|
5958
|
+
if (typeof through.messageID === "string") out.messageID = through.messageID;
|
|
5959
|
+
if (typeof through.time === "number") out.time = through.time;
|
|
5960
|
+
if (out.messageID === void 0 && out.time === void 0) return void 0;
|
|
5961
|
+
return out;
|
|
5962
|
+
}
|
|
5963
|
+
function appendEntriesAt(file, texts, now, through) {
|
|
5964
|
+
mkdirSync7(dirname5(file), { recursive: true });
|
|
5965
|
+
const cleaned = [];
|
|
5966
|
+
for (const t of texts) {
|
|
5967
|
+
if (typeof t !== "string" || t.length === 0) continue;
|
|
5968
|
+
cleaned.push(t.slice(0, 320));
|
|
5969
|
+
}
|
|
5970
|
+
let recent = [];
|
|
5971
|
+
try {
|
|
5972
|
+
recent = readEntriesAt(file, 20);
|
|
5973
|
+
} catch {
|
|
5974
|
+
recent = [];
|
|
5975
|
+
}
|
|
5976
|
+
const recentSet = new Set(recent.map((e) => e.text.toLowerCase()));
|
|
5977
|
+
const batchSeen = /* @__PURE__ */ new Set();
|
|
5978
|
+
const toAppend = [];
|
|
5979
|
+
for (const t of cleaned) {
|
|
5980
|
+
const key = t.toLowerCase();
|
|
5981
|
+
if (recentSet.has(key)) continue;
|
|
5982
|
+
if (batchSeen.has(key)) continue;
|
|
5983
|
+
batchSeen.add(key);
|
|
5984
|
+
toAppend.push(t);
|
|
5985
|
+
}
|
|
5986
|
+
const validThrough = normalizeThrough(through);
|
|
5987
|
+
let count = 0;
|
|
5988
|
+
for (const t of toAppend) {
|
|
5989
|
+
const ts = now ?? Date.now();
|
|
5990
|
+
const line = { ts, text: t };
|
|
5991
|
+
if (validThrough) line.through = validThrough;
|
|
5992
|
+
appendFileSync(file, JSON.stringify(line) + "\n");
|
|
5993
|
+
count++;
|
|
5994
|
+
}
|
|
5995
|
+
return count;
|
|
5996
|
+
}
|
|
5997
|
+
function readEntriesAt(file, limit) {
|
|
5998
|
+
let raw = "";
|
|
5999
|
+
try {
|
|
6000
|
+
raw = readFileSync10(file, "utf8");
|
|
6001
|
+
} catch {
|
|
6002
|
+
return [];
|
|
6003
|
+
}
|
|
6004
|
+
const entries = [];
|
|
6005
|
+
for (const line of raw.split("\n")) {
|
|
6006
|
+
if (line.trim() === "") continue;
|
|
6007
|
+
try {
|
|
6008
|
+
const obj = JSON.parse(line);
|
|
6009
|
+
if (obj && typeof obj.ts === "number" && typeof obj.text === "string") {
|
|
6010
|
+
const entry = { ts: obj.ts, text: obj.text };
|
|
6011
|
+
const t = normalizeThrough(obj.through);
|
|
6012
|
+
if (t) entry.through = t;
|
|
6013
|
+
entries.push(entry);
|
|
6014
|
+
}
|
|
6015
|
+
} catch {
|
|
6016
|
+
}
|
|
6017
|
+
}
|
|
6018
|
+
if (limit != null && limit <= 0) return [];
|
|
6019
|
+
const lim = limit == null ? 50 : limit;
|
|
6020
|
+
return entries.slice(-lim);
|
|
6021
|
+
}
|
|
6022
|
+
function appendEntries(sessionID, texts, now, through) {
|
|
6023
|
+
return appendEntriesAt(timelineFile(sessionID), texts, now, through);
|
|
6024
|
+
}
|
|
6025
|
+
function readEntries(sessionID, limit) {
|
|
6026
|
+
return readEntriesAt(timelineFile(sessionID), limit);
|
|
6027
|
+
}
|
|
6028
|
+
function removeEntries(sessionID) {
|
|
6029
|
+
try {
|
|
6030
|
+
rmSync2(timelineFile(sessionID), { force: true });
|
|
6031
|
+
} catch {
|
|
6032
|
+
}
|
|
6033
|
+
}
|
|
6034
|
+
function collapse(s) {
|
|
6035
|
+
return s.replace(/\s+/g, " ").trim();
|
|
6036
|
+
}
|
|
6037
|
+
function trunc(s, n) {
|
|
6038
|
+
return s.length > n ? s.slice(0, n) : s;
|
|
6039
|
+
}
|
|
6040
|
+
function stripAnsiAndControls(s) {
|
|
6041
|
+
let out = "";
|
|
6042
|
+
for (let i = 0; i < s.length; i++) {
|
|
6043
|
+
const code = s.charCodeAt(i);
|
|
6044
|
+
if (code === 27) {
|
|
6045
|
+
if (i + 1 < s.length && s[i + 1] === "[") {
|
|
6046
|
+
i += 2;
|
|
6047
|
+
while (i < s.length) {
|
|
6048
|
+
const c = s.charCodeAt(i);
|
|
6049
|
+
if (c >= 64 && c <= 126) {
|
|
6050
|
+
break;
|
|
6051
|
+
}
|
|
6052
|
+
i++;
|
|
6053
|
+
}
|
|
6054
|
+
continue;
|
|
6055
|
+
}
|
|
6056
|
+
if (i + 1 < s.length && s[i + 1] === "]") {
|
|
6057
|
+
i += 2;
|
|
6058
|
+
while (i < s.length) {
|
|
6059
|
+
const c = s.charCodeAt(i);
|
|
6060
|
+
if (c === 7) break;
|
|
6061
|
+
if (c === 27 && i + 1 < s.length && s[i + 1] === "\\") {
|
|
6062
|
+
i += 1;
|
|
6063
|
+
break;
|
|
6064
|
+
}
|
|
6065
|
+
i++;
|
|
6066
|
+
}
|
|
6067
|
+
continue;
|
|
6068
|
+
}
|
|
6069
|
+
if (i + 1 < s.length) {
|
|
6070
|
+
i += 1;
|
|
6071
|
+
}
|
|
6072
|
+
continue;
|
|
6073
|
+
}
|
|
6074
|
+
if (code === 9 || code === 10) {
|
|
6075
|
+
out += s[i];
|
|
6076
|
+
continue;
|
|
6077
|
+
}
|
|
6078
|
+
if (code < 32 || code >= 127 && code <= 159) {
|
|
6079
|
+
continue;
|
|
6080
|
+
}
|
|
6081
|
+
out += s[i];
|
|
6082
|
+
}
|
|
6083
|
+
return out;
|
|
6084
|
+
}
|
|
6085
|
+
function inputSummary(input) {
|
|
6086
|
+
if (input === null || input === void 0) return "";
|
|
6087
|
+
if (typeof input !== "object") {
|
|
6088
|
+
try {
|
|
6089
|
+
return trunc(collapse(JSON.stringify(input)), 160);
|
|
6090
|
+
} catch {
|
|
6091
|
+
return "";
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
6094
|
+
const rec = input;
|
|
6095
|
+
const keys = ["filePath", "path", "command", "description", "pattern", "query", "url", "name", "title"];
|
|
6096
|
+
for (const k of keys) {
|
|
6097
|
+
if (Object.prototype.hasOwnProperty.call(rec, k) && rec[k] != null) {
|
|
6098
|
+
const v = rec[k];
|
|
6099
|
+
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
6100
|
+
return trunc(collapse(s), 160);
|
|
6101
|
+
}
|
|
6102
|
+
}
|
|
6103
|
+
try {
|
|
6104
|
+
return trunc(collapse(JSON.stringify(rec)), 160);
|
|
6105
|
+
} catch {
|
|
6106
|
+
return "";
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
6109
|
+
function summarizeToolResult(state2) {
|
|
6110
|
+
if (!state2) return "";
|
|
6111
|
+
const err = state2.error;
|
|
6112
|
+
if (typeof err === "string") {
|
|
6113
|
+
if (err.trim() === "") return "";
|
|
6114
|
+
return `error: ${trunc(err, 180)}`;
|
|
6115
|
+
}
|
|
6116
|
+
if (err && typeof err === "object") {
|
|
6117
|
+
try {
|
|
6118
|
+
const s = JSON.stringify(err);
|
|
6119
|
+
if (typeof s === "string" && s.trim() !== "") {
|
|
6120
|
+
return trunc(s, 180);
|
|
6121
|
+
}
|
|
6122
|
+
} catch {
|
|
6123
|
+
}
|
|
6124
|
+
return "";
|
|
6125
|
+
}
|
|
6126
|
+
if (typeof state2.output === "string") {
|
|
6127
|
+
for (const raw of state2.output.split("\n")) {
|
|
6128
|
+
const line = raw.trim();
|
|
6129
|
+
if (line.length > 0) {
|
|
6130
|
+
return trunc(line, 180);
|
|
6131
|
+
}
|
|
6132
|
+
}
|
|
6133
|
+
}
|
|
6134
|
+
if (typeof state2.title === "string" && state2.title.trim() !== "") {
|
|
6135
|
+
return trunc(state2.title, 180);
|
|
6136
|
+
}
|
|
6137
|
+
return "";
|
|
6138
|
+
}
|
|
6139
|
+
function renderMessage(m) {
|
|
6140
|
+
const role = m.info.role;
|
|
6141
|
+
const lines = [];
|
|
6142
|
+
if (role === "user") {
|
|
6143
|
+
for (const p of m.parts) {
|
|
6144
|
+
if (p.type === "text" && !p.synthetic && !p.ignored) {
|
|
6145
|
+
if ((p.text ?? "").includes("timeline_reprocess")) continue;
|
|
6146
|
+
const text = collapse(p.text ?? "");
|
|
6147
|
+
lines.push(`[user] ${trunc(text, 2e3)}`);
|
|
6148
|
+
}
|
|
6149
|
+
}
|
|
6150
|
+
} else if (role === "assistant") {
|
|
6151
|
+
for (const p of m.parts) {
|
|
6152
|
+
if (p.type === "text") {
|
|
6153
|
+
const text = collapse(p.text ?? "");
|
|
6154
|
+
lines.push(`[assistant] ${trunc(text, 1500)}`);
|
|
6155
|
+
} else if (p.type === "tool") {
|
|
6156
|
+
const tool3 = p.tool ?? "";
|
|
6157
|
+
if (tool3 === "timeline_reprocess") continue;
|
|
6158
|
+
const status = p.state?.status;
|
|
6159
|
+
const summary = inputSummary(p.state?.input);
|
|
6160
|
+
const result = summarizeToolResult(p.state);
|
|
6161
|
+
let line = `[tool] ${tool3}`;
|
|
6162
|
+
if (status) line += ` (${status})`;
|
|
6163
|
+
const inputPart = summary ? trunc(summary, 220) : "";
|
|
6164
|
+
let rest = "";
|
|
6165
|
+
if (inputPart) rest += ` ${inputPart}`;
|
|
6166
|
+
if (result) rest += ` -> ${result}`;
|
|
6167
|
+
line += rest;
|
|
6168
|
+
lines.push(line);
|
|
6169
|
+
}
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
6172
|
+
return lines;
|
|
6173
|
+
}
|
|
6174
|
+
function buildExcerpt(messages, cursor, budget = 8e3) {
|
|
6175
|
+
let start = 0;
|
|
6176
|
+
if (cursor?.messageID) {
|
|
6177
|
+
const idx = messages.findIndex((m) => m.info.id === cursor.messageID);
|
|
6178
|
+
if (idx >= 0) {
|
|
6179
|
+
start = idx + 1;
|
|
6180
|
+
} else if (cursor.time != null) {
|
|
6181
|
+
const i = messages.findIndex((m) => (m.info.time?.created ?? -Infinity) > cursor.time);
|
|
6182
|
+
start = i < 0 ? messages.length : i;
|
|
6183
|
+
}
|
|
6184
|
+
} else if (cursor?.time != null) {
|
|
6185
|
+
const i = messages.findIndex((m) => (m.info.time?.created ?? -Infinity) > cursor.time);
|
|
6186
|
+
start = i < 0 ? messages.length : i;
|
|
6187
|
+
}
|
|
6188
|
+
const outLines = [];
|
|
6189
|
+
const included = [];
|
|
6190
|
+
for (let i = start; i < messages.length; i++) {
|
|
6191
|
+
const m = messages[i];
|
|
6192
|
+
const msgLines = renderMessage(m);
|
|
6193
|
+
if (msgLines.length === 0) continue;
|
|
6194
|
+
const candidateLines = outLines.length === 0 ? [...msgLines] : [...outLines, ...msgLines];
|
|
6195
|
+
const candidateText = candidateLines.join("\n");
|
|
6196
|
+
if (included.length > 0 && candidateText.length > budget) {
|
|
6197
|
+
break;
|
|
6198
|
+
}
|
|
6199
|
+
if (included.length === 0 && candidateText.length > budget) {
|
|
6200
|
+
const block = msgLines.join("\n");
|
|
6201
|
+
outLines.push(block.slice(0, budget));
|
|
6202
|
+
included.push(m);
|
|
6203
|
+
break;
|
|
6204
|
+
}
|
|
6205
|
+
outLines.push(...msgLines);
|
|
6206
|
+
included.push(m);
|
|
6207
|
+
}
|
|
6208
|
+
let model;
|
|
6209
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
6210
|
+
const info = messages[i].info;
|
|
6211
|
+
if (info.role !== "user") continue;
|
|
6212
|
+
if (info.model) {
|
|
6213
|
+
model = { providerID: info.model.providerID, modelID: info.model.modelID };
|
|
6214
|
+
break;
|
|
6215
|
+
}
|
|
6216
|
+
if (info.providerID && info.modelID) {
|
|
6217
|
+
model = { providerID: info.providerID, modelID: info.modelID };
|
|
6218
|
+
break;
|
|
6219
|
+
}
|
|
6220
|
+
}
|
|
6221
|
+
const lastIncluded = included[included.length - 1];
|
|
6222
|
+
const next = lastIncluded ? { messageID: lastIncluded.info.id, time: lastIncluded.info.time?.created } : void 0;
|
|
6223
|
+
return {
|
|
6224
|
+
text: outLines.join("\n"),
|
|
6225
|
+
next,
|
|
6226
|
+
count: included.length,
|
|
6227
|
+
model
|
|
6228
|
+
};
|
|
6229
|
+
}
|
|
6230
|
+
function parseBullets(text, max = 6) {
|
|
6231
|
+
const result = [];
|
|
6232
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6233
|
+
for (const raw of text.split("\n")) {
|
|
6234
|
+
const line = raw.trim();
|
|
6235
|
+
if (!line) continue;
|
|
6236
|
+
if (line.startsWith("```")) continue;
|
|
6237
|
+
if (line.length > 320) continue;
|
|
6238
|
+
const m = line.match(/^([-*•]|\d+[.)])\s+(.*)$/);
|
|
6239
|
+
if (!m) continue;
|
|
6240
|
+
const stripped = m[2].trim().replace(/`/g, "").replace(/\*\*/g, "").trim();
|
|
6241
|
+
if (!stripped) continue;
|
|
6242
|
+
const cleaned = stripAnsiAndControls(stripped).trim();
|
|
6243
|
+
if (!cleaned) continue;
|
|
6244
|
+
if (cleaned.length > 320) continue;
|
|
6245
|
+
const key = cleaned.toLowerCase();
|
|
6246
|
+
if (seen.has(key)) continue;
|
|
6247
|
+
seen.add(key);
|
|
6248
|
+
result.push(cleaned);
|
|
6249
|
+
if (result.length >= max) break;
|
|
6250
|
+
}
|
|
6251
|
+
return result;
|
|
6252
|
+
}
|
|
6253
|
+
function isInternalTitle(title) {
|
|
6254
|
+
const t = (title ?? "").trim();
|
|
6255
|
+
return t.startsWith("context (auto") || t.startsWith("timeline (auto") || t.startsWith("pool probe");
|
|
6256
|
+
}
|
|
6257
|
+
|
|
6258
|
+
// src/plugins/timeline.ts
|
|
6259
|
+
var DEFAULT_INTERVAL_MS = 3e5;
|
|
6260
|
+
var MIN_INTERVAL_MS = 1e4;
|
|
6261
|
+
var DEFAULT_MAX_ENTRIES = 8;
|
|
6262
|
+
var MAX_ENTRIES = 10;
|
|
6263
|
+
var PROMPT_TIMEOUT_MS = 9e4;
|
|
6264
|
+
var SYSTEM = `You are the session timeline summarizer. You receive a slice of a coding session transcript.
|
|
6265
|
+
Write 1-8 timeline entries describing the work as tasks, features, fixes, or stages.
|
|
6266
|
+
|
|
6267
|
+
Rules:
|
|
6268
|
+
- One entry per line, each starting with "- ".
|
|
6269
|
+
- Each entry is 6-16 words, past tense, plain text.
|
|
6270
|
+
- Cover each distinct task in the slice. Do not drop a provider, feature, fix, config, docs, or test change; merge only exact duplicates.
|
|
6271
|
+
- 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.
|
|
6272
|
+
- 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.
|
|
6273
|
+
- Never use these words in an entry: tag, alias, override, flag, probe, endpoint, config key, catalog, file, line, count.
|
|
6274
|
+
- For configuration plumbing, write the purpose: "Config: silenced warnings for unused providers", not "declared empty overrides".
|
|
6275
|
+
- Use a short area prefix when it clarifies: "Pool:", "Providers:", "Config:", "Docs:", "Tests:", "Fix:", "Feature:".
|
|
6276
|
+
- 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.
|
|
6277
|
+
- Do not include counts of slots, providers, tests, lines, or files.
|
|
6278
|
+
- Omit internal housekeeping: builds, dist rebuilds, test-file additions, formatting, and dependency churn. If the slice only contains housekeeping, output nothing.
|
|
6279
|
+
- Describe what was DONE or DECIDED. Never write "User asked...", "Assistant said...", "Discussed...", "Reviewed...", "Explained...", or "Considered...".
|
|
6280
|
+
- Merge related changes into one stage; do not repeat the same task in different words.
|
|
6281
|
+
- No markdown: no bold, italics, backticks, or code blocks.
|
|
6282
|
+
- 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.
|
|
6283
|
+
- If the slice contains no substantive work or decision, output nothing.
|
|
6284
|
+
- Output only the entries. No headings, no preamble, no closing sentence.
|
|
6285
|
+
|
|
6286
|
+
Example output:
|
|
6287
|
+
- Pool: added bucket-based routing for model groups.
|
|
6288
|
+
- Providers: added Gemini 3.5 Flash after the previous model timed out.
|
|
6289
|
+
- Providers: added Cloudflare Workers AI with a tool-compatible model.
|
|
6290
|
+
- Config: silenced warnings for unused providers.
|
|
6291
|
+
- Config: standardized provider key naming and added Groq and Google.
|
|
6292
|
+
- Docs: aligned the routing documentation with the implemented behavior.
|
|
6293
|
+
|
|
6294
|
+
Not this:
|
|
6295
|
+
- Config: declared empty OpenRouter overrides to silence warnings.
|
|
6296
|
+
- Providers: added a Google API key alias after Gemini 2.5-flash closure.
|
|
6297
|
+
- Config: standardized provider keys to _KEY-only naming.
|
|
6298
|
+
- Pool: reordered slots after tagging pool entries.
|
|
6299
|
+
- Pool: added bucket support and kept tags in pool.
|
|
6300
|
+
- Pool: standardized slots on opencode catalog.
|
|
6301
|
+
- Providers: added Gemini 3.5 Flash and a Google API key alias.
|
|
6302
|
+
|
|
6303
|
+
This:
|
|
6304
|
+
- Config: silenced warnings for unused providers.
|
|
6305
|
+
- Providers: replaced the retired Gemini 2.5 Flash with an available model.
|
|
6306
|
+
- Config: standardized provider key naming.
|
|
6307
|
+
- Pool: moved shared-window models to the end.
|
|
6308
|
+
- Pool: added named bucket support for model groups.
|
|
6309
|
+
- Pool: moved shared-window models to the end.
|
|
6310
|
+
- Providers: replaced the retired Gemini 2.5 Flash with Gemini 3.5 Flash.
|
|
6311
|
+
|
|
6312
|
+
Your entire response must consist only of lines beginning with "- ".`;
|
|
6313
|
+
function timelineHooks(input, options) {
|
|
6314
|
+
const { client, directory } = input;
|
|
6315
|
+
const opts = options ?? {};
|
|
6316
|
+
const config = typeof opts.timeline === "object" && opts.timeline !== null ? opts.timeline : void 0;
|
|
6317
|
+
const disabled = opts.timeline === false || config?.enabled === false || process.env.LACODE_TIMELINE === "0";
|
|
6318
|
+
const SERVICE5 = "timeline";
|
|
6319
|
+
const poolState = state(options);
|
|
6320
|
+
const log = (level, message, extra) => client.app.log({ body: { service: SERVICE5, level, message, extra } }).catch(() => {
|
|
6321
|
+
});
|
|
6322
|
+
const debug = (...args) => {
|
|
6323
|
+
if (process.env.LACODE_TIMELINE_DEBUG === "1") console.error("[timeline]", ...args);
|
|
6324
|
+
};
|
|
6325
|
+
const rawInterval = Number(process.env.LACODE_TIMELINE_INTERVAL_MS ?? config?.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
6326
|
+
const intervalMs = Number.isFinite(rawInterval) ? Math.max(MIN_INTERVAL_MS, rawInterval) : DEFAULT_INTERVAL_MS;
|
|
6327
|
+
const rawMaxEntries = Number(config?.maxEntries ?? DEFAULT_MAX_ENTRIES);
|
|
6328
|
+
const maxEntries = Number.isFinite(rawMaxEntries) ? Math.min(MAX_ENTRIES, Math.max(1, Math.trunc(rawMaxEntries))) : DEFAULT_MAX_ENTRIES;
|
|
6329
|
+
const shared = globalThis[/* @__PURE__ */ Symbol.for("lacode:timeline:state")] ??= {
|
|
6330
|
+
sessions: /* @__PURE__ */ new Map(),
|
|
6331
|
+
owned: /* @__PURE__ */ new Set(),
|
|
6332
|
+
reprocessing: /* @__PURE__ */ new Set()
|
|
6333
|
+
};
|
|
6334
|
+
const sessions = shared.sessions;
|
|
6335
|
+
const owned = shared.owned;
|
|
6336
|
+
const reprocessing = shared.reprocessing;
|
|
6337
|
+
const MAX_REPROCESS_SLICES = 100;
|
|
6338
|
+
let disposed = false;
|
|
6339
|
+
const promptModel = (raw) => {
|
|
6340
|
+
if (typeof raw !== "string" || !raw.includes("/")) return void 0;
|
|
6341
|
+
const { provider, id } = splitModel(raw);
|
|
6342
|
+
if (!id) return void 0;
|
|
6343
|
+
return { providerID: provider, modelID: id };
|
|
6344
|
+
};
|
|
6345
|
+
const stateFor = (id) => {
|
|
6346
|
+
let state2 = sessions.get(id);
|
|
6347
|
+
if (!state2) {
|
|
6348
|
+
state2 = {
|
|
6349
|
+
running: false,
|
|
6350
|
+
pending: false,
|
|
6351
|
+
failures: 0,
|
|
6352
|
+
cursor: readEntries(id, 1)[0]?.through
|
|
6353
|
+
};
|
|
6354
|
+
sessions.set(id, state2);
|
|
6355
|
+
while (sessions.size > 500) {
|
|
6356
|
+
let evicted = false;
|
|
6357
|
+
for (const [key, st] of sessions) {
|
|
6358
|
+
if (key === id) continue;
|
|
6359
|
+
if (!st.timer && !st.running) {
|
|
6360
|
+
sessions.delete(key);
|
|
6361
|
+
evicted = true;
|
|
6362
|
+
break;
|
|
6363
|
+
}
|
|
6364
|
+
}
|
|
6365
|
+
if (!evicted) break;
|
|
6366
|
+
}
|
|
6367
|
+
}
|
|
6368
|
+
return state2;
|
|
6369
|
+
};
|
|
6370
|
+
const note = (id) => {
|
|
6371
|
+
if (disabled || disposed) return;
|
|
6372
|
+
const state2 = stateFor(id);
|
|
6373
|
+
if (state2.root === false) return;
|
|
6374
|
+
state2.pending = true;
|
|
6375
|
+
if (state2.timer || state2.running) return;
|
|
6376
|
+
schedule(id, state2);
|
|
6377
|
+
};
|
|
6378
|
+
function schedule(id, state2) {
|
|
6379
|
+
if (state2.timer) clearTimeout(state2.timer);
|
|
6380
|
+
state2.timer = setTimeout(() => {
|
|
6381
|
+
state2.timer = void 0;
|
|
6382
|
+
void tick(id, state2);
|
|
6383
|
+
}, intervalMs);
|
|
6384
|
+
state2.timer.unref?.();
|
|
6385
|
+
}
|
|
6386
|
+
async function summarize(id, title, excerptText, prior, abort) {
|
|
6387
|
+
if (abort?.aborted) return { ok: false, bullets: [], error: "aborted" };
|
|
6388
|
+
let slot;
|
|
6389
|
+
try {
|
|
6390
|
+
init(poolState, (m) => log("warn", m));
|
|
6391
|
+
const snap = snapshot(poolState);
|
|
6392
|
+
slot = pickSlot(poolState, snap);
|
|
6393
|
+
} catch (error) {
|
|
6394
|
+
log("warn", String(error));
|
|
6395
|
+
return { ok: false, bullets: [], error: String(error) };
|
|
6396
|
+
}
|
|
6397
|
+
const model = promptModel(slot?.model);
|
|
6398
|
+
if (!model) {
|
|
6399
|
+
log("warn", "timeline: slot model cannot be split into provider/model", { model: slot?.model });
|
|
6400
|
+
return { ok: false, bullets: [], error: "slot model cannot be split into provider/model" };
|
|
6401
|
+
}
|
|
6402
|
+
const created = await client.session.create({
|
|
6403
|
+
body: { title: "timeline (auto)" },
|
|
6404
|
+
query: { directory }
|
|
6405
|
+
});
|
|
6406
|
+
const target = created?.data?.id;
|
|
6407
|
+
if (!target) {
|
|
6408
|
+
return { ok: false, bullets: [], error: "session create returned no id" };
|
|
6409
|
+
}
|
|
6410
|
+
const claimKey2 = claimKey(target, "timeline");
|
|
6411
|
+
let claimed = false;
|
|
6412
|
+
owned.add(target);
|
|
6413
|
+
let abortListener;
|
|
6414
|
+
try {
|
|
6415
|
+
claimed = true;
|
|
6416
|
+
acquire(poolState, "context", slot, target, claimKey2, target);
|
|
6417
|
+
const prompt = [
|
|
6418
|
+
`Session: ${title}`,
|
|
6419
|
+
...prior.length > 0 ? ["Existing timeline (do not repeat):", ...prior.map((entry) => `- ${entry.text}`)] : [],
|
|
6420
|
+
"Transcript slice:",
|
|
6421
|
+
excerptText
|
|
6422
|
+
].join("\n");
|
|
6423
|
+
if (abort) {
|
|
6424
|
+
abortListener = () => {
|
|
6425
|
+
void client.session.abort({ path: { id: target }, query: { directory } }).catch(() => {
|
|
6426
|
+
});
|
|
6427
|
+
};
|
|
6428
|
+
abort.addEventListener("abort", abortListener);
|
|
6429
|
+
}
|
|
6430
|
+
let timer;
|
|
6431
|
+
let timedOut = false;
|
|
6432
|
+
let result;
|
|
6433
|
+
try {
|
|
6434
|
+
const answer = client.session.prompt({
|
|
6435
|
+
path: { id: target },
|
|
6436
|
+
query: { directory },
|
|
6437
|
+
body: {
|
|
6438
|
+
model,
|
|
6439
|
+
tools: { "*": false },
|
|
6440
|
+
system: SYSTEM,
|
|
6441
|
+
parts: [{ type: "text", text: prompt }]
|
|
6442
|
+
}
|
|
6443
|
+
}).then(
|
|
6444
|
+
(value) => value,
|
|
6445
|
+
(error) => ({ error })
|
|
6446
|
+
);
|
|
6447
|
+
const deadline = new Promise((resolve5) => {
|
|
6448
|
+
timer = setTimeout(() => {
|
|
6449
|
+
timedOut = true;
|
|
6450
|
+
void client.session.abort({ path: { id: target }, query: { directory } }).catch(() => {
|
|
6451
|
+
});
|
|
6452
|
+
resolve5(void 0);
|
|
6453
|
+
}, PROMPT_TIMEOUT_MS);
|
|
6454
|
+
});
|
|
6455
|
+
result = await Promise.race([answer, deadline]);
|
|
6456
|
+
} finally {
|
|
6457
|
+
if (timer) clearTimeout(timer);
|
|
6458
|
+
}
|
|
6459
|
+
if (abort?.aborted) {
|
|
6460
|
+
return { ok: false, bullets: [], error: "aborted" };
|
|
6461
|
+
}
|
|
6462
|
+
const failure = result?.error ?? result?.data?.info?.error;
|
|
6463
|
+
if (timedOut || failure !== void 0) {
|
|
6464
|
+
return { ok: false, bullets: [], error: timedOut ? "timed out" : String(failure) };
|
|
6465
|
+
} else {
|
|
6466
|
+
const text = (result?.data?.parts ?? []).map((part) => part.type === "text" ? part.text ?? "" : "").join("\n");
|
|
6467
|
+
const bullets = parseBullets(text, maxEntries);
|
|
6468
|
+
if (bullets.length > 0) {
|
|
6469
|
+
return { ok: true, bullets };
|
|
6470
|
+
} else if (text.trim() === "") {
|
|
6471
|
+
return { ok: true, bullets: [] };
|
|
6472
|
+
} else {
|
|
6473
|
+
if (process.env.LACODE_TIMELINE_DEBUG === "1") {
|
|
6474
|
+
log("no bullet lines", { sessionID: id, text: text.slice(0, 300) });
|
|
6475
|
+
}
|
|
6476
|
+
return { ok: false, bullets: [], error: "no bullet lines" };
|
|
6477
|
+
}
|
|
6478
|
+
}
|
|
6479
|
+
} finally {
|
|
6480
|
+
if (abort && abortListener) abort.removeEventListener("abort", abortListener);
|
|
6481
|
+
if (claimed && claimKey2) release(poolState, claimKey2);
|
|
6482
|
+
await client.session.delete({ path: { id: target }, query: { directory } }).catch(() => {
|
|
6483
|
+
});
|
|
6484
|
+
owned.delete(target);
|
|
6485
|
+
}
|
|
6486
|
+
}
|
|
6487
|
+
async function tick(id, state2) {
|
|
6488
|
+
if (disposed) return;
|
|
6489
|
+
if (reprocessing.has(id)) return;
|
|
6490
|
+
try {
|
|
6491
|
+
state2.running = true;
|
|
6492
|
+
state2.pending = false;
|
|
6493
|
+
let title;
|
|
6494
|
+
let info = {};
|
|
6495
|
+
if (state2.root === void 0) {
|
|
6496
|
+
info = await client.session.get({ path: { id }, query: { directory } }).then((result2) => result2?.data).catch(() => void 0);
|
|
6497
|
+
if (!info) {
|
|
6498
|
+
sessions.delete(id);
|
|
6499
|
+
return;
|
|
6500
|
+
}
|
|
6501
|
+
title = info.title;
|
|
6502
|
+
state2.root = !info.parentID && !isInternalTitle(info.title);
|
|
6503
|
+
if (!state2.root) {
|
|
6504
|
+
sessions.delete(id);
|
|
6505
|
+
return;
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
const response = await client.session.messages({ path: { id }, query: { directory } });
|
|
6509
|
+
const messages = response?.data ?? [];
|
|
6510
|
+
const excerpt = buildExcerpt(messages, state2.cursor);
|
|
6511
|
+
if (!excerpt.text) {
|
|
6512
|
+
return;
|
|
6513
|
+
}
|
|
6514
|
+
const result = await summarize(id, info.title ?? "untitled", excerpt.text, readEntries(id, 4));
|
|
6515
|
+
if (result.ok) {
|
|
6516
|
+
if (result.bullets.length) appendEntries(id, result.bullets, Date.now(), excerpt.next);
|
|
6517
|
+
state2.cursor = excerpt.next;
|
|
6518
|
+
state2.failures = 0;
|
|
6519
|
+
} else {
|
|
6520
|
+
state2.failures++;
|
|
6521
|
+
log("summarize failed", { sessionID: id, error: result.error });
|
|
6522
|
+
}
|
|
6523
|
+
} catch (error) {
|
|
6524
|
+
debug("tick failed", { sessionID: id, error });
|
|
6525
|
+
} finally {
|
|
6526
|
+
state2.running = false;
|
|
6527
|
+
if (sessions.get(id) === state2 && state2.pending && !disabled && !disposed && state2.root !== false) {
|
|
6528
|
+
schedule(id, state2);
|
|
6529
|
+
}
|
|
6530
|
+
}
|
|
6531
|
+
}
|
|
6532
|
+
async function reprocess(id, abort) {
|
|
6533
|
+
if (reprocessing.has(id)) return "A timeline reprocess is already running for this session.";
|
|
6534
|
+
if (sessions.get(id)?.running) return "The timeline is currently summarizing this session; try again in a moment.";
|
|
6535
|
+
reprocessing.add(id);
|
|
6536
|
+
const targetFile = timelineFile(id);
|
|
6537
|
+
const temp = targetFile + ".reprocess." + process.pid + "." + Date.now() + ".tmp";
|
|
6538
|
+
let cursor;
|
|
6539
|
+
let entries = 0;
|
|
6540
|
+
let calls = 0;
|
|
6541
|
+
try {
|
|
6542
|
+
const infoRes = await client.session.get({ path: { id }, query: { directory } });
|
|
6543
|
+
const info = infoRes?.data;
|
|
6544
|
+
if (!info) return "Session not found.";
|
|
6545
|
+
const msgRes = await client.session.messages({ path: { id }, query: { directory } });
|
|
6546
|
+
const messages = msgRes?.data ?? [];
|
|
6547
|
+
writeFileSync8(temp, "");
|
|
6548
|
+
for (let i = 0; i < MAX_REPROCESS_SLICES; i++) {
|
|
6549
|
+
if (abort?.aborted) {
|
|
6550
|
+
try {
|
|
6551
|
+
rmSync3(temp, { force: true });
|
|
6552
|
+
} catch {
|
|
6553
|
+
}
|
|
6554
|
+
return `Aborted after ${calls} summarizer calls; previous timeline kept.`;
|
|
6555
|
+
}
|
|
6556
|
+
const excerpt = buildExcerpt(messages, cursor);
|
|
6557
|
+
if (!excerpt.text) break;
|
|
6558
|
+
const prior = readEntriesAt(temp, 4);
|
|
6559
|
+
const result = await summarize(id, info.title ?? "untitled", excerpt.text, prior, abort);
|
|
6560
|
+
calls++;
|
|
6561
|
+
if (!result.ok) {
|
|
6562
|
+
try {
|
|
6563
|
+
rmSync3(temp, { force: true });
|
|
6564
|
+
} catch {
|
|
6565
|
+
}
|
|
6566
|
+
return `Stopped after ${calls} summarizer calls: ${result.error}. Previous timeline kept.`;
|
|
6567
|
+
}
|
|
6568
|
+
if (result.bullets.length) {
|
|
6569
|
+
entries += appendEntriesAt(temp, result.bullets, Date.now(), excerpt.next);
|
|
6570
|
+
}
|
|
6571
|
+
cursor = excerpt.next;
|
|
6572
|
+
}
|
|
6573
|
+
if (abort?.aborted) {
|
|
6574
|
+
try {
|
|
6575
|
+
rmSync3(temp, { force: true });
|
|
6576
|
+
} catch {
|
|
6577
|
+
}
|
|
6578
|
+
return `Aborted after ${calls} summarizer calls; previous timeline kept.`;
|
|
6579
|
+
}
|
|
6580
|
+
if (entries === 0) {
|
|
6581
|
+
try {
|
|
6582
|
+
rmSync3(temp, { force: true });
|
|
6583
|
+
} catch {
|
|
6584
|
+
}
|
|
6585
|
+
return "No entries generated; previous timeline kept.";
|
|
6586
|
+
}
|
|
6587
|
+
renameSync6(temp, targetFile);
|
|
6588
|
+
const state2 = sessions.get(id);
|
|
6589
|
+
if (state2) {
|
|
6590
|
+
state2.cursor = cursor;
|
|
6591
|
+
state2.failures = 0;
|
|
6592
|
+
state2.pending = false;
|
|
6593
|
+
}
|
|
6594
|
+
return `Rebuilt ${messages.length} messages into ${entries} entries with ${calls} summarizer calls.`;
|
|
6595
|
+
} catch (error) {
|
|
6596
|
+
try {
|
|
6597
|
+
rmSync3(temp, { force: true });
|
|
6598
|
+
} catch {
|
|
6599
|
+
}
|
|
6600
|
+
return `Reprocess failed: ${String(error)}. Previous timeline kept.`;
|
|
6601
|
+
} finally {
|
|
6602
|
+
reprocessing.delete(id);
|
|
6603
|
+
}
|
|
6604
|
+
}
|
|
6605
|
+
const timelineReprocess = tool2({
|
|
6606
|
+
description: "Rebuild the current session's entire timeline from its transcript, replacing the existing entries. Used by /reprocess-timeline.",
|
|
6607
|
+
args: {},
|
|
6608
|
+
execute: async (_args, ctx) => {
|
|
6609
|
+
const output = await reprocess(ctx.sessionID, ctx.abort);
|
|
6610
|
+
return { title: "Timeline reprocess", output };
|
|
6611
|
+
}
|
|
6612
|
+
});
|
|
6613
|
+
return {
|
|
6614
|
+
event: async ({ event }) => {
|
|
6615
|
+
try {
|
|
6616
|
+
switch (event.type) {
|
|
6617
|
+
case "message.updated": {
|
|
6618
|
+
const id = event.properties.info?.sessionID;
|
|
6619
|
+
if (!id) return;
|
|
6620
|
+
if (owned.has(id)) return;
|
|
6621
|
+
const role = event.properties.info?.role;
|
|
6622
|
+
if (role === "user" || role === "assistant") note(id);
|
|
6623
|
+
return;
|
|
6624
|
+
}
|
|
6625
|
+
case "message.part.updated": {
|
|
6626
|
+
const id = event.properties.part?.sessionID;
|
|
6627
|
+
if (!id) return;
|
|
6628
|
+
if (owned.has(id)) return;
|
|
6629
|
+
const type = event.properties.part?.type;
|
|
6630
|
+
if (type === "text" || type === "tool") note(id);
|
|
6631
|
+
return;
|
|
6632
|
+
}
|
|
6633
|
+
case "session.created": {
|
|
6634
|
+
const info = event.properties.info;
|
|
6635
|
+
if (!info) return;
|
|
6636
|
+
if (isInternalTitle(info.title)) owned.add(info.id);
|
|
6637
|
+
return;
|
|
6638
|
+
}
|
|
6639
|
+
case "session.deleted": {
|
|
6640
|
+
const id = event.properties.info?.id;
|
|
6641
|
+
if (!id) return;
|
|
6642
|
+
const state2 = sessions.get(id);
|
|
6643
|
+
if (state2?.timer) {
|
|
6644
|
+
clearTimeout(state2.timer);
|
|
6645
|
+
state2.timer = void 0;
|
|
6646
|
+
}
|
|
6647
|
+
sessions.delete(id);
|
|
6648
|
+
removeEntries(id);
|
|
6649
|
+
owned.delete(id);
|
|
6650
|
+
return;
|
|
6651
|
+
}
|
|
6652
|
+
default:
|
|
6653
|
+
return;
|
|
6654
|
+
}
|
|
6655
|
+
} catch (error) {
|
|
6656
|
+
debug("event failed", error);
|
|
6657
|
+
}
|
|
6658
|
+
},
|
|
6659
|
+
dispose: async () => {
|
|
6660
|
+
disposed = true;
|
|
6661
|
+
for (const state2 of sessions.values()) {
|
|
6662
|
+
if (state2.timer) {
|
|
6663
|
+
clearTimeout(state2.timer);
|
|
6664
|
+
state2.timer = void 0;
|
|
6665
|
+
}
|
|
6666
|
+
}
|
|
6667
|
+
reprocessing.clear();
|
|
6668
|
+
},
|
|
6669
|
+
tool: { timeline_reprocess: timelineReprocess }
|
|
6670
|
+
};
|
|
6671
|
+
}
|
|
6672
|
+
|
|
5754
6673
|
// src/index.ts
|
|
5755
6674
|
var inertHooks = {
|
|
5756
6675
|
config: async () => {
|
|
@@ -5786,7 +6705,7 @@ var LaCode = (async (input, options) => {
|
|
|
5786
6705
|
return void 0;
|
|
5787
6706
|
}
|
|
5788
6707
|
};
|
|
5789
|
-
const agentSet = buildAgentSet();
|
|
6708
|
+
const agentSet = buildAgentSet(resolveModels({ models: opts.models, modelsFile: opts.modelsFile }).slots.length);
|
|
5790
6709
|
const pool = await settled("agent-pool", () => agentPoolHooks(input, options)) ?? inertHooks;
|
|
5791
6710
|
const autoupdate = await settled("context-autoupdate", () => contextAutoUpdateHooks(input, options)) ?? inertHooks;
|
|
5792
6711
|
const notifier = await settled("notify", () => notifyHooks(input, options)) ?? inertHooks;
|
|
@@ -5794,6 +6713,7 @@ var LaCode = (async (input, options) => {
|
|
|
5794
6713
|
const feedback = await settled("feedback", () => feedbackHooks(input, options)) ?? inertHooks;
|
|
5795
6714
|
const omniroute = await settled("omniroute", () => omniRouteHooks(input, options)) ?? inertHooks;
|
|
5796
6715
|
const imagepaste = await settled("image-paste", () => imagePasteHooks(input, opts.imagePaste)) ?? inertHooks;
|
|
6716
|
+
const timeline = await settled("timeline", () => timelineHooks(input, options)) ?? inertHooks;
|
|
5797
6717
|
const config = async (cfg) => {
|
|
5798
6718
|
try {
|
|
5799
6719
|
const injected = applyAgents(cfg, agentSet);
|
|
@@ -5817,6 +6737,8 @@ var LaCode = (async (input, options) => {
|
|
|
5817
6737
|
autoupdate.event?.(payload).catch(() => {
|
|
5818
6738
|
}),
|
|
5819
6739
|
notifier.event?.(payload).catch(() => {
|
|
6740
|
+
}),
|
|
6741
|
+
timeline.event?.(payload).catch(() => {
|
|
5820
6742
|
})
|
|
5821
6743
|
]);
|
|
5822
6744
|
};
|
|
@@ -5827,7 +6749,7 @@ var LaCode = (async (input, options) => {
|
|
|
5827
6749
|
// only thing that routes task calls, so there is nothing to merge them with.
|
|
5828
6750
|
"tool.execute.before": pool["tool.execute.before"],
|
|
5829
6751
|
"tool.execute.after": pool["tool.execute.after"],
|
|
5830
|
-
tool: pool.tool,
|
|
6752
|
+
tool: { ...pool.tool ?? {}, ...timeline.tool ?? {} },
|
|
5831
6753
|
"command.execute.before": imagepaste["command.execute.before"],
|
|
5832
6754
|
dispose: async () => {
|
|
5833
6755
|
await pool.dispose?.();
|
|
@@ -5837,6 +6759,7 @@ var LaCode = (async (input, options) => {
|
|
|
5837
6759
|
await feedback.dispose?.();
|
|
5838
6760
|
await omniroute.dispose?.();
|
|
5839
6761
|
await imagepaste.dispose?.();
|
|
6762
|
+
await timeline.dispose?.();
|
|
5840
6763
|
},
|
|
5841
6764
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
5842
6765
|
if (!Array.isArray(output?.system)) return;
|