@bojackduy/opencode-loopd 1.7.2 → 1.8.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/README.md +2 -2
- package/dist/server.js +130 -18
- package/dist/tui.js +551 -377
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> Run long-running tasks as **autonomous child sessions** (subagents) while the main chat stays interactive. Create a goal with `/goal`, watch it loop in the background, and monitor everything from a keyboard-driven TUI — without blocking the parent conversation.
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/@bojackduy/opencode-loopd)
|
|
8
|
-
[](LICENSE)
|
|
9
9
|
[](https://opencode.ai)
|
|
10
10
|
[](https://github.com/anomalyco/opentui)
|
|
11
11
|
|
|
@@ -298,4 +298,4 @@ OpenCode plugin for **background agents**, **autonomous subagents**, **loop**, *
|
|
|
298
298
|
|
|
299
299
|
## License
|
|
300
300
|
|
|
301
|
-
AGPL-3.0-
|
|
301
|
+
AGPL-3.0-or-later — see [LICENSE](./LICENSE).
|
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
|
}
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
};
|