@loompad/cli 1.2.1 → 1.2.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/CHANGELOG.md +36 -0
- package/dist/adapters/opencode.d.ts +2 -0
- package/dist/adapters/opencode.js +9 -2
- package/dist/adapters/opencode.js.map +1 -1
- package/dist/cli/doctor.d.ts +1 -1
- package/dist/cli/doctor.js +7 -2
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.js +80 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/ui.js +2 -1
- package/dist/cli/ui.js.map +1 -1
- package/dist/core/brain-index.d.ts +0 -18
- package/dist/core/brain-index.js +10 -2
- package/dist/core/brain-index.js.map +1 -1
- package/dist/core/crew.d.ts +10 -0
- package/dist/core/crew.js +40 -3
- package/dist/core/crew.js.map +1 -1
- package/dist/core/prices.d.ts +16 -0
- package/dist/core/prices.js +29 -1
- package/dist/core/prices.js.map +1 -1
- package/dist/core/registry.d.ts +7 -0
- package/dist/core/registry.js.map +1 -1
- package/dist/core/replay.js +2 -0
- package/dist/core/replay.js.map +1 -1
- package/dist/daemon/auth.d.ts +7 -1
- package/dist/daemon/auth.js +19 -3
- package/dist/daemon/auth.js.map +1 -1
- package/dist/daemon/client.d.ts +15 -0
- package/dist/daemon/client.js +31 -0
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/council.d.ts +8 -0
- package/dist/daemon/council.js +115 -36
- package/dist/daemon/council.js.map +1 -1
- package/dist/daemon/review.d.ts +4 -0
- package/dist/daemon/review.js +22 -2
- package/dist/daemon/review.js.map +1 -1
- package/dist/daemon/routes/brain.js +4 -0
- package/dist/daemon/routes/brain.js.map +1 -1
- package/dist/daemon/routes/council.js +9 -0
- package/dist/daemon/routes/council.js.map +1 -1
- package/dist/daemon/routes/crews.js +2 -0
- package/dist/daemon/routes/crews.js.map +1 -1
- package/dist/daemon/routes/pairing.js +6 -1
- package/dist/daemon/routes/pairing.js.map +1 -1
- package/dist/daemon/routes/team.js +2 -0
- package/dist/daemon/routes/team.js.map +1 -1
- package/dist/daemon/runtime/accounting.js +7 -3
- package/dist/daemon/runtime/accounting.js.map +1 -1
- package/dist/daemon/runtime/queue.d.ts +2 -0
- package/dist/daemon/runtime/queue.js +14 -0
- package/dist/daemon/runtime/queue.js.map +1 -1
- package/dist/daemon/runtime/turns.d.ts +2 -0
- package/dist/daemon/runtime/turns.js +28 -0
- package/dist/daemon/runtime/turns.js.map +1 -1
- package/dist/daemon/runtime-support.d.ts +5 -1
- package/dist/daemon/runtime-support.js +8 -2
- package/dist/daemon/runtime-support.js.map +1 -1
- package/dist/daemon/runtime.d.ts +21 -0
- package/dist/daemon/runtime.js +94 -6
- package/dist/daemon/runtime.js.map +1 -1
- package/dist/daemon/server.js +4 -0
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/shift.d.ts +6 -0
- package/dist/daemon/shift.js +10 -1
- package/dist/daemon/shift.js.map +1 -1
- package/dist/daemon/team.d.ts +6 -0
- package/dist/daemon/team.js +15 -0
- package/dist/daemon/team.js.map +1 -1
- package/dist/web/app.css +65 -0
- package/dist/web/app.js +11783 -11607
- package/dist/web/app.js.map +4 -4
- package/docs/release-notes/v1.2.2.md +39 -0
- package/package.json +1 -1
package/dist/daemon/runtime.js
CHANGED
|
@@ -55,10 +55,11 @@ import { ContinuityEngine } from "../core/continuity/engine.js";
|
|
|
55
55
|
import { ContinuityError } from "../core/continuity/contracts.js";
|
|
56
56
|
import { HarnessMonitor, isNativeKind } from "../core/continuity/capabilities.js";
|
|
57
57
|
import { ProviderAgent } from "../providers/agent.js";
|
|
58
|
-
import { READ_ONLY_RULE } from "../core/review.js";
|
|
58
|
+
import { READ_ONLY_RULE, decisionText } from "../core/review.js";
|
|
59
|
+
import { parseVerdicts } from "../core/council.js";
|
|
59
60
|
import { ReviewRunner } from "./review.js";
|
|
60
61
|
import { CouncilRunner } from "./council.js";
|
|
61
|
-
import { coldResume, priceBook } from "../core/prices.js";
|
|
62
|
+
import { coldResume, observedInputRate, priceBook } from "../core/prices.js";
|
|
62
63
|
import { LiveDeltaThrottle } from "../providers/live.js";
|
|
63
64
|
import { AdapterBase } from "../adapters/base.js";
|
|
64
65
|
export { BudgetExceededError, CLOCK_TICK_MS, LOOM_ASK_TIMEOUT_MESSAGE, LOOM_ASK_TIMEOUT_MS, LoomAskTimeoutError, QuarantinedError, activityLine, planModeBriefing, relativeToProject, withLoomAskTimeout } from './runtime-support.js';
|
|
@@ -399,6 +400,7 @@ export class ProjectRuntime {
|
|
|
399
400
|
list: (opts) => this.log.list(opts),
|
|
400
401
|
append: (e) => (this.closed ? { ...e, id: -1, ts: Date.now() } : this.log.append(e)),
|
|
401
402
|
warn: (message, detail) => logbook.warn("shift", message, String(detail), this.info.id),
|
|
403
|
+
noteDirtyStart: (chat, what) => this.noteDirtyStart(chat, what),
|
|
402
404
|
});
|
|
403
405
|
this.reviews = new ReviewRunner({
|
|
404
406
|
hasAgent: (id) => this.config.agents.some((a) => a.id === id),
|
|
@@ -420,10 +422,16 @@ export class ProjectRuntime {
|
|
|
420
422
|
},
|
|
421
423
|
maxReviewers: ProjectRuntime.MAX_CONCURRENT_SUBTASKS,
|
|
422
424
|
reviewerTimeoutMs: () => Math.max(1, Number(this.config.review?.timeoutMin) || 15) * 60_000,
|
|
425
|
+
// a reviewer left out at its time limit is stopped, and its process with it — an opencode serve
|
|
426
|
+
// outlived a left-out review by 42 minutes (#312); its next turn starts it again
|
|
423
427
|
stopReviewer: async (agentId) => {
|
|
424
428
|
const a = this.agents.get(agentId);
|
|
425
|
-
if (a
|
|
429
|
+
if (!a || !isAdapter(a))
|
|
430
|
+
return;
|
|
431
|
+
if (a.busy())
|
|
426
432
|
await a.interrupt();
|
|
433
|
+
setTimeout(() => { if (!this.closed && !a.busy())
|
|
434
|
+
void a.stop().catch(() => { }); }, 30_000).unref?.();
|
|
427
435
|
},
|
|
428
436
|
});
|
|
429
437
|
this.councils = new CouncilRunner({
|
|
@@ -460,6 +468,7 @@ export class ProjectRuntime {
|
|
|
460
468
|
append: (e) => (this.closed ? { ...e, id: -1, ts: Date.now() } : this.log.append(e)),
|
|
461
469
|
lastId: () => this.log.list({ limit: 1 }).at(-1)?.id ?? 0,
|
|
462
470
|
warn: (message, detail) => logbook.warn("council", message, String(detail), this.info.id),
|
|
471
|
+
noteDirtyStart: (chat, what) => this.noteDirtyStart(chat, what),
|
|
463
472
|
});
|
|
464
473
|
this.crews = new CrewEngine({
|
|
465
474
|
projectId: info.id,
|
|
@@ -2527,7 +2536,85 @@ export class ProjectRuntime {
|
|
|
2527
2536
|
const tokens = Number(last.payload.inputTokens ?? 0) || 0;
|
|
2528
2537
|
const model = typeof last.payload.model === "string" ? last.payload.model : undefined;
|
|
2529
2538
|
const ageMs = Date.now() - last.ts;
|
|
2530
|
-
|
|
2539
|
+
// what this agent is really billed per token, from its own recent priced turns (any chat)
|
|
2540
|
+
const rate = observedInputRate(this.log.list({ agentId, kinds: ["run_complete"], limit: 40 }).map((e) => ({
|
|
2541
|
+
costUsd: Number(e.payload.costUsd ?? 0) || 0, tokensIn: Number(e.payload.inputTokens ?? 0) || 0,
|
|
2542
|
+
cachedIn: Number(e.payload.cachedInputTokens ?? 0) || 0, tokensOut: Number(e.payload.outputTokens ?? 0) || 0
|
|
2543
|
+
})));
|
|
2544
|
+
return { tokens, ageMs, ...(model ? { model } : {}), ...coldResume({ tokens, ageMs, rate, ...(model ? { model } : {}) }, priceBook()) };
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* Uncommitted changes in the tree right now (Loom's own .loom/ aside) —
|
|
2548
|
+
* a shift or council starting on top of an interrupted turn's edits says so,
|
|
2549
|
+
* rather than building on them silently (#313).
|
|
2550
|
+
*/
|
|
2551
|
+
dirtyFiles() {
|
|
2552
|
+
try {
|
|
2553
|
+
const out = execFileSync("git", ["status", "--porcelain", "--", ".", ":(exclude).loom"], { cwd: this.info.dir, encoding: "utf8", timeout: 10_000, stdio: ["ignore", "pipe", "ignore"] });
|
|
2554
|
+
return out.split("\n").map((l) => l.slice(3).trim()).filter(Boolean);
|
|
2555
|
+
}
|
|
2556
|
+
catch {
|
|
2557
|
+
return [];
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
/** Say, in the chat that's starting, that the tree isn't clean — and how to put it back. */
|
|
2561
|
+
noteDirtyStart(chat, what) {
|
|
2562
|
+
const files = this.dirtyFiles();
|
|
2563
|
+
if (!files.length)
|
|
2564
|
+
return;
|
|
2565
|
+
this.log.append({ kind: "status", ...(chat !== MAIN_CHAT ? { chat } : {}), payload: { state: "dirty_start", files: files.slice(0, 40),
|
|
2566
|
+
text: `${what} starts on ${files.length} uncommitted change${files.length === 1 ? "" : "s"} already in the tree (${files.slice(0, 4).join(", ")}${files.length > 4 ? ", …" : ""}) — commit them first, or Rewind to a turn's checkpoint to put them back` } });
|
|
2567
|
+
}
|
|
2568
|
+
/**
|
|
2569
|
+
* Decisions recorded from a council's verdicts before #275 lost every `_`
|
|
2570
|
+
* ("test_reach.py" became "testreach.py") and briefings carried them on.
|
|
2571
|
+
* Rebuild each from the review finding (kept intact) and the planner's own
|
|
2572
|
+
* reply, still in the chat — the note exactly as written (#318). Returns
|
|
2573
|
+
* how many memories were mended.
|
|
2574
|
+
*/
|
|
2575
|
+
repairDecisions() {
|
|
2576
|
+
const findings = [];
|
|
2577
|
+
const chats = new Map(); // finding id → the review's chat
|
|
2578
|
+
for (const e of this.log.list({ kinds: ["review"] })) {
|
|
2579
|
+
if (e.payload.phase !== "done")
|
|
2580
|
+
continue;
|
|
2581
|
+
for (const f of e.payload.findings ?? []) {
|
|
2582
|
+
findings.push(f);
|
|
2583
|
+
chats.set(f.id, String(e.chat ?? e.payload.chat ?? MAIN_CHAT));
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
let repaired = 0, checked = 0;
|
|
2587
|
+
for (const m of this.brain.all()) {
|
|
2588
|
+
if (m.kind !== "decision" || !m.text.startsWith("Review finding accepted ("))
|
|
2589
|
+
continue;
|
|
2590
|
+
checked++;
|
|
2591
|
+
const f = findings.find((x) => m.text.startsWith(decisionText(x)));
|
|
2592
|
+
if (!f)
|
|
2593
|
+
continue;
|
|
2594
|
+
// the planner's raw verdict line for this finding, anywhere in that chat
|
|
2595
|
+
let note;
|
|
2596
|
+
for (const msg of this.log.list({ chat: chats.get(f.id) ?? MAIN_CHAT, kinds: ["message"], limit: 2000 }).reverse()) {
|
|
2597
|
+
if (!msg.agentId || typeof msg.payload.text !== "string")
|
|
2598
|
+
continue;
|
|
2599
|
+
const v = parseVerdicts(msg.payload.text, [f.id]).get(f.id);
|
|
2600
|
+
if (v) {
|
|
2601
|
+
note = v.note;
|
|
2602
|
+
break;
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
const fixed = decisionText(f, note);
|
|
2606
|
+
if (fixed === m.text)
|
|
2607
|
+
continue;
|
|
2608
|
+
this.brain.update(m.id, { text: fixed }, "loom");
|
|
2609
|
+
repaired++;
|
|
2610
|
+
}
|
|
2611
|
+
return { repaired, checked };
|
|
2612
|
+
}
|
|
2613
|
+
/** Resolves once the agent isn't mid-turn (or after `maxMs`, when the send gets the adapter's own answer). */
|
|
2614
|
+
async whenIdle(agentId, maxMs = 20 * 60_000) {
|
|
2615
|
+
const busy = () => { const a = this.agents.get(agentId); return Boolean(a && isAdapter(a) && a.busy()) || this.turns.busySince.has(agentId); };
|
|
2616
|
+
for (const end = Date.now() + maxMs; busy() && Date.now() < end && !this.closed;)
|
|
2617
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
2531
2618
|
}
|
|
2532
2619
|
/** The agents a real turn found signed out, with what the CLI said (for `loom doctor`). */
|
|
2533
2620
|
learnedSignOuts() {
|
|
@@ -2848,8 +2935,9 @@ export class ProjectRuntime {
|
|
|
2848
2935
|
const startedAt = Date.now();
|
|
2849
2936
|
// the child's reply belongs to the chat it was asked in, not wherever it last talked
|
|
2850
2937
|
this.turns.turnChat.set(opts.agentId, chat);
|
|
2851
|
-
|
|
2852
|
-
|
|
2938
|
+
// an agent that's mid-turn finishes it first: a review sent to a busy reviewer used to fail at once (#310)
|
|
2939
|
+
void this.whenIdle(opts.agentId)
|
|
2940
|
+
.then(() => { this.turns.turnChat.set(opts.agentId, chat); return child.send(input); })
|
|
2853
2941
|
.then(() => {
|
|
2854
2942
|
this.subtasks.delete(id);
|
|
2855
2943
|
// an adapter can report a failed turn as an error event and still resolve: that subtask
|