@lawaty/lacode 1.3.0-beta.0 → 1.3.0-beta.2
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/dist/agents.js +1 -1
- package/dist/commands.js +1 -1
- package/dist/index.js +182 -7
- package/dist/lacode.js +3 -3
- package/dist/pool.js +53 -1
- package/dist/tui.js +76 -6
- package/dist/tunnel.js +3 -3
- package/package.json +12 -2
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
|
|
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
|
|
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
|
|
|
@@ -799,6 +799,56 @@ function coolProvider(s, provider, until, reason) {
|
|
|
799
799
|
}
|
|
800
800
|
return affected;
|
|
801
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
|
+
}
|
|
802
852
|
|
|
803
853
|
// src/lib/agent-instructions.ts
|
|
804
854
|
var AGENT_INSTRUCTIONS_COMMAND = "agent-instructions";
|
|
@@ -2545,6 +2595,8 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2545
2595
|
}
|
|
2546
2596
|
};
|
|
2547
2597
|
const abortReports = /* @__PURE__ */ new Map();
|
|
2598
|
+
const spawns = /* @__PURE__ */ new Map();
|
|
2599
|
+
const observedErrors = /* @__PURE__ */ new Map();
|
|
2548
2600
|
const abortKind = (reason) => isQuotaReason(reason) || isLimitText(reason) ? "rate-limit" : "provider error";
|
|
2549
2601
|
const abortLabel = (kind, elapsedMs) => {
|
|
2550
2602
|
const seconds = Math.max(1, Math.round(elapsedMs / 1e3));
|
|
@@ -2565,13 +2617,29 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
2565
2617
|
if (oldest !== void 0) patchedCancelledParts.delete(oldest);
|
|
2566
2618
|
}
|
|
2567
2619
|
const error = `${cancelledPrefix} ${report.message}`;
|
|
2620
|
+
const updated = { ...part, state: { ...part.state, error } };
|
|
2568
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
|
+
}
|
|
2569
2636
|
const url = new URL(
|
|
2570
2637
|
`/session/${encodeURIComponent(part.sessionID)}/message/${encodeURIComponent(part.messageID)}/part/${encodeURIComponent(part.id)}`,
|
|
2571
2638
|
serverUrl
|
|
2572
2639
|
);
|
|
2573
2640
|
if (directory) url.searchParams.set("directory", directory);
|
|
2574
2641
|
const headers = { "Content-Type": "application/json" };
|
|
2642
|
+
if (directory) headers["x-opencode-directory"] = directory;
|
|
2575
2643
|
const password = process.env.OPENCODE_SERVER_PASSWORD;
|
|
2576
2644
|
if (password) {
|
|
2577
2645
|
const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode";
|
|
@@ -3051,6 +3119,26 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
3051
3119
|
}
|
|
3052
3120
|
if (!limited2 && !outage && !auth2) {
|
|
3053
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
|
+
}
|
|
3054
3142
|
release(s, own.key);
|
|
3055
3143
|
await log("warn", `released ${own.claim.v} claim on ${own.claim.m} after a terminal session error`, {
|
|
3056
3144
|
base: own.claim.v,
|
|
@@ -3122,6 +3210,21 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
3122
3210
|
const report = abortReports.get(key);
|
|
3123
3211
|
if (report) {
|
|
3124
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);
|
|
3125
3228
|
} else {
|
|
3126
3229
|
pendingCancelledParts.set(key, full);
|
|
3127
3230
|
if (pendingCancelledParts.size > 32) {
|
|
@@ -3142,7 +3245,31 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
3142
3245
|
const gateway5xx = status !== void 0 && status >= 500 && status <= 599;
|
|
3143
3246
|
const transientGateway = gateway5xx || isTransientGatewayText(haystack);
|
|
3144
3247
|
const missing = isModelMissingText(haystack);
|
|
3145
|
-
if (!limited && !transientGateway && !auth && !missing)
|
|
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
|
+
}
|
|
3146
3273
|
const model = `${info.providerID ?? "?"}/${info.modelID ?? "?"}`;
|
|
3147
3274
|
if (info.sessionID && isProbe(s, info.sessionID)) {
|
|
3148
3275
|
await log("debug", `probe of ${model} hit a limit (${isLimitText(haystack) ? "limit" : "transient"}); leaving the cooldown to the probe`);
|
|
@@ -3225,7 +3352,9 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
3225
3352
|
load: (snap.counts.get(slot.index) ?? 0) + 1
|
|
3226
3353
|
});
|
|
3227
3354
|
output.args.subagent_type = variant;
|
|
3228
|
-
|
|
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() });
|
|
3229
3358
|
return;
|
|
3230
3359
|
}
|
|
3231
3360
|
const pinned = s.models.get(requested);
|
|
@@ -3242,15 +3371,30 @@ var agentPoolHooks = (async ({ client, serverUrl, directory }, options) => {
|
|
|
3242
3371
|
"tool.execute.after": async (input, output) => {
|
|
3243
3372
|
if (input.tool !== "task") return;
|
|
3244
3373
|
const key = claimKey(input.sessionID, input.callID);
|
|
3374
|
+
const spawn3 = spawns.get(key);
|
|
3245
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);
|
|
3246
3379
|
const report = abortReports.get(key);
|
|
3247
3380
|
if (!report) return;
|
|
3248
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
|
+
});
|
|
3249
3394
|
const detail = [
|
|
3250
3395
|
`Task Cancelled by LaCode: ${report.message}`,
|
|
3251
3396
|
`slot ${report.slot}; model ${report.model}; elapsed ${report.elapsedMs}ms; failure ${report.kind}; claim ${report.released ? "released" : "held"}`
|
|
3252
3397
|
].join("\n");
|
|
3253
|
-
const text = typeof output.output === "string" ? output.output : "";
|
|
3254
3398
|
output.output = /task\s+cancel+ed/i.test(text) ? detail : text ? `${text}
|
|
3255
3399
|
|
|
3256
3400
|
${detail}` : detail;
|
|
@@ -3339,7 +3483,38 @@ one ordered pass in slot order: each slot fills to its weight, then overflow goe
|
|
|
3339
3483
|
// different situations and this is the line that tells them apart.
|
|
3340
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.",
|
|
3341
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).",
|
|
3342
|
-
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
|
+
})()
|
|
3343
3518
|
].join("\n")
|
|
3344
3519
|
};
|
|
3345
3520
|
}
|
|
@@ -6160,7 +6335,7 @@ import { writeFileSync as writeFileSync8, renameSync as renameSync6, rmSync as r
|
|
|
6160
6335
|
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
6161
6336
|
|
|
6162
6337
|
// src/lib/timeline.ts
|
|
6163
|
-
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";
|
|
6164
6339
|
import { homedir as homedir7 } from "node:os";
|
|
6165
6340
|
import { dirname as dirname5, join as join11, resolve as resolve4 } from "node:path";
|
|
6166
6341
|
function timelineDir() {
|
|
@@ -6209,7 +6384,7 @@ function appendEntriesAt(file, texts, now, through) {
|
|
|
6209
6384
|
const ts = now ?? Date.now();
|
|
6210
6385
|
const line = { ts, text: t };
|
|
6211
6386
|
if (validThrough) line.through = validThrough;
|
|
6212
|
-
|
|
6387
|
+
appendFileSync2(file, JSON.stringify(line) + "\n");
|
|
6213
6388
|
count++;
|
|
6214
6389
|
}
|
|
6215
6390
|
return count;
|
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
|
|
|
@@ -264,7 +264,7 @@ var recordNotificationId = (correlationKey, id) => {
|
|
|
264
264
|
if (!isNotificationId(id)) return false;
|
|
265
265
|
try {
|
|
266
266
|
mkdirSync3(notificationStateDir(), { recursive: true });
|
|
267
|
-
|
|
267
|
+
appendFileSync2(notificationIdFile(correlationKey), `${id}
|
|
268
268
|
`, "utf8");
|
|
269
269
|
return true;
|
|
270
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
|
|
|
@@ -802,6 +802,56 @@ function coolProvider(s, provider, until, reason) {
|
|
|
802
802
|
}
|
|
803
803
|
return affected;
|
|
804
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
|
+
}
|
|
805
855
|
export {
|
|
806
856
|
AUTH_FAILURE_COOLDOWN_MS,
|
|
807
857
|
AUTH_FAILURE_REASON,
|
|
@@ -869,9 +919,11 @@ export {
|
|
|
869
919
|
pruneOwn,
|
|
870
920
|
pruneSessions,
|
|
871
921
|
publish,
|
|
922
|
+
readFailures,
|
|
872
923
|
readLimits,
|
|
873
924
|
readSiblings,
|
|
874
925
|
readSlotFile,
|
|
926
|
+
recordFailure,
|
|
875
927
|
recordStrike,
|
|
876
928
|
refreshCatalog,
|
|
877
929
|
release,
|
package/dist/tui.js
CHANGED
|
@@ -85,17 +85,17 @@ function createRecorder(deps) {
|
|
|
85
85
|
const child = childHandle;
|
|
86
86
|
const filePath = currentPath;
|
|
87
87
|
if (!exited) {
|
|
88
|
-
await new Promise((
|
|
88
|
+
await new Promise((resolve2) => {
|
|
89
89
|
let done = false;
|
|
90
90
|
let timer;
|
|
91
91
|
const finish = () => {
|
|
92
92
|
if (done) return;
|
|
93
93
|
done = true;
|
|
94
94
|
if (timer) clearTimeout(timer);
|
|
95
|
-
if (resolveWait ===
|
|
96
|
-
|
|
95
|
+
if (resolveWait === resolve2) resolveWait = null;
|
|
96
|
+
resolve2();
|
|
97
97
|
};
|
|
98
|
-
resolveWait =
|
|
98
|
+
resolveWait = resolve2;
|
|
99
99
|
timer = setTimeout(() => {
|
|
100
100
|
try {
|
|
101
101
|
child.kill("SIGKILL");
|
|
@@ -821,6 +821,68 @@ function createVoiceClient(options, deps) {
|
|
|
821
821
|
return client;
|
|
822
822
|
}
|
|
823
823
|
|
|
824
|
+
// src/tui/timeline.tsx
|
|
825
|
+
import { createSignal, createEffect, onCleanup, For } from "solid-js";
|
|
826
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
827
|
+
import { homedir as homedir2 } from "node:os";
|
|
828
|
+
import { join as join3, resolve } from "node:path";
|
|
829
|
+
import { jsx, jsxs } from "@opentui/solid/jsx-runtime";
|
|
830
|
+
var registered = false;
|
|
831
|
+
var tui = async (api) => {
|
|
832
|
+
if (registered) return;
|
|
833
|
+
registered = true;
|
|
834
|
+
api.slots.register({
|
|
835
|
+
order: 150,
|
|
836
|
+
slots: {
|
|
837
|
+
sidebar_content(ctx, props) {
|
|
838
|
+
const [entries, setEntries] = createSignal([]);
|
|
839
|
+
const read = (id) => {
|
|
840
|
+
if (typeof id !== "string" || !/^[A-Za-z0-9_.-]+$/.test(id)) {
|
|
841
|
+
setEntries([]);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const rawDir = process.env.LACODE_TIMELINE_DIR;
|
|
845
|
+
const dir = rawDir ? resolve(rawDir) : join3(homedir2(), ".local/share/opencode/lacode/timeline");
|
|
846
|
+
const file = join3(dir, id + ".jsonl");
|
|
847
|
+
try {
|
|
848
|
+
const raw = readFileSync3(file, "utf8");
|
|
849
|
+
const parsed = [];
|
|
850
|
+
for (const line of raw.split("\n")) {
|
|
851
|
+
const trimmed = line.trim();
|
|
852
|
+
if (!trimmed) continue;
|
|
853
|
+
try {
|
|
854
|
+
const obj = JSON.parse(trimmed);
|
|
855
|
+
if (obj && typeof obj.ts === "number" && typeof obj.text === "string") {
|
|
856
|
+
parsed.push(obj.text);
|
|
857
|
+
}
|
|
858
|
+
} catch {
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
setEntries(parsed.slice(-14).reverse());
|
|
862
|
+
} catch {
|
|
863
|
+
setEntries([]);
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
createEffect(() => {
|
|
867
|
+
const id = props.session_id;
|
|
868
|
+
read(id);
|
|
869
|
+
const timer = setInterval(() => read(id), 1e3);
|
|
870
|
+
onCleanup(() => clearInterval(timer));
|
|
871
|
+
});
|
|
872
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "column", children: [
|
|
873
|
+
/* @__PURE__ */ jsx("text", { fg: ctx.theme.current.textMuted, children: "Timeline" }),
|
|
874
|
+
entries().length === 0 ? /* @__PURE__ */ jsx("text", { fg: ctx.theme.current.textMuted, children: "No timeline yet" }) : /* @__PURE__ */ jsx(For, { each: entries(), children: (text) => /* @__PURE__ */ jsx("text", { children: `\u2022 ${text}` }) })
|
|
875
|
+
] });
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
};
|
|
880
|
+
var plugin = {
|
|
881
|
+
id: "lacode.timeline",
|
|
882
|
+
tui
|
|
883
|
+
};
|
|
884
|
+
var timeline_default = plugin;
|
|
885
|
+
|
|
824
886
|
// src/tui/index.ts
|
|
825
887
|
var DEFAULT_MODEL = "whisper-large-v3-turbo";
|
|
826
888
|
var DEFAULT_LANGUAGE = "en";
|
|
@@ -1049,7 +1111,7 @@ async function answerWithText(api, opts, text, request) {
|
|
|
1049
1111
|
const labels = answers.map((answer) => answer.join(", ")).filter((answer) => answer.length > 0).join("; ");
|
|
1050
1112
|
toast(api, { variant: "success", message: labels });
|
|
1051
1113
|
}
|
|
1052
|
-
var
|
|
1114
|
+
var tui2 = async (api, options) => {
|
|
1053
1115
|
const opts = resolveOptions(options);
|
|
1054
1116
|
if (!opts.enabled) return;
|
|
1055
1117
|
daemonConnected = false;
|
|
@@ -1271,10 +1333,18 @@ var tui = async (api, options) => {
|
|
|
1271
1333
|
bindings.push({ key: opts.answerKey, cmd: "lacode.voice.answer", desc: "Voice answer" });
|
|
1272
1334
|
}
|
|
1273
1335
|
api.keymap.registerLayer({ mode: "base", commands, bindings });
|
|
1336
|
+
const timelineOpts = options ?? {};
|
|
1337
|
+
const timelineOff = timelineOpts.timeline === false || typeof timelineOpts.timeline === "object" && timelineOpts.timeline !== null && timelineOpts.timeline.enabled === false;
|
|
1338
|
+
if (!timelineOff) {
|
|
1339
|
+
try {
|
|
1340
|
+
await timeline_default.tui(api, options);
|
|
1341
|
+
} catch {
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1274
1344
|
};
|
|
1275
1345
|
var mod = {
|
|
1276
1346
|
id: "lacode.voice",
|
|
1277
|
-
tui
|
|
1347
|
+
tui: tui2
|
|
1278
1348
|
};
|
|
1279
1349
|
var index_default = mod;
|
|
1280
1350
|
export {
|
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
|
|
|
@@ -257,7 +257,7 @@ var recordNotificationId = (correlationKey, id) => {
|
|
|
257
257
|
if (!isNotificationId(id)) return false;
|
|
258
258
|
try {
|
|
259
259
|
mkdirSync3(notificationStateDir(), { recursive: true });
|
|
260
|
-
|
|
260
|
+
appendFileSync2(notificationIdFile(correlationKey), `${id}
|
|
261
261
|
`, "utf8");
|
|
262
262
|
return true;
|
|
263
263
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lawaty/lacode",
|
|
3
|
-
"version": "1.3.0-beta.
|
|
3
|
+
"version": "1.3.0-beta.2",
|
|
4
4
|
"description": "Ultimate opencode setup for Go users — cost-controlled pooled subagents, hang recovery, and notifications for opencode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -64,7 +64,17 @@
|
|
|
64
64
|
"esbuild": "^0.28.2"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"@opencode-ai/plugin": "1.18.34"
|
|
67
|
+
"@opencode-ai/plugin": "1.18.34",
|
|
68
|
+
"solid-js": "*",
|
|
69
|
+
"@opentui/solid": "*"
|
|
70
|
+
},
|
|
71
|
+
"peerDependenciesMeta": {
|
|
72
|
+
"solid-js": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
75
|
+
"@opentui/solid": {
|
|
76
|
+
"optional": true
|
|
77
|
+
}
|
|
68
78
|
},
|
|
69
79
|
"optionalDependencies": {
|
|
70
80
|
"@picovoice/porcupine-node": "^4.0.2",
|