@lawaty/lacode 1.2.0 → 1.3.0-beta.1
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 +3 -1
- package/dist/commands.js +3 -1
- package/dist/index.js +437 -30
- package/dist/lacode.js +5 -3
- package/dist/pool.js +83 -6
- package/dist/tunnel.js +5 -3
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/lib/pool.ts
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join as join2 } from "node:path";
|
|
5
5
|
|
|
@@ -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
|
@@ -3,7 +3,7 @@ var AGENT_INSTRUCTIONS_COMMAND = "agent-instructions";
|
|
|
3
3
|
var AGENT_INSTRUCTIONS_WRITER = "instructions";
|
|
4
4
|
|
|
5
5
|
// src/lib/pool.ts
|
|
6
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { join as join2 } from "node:path";
|
|
9
9
|
|
|
@@ -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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/lib/pool.ts
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join as join2 } from "node:path";
|
|
5
5
|
|
|
@@ -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
|
}
|
|
@@ -777,6 +799,56 @@ function coolProvider(s, provider, until, reason) {
|
|
|
777
799
|
}
|
|
778
800
|
return affected;
|
|
779
801
|
}
|
|
802
|
+
function recordFailure(s, f) {
|
|
803
|
+
try {
|
|
804
|
+
const path5 = join2(s.dir, "failures.jsonl");
|
|
805
|
+
const trimSize = 64 * 1024;
|
|
806
|
+
const keepLines = 200;
|
|
807
|
+
if (existsSync(path5)) {
|
|
808
|
+
const st = statSync(path5);
|
|
809
|
+
if (typeof st.size === "number" && st.size > trimSize) {
|
|
810
|
+
try {
|
|
811
|
+
const content = readFileSync2(path5, "utf8");
|
|
812
|
+
const parts = content.split("\n");
|
|
813
|
+
const lines = parts.filter((line) => line.length > 0);
|
|
814
|
+
if (lines.length > keepLines) {
|
|
815
|
+
const kept = lines.slice(lines.length - keepLines);
|
|
816
|
+
writeFileSync(`${path5}.tmp`, kept.join("\n") + "\n");
|
|
817
|
+
renameSync(`${path5}.tmp`, path5);
|
|
818
|
+
} else if (lines.length === 0 && parts.length >= 1) {
|
|
819
|
+
}
|
|
820
|
+
} catch {
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
appendFileSync(path5, JSON.stringify(f) + "\n");
|
|
825
|
+
} catch (error) {
|
|
826
|
+
console.error("[agent-pool] failed to record failure:", String(error));
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function readFailures(s, limit) {
|
|
830
|
+
const path5 = join2(s.dir, "failures.jsonl");
|
|
831
|
+
try {
|
|
832
|
+
if (!existsSync(path5)) return [];
|
|
833
|
+
const content = readFileSync2(path5, "utf8");
|
|
834
|
+
const parts = content.split("\n");
|
|
835
|
+
const records = [];
|
|
836
|
+
for (let i = 0; i < parts.length; i++) {
|
|
837
|
+
const line = parts[i];
|
|
838
|
+
if (line.length === 0) continue;
|
|
839
|
+
try {
|
|
840
|
+
const parsed = JSON.parse(line);
|
|
841
|
+
records.push(parsed);
|
|
842
|
+
} catch {
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (limit <= 0) return [];
|
|
846
|
+
if (records.length <= limit) return records;
|
|
847
|
+
return records.slice(records.length - limit);
|
|
848
|
+
} catch {
|
|
849
|
+
return [];
|
|
850
|
+
}
|
|
851
|
+
}
|
|
780
852
|
|
|
781
853
|
// src/lib/agent-instructions.ts
|
|
782
854
|
var AGENT_INSTRUCTIONS_COMMAND = "agent-instructions";
|
|
@@ -2439,6 +2511,63 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2439
2511
|
s.warned.add(message);
|
|
2440
2512
|
await log("warn", message);
|
|
2441
2513
|
};
|
|
2514
|
+
const refreshProviders = async (force = false) => {
|
|
2515
|
+
if (!force && Date.now() - s.connectedAt < PROVIDER_LIST_TTL_MS) return;
|
|
2516
|
+
s.connectedAt = Date.now();
|
|
2517
|
+
try {
|
|
2518
|
+
const providerApi = client.provider;
|
|
2519
|
+
if (!providerApi || typeof providerApi.list !== "function") return;
|
|
2520
|
+
const result = await providerApi.list({ query: { directory } });
|
|
2521
|
+
const data = result?.data;
|
|
2522
|
+
const connected = data?.connected;
|
|
2523
|
+
const all = data?.all;
|
|
2524
|
+
if (Array.isArray(connected) && Array.isArray(all)) {
|
|
2525
|
+
s.connected = new Set(connected.filter((id) => typeof id === "string"));
|
|
2526
|
+
const credentialed = /* @__PURE__ */ new Set();
|
|
2527
|
+
for (const entry of all) {
|
|
2528
|
+
if (!entry || typeof entry !== "object") continue;
|
|
2529
|
+
const rec = entry;
|
|
2530
|
+
const id = typeof rec.id === "string" ? rec.id : void 0;
|
|
2531
|
+
if (!id) continue;
|
|
2532
|
+
if (typeof rec.key === "string" && rec.key.trim() !== "") {
|
|
2533
|
+
credentialed.add(id);
|
|
2534
|
+
continue;
|
|
2535
|
+
}
|
|
2536
|
+
const options2 = rec.options;
|
|
2537
|
+
if (options2 && typeof options2 === "object") {
|
|
2538
|
+
const opts2 = options2;
|
|
2539
|
+
if (typeof opts2.apiKey === "string" && opts2.apiKey.trim() !== "") {
|
|
2540
|
+
credentialed.add(id);
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2543
|
+
const baseURL = typeof opts2.baseURL === "string" ? opts2.baseURL.trim() : "";
|
|
2544
|
+
if (baseURL !== "") {
|
|
2545
|
+
const envArr2 = Array.isArray(rec.env) ? rec.env : [];
|
|
2546
|
+
const hasCredEnv = envArr2.some((e) => typeof e === "string" && /key|token|secret|password|credential|auth/i.test(e));
|
|
2547
|
+
if (!hasCredEnv) {
|
|
2548
|
+
credentialed.add(id);
|
|
2549
|
+
continue;
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
const envArr = Array.isArray(rec.env) ? rec.env : [];
|
|
2554
|
+
for (const e of envArr) {
|
|
2555
|
+
if (typeof e === "string" && /key|token|secret|password|credential|auth/i.test(e)) {
|
|
2556
|
+
if (typeof process.env[e] === "string" && process.env[e].trim() !== "") {
|
|
2557
|
+
credentialed.add(id);
|
|
2558
|
+
break;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
s.credentialed = credentialed;
|
|
2564
|
+
s.connectedReady = true;
|
|
2565
|
+
} else if (Array.isArray(connected)) {
|
|
2566
|
+
s.connected = new Set(connected.filter((id) => typeof id === "string"));
|
|
2567
|
+
}
|
|
2568
|
+
} catch {
|
|
2569
|
+
}
|
|
2570
|
+
};
|
|
2442
2571
|
init(s, (m) => void warnOnce(m));
|
|
2443
2572
|
const sweepDirIfDue = () => {
|
|
2444
2573
|
if (Date.now() - s.lastDirSweep < STATE_SWEEP_INTERVAL_MS) return;
|
|
@@ -2466,6 +2595,8 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2466
2595
|
}
|
|
2467
2596
|
};
|
|
2468
2597
|
const abortReports = /* @__PURE__ */ new Map();
|
|
2598
|
+
const spawns = /* @__PURE__ */ new Map();
|
|
2599
|
+
const observedErrors = /* @__PURE__ */ new Map();
|
|
2469
2600
|
const abortKind = (reason) => isQuotaReason(reason) || isLimitText(reason) ? "rate-limit" : "provider error";
|
|
2470
2601
|
const abortLabel = (kind, elapsedMs) => {
|
|
2471
2602
|
const seconds = Math.max(1, Math.round(elapsedMs / 1e3));
|
|
@@ -2475,6 +2606,60 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2475
2606
|
if (kind === "permission") return `permission denied after ${seconds}s`;
|
|
2476
2607
|
return `provider error after ${seconds}s`;
|
|
2477
2608
|
};
|
|
2609
|
+
const pendingCancelledParts = /* @__PURE__ */ new Map();
|
|
2610
|
+
const patchedCancelledParts = /* @__PURE__ */ new Set();
|
|
2611
|
+
const cancelledPrefix = "Task Cancelled by LaCode:";
|
|
2612
|
+
const patchCancelledPart = async (part, key, report) => {
|
|
2613
|
+
if (patchedCancelledParts.has(key)) return;
|
|
2614
|
+
patchedCancelledParts.add(key);
|
|
2615
|
+
if (patchedCancelledParts.size > 64) {
|
|
2616
|
+
const oldest = patchedCancelledParts.values().next().value;
|
|
2617
|
+
if (oldest !== void 0) patchedCancelledParts.delete(oldest);
|
|
2618
|
+
}
|
|
2619
|
+
const error = `${cancelledPrefix} ${report.message}`;
|
|
2620
|
+
const updated = { ...part, state: { ...part.state, error } };
|
|
2621
|
+
try {
|
|
2622
|
+
const updater = client.part?.update;
|
|
2623
|
+
if (typeof updater === "function") {
|
|
2624
|
+
const result = await updater({
|
|
2625
|
+
sessionID: part.sessionID,
|
|
2626
|
+
messageID: part.messageID,
|
|
2627
|
+
partID: part.id,
|
|
2628
|
+
directory,
|
|
2629
|
+
part: updated,
|
|
2630
|
+
signal: AbortSignal.timeout(5e3)
|
|
2631
|
+
});
|
|
2632
|
+
if (!result?.response?.ok) throw new Error(`server answered HTTP ${result?.response?.status ?? "?"}`);
|
|
2633
|
+
abortReports.delete(key);
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
const url = new URL(
|
|
2637
|
+
`/session/${encodeURIComponent(part.sessionID)}/message/${encodeURIComponent(part.messageID)}/part/${encodeURIComponent(part.id)}`,
|
|
2638
|
+
serverUrl
|
|
2639
|
+
);
|
|
2640
|
+
if (directory) url.searchParams.set("directory", directory);
|
|
2641
|
+
const headers = { "Content-Type": "application/json" };
|
|
2642
|
+
if (directory) headers["x-opencode-directory"] = directory;
|
|
2643
|
+
const password = process.env.OPENCODE_SERVER_PASSWORD;
|
|
2644
|
+
if (password) {
|
|
2645
|
+
const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode";
|
|
2646
|
+
headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
2647
|
+
}
|
|
2648
|
+
const response = await fetch(url, {
|
|
2649
|
+
method: "PATCH",
|
|
2650
|
+
headers,
|
|
2651
|
+
body: JSON.stringify({ ...part, state: { ...part.state, error } }),
|
|
2652
|
+
signal: AbortSignal.timeout(5e3)
|
|
2653
|
+
});
|
|
2654
|
+
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`.trim());
|
|
2655
|
+
abortReports.delete(key);
|
|
2656
|
+
} catch (cause) {
|
|
2657
|
+
await log("warn", `could not rewrite cancelled task ${part.callID}: ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
2658
|
+
session: part.sessionID,
|
|
2659
|
+
callID: part.callID
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
};
|
|
2478
2663
|
const noteAbort = async (key, report) => {
|
|
2479
2664
|
abortReports.set(key, report);
|
|
2480
2665
|
if (abortReports.size > 32) {
|
|
@@ -2488,11 +2673,10 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2488
2673
|
failure: report.kind,
|
|
2489
2674
|
released: report.released
|
|
2490
2675
|
});
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
} catch {
|
|
2676
|
+
const pending = pendingCancelledParts.get(key);
|
|
2677
|
+
if (pending) {
|
|
2678
|
+
pendingCancelledParts.delete(key);
|
|
2679
|
+
await patchCancelledPart(pending, key, report);
|
|
2496
2680
|
}
|
|
2497
2681
|
};
|
|
2498
2682
|
const MAX_ABORT_ATTEMPTS = 3;
|
|
@@ -2726,6 +2910,12 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2726
2910
|
const slots = ensureSlots(s, (m) => void warnOnce(m));
|
|
2727
2911
|
const agents = cfg.agent;
|
|
2728
2912
|
const providers = cfg.provider;
|
|
2913
|
+
const configCreds = /* @__PURE__ */ new Set();
|
|
2914
|
+
for (const [id, entry] of Object.entries(providers ?? {})) {
|
|
2915
|
+
const options2 = entry.options;
|
|
2916
|
+
if (typeof options2?.apiKey === "string" && options2.apiKey.trim() !== "") configCreds.add(id);
|
|
2917
|
+
}
|
|
2918
|
+
s.configCreds = configCreds;
|
|
2729
2919
|
const known = /* @__PURE__ */ new Set();
|
|
2730
2920
|
for (const [name, def] of Object.entries(cfg.agent ?? {})) if (def?.model) s.models.set(name, def.model);
|
|
2731
2921
|
for (const slot of slots) {
|
|
@@ -2760,6 +2950,11 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2760
2950
|
const provider = slot.model.slice(0, slash);
|
|
2761
2951
|
const id = slot.model.slice(slash + 1);
|
|
2762
2952
|
const entry = providers?.[provider];
|
|
2953
|
+
if (!providerReady(s, slot.model)) {
|
|
2954
|
+
await warnOnce(
|
|
2955
|
+
`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)`
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2763
2958
|
if (!entry) {
|
|
2764
2959
|
await warnOnce(`provider ${provider} is not configured in opencode.jsonc; slot ${slot.index} (${slot.model}) cannot resolve`);
|
|
2765
2960
|
continue;
|
|
@@ -2891,12 +3086,69 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2891
3086
|
await log("debug", `probe session ${sessionID} ended with an error; not cooling anything`);
|
|
2892
3087
|
return;
|
|
2893
3088
|
}
|
|
3089
|
+
const own = claimFor(sessionID);
|
|
3090
|
+
const missing2 = isModelMissingText(haystack2);
|
|
2894
3091
|
const status2 = typeof error?.data?.statusCode === "number" ? error.data.statusCode : void 0;
|
|
2895
3092
|
const auth2 = isAuthFailureText(haystack2) || status2 === 401 || status2 === 403;
|
|
2896
3093
|
const limited2 = isLimitText(haystack2) || status2 === 429;
|
|
2897
3094
|
const outage = isUnavailableText(haystack2);
|
|
2898
|
-
if (
|
|
2899
|
-
|
|
3095
|
+
if (missing2) {
|
|
3096
|
+
const unbound = !own && sessionID ? [...s.own.entries()].find(([, claim]) => !claim.g && claim.s === sessionID && haystack2.includes(claim.m)) : void 0;
|
|
3097
|
+
const target = own ?? (unbound ? { key: unbound[0], claim: unbound[1] } : void 0);
|
|
3098
|
+
const model2 = target?.claim.m;
|
|
3099
|
+
if (model2) {
|
|
3100
|
+
const strike2 = recordStrike(s, model2, MODEL_MISSING_REASON, MODEL_MISSING_COOLDOWN_MS);
|
|
3101
|
+
if (strike2) {
|
|
3102
|
+
await log("warn", `bench ${model2} (${MODEL_MISSING_REASON}, strike ${strike2.strikes}) after a terminal model-not-found error`, {
|
|
3103
|
+
model: model2,
|
|
3104
|
+
slot: target?.claim.k,
|
|
3105
|
+
session: sessionID
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
if (target) {
|
|
3110
|
+
release(s, target.key);
|
|
3111
|
+
await log("warn", `released ${target.claim.v} claim on ${target.claim.m} after a terminal model-not-found error`, {
|
|
3112
|
+
base: target.claim.v,
|
|
3113
|
+
model: target.claim.m,
|
|
3114
|
+
slot: target.claim.k,
|
|
3115
|
+
session: sessionID
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
if (!limited2 && !outage && !auth2) {
|
|
3121
|
+
if (own) {
|
|
3122
|
+
const detail = haystack2.slice(0, 400);
|
|
3123
|
+
recordFailure(s, {
|
|
3124
|
+
ts: Date.now(),
|
|
3125
|
+
source: "observed",
|
|
3126
|
+
base: own.claim.v,
|
|
3127
|
+
slot: own.claim.k,
|
|
3128
|
+
model: own.claim.m,
|
|
3129
|
+
session: sessionID,
|
|
3130
|
+
reason: "provider error",
|
|
3131
|
+
elapsedMs: Date.now() - own.claim.t,
|
|
3132
|
+
released: true,
|
|
3133
|
+
detail
|
|
3134
|
+
});
|
|
3135
|
+
if (!observedErrors.has(own.key)) {
|
|
3136
|
+
observedErrors.set(own.key, { reason: "provider error", detail });
|
|
3137
|
+
if (observedErrors.size > 32) {
|
|
3138
|
+
const oldest = observedErrors.keys().next().value;
|
|
3139
|
+
if (oldest !== void 0) observedErrors.delete(oldest);
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
release(s, own.key);
|
|
3143
|
+
await log("warn", `released ${own.claim.v} claim on ${own.claim.m} after a terminal session error`, {
|
|
3144
|
+
base: own.claim.v,
|
|
3145
|
+
model: own.claim.m,
|
|
3146
|
+
slot: own.claim.k,
|
|
3147
|
+
session: sessionID
|
|
3148
|
+
});
|
|
3149
|
+
}
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
2900
3152
|
const named = auth2 ? AUTH_FAILURE_REASON : outage ? UNAVAILABLE_REASON : limitReason(haystack2) ?? "rate-limited";
|
|
2901
3153
|
let stated2;
|
|
2902
3154
|
if (own) {
|
|
@@ -2928,6 +3180,15 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2928
3180
|
}
|
|
2929
3181
|
}
|
|
2930
3182
|
await failOver(sessionID, own?.claim.m ?? "?", named, { stated: stated2, cool: !own });
|
|
3183
|
+
if (own && s.own.has(own.key)) {
|
|
3184
|
+
release(s, own.key);
|
|
3185
|
+
await log("warn", `released ${own.claim.v} claim on ${own.claim.m} after a terminal session error (abort did not confirm)`, {
|
|
3186
|
+
base: own.claim.v,
|
|
3187
|
+
model: own.claim.m,
|
|
3188
|
+
slot: own.claim.k,
|
|
3189
|
+
session: sessionID
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
2931
3192
|
return;
|
|
2932
3193
|
}
|
|
2933
3194
|
if (event.type === "session.created") {
|
|
@@ -2941,6 +3202,39 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2941
3202
|
const sessionID = info2?.sessionID ?? part?.sessionID;
|
|
2942
3203
|
if (sessionID) noteActivity(s, sessionID);
|
|
2943
3204
|
}
|
|
3205
|
+
if (event.type === "message.part.updated") {
|
|
3206
|
+
const part = props.part;
|
|
3207
|
+
if (part?.type === "tool" && part.tool === "task" && part.state?.status === "error" && part.state.error === "Task cancelled" && part.id && part.sessionID && part.messageID && part.callID) {
|
|
3208
|
+
const key = claimKey(part.sessionID, part.callID);
|
|
3209
|
+
const full = part;
|
|
3210
|
+
const report = abortReports.get(key);
|
|
3211
|
+
if (report) {
|
|
3212
|
+
await patchCancelledPart(full, key, report);
|
|
3213
|
+
} else if (observedErrors.has(key)) {
|
|
3214
|
+
const spawn3 = spawns.get(key);
|
|
3215
|
+
const seen = observedErrors.get(key);
|
|
3216
|
+
const elapsedMs = spawn3 ? Date.now() - spawn3.t : 0;
|
|
3217
|
+
const failureReport = {
|
|
3218
|
+
slot: spawn3?.slot ?? 0,
|
|
3219
|
+
model: spawn3?.model ?? "?",
|
|
3220
|
+
elapsedMs,
|
|
3221
|
+
kind: seen.reason,
|
|
3222
|
+
released: true,
|
|
3223
|
+
message: `${spawn3?.model ?? "?"} \u2014 ${seen.reason}${seen.detail ? `: ${seen.detail.slice(0, 200)}` : ""}`
|
|
3224
|
+
};
|
|
3225
|
+
await patchCancelledPart(full, key, failureReport);
|
|
3226
|
+
observedErrors.delete(key);
|
|
3227
|
+
spawns.delete(key);
|
|
3228
|
+
} else {
|
|
3229
|
+
pendingCancelledParts.set(key, full);
|
|
3230
|
+
if (pendingCancelledParts.size > 32) {
|
|
3231
|
+
const oldest = pendingCancelledParts.keys().next().value;
|
|
3232
|
+
if (oldest !== void 0) pendingCancelledParts.delete(oldest);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
2944
3238
|
if (event.type !== "message.updated") return;
|
|
2945
3239
|
const info = props.info;
|
|
2946
3240
|
if (!info || info.role !== "assistant" || !info.error) return;
|
|
@@ -2950,13 +3244,62 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2950
3244
|
const limited = isLimitText(haystack) || status === 429;
|
|
2951
3245
|
const gateway5xx = status !== void 0 && status >= 500 && status <= 599;
|
|
2952
3246
|
const transientGateway = gateway5xx || isTransientGatewayText(haystack);
|
|
2953
|
-
|
|
3247
|
+
const missing = isModelMissingText(haystack);
|
|
3248
|
+
if (!limited && !transientGateway && !auth && !missing) {
|
|
3249
|
+
const ownMsgUnclassified = claimFor(info.sessionID);
|
|
3250
|
+
if (ownMsgUnclassified) {
|
|
3251
|
+
recordFailure(s, {
|
|
3252
|
+
ts: Date.now(),
|
|
3253
|
+
source: "observed",
|
|
3254
|
+
base: ownMsgUnclassified.claim.v,
|
|
3255
|
+
slot: ownMsgUnclassified.claim.k,
|
|
3256
|
+
model: ownMsgUnclassified.claim.m,
|
|
3257
|
+
session: info.sessionID,
|
|
3258
|
+
reason: "provider error",
|
|
3259
|
+
elapsedMs: Date.now() - ownMsgUnclassified.claim.t,
|
|
3260
|
+
released: false,
|
|
3261
|
+
detail: haystack.slice(0, 400)
|
|
3262
|
+
});
|
|
3263
|
+
if (!observedErrors.has(ownMsgUnclassified.key)) {
|
|
3264
|
+
observedErrors.set(ownMsgUnclassified.key, { reason: "provider error", detail: haystack.slice(0, 400) });
|
|
3265
|
+
if (observedErrors.size > 32) {
|
|
3266
|
+
const oldest = observedErrors.keys().next().value;
|
|
3267
|
+
if (oldest !== void 0) observedErrors.delete(oldest);
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
2954
3273
|
const model = `${info.providerID ?? "?"}/${info.modelID ?? "?"}`;
|
|
2955
3274
|
if (info.sessionID && isProbe(s, info.sessionID)) {
|
|
2956
3275
|
await log("debug", `probe of ${model} hit a limit (${isLimitText(haystack) ? "limit" : "transient"}); leaving the cooldown to the probe`);
|
|
2957
3276
|
return;
|
|
2958
3277
|
}
|
|
2959
3278
|
const ownMsg = claimFor(info.sessionID);
|
|
3279
|
+
if (missing) {
|
|
3280
|
+
const ownMsgMissing = ownMsg;
|
|
3281
|
+
const missingModel = ownMsgMissing?.claim.m ?? model;
|
|
3282
|
+
if (missingModel !== "?/?") {
|
|
3283
|
+
const strike2 = recordStrike(s, missingModel, MODEL_MISSING_REASON, MODEL_MISSING_COOLDOWN_MS);
|
|
3284
|
+
if (strike2) {
|
|
3285
|
+
await log("warn", `bench ${missingModel} (${MODEL_MISSING_REASON}, strike ${strike2.strikes}) after a model-not-found error`, {
|
|
3286
|
+
model: missingModel,
|
|
3287
|
+
slot: ownMsgMissing?.claim.k,
|
|
3288
|
+
session: info.sessionID
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
if (ownMsgMissing) {
|
|
3293
|
+
release(s, ownMsgMissing.key);
|
|
3294
|
+
await log("warn", `released ${ownMsgMissing.claim.v} claim on ${ownMsgMissing.claim.m} after a model-not-found error`, {
|
|
3295
|
+
base: ownMsgMissing.claim.v,
|
|
3296
|
+
model: ownMsgMissing.claim.m,
|
|
3297
|
+
slot: ownMsgMissing.claim.k,
|
|
3298
|
+
session: info.sessionID
|
|
3299
|
+
});
|
|
3300
|
+
}
|
|
3301
|
+
return;
|
|
3302
|
+
}
|
|
2960
3303
|
if (isUnavailableText(haystack) || !limited && !auth && transientGateway) {
|
|
2961
3304
|
const transient = coolUnavailable(s, model);
|
|
2962
3305
|
if (transient) await log("warn", `cooling ${model} for ${Math.round((transient.until - Date.now()) / 1e3)}s (${transient.reason})`);
|
|
@@ -2991,6 +3334,7 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2991
3334
|
}
|
|
2992
3335
|
try {
|
|
2993
3336
|
await reapStuck();
|
|
3337
|
+
await refreshProviders();
|
|
2994
3338
|
refreshCatalog(s, serverUrl);
|
|
2995
3339
|
const snap = snapshot(s);
|
|
2996
3340
|
if (BASES.includes(requested)) {
|
|
@@ -3008,7 +3352,9 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
3008
3352
|
load: (snap.counts.get(slot.index) ?? 0) + 1
|
|
3009
3353
|
});
|
|
3010
3354
|
output.args.subagent_type = variant;
|
|
3011
|
-
|
|
3355
|
+
const routeKey = claimKey(input.sessionID, input.callID);
|
|
3356
|
+
acquire(s, requested, slot, input.sessionID, routeKey);
|
|
3357
|
+
spawns.set(routeKey, { base: requested, slot: slot.index, model: slot.model, t: Date.now() });
|
|
3012
3358
|
return;
|
|
3013
3359
|
}
|
|
3014
3360
|
const pinned = s.models.get(requested);
|
|
@@ -3025,15 +3371,30 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
3025
3371
|
"tool.execute.after": async (input, output) => {
|
|
3026
3372
|
if (input.tool !== "task") return;
|
|
3027
3373
|
const key = claimKey(input.sessionID, input.callID);
|
|
3374
|
+
const spawn3 = spawns.get(key);
|
|
3028
3375
|
release(s, key);
|
|
3376
|
+
const text = typeof output.output === "string" ? output.output : "";
|
|
3377
|
+
const failed = abortReports.has(key) || /task\s+cancel+ed/i.test(text);
|
|
3378
|
+
spawns.delete(key);
|
|
3029
3379
|
const report = abortReports.get(key);
|
|
3030
3380
|
if (!report) return;
|
|
3031
3381
|
abortReports.delete(key);
|
|
3382
|
+
recordFailure(s, {
|
|
3383
|
+
ts: Date.now(),
|
|
3384
|
+
source: "pool",
|
|
3385
|
+
base: void 0,
|
|
3386
|
+
slot: report.slot,
|
|
3387
|
+
model: report.model,
|
|
3388
|
+
session: input.sessionID,
|
|
3389
|
+
reason: report.kind,
|
|
3390
|
+
elapsedMs: report.elapsedMs,
|
|
3391
|
+
released: report.released,
|
|
3392
|
+
detail: report.message
|
|
3393
|
+
});
|
|
3032
3394
|
const detail = [
|
|
3033
|
-
`
|
|
3395
|
+
`Task Cancelled by LaCode: ${report.message}`,
|
|
3034
3396
|
`slot ${report.slot}; model ${report.model}; elapsed ${report.elapsedMs}ms; failure ${report.kind}; claim ${report.released ? "released" : "held"}`
|
|
3035
3397
|
].join("\n");
|
|
3036
|
-
const text = typeof output.output === "string" ? output.output : "";
|
|
3037
3398
|
output.output = /task\s+cancel+ed/i.test(text) ? detail : text ? `${text}
|
|
3038
3399
|
|
|
3039
3400
|
${detail}` : detail;
|
|
@@ -3045,6 +3406,7 @@ ${detail}` : detail;
|
|
|
3045
3406
|
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
3407
|
},
|
|
3047
3408
|
async execute(args) {
|
|
3409
|
+
await refreshProviders(true);
|
|
3048
3410
|
const models = args?.models;
|
|
3049
3411
|
const report = models && models.length > 0 ? await runValidation(models) : await runProbe();
|
|
3050
3412
|
return { title: `agent-pool: ${reportTitle(report)}`, output: reportText(report) };
|
|
@@ -3054,6 +3416,7 @@ ${detail}` : detail;
|
|
|
3054
3416
|
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
3417
|
args: {},
|
|
3056
3418
|
async execute() {
|
|
3419
|
+
await refreshProviders(true);
|
|
3057
3420
|
refreshCatalog(s, serverUrl);
|
|
3058
3421
|
const slots = ensureSlots(s, (m) => void warnOnce(m));
|
|
3059
3422
|
const snap = snapshot(s);
|
|
@@ -3076,6 +3439,7 @@ ${detail}` : detail;
|
|
|
3076
3439
|
const status = s.catalog.get(slot.model);
|
|
3077
3440
|
const notes = [
|
|
3078
3441
|
strike ? `UNAVAILABLE (COOLING ${Math.ceil((strike.until - now) / 1e3)}s, ${strike.reason}, strike ${strike.strikes})` : "",
|
|
3442
|
+
!providerReady(s, slot.model) ? `UNAVAILABLE (provider-unconfigured: ${providerOf(slot.model)}; run \`opencode auth login ${providerOf(slot.model)}\`)` : "",
|
|
3079
3443
|
status && status !== "active" && status !== "unknown" ? `catalog:${status}` : ""
|
|
3080
3444
|
].filter(Boolean);
|
|
3081
3445
|
const marker = slot.index === next.index ? " <- next" : "";
|
|
@@ -3119,7 +3483,38 @@ one ordered pass in slot order: each slot fills to its weight, then overflow goe
|
|
|
3119
3483
|
// different situations and this is the line that tells them apart.
|
|
3120
3484
|
probeMs > 0 ? `cooldown probe: last ${s.lastProbe ? `${Math.max(0, Math.round((now - s.lastProbe) / 1e3))}s ago` : coolingNow.length > 0 ? "never in this process (cooldowns came from disk)" : "never (nothing has been cooling)"}, every ${Math.round(probeMs / 6e4)}m; call the pool_recheck tool (or run /pool-recheck) to ask now.` : "cooldown probe: off (probeMs: 0); call the pool_recheck tool (or run /pool-recheck) to ask now.",
|
|
3121
3485
|
watchMs > 0 ? `pool config watch: ${sources.join(", ")} every ${watchMs}ms; a change reloads the slot table and validates every slot.` : "pool config watch: off (watchMs: 0).",
|
|
3122
|
-
coolingNow.length === 0 ? "no models cooling down." : `unavailable/cooling: ${coolingNow.map(([m, st]) => `${m} for ${Math.ceil((st.until - now) / 1e3)}s`).join(", ")}
|
|
3486
|
+
coolingNow.length === 0 ? "no models cooling down." : `unavailable/cooling: ${coolingNow.map(([m, st]) => `${m} for ${Math.ceil((st.until - now) / 1e3)}s`).join(", ")}`,
|
|
3487
|
+
(() => {
|
|
3488
|
+
const recent = readFailures(s, 5);
|
|
3489
|
+
if (recent.length === 0) {
|
|
3490
|
+
return "recent failures: none recorded";
|
|
3491
|
+
}
|
|
3492
|
+
const all = readFailures(s, 1e3);
|
|
3493
|
+
const total2 = all.length;
|
|
3494
|
+
const fmtAge = (ts) => {
|
|
3495
|
+
const diff = Math.max(0, Date.now() - ts);
|
|
3496
|
+
const sAge = Math.floor(diff / 1e3);
|
|
3497
|
+
if (sAge < 60) return `${sAge}s`;
|
|
3498
|
+
const mAge = Math.floor(sAge / 60);
|
|
3499
|
+
if (mAge < 60) return `${mAge}m`;
|
|
3500
|
+
const hAge = Math.floor(mAge / 60);
|
|
3501
|
+
if (hAge < 24) return `${hAge}h`;
|
|
3502
|
+
const dAge = Math.floor(hAge / 24);
|
|
3503
|
+
return `${dAge}d`;
|
|
3504
|
+
};
|
|
3505
|
+
const lines = [`recent failures: ${total2} recorded in ${s.dir}/failures.jsonl`];
|
|
3506
|
+
const ordered = [...recent].reverse();
|
|
3507
|
+
for (const rec of ordered) {
|
|
3508
|
+
const age = fmtAge(rec.ts);
|
|
3509
|
+
const slot = rec.slot === void 0 || rec.slot === null ? "-" : rec.slot;
|
|
3510
|
+
const model = rec.model;
|
|
3511
|
+
const reason = rec.reason;
|
|
3512
|
+
const source = rec.source;
|
|
3513
|
+
const suffix = source === "observed" ? " (unclassified)" : "";
|
|
3514
|
+
lines.push(` ${age} ago slot ${slot} ${model} ${reason} [${source}]${suffix}`);
|
|
3515
|
+
}
|
|
3516
|
+
return lines.join("\n");
|
|
3517
|
+
})()
|
|
3123
3518
|
].join("\n")
|
|
3124
3519
|
};
|
|
3125
3520
|
}
|
|
@@ -5940,7 +6335,7 @@ import { writeFileSync as writeFileSync8, renameSync as renameSync6, rmSync as r
|
|
|
5940
6335
|
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
5941
6336
|
|
|
5942
6337
|
// src/lib/timeline.ts
|
|
5943
|
-
import { appendFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync10, rmSync as rmSync2 } from "node:fs";
|
|
6338
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync10, rmSync as rmSync2 } from "node:fs";
|
|
5944
6339
|
import { homedir as homedir7 } from "node:os";
|
|
5945
6340
|
import { dirname as dirname5, join as join11, resolve as resolve4 } from "node:path";
|
|
5946
6341
|
function timelineDir() {
|
|
@@ -5989,7 +6384,7 @@ function appendEntriesAt(file, texts, now, through) {
|
|
|
5989
6384
|
const ts = now ?? Date.now();
|
|
5990
6385
|
const line = { ts, text: t };
|
|
5991
6386
|
if (validThrough) line.through = validThrough;
|
|
5992
|
-
|
|
6387
|
+
appendFileSync2(file, JSON.stringify(line) + "\n");
|
|
5993
6388
|
count++;
|
|
5994
6389
|
}
|
|
5995
6390
|
return count;
|
|
@@ -6529,20 +6924,32 @@ function timelineHooks(input, options) {
|
|
|
6529
6924
|
}
|
|
6530
6925
|
}
|
|
6531
6926
|
}
|
|
6927
|
+
async function resolveRootSession(id) {
|
|
6928
|
+
let current = id;
|
|
6929
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6930
|
+
while (true) {
|
|
6931
|
+
if (seen.has(current)) return current;
|
|
6932
|
+
seen.add(current);
|
|
6933
|
+
const info = await client.session.get({ path: { id: current }, query: { directory } }).then((result) => result?.data).catch(() => void 0);
|
|
6934
|
+
if (!info?.parentID) return current;
|
|
6935
|
+
current = info.parentID;
|
|
6936
|
+
}
|
|
6937
|
+
}
|
|
6532
6938
|
async function reprocess(id, abort) {
|
|
6533
|
-
|
|
6534
|
-
if (
|
|
6535
|
-
|
|
6536
|
-
|
|
6939
|
+
const rootId = await resolveRootSession(id);
|
|
6940
|
+
if (reprocessing.has(rootId)) return "A timeline reprocess is already running for this session.";
|
|
6941
|
+
if (sessions.get(rootId)?.running) return "The timeline is currently summarizing this session; try again in a moment.";
|
|
6942
|
+
reprocessing.add(rootId);
|
|
6943
|
+
const targetFile = timelineFile(rootId);
|
|
6537
6944
|
const temp = targetFile + ".reprocess." + process.pid + "." + Date.now() + ".tmp";
|
|
6538
6945
|
let cursor;
|
|
6539
6946
|
let entries = 0;
|
|
6540
6947
|
let calls = 0;
|
|
6541
6948
|
try {
|
|
6542
|
-
const infoRes = await client.session.get({ path: { id }, query: { directory } });
|
|
6949
|
+
const infoRes = await client.session.get({ path: { id: rootId }, query: { directory } });
|
|
6543
6950
|
const info = infoRes?.data;
|
|
6544
6951
|
if (!info) return "Session not found.";
|
|
6545
|
-
const msgRes = await client.session.messages({ path: { id }, query: { directory } });
|
|
6952
|
+
const msgRes = await client.session.messages({ path: { id: rootId }, query: { directory } });
|
|
6546
6953
|
const messages = msgRes?.data ?? [];
|
|
6547
6954
|
writeFileSync8(temp, "");
|
|
6548
6955
|
for (let i = 0; i < MAX_REPROCESS_SLICES; i++) {
|
|
@@ -6556,7 +6963,7 @@ function timelineHooks(input, options) {
|
|
|
6556
6963
|
const excerpt = buildExcerpt(messages, cursor);
|
|
6557
6964
|
if (!excerpt.text) break;
|
|
6558
6965
|
const prior = readEntriesAt(temp, 4);
|
|
6559
|
-
const result = await summarize(
|
|
6966
|
+
const result = await summarize(rootId, info.title ?? "untitled", excerpt.text, prior, abort);
|
|
6560
6967
|
calls++;
|
|
6561
6968
|
if (!result.ok) {
|
|
6562
6969
|
try {
|
|
@@ -6585,7 +6992,7 @@ function timelineHooks(input, options) {
|
|
|
6585
6992
|
return "No entries generated; previous timeline kept.";
|
|
6586
6993
|
}
|
|
6587
6994
|
renameSync6(temp, targetFile);
|
|
6588
|
-
const state2 = sessions.get(
|
|
6995
|
+
const state2 = sessions.get(rootId);
|
|
6589
6996
|
if (state2) {
|
|
6590
6997
|
state2.cursor = cursor;
|
|
6591
6998
|
state2.failures = 0;
|
|
@@ -6599,11 +7006,11 @@ function timelineHooks(input, options) {
|
|
|
6599
7006
|
}
|
|
6600
7007
|
return `Reprocess failed: ${String(error)}. Previous timeline kept.`;
|
|
6601
7008
|
} finally {
|
|
6602
|
-
reprocessing.delete(
|
|
7009
|
+
reprocessing.delete(rootId);
|
|
6603
7010
|
}
|
|
6604
7011
|
}
|
|
6605
7012
|
const timelineReprocess = tool2({
|
|
6606
|
-
description: "Rebuild the
|
|
7013
|
+
description: "Rebuild the root session's entire timeline from its transcript, replacing the existing entries. Used by /reprocess-timeline.",
|
|
6607
7014
|
args: {},
|
|
6608
7015
|
execute: async (_args, ctx) => {
|
|
6609
7016
|
const output = await reprocess(ctx.sessionID, ctx.abort);
|
package/dist/lacode.js
CHANGED
|
@@ -7,7 +7,7 @@ import { spawnSync as spawnSync3 } from "node:child_process";
|
|
|
7
7
|
|
|
8
8
|
// src/tunnel/index.ts
|
|
9
9
|
import { spawn, spawnSync } from "node:child_process";
|
|
10
|
-
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "node:fs";
|
|
10
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "node:fs";
|
|
11
11
|
import { homedir as homedir3 } from "node:os";
|
|
12
12
|
import { join as join4 } from "node:path";
|
|
13
13
|
|
|
@@ -17,7 +17,7 @@ import { homedir as homedir2 } from "node:os";
|
|
|
17
17
|
import { basename, join as join3 } from "node:path";
|
|
18
18
|
|
|
19
19
|
// src/lib/pool.ts
|
|
20
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
22
|
import { join as join2 } from "node:path";
|
|
23
23
|
|
|
@@ -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;
|
|
@@ -262,7 +264,7 @@ var recordNotificationId = (correlationKey, id) => {
|
|
|
262
264
|
if (!isNotificationId(id)) return false;
|
|
263
265
|
try {
|
|
264
266
|
mkdirSync3(notificationStateDir(), { recursive: true });
|
|
265
|
-
|
|
267
|
+
appendFileSync2(notificationIdFile(correlationKey), `${id}
|
|
266
268
|
`, "utf8");
|
|
267
269
|
return true;
|
|
268
270
|
} catch {
|
package/dist/pool.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/lib/pool.ts
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join as join2 } from "node:path";
|
|
5
5
|
|
|
@@ -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
|
}
|
|
@@ -780,6 +802,56 @@ function coolProvider(s, provider, until, reason) {
|
|
|
780
802
|
}
|
|
781
803
|
return affected;
|
|
782
804
|
}
|
|
805
|
+
function recordFailure(s, f) {
|
|
806
|
+
try {
|
|
807
|
+
const path = join2(s.dir, "failures.jsonl");
|
|
808
|
+
const trimSize = 64 * 1024;
|
|
809
|
+
const keepLines = 200;
|
|
810
|
+
if (existsSync(path)) {
|
|
811
|
+
const st = statSync(path);
|
|
812
|
+
if (typeof st.size === "number" && st.size > trimSize) {
|
|
813
|
+
try {
|
|
814
|
+
const content = readFileSync2(path, "utf8");
|
|
815
|
+
const parts = content.split("\n");
|
|
816
|
+
const lines = parts.filter((line) => line.length > 0);
|
|
817
|
+
if (lines.length > keepLines) {
|
|
818
|
+
const kept = lines.slice(lines.length - keepLines);
|
|
819
|
+
writeFileSync(`${path}.tmp`, kept.join("\n") + "\n");
|
|
820
|
+
renameSync(`${path}.tmp`, path);
|
|
821
|
+
} else if (lines.length === 0 && parts.length >= 1) {
|
|
822
|
+
}
|
|
823
|
+
} catch {
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
appendFileSync(path, JSON.stringify(f) + "\n");
|
|
828
|
+
} catch (error) {
|
|
829
|
+
console.error("[agent-pool] failed to record failure:", String(error));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function readFailures(s, limit) {
|
|
833
|
+
const path = join2(s.dir, "failures.jsonl");
|
|
834
|
+
try {
|
|
835
|
+
if (!existsSync(path)) return [];
|
|
836
|
+
const content = readFileSync2(path, "utf8");
|
|
837
|
+
const parts = content.split("\n");
|
|
838
|
+
const records = [];
|
|
839
|
+
for (let i = 0; i < parts.length; i++) {
|
|
840
|
+
const line = parts[i];
|
|
841
|
+
if (line.length === 0) continue;
|
|
842
|
+
try {
|
|
843
|
+
const parsed = JSON.parse(line);
|
|
844
|
+
records.push(parsed);
|
|
845
|
+
} catch {
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (limit <= 0) return [];
|
|
849
|
+
if (records.length <= limit) return records;
|
|
850
|
+
return records.slice(records.length - limit);
|
|
851
|
+
} catch {
|
|
852
|
+
return [];
|
|
853
|
+
}
|
|
854
|
+
}
|
|
783
855
|
export {
|
|
784
856
|
AUTH_FAILURE_COOLDOWN_MS,
|
|
785
857
|
AUTH_FAILURE_REASON,
|
|
@@ -797,6 +869,7 @@ export {
|
|
|
797
869
|
ORPHAN_TMP_MS,
|
|
798
870
|
PACKAGE_ROOT,
|
|
799
871
|
PRESETS_DIR,
|
|
872
|
+
PROVIDER_LIST_TTL_MS,
|
|
800
873
|
QUOTA_COOLDOWN_BASE_MS,
|
|
801
874
|
QUOTA_COOLDOWN_MAX_MS,
|
|
802
875
|
SLOTS_WARN_AT,
|
|
@@ -806,6 +879,7 @@ export {
|
|
|
806
879
|
STUCK_MIN_AGE_MS,
|
|
807
880
|
UNAVAILABLE_COOLDOWN_MS,
|
|
808
881
|
UNAVAILABLE_REASON,
|
|
882
|
+
UNBOUND_CLAIM_MS,
|
|
809
883
|
USER_CONFIG_DIR,
|
|
810
884
|
USER_MODELS_PATH,
|
|
811
885
|
USER_MODELS_TXT,
|
|
@@ -841,12 +915,15 @@ export {
|
|
|
841
915
|
pickSlot,
|
|
842
916
|
poolPass,
|
|
843
917
|
providerOf,
|
|
918
|
+
providerReady,
|
|
844
919
|
pruneOwn,
|
|
845
920
|
pruneSessions,
|
|
846
921
|
publish,
|
|
922
|
+
readFailures,
|
|
847
923
|
readLimits,
|
|
848
924
|
readSiblings,
|
|
849
925
|
readSlotFile,
|
|
926
|
+
recordFailure,
|
|
850
927
|
recordStrike,
|
|
851
928
|
refreshCatalog,
|
|
852
929
|
release,
|
package/dist/tunnel.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/tunnel/index.ts
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
-
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "node:fs";
|
|
3
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "node:fs";
|
|
4
4
|
import { homedir as homedir3 } from "node:os";
|
|
5
5
|
import { join as join4 } from "node:path";
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ import { homedir as homedir2 } from "node:os";
|
|
|
10
10
|
import { basename, join as join3 } from "node:path";
|
|
11
11
|
|
|
12
12
|
// src/lib/pool.ts
|
|
13
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { join as join2 } from "node:path";
|
|
16
16
|
|
|
@@ -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;
|
|
@@ -255,7 +257,7 @@ var recordNotificationId = (correlationKey, id) => {
|
|
|
255
257
|
if (!isNotificationId(id)) return false;
|
|
256
258
|
try {
|
|
257
259
|
mkdirSync3(notificationStateDir(), { recursive: true });
|
|
258
|
-
|
|
260
|
+
appendFileSync2(notificationIdFile(correlationKey), `${id}
|
|
259
261
|
`, "utf8");
|
|
260
262
|
return true;
|
|
261
263
|
} catch {
|
package/package.json
CHANGED