@bojackduy/opencode-loopd 1.10.0 → 1.10.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/dist/server.js +121 -45
- package/dist/tui.js +542 -489
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1639,6 +1639,9 @@ function createLoopEngine(options) {
|
|
|
1639
1639
|
}
|
|
1640
1640
|
return true;
|
|
1641
1641
|
}
|
|
1642
|
+
function hasExecutionLeak(runtime) {
|
|
1643
|
+
return runtime.phase !== "idle" || Boolean(runtime.activeRunID) || Boolean(runtime.leaseExpiresAt) || Boolean(runtime.turnStartedAt) || Boolean(runtime.activePromptMessageID) || Boolean(runtime.activePromptObservedAt) || Boolean(runtime.activeAssistantMessageID) || Boolean(runtime.activeAssistantCompletedAt) || Boolean(runtime.idleCandidateAt) || (runtime.activeToolCallIDs?.length ?? 0) > 0;
|
|
1644
|
+
}
|
|
1642
1645
|
async function maintenance() {
|
|
1643
1646
|
syncWorkerSessionsFromService();
|
|
1644
1647
|
if (knownWorkerSessions.size === 0)
|
|
@@ -1650,10 +1653,11 @@ function createLoopEngine(options) {
|
|
|
1650
1653
|
const rt = state.runtimes.find((r) => r.goalID === goal.id);
|
|
1651
1654
|
if (!rt)
|
|
1652
1655
|
continue;
|
|
1653
|
-
if (
|
|
1656
|
+
if (hasExecutionLeak(rt)) {
|
|
1654
1657
|
await mutateState(directory, `maintenance.clear-terminal-leak:${goal.id}`, async (s) => {
|
|
1655
1658
|
const r = s.runtimes.find((x) => x.goalID === goal.id);
|
|
1656
|
-
|
|
1659
|
+
const g = s.goals.find((item) => item.id === goal.id);
|
|
1660
|
+
if (g?.status !== "active" && r && hasExecutionLeak(r)) {
|
|
1657
1661
|
Object.assign(r, releaseLease(r));
|
|
1658
1662
|
r.activeRunID = undefined;
|
|
1659
1663
|
r.unknownStatusCount = 0;
|
|
@@ -2894,9 +2898,13 @@ function createGoalService(host) {
|
|
|
2894
2898
|
runtime.consecutiveFailures = 0;
|
|
2895
2899
|
runtime.lastError = undefined;
|
|
2896
2900
|
runtime.forceFinishRequested = undefined;
|
|
2901
|
+
runtime.evaluatorRejectionCount = 0;
|
|
2902
|
+
runtime.lastRejectionDetails = undefined;
|
|
2903
|
+
runtime.freeRetryPending = false;
|
|
2897
2904
|
runtime.lastParentNotifiedAt = undefined;
|
|
2898
2905
|
runtime.lastParentNotifiedFor = undefined;
|
|
2899
|
-
runtime
|
|
2906
|
+
Object.assign(runtime, releaseLease(runtime));
|
|
2907
|
+
runtime.activeRunID = undefined;
|
|
2900
2908
|
runtime.updatedAt = new Date().toISOString();
|
|
2901
2909
|
}
|
|
2902
2910
|
return state;
|
|
@@ -3528,25 +3536,32 @@ function goalTools(dir, goalService, hostSessionID, defaults = {}, host) {
|
|
|
3528
3536
|
const cwd = goal.config.checkCwd || goal.config.artifactDir || dir;
|
|
3529
3537
|
const checkResults = await runCompletionChecks(goal.config.checks, cwd);
|
|
3530
3538
|
if (!checkResults.passed) {
|
|
3531
|
-
const
|
|
3532
|
-
|
|
3533
|
-
runtime.evaluatorRejectionCount = (runtime.evaluatorRejectionCount || 0) + 1;
|
|
3534
|
-
const failureDetails = checkResults.failures.map((f) => {
|
|
3535
|
-
const stdoutSnippet = f.stdout ? `
|
|
3539
|
+
const failureDetails = checkResults.failures.map((f) => {
|
|
3540
|
+
const stdoutSnippet = f.stdout ? `
|
|
3536
3541
|
Stdout: ${f.stdout.slice(0, 500)}` : "";
|
|
3537
|
-
|
|
3542
|
+
const stderrSnippet = f.stderr ? `
|
|
3538
3543
|
Stderr: ${f.stderr.slice(0, 500)}` : "";
|
|
3539
|
-
|
|
3544
|
+
return `Command: ${f.command}
|
|
3540
3545
|
Exit code: ${f.exitCode}${stdoutSnippet}${stderrSnippet}`;
|
|
3541
|
-
|
|
3546
|
+
}).join(`
|
|
3542
3547
|
|
|
3543
3548
|
`);
|
|
3549
|
+
let rejectionCount = 0;
|
|
3550
|
+
let blocked = false;
|
|
3551
|
+
let attemptID = "";
|
|
3552
|
+
const rejectionState = await mutateState(dir, `goal.completion-rejected:${goal.id}`, async (current) => {
|
|
3553
|
+
const currentGoal = current.goals.find((item) => item.id === goal.id);
|
|
3554
|
+
const runtime = current.runtimes.find((r) => r.goalID === goal.id);
|
|
3555
|
+
if (!currentGoal || !runtime || currentGoal.status !== "active")
|
|
3556
|
+
return current;
|
|
3557
|
+
runtime.evaluatorRejectionCount = (runtime.evaluatorRejectionCount || 0) + 1;
|
|
3558
|
+
rejectionCount = runtime.evaluatorRejectionCount;
|
|
3544
3559
|
runtime.lastRejectionDetails = `Rejection #${runtime.evaluatorRejectionCount} at ${new Date().toISOString()}
|
|
3545
3560
|
|
|
3546
3561
|
Working directory: ${cwd}
|
|
3547
3562
|
|
|
3548
3563
|
${failureDetails}`;
|
|
3549
|
-
|
|
3564
|
+
attemptID = randomUUID5();
|
|
3550
3565
|
const verificationAttempt = {
|
|
3551
3566
|
id: attemptID,
|
|
3552
3567
|
sequence: runtime.evaluatorRejectionCount,
|
|
@@ -3566,55 +3581,71 @@ ${failureDetails}`;
|
|
|
3566
3581
|
};
|
|
3567
3582
|
runtime.lastVerificationAttempt = verificationAttempt;
|
|
3568
3583
|
runtime.recentVerificationAttempts = appendVerificationAttempt(runtime.recentVerificationAttempts || [], verificationAttempt);
|
|
3569
|
-
const
|
|
3570
|
-
version: 1,
|
|
3571
|
-
eventID: randomUUID5(),
|
|
3572
|
-
goalID: goal.id,
|
|
3573
|
-
type: "goal.completion_rejected",
|
|
3574
|
-
attemptID,
|
|
3575
|
-
rejectionCount: runtime.evaluatorRejectionCount,
|
|
3576
|
-
failedCheckCount: checkResults.failures.length,
|
|
3577
|
-
failureSummary: failureDetails.slice(0, 500),
|
|
3578
|
-
timestamp: new Date().toISOString(),
|
|
3579
|
-
revision: state.revision
|
|
3580
|
-
};
|
|
3581
|
-
await appendEvent(dir, rejectEvent);
|
|
3582
|
-
const maxRejections = goal.config.maxEvaluatorRejections || 3;
|
|
3584
|
+
const maxRejections = currentGoal.config.maxEvaluatorRejections || 3;
|
|
3583
3585
|
if (runtime.evaluatorRejectionCount >= maxRejections) {
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3586
|
+
blocked = true;
|
|
3587
|
+
currentGoal.status = "blocked";
|
|
3588
|
+
currentGoal.updatedAt = new Date().toISOString();
|
|
3589
|
+
currentGoal.blocker = {
|
|
3587
3590
|
reason: `Evaluator rejected ${runtime.evaluatorRejectionCount} time(s). Last failure:
|
|
3588
3591
|
${failureDetails.slice(0, 500)}`,
|
|
3589
3592
|
needed: "Fix the failing checks and retry the goal.",
|
|
3590
3593
|
at: new Date().toISOString()
|
|
3591
3594
|
};
|
|
3595
|
+
Object.assign(runtime, releaseLease(runtime));
|
|
3596
|
+
runtime.activeRunID = undefined;
|
|
3592
3597
|
runtime.forceFinishRequested = undefined;
|
|
3593
|
-
|
|
3594
|
-
version: 1,
|
|
3595
|
-
eventID: randomUUID5(),
|
|
3596
|
-
goalID: goal.id,
|
|
3597
|
-
type: "goal.blocked",
|
|
3598
|
-
reason: goal.blocker.reason,
|
|
3599
|
-
needed: goal.blocker.needed,
|
|
3600
|
-
timestamp: new Date().toISOString(),
|
|
3601
|
-
revision: state.revision
|
|
3602
|
-
});
|
|
3598
|
+
runtime.freeRetryPending = false;
|
|
3603
3599
|
} else {
|
|
3604
3600
|
runtime.forceFinishRequested = false;
|
|
3605
3601
|
runtime.freeRetryPending = true;
|
|
3606
3602
|
}
|
|
3607
3603
|
runtime.updatedAt = new Date().toISOString();
|
|
3608
|
-
|
|
3604
|
+
return current;
|
|
3605
|
+
});
|
|
3606
|
+
if (attemptID) {
|
|
3607
|
+
await appendEvent(dir, {
|
|
3608
|
+
version: 1,
|
|
3609
|
+
eventID: randomUUID5(),
|
|
3610
|
+
goalID: goal.id,
|
|
3611
|
+
type: "goal.completion_rejected",
|
|
3612
|
+
attemptID,
|
|
3613
|
+
rejectionCount,
|
|
3614
|
+
failedCheckCount: checkResults.failures.length,
|
|
3615
|
+
failureSummary: failureDetails.slice(0, 500),
|
|
3616
|
+
timestamp: new Date().toISOString(),
|
|
3617
|
+
revision: rejectionState.revision
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
const rejectedGoal = rejectionState.goals.find((item) => item.id === goal.id);
|
|
3621
|
+
const rejectedRuntime = rejectionState.runtimes.find((item) => item.goalID === goal.id);
|
|
3622
|
+
rejectionCount = rejectedRuntime?.evaluatorRejectionCount ?? rejectionCount;
|
|
3623
|
+
const goalIsBlocked = rejectedGoal?.status === "blocked";
|
|
3624
|
+
if (blocked && rejectedGoal?.blocker) {
|
|
3625
|
+
await appendEvent(dir, {
|
|
3626
|
+
version: 1,
|
|
3627
|
+
eventID: randomUUID5(),
|
|
3628
|
+
goalID: goal.id,
|
|
3629
|
+
type: "goal.blocked",
|
|
3630
|
+
reason: rejectedGoal.blocker.reason,
|
|
3631
|
+
needed: rejectedGoal.blocker.needed,
|
|
3632
|
+
timestamp: new Date().toISOString(),
|
|
3633
|
+
revision: rejectionState.revision
|
|
3634
|
+
});
|
|
3635
|
+
await goalService.accountUsage(dir, goal.id).catch(() => {
|
|
3636
|
+
return;
|
|
3637
|
+
});
|
|
3609
3638
|
}
|
|
3610
3639
|
return {
|
|
3611
|
-
title: "Completion rejected \u2014 keep working",
|
|
3640
|
+
title: goalIsBlocked ? "Completion rejected \u2014 goal blocked" : "Completion rejected \u2014 keep working",
|
|
3612
3641
|
output: JSON.stringify({
|
|
3642
|
+
goalID: goal.id,
|
|
3643
|
+
goalName: goal.name,
|
|
3613
3644
|
passed: false,
|
|
3614
3645
|
failedChecks: checkResults.failures,
|
|
3615
|
-
message: "Evaluator rejected completion. Fix the issues above and try again.",
|
|
3616
|
-
rejectionCount
|
|
3617
|
-
status: goal.status
|
|
3646
|
+
message: goalIsBlocked ? "Evaluator rejection limit reached. Goal blocked; owner retry required." : "Evaluator rejected completion. Fix the issues above and try again.",
|
|
3647
|
+
rejectionCount,
|
|
3648
|
+
status: rejectedGoal?.status ?? goal.status
|
|
3618
3649
|
})
|
|
3619
3650
|
};
|
|
3620
3651
|
}
|
|
@@ -3881,6 +3912,42 @@ async function runCompletionChecks(checks, cwd) {
|
|
|
3881
3912
|
// src/server/owner-tools.ts
|
|
3882
3913
|
init_state_repository();
|
|
3883
3914
|
import { tool as tool2 } from "@opencode-ai/plugin/tool";
|
|
3915
|
+
|
|
3916
|
+
// src/domain/status-labels.ts
|
|
3917
|
+
var GOAL_STATUS_META = {
|
|
3918
|
+
active: { short: "Active", hint: "loopd owns it" },
|
|
3919
|
+
paused: { short: "Paused", hint: "stopped by you" },
|
|
3920
|
+
blocked: { short: "Blocked", hint: "needs you" },
|
|
3921
|
+
budget_limited: { short: "Out of budget", hint: "resume to spend" },
|
|
3922
|
+
usage_limited: { short: "Waiting for capacity", hint: "auto-resumes" },
|
|
3923
|
+
complete: { short: "Done", hint: "verified" }
|
|
3924
|
+
};
|
|
3925
|
+
var PHASE_META = {
|
|
3926
|
+
idle: { short: "Idle", hint: "between turns" },
|
|
3927
|
+
queued: { short: "Queued", hint: "waiting to start" },
|
|
3928
|
+
running: { short: "Running", hint: "worker acting now" },
|
|
3929
|
+
compacting: { short: "Compacting", hint: "summarizing context" },
|
|
3930
|
+
waiting_retry: { short: "Retrying", hint: "backing off" },
|
|
3931
|
+
stopping: { short: "Stopping", hint: "abort in flight" }
|
|
3932
|
+
};
|
|
3933
|
+
function goalStatusLabel(status) {
|
|
3934
|
+
return GOAL_STATUS_META[status] ?? { short: status, hint: "" };
|
|
3935
|
+
}
|
|
3936
|
+
function phaseLabel(phase) {
|
|
3937
|
+
return PHASE_META[phase] ?? { short: phase, hint: "" };
|
|
3938
|
+
}
|
|
3939
|
+
function describeGoalState(status, phase) {
|
|
3940
|
+
const goal = goalStatusLabel(status);
|
|
3941
|
+
const activity = phase ? phaseLabel(phase) : undefined;
|
|
3942
|
+
const workerClause = activity ? `worker is ${activity.hint || activity.short.toLowerCase()}` : "worker state unknown";
|
|
3943
|
+
if (status === "active")
|
|
3944
|
+
return `Loopd owns this; ${workerClause}.`;
|
|
3945
|
+
if (status === "complete")
|
|
3946
|
+
return "Done \u2014 verified; worker is stopped.";
|
|
3947
|
+
return `Parked \u2014 ${goal.hint || goal.short.toLowerCase()}; ${workerClause}.`;
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
// src/server/owner-tools.ts
|
|
3884
3951
|
import { promises as fs3 } from "fs";
|
|
3885
3952
|
function withTimeout2(promise, ms) {
|
|
3886
3953
|
return Promise.race([
|
|
@@ -3916,11 +3983,17 @@ function ownerTools(options) {
|
|
|
3916
3983
|
}
|
|
3917
3984
|
const summaries = goals.map((g) => {
|
|
3918
3985
|
const runtime = state.runtimes.find((r) => r.goalID === g.id);
|
|
3986
|
+
const phase = runtime?.phase ?? "unknown";
|
|
3987
|
+
const goalLabel = goalStatusLabel(g.status);
|
|
3988
|
+
const activityLabel = phaseLabel(phase);
|
|
3919
3989
|
return {
|
|
3920
3990
|
id: g.id,
|
|
3921
3991
|
name: g.name,
|
|
3922
3992
|
status: g.status,
|
|
3923
|
-
|
|
3993
|
+
statusDisplay: `${goalLabel.short} \u2014 ${goalLabel.hint}`,
|
|
3994
|
+
phase,
|
|
3995
|
+
activityDisplay: `${activityLabel.short} \u2014 ${activityLabel.hint}`,
|
|
3996
|
+
stateSummary: describeGoalState(g.status, runtime?.phase),
|
|
3924
3997
|
turn: runtime?.runCount ?? 0,
|
|
3925
3998
|
budgetTurnCount: runtime?.budgetTurnCount ?? 0,
|
|
3926
3999
|
maxTurns: g.config.maxTurns,
|
|
@@ -3990,6 +4063,9 @@ function ownerTools(options) {
|
|
|
3990
4063
|
name: goal.name,
|
|
3991
4064
|
objective: goal.objective,
|
|
3992
4065
|
status: goal.status,
|
|
4066
|
+
statusDisplay: `${goalStatusLabel(goal.status).short} \u2014 ${goalStatusLabel(goal.status).hint}`,
|
|
4067
|
+
activityDisplay: runtime?.phase ? `${phaseLabel(runtime.phase).short} \u2014 ${phaseLabel(runtime.phase).hint}` : undefined,
|
|
4068
|
+
stateSummary: describeGoalState(goal.status, runtime?.phase),
|
|
3993
4069
|
ownerSessionID: goal.ownerSessionID,
|
|
3994
4070
|
workerSessionID: goal.workerSessionID,
|
|
3995
4071
|
config: {
|