@lawaty/lacode 2.3.0-beta.4 → 2.3.0-beta.6
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 +39 -0
- package/README.md +13 -5
- package/dist/agents.js +1 -0
- package/dist/commands.js +6 -3
- package/dist/index.js +421 -161
- package/dist/lacode.js +1 -0
- package/dist/pool.js +152 -68
- package/dist/tunnel.js +1 -0
- package/docs/CONFIGURATION.md +15 -4
- package/docs/USER-STORIES.md +19 -2
- package/package.json +1 -1
- package/presets/README.md +35 -16
- package/presets/free-tier.json +17 -9
- package/scripts/pool-selection.mjs +316 -13
- package/scripts/refresh-pool.mjs +211 -94
- package/scripts/score-models.mjs +123 -17
package/dist/index.js
CHANGED
|
@@ -39,14 +39,16 @@ var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
|
|
|
39
39
|
var UNAVAILABLE_REASON = "endpoint-unavailable";
|
|
40
40
|
var PROVIDER_DEGRADED_REASON = "provider-degraded";
|
|
41
41
|
var PROVIDER_DEGRADED_COOLDOWN_MS = 30 * 60 * 1e3;
|
|
42
|
+
var SCAN_KEPT_REASON = "scan-kept";
|
|
42
43
|
var PROVIDER_DEGRADED_WINDOW_MS = 10 * 60 * 1e3;
|
|
43
44
|
var PROVIDER_DEGRADED_MIN_MODELS = 2;
|
|
44
45
|
var TRANSIENT_DEGRADATION_REASONS = /^(?:no answer|no-answer|hang|timeout|timed out|endpoint-unavailable|provider-degraded|empty response|network|server)$/i;
|
|
45
46
|
var FLAT_OUTAGE_REASONS = /* @__PURE__ */ new Set([UNAVAILABLE_REASON, PROVIDER_DEGRADED_REASON]);
|
|
46
47
|
var MAX_WEIGHT = 100;
|
|
48
|
+
var MAX_CEILING = MAX_WEIGHT;
|
|
47
49
|
var MIN_SLOT_CONTEXT = 1e5;
|
|
48
50
|
var MIN_SLOT_TPM = 2e4;
|
|
49
|
-
var slotCeiling = (slot) => slot.weight;
|
|
51
|
+
var slotCeiling = (slot) => slot.ceiling ?? slot.weight;
|
|
50
52
|
var slotBucket = (slot) => slot.bucket ?? slot.model;
|
|
51
53
|
var NOTIFY_DEFAULT_ON_PROVIDERS = /* @__PURE__ */ new Set(["opencode", "opencode-go"]);
|
|
52
54
|
var notifyDefault = (model) => {
|
|
@@ -66,7 +68,7 @@ var QUOTA_COOLDOWN_BASE_MS = 15 * 60 * 1e3;
|
|
|
66
68
|
var QUOTA_COOLDOWN_MAX_MS = 6 * 60 * 60 * 1e3;
|
|
67
69
|
var STRIKE_DECAY_MS = 30 * 60 * 1e3;
|
|
68
70
|
var STATED_COOLDOWN_MAX_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
69
|
-
var isQuotaReason = (reason) => /quota|limit|rate|too-many|overloaded|429/i.test(reason);
|
|
71
|
+
var isQuotaReason = (reason) => /quota|limit|rate|too-many|overloaded|budget|429/i.test(reason);
|
|
70
72
|
var MODEL_MISSING_REASON = "model-missing";
|
|
71
73
|
var MODEL_MISSING_COOLDOWN_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
72
74
|
var AUTH_FAILURE_REASON = "auth-failed";
|
|
@@ -75,6 +77,16 @@ var MODEL_MISSING_PATTERN = /model\s+(?:not\s+found|does\s+not\s+exist|is\s+not\
|
|
|
75
77
|
var isModelMissingText = (text) => MODEL_MISSING_PATTERN.test(text);
|
|
76
78
|
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;
|
|
77
79
|
var isAuthFailureText = (text) => AUTH_FAILURE_PATTERN.test(text);
|
|
80
|
+
var ABORT_PATTERN = /MessageAbortedError|"name"\s*:\s*"[^"]*Abort/i;
|
|
81
|
+
var isAbortText = (text) => ABORT_PATTERN.test(text);
|
|
82
|
+
var INVALID_REQUEST_REASON = "invalid-request";
|
|
83
|
+
var INVALID_REQUEST_COOLDOWN_MS = MODEL_MISSING_COOLDOWN_MS;
|
|
84
|
+
var UNSUPPORTED_PARAMETER_PATTERN = /not\s+a\s+supported\s+value|unsupported\s+(?:value|parameter|field)|(?:invalid|unknown)\s+(?:value|parameter)\s+for|is\s+not\s+supported/i;
|
|
85
|
+
var isUnsupportedParameterText = (text) => UNSUPPORTED_PARAMETER_PATTERN.test(text);
|
|
86
|
+
var TRANSIENT_UPSTREAM_REASON = "upstream-transient";
|
|
87
|
+
var TRANSIENT_UPSTREAM_COOLDOWN_MS = 60 * 1e3;
|
|
88
|
+
var TRANSIENT_UPSTREAM_PATTERN = /reasoning_display/i;
|
|
89
|
+
var isTransientUpstreamText = (text) => TRANSIENT_UPSTREAM_PATTERN.test(text);
|
|
78
90
|
var BASES = ["explore", "implement", "context"];
|
|
79
91
|
var LEARNED_TPM_FILE = "learned-tpm.json";
|
|
80
92
|
var PRESETS_DIR = join2(PACKAGE_ROOT, "presets");
|
|
@@ -101,64 +113,100 @@ var coerceNotify = (raw, at, model, problems) => {
|
|
|
101
113
|
problems.push(`${at} (${model}): notify must be a boolean or one of on/off, got ${JSON.stringify(raw)}; keeping the provider default`);
|
|
102
114
|
return notifyDefault(model);
|
|
103
115
|
};
|
|
116
|
+
var FALLBACK_MARKER = /^(?:#\s*-*\s*fallback(?:\s+slots)?\s*-*|\[fallback\])$/i;
|
|
117
|
+
var isFallbackMarker = (line) => FALLBACK_MARKER.test(String(line).trim());
|
|
118
|
+
function parseSlotEntry(entry, at, problems) {
|
|
119
|
+
const model = typeof entry?.model === "string" ? entry.model.trim() : "";
|
|
120
|
+
if (!model.includes("/")) {
|
|
121
|
+
problems.push(`${at}: model must be written provider/model-id, got "${model}"`);
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
124
|
+
const weight = readWeight(entry, at, model, problems);
|
|
125
|
+
const ceiling = readCeiling(entry, weight, at, model, problems);
|
|
126
|
+
const rawBucket = entry?.bucket;
|
|
127
|
+
const bucket = typeof rawBucket === "string" ? rawBucket.trim() : void 0;
|
|
128
|
+
if (rawBucket !== void 0 && !bucket) {
|
|
129
|
+
problems.push(`${at} (${model}): bucket must be a non-empty name; using the model id`);
|
|
130
|
+
}
|
|
131
|
+
const notify = coerceNotify(entry.notify, at, model, problems);
|
|
132
|
+
const rawContext = entry?.context;
|
|
133
|
+
let context;
|
|
134
|
+
if (rawContext !== void 0) {
|
|
135
|
+
const parsedContext = typeof rawContext === "number" ? rawContext : Number(rawContext);
|
|
136
|
+
if (Number.isFinite(parsedContext) && parsedContext > 0) context = parsedContext;
|
|
137
|
+
else problems.push(`${at} (${model}): context must be a positive number, got ${JSON.stringify(rawContext)}; ignoring it`);
|
|
138
|
+
}
|
|
139
|
+
if (context !== void 0 && context < MIN_SLOT_CONTEXT) {
|
|
140
|
+
problems.push(`${at} (${model}): context ${context} is below the ${MIN_SLOT_CONTEXT}-token minimum; dropping the slot`);
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
const rawTpm = entry?.tpm;
|
|
144
|
+
let tpm;
|
|
145
|
+
if (rawTpm !== void 0) {
|
|
146
|
+
const parsedTpm = typeof rawTpm === "number" ? rawTpm : Number(rawTpm);
|
|
147
|
+
if (Number.isFinite(parsedTpm) && parsedTpm > 0) tpm = parsedTpm;
|
|
148
|
+
else problems.push(`${at} (${model}): tpm must be a positive number, got ${JSON.stringify(rawTpm)}; ignoring it`);
|
|
149
|
+
}
|
|
150
|
+
if (tpm !== void 0 && tpm < MIN_SLOT_TPM) {
|
|
151
|
+
problems.push(`${at} (${model}): tpm ${tpm} is below the ${MIN_SLOT_TPM}-token/minute minimum; dropping the slot`);
|
|
152
|
+
return void 0;
|
|
153
|
+
}
|
|
154
|
+
return { index: 0, model, weight, ...ceiling === void 0 ? {} : { ceiling }, ...bucket ? { bucket } : {}, notify, ...context === void 0 ? {} : { context }, ...tpm === void 0 ? {} : { tpm } };
|
|
155
|
+
}
|
|
104
156
|
function parseSlots(parsed, origin) {
|
|
105
157
|
const problems = [];
|
|
106
|
-
const
|
|
107
|
-
|
|
158
|
+
const primaryEntries = parsed?.slots;
|
|
159
|
+
const fallbackEntries = parsed?.fallbackSlots;
|
|
160
|
+
if ((!Array.isArray(primaryEntries) || primaryEntries.length === 0) && (!Array.isArray(fallbackEntries) || fallbackEntries.length === 0)) {
|
|
108
161
|
return { slots: FALLBACK_SLOTS, problems: [`${origin} has no non-empty "slots" array; using built-in defaults`] };
|
|
109
162
|
}
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
problems.push(`${at} (${model}): context ${context} is below the ${MIN_SLOT_CONTEXT}-token minimum; dropping the slot`);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const rawTpm = entry?.tpm;
|
|
134
|
-
let tpm;
|
|
135
|
-
if (rawTpm !== void 0) {
|
|
136
|
-
const parsedTpm = typeof rawTpm === "number" ? rawTpm : Number(rawTpm);
|
|
137
|
-
if (Number.isFinite(parsedTpm) && parsedTpm > 0) tpm = parsedTpm;
|
|
138
|
-
else problems.push(`${at} (${model}): tpm must be a positive number, got ${JSON.stringify(rawTpm)}; ignoring it`);
|
|
139
|
-
}
|
|
140
|
-
if (tpm !== void 0 && tpm < MIN_SLOT_TPM) {
|
|
141
|
-
problems.push(`${at} (${model}): tpm ${tpm} is below the ${MIN_SLOT_TPM}-token/minute minimum; dropping the slot`);
|
|
142
|
-
return;
|
|
163
|
+
const parseList = (list, fallback2) => {
|
|
164
|
+
if (!Array.isArray(list)) return [];
|
|
165
|
+
const out = [];
|
|
166
|
+
list.forEach((entry, i) => {
|
|
167
|
+
const slot = parseSlotEntry(entry, `${fallback2 ? "fallback slot" : "slot"} ${i + 1}`, problems);
|
|
168
|
+
if (slot) out.push({ ...slot, ...fallback2 ? { fallback: true } : {} });
|
|
169
|
+
});
|
|
170
|
+
return out;
|
|
171
|
+
};
|
|
172
|
+
const primary = parseList(primaryEntries, false);
|
|
173
|
+
let fallback = parseList(fallbackEntries, true);
|
|
174
|
+
const primaryBuckets = new Set(primary.map((slot) => slotBucket(slot)));
|
|
175
|
+
const primaryModels = new Set(primary.map((slot) => slot.model));
|
|
176
|
+
fallback = fallback.filter((slot) => {
|
|
177
|
+
const shared = primaryModels.has(slot.model) || primaryBuckets.has(slotBucket(slot));
|
|
178
|
+
if (shared) {
|
|
179
|
+
problems.push(
|
|
180
|
+
`fallback slot (${slot.model}): shares a limit window with the primary pool (${slotBucket(slot)}); dropping it`
|
|
181
|
+
);
|
|
182
|
+
return false;
|
|
143
183
|
}
|
|
144
|
-
|
|
184
|
+
return true;
|
|
145
185
|
});
|
|
186
|
+
const slots = [...primary, ...fallback].map((slot, i) => ({ ...slot, index: i + 1 }));
|
|
146
187
|
if (slots.length > SLOTS_WARN_AT) {
|
|
147
188
|
problems.push(`${origin}: ${slots.length} slots is unusually large, but every entry is kept (the pool has no slot cap)`);
|
|
148
189
|
}
|
|
149
|
-
if (
|
|
150
|
-
problems.push(`${origin} yielded no usable slot; using built-in defaults`);
|
|
190
|
+
if (primary.length === 0) {
|
|
191
|
+
problems.push(`${origin} yielded no usable primary slot; using built-in defaults`);
|
|
151
192
|
return { slots: FALLBACK_SLOTS, problems };
|
|
152
193
|
}
|
|
153
194
|
return { slots, problems };
|
|
154
195
|
}
|
|
155
196
|
function parseTextSlots(text, origin) {
|
|
156
197
|
const entries = [];
|
|
198
|
+
const fallbackEntries = [];
|
|
199
|
+
let section = "primary";
|
|
157
200
|
for (const raw of text.split(/\r?\n/)) {
|
|
201
|
+
if (isFallbackMarker(raw)) {
|
|
202
|
+
section = "fallback";
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
158
205
|
const line = raw.split("#")[0].trim();
|
|
159
206
|
if (!line) continue;
|
|
160
207
|
const [model, ...rest] = line.split(/\s+/);
|
|
161
208
|
let weight;
|
|
209
|
+
let ceiling;
|
|
162
210
|
let bucket;
|
|
163
211
|
let notify;
|
|
164
212
|
let context;
|
|
@@ -168,6 +216,13 @@ function parseTextSlots(text, origin) {
|
|
|
168
216
|
if (notify === void 0) notify = token.slice("notify:".length);
|
|
169
217
|
continue;
|
|
170
218
|
}
|
|
219
|
+
if (token.startsWith("ceil:")) {
|
|
220
|
+
if (ceiling === void 0) {
|
|
221
|
+
const parsedCeiling = Number(token.slice("ceil:".length));
|
|
222
|
+
if (Number.isFinite(parsedCeiling) && parsedCeiling > 0) ceiling = parsedCeiling;
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
171
226
|
if (token.startsWith("c:")) {
|
|
172
227
|
if (context === void 0) {
|
|
173
228
|
const parsedContext = Number(token.slice(2));
|
|
@@ -188,9 +243,11 @@ function parseTextSlots(text, origin) {
|
|
|
188
243
|
weight = token;
|
|
189
244
|
}
|
|
190
245
|
}
|
|
191
|
-
|
|
246
|
+
const entry = { model, ...weight === void 0 ? {} : { weight }, ...ceiling === void 0 ? {} : { ceiling }, ...bucket === void 0 ? {} : { bucket }, ...notify === void 0 ? {} : { notify }, ...context === void 0 ? {} : { context }, ...tpm === void 0 ? {} : { tpm } };
|
|
247
|
+
if (section === "fallback") fallbackEntries.push(entry);
|
|
248
|
+
else entries.push(entry);
|
|
192
249
|
}
|
|
193
|
-
return parseSlots({ slots: entries }, origin);
|
|
250
|
+
return parseSlots({ slots: entries, ...fallbackEntries.length > 0 ? { fallbackSlots: fallbackEntries } : {} }, origin);
|
|
194
251
|
}
|
|
195
252
|
function parseSlotText(text, origin) {
|
|
196
253
|
if (text.trimStart().startsWith("{")) {
|
|
@@ -295,6 +352,20 @@ function readWeight(entry, at, model, problems) {
|
|
|
295
352
|
}
|
|
296
353
|
return weight;
|
|
297
354
|
}
|
|
355
|
+
function readCeiling(entry, weight, at, model, problems) {
|
|
356
|
+
const raw = entry?.ceiling;
|
|
357
|
+
if (raw === void 0) return void 0;
|
|
358
|
+
const ceiling = typeof raw === "number" ? raw : Number(raw);
|
|
359
|
+
if (!Number.isFinite(ceiling) || ceiling <= 0 || ceiling > MAX_CEILING) {
|
|
360
|
+
problems.push(`${at} (${model}): ceiling must be a number in (0, ${MAX_CEILING}], got ${JSON.stringify(raw)}; using the weight`);
|
|
361
|
+
return void 0;
|
|
362
|
+
}
|
|
363
|
+
if (ceiling < weight) {
|
|
364
|
+
problems.push(`${at} (${model}): ceiling ${ceiling} is below weight ${weight}; using the weight as the ceiling`);
|
|
365
|
+
return void 0;
|
|
366
|
+
}
|
|
367
|
+
return Math.floor(ceiling);
|
|
368
|
+
}
|
|
298
369
|
var UNAVAILABLE_PATTERN = /endpoint\s+is\s+unavailable/i;
|
|
299
370
|
var LIMIT_PATTERNS = [
|
|
300
371
|
/rate[\s_-]?limit/i,
|
|
@@ -302,6 +373,7 @@ var LIMIT_PATTERNS = [
|
|
|
302
373
|
/too many requests/i,
|
|
303
374
|
/\b429\b/,
|
|
304
375
|
/quota/i,
|
|
376
|
+
/budget\s+exhausted|api\s+budget/i,
|
|
305
377
|
/freelimiterror/i,
|
|
306
378
|
/overloaded/i,
|
|
307
379
|
UNAVAILABLE_PATTERN
|
|
@@ -345,7 +417,7 @@ function parseRateLimitHeaders(headers) {
|
|
|
345
417
|
}
|
|
346
418
|
return void 0;
|
|
347
419
|
}
|
|
348
|
-
var LIMIT_REASON_PATTERN = /rate[\s_-]?limit|usage limit|too many requests|quota|freelimiterror|overloaded|limits?[\s_-]?(?:reached|exceeded)/i;
|
|
420
|
+
var LIMIT_REASON_PATTERN = /rate[\s_-]?limit|usage limit|too many requests|quota|budget\s+exhausted|freelimiterror|overloaded|limits?[\s_-]?(?:reached|exceeded)/i;
|
|
349
421
|
var isLimitText = (text) => LIMIT_PATTERNS.some((re) => re.test(text)) || LIMIT_REACHED_PATTERN.test(text) && !CONTEXT_LIMIT_PATTERN.test(text);
|
|
350
422
|
var limitReason = (text) => {
|
|
351
423
|
if (!isLimitText(text)) return void 0;
|
|
@@ -435,7 +507,7 @@ function ensureSlots(s, onWarn) {
|
|
|
435
507
|
for (const problem of problems) onWarn(problem);
|
|
436
508
|
return slots;
|
|
437
509
|
}
|
|
438
|
-
var slotSignature = (origin, slots) => `${origin}\0${slots.map((slot) => `${slot.index}:${slot.model}:${slot.weight}:${slot.bucket ?? ""}:${slotNotify(slot) ? "1" : "0"}:${slot.tpm ?? ""}`).join("|")}`;
|
|
510
|
+
var slotSignature = (origin, slots) => `${origin}\0${slots.map((slot) => `${slot.index}:${slot.model}:${slot.weight}:${slot.ceiling ?? ""}:${slot.bucket ?? ""}:${slotNotify(slot) ? "1" : "0"}:${slot.tpm ?? ""}:${slot.fallback ? "f" : "p"}`).join("|")}`;
|
|
439
511
|
function reloadSlots(s, onWarn) {
|
|
440
512
|
const { slots, problems, origin } = resolveModels({ models: s.inlineModels, modelsFile: s.modelsFile });
|
|
441
513
|
const effective = applyLearnedTpm(s, slots, onWarn);
|
|
@@ -695,7 +767,7 @@ function readLimits(s) {
|
|
|
695
767
|
try {
|
|
696
768
|
const strike = JSON.parse(readFileSync2(path4, "utf8"));
|
|
697
769
|
if (typeof strike.until !== "number" || typeof strike.model !== "string") continue;
|
|
698
|
-
const countsAsStrike = strike.reason !== UNAVAILABLE_REASON;
|
|
770
|
+
const countsAsStrike = strike.reason !== UNAVAILABLE_REASON && strike.reason !== SCAN_KEPT_REASON;
|
|
699
771
|
if (strike.until > now) {
|
|
700
772
|
cooling.set(strike.model, strike);
|
|
701
773
|
if (countsAsStrike) memory.set(strike.model, strike);
|
|
@@ -849,52 +921,54 @@ function snapshot(s) {
|
|
|
849
921
|
}
|
|
850
922
|
var loadOf = (counts, slot) => counts.get(slot) ?? 0;
|
|
851
923
|
var underCeiling = (slot, counts) => loadOf(counts, slot.index) < slotCeiling(slot);
|
|
924
|
+
var fillPassIndex = (slot, counts) => Math.floor(loadOf(counts, slot.index) / Math.max(1, slot.weight));
|
|
925
|
+
var pickByPass = (group, counts, prefer) => {
|
|
926
|
+
const open = group.filter((slot) => underCeiling(slot, counts));
|
|
927
|
+
if (open.length === 0) return void 0;
|
|
928
|
+
const lowest = Math.min(...open.map((slot) => fillPassIndex(slot, counts)));
|
|
929
|
+
const atPass = open.filter((slot) => fillPassIndex(slot, counts) === lowest);
|
|
930
|
+
return prefer ? atPass.reduce(prefer, atPass[0]) : atPass[0];
|
|
931
|
+
};
|
|
852
932
|
function pickSlot(s, snap, neededContext) {
|
|
853
933
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
854
|
-
const
|
|
934
|
+
const primary = slots.filter((slot) => !slot.fallback);
|
|
935
|
+
const fallback = slots.filter((slot) => slot.fallback);
|
|
936
|
+
const usablePrimary = primary.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
937
|
+
const usable = usablePrimary.length > 0 ? usablePrimary : fallback.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
855
938
|
if (usable.length === 0) return void 0;
|
|
856
939
|
const effective = (slot) => Math.min(slot.context ?? Number.POSITIVE_INFINITY, slot.tpm ?? Number.POSITIVE_INFINITY);
|
|
940
|
+
const leastLoaded = (best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best;
|
|
857
941
|
if (neededContext === void 0) {
|
|
858
|
-
|
|
859
|
-
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
860
|
-
}
|
|
861
|
-
return usable.reduce((best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best, usable[0]);
|
|
942
|
+
return pickByPass(usable, snap.counts) ?? usable.reduce(leastLoaded, usable[0]);
|
|
862
943
|
}
|
|
863
944
|
const adequate = usable.filter((slot) => effective(slot) >= neededContext);
|
|
864
945
|
const adequateKnown = adequate.filter((slot) => Number.isFinite(effective(slot)));
|
|
865
946
|
const candidates = adequate.length > 0 ? adequate : usable;
|
|
947
|
+
const smallest = (best, slot) => Number.isFinite(effective(slot)) && effective(slot) < effective(best) ? slot : best;
|
|
866
948
|
if (adequateKnown.length === 0) {
|
|
867
949
|
const known = usable.filter((slot) => Number.isFinite(effective(slot)));
|
|
868
950
|
if (known.length > 0) {
|
|
869
951
|
const largest = Math.max(...known.map((slot) => effective(slot)));
|
|
870
952
|
const largestSlots = known.filter((slot) => effective(slot) === largest);
|
|
871
|
-
|
|
872
|
-
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
873
|
-
}
|
|
874
|
-
return largestSlots.reduce((best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best, largestSlots[0]);
|
|
953
|
+
return pickByPass(largestSlots, snap.counts) ?? largestSlots.reduce(leastLoaded, largestSlots[0]);
|
|
875
954
|
}
|
|
876
|
-
|
|
877
|
-
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
878
|
-
}
|
|
879
|
-
return candidates.reduce((best, slot) => loadOf(snap.counts, slot.index) < loadOf(snap.counts, best.index) ? slot : best, candidates[0]);
|
|
880
|
-
}
|
|
881
|
-
const underWeight = candidates.filter((slot) => loadOf(snap.counts, slot.index) < slot.weight);
|
|
882
|
-
if (underWeight.length > 0) {
|
|
883
|
-
const known = underWeight.filter((slot) => Number.isFinite(effective(slot)));
|
|
884
|
-
if (known.length === 0) return underWeight[0];
|
|
885
|
-
return known.reduce((best, slot) => effective(slot) < effective(best) ? slot : best, known[0]);
|
|
955
|
+
return pickByPass(candidates, snap.counts) ?? candidates.reduce(leastLoaded, candidates[0]);
|
|
886
956
|
}
|
|
887
|
-
return candidates
|
|
957
|
+
return pickByPass(candidates, snap.counts, smallest) ?? candidates.reduce(leastLoaded, candidates[0]);
|
|
888
958
|
}
|
|
889
959
|
function poolPass(s, snap, neededContext) {
|
|
890
960
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
891
|
-
const
|
|
961
|
+
const primary = slots.filter((slot) => !slot.fallback);
|
|
962
|
+
const fallback = slots.filter((slot) => slot.fallback);
|
|
963
|
+
const usablePrimary = primary.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
964
|
+
const usable = usablePrimary.length > 0 ? usablePrimary : fallback.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
892
965
|
if (usable.length === 0) return "none";
|
|
893
966
|
const effective = (slot) => Math.min(slot.context ?? Number.POSITIVE_INFINITY, slot.tpm ?? Number.POSITIVE_INFINITY);
|
|
894
967
|
const candidates = neededContext === void 0 ? usable : usable.filter((slot) => effective(slot) >= neededContext);
|
|
895
968
|
const pool = candidates.length > 0 ? candidates : usable;
|
|
896
|
-
|
|
897
|
-
return "overflow";
|
|
969
|
+
const open = pool.filter((slot) => underCeiling(slot, snap.counts));
|
|
970
|
+
if (open.length === 0) return "overflow";
|
|
971
|
+
return Math.min(...open.map((slot) => fillPassIndex(slot, snap.counts))) + 1;
|
|
898
972
|
}
|
|
899
973
|
function agentFor(base, slot) {
|
|
900
974
|
return `${base}-${slot}`;
|
|
@@ -2166,7 +2240,7 @@ Follow the rules for the file in your system prompt, then report what you change
|
|
|
2166
2240
|
Print one PASS/FAIL line per check below, with the exact fix for any FAIL.
|
|
2167
2241
|
|
|
2168
2242
|
1. Pool status \u2014 call the \`pool_status\` tool. Report the slot list, each
|
|
2169
|
-
slot's load against its
|
|
2243
|
+
slot's load against its ceiling, and which models
|
|
2170
2244
|
are cooling.
|
|
2171
2245
|
|
|
2172
2246
|
2. OmniRoute configured \u2014 inspect the \`@lawaty/lacode\` plugin options in the
|
|
@@ -2202,9 +2276,11 @@ The pool is ONE global slot table shared by every pooled base (explore, implemen
|
|
|
2202
2276
|
1. Establish the baseline. Read \`${USER_MODELS_TXT}\`. If it is absent, the pool is resolving from the bundled preset \`presets/free-tier.json\`; read that file to learn what is in effect. Call the \`pool_status\` tool only if you need live load or cooldown state.
|
|
2203
2277
|
|
|
2204
2278
|
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:
|
|
2205
|
-
- one slot per line: \`<provider>/<model-id> [weight]\`
|
|
2279
|
+
- one slot per line: \`<provider>/<model-id> [weight] [ceil:<max-parallel>] [c:<context>] [b:<bucket>]\`
|
|
2206
2280
|
- any number of slots; a model may appear on more than one line, and each line is its own slot with its own budget
|
|
2207
|
-
- weight is optional (default 1) and must be a number in (0, ${MAX_WEIGHT}]. It is the slot's
|
|
2281
|
+
- weight is optional (default 1) and must be a number in (0, ${MAX_WEIGHT}]. It is the slot's per-pass budget \u2014 how many concurrent sessions one pass may add. The pool's total capacity is the sum of the ceilings (which equals the sum of the weights when no ceiling is set). NOT a priority; pick order follows line order.
|
|
2282
|
+
- \`ceil:<max-parallel>\` is optional and is the slot's hard concurrency cap (its max parallel sessions). It must be at least the weight; later passes deepen a slot toward it after every slot has had its weight. Absent means the ceiling equals the weight (one pass).
|
|
2283
|
+
- a line that is exactly \`# --- fallback ---\` starts the fallback section. Keep it and the slots below it when rewriting the file: those slots are used only when no primary slot is usable, and each must not share a bucket or model with the primary.
|
|
2208
2284
|
- \`#\` starts a comment (whole-line or trailing); blank lines are ignored.
|
|
2209
2285
|
If the instruction names a model ambiguously, stop and ask rather than guessing.
|
|
2210
2286
|
|
|
@@ -2527,7 +2603,9 @@ async function maybeSelfUpdate(opts) {
|
|
|
2527
2603
|
}
|
|
2528
2604
|
|
|
2529
2605
|
// src/plugins/agent-pool.ts
|
|
2530
|
-
import { unwatchFile, watchFile } from "node:fs";
|
|
2606
|
+
import { readFileSync as readFileSync5, unwatchFile, watchFile } from "node:fs";
|
|
2607
|
+
import { homedir as homedir4 } from "node:os";
|
|
2608
|
+
import { join as join6 } from "node:path";
|
|
2531
2609
|
import { tool } from "@opencode-ai/plugin";
|
|
2532
2610
|
|
|
2533
2611
|
// src/lib/probe.ts
|
|
@@ -3179,6 +3257,43 @@ function estimateContextTokens(prompt) {
|
|
|
3179
3257
|
if (typeof prompt !== "string" || prompt.length === 0) return void 0;
|
|
3180
3258
|
return Math.ceil(prompt.length / 4) + CONTEXT_HEADROOM_TOKENS;
|
|
3181
3259
|
}
|
|
3260
|
+
var poolHealthLines = (slots, now) => {
|
|
3261
|
+
const lines = [];
|
|
3262
|
+
const heads = slots.filter((slot) => slot.model.startsWith("opencode-go/"));
|
|
3263
|
+
const headOk = heads.length > 0 && slots.slice(0, heads.length).every((slot) => slot.model.startsWith("opencode-go/"));
|
|
3264
|
+
const tail = slots[slots.length - 1];
|
|
3265
|
+
const tailOk = !!tail && tail.model.startsWith("opencode/") && tail.bucket === "opencode";
|
|
3266
|
+
const verdict = headOk && tailOk ? "intact" : "BROKEN";
|
|
3267
|
+
lines.push(
|
|
3268
|
+
`pool structure: ${verdict} (${slots.length}/10 slots, head=${headOk ? `${heads.length} opencode-go` : "missing"}, tail=${tailOk ? tail.model : "missing"})`
|
|
3269
|
+
);
|
|
3270
|
+
try {
|
|
3271
|
+
const text = readFileSync5(join6(homedir4(), ".config", "lacode", "pool.txt"), "utf8");
|
|
3272
|
+
const stamp = /^#\s*Refreshed\s+(\S+)/m.exec(text)?.[1];
|
|
3273
|
+
if (stamp) {
|
|
3274
|
+
const ageMs = now - Date.parse(stamp);
|
|
3275
|
+
if (Number.isFinite(ageMs) && ageMs >= 0) {
|
|
3276
|
+
const mins = Math.floor(ageMs / 6e4);
|
|
3277
|
+
lines.push(`last refresh: ${mins < 60 ? `${mins}m ago` : `${Math.floor(mins / 60)}h${mins % 60}m ago`} (${stamp})`);
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
} catch {
|
|
3281
|
+
}
|
|
3282
|
+
try {
|
|
3283
|
+
const raw = readFileSync5(join6(homedir4(), ".config", "lacode", "provider-status.json"), "utf8");
|
|
3284
|
+
const status = JSON.parse(raw);
|
|
3285
|
+
if (status && typeof status === "object") {
|
|
3286
|
+
const ageS = typeof status.checkedAt === "number" ? Math.max(0, Math.round((now - status.checkedAt) / 1e3)) : -1;
|
|
3287
|
+
const bad = (status.services ?? []).filter((entry) => String(entry?.status ?? "") !== "operational");
|
|
3288
|
+
const detail = bad.length > 0 ? `: ${bad.map((entry) => `${entry.service}=${entry.status}`).join(", ")}` : "";
|
|
3289
|
+
lines.push(
|
|
3290
|
+
`status page: ${status.ok === false ? "unreachable" : status.overall ?? "unknown"}${detail}${ageS >= 0 ? ` (checked ${ageS}s ago)` : ""}`
|
|
3291
|
+
);
|
|
3292
|
+
}
|
|
3293
|
+
} catch {
|
|
3294
|
+
}
|
|
3295
|
+
return lines;
|
|
3296
|
+
};
|
|
3182
3297
|
var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
3183
3298
|
const opts = options ?? {};
|
|
3184
3299
|
const notify = opts.notify ?? ((title, message) => void notifyDesktop(title, message));
|
|
@@ -3862,6 +3977,8 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
3862
3977
|
const auth2 = isAuthFailureText(haystack2) || status2 === 401 || status2 === 403;
|
|
3863
3978
|
const limited2 = isLimitText(haystack2) || status2 === 429;
|
|
3864
3979
|
const outage = isUnavailableText(haystack2);
|
|
3980
|
+
const transientUpstream2 = status2 === 400 && isTransientUpstreamText(haystack2) && !auth2 && !limited2 && !outage;
|
|
3981
|
+
const unsupported2 = status2 === 400 && error?.data?.isRetryable === false && isUnsupportedParameterText(haystack2) && !transientUpstream2 && !auth2 && !limited2 && !outage;
|
|
3865
3982
|
if (missing2) {
|
|
3866
3983
|
const unbound = !own && sessionID ? [...s.own.entries()].find(([, claim]) => !claim.g && claim.s === sessionID && haystack2.includes(claim.m)) : void 0;
|
|
3867
3984
|
const target = own ?? (unbound ? { key: unbound[0], claim: unbound[1] } : void 0);
|
|
@@ -3887,6 +4004,30 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
3887
4004
|
}
|
|
3888
4005
|
return;
|
|
3889
4006
|
}
|
|
4007
|
+
if (transientUpstream2) {
|
|
4008
|
+
if (own) {
|
|
4009
|
+
const transient = coolUnavailable(s, own.claim.m, TRANSIENT_UPSTREAM_COOLDOWN_MS, TRANSIENT_UPSTREAM_REASON);
|
|
4010
|
+
recordFailure(s, {
|
|
4011
|
+
ts: Date.now(),
|
|
4012
|
+
source: "observed",
|
|
4013
|
+
base: own.claim.v,
|
|
4014
|
+
slot: own.claim.k,
|
|
4015
|
+
model: own.claim.m,
|
|
4016
|
+
session: sessionID,
|
|
4017
|
+
reason: TRANSIENT_UPSTREAM_REASON,
|
|
4018
|
+
elapsedMs: Date.now() - own.claim.t,
|
|
4019
|
+
released: true,
|
|
4020
|
+
detail: haystack2.slice(0, 400)
|
|
4021
|
+
});
|
|
4022
|
+
release(s, own.key);
|
|
4023
|
+
await log(
|
|
4024
|
+
"warn",
|
|
4025
|
+
transient ? `cooling ${own.claim.m} for ${Math.round((transient.until - Date.now()) / 1e3)}s (${TRANSIENT_UPSTREAM_REASON}) after a flaky upstream error` : `released ${own.claim.v} claim on ${own.claim.m} after a flaky upstream error`,
|
|
4026
|
+
{ base: own.claim.v, model: own.claim.m, slot: own.claim.k, session: sessionID }
|
|
4027
|
+
);
|
|
4028
|
+
}
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
3890
4031
|
const isTpm2 = isRequestTooLargeText(haystack2);
|
|
3891
4032
|
const statedLimit2 = isTpm2 ? parseStatedTokenLimit(haystack2) : void 0;
|
|
3892
4033
|
const headers2 = error?.data?.responseHeaders;
|
|
@@ -3913,23 +4054,40 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
3913
4054
|
if (!limited2 && !outage && !auth2) {
|
|
3914
4055
|
if (own) {
|
|
3915
4056
|
const detail = haystack2.slice(0, 400);
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
4057
|
+
const aborted = isAbortText(haystack2);
|
|
4058
|
+
const reason2 = unsupported2 ? INVALID_REQUEST_REASON : "provider error";
|
|
4059
|
+
if (!aborted) {
|
|
4060
|
+
recordFailure(s, {
|
|
4061
|
+
ts: Date.now(),
|
|
4062
|
+
source: "observed",
|
|
4063
|
+
base: own.claim.v,
|
|
4064
|
+
slot: own.claim.k,
|
|
4065
|
+
model: own.claim.m,
|
|
4066
|
+
session: sessionID,
|
|
4067
|
+
reason: reason2,
|
|
4068
|
+
elapsedMs: Date.now() - own.claim.t,
|
|
4069
|
+
released: true,
|
|
4070
|
+
detail
|
|
4071
|
+
});
|
|
4072
|
+
if (!observedErrors.has(own.key)) {
|
|
4073
|
+
observedErrors.set(own.key, { reason: reason2, detail });
|
|
4074
|
+
if (observedErrors.size > 32) {
|
|
4075
|
+
const oldest = observedErrors.keys().next().value;
|
|
4076
|
+
if (oldest !== void 0) observedErrors.delete(oldest);
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
const strike2 = recordStrike(
|
|
4080
|
+
s,
|
|
4081
|
+
own.claim.m,
|
|
4082
|
+
reason2,
|
|
4083
|
+
unsupported2 ? INVALID_REQUEST_COOLDOWN_MS : void 0
|
|
4084
|
+
);
|
|
4085
|
+
if (strike2) {
|
|
4086
|
+
await log(
|
|
4087
|
+
"warn",
|
|
4088
|
+
`bench ${own.claim.m} (${reason2}, strike ${strike2.strikes}, ${Math.round((strike2.until - Date.now()) / 1e3)}s) after a terminal session error`,
|
|
4089
|
+
{ model: own.claim.m, slot: own.claim.k, session: sessionID, reason: detail }
|
|
4090
|
+
);
|
|
3933
4091
|
}
|
|
3934
4092
|
}
|
|
3935
4093
|
release(s, own.key);
|
|
@@ -4054,6 +4212,8 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
4054
4212
|
const gateway5xx = status !== void 0 && status >= 500 && status <= 599;
|
|
4055
4213
|
const transientGateway = gateway5xx || isTransientGatewayText(haystack);
|
|
4056
4214
|
const missing = isModelMissingText(haystack);
|
|
4215
|
+
const transientUpstream = status === 400 && isTransientUpstreamText(haystack) && !auth && !limited && !transientGateway;
|
|
4216
|
+
const unsupported = status === 400 && info.error.data?.isRetryable === false && isUnsupportedParameterText(haystack) && !transientUpstream && !auth && !limited && !transientGateway;
|
|
4057
4217
|
const model = `${info.providerID ?? "?"}/${info.modelID ?? "?"}`;
|
|
4058
4218
|
const ownMsg = claimFor(info.sessionID);
|
|
4059
4219
|
const isTpm = isRequestTooLargeText(haystack);
|
|
@@ -4080,24 +4240,36 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
4080
4240
|
if (!limited && !transientGateway && !auth && !missing) {
|
|
4081
4241
|
const ownMsgUnclassified = ownMsg;
|
|
4082
4242
|
if (ownMsgUnclassified) {
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
if (observedErrors.
|
|
4098
|
-
|
|
4099
|
-
if (
|
|
4243
|
+
const aborted = isAbortText(haystack);
|
|
4244
|
+
if (!aborted) {
|
|
4245
|
+
recordFailure(s, {
|
|
4246
|
+
ts: Date.now(),
|
|
4247
|
+
source: "observed",
|
|
4248
|
+
base: ownMsgUnclassified.claim.v,
|
|
4249
|
+
slot: ownMsgUnclassified.claim.k,
|
|
4250
|
+
model: ownMsgUnclassified.claim.m,
|
|
4251
|
+
session: info.sessionID,
|
|
4252
|
+
reason: "provider error",
|
|
4253
|
+
elapsedMs: Date.now() - ownMsgUnclassified.claim.t,
|
|
4254
|
+
released: true,
|
|
4255
|
+
detail: haystack.slice(0, 400)
|
|
4256
|
+
});
|
|
4257
|
+
if (!observedErrors.has(ownMsgUnclassified.key)) {
|
|
4258
|
+
observedErrors.set(ownMsgUnclassified.key, { reason: "provider error", detail: haystack.slice(0, 400) });
|
|
4259
|
+
if (observedErrors.size > 32) {
|
|
4260
|
+
const oldest = observedErrors.keys().next().value;
|
|
4261
|
+
if (oldest !== void 0) observedErrors.delete(oldest);
|
|
4262
|
+
}
|
|
4100
4263
|
}
|
|
4264
|
+
const strike2 = recordStrike(s, ownMsgUnclassified.claim.m, "provider error");
|
|
4265
|
+
if (strike2) {
|
|
4266
|
+
await log(
|
|
4267
|
+
"warn",
|
|
4268
|
+
`bench ${ownMsgUnclassified.claim.m} (provider error, strike ${strike2.strikes}) after an unclassified terminal error`,
|
|
4269
|
+
{ model: ownMsgUnclassified.claim.m, slot: ownMsgUnclassified.claim.k, session: info.sessionID, reason: haystack.slice(0, 400) }
|
|
4270
|
+
);
|
|
4271
|
+
}
|
|
4272
|
+
release(s, ownMsgUnclassified.key);
|
|
4101
4273
|
}
|
|
4102
4274
|
}
|
|
4103
4275
|
return;
|
|
@@ -4130,6 +4302,39 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
4130
4302
|
}
|
|
4131
4303
|
return;
|
|
4132
4304
|
}
|
|
4305
|
+
if (unsupported) {
|
|
4306
|
+
const ownUnsupported = ownMsg;
|
|
4307
|
+
if (ownUnsupported) {
|
|
4308
|
+
const strike2 = recordStrike(s, ownUnsupported.claim.m, INVALID_REQUEST_REASON, INVALID_REQUEST_COOLDOWN_MS);
|
|
4309
|
+
if (strike2) {
|
|
4310
|
+
await log(
|
|
4311
|
+
"warn",
|
|
4312
|
+
`bench ${ownUnsupported.claim.m} (${INVALID_REQUEST_REASON}, ${Math.round((strike2.until - Date.now()) / 1e3)}s) after a non-retryable unsupported-parameter error`,
|
|
4313
|
+
{ model: ownUnsupported.claim.m, slot: ownUnsupported.claim.k, session: info.sessionID, reason: haystack.slice(0, 400) }
|
|
4314
|
+
);
|
|
4315
|
+
}
|
|
4316
|
+
recordFailure(s, {
|
|
4317
|
+
ts: Date.now(),
|
|
4318
|
+
source: "observed",
|
|
4319
|
+
base: ownUnsupported.claim.v,
|
|
4320
|
+
slot: ownUnsupported.claim.k,
|
|
4321
|
+
model: ownUnsupported.claim.m,
|
|
4322
|
+
session: info.sessionID,
|
|
4323
|
+
reason: INVALID_REQUEST_REASON,
|
|
4324
|
+
elapsedMs: Date.now() - ownUnsupported.claim.t,
|
|
4325
|
+
released: true,
|
|
4326
|
+
detail: haystack.slice(0, 400)
|
|
4327
|
+
});
|
|
4328
|
+
release(s, ownUnsupported.key);
|
|
4329
|
+
await log("warn", `released ${ownUnsupported.claim.v} claim on ${ownUnsupported.claim.m} after an unsupported-parameter error`, {
|
|
4330
|
+
base: ownUnsupported.claim.v,
|
|
4331
|
+
model: ownUnsupported.claim.m,
|
|
4332
|
+
slot: ownUnsupported.claim.k,
|
|
4333
|
+
session: info.sessionID
|
|
4334
|
+
});
|
|
4335
|
+
}
|
|
4336
|
+
return;
|
|
4337
|
+
}
|
|
4133
4338
|
if (isUnavailableText(haystack) || !limited && !auth && transientGateway) {
|
|
4134
4339
|
const transient = coolUnavailable(s, model);
|
|
4135
4340
|
if (transient) await log("warn", `cooling ${model} for ${Math.round((transient.until - Date.now()) / 1e3)}s (${transient.reason})`);
|
|
@@ -4278,15 +4483,19 @@ ${detail}` : detail;
|
|
|
4278
4483
|
const users = entry && entry.bases.size > 0 ? [...entry.bases].map(([b, n]) => `${b}x${n}`).join(" ") : "idle";
|
|
4279
4484
|
const strike = snap.cooling.get(slot.model);
|
|
4280
4485
|
const status = s.catalog.get(slot.model);
|
|
4486
|
+
const untilClock = strike ? new Date(strike.until).toISOString().slice(11, 19) + "Z" : "";
|
|
4281
4487
|
const notes = [
|
|
4282
|
-
strike ? `UNAVAILABLE (COOLING ${Math.ceil((strike.until - now) / 1e3)}s, ${strike.reason}, strike ${strike.strikes})` : "",
|
|
4488
|
+
strike ? `UNAVAILABLE (COOLING ${Math.ceil((strike.until - now) / 1e3)}s left, until ${untilClock}, ${strike.reason}, strike ${strike.strikes})` : "",
|
|
4283
4489
|
!providerReady(s, slot.model) ? `UNAVAILABLE (provider-unconfigured: ${providerOf(slot.model)}; run \`opencode auth login ${providerOf(slot.model)}\`)` : "",
|
|
4284
4490
|
status && status !== "active" && status !== "unknown" ? `catalog:${status}` : ""
|
|
4285
4491
|
].filter(Boolean);
|
|
4286
4492
|
const marker = next && slot.index === next.index ? " <- next" : "";
|
|
4287
4493
|
const load = entry?.total ?? 0;
|
|
4494
|
+
const ceiling = slotCeiling(slot);
|
|
4288
4495
|
const atCeiling = !underCeiling(slot, snap.counts);
|
|
4289
|
-
|
|
4496
|
+
const cap = ceiling === slot.weight ? `w${slot.weight}` : `w${slot.weight} c${ceiling}`;
|
|
4497
|
+
const tier = slot.fallback ? " [fallback]" : "";
|
|
4498
|
+
return ` slot ${slot.index} ${cap} ${slot.model}${tier} load=${load}/${ceiling}${atCeiling ? " FULL" : ""}${marker} [${users}${notes.length ? "; " + notes.join("; ") : ""}]`;
|
|
4290
4499
|
};
|
|
4291
4500
|
const total = [...snap.counts.values()].reduce((sum, n) => sum + n, 0);
|
|
4292
4501
|
const coolingNow = [...snap.cooling.entries()].filter(([, st]) => st.until > now);
|
|
@@ -4295,7 +4504,7 @@ ${detail}` : detail;
|
|
|
4295
4504
|
for (const slot of slots) {
|
|
4296
4505
|
const name = slotBucket(slot);
|
|
4297
4506
|
const entry = buckets.get(name) ?? { seats: 0, slots: 0, cooling: false };
|
|
4298
|
-
entry.seats += slot
|
|
4507
|
+
entry.seats += slotCeiling(slot);
|
|
4299
4508
|
entry.slots++;
|
|
4300
4509
|
if (snap.cooling.has(slot.model)) entry.cooling = true;
|
|
4301
4510
|
buckets.set(name, entry);
|
|
@@ -4307,12 +4516,13 @@ ${detail}` : detail;
|
|
|
4307
4516
|
return {
|
|
4308
4517
|
title: `agent-pool: ${total} task(s) in flight, ${coolingNow.length} model(s) cooling`,
|
|
4309
4518
|
output: [
|
|
4310
|
-
`shared pool over ${slots.length} slot(s) from ${s.origin}; state ${pass}; bases: ${BASES.join(", ")}
|
|
4311
|
-
|
|
4519
|
+
`shared pool over ${slots.filter((slot) => !slot.fallback).length} primary slot(s) + ${slots.filter((slot) => slot.fallback).length} fallback slot(s) from ${s.origin}; state ${pass}; bases: ${BASES.join(", ")}
|
|
4520
|
+
ordered fill in declaration order: each pass fills every usable slot to its weight, then the next pass deepens a slot toward its ceiling (max parallel sessions). The fallback section is used only when no primary slot is usable. Capacity is the sum of the ceilings; weight alone is the per-pass budget (FULL = at ceiling).`,
|
|
4312
4521
|
...next ? [] : ["next: none - no routable slot; every slot is cooling, catalog-retired, or provider-unconfigured. A spawn will fail explicitly instead of routing."],
|
|
4313
4522
|
...sharedBuckets.length ? [`buckets: ${sharedBuckets.map(([name, entry]) => `${name} ${entry.seats} seat(s)${entry.cooling ? " COOLING" : ""}`).join("; ")}`] : [],
|
|
4314
4523
|
...degradedProviders.length ? [`provider degraded: ${degradedProviders.join(", ")} (flat window; a probe re-admits a model when it answers)`] : [],
|
|
4315
4524
|
...slots.map(line),
|
|
4525
|
+
...poolHealthLines(slots.filter((slot) => !slot.fallback), now),
|
|
4316
4526
|
"",
|
|
4317
4527
|
"by process:",
|
|
4318
4528
|
` ${s.id} (this process): ${snap.own.size} claim(s)`,
|
|
@@ -4380,11 +4590,11 @@ one ordered pass in slot order: each slot fills to its weight, then overflow goe
|
|
|
4380
4590
|
|
|
4381
4591
|
// src/plugins/context-autoupdate.ts
|
|
4382
4592
|
import { existsSync as existsSync4, mkdirSync as mkdirSync6, readdirSync as readdirSync3, statSync as statSync6, unlinkSync as unlinkSync5, writeFileSync as writeFileSync7 } from "node:fs";
|
|
4383
|
-
import { homedir as
|
|
4593
|
+
import { homedir as homedir6 } from "node:os";
|
|
4384
4594
|
import * as path3 from "node:path";
|
|
4385
4595
|
|
|
4386
4596
|
// src/lib/map-schema.ts
|
|
4387
|
-
import { existsSync as existsSync3, readFileSync as
|
|
4597
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6, statSync as statSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4388
4598
|
import * as path from "node:path";
|
|
4389
4599
|
var PROVISIONAL_DAYS = 30;
|
|
4390
4600
|
var MAP_SPECS = [
|
|
@@ -4537,7 +4747,7 @@ var resolvePointer = (root, pointer, section) => {
|
|
|
4537
4747
|
if (!stat.isFile()) return { exists: false };
|
|
4538
4748
|
let text;
|
|
4539
4749
|
try {
|
|
4540
|
-
if (stat.size <= 512 * 1024) text =
|
|
4750
|
+
if (stat.size <= 512 * 1024) text = readFileSync6(absolute, "utf8");
|
|
4541
4751
|
} catch {
|
|
4542
4752
|
text = void 0;
|
|
4543
4753
|
}
|
|
@@ -4786,7 +4996,7 @@ var maintainMap = (root, mapDir, now = Date.now()) => {
|
|
|
4786
4996
|
if (!existsSync3(absolute)) continue;
|
|
4787
4997
|
let before;
|
|
4788
4998
|
try {
|
|
4789
|
-
before =
|
|
4999
|
+
before = readFileSync6(absolute, "utf8");
|
|
4790
5000
|
} catch {
|
|
4791
5001
|
continue;
|
|
4792
5002
|
}
|
|
@@ -4814,7 +5024,7 @@ var auditMap = (root, mapDir, now = Date.now()) => {
|
|
|
4814
5024
|
}
|
|
4815
5025
|
let text;
|
|
4816
5026
|
try {
|
|
4817
|
-
text =
|
|
5027
|
+
text = readFileSync6(absolute, "utf8");
|
|
4818
5028
|
} catch {
|
|
4819
5029
|
violations.push({ file: spec.file, kind: "missing-file", detail: "unreadable" });
|
|
4820
5030
|
continue;
|
|
@@ -4893,7 +5103,7 @@ var parseJsonc = (raw) => {
|
|
|
4893
5103
|
};
|
|
4894
5104
|
|
|
4895
5105
|
// src/lib/writer-rule.ts
|
|
4896
|
-
import { readFileSync as
|
|
5106
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4897
5107
|
import * as path2 from "node:path";
|
|
4898
5108
|
var CONTEXT_DIR = path2.join(".opencode", "context");
|
|
4899
5109
|
var WRITER = "context";
|
|
@@ -4933,7 +5143,7 @@ var agentsForm = (mapDir, projectRoot) => {
|
|
|
4933
5143
|
var verifyWriterRule = (configPath, mapDir, projectRoot) => {
|
|
4934
5144
|
let raw;
|
|
4935
5145
|
try {
|
|
4936
|
-
raw =
|
|
5146
|
+
raw = readFileSync7(configPath, "utf8");
|
|
4937
5147
|
} catch {
|
|
4938
5148
|
return void 0;
|
|
4939
5149
|
}
|
|
@@ -4978,25 +5188,25 @@ var verifyWriterRule = (configPath, mapDir, projectRoot) => {
|
|
|
4978
5188
|
};
|
|
4979
5189
|
|
|
4980
5190
|
// src/lib/timeline.ts
|
|
4981
|
-
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as
|
|
4982
|
-
import { homedir as
|
|
4983
|
-
import { dirname as dirname3, join as
|
|
5191
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync8, renameSync as renameSync4, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
|
|
5192
|
+
import { homedir as homedir5 } from "node:os";
|
|
5193
|
+
import { dirname as dirname3, join as join9, resolve as resolve3 } from "node:path";
|
|
4984
5194
|
var TIMELINE_STATE_VERSION = 1;
|
|
4985
5195
|
function timelineDir() {
|
|
4986
5196
|
const env = process.env.LACODE_TIMELINE_DIR;
|
|
4987
5197
|
if (env) return resolve3(env);
|
|
4988
|
-
return
|
|
5198
|
+
return join9(homedir5(), ".local/share/opencode/lacode/timeline");
|
|
4989
5199
|
}
|
|
4990
5200
|
function assertValidSessionID(sessionID) {
|
|
4991
5201
|
if (!/^[A-Za-z0-9_.-]+$/.test(sessionID)) throw new Error("invalid sessionID");
|
|
4992
5202
|
}
|
|
4993
5203
|
function timelineFile(sessionID) {
|
|
4994
5204
|
assertValidSessionID(sessionID);
|
|
4995
|
-
return
|
|
5205
|
+
return join9(timelineDir(), sessionID + ".jsonl");
|
|
4996
5206
|
}
|
|
4997
5207
|
function timelineStateFile(sessionID) {
|
|
4998
5208
|
assertValidSessionID(sessionID);
|
|
4999
|
-
return
|
|
5209
|
+
return join9(timelineDir(), sessionID + ".state.json");
|
|
5000
5210
|
}
|
|
5001
5211
|
function normalizeThrough(through) {
|
|
5002
5212
|
if (!through || typeof through !== "object") return void 0;
|
|
@@ -5043,7 +5253,7 @@ function appendEntriesAt(file, texts, now, through) {
|
|
|
5043
5253
|
function readEntriesAt(file, limit) {
|
|
5044
5254
|
let raw = "";
|
|
5045
5255
|
try {
|
|
5046
|
-
raw =
|
|
5256
|
+
raw = readFileSync8(file, "utf8");
|
|
5047
5257
|
} catch {
|
|
5048
5258
|
return [];
|
|
5049
5259
|
}
|
|
@@ -5096,7 +5306,7 @@ function normalizePersistedState(raw) {
|
|
|
5096
5306
|
}
|
|
5097
5307
|
function readTimelineState(sessionID) {
|
|
5098
5308
|
try {
|
|
5099
|
-
const raw =
|
|
5309
|
+
const raw = readFileSync8(timelineStateFile(sessionID), "utf8");
|
|
5100
5310
|
return normalizePersistedState(JSON.parse(raw));
|
|
5101
5311
|
} catch {
|
|
5102
5312
|
return void 0;
|
|
@@ -5522,7 +5732,7 @@ var changedSince = (before, dir) => {
|
|
|
5522
5732
|
return changed;
|
|
5523
5733
|
};
|
|
5524
5734
|
var RETAIN_TRANSCRIPTS = 20;
|
|
5525
|
-
var transcriptDir = () => process.env.LACODE_TRANSCRIPT_DIR ?? path3.join(
|
|
5735
|
+
var transcriptDir = () => process.env.LACODE_TRANSCRIPT_DIR ?? path3.join(homedir6(), ".local", "share", "opencode", "lacode", "cartographer");
|
|
5526
5736
|
var retireSession = async (client, directory, sessionID, mode, log) => {
|
|
5527
5737
|
try {
|
|
5528
5738
|
const transcript = await client.session.messages({ path: { id: sessionID }, query: { directory } }).then((r) => r.data).catch(() => void 0);
|
|
@@ -5778,15 +5988,15 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
5778
5988
|
});
|
|
5779
5989
|
|
|
5780
5990
|
// src/lib/feedback.ts
|
|
5781
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
5782
|
-
import { homedir as
|
|
5783
|
-
import { dirname as dirname5, join as
|
|
5991
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, renameSync as renameSync5, unlinkSync as unlinkSync6, writeFileSync as writeFileSync8 } from "node:fs";
|
|
5992
|
+
import { homedir as homedir7 } from "node:os";
|
|
5993
|
+
import { dirname as dirname5, join as join11 } from "node:path";
|
|
5784
5994
|
var FEEDBACK_HINT_ENV = "LACODE_NO_FEEDBACK_HINT";
|
|
5785
5995
|
var FEEDBACK_HINT = "Enjoying lacode? Bugs, ideas, or a quick note on how you use it: github.com/lawaty/lacode/issues (a \u2B50 on the repo helps others find it)";
|
|
5786
|
-
var feedbackStatePath = () =>
|
|
5996
|
+
var feedbackStatePath = () => join11(homedir7(), ".local", "share", "opencode", "lacode", "feedback.json");
|
|
5787
5997
|
function readFeedbackState(path4 = feedbackStatePath()) {
|
|
5788
5998
|
try {
|
|
5789
|
-
const parsed = JSON.parse(
|
|
5999
|
+
const parsed = JSON.parse(readFileSync9(path4, "utf8"));
|
|
5790
6000
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
5791
6001
|
return parsed;
|
|
5792
6002
|
} catch (error) {
|
|
@@ -5860,9 +6070,9 @@ async function feedbackHooks(input, options, deps = {}) {
|
|
|
5860
6070
|
|
|
5861
6071
|
// src/plugins/image-paste.ts
|
|
5862
6072
|
import { createHash, randomUUID } from "node:crypto";
|
|
5863
|
-
import { globSync, mkdirSync as mkdirSync8, readFileSync as
|
|
5864
|
-
import { homedir as
|
|
5865
|
-
import { basename as basename3, join as
|
|
6073
|
+
import { globSync, mkdirSync as mkdirSync8, readFileSync as readFileSync10, renameSync as renameSync6, statSync as statSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
6074
|
+
import { homedir as homedir8 } from "node:os";
|
|
6075
|
+
import { basename as basename3, join as join12 } from "node:path";
|
|
5866
6076
|
|
|
5867
6077
|
// src/lib/clipboard.ts
|
|
5868
6078
|
import { spawnSync } from "node:child_process";
|
|
@@ -6012,8 +6222,8 @@ function splitArguments(input) {
|
|
|
6012
6222
|
return out;
|
|
6013
6223
|
}
|
|
6014
6224
|
function expandTilde(p) {
|
|
6015
|
-
if (p === "~") return
|
|
6016
|
-
if (p.startsWith("~/")) return
|
|
6225
|
+
if (p === "~") return homedir8();
|
|
6226
|
+
if (p.startsWith("~/")) return join12(homedir8(), p.slice(2));
|
|
6017
6227
|
return p;
|
|
6018
6228
|
}
|
|
6019
6229
|
function expandGlob(p) {
|
|
@@ -6029,7 +6239,7 @@ function storeImage(bytes, dir, sessionID, label) {
|
|
|
6029
6239
|
if (kind === null) return null;
|
|
6030
6240
|
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
6031
6241
|
mkdirSync8(dir, { recursive: true });
|
|
6032
|
-
const target =
|
|
6242
|
+
const target = join12(dir, `${hash}.${kind.ext}`);
|
|
6033
6243
|
const tmp = `${target}.tmp`;
|
|
6034
6244
|
writeFileSync9(tmp, bytes, { mode: 384 });
|
|
6035
6245
|
renameSync6(tmp, target);
|
|
@@ -6060,7 +6270,7 @@ async function imagePasteHooks(input, options, deps) {
|
|
|
6060
6270
|
if (opts.enabled === false) return {};
|
|
6061
6271
|
const maxBytes = resolveMaxBytes(opts);
|
|
6062
6272
|
const maxImages = resolveMaxImages(opts);
|
|
6063
|
-
const dir = opts.dir ?? process.env.LACODE_IMAGE_DIR ??
|
|
6273
|
+
const dir = opts.dir ?? process.env.LACODE_IMAGE_DIR ?? join12(homedir8(), ".local/share/opencode/lacode/images");
|
|
6064
6274
|
const readClipboard = (deps ?? {}).readClipboard ?? readClipboardImage;
|
|
6065
6275
|
return {
|
|
6066
6276
|
"command.execute.before": async (cmd, output) => {
|
|
@@ -6117,7 +6327,7 @@ async function imagePasteHooks(input, options, deps) {
|
|
|
6117
6327
|
}
|
|
6118
6328
|
let bytes;
|
|
6119
6329
|
try {
|
|
6120
|
-
bytes =
|
|
6330
|
+
bytes = readFileSync10(filePath);
|
|
6121
6331
|
} catch {
|
|
6122
6332
|
skipped.push(name);
|
|
6123
6333
|
continue;
|
|
@@ -6168,14 +6378,14 @@ async function imagePasteHooks(input, options, deps) {
|
|
|
6168
6378
|
}
|
|
6169
6379
|
|
|
6170
6380
|
// src/lib/opencode-config.ts
|
|
6171
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync9, readFileSync as
|
|
6172
|
-
import { homedir as
|
|
6173
|
-
import { dirname as dirname6, join as
|
|
6381
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync9, readFileSync as readFileSync11, renameSync as renameSync7, writeFileSync as writeFileSync10 } from "node:fs";
|
|
6382
|
+
import { homedir as homedir9 } from "node:os";
|
|
6383
|
+
import { dirname as dirname6, join as join13 } from "node:path";
|
|
6174
6384
|
import { applyEdits, modify, parse, printParseErrorCode } from "jsonc-parser";
|
|
6175
6385
|
function globalOpencodeConfigPath() {
|
|
6176
|
-
const base = process.env.XDG_CONFIG_HOME ??
|
|
6177
|
-
const jsonc =
|
|
6178
|
-
return existsSync5(jsonc) ? jsonc :
|
|
6386
|
+
const base = process.env.XDG_CONFIG_HOME ?? join13(homedir9(), ".config");
|
|
6387
|
+
const jsonc = join13(base, "opencode", "opencode.jsonc");
|
|
6388
|
+
return existsSync5(jsonc) ? jsonc : join13(base, "opencode", "opencode.json");
|
|
6179
6389
|
}
|
|
6180
6390
|
var OpenCodeConfigError = class extends Error {
|
|
6181
6391
|
constructor(message) {
|
|
@@ -6188,7 +6398,7 @@ function isPlainObject(value) {
|
|
|
6188
6398
|
}
|
|
6189
6399
|
function readOpencodeConfig(path4 = globalOpencodeConfigPath()) {
|
|
6190
6400
|
if (!existsSync5(path4)) return {};
|
|
6191
|
-
const text =
|
|
6401
|
+
const text = readFileSync11(path4, "utf8");
|
|
6192
6402
|
const errors = [];
|
|
6193
6403
|
const parsed = parse(text, errors, { allowTrailingComma: true, disallowComments: false });
|
|
6194
6404
|
if (errors.length > 0) {
|
|
@@ -7313,6 +7523,43 @@ function normalizeChunk(value) {
|
|
|
7313
7523
|
}
|
|
7314
7524
|
return changed;
|
|
7315
7525
|
}
|
|
7526
|
+
var UPSTREAM_ATTEMPTS = 3;
|
|
7527
|
+
var UPSTREAM_RETRY_DELAY_MS = 250;
|
|
7528
|
+
var TRANSIENT_UPSTREAM_PATTERN2 = /reasoning_display/i;
|
|
7529
|
+
function requestUrl(input) {
|
|
7530
|
+
if (typeof input === "string") return input;
|
|
7531
|
+
if (input instanceof URL) return input.toString();
|
|
7532
|
+
if (input && typeof input === "object") {
|
|
7533
|
+
const url = input.url;
|
|
7534
|
+
if (typeof url === "string") return url;
|
|
7535
|
+
}
|
|
7536
|
+
return "";
|
|
7537
|
+
}
|
|
7538
|
+
function isOpencodeInferenceChat(raw) {
|
|
7539
|
+
if (!raw) return false;
|
|
7540
|
+
let url;
|
|
7541
|
+
try {
|
|
7542
|
+
url = new URL(raw);
|
|
7543
|
+
} catch {
|
|
7544
|
+
return false;
|
|
7545
|
+
}
|
|
7546
|
+
if (url.hostname !== "opencode.ai" && !url.hostname.endsWith(".opencode.ai")) return false;
|
|
7547
|
+
return url.pathname.includes("/inference/") && url.pathname.endsWith("/chat/completions");
|
|
7548
|
+
}
|
|
7549
|
+
function isReplayableInit(init2) {
|
|
7550
|
+
if (!init2 || typeof init2 !== "object") return false;
|
|
7551
|
+
const body = init2.body;
|
|
7552
|
+
return typeof body === "string" || body instanceof Uint8Array || body instanceof ArrayBuffer;
|
|
7553
|
+
}
|
|
7554
|
+
async function isTransientUpstreamResponse(response) {
|
|
7555
|
+
if (response.status !== 400) return false;
|
|
7556
|
+
try {
|
|
7557
|
+
const text = await response.clone().text();
|
|
7558
|
+
return TRANSIENT_UPSTREAM_PATTERN2.test(text) && /not\s+a\s+supported\s+value|unsupported/i.test(text);
|
|
7559
|
+
} catch {
|
|
7560
|
+
return false;
|
|
7561
|
+
}
|
|
7562
|
+
}
|
|
7316
7563
|
function normalizeSseLine(line) {
|
|
7317
7564
|
const trimmed = line.trim();
|
|
7318
7565
|
if (!trimmed.startsWith("data:")) return line;
|
|
@@ -7349,9 +7596,22 @@ function createChunkNormalizer() {
|
|
|
7349
7596
|
}
|
|
7350
7597
|
});
|
|
7351
7598
|
}
|
|
7352
|
-
|
|
7599
|
+
var sleep = (ms) => ms > 0 ? new Promise((resolve5) => setTimeout(resolve5, ms)) : Promise.resolve();
|
|
7600
|
+
function wrapFetch(original, options = {}) {
|
|
7601
|
+
const attempts = Math.max(1, Math.trunc(options.attempts ?? UPSTREAM_ATTEMPTS));
|
|
7602
|
+
const delayMs = Math.max(0, Math.trunc(options.delayMs ?? UPSTREAM_RETRY_DELAY_MS));
|
|
7353
7603
|
const wrapped = async (input, init2) => {
|
|
7354
|
-
const
|
|
7604
|
+
const retryable = attempts > 1 && isReplayableInit(init2) && isOpencodeInferenceChat(requestUrl(input));
|
|
7605
|
+
let response = await original(input, init2);
|
|
7606
|
+
for (let attempt = 1; retryable && attempt < attempts; attempt++) {
|
|
7607
|
+
if (!await isTransientUpstreamResponse(response)) break;
|
|
7608
|
+
try {
|
|
7609
|
+
await response.body?.cancel();
|
|
7610
|
+
} catch {
|
|
7611
|
+
}
|
|
7612
|
+
await sleep(delayMs);
|
|
7613
|
+
response = await original(input, init2);
|
|
7614
|
+
}
|
|
7355
7615
|
try {
|
|
7356
7616
|
const contentType = response.headers.get("content-type") ?? "";
|
|
7357
7617
|
if (!contentType.includes("text/event-stream") || !response.body) return response;
|
|
@@ -7424,7 +7684,7 @@ async function selfUpdateHooks(input, options) {
|
|
|
7424
7684
|
|
|
7425
7685
|
// src/plugins/timeline.ts
|
|
7426
7686
|
import { existsSync as existsSync6, writeFileSync as writeFileSync11, renameSync as renameSync8, rmSync as rmSync3 } from "node:fs";
|
|
7427
|
-
import { join as
|
|
7687
|
+
import { join as join14 } from "node:path";
|
|
7428
7688
|
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
7429
7689
|
var DEFAULT_INTERVAL_MS = 3e5;
|
|
7430
7690
|
var MIN_INTERVAL_MS = 1e4;
|
|
@@ -7500,7 +7760,7 @@ function errorText(value) {
|
|
|
7500
7760
|
}
|
|
7501
7761
|
var MAX_TASK_CHILDREN_EXPANDED = 40;
|
|
7502
7762
|
async function expandTaskSessions(client, messages, directory, owned, onError, cursor) {
|
|
7503
|
-
const projectOwnsTooling = existsSync6(
|
|
7763
|
+
const projectOwnsTooling = existsSync6(join14(directory, "src/plugins/timeline.ts"));
|
|
7504
7764
|
let attempts = 0;
|
|
7505
7765
|
const expandedMessages = [];
|
|
7506
7766
|
let startIndex = 0;
|
|
@@ -8066,9 +8326,9 @@ function timelineHooks(input, options) {
|
|
|
8066
8326
|
|
|
8067
8327
|
// src/lib/timer-migration.ts
|
|
8068
8328
|
import { execFile } from "node:child_process";
|
|
8069
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync10, readFileSync as
|
|
8070
|
-
import { homedir as
|
|
8071
|
-
import { basename as basename4, dirname as dirname7, join as
|
|
8329
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync10, readFileSync as readFileSync12, renameSync as renameSync9, writeFileSync as writeFileSync12 } from "node:fs";
|
|
8330
|
+
import { homedir as homedir10 } from "node:os";
|
|
8331
|
+
import { basename as basename4, dirname as dirname7, join as join15 } from "node:path";
|
|
8072
8332
|
var TIMER_MIGRATION_ENV = "LACODE_NO_TIMER_MIGRATION";
|
|
8073
8333
|
var TIMER_MIGRATION_STATE_VERSION = 1;
|
|
8074
8334
|
var defaultRun2 = (file, args, options) => new Promise((resolve5, reject) => {
|
|
@@ -8082,15 +8342,15 @@ function resolveNodeBinary(execPath = process.execPath) {
|
|
|
8082
8342
|
if (existsSync7("/usr/bin/node")) return "/usr/bin/node";
|
|
8083
8343
|
return "node";
|
|
8084
8344
|
}
|
|
8085
|
-
function timerMigrationStatePath(home =
|
|
8086
|
-
return
|
|
8345
|
+
function timerMigrationStatePath(home = homedir10()) {
|
|
8346
|
+
return join15(home, ".local", "share", "opencode", "lacode", "migrations.json");
|
|
8087
8347
|
}
|
|
8088
|
-
function timerMigrationUnitPath(home =
|
|
8089
|
-
return
|
|
8348
|
+
function timerMigrationUnitPath(home = homedir10()) {
|
|
8349
|
+
return join15(home, ".config", "systemd", "user", "lacode-refresh-pool.timer");
|
|
8090
8350
|
}
|
|
8091
8351
|
function readState2(path4) {
|
|
8092
8352
|
try {
|
|
8093
|
-
return JSON.parse(
|
|
8353
|
+
return JSON.parse(readFileSync12(path4, "utf8"));
|
|
8094
8354
|
} catch {
|
|
8095
8355
|
return {};
|
|
8096
8356
|
}
|
|
@@ -8109,7 +8369,7 @@ async function runTimerMigration(options = {}) {
|
|
|
8109
8369
|
const packageRoot = options.packageRoot ?? PACKAGE_ROOT;
|
|
8110
8370
|
const version = options.version ?? readInstalledVersion(packageRoot);
|
|
8111
8371
|
if (!version) return { action: "skipped", reason: "no-version" };
|
|
8112
|
-
const home = options.home ??
|
|
8372
|
+
const home = options.home ?? homedir10();
|
|
8113
8373
|
const statePath = options.statePath ?? timerMigrationStatePath(home);
|
|
8114
8374
|
const state2 = readState2(statePath);
|
|
8115
8375
|
if (state2.timersVersion === version && existsSync7(timerMigrationUnitPath(home))) {
|
|
@@ -8121,7 +8381,7 @@ async function runTimerMigration(options = {}) {
|
|
|
8121
8381
|
} catch {
|
|
8122
8382
|
return { action: "skipped", reason: "no-systemd" };
|
|
8123
8383
|
}
|
|
8124
|
-
const script =
|
|
8384
|
+
const script = join15(packageRoot, "scripts", "refresh-pool.mjs");
|
|
8125
8385
|
if (!existsSync7(script)) return { action: "failed", version, error: `timer script not found: ${script}` };
|
|
8126
8386
|
try {
|
|
8127
8387
|
await run(resolveNodeBinary(), [script, "--install-timer", "--stable"], { stdio: "ignore" });
|