@bojackduy/opencode-loopd 1.7.1 → 1.8.0

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 +131 -19
  2. package/dist/tui.js +551 -377
  3. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -486,6 +486,20 @@ async function drainGoalInbox(directory, goalID) {
486
486
  return [];
487
487
  }
488
488
  }
489
+ async function peekGoalInbox(directory, goalID) {
490
+ const file = inboxFile(directory, goalID);
491
+ try {
492
+ const raw = await fs.readFile(file, "utf8");
493
+ const lines = raw.trim().split(`
494
+ `).filter(Boolean);
495
+ if (lines.length === 0)
496
+ return [];
497
+ const messages = lines.map((l) => JSON.parse(l));
498
+ return messages.map((m) => `[${m.from}] ${m.text}`);
499
+ } catch {
500
+ return [];
501
+ }
502
+ }
489
503
  function delay(ms) {
490
504
  return new Promise((resolve) => setTimeout(resolve, ms));
491
505
  }
@@ -499,7 +513,7 @@ function resolveGoalCreationConfig(input) {
499
513
  const agent = explicitAgent || defaultAgent || undefined;
500
514
  const workspaceWrite = requested.workspaceWrite ?? true;
501
515
  const explicitChecks = cleanList(requested.checks);
502
- const defaultChecks = workspaceWrite ? cleanList(defaults.defaultChecks) : [];
516
+ const defaultChecks = workspaceWrite ? cleanList(defaults.defaultChecks || ["bun test"]) : [];
503
517
  const checks = explicitChecks.length > 0 ? explicitChecks : defaultChecks;
504
518
  if (workspaceWrite && checks.length === 0) {
505
519
  return {
@@ -791,7 +805,8 @@ function createControlWorker(options) {
791
805
  };
792
806
  const runtime = state2.runtimes.find((r) => r.goalID === goal.id);
793
807
  if (runtime) {
794
- runtime.phase = "idle";
808
+ Object.assign(runtime, releaseLease(runtime));
809
+ runtime.activeRunID = undefined;
795
810
  runtime.lastError = undefined;
796
811
  runtime.updatedAt = new Date().toISOString();
797
812
  }
@@ -831,7 +846,8 @@ function createControlWorker(options) {
831
846
  };
832
847
  const runtime = state2.runtimes.find((r) => r.goalID === goal.id);
833
848
  if (runtime) {
834
- runtime.phase = "idle";
849
+ Object.assign(runtime, releaseLease(runtime));
850
+ runtime.activeRunID = undefined;
835
851
  runtime.lastError = undefined;
836
852
  runtime.updatedAt = new Date().toISOString();
837
853
  }
@@ -1461,6 +1477,28 @@ function createLoopEngine(options) {
1461
1477
  if (knownWorkerSessions.size === 0)
1462
1478
  return;
1463
1479
  const state = await readState(directory);
1480
+ for (const goal of state.goals) {
1481
+ if (goal.status === "active")
1482
+ continue;
1483
+ const rt = state.runtimes.find((r) => r.goalID === goal.id);
1484
+ if (!rt)
1485
+ continue;
1486
+ if (rt.phase === "idle" && (rt.activeRunID || rt.leaseExpiresAt || rt.activePromptMessageID)) {
1487
+ await mutateState(directory, `maintenance.clear-terminal-leak:${goal.id}`, async (s) => {
1488
+ const r = s.runtimes.find((x) => x.goalID === goal.id);
1489
+ if (r && r.phase === "idle" && (r.activeRunID || r.leaseExpiresAt || r.activePromptMessageID)) {
1490
+ Object.assign(r, releaseLease(r));
1491
+ r.activeRunID = undefined;
1492
+ r.unknownStatusCount = 0;
1493
+ r.lastUnknownStatusAt = undefined;
1494
+ r.workerUnreachableNotifiedAt = undefined;
1495
+ r.updatedAt = new Date().toISOString();
1496
+ }
1497
+ return s;
1498
+ });
1499
+ await logServerEvent(directory, "maintenance.terminal-leak-cleared", { goalID: goal.id, status: goal.status });
1500
+ }
1501
+ }
1464
1502
  const hasActiveGoals = state.goals.some((g) => g.status === "active");
1465
1503
  if (!hasActiveGoals)
1466
1504
  return;
@@ -1496,6 +1534,22 @@ function createLoopEngine(options) {
1496
1534
  });
1497
1535
  await logServerEvent(directory, "maintenance.stale-run-cleared", { goalID: goal.id });
1498
1536
  }
1537
+ if ((runtime.activeToolCallIDs?.length ?? 0) > 0 && runtime.lastActivityAt) {
1538
+ const age = Date.now() - Date.parse(runtime.lastActivityAt);
1539
+ if (age > 30000) {
1540
+ await mutateState(directory, `maintenance.toolcall-ttl:${goal.id}`, async (s) => {
1541
+ const rt = s.runtimes.find((r) => r.goalID === goal.id);
1542
+ if (rt && (rt.activeToolCallIDs?.length ?? 0) > 0) {
1543
+ rt.activeToolCallIDs = [];
1544
+ rt.idleCandidateAt = undefined;
1545
+ rt.idleCandidateGeneration = undefined;
1546
+ rt.updatedAt = new Date().toISOString();
1547
+ }
1548
+ return s;
1549
+ });
1550
+ await logServerEvent(directory, "maintenance.toolcall-ttl-cleared", { goalID: goal.id, age });
1551
+ }
1552
+ }
1499
1553
  const status = await host.sessionStatus(goal.workerSessionID);
1500
1554
  if (status === "unknown") {
1501
1555
  let shouldNotify = false;
@@ -2379,7 +2433,8 @@ function createScheduleWorker(options) {
2379
2433
  g.status = "active";
2380
2434
  g.updatedAt = new Date().toISOString();
2381
2435
  g.blocker = undefined;
2382
- rt.phase = "idle";
2436
+ Object.assign(rt, releaseLease(rt));
2437
+ rt.activeRunID = undefined;
2383
2438
  rt.consecutiveFailures = 0;
2384
2439
  rt.noProgressCount = 0;
2385
2440
  rt.progressDuringTurn = false;
@@ -2906,18 +2961,10 @@ ${failureDetails.slice(0, 500)}`,
2906
2961
  };
2907
2962
  const runtime = state.runtimes.find((r) => r.goalID === goal.id);
2908
2963
  if (runtime) {
2909
- runtime.phase = "idle";
2910
- runtime.leaseExpiresAt = undefined;
2911
- runtime.turnStartedAt = undefined;
2964
+ Object.assign(runtime, releaseLease(runtime));
2912
2965
  runtime.activeRunID = undefined;
2913
- runtime.activePromptMessageID = undefined;
2914
- runtime.activePromptObservedAt = undefined;
2915
- runtime.activeAssistantMessageID = undefined;
2916
- runtime.activeAssistantCompletedAt = undefined;
2917
- runtime.idleCandidateAt = undefined;
2918
- runtime.idleCandidateGeneration = undefined;
2919
- runtime.activeToolCallIDs = [];
2920
2966
  runtime.lastError = undefined;
2967
+ runtime.updatedAt = new Date().toISOString();
2921
2968
  const schedule = goal.config.schedule;
2922
2969
  if (schedule && typeof schedule.everyMs === "number" && schedule.everyMs >= 1000) {
2923
2970
  const cur = typeof runtime.scheduleRunCount === "number" ? runtime.scheduleRunCount : 0;
@@ -3003,8 +3050,10 @@ ${failureDetails.slice(0, 500)}`,
3003
3050
  };
3004
3051
  const runtime = state.runtimes.find((r) => r.goalID === goal.id);
3005
3052
  if (runtime) {
3006
- runtime.phase = "idle";
3053
+ Object.assign(runtime, releaseLease(runtime));
3054
+ runtime.activeRunID = undefined;
3007
3055
  runtime.lastError = undefined;
3056
+ runtime.updatedAt = new Date().toISOString();
3008
3057
  }
3009
3058
  await writeState(dir, state);
3010
3059
  const event = {
@@ -3120,6 +3169,13 @@ async function runCompletionChecks(checks, cwd) {
3120
3169
 
3121
3170
  // src/server/owner-tools.ts
3122
3171
  import { tool as tool2 } from "@opencode-ai/plugin/tool";
3172
+ import { promises as fs3 } from "fs";
3173
+ function withTimeout2(promise, ms) {
3174
+ return Promise.race([
3175
+ promise,
3176
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), ms))
3177
+ ]);
3178
+ }
3123
3179
  function ownerTools(options) {
3124
3180
  const { directory, host, goalService } = options;
3125
3181
  return {
@@ -3154,9 +3210,19 @@ function ownerTools(options) {
3154
3210
  status: g.status,
3155
3211
  phase: runtime?.phase ?? "unknown",
3156
3212
  turn: runtime?.runCount ?? 0,
3213
+ budgetTurnCount: runtime?.budgetTurnCount ?? 0,
3214
+ maxTurns: g.config.maxTurns,
3157
3215
  lastProgress: g.lastProgress?.summary?.slice(0, 120),
3158
3216
  lastProgressAt: g.lastProgress?.at,
3159
- blocker: g.blocker?.reason?.slice(0, 120)
3217
+ blocker: g.blocker?.reason?.slice(0, 120),
3218
+ evaluatorRejectionCount: runtime?.evaluatorRejectionCount ?? 0,
3219
+ unknownStatusCount: runtime?.unknownStatusCount ?? 0,
3220
+ lastActivityAt: runtime?.lastActivityAt,
3221
+ retryAfter: runtime?.retryAfter,
3222
+ nextRunAt: runtime?.nextRunAt,
3223
+ scheduleRunCount: runtime?.scheduleRunCount,
3224
+ consecutiveFailures: runtime?.consecutiveFailures ?? 0,
3225
+ noProgressCount: runtime?.noProgressCount ?? 0
3160
3226
  };
3161
3227
  });
3162
3228
  return {
@@ -3166,9 +3232,11 @@ function ownerTools(options) {
3166
3232
  }
3167
3233
  }),
3168
3234
  inspect_background_goal: tool2({
3169
- description: "Inspect a goal\u2019s full contract and runtime: objective, config{agent,checks,checkCwd,workspaceWrite,limits}, progress, blocker, and runtime{phase,runCount,budgetTurnCount,runGeneration,evaluatorRejectionCount,unknownStatusCount,lastActivityAt,activePromptMessageID}. The source for recovery decisions.",
3235
+ description: "Inspect a goal\u2019s full contract, runtime, and live execution state: objective, config{agent,checks,checkCwd,workspaceWrite,limits}, progress, blocker, runtime{phase,runCount,budgetTurnCount,runGeneration,evaluatorRejectionCount,unknownStatusCount,lastActivityAt,activePromptMessageID}, plus live transcriptTail, activeToolCallIDs, progressHistory, artifactSummary, pendingInbox. Single-call follow-up for parent to see what child is actually doing.",
3170
3236
  args: {
3171
- goal_id: tool2.schema.string().optional().describe("Goal ID. Omit to inspect the first active goal.")
3237
+ goal_id: tool2.schema.string().optional().describe("Goal ID. Omit to inspect the first active goal."),
3238
+ includeTranscript: tool2.schema.boolean().optional().describe("Include live transcript tail (adds ~100ms). Default true. Set false for fast metadata-only."),
3239
+ transcriptLimit: tool2.schema.number().optional().describe("Number of transcript messages to include (1-10). Default 3.")
3172
3240
  },
3173
3241
  execute: async (args, context) => {
3174
3242
  const state = await readState(directory);
@@ -3181,6 +3249,25 @@ function ownerTools(options) {
3181
3249
  };
3182
3250
  }
3183
3251
  const runtime = state.runtimes.find((r) => r.goalID === goal.id);
3252
+ const includeTranscript = args.includeTranscript !== false;
3253
+ const tLimit = Math.min(10, Math.max(1, args.transcriptLimit ?? 3));
3254
+ const [transcriptTail, progressHistory, artifactSummary, pendingInbox] = await Promise.all([
3255
+ includeTranscript && goal.workerSessionID ? withTimeout2(host.readMessages(goal.workerSessionID, tLimit), 900).catch(() => null) : Promise.resolve(null),
3256
+ readEvents(directory, 60).then((evs) => evs.filter((e) => e.goalID === goal.id && e.type === "goal.progress").slice(-5).map((e) => ({ summary: String(e.summary || "").slice(0, 120), next: e.next ? String(e.next).slice(0, 80) : undefined, at: String(e.timestamp || "") }))).catch(() => []),
3257
+ (async () => {
3258
+ const dir = goal.config.artifactDir;
3259
+ if (!dir)
3260
+ return;
3261
+ try {
3262
+ const files = await fs3.readdir(dir);
3263
+ return files.length ? `${files.length} file(s): ${files.slice(0, 8).join(", ")}` : "no artifacts yet";
3264
+ } catch {
3265
+ return "no artifacts yet";
3266
+ }
3267
+ })(),
3268
+ peekGoalInbox(directory, goal.id).then((msgs) => msgs.slice(-3)).catch(() => [])
3269
+ ]);
3270
+ const activeToolCallIDs = runtime?.activeToolCallIDs ?? [];
3184
3271
  return {
3185
3272
  title: `Goal: ${goal.name}`,
3186
3273
  output: JSON.stringify({
@@ -3207,6 +3294,15 @@ function ownerTools(options) {
3207
3294
  blocker: goal.blocker,
3208
3295
  tokensUsed: goal.tokensUsed,
3209
3296
  timeUsedSeconds: goal.timeUsedSeconds,
3297
+ progressHistory,
3298
+ pendingInbox,
3299
+ live: {
3300
+ artifactSummary,
3301
+ transcriptTail: transcriptTail ? transcriptTail.map((m) => ({ role: m.role, content: String(m.content || "").slice(0, 400), timestamp: m.timestamp, messageID: m.messageID, parentMessageID: m.parentMessageID })) : undefined,
3302
+ activeToolCallIDs,
3303
+ pendingToolCalls: activeToolCallIDs.length,
3304
+ lastActivityAge: runtime?.lastActivityAt ? `${Math.floor((Date.now() - Date.parse(runtime.lastActivityAt)) / 1000)}s ago` : undefined
3305
+ },
3210
3306
  runtime: runtime ? {
3211
3307
  phase: runtime.phase,
3212
3308
  runCount: runtime.runCount,
@@ -3214,13 +3310,29 @@ function ownerTools(options) {
3214
3310
  runGeneration: runtime.runGeneration,
3215
3311
  evaluatorRejectionCount: runtime.evaluatorRejectionCount,
3216
3312
  freeRetryPending: runtime.freeRetryPending,
3313
+ lastRejectionDetails: runtime.lastRejectionDetails?.slice(0, 800),
3217
3314
  consecutiveFailures: runtime.consecutiveFailures,
3315
+ noProgressCount: runtime.noProgressCount,
3218
3316
  lastError: runtime.lastError,
3219
3317
  lastProgressAt: runtime.lastProgressAt,
3220
3318
  lastRunAt: runtime.lastRunAt,
3319
+ lastActivityAt: runtime.lastActivityAt,
3320
+ lastCompactAt: runtime.lastCompactAt,
3321
+ activePromptMessageID: runtime.activePromptMessageID,
3322
+ activeAssistantMessageID: runtime.activeAssistantMessageID,
3323
+ activeAssistantCompletedAt: runtime.activeAssistantCompletedAt,
3324
+ idleCandidateAt: runtime.idleCandidateAt,
3325
+ idleCandidateGeneration: runtime.idleCandidateGeneration,
3326
+ unknownStatusCount: runtime.unknownStatusCount,
3327
+ lastUnknownStatusAt: runtime.lastUnknownStatusAt,
3328
+ workerUnreachableNotifiedAt: runtime.workerUnreachableNotifiedAt,
3329
+ retryAfter: runtime.retryAfter,
3330
+ forceFinishRequested: runtime.forceFinishRequested,
3221
3331
  scheduleRunCount: runtime.scheduleRunCount,
3222
3332
  nextRunAt: runtime.nextRunAt,
3223
- lastScheduleAt: runtime.lastScheduleAt
3333
+ lastScheduleAt: runtime.lastScheduleAt,
3334
+ lastVerificationAttempt: runtime.lastVerificationAttempt,
3335
+ recentVerificationAttempts: runtime.recentVerificationAttempts?.slice(-2)
3224
3336
  } : undefined
3225
3337
  }, null, 2)
3226
3338
  };