@lawaty/lacode 1.2.0 → 1.3.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +3 -3
- package/dist/agents.js +2 -0
- package/dist/commands.js +2 -0
- package/dist/index.js +256 -24
- package/dist/lacode.js +2 -0
- package/dist/pool.js +30 -5
- package/dist/tunnel.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,45 @@ All notable changes to LaCode are recorded here. The format follows
|
|
|
7
7
|
Versions come from git release commits; the npm version is bumped in the same
|
|
8
8
|
commit as the changelog entry it describes.
|
|
9
9
|
|
|
10
|
+
## [1.3.0-beta.0] — provider readiness, timeline roots, and a reason on cancelled tasks
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **The pool gates slots on provider readiness.** Before routing, LaCode asks
|
|
15
|
+
opencode which providers are connected and which carry credentials, then
|
|
16
|
+
excludes slots whose provider is unconfigured or unauthenticated. Readiness
|
|
17
|
+
fails open until the first successful probe, so an offline start still routes;
|
|
18
|
+
`pool_status` names the provider to log in to.
|
|
19
|
+
- **`/reprocess-timeline` rebuilds the root session.** A reprocess started from a
|
|
20
|
+
subagent walks the `parentID` chain (with cycle protection) and summarizes the
|
|
21
|
+
root session, instead of only the child it was invoked from.
|
|
22
|
+
- **Host sync merges a Cloudflare Workers AI provider.** `bin/oc-sync` reads
|
|
23
|
+
`CLOUDFLARE_ACCOUNT_ID` (or derives it from the local provider URL) and merges
|
|
24
|
+
the provider into each host's `opencode.jsonc` after the auth sync; existing
|
|
25
|
+
providers are preserved and the write is backed up.
|
|
26
|
+
- **`OC_AUTH_PROVIDERS` can come from the env file.** The post-publish host sync
|
|
27
|
+
falls back to `~/.config/lacode/env` when the variable is unset.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **A pool-cancelled subtask now carries its cause.** The bare `Task cancelled`
|
|
32
|
+
card is rewritten in place to `Task Cancelled by LaCode: <cause>` — slot,
|
|
33
|
+
model, elapsed time, failure kind and claim state. The old warning toast is
|
|
34
|
+
gone; the reason lives on the task itself. The rewrite is best-effort: if
|
|
35
|
+
opencode rejects the part update, a warning is logged and the original text
|
|
36
|
+
stays.
|
|
37
|
+
- **A model that disappears is benched, not retried forever.** A terminal
|
|
38
|
+
model-not-found error cools the model for the cap and releases the claim.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **Startup no longer waits on the provider endpoint.** The config hook used to
|
|
43
|
+
await `client.provider.list` while opencode was still bootstrapping the same
|
|
44
|
+
instance, which deadlocked startup. The first spawn now refreshes readiness
|
|
45
|
+
before routing, and `pool_status`/`pool_recheck` still force a refresh.
|
|
46
|
+
- **A terminal session error, or an abort that does not confirm, releases the
|
|
47
|
+
claim** instead of holding it until the reaper.
|
|
48
|
+
|
|
10
49
|
## [1.2.0] — voice, a per-session timeline, and a pool without a cap
|
|
11
50
|
|
|
12
51
|
### Added
|
|
@@ -920,6 +959,7 @@ experimental; `latest` remains 0.4.2.
|
|
|
920
959
|
- First published release: pooled subagents over a shared set of models.
|
|
921
960
|
|
|
922
961
|
[Unreleased]: https://github.com/lawaty/lacode/compare/v1.2.0...HEAD
|
|
962
|
+
[1.3.0-beta.0]: https://github.com/lawaty/lacode/releases/tag/v1.3.0-beta.0
|
|
923
963
|
[1.2.0]: https://github.com/lawaty/lacode/compare/v1.1.0...v1.2.0
|
|
924
964
|
[1.1.0]: https://github.com/lawaty/lacode/compare/v1.0.0...v1.1.0
|
|
925
965
|
[1.0.0]: https://github.com/lawaty/lacode/compare/v0.11.0...v1.0.0
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Ultimate opencode setup for Go users
|
|
4
4
|
|
|
5
|
-
> LaCode 1.
|
|
5
|
+
> LaCode 1.2.0 ships on the npm **`latest`** dist-tag. Prereleases use
|
|
6
6
|
> `beta`: install `@lawaty/lacode@beta`, or pin an exact version.
|
|
7
7
|
|
|
8
8
|
LaCode is an opencode plugin. It routes delegated work onto a weighted pool of
|
|
@@ -170,7 +170,7 @@ The running session keeps its loaded code; the new version takes effect on the
|
|
|
170
170
|
next start, and the log says so. Run `lacode update` to do it on demand, or
|
|
171
171
|
`lacode update --check` to just report.
|
|
172
172
|
|
|
173
|
-
1.
|
|
173
|
+
1.2.0 is published to the `latest` dist-tag; prereleases use `beta`. This check
|
|
174
174
|
follows `latest`. A beta install is therefore reported as current even when a
|
|
175
175
|
newer beta exists,
|
|
176
176
|
and `lacode update` will not move it: pin the version you want in the `plugin`
|
|
@@ -186,7 +186,7 @@ lacode version
|
|
|
186
186
|
```
|
|
187
187
|
|
|
188
188
|
```
|
|
189
|
-
version 1.
|
|
189
|
+
version 1.2.0
|
|
190
190
|
root /home/you/lacode
|
|
191
191
|
source checkout
|
|
192
192
|
commit <hash> (dirty)
|
package/dist/agents.js
CHANGED
|
@@ -25,9 +25,11 @@ var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
|
25
25
|
|
|
26
26
|
// src/lib/pool.ts
|
|
27
27
|
var CLAIM_TTL_MS = 10 * 60 * 1e3;
|
|
28
|
+
var UNBOUND_CLAIM_MS = 2 * 60 * 1e3;
|
|
28
29
|
var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
|
|
29
30
|
var ORPHAN_TMP_MS = 60 * 1e3;
|
|
30
31
|
var CATALOG_TTL_MS = 10 * 60 * 1e3;
|
|
32
|
+
var PROVIDER_LIST_TTL_MS = 60 * 1e3;
|
|
31
33
|
var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
|
|
32
34
|
var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
33
35
|
var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
package/dist/commands.js
CHANGED
|
@@ -29,9 +29,11 @@ var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
|
29
29
|
|
|
30
30
|
// src/lib/pool.ts
|
|
31
31
|
var CLAIM_TTL_MS = 10 * 60 * 1e3;
|
|
32
|
+
var UNBOUND_CLAIM_MS = 2 * 60 * 1e3;
|
|
32
33
|
var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
|
|
33
34
|
var ORPHAN_TMP_MS = 60 * 1e3;
|
|
34
35
|
var CATALOG_TTL_MS = 10 * 60 * 1e3;
|
|
36
|
+
var PROVIDER_LIST_TTL_MS = 60 * 1e3;
|
|
35
37
|
var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
|
|
36
38
|
var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
37
39
|
var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
package/dist/index.js
CHANGED
|
@@ -25,9 +25,11 @@ var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
|
25
25
|
|
|
26
26
|
// src/lib/pool.ts
|
|
27
27
|
var CLAIM_TTL_MS = 10 * 60 * 1e3;
|
|
28
|
+
var UNBOUND_CLAIM_MS = 2 * 60 * 1e3;
|
|
28
29
|
var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
|
|
29
30
|
var ORPHAN_TMP_MS = 60 * 1e3;
|
|
30
31
|
var CATALOG_TTL_MS = 10 * 60 * 1e3;
|
|
32
|
+
var PROVIDER_LIST_TTL_MS = 60 * 1e3;
|
|
31
33
|
var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
|
|
32
34
|
var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
33
35
|
var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
|
@@ -294,6 +296,11 @@ function state(opts = {}) {
|
|
|
294
296
|
reaping: false,
|
|
295
297
|
lastSweep: 0,
|
|
296
298
|
lastDirSweep: 0,
|
|
299
|
+
connected: /* @__PURE__ */ new Set(),
|
|
300
|
+
connectedAt: 0,
|
|
301
|
+
connectedReady: false,
|
|
302
|
+
credentialed: /* @__PURE__ */ new Set(),
|
|
303
|
+
configCreds: /* @__PURE__ */ new Set(),
|
|
297
304
|
id,
|
|
298
305
|
dir,
|
|
299
306
|
modelsFile,
|
|
@@ -401,9 +408,16 @@ function sweepStateDir(s) {
|
|
|
401
408
|
}
|
|
402
409
|
return removed;
|
|
403
410
|
}
|
|
411
|
+
var claimExpired = (claim, now) => now - (claim.hb ?? claim.t) > CLAIM_TTL_MS || !claim.g && now - claim.t > UNBOUND_CLAIM_MS;
|
|
404
412
|
function pruneOwn(s) {
|
|
405
413
|
const now = Date.now();
|
|
406
|
-
|
|
414
|
+
let removed = false;
|
|
415
|
+
for (const [key, claim] of s.own)
|
|
416
|
+
if (claimExpired(claim, now)) {
|
|
417
|
+
s.own.delete(key);
|
|
418
|
+
removed = true;
|
|
419
|
+
}
|
|
420
|
+
if (removed) publish(s);
|
|
407
421
|
}
|
|
408
422
|
function pruneActivity(s, now) {
|
|
409
423
|
if (s.activity.size === 0) return;
|
|
@@ -431,7 +445,7 @@ function publish(s) {
|
|
|
431
445
|
if (!s.dirReady) return;
|
|
432
446
|
const now = Date.now();
|
|
433
447
|
const payload = {};
|
|
434
|
-
for (const [key, claim] of s.own) if (
|
|
448
|
+
for (const [key, claim] of s.own) if (!claimExpired(claim, now)) payload[key] = claim;
|
|
435
449
|
try {
|
|
436
450
|
writeFileSync(join2(s.dir, `${selfFile(s.id)}.tmp`), JSON.stringify(payload));
|
|
437
451
|
renameSync(join2(s.dir, `${selfFile(s.id)}.tmp`), join2(s.dir, selfFile(s.id)));
|
|
@@ -490,7 +504,7 @@ function readSiblings(s) {
|
|
|
490
504
|
try {
|
|
491
505
|
const parsed = JSON.parse(readFileSync2(path5, "utf8"));
|
|
492
506
|
const claims = Object.values(parsed).filter(
|
|
493
|
-
(c) => c && typeof c.v === "string" && typeof c.m === "string" && typeof c.k === "number" && Number.isFinite(c.k) &&
|
|
507
|
+
(c) => c && typeof c.v === "string" && typeof c.m === "string" && typeof c.k === "number" && Number.isFinite(c.k) && !claimExpired(c, now)
|
|
494
508
|
);
|
|
495
509
|
siblings.push({ id: match[1], claims });
|
|
496
510
|
} catch {
|
|
@@ -629,6 +643,12 @@ function clearCooldowns(s, models) {
|
|
|
629
643
|
var markProbe = (s, sessionID) => s.probes.add(sessionID);
|
|
630
644
|
var unmarkProbe = (s, sessionID) => s.probes.delete(sessionID);
|
|
631
645
|
var isProbe = (s, sessionID) => s.probes.has(sessionID);
|
|
646
|
+
function providerReady(s, model) {
|
|
647
|
+
if (!s.connectedReady) return true;
|
|
648
|
+
const provider = providerOf(model);
|
|
649
|
+
if (!s.connected.has(provider)) return false;
|
|
650
|
+
return s.credentialed.has(provider) || s.configCreds.has(provider) || provider === "opencode";
|
|
651
|
+
}
|
|
632
652
|
function refreshCatalog(s, serverUrl) {
|
|
633
653
|
if (!serverUrl || Date.now() - s.catalogAt < CATALOG_TTL_MS) return;
|
|
634
654
|
s.catalogAt = Date.now();
|
|
@@ -645,6 +665,7 @@ function refreshCatalog(s, serverUrl) {
|
|
|
645
665
|
})();
|
|
646
666
|
}
|
|
647
667
|
function isAvailable(s, model, cooling) {
|
|
668
|
+
if (!providerReady(s, model)) return false;
|
|
648
669
|
if (cooling.has(model)) return false;
|
|
649
670
|
const status = s.catalog.get(model);
|
|
650
671
|
return status === void 0 || status === "active" || status === "unknown";
|
|
@@ -664,8 +685,9 @@ var loadOf = (counts, slot) => counts.get(slot) ?? 0;
|
|
|
664
685
|
var underCeiling = (slot, counts) => loadOf(counts, slot.index) < slotCeiling(slot);
|
|
665
686
|
function pickSlot(s, snap) {
|
|
666
687
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
667
|
-
const
|
|
668
|
-
const
|
|
688
|
+
const configured = slots.filter((slot) => providerReady(s, slot.model));
|
|
689
|
+
const usable = configured.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
690
|
+
const pool = usable.length > 0 ? usable : configured.length > 0 ? configured : slots;
|
|
669
691
|
for (const slot of usable) {
|
|
670
692
|
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
671
693
|
}
|
|
@@ -2439,6 +2461,63 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2439
2461
|
s.warned.add(message);
|
|
2440
2462
|
await log("warn", message);
|
|
2441
2463
|
};
|
|
2464
|
+
const refreshProviders = async (force = false) => {
|
|
2465
|
+
if (!force && Date.now() - s.connectedAt < PROVIDER_LIST_TTL_MS) return;
|
|
2466
|
+
s.connectedAt = Date.now();
|
|
2467
|
+
try {
|
|
2468
|
+
const providerApi = client.provider;
|
|
2469
|
+
if (!providerApi || typeof providerApi.list !== "function") return;
|
|
2470
|
+
const result = await providerApi.list({ query: { directory } });
|
|
2471
|
+
const data = result?.data;
|
|
2472
|
+
const connected = data?.connected;
|
|
2473
|
+
const all = data?.all;
|
|
2474
|
+
if (Array.isArray(connected) && Array.isArray(all)) {
|
|
2475
|
+
s.connected = new Set(connected.filter((id) => typeof id === "string"));
|
|
2476
|
+
const credentialed = /* @__PURE__ */ new Set();
|
|
2477
|
+
for (const entry of all) {
|
|
2478
|
+
if (!entry || typeof entry !== "object") continue;
|
|
2479
|
+
const rec = entry;
|
|
2480
|
+
const id = typeof rec.id === "string" ? rec.id : void 0;
|
|
2481
|
+
if (!id) continue;
|
|
2482
|
+
if (typeof rec.key === "string" && rec.key.trim() !== "") {
|
|
2483
|
+
credentialed.add(id);
|
|
2484
|
+
continue;
|
|
2485
|
+
}
|
|
2486
|
+
const options2 = rec.options;
|
|
2487
|
+
if (options2 && typeof options2 === "object") {
|
|
2488
|
+
const opts2 = options2;
|
|
2489
|
+
if (typeof opts2.apiKey === "string" && opts2.apiKey.trim() !== "") {
|
|
2490
|
+
credentialed.add(id);
|
|
2491
|
+
continue;
|
|
2492
|
+
}
|
|
2493
|
+
const baseURL = typeof opts2.baseURL === "string" ? opts2.baseURL.trim() : "";
|
|
2494
|
+
if (baseURL !== "") {
|
|
2495
|
+
const envArr2 = Array.isArray(rec.env) ? rec.env : [];
|
|
2496
|
+
const hasCredEnv = envArr2.some((e) => typeof e === "string" && /key|token|secret|password|credential|auth/i.test(e));
|
|
2497
|
+
if (!hasCredEnv) {
|
|
2498
|
+
credentialed.add(id);
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
const envArr = Array.isArray(rec.env) ? rec.env : [];
|
|
2504
|
+
for (const e of envArr) {
|
|
2505
|
+
if (typeof e === "string" && /key|token|secret|password|credential|auth/i.test(e)) {
|
|
2506
|
+
if (typeof process.env[e] === "string" && process.env[e].trim() !== "") {
|
|
2507
|
+
credentialed.add(id);
|
|
2508
|
+
break;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
s.credentialed = credentialed;
|
|
2514
|
+
s.connectedReady = true;
|
|
2515
|
+
} else if (Array.isArray(connected)) {
|
|
2516
|
+
s.connected = new Set(connected.filter((id) => typeof id === "string"));
|
|
2517
|
+
}
|
|
2518
|
+
} catch {
|
|
2519
|
+
}
|
|
2520
|
+
};
|
|
2442
2521
|
init(s, (m) => void warnOnce(m));
|
|
2443
2522
|
const sweepDirIfDue = () => {
|
|
2444
2523
|
if (Date.now() - s.lastDirSweep < STATE_SWEEP_INTERVAL_MS) return;
|
|
@@ -2475,6 +2554,44 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2475
2554
|
if (kind === "permission") return `permission denied after ${seconds}s`;
|
|
2476
2555
|
return `provider error after ${seconds}s`;
|
|
2477
2556
|
};
|
|
2557
|
+
const pendingCancelledParts = /* @__PURE__ */ new Map();
|
|
2558
|
+
const patchedCancelledParts = /* @__PURE__ */ new Set();
|
|
2559
|
+
const cancelledPrefix = "Task Cancelled by LaCode:";
|
|
2560
|
+
const patchCancelledPart = async (part, key, report) => {
|
|
2561
|
+
if (patchedCancelledParts.has(key)) return;
|
|
2562
|
+
patchedCancelledParts.add(key);
|
|
2563
|
+
if (patchedCancelledParts.size > 64) {
|
|
2564
|
+
const oldest = patchedCancelledParts.values().next().value;
|
|
2565
|
+
if (oldest !== void 0) patchedCancelledParts.delete(oldest);
|
|
2566
|
+
}
|
|
2567
|
+
const error = `${cancelledPrefix} ${report.message}`;
|
|
2568
|
+
try {
|
|
2569
|
+
const url = new URL(
|
|
2570
|
+
`/session/${encodeURIComponent(part.sessionID)}/message/${encodeURIComponent(part.messageID)}/part/${encodeURIComponent(part.id)}`,
|
|
2571
|
+
serverUrl
|
|
2572
|
+
);
|
|
2573
|
+
if (directory) url.searchParams.set("directory", directory);
|
|
2574
|
+
const headers = { "Content-Type": "application/json" };
|
|
2575
|
+
const password = process.env.OPENCODE_SERVER_PASSWORD;
|
|
2576
|
+
if (password) {
|
|
2577
|
+
const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode";
|
|
2578
|
+
headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
2579
|
+
}
|
|
2580
|
+
const response = await fetch(url, {
|
|
2581
|
+
method: "PATCH",
|
|
2582
|
+
headers,
|
|
2583
|
+
body: JSON.stringify({ ...part, state: { ...part.state, error } }),
|
|
2584
|
+
signal: AbortSignal.timeout(5e3)
|
|
2585
|
+
});
|
|
2586
|
+
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`.trim());
|
|
2587
|
+
abortReports.delete(key);
|
|
2588
|
+
} catch (cause) {
|
|
2589
|
+
await log("warn", `could not rewrite cancelled task ${part.callID}: ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
2590
|
+
session: part.sessionID,
|
|
2591
|
+
callID: part.callID
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
};
|
|
2478
2595
|
const noteAbort = async (key, report) => {
|
|
2479
2596
|
abortReports.set(key, report);
|
|
2480
2597
|
if (abortReports.size > 32) {
|
|
@@ -2488,11 +2605,10 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2488
2605
|
failure: report.kind,
|
|
2489
2606
|
released: report.released
|
|
2490
2607
|
});
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
} catch {
|
|
2608
|
+
const pending = pendingCancelledParts.get(key);
|
|
2609
|
+
if (pending) {
|
|
2610
|
+
pendingCancelledParts.delete(key);
|
|
2611
|
+
await patchCancelledPart(pending, key, report);
|
|
2496
2612
|
}
|
|
2497
2613
|
};
|
|
2498
2614
|
const MAX_ABORT_ATTEMPTS = 3;
|
|
@@ -2726,6 +2842,12 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2726
2842
|
const slots = ensureSlots(s, (m) => void warnOnce(m));
|
|
2727
2843
|
const agents = cfg.agent;
|
|
2728
2844
|
const providers = cfg.provider;
|
|
2845
|
+
const configCreds = /* @__PURE__ */ new Set();
|
|
2846
|
+
for (const [id, entry] of Object.entries(providers ?? {})) {
|
|
2847
|
+
const options2 = entry.options;
|
|
2848
|
+
if (typeof options2?.apiKey === "string" && options2.apiKey.trim() !== "") configCreds.add(id);
|
|
2849
|
+
}
|
|
2850
|
+
s.configCreds = configCreds;
|
|
2729
2851
|
const known = /* @__PURE__ */ new Set();
|
|
2730
2852
|
for (const [name, def] of Object.entries(cfg.agent ?? {})) if (def?.model) s.models.set(name, def.model);
|
|
2731
2853
|
for (const slot of slots) {
|
|
@@ -2760,6 +2882,11 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2760
2882
|
const provider = slot.model.slice(0, slash);
|
|
2761
2883
|
const id = slot.model.slice(slash + 1);
|
|
2762
2884
|
const entry = providers?.[provider];
|
|
2885
|
+
if (!providerReady(s, slot.model)) {
|
|
2886
|
+
await warnOnce(
|
|
2887
|
+
`provider ${provider} is not connected in opencode; slot ${slot.index} (${slot.model}) is excluded from routing until it is authenticated (run \`opencode auth login ${provider}\` or remove the line)`
|
|
2888
|
+
);
|
|
2889
|
+
}
|
|
2763
2890
|
if (!entry) {
|
|
2764
2891
|
await warnOnce(`provider ${provider} is not configured in opencode.jsonc; slot ${slot.index} (${slot.model}) cannot resolve`);
|
|
2765
2892
|
continue;
|
|
@@ -2891,12 +3018,49 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2891
3018
|
await log("debug", `probe session ${sessionID} ended with an error; not cooling anything`);
|
|
2892
3019
|
return;
|
|
2893
3020
|
}
|
|
3021
|
+
const own = claimFor(sessionID);
|
|
3022
|
+
const missing2 = isModelMissingText(haystack2);
|
|
2894
3023
|
const status2 = typeof error?.data?.statusCode === "number" ? error.data.statusCode : void 0;
|
|
2895
3024
|
const auth2 = isAuthFailureText(haystack2) || status2 === 401 || status2 === 403;
|
|
2896
3025
|
const limited2 = isLimitText(haystack2) || status2 === 429;
|
|
2897
3026
|
const outage = isUnavailableText(haystack2);
|
|
2898
|
-
if (
|
|
2899
|
-
|
|
3027
|
+
if (missing2) {
|
|
3028
|
+
const unbound = !own && sessionID ? [...s.own.entries()].find(([, claim]) => !claim.g && claim.s === sessionID && haystack2.includes(claim.m)) : void 0;
|
|
3029
|
+
const target = own ?? (unbound ? { key: unbound[0], claim: unbound[1] } : void 0);
|
|
3030
|
+
const model2 = target?.claim.m;
|
|
3031
|
+
if (model2) {
|
|
3032
|
+
const strike2 = recordStrike(s, model2, MODEL_MISSING_REASON, MODEL_MISSING_COOLDOWN_MS);
|
|
3033
|
+
if (strike2) {
|
|
3034
|
+
await log("warn", `bench ${model2} (${MODEL_MISSING_REASON}, strike ${strike2.strikes}) after a terminal model-not-found error`, {
|
|
3035
|
+
model: model2,
|
|
3036
|
+
slot: target?.claim.k,
|
|
3037
|
+
session: sessionID
|
|
3038
|
+
});
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
if (target) {
|
|
3042
|
+
release(s, target.key);
|
|
3043
|
+
await log("warn", `released ${target.claim.v} claim on ${target.claim.m} after a terminal model-not-found error`, {
|
|
3044
|
+
base: target.claim.v,
|
|
3045
|
+
model: target.claim.m,
|
|
3046
|
+
slot: target.claim.k,
|
|
3047
|
+
session: sessionID
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
if (!limited2 && !outage && !auth2) {
|
|
3053
|
+
if (own) {
|
|
3054
|
+
release(s, own.key);
|
|
3055
|
+
await log("warn", `released ${own.claim.v} claim on ${own.claim.m} after a terminal session error`, {
|
|
3056
|
+
base: own.claim.v,
|
|
3057
|
+
model: own.claim.m,
|
|
3058
|
+
slot: own.claim.k,
|
|
3059
|
+
session: sessionID
|
|
3060
|
+
});
|
|
3061
|
+
}
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
2900
3064
|
const named = auth2 ? AUTH_FAILURE_REASON : outage ? UNAVAILABLE_REASON : limitReason(haystack2) ?? "rate-limited";
|
|
2901
3065
|
let stated2;
|
|
2902
3066
|
if (own) {
|
|
@@ -2928,6 +3092,15 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2928
3092
|
}
|
|
2929
3093
|
}
|
|
2930
3094
|
await failOver(sessionID, own?.claim.m ?? "?", named, { stated: stated2, cool: !own });
|
|
3095
|
+
if (own && s.own.has(own.key)) {
|
|
3096
|
+
release(s, own.key);
|
|
3097
|
+
await log("warn", `released ${own.claim.v} claim on ${own.claim.m} after a terminal session error (abort did not confirm)`, {
|
|
3098
|
+
base: own.claim.v,
|
|
3099
|
+
model: own.claim.m,
|
|
3100
|
+
slot: own.claim.k,
|
|
3101
|
+
session: sessionID
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
2931
3104
|
return;
|
|
2932
3105
|
}
|
|
2933
3106
|
if (event.type === "session.created") {
|
|
@@ -2941,6 +3114,24 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2941
3114
|
const sessionID = info2?.sessionID ?? part?.sessionID;
|
|
2942
3115
|
if (sessionID) noteActivity(s, sessionID);
|
|
2943
3116
|
}
|
|
3117
|
+
if (event.type === "message.part.updated") {
|
|
3118
|
+
const part = props.part;
|
|
3119
|
+
if (part?.type === "tool" && part.tool === "task" && part.state?.status === "error" && part.state.error === "Task cancelled" && part.id && part.sessionID && part.messageID && part.callID) {
|
|
3120
|
+
const key = claimKey(part.sessionID, part.callID);
|
|
3121
|
+
const full = part;
|
|
3122
|
+
const report = abortReports.get(key);
|
|
3123
|
+
if (report) {
|
|
3124
|
+
await patchCancelledPart(full, key, report);
|
|
3125
|
+
} else {
|
|
3126
|
+
pendingCancelledParts.set(key, full);
|
|
3127
|
+
if (pendingCancelledParts.size > 32) {
|
|
3128
|
+
const oldest = pendingCancelledParts.keys().next().value;
|
|
3129
|
+
if (oldest !== void 0) pendingCancelledParts.delete(oldest);
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
2944
3135
|
if (event.type !== "message.updated") return;
|
|
2945
3136
|
const info = props.info;
|
|
2946
3137
|
if (!info || info.role !== "assistant" || !info.error) return;
|
|
@@ -2950,13 +3141,38 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2950
3141
|
const limited = isLimitText(haystack) || status === 429;
|
|
2951
3142
|
const gateway5xx = status !== void 0 && status >= 500 && status <= 599;
|
|
2952
3143
|
const transientGateway = gateway5xx || isTransientGatewayText(haystack);
|
|
2953
|
-
|
|
3144
|
+
const missing = isModelMissingText(haystack);
|
|
3145
|
+
if (!limited && !transientGateway && !auth && !missing) return;
|
|
2954
3146
|
const model = `${info.providerID ?? "?"}/${info.modelID ?? "?"}`;
|
|
2955
3147
|
if (info.sessionID && isProbe(s, info.sessionID)) {
|
|
2956
3148
|
await log("debug", `probe of ${model} hit a limit (${isLimitText(haystack) ? "limit" : "transient"}); leaving the cooldown to the probe`);
|
|
2957
3149
|
return;
|
|
2958
3150
|
}
|
|
2959
3151
|
const ownMsg = claimFor(info.sessionID);
|
|
3152
|
+
if (missing) {
|
|
3153
|
+
const ownMsgMissing = ownMsg;
|
|
3154
|
+
const missingModel = ownMsgMissing?.claim.m ?? model;
|
|
3155
|
+
if (missingModel !== "?/?") {
|
|
3156
|
+
const strike2 = recordStrike(s, missingModel, MODEL_MISSING_REASON, MODEL_MISSING_COOLDOWN_MS);
|
|
3157
|
+
if (strike2) {
|
|
3158
|
+
await log("warn", `bench ${missingModel} (${MODEL_MISSING_REASON}, strike ${strike2.strikes}) after a model-not-found error`, {
|
|
3159
|
+
model: missingModel,
|
|
3160
|
+
slot: ownMsgMissing?.claim.k,
|
|
3161
|
+
session: info.sessionID
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
if (ownMsgMissing) {
|
|
3166
|
+
release(s, ownMsgMissing.key);
|
|
3167
|
+
await log("warn", `released ${ownMsgMissing.claim.v} claim on ${ownMsgMissing.claim.m} after a model-not-found error`, {
|
|
3168
|
+
base: ownMsgMissing.claim.v,
|
|
3169
|
+
model: ownMsgMissing.claim.m,
|
|
3170
|
+
slot: ownMsgMissing.claim.k,
|
|
3171
|
+
session: info.sessionID
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
2960
3176
|
if (isUnavailableText(haystack) || !limited && !auth && transientGateway) {
|
|
2961
3177
|
const transient = coolUnavailable(s, model);
|
|
2962
3178
|
if (transient) await log("warn", `cooling ${model} for ${Math.round((transient.until - Date.now()) / 1e3)}s (${transient.reason})`);
|
|
@@ -2991,6 +3207,7 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2991
3207
|
}
|
|
2992
3208
|
try {
|
|
2993
3209
|
await reapStuck();
|
|
3210
|
+
await refreshProviders();
|
|
2994
3211
|
refreshCatalog(s, serverUrl);
|
|
2995
3212
|
const snap = snapshot(s);
|
|
2996
3213
|
if (BASES.includes(requested)) {
|
|
@@ -3030,7 +3247,7 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
3030
3247
|
if (!report) return;
|
|
3031
3248
|
abortReports.delete(key);
|
|
3032
3249
|
const detail = [
|
|
3033
|
-
`
|
|
3250
|
+
`Task Cancelled by LaCode: ${report.message}`,
|
|
3034
3251
|
`slot ${report.slot}; model ${report.model}; elapsed ${report.elapsedMs}ms; failure ${report.kind}; claim ${report.released ? "released" : "held"}`
|
|
3035
3252
|
].join("\n");
|
|
3036
3253
|
const text = typeof output.output === "string" ? output.output : "";
|
|
@@ -3045,6 +3262,7 @@ ${detail}` : detail;
|
|
|
3045
3262
|
models: tool.schema.array(tool.schema.string()).optional().describe("Model ids to validate, as provider/model-id. Omit to probe only the models that are cooling.")
|
|
3046
3263
|
},
|
|
3047
3264
|
async execute(args) {
|
|
3265
|
+
await refreshProviders(true);
|
|
3048
3266
|
const models = args?.models;
|
|
3049
3267
|
const report = models && models.length > 0 ? await runValidation(models) : await runProbe();
|
|
3050
3268
|
return { title: `agent-pool: ${reportTitle(report)}`, output: reportText(report) };
|
|
@@ -3054,6 +3272,7 @@ ${detail}` : detail;
|
|
|
3054
3272
|
description: "Show live pool load across every opencode process, per free model and per agent type, each slot's load against its ceiling, the slot marked <- next that the next spawn would take, and which models are cooling after a rate limit or a stuck task; COOLING/UNAVAILABLE = not routable. Check it before launching many parallel subagents.",
|
|
3055
3273
|
args: {},
|
|
3056
3274
|
async execute() {
|
|
3275
|
+
await refreshProviders(true);
|
|
3057
3276
|
refreshCatalog(s, serverUrl);
|
|
3058
3277
|
const slots = ensureSlots(s, (m) => void warnOnce(m));
|
|
3059
3278
|
const snap = snapshot(s);
|
|
@@ -3076,6 +3295,7 @@ ${detail}` : detail;
|
|
|
3076
3295
|
const status = s.catalog.get(slot.model);
|
|
3077
3296
|
const notes = [
|
|
3078
3297
|
strike ? `UNAVAILABLE (COOLING ${Math.ceil((strike.until - now) / 1e3)}s, ${strike.reason}, strike ${strike.strikes})` : "",
|
|
3298
|
+
!providerReady(s, slot.model) ? `UNAVAILABLE (provider-unconfigured: ${providerOf(slot.model)}; run \`opencode auth login ${providerOf(slot.model)}\`)` : "",
|
|
3079
3299
|
status && status !== "active" && status !== "unknown" ? `catalog:${status}` : ""
|
|
3080
3300
|
].filter(Boolean);
|
|
3081
3301
|
const marker = slot.index === next.index ? " <- next" : "";
|
|
@@ -6529,20 +6749,32 @@ function timelineHooks(input, options) {
|
|
|
6529
6749
|
}
|
|
6530
6750
|
}
|
|
6531
6751
|
}
|
|
6752
|
+
async function resolveRootSession(id) {
|
|
6753
|
+
let current = id;
|
|
6754
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6755
|
+
while (true) {
|
|
6756
|
+
if (seen.has(current)) return current;
|
|
6757
|
+
seen.add(current);
|
|
6758
|
+
const info = await client.session.get({ path: { id: current }, query: { directory } }).then((result) => result?.data).catch(() => void 0);
|
|
6759
|
+
if (!info?.parentID) return current;
|
|
6760
|
+
current = info.parentID;
|
|
6761
|
+
}
|
|
6762
|
+
}
|
|
6532
6763
|
async function reprocess(id, abort) {
|
|
6533
|
-
|
|
6534
|
-
if (
|
|
6535
|
-
|
|
6536
|
-
|
|
6764
|
+
const rootId = await resolveRootSession(id);
|
|
6765
|
+
if (reprocessing.has(rootId)) return "A timeline reprocess is already running for this session.";
|
|
6766
|
+
if (sessions.get(rootId)?.running) return "The timeline is currently summarizing this session; try again in a moment.";
|
|
6767
|
+
reprocessing.add(rootId);
|
|
6768
|
+
const targetFile = timelineFile(rootId);
|
|
6537
6769
|
const temp = targetFile + ".reprocess." + process.pid + "." + Date.now() + ".tmp";
|
|
6538
6770
|
let cursor;
|
|
6539
6771
|
let entries = 0;
|
|
6540
6772
|
let calls = 0;
|
|
6541
6773
|
try {
|
|
6542
|
-
const infoRes = await client.session.get({ path: { id }, query: { directory } });
|
|
6774
|
+
const infoRes = await client.session.get({ path: { id: rootId }, query: { directory } });
|
|
6543
6775
|
const info = infoRes?.data;
|
|
6544
6776
|
if (!info) return "Session not found.";
|
|
6545
|
-
const msgRes = await client.session.messages({ path: { id }, query: { directory } });
|
|
6777
|
+
const msgRes = await client.session.messages({ path: { id: rootId }, query: { directory } });
|
|
6546
6778
|
const messages = msgRes?.data ?? [];
|
|
6547
6779
|
writeFileSync8(temp, "");
|
|
6548
6780
|
for (let i = 0; i < MAX_REPROCESS_SLICES; i++) {
|
|
@@ -6556,7 +6788,7 @@ function timelineHooks(input, options) {
|
|
|
6556
6788
|
const excerpt = buildExcerpt(messages, cursor);
|
|
6557
6789
|
if (!excerpt.text) break;
|
|
6558
6790
|
const prior = readEntriesAt(temp, 4);
|
|
6559
|
-
const result = await summarize(
|
|
6791
|
+
const result = await summarize(rootId, info.title ?? "untitled", excerpt.text, prior, abort);
|
|
6560
6792
|
calls++;
|
|
6561
6793
|
if (!result.ok) {
|
|
6562
6794
|
try {
|
|
@@ -6585,7 +6817,7 @@ function timelineHooks(input, options) {
|
|
|
6585
6817
|
return "No entries generated; previous timeline kept.";
|
|
6586
6818
|
}
|
|
6587
6819
|
renameSync6(temp, targetFile);
|
|
6588
|
-
const state2 = sessions.get(
|
|
6820
|
+
const state2 = sessions.get(rootId);
|
|
6589
6821
|
if (state2) {
|
|
6590
6822
|
state2.cursor = cursor;
|
|
6591
6823
|
state2.failures = 0;
|
|
@@ -6599,11 +6831,11 @@ function timelineHooks(input, options) {
|
|
|
6599
6831
|
}
|
|
6600
6832
|
return `Reprocess failed: ${String(error)}. Previous timeline kept.`;
|
|
6601
6833
|
} finally {
|
|
6602
|
-
reprocessing.delete(
|
|
6834
|
+
reprocessing.delete(rootId);
|
|
6603
6835
|
}
|
|
6604
6836
|
}
|
|
6605
6837
|
const timelineReprocess = tool2({
|
|
6606
|
-
description: "Rebuild the
|
|
6838
|
+
description: "Rebuild the root session's entire timeline from its transcript, replacing the existing entries. Used by /reprocess-timeline.",
|
|
6607
6839
|
args: {},
|
|
6608
6840
|
execute: async (_args, ctx) => {
|
|
6609
6841
|
const output = await reprocess(ctx.sessionID, ctx.abort);
|
package/dist/lacode.js
CHANGED
|
@@ -43,9 +43,11 @@ var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
|
43
43
|
|
|
44
44
|
// src/lib/pool.ts
|
|
45
45
|
var CLAIM_TTL_MS = 10 * 60 * 1e3;
|
|
46
|
+
var UNBOUND_CLAIM_MS = 2 * 60 * 1e3;
|
|
46
47
|
var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
|
|
47
48
|
var ORPHAN_TMP_MS = 60 * 1e3;
|
|
48
49
|
var CATALOG_TTL_MS = 10 * 60 * 1e3;
|
|
50
|
+
var PROVIDER_LIST_TTL_MS = 60 * 1e3;
|
|
49
51
|
var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
|
|
50
52
|
var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
51
53
|
var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
package/dist/pool.js
CHANGED
|
@@ -25,9 +25,11 @@ var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
|
25
25
|
|
|
26
26
|
// src/lib/pool.ts
|
|
27
27
|
var CLAIM_TTL_MS = 10 * 60 * 1e3;
|
|
28
|
+
var UNBOUND_CLAIM_MS = 2 * 60 * 1e3;
|
|
28
29
|
var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
|
|
29
30
|
var ORPHAN_TMP_MS = 60 * 1e3;
|
|
30
31
|
var CATALOG_TTL_MS = 10 * 60 * 1e3;
|
|
32
|
+
var PROVIDER_LIST_TTL_MS = 60 * 1e3;
|
|
31
33
|
var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
|
|
32
34
|
var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
33
35
|
var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
|
@@ -294,6 +296,11 @@ function state(opts = {}) {
|
|
|
294
296
|
reaping: false,
|
|
295
297
|
lastSweep: 0,
|
|
296
298
|
lastDirSweep: 0,
|
|
299
|
+
connected: /* @__PURE__ */ new Set(),
|
|
300
|
+
connectedAt: 0,
|
|
301
|
+
connectedReady: false,
|
|
302
|
+
credentialed: /* @__PURE__ */ new Set(),
|
|
303
|
+
configCreds: /* @__PURE__ */ new Set(),
|
|
297
304
|
id,
|
|
298
305
|
dir,
|
|
299
306
|
modelsFile,
|
|
@@ -401,9 +408,16 @@ function sweepStateDir(s) {
|
|
|
401
408
|
}
|
|
402
409
|
return removed;
|
|
403
410
|
}
|
|
411
|
+
var claimExpired = (claim, now) => now - (claim.hb ?? claim.t) > CLAIM_TTL_MS || !claim.g && now - claim.t > UNBOUND_CLAIM_MS;
|
|
404
412
|
function pruneOwn(s) {
|
|
405
413
|
const now = Date.now();
|
|
406
|
-
|
|
414
|
+
let removed = false;
|
|
415
|
+
for (const [key, claim] of s.own)
|
|
416
|
+
if (claimExpired(claim, now)) {
|
|
417
|
+
s.own.delete(key);
|
|
418
|
+
removed = true;
|
|
419
|
+
}
|
|
420
|
+
if (removed) publish(s);
|
|
407
421
|
}
|
|
408
422
|
function pruneActivity(s, now) {
|
|
409
423
|
if (s.activity.size === 0) return;
|
|
@@ -431,7 +445,7 @@ function publish(s) {
|
|
|
431
445
|
if (!s.dirReady) return;
|
|
432
446
|
const now = Date.now();
|
|
433
447
|
const payload = {};
|
|
434
|
-
for (const [key, claim] of s.own) if (
|
|
448
|
+
for (const [key, claim] of s.own) if (!claimExpired(claim, now)) payload[key] = claim;
|
|
435
449
|
try {
|
|
436
450
|
writeFileSync(join2(s.dir, `${selfFile(s.id)}.tmp`), JSON.stringify(payload));
|
|
437
451
|
renameSync(join2(s.dir, `${selfFile(s.id)}.tmp`), join2(s.dir, selfFile(s.id)));
|
|
@@ -490,7 +504,7 @@ function readSiblings(s) {
|
|
|
490
504
|
try {
|
|
491
505
|
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
492
506
|
const claims = Object.values(parsed).filter(
|
|
493
|
-
(c) => c && typeof c.v === "string" && typeof c.m === "string" && typeof c.k === "number" && Number.isFinite(c.k) &&
|
|
507
|
+
(c) => c && typeof c.v === "string" && typeof c.m === "string" && typeof c.k === "number" && Number.isFinite(c.k) && !claimExpired(c, now)
|
|
494
508
|
);
|
|
495
509
|
siblings.push({ id: match[1], claims });
|
|
496
510
|
} catch {
|
|
@@ -629,6 +643,12 @@ function clearCooldowns(s, models) {
|
|
|
629
643
|
var markProbe = (s, sessionID) => s.probes.add(sessionID);
|
|
630
644
|
var unmarkProbe = (s, sessionID) => s.probes.delete(sessionID);
|
|
631
645
|
var isProbe = (s, sessionID) => s.probes.has(sessionID);
|
|
646
|
+
function providerReady(s, model) {
|
|
647
|
+
if (!s.connectedReady) return true;
|
|
648
|
+
const provider = providerOf(model);
|
|
649
|
+
if (!s.connected.has(provider)) return false;
|
|
650
|
+
return s.credentialed.has(provider) || s.configCreds.has(provider) || provider === "opencode";
|
|
651
|
+
}
|
|
632
652
|
function refreshCatalog(s, serverUrl) {
|
|
633
653
|
if (!serverUrl || Date.now() - s.catalogAt < CATALOG_TTL_MS) return;
|
|
634
654
|
s.catalogAt = Date.now();
|
|
@@ -645,6 +665,7 @@ function refreshCatalog(s, serverUrl) {
|
|
|
645
665
|
})();
|
|
646
666
|
}
|
|
647
667
|
function isAvailable(s, model, cooling) {
|
|
668
|
+
if (!providerReady(s, model)) return false;
|
|
648
669
|
if (cooling.has(model)) return false;
|
|
649
670
|
const status = s.catalog.get(model);
|
|
650
671
|
return status === void 0 || status === "active" || status === "unknown";
|
|
@@ -664,8 +685,9 @@ var loadOf = (counts, slot) => counts.get(slot) ?? 0;
|
|
|
664
685
|
var underCeiling = (slot, counts) => loadOf(counts, slot.index) < slotCeiling(slot);
|
|
665
686
|
function pickSlot(s, snap) {
|
|
666
687
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
667
|
-
const
|
|
668
|
-
const
|
|
688
|
+
const configured = slots.filter((slot) => providerReady(s, slot.model));
|
|
689
|
+
const usable = configured.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
690
|
+
const pool = usable.length > 0 ? usable : configured.length > 0 ? configured : slots;
|
|
669
691
|
for (const slot of usable) {
|
|
670
692
|
if (loadOf(snap.counts, slot.index) < slot.weight) return slot;
|
|
671
693
|
}
|
|
@@ -797,6 +819,7 @@ export {
|
|
|
797
819
|
ORPHAN_TMP_MS,
|
|
798
820
|
PACKAGE_ROOT,
|
|
799
821
|
PRESETS_DIR,
|
|
822
|
+
PROVIDER_LIST_TTL_MS,
|
|
800
823
|
QUOTA_COOLDOWN_BASE_MS,
|
|
801
824
|
QUOTA_COOLDOWN_MAX_MS,
|
|
802
825
|
SLOTS_WARN_AT,
|
|
@@ -806,6 +829,7 @@ export {
|
|
|
806
829
|
STUCK_MIN_AGE_MS,
|
|
807
830
|
UNAVAILABLE_COOLDOWN_MS,
|
|
808
831
|
UNAVAILABLE_REASON,
|
|
832
|
+
UNBOUND_CLAIM_MS,
|
|
809
833
|
USER_CONFIG_DIR,
|
|
810
834
|
USER_MODELS_PATH,
|
|
811
835
|
USER_MODELS_TXT,
|
|
@@ -841,6 +865,7 @@ export {
|
|
|
841
865
|
pickSlot,
|
|
842
866
|
poolPass,
|
|
843
867
|
providerOf,
|
|
868
|
+
providerReady,
|
|
844
869
|
pruneOwn,
|
|
845
870
|
pruneSessions,
|
|
846
871
|
publish,
|
package/dist/tunnel.js
CHANGED
|
@@ -36,9 +36,11 @@ var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
|
36
36
|
|
|
37
37
|
// src/lib/pool.ts
|
|
38
38
|
var CLAIM_TTL_MS = 10 * 60 * 1e3;
|
|
39
|
+
var UNBOUND_CLAIM_MS = 2 * 60 * 1e3;
|
|
39
40
|
var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
|
|
40
41
|
var ORPHAN_TMP_MS = 60 * 1e3;
|
|
41
42
|
var CATALOG_TTL_MS = 10 * 60 * 1e3;
|
|
43
|
+
var PROVIDER_LIST_TTL_MS = 60 * 1e3;
|
|
42
44
|
var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
|
|
43
45
|
var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
44
46
|
var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
package/package.json
CHANGED