@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.
Files changed (3) hide show
  1. package/dist/server.js +121 -45
  2. package/dist/tui.js +542 -489
  3. 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 (rt.phase === "idle" && (rt.activeRunID || rt.leaseExpiresAt || rt.activePromptMessageID)) {
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
- if (r && r.phase === "idle" && (r.activeRunID || r.leaseExpiresAt || r.activePromptMessageID)) {
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.phase = "idle";
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 runtime = state.runtimes.find((r) => r.goalID === goal.id);
3532
- if (runtime) {
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
- const stderrSnippet = f.stderr ? `
3542
+ const stderrSnippet = f.stderr ? `
3538
3543
  Stderr: ${f.stderr.slice(0, 500)}` : "";
3539
- return `Command: ${f.command}
3544
+ return `Command: ${f.command}
3540
3545
  Exit code: ${f.exitCode}${stdoutSnippet}${stderrSnippet}`;
3541
- }).join(`
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
- const attemptID = randomUUID5();
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 rejectEvent = {
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
- goal.status = "blocked";
3585
- goal.updatedAt = new Date().toISOString();
3586
- goal.blocker = {
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
- await appendEvent(dir, {
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
- await writeState(dir, state);
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: runtime?.evaluatorRejectionCount || 0,
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
- phase: runtime?.phase ?? "unknown",
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: {