@bybrawe/opencode-loop 0.5.32 → 0.5.34
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 +17 -0
- package/README.md +197 -283
- package/commands/loop.md +1 -1
- package/package.json +3 -3
- package/scripts/action-dispatch-test.mjs +117 -0
- package/scripts/comprehensive-test.mjs +1 -1
- package/scripts/host-loop-canary.mjs +8 -3
- package/scripts/loop-command-handlers-test.mjs +33 -4
- package/scripts/loop-registration-test.mjs +77 -3
- package/scripts/run-admission-test.mjs +133 -0
- package/scripts/run-finalization-test.mjs +119 -0
- package/scripts/schedule-policy-test.mjs +114 -0
- package/scripts/schedule-syntax-test.mjs +75 -0
- package/scripts/session-status-idle-recovery-test.mjs +44 -0
- package/src/index.js +916 -473
- package/src/source/core/continuation.js +20 -0
- package/src/source/core/jobs.js +3 -0
- package/src/source/core/schedule-syntax.js +93 -0
- package/src/source/opencode/loop-commands.js +39 -30
- package/src/source/opencode/loop-registration.js +40 -3
- package/src/source/runtime/action-dispatch.js +108 -0
- package/src/source/runtime/companion-goal.js +37 -0
- package/src/source/runtime/executor.js +55 -228
- package/src/source/runtime/goal-evidence.js +39 -0
- package/src/source/runtime/goal-policy.js +1 -1
- package/src/source/runtime/goal-prompt.js +53 -0
- package/src/source/runtime/goal-report.js +52 -0
- package/src/source/runtime/goal-runtime.js +9 -141
- package/src/source/runtime/loop-diagnostics.js +45 -0
- package/src/source/runtime/run-admission.js +121 -0
- package/src/source/runtime/run-finalization.js +100 -0
- package/src/source/runtime/schedule-policy.js +80 -0
- package/src/source/runtime/scheduler-diagnostics.js +30 -0
- package/src/source/runtime/scheduler.js +10 -23
- package/src/source/runtime/session-status.js +28 -4
package/src/index.js
CHANGED
|
@@ -1021,6 +1021,27 @@ function commandArgsText(args) {
|
|
|
1021
1021
|
return String(args);
|
|
1022
1022
|
}
|
|
1023
1023
|
|
|
1024
|
+
// src/source/core/continuation.js
|
|
1025
|
+
var CONTINUATION_SHORTHANDS = new Set([
|
|
1026
|
+
"continue",
|
|
1027
|
+
"continue.",
|
|
1028
|
+
"continue working",
|
|
1029
|
+
"keep going",
|
|
1030
|
+
"go on",
|
|
1031
|
+
"devam",
|
|
1032
|
+
"devam et",
|
|
1033
|
+
"devam et.",
|
|
1034
|
+
"devam et bakal\u0131m"
|
|
1035
|
+
]);
|
|
1036
|
+
function isContinuationShorthand(value) {
|
|
1037
|
+
return CONTINUATION_SHORTHANDS.has(String(value || "").trim().toLowerCase().replace(/\s+/g, " "));
|
|
1038
|
+
}
|
|
1039
|
+
function continuationProjectInstruction(value) {
|
|
1040
|
+
if (!isContinuationShorthand(value))
|
|
1041
|
+
return "";
|
|
1042
|
+
return "Treat this as continuation of the current project and conversation, not a fresh task. Inspect the repository state, relevant files, TODO/progress notes, recent changes, and git status as needed to identify the next unfinished step. Continue from existing work, do not redo completed work, and verify meaningful changes when practical.";
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1024
1045
|
// src/source/core/jobs.js
|
|
1025
1046
|
function presetDefaults(name) {
|
|
1026
1047
|
if (name === "loop-compact")
|
|
@@ -1086,6 +1107,9 @@ function actionKind(action, job = {}) {
|
|
|
1086
1107
|
}
|
|
1087
1108
|
function decoratePrompt(job) {
|
|
1088
1109
|
const additions = [];
|
|
1110
|
+
const continuation = continuationProjectInstruction(job.action);
|
|
1111
|
+
if (continuation)
|
|
1112
|
+
additions.push(continuation);
|
|
1089
1113
|
if (job.progressFile)
|
|
1090
1114
|
additions.push(`Use ${job.progressFile} as the main progress/TODO state file. Read it before choosing the next task and update it after work.`);
|
|
1091
1115
|
if (job.lastVerifyFailure)
|
|
@@ -1231,91 +1255,10 @@ function createCommandRouter(options = {}) {
|
|
|
1231
1255
|
};
|
|
1232
1256
|
}
|
|
1233
1257
|
|
|
1234
|
-
// src/source/runtime/goal-
|
|
1258
|
+
// src/source/runtime/goal-report.js
|
|
1235
1259
|
import { promises as fs3 } from "fs";
|
|
1236
1260
|
import path3 from "path";
|
|
1237
1261
|
var GOAL_REPORT_DIR = "goals";
|
|
1238
|
-
var GOAL_PROMPT_PREFIX = "EXPERIMENTAL OPENCODE GOAL MODE ITERATION";
|
|
1239
|
-
async function buildGoalPrompt(directory, job) {
|
|
1240
|
-
const sections = [];
|
|
1241
|
-
sections.push(`Working directory:
|
|
1242
|
-
${path3.resolve(directory)}
|
|
1243
|
-
Keep every file operation inside this directory. Prefer workspace-relative paths such as "src/index.js"; never turn a relative path into a root path such as "/src/index.js".`);
|
|
1244
|
-
const objective = String(job.action || "").trim();
|
|
1245
|
-
if (objective)
|
|
1246
|
-
sections.push(`Goal objective:
|
|
1247
|
-
${objective}`);
|
|
1248
|
-
if (job.goalFile) {
|
|
1249
|
-
const text = await readSmallTextFile(path3.resolve(directory, job.goalFile), 120000);
|
|
1250
|
-
if (text.trim())
|
|
1251
|
-
sections.push(`Goal file ${job.goalFile}:
|
|
1252
|
-
${text.trim()}`);
|
|
1253
|
-
else
|
|
1254
|
-
sections.push(`Goal file ${job.goalFile} was requested but could not be read. Continue from the inline goal objective.`);
|
|
1255
|
-
}
|
|
1256
|
-
if (job.promptFile) {
|
|
1257
|
-
const text = await readSmallTextFile(path3.resolve(directory, job.promptFile), 120000);
|
|
1258
|
-
if (text.trim())
|
|
1259
|
-
sections.push(`Extra goal instructions from ${job.promptFile}:
|
|
1260
|
-
${text.trim()}`);
|
|
1261
|
-
}
|
|
1262
|
-
if (job.goalAcceptance?.length)
|
|
1263
|
-
sections.push(`Acceptance criteria:
|
|
1264
|
-
` + job.goalAcceptance.map((item, index) => `${index + 1}. ${item}`).join(`
|
|
1265
|
-
`));
|
|
1266
|
-
if (job.goalChecks?.length)
|
|
1267
|
-
sections.push(`Verification commands that define useful evidence:
|
|
1268
|
-
` + job.goalChecks.map((item, index) => `${index + 1}. ${item}`).join(`
|
|
1269
|
-
`));
|
|
1270
|
-
if (job.verifyCommand)
|
|
1271
|
-
sections.push(`Post-turn verify command configured by the loop: ${job.verifyCommand}`);
|
|
1272
|
-
if (job.lastGoalChecks?.length)
|
|
1273
|
-
sections.push(`Latest goal check results:
|
|
1274
|
-
` + job.lastGoalChecks.map((item) => `- ${item.command}: exit ${item.code}`).join(`
|
|
1275
|
-
`));
|
|
1276
|
-
if (job.lastVerifyFailure)
|
|
1277
|
-
sections.push(`Previous verify/check failure summary:
|
|
1278
|
-
` + String(job.lastVerifyFailure).slice(0, 1600));
|
|
1279
|
-
if (job.goalCompletionRejectedReason)
|
|
1280
|
-
sections.push(`Previous completion attempt was rejected:
|
|
1281
|
-
${job.goalCompletionRejectedReason}`);
|
|
1282
|
-
if ((job.maxNoProgress ?? DEFAULT_GOAL_MAX_NO_PROGRESS) > 0)
|
|
1283
|
-
sections.push(`No-progress guard:
|
|
1284
|
-
${job.noProgressCount || 0}/${job.maxNoProgress ?? DEFAULT_GOAL_MAX_NO_PROGRESS} recent turn(s) without recorded meaningful progress.`);
|
|
1285
|
-
if (job.goalProgress?.length)
|
|
1286
|
-
sections.push(`Recent goal progress:
|
|
1287
|
-
` + job.goalProgress.slice(-5).map((item) => `- ${item.time}: ${item.summary}`).join(`
|
|
1288
|
-
`));
|
|
1289
|
-
for (const file of job.includeFiles || []) {
|
|
1290
|
-
const text = await readSmallTextFile(path3.resolve(directory, file), 80000);
|
|
1291
|
-
if (text.trim())
|
|
1292
|
-
sections.push(`Context from ${file}:
|
|
1293
|
-
${text.trim().slice(0, 20000)}`);
|
|
1294
|
-
}
|
|
1295
|
-
return `${GOAL_PROMPT_PREFIX}.
|
|
1296
|
-
|
|
1297
|
-
You are pursuing an experimental persistent goal for this OpenCode session. This is not a timer loop and not a one-shot prompt. Keep working toward the goal until it is completed, blocked, paused, cleared, or stopped by safety limits.
|
|
1298
|
-
|
|
1299
|
-
Rules:
|
|
1300
|
-
- Work on the next smallest useful step toward the goal.
|
|
1301
|
-
- Prefer direct code changes, tests, typechecks, builds, and evidence over discussion.
|
|
1302
|
-
- Do not claim the goal is complete unless the acceptance criteria are satisfied and verification evidence supports it.
|
|
1303
|
-
- If verification commands are configured, do not call opencode_loop_goal_complete until the latest relevant checks have passed unless the user explicitly overrides the goal.
|
|
1304
|
-
- Completion evidence must be concrete: mention commands, files, checks, results, or code inspection details.
|
|
1305
|
-
- When the goal is complete, call the tool opencode_loop_goal_complete with a summary and evidence.
|
|
1306
|
-
- If you are truly blocked and need user input, call the tool opencode_loop_goal_blocked with the reason and what is needed.
|
|
1307
|
-
- If you made meaningful progress but the goal is not complete, call the tool opencode_loop_goal_progress with the summary and next step.
|
|
1308
|
-
- If you cannot make meaningful progress for this turn, call opencode_loop_goal_blocked instead of repeating the same attempt.
|
|
1309
|
-
- Do not call completion tools just to be polite; only call them when the state is real.
|
|
1310
|
-
- Do not ask the user questions unless blocked; make reasonable assumptions and continue.
|
|
1311
|
-
- Follow safety rules: no destructive commands, force pushes, production deploys, production database resets, or deleting user data.
|
|
1312
|
-
|
|
1313
|
-
${sections.join(`
|
|
1314
|
-
|
|
1315
|
-
---
|
|
1316
|
-
|
|
1317
|
-
`)}`;
|
|
1318
|
-
}
|
|
1319
1262
|
function goalReportPath(directory, sessionID, job) {
|
|
1320
1263
|
return path3.join(stateDir(directory), GOAL_REPORT_DIR, `${safeID(sessionID)}-${safeID(job.name || job.id)}.md`);
|
|
1321
1264
|
}
|
|
@@ -1375,21 +1318,8 @@ async function writeGoalReport(directory, sessionID, job) {
|
|
|
1375
1318
|
await ensureDir(path3.dirname(target));
|
|
1376
1319
|
await fs3.writeFile(target, goalReportText(job), "utf8");
|
|
1377
1320
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
if (!goals.length)
|
|
1381
|
-
return;
|
|
1382
|
-
const text = String(target || "").trim();
|
|
1383
|
-
if (!text || ["active", "current", "goal"].includes(text.toLowerCase()))
|
|
1384
|
-
return goals.find((job) => job.goalStatus === "active" && job.enabled !== false) || goals[0];
|
|
1385
|
-
return goals.find((job, index) => matchJob(job, text, index));
|
|
1386
|
-
}
|
|
1387
|
-
function parseGoalToolText(args, fields) {
|
|
1388
|
-
const result = {};
|
|
1389
|
-
for (const field of fields)
|
|
1390
|
-
result[field] = String(args?.[field] || "").trim();
|
|
1391
|
-
return result;
|
|
1392
|
-
}
|
|
1321
|
+
|
|
1322
|
+
// src/source/runtime/goal-evidence.js
|
|
1393
1323
|
function hasConcreteGoalEvidence(value) {
|
|
1394
1324
|
const text = String(value || "").trim();
|
|
1395
1325
|
if (text.length < 24)
|
|
@@ -1432,6 +1362,107 @@ function goalMadeMeaningfulProgress(beforeJob, afterJob) {
|
|
|
1432
1362
|
return true;
|
|
1433
1363
|
return false;
|
|
1434
1364
|
}
|
|
1365
|
+
|
|
1366
|
+
// src/source/runtime/goal-prompt.js
|
|
1367
|
+
import path4 from "path";
|
|
1368
|
+
var GOAL_PROMPT_PREFIX = "EXPERIMENTAL OPENCODE GOAL MODE ITERATION";
|
|
1369
|
+
async function buildGoalPrompt(directory, job) {
|
|
1370
|
+
const sections = [];
|
|
1371
|
+
sections.push(`Working directory:
|
|
1372
|
+
${path4.resolve(directory)}
|
|
1373
|
+
Keep every file operation inside this directory. Prefer workspace-relative paths such as "src/index.js"; never turn a relative path into a root path such as "/src/index.js".`);
|
|
1374
|
+
const objective = String(job.action || "").trim();
|
|
1375
|
+
if (objective)
|
|
1376
|
+
sections.push(`Goal objective:
|
|
1377
|
+
${objective}`);
|
|
1378
|
+
if (job.goalFile) {
|
|
1379
|
+
const text = await readSmallTextFile(path4.resolve(directory, job.goalFile), 120000);
|
|
1380
|
+
if (text.trim())
|
|
1381
|
+
sections.push(`Goal file ${job.goalFile}:
|
|
1382
|
+
${text.trim()}`);
|
|
1383
|
+
else
|
|
1384
|
+
sections.push(`Goal file ${job.goalFile} was requested but could not be read. Continue from the inline goal objective.`);
|
|
1385
|
+
}
|
|
1386
|
+
if (job.promptFile) {
|
|
1387
|
+
const text = await readSmallTextFile(path4.resolve(directory, job.promptFile), 120000);
|
|
1388
|
+
if (text.trim())
|
|
1389
|
+
sections.push(`Extra goal instructions from ${job.promptFile}:
|
|
1390
|
+
${text.trim()}`);
|
|
1391
|
+
}
|
|
1392
|
+
if (job.goalAcceptance?.length)
|
|
1393
|
+
sections.push(`Acceptance criteria:
|
|
1394
|
+
` + job.goalAcceptance.map((item, index) => `${index + 1}. ${item}`).join(`
|
|
1395
|
+
`));
|
|
1396
|
+
if (job.goalChecks?.length)
|
|
1397
|
+
sections.push(`Verification commands that define useful evidence:
|
|
1398
|
+
` + job.goalChecks.map((item, index) => `${index + 1}. ${item}`).join(`
|
|
1399
|
+
`));
|
|
1400
|
+
if (job.verifyCommand)
|
|
1401
|
+
sections.push(`Post-turn verify command configured by the loop: ${job.verifyCommand}`);
|
|
1402
|
+
if (job.lastGoalChecks?.length)
|
|
1403
|
+
sections.push(`Latest goal check results:
|
|
1404
|
+
` + job.lastGoalChecks.map((item) => `- ${item.command}: exit ${item.code}`).join(`
|
|
1405
|
+
`));
|
|
1406
|
+
if (job.lastVerifyFailure)
|
|
1407
|
+
sections.push(`Previous verify/check failure summary:
|
|
1408
|
+
` + String(job.lastVerifyFailure).slice(0, 1600));
|
|
1409
|
+
if (job.goalCompletionRejectedReason)
|
|
1410
|
+
sections.push(`Previous completion attempt was rejected:
|
|
1411
|
+
${job.goalCompletionRejectedReason}`);
|
|
1412
|
+
if ((job.maxNoProgress ?? DEFAULT_GOAL_MAX_NO_PROGRESS) > 0)
|
|
1413
|
+
sections.push(`No-progress guard:
|
|
1414
|
+
${job.noProgressCount || 0}/${job.maxNoProgress ?? DEFAULT_GOAL_MAX_NO_PROGRESS} recent turn(s) without recorded meaningful progress.`);
|
|
1415
|
+
if (job.goalProgress?.length)
|
|
1416
|
+
sections.push(`Recent goal progress:
|
|
1417
|
+
` + job.goalProgress.slice(-5).map((item) => `- ${item.time}: ${item.summary}`).join(`
|
|
1418
|
+
`));
|
|
1419
|
+
for (const file of job.includeFiles || []) {
|
|
1420
|
+
const text = await readSmallTextFile(path4.resolve(directory, file), 80000);
|
|
1421
|
+
if (text.trim())
|
|
1422
|
+
sections.push(`Context from ${file}:
|
|
1423
|
+
${text.trim().slice(0, 20000)}`);
|
|
1424
|
+
}
|
|
1425
|
+
return `${GOAL_PROMPT_PREFIX}.
|
|
1426
|
+
|
|
1427
|
+
You are pursuing an experimental persistent goal for this OpenCode session. This is not a timer loop and not a one-shot prompt. Keep working toward the goal until it is completed, blocked, paused, cleared, or stopped by safety limits.
|
|
1428
|
+
|
|
1429
|
+
Rules:
|
|
1430
|
+
- Work on the next smallest useful step toward the goal.
|
|
1431
|
+
- Prefer direct code changes, tests, typechecks, builds, and evidence over discussion.
|
|
1432
|
+
- Do not claim the goal is complete unless the acceptance criteria are satisfied and verification evidence supports it.
|
|
1433
|
+
- If verification commands are configured, do not call opencode_loop_goal_complete until the latest relevant checks have passed unless the user explicitly overrides the goal.
|
|
1434
|
+
- Completion evidence must be concrete: mention commands, files, checks, results, or code inspection details.
|
|
1435
|
+
- When the goal is complete, call the tool opencode_loop_goal_complete with a summary and evidence.
|
|
1436
|
+
- If you are truly blocked and need user input, call the tool opencode_loop_goal_blocked with the reason and what is needed.
|
|
1437
|
+
- If you made meaningful progress but the goal is not complete, call the tool opencode_loop_goal_progress with the summary and next step.
|
|
1438
|
+
- If you cannot make meaningful progress for this turn, call opencode_loop_goal_blocked instead of repeating the same attempt.
|
|
1439
|
+
- Do not call completion tools just to be polite; only call them when the state is real.
|
|
1440
|
+
- Do not ask the user questions unless blocked; make reasonable assumptions and continue.
|
|
1441
|
+
- Follow safety rules: no destructive commands, force pushes, production deploys, production database resets, or deleting user data.
|
|
1442
|
+
|
|
1443
|
+
${sections.join(`
|
|
1444
|
+
|
|
1445
|
+
---
|
|
1446
|
+
|
|
1447
|
+
`)}`;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// src/source/runtime/goal-runtime.js
|
|
1451
|
+
function pickGoalJob(state, target = "") {
|
|
1452
|
+
const goals = (state.jobs || []).filter(isGoalJob);
|
|
1453
|
+
if (!goals.length)
|
|
1454
|
+
return;
|
|
1455
|
+
const text = String(target || "").trim();
|
|
1456
|
+
if (!text || ["active", "current", "goal"].includes(text.toLowerCase()))
|
|
1457
|
+
return goals.find((job) => job.goalStatus === "active" && job.enabled !== false) || goals[0];
|
|
1458
|
+
return goals.find((job, index) => matchJob(job, text, index));
|
|
1459
|
+
}
|
|
1460
|
+
function parseGoalToolText(args, fields) {
|
|
1461
|
+
const result = {};
|
|
1462
|
+
for (const field of fields)
|
|
1463
|
+
result[field] = String(args?.[field] || "").trim();
|
|
1464
|
+
return result;
|
|
1465
|
+
}
|
|
1435
1466
|
async function rejectGoalCompletion(directory, sessionID, state, job, reason) {
|
|
1436
1467
|
job.goalCompletionRejectedAt = now();
|
|
1437
1468
|
job.goalCompletionRejectedReason = reason;
|
|
@@ -1616,47 +1647,224 @@ function createGoalCommandHandlers(options = {}) {
|
|
|
1616
1647
|
}
|
|
1617
1648
|
|
|
1618
1649
|
// src/source/opencode/loop-commands.js
|
|
1619
|
-
import { promises as
|
|
1620
|
-
import
|
|
1621
|
-
var SERVICE2 = "opencode-loop";
|
|
1622
|
-
var DEFAULT_PROGRESS_MD = `# Progress
|
|
1623
|
-
|
|
1624
|
-
## Current Goal
|
|
1625
|
-
Describe the current project goal here.
|
|
1626
|
-
|
|
1627
|
-
## Agent Rules
|
|
1628
|
-
- Do not ask questions unless truly blocked.
|
|
1629
|
-
- Make reasonable assumptions and continue.
|
|
1630
|
-
- Work on unfinished TODOs in order.
|
|
1631
|
-
- Mark completed TODOs with [x].
|
|
1632
|
-
- Add new bugs, ideas, and follow-up work as TODOs.
|
|
1633
|
-
- Run tests, lint, or build when available.
|
|
1634
|
-
- Do not run destructive commands, force pushes, production deploys, or database resets.
|
|
1635
|
-
|
|
1636
|
-
## Active TODO
|
|
1637
|
-
- [ ] Review the project structure and pick the next safe improvement.
|
|
1650
|
+
import { promises as fs6 } from "fs";
|
|
1651
|
+
import path7 from "path";
|
|
1638
1652
|
|
|
1639
|
-
|
|
1640
|
-
|
|
1653
|
+
// src/source/runtime/companion-goal.js
|
|
1654
|
+
import { promises as fs4 } from "fs";
|
|
1655
|
+
import path5 from "path";
|
|
1656
|
+
function goalRoot(directory) {
|
|
1657
|
+
return path5.join(directory, ".opencode", "goals");
|
|
1658
|
+
}
|
|
1659
|
+
async function findDedicatedGoalForSession(directory, sessionID) {
|
|
1660
|
+
if (!directory || !sessionID)
|
|
1661
|
+
return;
|
|
1662
|
+
let names;
|
|
1663
|
+
try {
|
|
1664
|
+
names = await fs4.readdir(goalRoot(directory));
|
|
1665
|
+
} catch (error) {
|
|
1666
|
+
if (error?.code === "ENOENT")
|
|
1667
|
+
return;
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
for (const name of names) {
|
|
1671
|
+
if (!name.endsWith(".json"))
|
|
1672
|
+
continue;
|
|
1673
|
+
try {
|
|
1674
|
+
const value = JSON.parse(await fs4.readFile(path5.join(goalRoot(directory), name), "utf8"));
|
|
1675
|
+
if (value?.sessionID === sessionID)
|
|
1676
|
+
return value;
|
|
1677
|
+
} catch {}
|
|
1678
|
+
}
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
function dedicatedGoalOwnsContinuation(goal) {
|
|
1682
|
+
return goal?.status === "active";
|
|
1683
|
+
}
|
|
1684
|
+
function dedicatedGoalSummary(goal) {
|
|
1685
|
+
if (!goal)
|
|
1686
|
+
return "not detected";
|
|
1687
|
+
const id = String(goal.id || "unknown").slice(0, 12);
|
|
1688
|
+
const status = String(goal.status || "unknown");
|
|
1689
|
+
return `${status} (${id})`;
|
|
1690
|
+
}
|
|
1641
1691
|
|
|
1642
|
-
|
|
1643
|
-
|
|
1692
|
+
// src/source/runtime/loop-diagnostics.js
|
|
1693
|
+
import { promises as fs5 } from "fs";
|
|
1694
|
+
import path6 from "path";
|
|
1644
1695
|
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
if (
|
|
1650
|
-
|
|
1651
|
-
|
|
1696
|
+
// src/source/runtime/schedule-policy.js
|
|
1697
|
+
var TERMINAL_GOAL_STATUSES = new Set(["completed", "blocked", "cleared"]);
|
|
1698
|
+
function inferredScheduleMode(job) {
|
|
1699
|
+
const explicit = String(job?.scheduleMode || "").toLowerCase();
|
|
1700
|
+
if (["idle", "interval", "once", "watch"].includes(explicit))
|
|
1701
|
+
return explicit;
|
|
1702
|
+
if (job?.watchPaths?.length)
|
|
1703
|
+
return "watch";
|
|
1704
|
+
if (Number(job?.maxRuns || 0) === 1 && job?.immediate === false && Number(job?.intervalMs || 0) > 0)
|
|
1705
|
+
return "once";
|
|
1706
|
+
return Number(job?.intervalMs || 0) === 0 ? "idle" : "interval";
|
|
1707
|
+
}
|
|
1708
|
+
function jobRunnable(job) {
|
|
1709
|
+
if (!job)
|
|
1710
|
+
return false;
|
|
1711
|
+
if (isGoalJob(job) && TERMINAL_GOAL_STATUSES.has(job.goalStatus))
|
|
1712
|
+
return false;
|
|
1713
|
+
if (!job.enabled || job.paused)
|
|
1714
|
+
return false;
|
|
1715
|
+
if (Number(job.maxRuns || 0) > 0 && Number(job.runCount || 0) >= Number(job.maxRuns || 0))
|
|
1716
|
+
return false;
|
|
1717
|
+
return true;
|
|
1652
1718
|
}
|
|
1653
|
-
function
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
const
|
|
1659
|
-
|
|
1719
|
+
function jobDueAt(job, current = Date.now()) {
|
|
1720
|
+
if (!jobRunnable(job))
|
|
1721
|
+
return Infinity;
|
|
1722
|
+
if (Number(job.runNowRequestedAt || 0) > 0)
|
|
1723
|
+
return current;
|
|
1724
|
+
const created = Date.parse(job.createdAt || "");
|
|
1725
|
+
if (Number(job.maxRuntimeMs || 0) > 0 && Number.isFinite(created) && current - created >= Number(job.maxRuntimeMs || 0))
|
|
1726
|
+
return current;
|
|
1727
|
+
if (job.watchPaths?.length)
|
|
1728
|
+
return job.watchTriggered === true ? current : Infinity;
|
|
1729
|
+
const intervalMs = Number(job.intervalMs || 0);
|
|
1730
|
+
if (intervalMs === 0)
|
|
1731
|
+
return current;
|
|
1732
|
+
const lastRunAt = Number(job.lastRunAt || 0);
|
|
1733
|
+
if (!lastRunAt) {
|
|
1734
|
+
if (job.immediate === false)
|
|
1735
|
+
return (Number.isFinite(created) ? created : current) + intervalMs;
|
|
1736
|
+
return current;
|
|
1737
|
+
}
|
|
1738
|
+
return lastRunAt + intervalMs;
|
|
1739
|
+
}
|
|
1740
|
+
function jobIsDue(job, current = Date.now(), force = false) {
|
|
1741
|
+
if (!jobRunnable(job))
|
|
1742
|
+
return false;
|
|
1743
|
+
if (force)
|
|
1744
|
+
return true;
|
|
1745
|
+
return jobDueAt(job, current) <= current;
|
|
1746
|
+
}
|
|
1747
|
+
function dueJobs(state, current = Date.now(), force = false) {
|
|
1748
|
+
return (state?.jobs || []).filter((job) => jobIsDue(job, current, force)).sort((a, b) => Number(Number(b.runNowRequestedAt || 0) > 0) - Number(Number(a.runNowRequestedAt || 0) > 0));
|
|
1749
|
+
}
|
|
1750
|
+
function nextDueDelay(state, current = Date.now()) {
|
|
1751
|
+
let soonest = Infinity;
|
|
1752
|
+
for (const job of state?.jobs || [])
|
|
1753
|
+
soonest = Math.min(soonest, jobDueAt(job, current));
|
|
1754
|
+
if (!Number.isFinite(soonest))
|
|
1755
|
+
return Infinity;
|
|
1756
|
+
return Math.max(0, soonest - current);
|
|
1757
|
+
}
|
|
1758
|
+
function scheduleDescription(job) {
|
|
1759
|
+
const mode = inferredScheduleMode(job);
|
|
1760
|
+
const intervalMs = Number(job?.intervalMs || 0);
|
|
1761
|
+
if (mode === "idle")
|
|
1762
|
+
return "every idle";
|
|
1763
|
+
if (mode === "watch")
|
|
1764
|
+
return `on watch: ${(job.watchPaths || []).join(", ")}`;
|
|
1765
|
+
if (mode === "once")
|
|
1766
|
+
return intervalMs > 0 ? `once after ${durationToText(intervalMs)}` : "once on next idle";
|
|
1767
|
+
if (job?.immediate === false)
|
|
1768
|
+
return `every ${durationToText(intervalMs)}, first after ${durationToText(intervalMs)}`;
|
|
1769
|
+
return `every ${durationToText(intervalMs)}, starts on next idle`;
|
|
1770
|
+
}
|
|
1771
|
+
function scheduleState(job, current = Date.now()) {
|
|
1772
|
+
if (!job?.enabled)
|
|
1773
|
+
return "stopped";
|
|
1774
|
+
if (job?.paused)
|
|
1775
|
+
return "paused";
|
|
1776
|
+
if (Number(job?.runNowRequestedAt || 0) > 0)
|
|
1777
|
+
return "due now; waiting for idle";
|
|
1778
|
+
const mode = inferredScheduleMode(job);
|
|
1779
|
+
const dueAt = jobDueAt(job, current);
|
|
1780
|
+
if (!Number.isFinite(dueAt))
|
|
1781
|
+
return mode === "watch" ? "waiting for watched change" : "not scheduled";
|
|
1782
|
+
if (dueAt <= current)
|
|
1783
|
+
return mode === "idle" ? "waiting for idle" : "due; waiting for idle";
|
|
1784
|
+
return `due in ${durationToText(dueAt - current)}`;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// src/source/runtime/loop-diagnostics.js
|
|
1788
|
+
function describeJobScheduling(job, current = Date.now()) {
|
|
1789
|
+
return {
|
|
1790
|
+
schedule: scheduleDescription(job),
|
|
1791
|
+
state: scheduleState(job, current)
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
async function listPersistedLoopSessions(directory, currentSessionID) {
|
|
1795
|
+
const root = stateDir(directory);
|
|
1796
|
+
let names;
|
|
1797
|
+
try {
|
|
1798
|
+
names = await fs5.readdir(root);
|
|
1799
|
+
} catch (error) {
|
|
1800
|
+
if (error?.code === "ENOENT")
|
|
1801
|
+
return [];
|
|
1802
|
+
return [];
|
|
1803
|
+
}
|
|
1804
|
+
const sessions = [];
|
|
1805
|
+
for (const name of names) {
|
|
1806
|
+
if (!name.endsWith(".json"))
|
|
1807
|
+
continue;
|
|
1808
|
+
const sessionID = name.slice(0, -5);
|
|
1809
|
+
try {
|
|
1810
|
+
const parsed = JSON.parse(await fs5.readFile(path6.join(root, name), "utf8"));
|
|
1811
|
+
const jobs = Array.isArray(parsed?.jobs) ? parsed.jobs : [];
|
|
1812
|
+
const enabled = jobs.filter((job) => job?.enabled !== false && !job?.paused).length;
|
|
1813
|
+
const neverRan = jobs.filter((job) => Number(job?.runCount || 0) === 0).length;
|
|
1814
|
+
sessions.push({
|
|
1815
|
+
sessionID,
|
|
1816
|
+
current: sessionID === currentSessionID,
|
|
1817
|
+
jobs: jobs.length,
|
|
1818
|
+
enabled,
|
|
1819
|
+
neverRan
|
|
1820
|
+
});
|
|
1821
|
+
} catch {
|
|
1822
|
+
sessions.push({ sessionID, current: sessionID === currentSessionID, jobs: 0, enabled: 0, neverRan: 0, corrupt: true });
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
return sessions.sort((a, b) => Number(b.current) - Number(a.current) || b.enabled - a.enabled || a.sessionID.localeCompare(b.sessionID));
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// src/source/opencode/loop-commands.js
|
|
1829
|
+
var SERVICE2 = "opencode-loop";
|
|
1830
|
+
var DEFAULT_PROGRESS_MD = `# Progress
|
|
1831
|
+
|
|
1832
|
+
## Current Goal
|
|
1833
|
+
Describe the current project goal here.
|
|
1834
|
+
|
|
1835
|
+
## Agent Rules
|
|
1836
|
+
- Do not ask questions unless truly blocked.
|
|
1837
|
+
- Make reasonable assumptions and continue.
|
|
1838
|
+
- Work on unfinished TODOs in order.
|
|
1839
|
+
- Mark completed TODOs with [x].
|
|
1840
|
+
- Add new bugs, ideas, and follow-up work as TODOs.
|
|
1841
|
+
- Run tests, lint, or build when available.
|
|
1842
|
+
- Do not run destructive commands, force pushes, production deploys, or database resets.
|
|
1843
|
+
|
|
1844
|
+
## Active TODO
|
|
1845
|
+
- [ ] Review the project structure and pick the next safe improvement.
|
|
1846
|
+
|
|
1847
|
+
## Completed
|
|
1848
|
+
- [x] Created progress.md.
|
|
1849
|
+
|
|
1850
|
+
## Backlog Ideas
|
|
1851
|
+
- [ ] Add more project-specific tasks here.
|
|
1852
|
+
|
|
1853
|
+
## Blocked
|
|
1854
|
+
- None.
|
|
1855
|
+
`;
|
|
1856
|
+
function requireFunction3(value, name) {
|
|
1857
|
+
if (typeof value !== "function")
|
|
1858
|
+
throw new TypeError(`createLoopCommandHandlers requires ${name}`);
|
|
1859
|
+
return value;
|
|
1860
|
+
}
|
|
1861
|
+
function createLoopCommandHandlers(options = {}) {
|
|
1862
|
+
const clearActiveRun = requireFunction3(options.clearActiveRun, "clearActiveRun");
|
|
1863
|
+
const cancelDueWork = requireFunction3(options.cancelDueWork, "cancelDueWork");
|
|
1864
|
+
const stopWatchdog = requireFunction3(options.stopWatchdog, "stopWatchdog");
|
|
1865
|
+
const scheduleDueWork = requireFunction3(options.scheduleDueWork, "scheduleDueWork");
|
|
1866
|
+
const maybeRunDueJobs = requireFunction3(options.maybeRunDueJobs, "maybeRunDueJobs");
|
|
1867
|
+
const toast2 = requireFunction3(options.toast, "toast");
|
|
1660
1868
|
const say2 = requireFunction3(options.say, "say");
|
|
1661
1869
|
const now2 = typeof options.now === "function" ? options.now : now;
|
|
1662
1870
|
const readState2 = typeof options.readState === "function" ? options.readState : readState;
|
|
@@ -1664,8 +1872,10 @@ function createLoopCommandHandlers(options = {}) {
|
|
|
1664
1872
|
const removeState2 = typeof options.removeState === "function" ? options.removeState : removeState;
|
|
1665
1873
|
const pathExists2 = typeof options.pathExists === "function" ? options.pathExists : pathExists;
|
|
1666
1874
|
const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
|
|
1667
|
-
const readFile = typeof options.readFile === "function" ? options.readFile : (...args) =>
|
|
1668
|
-
const writeFile = typeof options.writeFile === "function" ? options.writeFile : (...args) =>
|
|
1875
|
+
const readFile = typeof options.readFile === "function" ? options.readFile : (...args) => fs6.readFile(...args);
|
|
1876
|
+
const writeFile = typeof options.writeFile === "function" ? options.writeFile : (...args) => fs6.writeFile(...args);
|
|
1877
|
+
const listPersistedLoopSessions2 = typeof options.listPersistedLoopSessions === "function" ? options.listPersistedLoopSessions : listPersistedLoopSessions;
|
|
1878
|
+
const findDedicatedGoalForSession2 = typeof options.findDedicatedGoalForSession === "function" ? options.findDedicatedGoalForSession : findDedicatedGoalForSession;
|
|
1669
1879
|
const runtimeVersion = options.runtimeVersion || process.version;
|
|
1670
1880
|
const runtimePlatform = options.runtimePlatform || process.platform;
|
|
1671
1881
|
async function stopLoop(directory, client, sessionID, args) {
|
|
@@ -1697,15 +1907,11 @@ function createLoopCommandHandlers(options = {}) {
|
|
|
1697
1907
|
async function statusLoop(directory, client, sessionID) {
|
|
1698
1908
|
const state = await readState2(directory, sessionID);
|
|
1699
1909
|
const jobs = state.jobs || [];
|
|
1910
|
+
const current = now2();
|
|
1700
1911
|
const lines = jobs.length ? jobs.map((job, index) => {
|
|
1701
|
-
const
|
|
1702
|
-
const intervalMs = Number(job.intervalMs || 0);
|
|
1703
|
-
const lastRunAt = Number(job.lastRunAt || 0);
|
|
1704
|
-
const createdAt = Date.parse(job.createdAt || "");
|
|
1705
|
-
const dueAt = Number(job.runNowRequestedAt || 0) > 0 ? current : lastRunAt > 0 ? lastRunAt + intervalMs : job.immediate === false && Number.isFinite(createdAt) ? createdAt + intervalMs : current;
|
|
1706
|
-
const dueIn = Math.max(0, dueAt - current);
|
|
1912
|
+
const scheduling = describeJobScheduling(job, current);
|
|
1707
1913
|
const flags = [isGoalJob(job) ? `goal:${goalStatusText(job)}` : undefined, job.paused ? "paused" : "active", Number(job.runNowRequestedAt || 0) > 0 ? "run-now" : undefined, job.safe ? "safe" : undefined, job.askNever ? "ask-never" : undefined, job.noOverlap ? "no-overlap" : undefined, job.checkpointOnly ? "checkpoint-only" : undefined, job.gitCheckpoint ? "git-checkpoint" : undefined].filter(Boolean).join(",");
|
|
1708
|
-
return `${index + 1}. ${job.id}${job.name ? ` (${job.name})` : ""}: ${jobLabel(job)} | runs=${job.runCount || 0} | failures=${job.failureCount || 0} |
|
|
1914
|
+
return `${index + 1}. ${job.id}${job.name ? ` (${job.name})` : ""}: ${jobLabel(job)} | schedule=${scheduling.schedule} | state=${scheduling.state} | runs=${job.runCount || 0} | failures=${job.failureCount || 0} | ${flags}`;
|
|
1709
1915
|
}) : ["No active loop jobs."];
|
|
1710
1916
|
await toast2(client, jobs.length ? `${jobs.length} loop job(s).` : "No active loop jobs.", jobs.length ? "info" : "warning");
|
|
1711
1917
|
await say2(client, sessionID, `OpenCode loop status:
|
|
@@ -1715,7 +1921,7 @@ function createLoopCommandHandlers(options = {}) {
|
|
|
1715
1921
|
async function logsLoop(directory, client, sessionID) {
|
|
1716
1922
|
let text = "No loop log found.";
|
|
1717
1923
|
try {
|
|
1718
|
-
text = (await readFile(
|
|
1924
|
+
text = (await readFile(path7.join(stateDir(directory), "loop.log"), "utf8")).trim().split(/\r?\n/).slice(-80).join(`
|
|
1719
1925
|
`) || text;
|
|
1720
1926
|
} catch {}
|
|
1721
1927
|
await say2(client, sessionID, `OpenCode loop logs:
|
|
@@ -1724,18 +1930,20 @@ function createLoopCommandHandlers(options = {}) {
|
|
|
1724
1930
|
async function helpLoop(client, sessionID) {
|
|
1725
1931
|
await say2(client, sessionID, [
|
|
1726
1932
|
"OpenCode Loop help:",
|
|
1727
|
-
"/loop
|
|
1728
|
-
"/loop
|
|
1729
|
-
"/loop
|
|
1730
|
-
"/loop-
|
|
1731
|
-
"/loop
|
|
1732
|
-
"/loop-
|
|
1733
|
-
|
|
1734
|
-
"/loop-
|
|
1735
|
-
"/loop
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
"/loop
|
|
1933
|
+
"/loop continue the project auto-continue forever whenever the session becomes idle",
|
|
1934
|
+
"/loop idle continue the project explicit form of the same idle loop",
|
|
1935
|
+
"/loop every 5m continue the project recurring timer; first run after 5m, always waits for idle",
|
|
1936
|
+
"/loop after 5m continue the project one-shot delayed prompt; runs once when 5m has passed and session is idle",
|
|
1937
|
+
"/loop 5m continue the project legacy compact form: starts on next idle, then every 5m",
|
|
1938
|
+
"/loop 5m --no-now continue the project legacy recurring form with first run delayed 5m",
|
|
1939
|
+
"/loop-command 200m /compact OpenCode slash-command loop, waits for idle",
|
|
1940
|
+
"/loop-ask 1h did you run tests and tsc --noEmit? scheduled question/check prompt",
|
|
1941
|
+
"/loop-shell 10m npm test shell loop, waits for idle",
|
|
1942
|
+
"/loop-goal finish the feature and keep tests green experimental persistent goal mode",
|
|
1943
|
+
'/loop 0s --verify "npm test" <prompt> verify after each assistant turn',
|
|
1944
|
+
"/loop --prompt-file loop-prompt.md idle loop loading its prompt from a file",
|
|
1945
|
+
"/loop 0s --max-runtime 6h --max-failures 3 <task> stop safely after limits",
|
|
1946
|
+
"Prompt-producing /loop jobs are blocked while dedicated /goal owns the same session; use another session or --allow-goal-overlap only intentionally.",
|
|
1739
1947
|
"/loop-doctor | /loop-init | /loop-export"
|
|
1740
1948
|
].join(`
|
|
1741
1949
|
`));
|
|
@@ -1760,22 +1968,36 @@ function createLoopCommandHandlers(options = {}) {
|
|
|
1760
1968
|
}
|
|
1761
1969
|
async function doctorLoop(directory, client, sessionID) {
|
|
1762
1970
|
const state = await readState2(directory, sessionID);
|
|
1763
|
-
await
|
|
1971
|
+
const persisted = await listPersistedLoopSessions2(directory, sessionID);
|
|
1972
|
+
const otherSessions = persisted.filter((entry) => !entry.current && entry.enabled > 0);
|
|
1973
|
+
const dedicatedGoal = await findDedicatedGoalForSession2(directory, sessionID);
|
|
1974
|
+
const lines = [
|
|
1764
1975
|
"OpenCode Loop doctor:",
|
|
1765
1976
|
`- plugin: ${SERVICE2}`,
|
|
1766
1977
|
`- project directory: ${directory}`,
|
|
1767
1978
|
`- state directory: ${stateDir(directory)}`,
|
|
1768
|
-
`-
|
|
1979
|
+
`- current session: ${sessionID}`,
|
|
1980
|
+
`- current-session jobs: ${(state.jobs || []).length}`,
|
|
1981
|
+
`- dedicated /goal: ${dedicatedGoalSummary(dedicatedGoal)}`,
|
|
1982
|
+
`- other persisted sessions with enabled jobs: ${otherSessions.length}`,
|
|
1769
1983
|
`- node: ${runtimeVersion}`,
|
|
1770
1984
|
`- platform: ${runtimePlatform}`,
|
|
1771
|
-
"- smoke test: /loop
|
|
1985
|
+
"- smoke test: /loop --max-runs 1 --dry-run continue from progress.md",
|
|
1986
|
+
"- delayed smoke test: /loop after 5m --dry-run continue from progress.md",
|
|
1987
|
+
"- recurring smoke test: /loop every 5m --dry-run continue from progress.md",
|
|
1772
1988
|
"- experimental goal smoke test: /loop-goal --dry-run finish the current task and verify it"
|
|
1773
|
-
]
|
|
1989
|
+
];
|
|
1990
|
+
for (const entry of otherSessions.slice(0, 8)) {
|
|
1991
|
+
lines.push(`- other session ${entry.sessionID}: jobs=${entry.jobs}, enabled=${entry.enabled}, never-ran=${entry.neverRan}`);
|
|
1992
|
+
}
|
|
1993
|
+
if (otherSessions.length > 8)
|
|
1994
|
+
lines.push(`- ... ${otherSessions.length - 8} more persisted session(s)`);
|
|
1995
|
+
await say2(client, sessionID, lines.join(`
|
|
1774
1996
|
`));
|
|
1775
1997
|
}
|
|
1776
1998
|
async function initLoop(directory, client, sessionID, args) {
|
|
1777
1999
|
const target = String(args || "").trim() || "progress.md";
|
|
1778
|
-
const full =
|
|
2000
|
+
const full = path7.resolve(directory, target);
|
|
1779
2001
|
if (await pathExists2(full)) {
|
|
1780
2002
|
await toast2(client, `${target} already exists.`, "warning");
|
|
1781
2003
|
return;
|
|
@@ -1801,6 +2023,81 @@ function createLoopCommandHandlers(options = {}) {
|
|
|
1801
2023
|
};
|
|
1802
2024
|
}
|
|
1803
2025
|
|
|
2026
|
+
// src/source/core/schedule-syntax.js
|
|
2027
|
+
function removeBooleanFlag(input, flag) {
|
|
2028
|
+
const pattern = new RegExp(`(^|\\s)${flag.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}(?=\\s|$)`, "i");
|
|
2029
|
+
const found = pattern.test(input);
|
|
2030
|
+
return {
|
|
2031
|
+
found,
|
|
2032
|
+
value: String(input || "").replace(pattern, " ").replace(/\s+/g, " ").trim()
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
function firstToken(input) {
|
|
2036
|
+
const match = String(input || "").trim().match(/^(\S+)(?:\s+([\s\S]*))?$/);
|
|
2037
|
+
return match ? { token: match[1], rest: String(match[2] || "").trim() } : { token: "", rest: "" };
|
|
2038
|
+
}
|
|
2039
|
+
function inferredMode(intervalMs, maxRuns) {
|
|
2040
|
+
if (Number(maxRuns || 0) === 1 && Number(intervalMs || 0) > 0)
|
|
2041
|
+
return "once";
|
|
2042
|
+
return Number(intervalMs || 0) === 0 ? "idle" : "interval";
|
|
2043
|
+
}
|
|
2044
|
+
function normalizeLoopScheduleArgs(raw, defaults = {}) {
|
|
2045
|
+
const overlap = removeBooleanFlag(String(raw || "").trim(), "--allow-goal-overlap");
|
|
2046
|
+
let input = overlap.value;
|
|
2047
|
+
const nextDefaults = { ...defaults };
|
|
2048
|
+
let scheduleMode = defaults.scheduleMode;
|
|
2049
|
+
let scheduleSyntax = "legacy";
|
|
2050
|
+
const first = firstToken(input);
|
|
2051
|
+
const keyword = first.token.toLowerCase();
|
|
2052
|
+
if (keyword === "idle") {
|
|
2053
|
+
nextDefaults.intervalMs = 0;
|
|
2054
|
+
nextDefaults.immediate = true;
|
|
2055
|
+
scheduleMode = "idle";
|
|
2056
|
+
scheduleSyntax = "idle";
|
|
2057
|
+
input = first.rest;
|
|
2058
|
+
} else if (keyword === "every" || keyword === "after" || keyword === "in") {
|
|
2059
|
+
const duration = firstToken(first.rest);
|
|
2060
|
+
const intervalMs = parseDuration(duration.token);
|
|
2061
|
+
if (intervalMs === null) {
|
|
2062
|
+
return {
|
|
2063
|
+
ok: false,
|
|
2064
|
+
error: `Invalid ${keyword} schedule. Example: /loop ${keyword === "every" ? "every" : "after"} 5m continue the project`
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
nextDefaults.intervalMs = intervalMs;
|
|
2068
|
+
nextDefaults.immediate = false;
|
|
2069
|
+
input = duration.rest;
|
|
2070
|
+
if (keyword === "every") {
|
|
2071
|
+
scheduleMode = intervalMs === 0 ? "idle" : "interval";
|
|
2072
|
+
scheduleSyntax = "every";
|
|
2073
|
+
} else {
|
|
2074
|
+
nextDefaults.maxRuns = 1;
|
|
2075
|
+
scheduleMode = "once";
|
|
2076
|
+
scheduleSyntax = "after";
|
|
2077
|
+
}
|
|
2078
|
+
} else {
|
|
2079
|
+
const duration = parseDuration(first.token);
|
|
2080
|
+
if (duration !== null) {
|
|
2081
|
+
scheduleMode = duration === 0 ? "idle" : "interval";
|
|
2082
|
+
} else if (nextDefaults.intervalMs === undefined || nextDefaults.intervalMs === null) {
|
|
2083
|
+
nextDefaults.intervalMs = 0;
|
|
2084
|
+
nextDefaults.immediate = nextDefaults.immediate ?? true;
|
|
2085
|
+
scheduleMode = "idle";
|
|
2086
|
+
scheduleSyntax = "idle-shorthand";
|
|
2087
|
+
} else {
|
|
2088
|
+
scheduleMode = scheduleMode || inferredMode(nextDefaults.intervalMs, nextDefaults.maxRuns);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
return {
|
|
2092
|
+
ok: true,
|
|
2093
|
+
args: input,
|
|
2094
|
+
defaults: nextDefaults,
|
|
2095
|
+
scheduleMode: scheduleMode || inferredMode(nextDefaults.intervalMs, nextDefaults.maxRuns),
|
|
2096
|
+
scheduleSyntax,
|
|
2097
|
+
allowGoalOverlap: overlap.found || defaults.allowGoalOverlap === true
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
|
|
1804
2101
|
// src/source/opencode/loop-registration.js
|
|
1805
2102
|
var DEFAULT_GOAL_ACTIVE_RECOVERY_MS = 180000;
|
|
1806
2103
|
var FALLBACK_ACTIVE_GUARD_MS = 45000;
|
|
@@ -1824,19 +2121,34 @@ function createLoopRegistration(options = {}) {
|
|
|
1824
2121
|
const toast2 = requireFunction4(options.toast, "toast");
|
|
1825
2122
|
const say2 = requireFunction4(options.say, "say");
|
|
1826
2123
|
const parseLoopArgs2 = typeof options.parseLoopArgs === "function" ? options.parseLoopArgs : parseLoopArgs;
|
|
2124
|
+
const normalizeLoopScheduleArgs2 = typeof options.normalizeLoopScheduleArgs === "function" ? options.normalizeLoopScheduleArgs : normalizeLoopScheduleArgs;
|
|
1827
2125
|
const readState2 = typeof options.readState === "function" ? options.readState : readState;
|
|
1828
2126
|
const writeState2 = typeof options.writeState === "function" ? options.writeState : writeState;
|
|
1829
2127
|
const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
|
|
1830
2128
|
const normalizedModelRef2 = typeof options.normalizedModelRef === "function" ? options.normalizedModelRef : normalizedModelRef;
|
|
1831
2129
|
const getSessionExecutionContext2 = typeof options.getSessionExecutionContext === "function" ? options.getSessionExecutionContext : getSessionExecutionContext;
|
|
2130
|
+
const findDedicatedGoalForSession2 = typeof options.findDedicatedGoalForSession === "function" ? options.findDedicatedGoalForSession : findDedicatedGoalForSession;
|
|
1832
2131
|
const configuredGuard = Number(options.defaultActiveGuardMs);
|
|
1833
2132
|
const defaultActiveGuardMs = Number.isFinite(configuredGuard) && configuredGuard > 0 ? configuredGuard : FALLBACK_ACTIVE_GUARD_MS;
|
|
1834
2133
|
async function addLoop(directory, client, sessionID, args, defaults = {}) {
|
|
1835
|
-
const
|
|
2134
|
+
const normalized = normalizeLoopScheduleArgs2(args, defaults);
|
|
2135
|
+
if (!normalized.ok) {
|
|
2136
|
+
await toast2(client, normalized.error, "warning");
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
const parsed = parseLoopArgs2(normalized.args, normalized.defaults);
|
|
1836
2140
|
if (!parsed.ok) {
|
|
1837
2141
|
await toast2(client, parsed.error, "warning");
|
|
1838
2142
|
return;
|
|
1839
2143
|
}
|
|
2144
|
+
parsed.job.scheduleMode = normalized.scheduleMode;
|
|
2145
|
+
parsed.job.scheduleSyntax = normalized.scheduleSyntax;
|
|
2146
|
+
parsed.job.allowGoalOverlap = normalized.allowGoalOverlap === true;
|
|
2147
|
+
if (normalized.scheduleSyntax === "after") {
|
|
2148
|
+
parsed.job.immediate = false;
|
|
2149
|
+
parsed.job.maxRuns = 1;
|
|
2150
|
+
parsed.job.lastRunAt = Date.parse(parsed.job.createdAt || "") || Date.now();
|
|
2151
|
+
}
|
|
1840
2152
|
const executionContext = getSessionExecutionContext2(sessionID) || { agent: "build" };
|
|
1841
2153
|
parsed.job.agent = defaults.agent || executionContext.agent || "build";
|
|
1842
2154
|
parsed.job.model = normalizedModelRef2(defaults.model) || executionContext.model;
|
|
@@ -1856,6 +2168,19 @@ function createLoopRegistration(options = {}) {
|
|
|
1856
2168
|
if (!parsed.job.activeRecoveryMs) {
|
|
1857
2169
|
parsed.job.activeRecoveryMs = isGoalJob(parsed.job) ? DEFAULT_GOAL_ACTIVE_RECOVERY_MS : Math.max(defaultActiveGuardMs, Math.min(90000, (parsed.job.intervalMs || 0) + 1e4));
|
|
1858
2170
|
}
|
|
2171
|
+
const promptProducing = actionKind(parsed.job.action, parsed.job) === "prompt";
|
|
2172
|
+
if (promptProducing && !parsed.job.allowGoalOverlap) {
|
|
2173
|
+
const dedicatedGoal = await findDedicatedGoalForSession2(directory, sessionID);
|
|
2174
|
+
if (dedicatedGoalOwnsContinuation(dedicatedGoal)) {
|
|
2175
|
+
await appendLoopLog2(directory, "goal-overlap-blocked", {
|
|
2176
|
+
sessionID,
|
|
2177
|
+
job: parsed.job.name || parsed.job.id,
|
|
2178
|
+
goal: dedicatedGoal.id
|
|
2179
|
+
});
|
|
2180
|
+
await toast2(client, "Prompt loop not added: dedicated /goal already owns continuation in this session. Pause/finish the Goal, use another session, or pass --allow-goal-overlap intentionally.", "warning");
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
1859
2184
|
if (parsed.job.dryRun) {
|
|
1860
2185
|
await toast2(client, `Loop dry run: ${jobLabel(parsed.job)}`, "info");
|
|
1861
2186
|
await say2(client, sessionID, "OpenCode loop dry run:\n```json\n" + JSON.stringify(parsed.job, null, 2) + "\n```");
|
|
@@ -1883,7 +2208,13 @@ function createLoopRegistration(options = {}) {
|
|
|
1883
2208
|
if (parsed.job.immediate)
|
|
1884
2209
|
scheduleIdleWork(directory, client, sessionID);
|
|
1885
2210
|
await toast2(client, `${replaced ? "Loop replaced" : "Loop added"}: ${jobLabel(parsed.job)}`, "success");
|
|
1886
|
-
await appendLoopLog2(directory, replaced ? "replace" : "add", {
|
|
2211
|
+
await appendLoopLog2(directory, replaced ? "replace" : "add", {
|
|
2212
|
+
sessionID,
|
|
2213
|
+
job: parsed.job.name || parsed.job.id,
|
|
2214
|
+
label: jobLabel(parsed.job),
|
|
2215
|
+
scheduleMode: parsed.job.scheduleMode,
|
|
2216
|
+
scheduleSyntax: parsed.job.scheduleSyntax
|
|
2217
|
+
});
|
|
1887
2218
|
}
|
|
1888
2219
|
return { addLoop };
|
|
1889
2220
|
}
|
|
@@ -2000,6 +2331,37 @@ function clearSessionActivity(sessionID) {
|
|
|
2000
2331
|
deleteSessionExecutionContext(sessionID);
|
|
2001
2332
|
}
|
|
2002
2333
|
|
|
2334
|
+
// src/source/runtime/scheduler-diagnostics.js
|
|
2335
|
+
var DEFAULT_DEFERRAL_LOG_THROTTLE_MS = 30000;
|
|
2336
|
+
function createSchedulerDiagnostics(options = {}) {
|
|
2337
|
+
const now2 = typeof options.now === "function" ? options.now : Date.now;
|
|
2338
|
+
const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : async () => {};
|
|
2339
|
+
const configured = Number(options.throttleMs);
|
|
2340
|
+
const throttleMs = Number.isFinite(configured) && configured >= 0 ? configured : DEFAULT_DEFERRAL_LOG_THROTTLE_MS;
|
|
2341
|
+
const lastLogged = new Map;
|
|
2342
|
+
async function logDeferral(directory, sessionID, reason, extra = {}) {
|
|
2343
|
+
const key = `${sessionID || "unknown"}:${reason || "deferred"}:${extra.source || "runtime"}`;
|
|
2344
|
+
const current = now2();
|
|
2345
|
+
const previous = Number(lastLogged.get(key) || 0);
|
|
2346
|
+
if (previous > 0 && current - previous < throttleMs)
|
|
2347
|
+
return false;
|
|
2348
|
+
lastLogged.set(key, current);
|
|
2349
|
+
await appendLoopLog2(directory, "deferred", {
|
|
2350
|
+
sessionID,
|
|
2351
|
+
reason,
|
|
2352
|
+
...extra
|
|
2353
|
+
});
|
|
2354
|
+
return true;
|
|
2355
|
+
}
|
|
2356
|
+
function clearSession(sessionID) {
|
|
2357
|
+
const prefix = `${sessionID || "unknown"}:`;
|
|
2358
|
+
for (const key of lastLogged.keys())
|
|
2359
|
+
if (key.startsWith(prefix))
|
|
2360
|
+
lastLogged.delete(key);
|
|
2361
|
+
}
|
|
2362
|
+
return { logDeferral, clearSession };
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2003
2365
|
// src/source/runtime/scheduler.js
|
|
2004
2366
|
var DEFAULT_IDLE_DEBOUNCE_MS = 1200;
|
|
2005
2367
|
var DEFAULT_BUSY_RETRY_MS = 5000;
|
|
@@ -2007,37 +2369,6 @@ var DEFAULT_MIN_DUE_TIMER_MS = 250;
|
|
|
2007
2369
|
var DEFAULT_MAX_DUE_TIMER_MS = 2147000000;
|
|
2008
2370
|
var DEFAULT_HEARTBEAT_MS = 2500;
|
|
2009
2371
|
var DEFAULT_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
|
|
2010
|
-
function jobDueAt(job, current = now()) {
|
|
2011
|
-
if (isGoalJob(job) && ["completed", "blocked", "cleared"].includes(job.goalStatus))
|
|
2012
|
-
return Infinity;
|
|
2013
|
-
if (!job.enabled || job.paused)
|
|
2014
|
-
return Infinity;
|
|
2015
|
-
if (job.maxRuns > 0 && (job.runCount || 0) >= job.maxRuns)
|
|
2016
|
-
return Infinity;
|
|
2017
|
-
if (Number(job.runNowRequestedAt || 0) > 0)
|
|
2018
|
-
return current;
|
|
2019
|
-
if (job.watchPaths?.length)
|
|
2020
|
-
return Infinity;
|
|
2021
|
-
const created = Date.parse(job.createdAt || "");
|
|
2022
|
-
if (job.maxRuntimeMs > 0 && Number.isFinite(created) && current - created >= job.maxRuntimeMs)
|
|
2023
|
-
return current;
|
|
2024
|
-
if (job.intervalMs === 0)
|
|
2025
|
-
return current;
|
|
2026
|
-
if (!job.lastRunAt) {
|
|
2027
|
-
if (job.immediate === false)
|
|
2028
|
-
return (Number.isFinite(created) ? created : current) + (job.intervalMs || 0);
|
|
2029
|
-
return current;
|
|
2030
|
-
}
|
|
2031
|
-
return job.lastRunAt + (job.intervalMs || 0);
|
|
2032
|
-
}
|
|
2033
|
-
function nextDueDelay(state, current = now()) {
|
|
2034
|
-
let soonest = Infinity;
|
|
2035
|
-
for (const job of state.jobs || [])
|
|
2036
|
-
soonest = Math.min(soonest, jobDueAt(job, current));
|
|
2037
|
-
if (!Number.isFinite(soonest))
|
|
2038
|
-
return Infinity;
|
|
2039
|
-
return Math.max(0, soonest - current);
|
|
2040
|
-
}
|
|
2041
2372
|
function createSchedulerRuntime(options = {}) {
|
|
2042
2373
|
const idleTimers = new Map;
|
|
2043
2374
|
const dueTimers = new Map;
|
|
@@ -2065,6 +2396,7 @@ function createSchedulerRuntime(options = {}) {
|
|
|
2065
2396
|
if (options.toast)
|
|
2066
2397
|
await options.toast(client, message, level);
|
|
2067
2398
|
};
|
|
2399
|
+
const diagnostics = createSchedulerDiagnostics({ appendLoopLog: appendLog, now: clock, throttleMs: options.deferralLogThrottleMs });
|
|
2068
2400
|
function stopHeartbeatIfIdle() {
|
|
2069
2401
|
if (!knownSessions.size && heartbeatTimer) {
|
|
2070
2402
|
clearIntervalFn(heartbeatTimer);
|
|
@@ -2112,11 +2444,13 @@ function createSchedulerRuntime(options = {}) {
|
|
|
2112
2444
|
idleTimers.delete(sessionID);
|
|
2113
2445
|
Promise.resolve().then(async () => {
|
|
2114
2446
|
if (!await options.sessionIsIdle?.(client, sessionID, directory)) {
|
|
2447
|
+
await diagnostics.logDeferral(directory, sessionID, "session-busy", { source: "idle", retryMs: busyRetryMs });
|
|
2115
2448
|
await scheduleDueWork(directory, client, sessionID, busyRetryMs);
|
|
2116
2449
|
return;
|
|
2117
2450
|
}
|
|
2118
2451
|
await options.finalizeActiveRun?.(directory, client, sessionID);
|
|
2119
2452
|
if (!await options.sessionIsIdle?.(client, sessionID, directory)) {
|
|
2453
|
+
await diagnostics.logDeferral(directory, sessionID, "session-busy-after-finalize", { source: "idle", retryMs: busyRetryMs });
|
|
2120
2454
|
await scheduleDueWork(directory, client, sessionID, busyRetryMs);
|
|
2121
2455
|
return;
|
|
2122
2456
|
}
|
|
@@ -2165,11 +2499,13 @@ function createSchedulerRuntime(options = {}) {
|
|
|
2165
2499
|
dueTimers.delete(sessionID);
|
|
2166
2500
|
Promise.resolve().then(async () => {
|
|
2167
2501
|
if (!await options.sessionIsIdle?.(client, sessionID, directory)) {
|
|
2502
|
+
await diagnostics.logDeferral(directory, sessionID, "session-busy", { source: "due", retryMs: busyRetryMs });
|
|
2168
2503
|
await scheduleDueWork(directory, client, sessionID, busyRetryMs);
|
|
2169
2504
|
return;
|
|
2170
2505
|
}
|
|
2171
2506
|
await options.finalizeActiveRun?.(directory, client, sessionID);
|
|
2172
2507
|
if (!await options.sessionIsIdle?.(client, sessionID, directory)) {
|
|
2508
|
+
await diagnostics.logDeferral(directory, sessionID, "session-busy-after-finalize", { source: "due", retryMs: busyRetryMs });
|
|
2173
2509
|
await scheduleDueWork(directory, client, sessionID, busyRetryMs);
|
|
2174
2510
|
return;
|
|
2175
2511
|
}
|
|
@@ -2189,6 +2525,7 @@ function createSchedulerRuntime(options = {}) {
|
|
|
2189
2525
|
cancelIdleWork(sessionID);
|
|
2190
2526
|
cancelDueWork(sessionID);
|
|
2191
2527
|
stopWatchdog(sessionID);
|
|
2528
|
+
diagnostics.clearSession(sessionID);
|
|
2192
2529
|
knownSessions.delete(sessionID);
|
|
2193
2530
|
stopHeartbeatIfIdle();
|
|
2194
2531
|
}
|
|
@@ -2287,8 +2624,8 @@ ${item.output}`).join(`
|
|
|
2287
2624
|
}
|
|
2288
2625
|
|
|
2289
2626
|
// src/source/runtime/job-workspace.js
|
|
2290
|
-
import { promises as
|
|
2291
|
-
import
|
|
2627
|
+
import { promises as fs7 } from "fs";
|
|
2628
|
+
import path8 from "path";
|
|
2292
2629
|
var MAX_SCAN_FILES = 200;
|
|
2293
2630
|
var MAX_SCAN_BYTES = 2000000;
|
|
2294
2631
|
function requireFunction6(value, name) {
|
|
@@ -2323,7 +2660,7 @@ function createJobWorkspaceRuntime(options = {}) {
|
|
|
2323
2660
|
return await buildGoalPrompt2(directory, job);
|
|
2324
2661
|
const sections = [];
|
|
2325
2662
|
if (job.promptFile) {
|
|
2326
|
-
const text = await readSmallTextFile2(
|
|
2663
|
+
const text = await readSmallTextFile2(path8.resolve(directory, job.promptFile));
|
|
2327
2664
|
if (text.trim())
|
|
2328
2665
|
sections.push(`Instructions from ${job.promptFile}:
|
|
2329
2666
|
${text.trim()}`);
|
|
@@ -2333,7 +2670,7 @@ ${text.trim()}`);
|
|
|
2333
2670
|
if (job.action)
|
|
2334
2671
|
sections.push(decoratePrompt(job));
|
|
2335
2672
|
for (const file of job.includeFiles || []) {
|
|
2336
|
-
const text = await readSmallTextFile2(
|
|
2673
|
+
const text = await readSmallTextFile2(path8.resolve(directory, file), 80000);
|
|
2337
2674
|
if (text.trim())
|
|
2338
2675
|
sections.push(`Context from ${file}:
|
|
2339
2676
|
${text.trim().slice(0, 20000)}`);
|
|
@@ -2365,7 +2702,7 @@ ${text.trim().slice(0, 20000)}`);
|
|
|
2365
2702
|
const snapshot = {};
|
|
2366
2703
|
for (const file of files || []) {
|
|
2367
2704
|
try {
|
|
2368
|
-
const stat = await
|
|
2705
|
+
const stat = await fs7.stat(path8.resolve(directory, file));
|
|
2369
2706
|
snapshot[file] = `${stat.mtimeMs}:${stat.size}`;
|
|
2370
2707
|
} catch {
|
|
2371
2708
|
snapshot[file] = "missing";
|
|
@@ -2385,10 +2722,10 @@ ${text.trim().slice(0, 20000)}`);
|
|
|
2385
2722
|
}
|
|
2386
2723
|
async function fileContains(filePath, needle) {
|
|
2387
2724
|
try {
|
|
2388
|
-
const stat = await
|
|
2725
|
+
const stat = await fs7.stat(filePath);
|
|
2389
2726
|
if (!stat.isFile() || stat.size > MAX_SCAN_BYTES)
|
|
2390
2727
|
return false;
|
|
2391
|
-
return (await
|
|
2728
|
+
return (await fs7.readFile(filePath, "utf8")).includes(needle);
|
|
2392
2729
|
} catch {
|
|
2393
2730
|
return false;
|
|
2394
2731
|
}
|
|
@@ -2396,9 +2733,9 @@ ${text.trim().slice(0, 20000)}`);
|
|
|
2396
2733
|
async function untilReached(directory, job) {
|
|
2397
2734
|
if (!job.until)
|
|
2398
2735
|
return false;
|
|
2399
|
-
const files = ["progress.md", "PROGRESS.md", "todo.md", "TODO.md", "todolist.md", "TODOLIST.md",
|
|
2736
|
+
const files = ["progress.md", "PROGRESS.md", "todo.md", "TODO.md", "todolist.md", "TODOLIST.md", path8.join(".opencode", "opencode-loop", "until.txt")];
|
|
2400
2737
|
for (const file of files)
|
|
2401
|
-
if (await fileContains(
|
|
2738
|
+
if (await fileContains(path8.resolve(directory, file), job.until))
|
|
2402
2739
|
return true;
|
|
2403
2740
|
let scanned = 0;
|
|
2404
2741
|
async function walk(current) {
|
|
@@ -2406,7 +2743,7 @@ ${text.trim().slice(0, 20000)}`);
|
|
|
2406
2743
|
return false;
|
|
2407
2744
|
let entries;
|
|
2408
2745
|
try {
|
|
2409
|
-
entries = await
|
|
2746
|
+
entries = await fs7.readdir(current, { withFileTypes: true });
|
|
2410
2747
|
} catch {
|
|
2411
2748
|
return false;
|
|
2412
2749
|
}
|
|
@@ -2415,7 +2752,7 @@ ${text.trim().slice(0, 20000)}`);
|
|
|
2415
2752
|
return false;
|
|
2416
2753
|
if ([".git", "node_modules", "dist", "build", ".next", "coverage"].includes(entry.name))
|
|
2417
2754
|
continue;
|
|
2418
|
-
const full =
|
|
2755
|
+
const full = path8.join(current, entry.name);
|
|
2419
2756
|
if (entry.isDirectory()) {
|
|
2420
2757
|
if (await walk(full))
|
|
2421
2758
|
return true;
|
|
@@ -2439,13 +2776,13 @@ ${text.trim().slice(0, 20000)}`);
|
|
|
2439
2776
|
if (!status.stdout.trim())
|
|
2440
2777
|
return;
|
|
2441
2778
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2442
|
-
const checkpointDir =
|
|
2779
|
+
const checkpointDir = path8.join(stateDir(directory), "checkpoints", safeID(sessionID));
|
|
2443
2780
|
await ensureDir(checkpointDir);
|
|
2444
2781
|
const diff = await runProcess2("git", ["diff", "--binary"], directory, 120000);
|
|
2445
2782
|
const staged = await runProcess2("git", ["diff", "--cached", "--binary"], directory, 120000);
|
|
2446
2783
|
const prefix = `${timestamp}-${safeID(job.name || job.id)}`;
|
|
2447
|
-
await
|
|
2448
|
-
await
|
|
2784
|
+
await fs7.writeFile(path8.join(checkpointDir, `${prefix}.status.txt`), status.stdout + status.stderr);
|
|
2785
|
+
await fs7.writeFile(path8.join(checkpointDir, `${prefix}.patch`), `${diff.stdout}
|
|
2449
2786
|
${staged.stdout}`);
|
|
2450
2787
|
if (job.gitCheckpoint) {
|
|
2451
2788
|
await runProcess2("git", ["add", "-A"], directory, 120000);
|
|
@@ -2463,9 +2800,6 @@ ${staged.stdout}`);
|
|
|
2463
2800
|
};
|
|
2464
2801
|
}
|
|
2465
2802
|
|
|
2466
|
-
// src/source/runtime/executor.js
|
|
2467
|
-
import path6 from "path";
|
|
2468
|
-
|
|
2469
2803
|
// src/source/runtime/session-status.js
|
|
2470
2804
|
var DEFAULT_STALE_ACTIVE_RECOVERY_MS = 45000;
|
|
2471
2805
|
var DEFAULT_SESSION_STATUS_CACHE_MS = 1500;
|
|
@@ -2587,6 +2921,25 @@ function createSessionStatusRuntime(options = {}) {
|
|
|
2587
2921
|
const seenAt = sessionStatusSeenAt.get(sessionID) || 0;
|
|
2588
2922
|
return cached === "idle" && seenAt > (active.startedAt || 0);
|
|
2589
2923
|
}
|
|
2924
|
+
async function recoverCompletedTailWithoutActiveRun(directory, client, sessionID, liveType, seenAt) {
|
|
2925
|
+
if (activeRuns.has(sessionID))
|
|
2926
|
+
return false;
|
|
2927
|
+
if (liveType !== "busy" && liveType !== "retry")
|
|
2928
|
+
return false;
|
|
2929
|
+
if (!seenAt || now2() - seenAt < sessionStatusCacheMs)
|
|
2930
|
+
return false;
|
|
2931
|
+
const completion = await activeRunCompletionFromMessages2(directory, client, sessionID, { startedAt: 0 });
|
|
2932
|
+
if (completion !== "completed")
|
|
2933
|
+
return false;
|
|
2934
|
+
markSessionStatus(sessionID, "idle");
|
|
2935
|
+
await appendLoopLog2(directory, "status-message-idle-recovery", {
|
|
2936
|
+
sessionID,
|
|
2937
|
+
staleStatus: liveType,
|
|
2938
|
+
statusSeenAt: seenAt,
|
|
2939
|
+
staleForMs: Math.max(0, now2() - seenAt)
|
|
2940
|
+
});
|
|
2941
|
+
return true;
|
|
2942
|
+
}
|
|
2590
2943
|
async function sessionStatusType(client, sessionID, directory, options2 = {}) {
|
|
2591
2944
|
if (hasActiveToolCalls(sessionID) || hasBusyDescendant(sessionID)) {
|
|
2592
2945
|
markSessionStatus(sessionID, "busy");
|
|
@@ -2600,6 +2953,8 @@ function createSessionStatusRuntime(options = {}) {
|
|
|
2600
2953
|
return cached;
|
|
2601
2954
|
const live = await readLiveSessionStatus(client, sessionID, directory);
|
|
2602
2955
|
if (live?.type) {
|
|
2956
|
+
if (await recoverCompletedTailWithoutActiveRun(directory, client, sessionID, live.type, seenAt))
|
|
2957
|
+
return "idle";
|
|
2603
2958
|
if ((live.type === "busy" || live.type === "retry") && options2.recoverStaleActive !== false) {
|
|
2604
2959
|
const active = activeRuns.get(sessionID);
|
|
2605
2960
|
if (active) {
|
|
@@ -2751,166 +3106,101 @@ function createCompactionRuntime(options = {}) {
|
|
|
2751
3106
|
};
|
|
2752
3107
|
}
|
|
2753
3108
|
|
|
2754
|
-
// src/source/runtime/
|
|
2755
|
-
var DEFAULT_ACTIVE_GUARD_MS = 45000;
|
|
2756
|
-
var DEFAULT_BUSY_RETRY_MS2 = 5000;
|
|
3109
|
+
// src/source/runtime/action-dispatch.js
|
|
2757
3110
|
function requireFunction7(value, label) {
|
|
2758
3111
|
if (typeof value !== "function")
|
|
2759
|
-
throw new TypeError(`
|
|
3112
|
+
throw new TypeError(`createActionDispatcher requires ${label}`);
|
|
2760
3113
|
return value;
|
|
2761
3114
|
}
|
|
2762
|
-
function
|
|
2763
|
-
const
|
|
2764
|
-
const
|
|
2765
|
-
const scheduler = options.scheduler || {};
|
|
2766
|
-
const buildPrompt = requireFunction7(workspace.buildPrompt, "workspace.buildPrompt");
|
|
2767
|
-
const ensureBranch = requireFunction7(workspace.ensureBranch, "workspace.ensureBranch");
|
|
2768
|
-
const watchChanged = requireFunction7(workspace.watchChanged, "workspace.watchChanged");
|
|
2769
|
-
const untilReached = requireFunction7(workspace.untilReached, "workspace.untilReached");
|
|
2770
|
-
const createCheckpoint = requireFunction7(workspace.createCheckpoint, "workspace.createCheckpoint");
|
|
2771
|
-
const runGoalChecks = requireFunction7(goalPolicy.runGoalChecks, "goalPolicy.runGoalChecks");
|
|
2772
|
-
const applyGoalNoProgressGuard = requireFunction7(goalPolicy.applyGoalNoProgressGuard, "goalPolicy.applyGoalNoProgressGuard");
|
|
2773
|
-
const rememberSession = requireFunction7(scheduler.rememberSession, "scheduler.rememberSession");
|
|
2774
|
-
const scheduleDueWork = requireFunction7(scheduler.scheduleDueWork, "scheduler.scheduleDueWork");
|
|
2775
|
-
const now2 = typeof options.now === "function" ? options.now : now;
|
|
2776
|
-
const readState2 = typeof options.readState === "function" ? options.readState : readState;
|
|
2777
|
-
const writeState2 = typeof options.writeState === "function" ? options.writeState : writeState;
|
|
2778
|
-
const pathExists2 = typeof options.pathExists === "function" ? options.pathExists : pathExists;
|
|
3115
|
+
function createActionDispatcher(options = {}) {
|
|
3116
|
+
const buildPrompt = requireFunction7(options.buildPrompt, "buildPrompt");
|
|
3117
|
+
const startCompaction = requireFunction7(options.compactionRuntime?.start, "compactionRuntime.start");
|
|
2779
3118
|
const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
|
|
2780
|
-
const runShellCommand2 = typeof options.runShellCommand === "function" ? options.runShellCommand : runShellCommand;
|
|
2781
|
-
const notifyJob2 = typeof options.notifyJob === "function" ? options.notifyJob : notifyJob;
|
|
2782
|
-
const errorMessage = typeof options.errorMessage === "function" ? options.errorMessage : sdkErrorMessage;
|
|
2783
3119
|
const sdkCall2 = typeof options.sdkCall === "function" ? options.sdkCall : sdkCall;
|
|
2784
3120
|
const normalizedModelRef2 = typeof options.normalizedModelRef === "function" ? options.normalizedModelRef : normalizedModelRef;
|
|
2785
3121
|
const fireSdk2 = typeof options.fireSdk === "function" ? options.fireSdk : fireSdk;
|
|
2786
3122
|
const compactTuiCommandName2 = typeof options.compactTuiCommandName === "function" ? options.compactTuiCommandName : compactTuiCommandName;
|
|
2787
|
-
const log2 = typeof options.log === "function" ? options.log : log;
|
|
2788
3123
|
const toast2 = typeof options.toast === "function" ? options.toast : toast;
|
|
2789
3124
|
const guardLoopOwnedUserMessage2 = typeof options.guardLoopOwnedUserMessage === "function" ? options.guardLoopOwnedUserMessage : guardLoopOwnedUserMessage;
|
|
2790
|
-
const writeGoalReport2 = typeof options.writeGoalReport === "function" ? options.writeGoalReport : writeGoalReport;
|
|
2791
3125
|
const dangerousShell2 = typeof options.dangerousShell === "function" ? options.dangerousShell : dangerousShell;
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
activeRunCompletionFromMessages: options.activeRunCompletionFromMessages,
|
|
2801
|
-
staleActiveRecoveryMs: options.staleActiveRecoveryMs,
|
|
2802
|
-
sessionStatusCacheMs: options.sessionStatusCacheMs
|
|
2803
|
-
});
|
|
2804
|
-
const {
|
|
2805
|
-
updateSessionStatusFromEvent,
|
|
2806
|
-
staleActiveRun,
|
|
2807
|
-
canFinalizeActiveRun,
|
|
2808
|
-
sessionIsIdle,
|
|
2809
|
-
markSessionStatus,
|
|
2810
|
-
clearSessionStatus
|
|
2811
|
-
} = statusRuntime;
|
|
2812
|
-
const compactionRuntime = createCompactionRuntime({
|
|
2813
|
-
activeRuns,
|
|
2814
|
-
finalizeActiveRun,
|
|
2815
|
-
appendLoopLog: appendLoopLog2,
|
|
2816
|
-
compactSession: options.compactSession,
|
|
2817
|
-
log: log2,
|
|
2818
|
-
errorMessage,
|
|
2819
|
-
now: now2
|
|
2820
|
-
});
|
|
2821
|
-
function dueJobs(state, force = false) {
|
|
2822
|
-
const current = now2();
|
|
2823
|
-
const due = (state.jobs || []).filter((job) => {
|
|
2824
|
-
if (isGoalJob(job) && ["completed", "blocked", "cleared"].includes(job.goalStatus))
|
|
2825
|
-
return false;
|
|
2826
|
-
if (!job.enabled || job.paused)
|
|
2827
|
-
return false;
|
|
2828
|
-
if (job.maxRuns > 0 && (job.runCount || 0) >= job.maxRuns)
|
|
2829
|
-
return false;
|
|
2830
|
-
if (job.maxRuntimeMs > 0 && current - Date.parse(job.createdAt || new Date().toISOString()) >= job.maxRuntimeMs)
|
|
2831
|
-
return true;
|
|
2832
|
-
if (Number(job.runNowRequestedAt || 0) > 0)
|
|
2833
|
-
return true;
|
|
2834
|
-
if (force)
|
|
2835
|
-
return true;
|
|
2836
|
-
if (job.watchPaths?.length)
|
|
2837
|
-
return job.watchTriggered === true;
|
|
2838
|
-
return job.intervalMs === 0 || !job.lastRunAt || current - job.lastRunAt >= job.intervalMs;
|
|
2839
|
-
});
|
|
2840
|
-
return due.sort((a, b) => Number(Number(b.runNowRequestedAt || 0) > 0) - Number(Number(a.runNowRequestedAt || 0) > 0));
|
|
2841
|
-
}
|
|
2842
|
-
function clearActiveRun(sessionID) {
|
|
2843
|
-
const active = activeRuns.get(sessionID);
|
|
2844
|
-
if (active?.timer)
|
|
2845
|
-
clearTimeout(active.timer);
|
|
2846
|
-
compactionRuntime.clearForActiveRun(sessionID, active);
|
|
2847
|
-
activeRuns.delete(sessionID);
|
|
2848
|
-
}
|
|
2849
|
-
function disposeSession(sessionID) {
|
|
2850
|
-
clearActiveRun(sessionID);
|
|
2851
|
-
runLocks.delete(sessionID);
|
|
2852
|
-
compactionRuntime.clear(sessionID);
|
|
2853
|
-
clearSessionStatus(sessionID);
|
|
2854
|
-
}
|
|
2855
|
-
async function recoverActiveDispatchFailure(directory, client, sessionID, jobId, runToken, error) {
|
|
2856
|
-
const active = activeRuns.get(sessionID);
|
|
2857
|
-
if (!active || active.jobId !== jobId || active.runToken !== runToken)
|
|
2858
|
-
return false;
|
|
2859
|
-
clearActiveRun(sessionID);
|
|
2860
|
-
clearSessionStatus(sessionID);
|
|
2861
|
-
const message = errorMessage(error);
|
|
2862
|
-
const state = await readState2(directory, sessionID);
|
|
2863
|
-
const job = (state.jobs || []).find((candidate) => candidate.id === jobId);
|
|
2864
|
-
if (job) {
|
|
2865
|
-
job.failureCount = (job.failureCount || 0) + 1;
|
|
2866
|
-
job.lastFailureReason = "dispatch_failed";
|
|
2867
|
-
job.lastDispatchFailure = message.slice(0, 4000);
|
|
2868
|
-
job.lastDispatchFailureAt = now2();
|
|
2869
|
-
if (job.maxFailures > 0 && job.failureCount >= job.maxFailures) {
|
|
2870
|
-
job.paused = true;
|
|
2871
|
-
await notifyJob2(directory, job, "dispatch_failed");
|
|
2872
|
-
}
|
|
2873
|
-
state.jobs = (state.jobs || []).map((candidate) => candidate.id === job.id ? job : candidate);
|
|
2874
|
-
await writeState2(directory, sessionID, state);
|
|
3126
|
+
async function fireAction(directory, client, sessionID, job) {
|
|
3127
|
+
const action = String(job.action || "").trim();
|
|
3128
|
+
const kind = actionKind(action, job);
|
|
3129
|
+
const agent = job.agent || "build";
|
|
3130
|
+
const model = normalizedModelRef2(job.model);
|
|
3131
|
+
if (kind === "compact") {
|
|
3132
|
+
const ok = await startCompaction(directory, client, sessionID, job.id, model, false);
|
|
3133
|
+
return { startsAssistantTurn: ok, pause: !ok, reason: "compact_failed", compaction: ok };
|
|
2875
3134
|
}
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
startedAt: active.startedAt,
|
|
2896
|
-
nativeEvent: Boolean(pending?.completedAt)
|
|
2897
|
-
});
|
|
2898
|
-
await scheduleDueWork(directory, client, sessionID);
|
|
2899
|
-
return true;
|
|
3135
|
+
if (kind === "command") {
|
|
3136
|
+
const normalized = action.startsWith("/") ? action.slice(1) : action;
|
|
3137
|
+
const [command, argumentsText] = splitFirst(normalized);
|
|
3138
|
+
if (!command) {
|
|
3139
|
+
await toast2(client, "Loop command action is empty. Example: /loop-command 200m /compact", "warning");
|
|
3140
|
+
return { startsAssistantTurn: false, pause: true, reason: "empty_command" };
|
|
3141
|
+
}
|
|
3142
|
+
const tuiCommand = compactTuiCommandName2(command);
|
|
3143
|
+
if (tuiCommand) {
|
|
3144
|
+
guardLoopOwnedUserMessage2(sessionID);
|
|
3145
|
+
const ok = await startCompaction(directory, client, sessionID, job.id, model, false);
|
|
3146
|
+
return { startsAssistantTurn: ok, pause: !ok, reason: "compact_failed", compaction: ok };
|
|
3147
|
+
}
|
|
3148
|
+
guardLoopOwnedUserMessage2(sessionID);
|
|
3149
|
+
const commandBody = { command, arguments: argumentsText, agent };
|
|
3150
|
+
if (model)
|
|
3151
|
+
commandBody.model = `${model.providerID}/${model.modelID}`;
|
|
3152
|
+
await sdkCall2(client.session.command.bind(client.session), { path: { id: sessionID }, body: commandBody }, { path: { sessionID }, body: commandBody }, { sessionID, ...commandBody });
|
|
3153
|
+
return { startsAssistantTurn: true };
|
|
2900
3154
|
}
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
});
|
|
3155
|
+
if (kind === "shell") {
|
|
3156
|
+
const command = action.replace(/^[!$]\s*/, "").trim();
|
|
3157
|
+
if (job.safe && dangerousShell2(command)) {
|
|
3158
|
+
await toast2(client, `Blocked dangerous shell command in safe mode: ${command}`, "error");
|
|
3159
|
+
await appendLoopLog2(directory, "blocked", { sessionID, job: job.name || job.id, command });
|
|
3160
|
+
return { startsAssistantTurn: false, pause: true, reason: "safe_shell_blocked" };
|
|
3161
|
+
}
|
|
3162
|
+
guardLoopOwnedUserMessage2(sessionID);
|
|
3163
|
+
const shellBody = { command, agent };
|
|
3164
|
+
if (model)
|
|
3165
|
+
shellBody.model = model;
|
|
3166
|
+
const dispatch2 = fireSdk2(client, "session.shell", client.session.shell.bind(client.session), { path: { id: sessionID }, body: shellBody }, { path: { sessionID }, body: shellBody }, { sessionID, ...shellBody });
|
|
3167
|
+
return { startsAssistantTurn: true, dispatch: dispatch2 };
|
|
2913
3168
|
}
|
|
3169
|
+
const prompt = await buildPrompt(directory, job);
|
|
3170
|
+
const prefix = kind === "goal" ? "EXPERIMENTAL GOAL MODE CONTINUATION. Continue pursuing the active goal. Do not explain the /loop-goal command. Use the goal tools only when progress/completion/block state is real." : "AUTONOMOUS OPENCODE LOOP ITERATION. Continue the configured task now. Do not explain the /loop command. Do not search for documentation about this plugin. Do not create scheduler files. Do not ask questions. Make reasonable assumptions and work directly.";
|
|
3171
|
+
const promptText = `${prefix}
|
|
3172
|
+
|
|
3173
|
+
${prompt}`;
|
|
3174
|
+
guardLoopOwnedUserMessage2(sessionID);
|
|
3175
|
+
const promptBody = { agent, parts: [{ type: "text", text: promptText }] };
|
|
3176
|
+
if (model)
|
|
3177
|
+
promptBody.model = model;
|
|
3178
|
+
const dispatch = fireSdk2(client, "session.prompt", client.session.prompt.bind(client.session), { path: { id: sessionID }, body: promptBody }, { path: { sessionID }, body: promptBody }, { sessionID, ...promptBody });
|
|
3179
|
+
return { startsAssistantTurn: true, dispatch };
|
|
3180
|
+
}
|
|
3181
|
+
return { fireAction };
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
// src/source/runtime/run-finalization.js
|
|
3185
|
+
function requireFunction8(value, label) {
|
|
3186
|
+
if (typeof value !== "function")
|
|
3187
|
+
throw new TypeError(`createRunFinalizationRuntime requires ${label}`);
|
|
3188
|
+
return value;
|
|
3189
|
+
}
|
|
3190
|
+
function createRunFinalizationRuntime(options = {}) {
|
|
3191
|
+
const runGoalChecks = requireFunction8(options.runGoalChecks, "runGoalChecks");
|
|
3192
|
+
const applyGoalNoProgressGuard = requireFunction8(options.applyGoalNoProgressGuard, "applyGoalNoProgressGuard");
|
|
3193
|
+
const createCheckpoint = requireFunction8(options.createCheckpoint, "createCheckpoint");
|
|
3194
|
+
const scheduleDueWork = requireFunction8(options.scheduleDueWork, "scheduleDueWork");
|
|
3195
|
+
const now2 = typeof options.now === "function" ? options.now : now;
|
|
3196
|
+
const writeState2 = typeof options.writeState === "function" ? options.writeState : writeState;
|
|
3197
|
+
const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
|
|
3198
|
+
const runShellCommand2 = typeof options.runShellCommand === "function" ? options.runShellCommand : runShellCommand;
|
|
3199
|
+
const notifyJob2 = typeof options.notifyJob === "function" ? options.notifyJob : notifyJob;
|
|
3200
|
+
const toast2 = typeof options.toast === "function" ? options.toast : toast;
|
|
3201
|
+
const writeGoalReport2 = typeof options.writeGoalReport === "function" ? options.writeGoalReport : writeGoalReport;
|
|
3202
|
+
const dangerousShell2 = typeof options.dangerousShell === "function" ? options.dangerousShell : dangerousShell;
|
|
3203
|
+
async function finalizeJob(directory, client, sessionID, state, job, previousJob) {
|
|
2914
3204
|
if (job.verifyCommand) {
|
|
2915
3205
|
const verify = await runShellCommand2(job.verifyCommand, directory, job.timeoutMs || 300000);
|
|
2916
3206
|
job.lastVerifyAt = now2();
|
|
@@ -2971,7 +3261,7 @@ exit=` + postrun.code + `
|
|
|
2971
3261
|
}
|
|
2972
3262
|
if (isGoalJob(job)) {
|
|
2973
3263
|
job = await runGoalChecks(directory, sessionID, job, client);
|
|
2974
|
-
job = await applyGoalNoProgressGuard(directory, client, sessionID, job,
|
|
3264
|
+
job = await applyGoalNoProgressGuard(directory, client, sessionID, job, previousJob);
|
|
2975
3265
|
}
|
|
2976
3266
|
state.jobs = (state.jobs || []).map((candidate) => candidate.id === job.id ? job : candidate).filter((candidate) => candidate.enabled !== false || isGoalJob(candidate));
|
|
2977
3267
|
await writeState2(directory, sessionID, state);
|
|
@@ -2979,63 +3269,272 @@ exit=` + postrun.code + `
|
|
|
2979
3269
|
await writeGoalReport2(directory, sessionID, job);
|
|
2980
3270
|
await createCheckpoint(directory, sessionID, job, client);
|
|
2981
3271
|
await scheduleDueWork(directory, client, sessionID);
|
|
2982
|
-
return
|
|
3272
|
+
return job;
|
|
2983
3273
|
}
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
3274
|
+
return { finalizeJob };
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
// src/source/runtime/run-admission.js
|
|
3278
|
+
import path9 from "path";
|
|
3279
|
+
function requireFunction9(value, label) {
|
|
3280
|
+
if (typeof value !== "function")
|
|
3281
|
+
throw new TypeError(`createRunAdmissionRuntime requires ${label}`);
|
|
3282
|
+
return value;
|
|
3283
|
+
}
|
|
3284
|
+
function createRunAdmissionRuntime(options = {}) {
|
|
3285
|
+
const untilReached = requireFunction9(options.untilReached, "untilReached");
|
|
3286
|
+
const scheduleDueWork = requireFunction9(options.scheduleDueWork, "scheduleDueWork");
|
|
3287
|
+
const now2 = typeof options.now === "function" ? options.now : now;
|
|
3288
|
+
const pathExists2 = typeof options.pathExists === "function" ? options.pathExists : pathExists;
|
|
3289
|
+
const writeState2 = typeof options.writeState === "function" ? options.writeState : writeState;
|
|
3290
|
+
const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
|
|
3291
|
+
const runShellCommand2 = typeof options.runShellCommand === "function" ? options.runShellCommand : runShellCommand;
|
|
3292
|
+
const notifyJob2 = typeof options.notifyJob === "function" ? options.notifyJob : notifyJob;
|
|
3293
|
+
const toast2 = typeof options.toast === "function" ? options.toast : toast;
|
|
3294
|
+
const dangerousShell2 = typeof options.dangerousShell === "function" ? options.dangerousShell : dangerousShell;
|
|
3295
|
+
function dueJobs2(state, force = false) {
|
|
3296
|
+
return dueJobs(state, now2(), force);
|
|
3297
|
+
}
|
|
3298
|
+
async function reschedule(directory, client, sessionID) {
|
|
3299
|
+
await scheduleDueWork(directory, client, sessionID);
|
|
3300
|
+
}
|
|
3301
|
+
async function stopAndRemove(directory, client, sessionID, state, job, reason, message, logEvent) {
|
|
3302
|
+
state.jobs = (state.jobs || []).filter((candidate) => candidate.id !== job.id);
|
|
3303
|
+
await writeState2(directory, sessionID, state);
|
|
3304
|
+
await notifyJob2(directory, job, reason);
|
|
3305
|
+
await toast2(client, message, "success");
|
|
3306
|
+
if (logEvent)
|
|
3307
|
+
await appendLoopLog2(directory, logEvent, { sessionID, job: job.name || job.id });
|
|
3308
|
+
await reschedule(directory, client, sessionID);
|
|
3309
|
+
return { admitted: false, reason };
|
|
3310
|
+
}
|
|
3311
|
+
async function admitJob(directory, client, sessionID, state, job) {
|
|
3312
|
+
const runNowRequested = Number(job.runNowRequestedAt || 0) > 0;
|
|
3313
|
+
if (job.maxRuntimeMs > 0 && now2() - Date.parse(job.createdAt || new Date().toISOString()) >= job.maxRuntimeMs) {
|
|
3314
|
+
return await stopAndRemove(directory, client, sessionID, state, job, "max_runtime_reached", `Loop stopped by --max-runtime: ${job.name || job.id}`, "max-runtime");
|
|
2992
3315
|
}
|
|
2993
|
-
if (
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
3316
|
+
if (job.stopFile && await pathExists2(path9.resolve(directory, job.stopFile))) {
|
|
3317
|
+
return await stopAndRemove(directory, client, sessionID, state, job, "stop_file", "Loop stopped by --stop-file: " + job.stopFile);
|
|
3318
|
+
}
|
|
3319
|
+
if (await untilReached(directory, job)) {
|
|
3320
|
+
return await stopAndRemove(directory, client, sessionID, state, job, "until_reached", `Loop stopped by --until: ${job.until}`);
|
|
3321
|
+
}
|
|
3322
|
+
if (job.preflightCommand) {
|
|
3323
|
+
if (job.safe && dangerousShell2(job.preflightCommand)) {
|
|
3324
|
+
if (runNowRequested)
|
|
3325
|
+
delete job.runNowRequestedAt;
|
|
3326
|
+
job.paused = true;
|
|
3327
|
+
await writeState2(directory, sessionID, state);
|
|
3328
|
+
await notifyJob2(directory, job, "preflight_blocked");
|
|
3329
|
+
await toast2(client, "Preflight blocked in safe mode and loop paused: " + job.preflightCommand, "error");
|
|
3330
|
+
await reschedule(directory, client, sessionID);
|
|
3331
|
+
return { admitted: false, reason: "preflight_blocked" };
|
|
2999
3332
|
}
|
|
3000
|
-
const
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3333
|
+
const preflight = await runShellCommand2(job.preflightCommand, directory, job.timeoutMs || 300000);
|
|
3334
|
+
await appendLoopLog2(directory, "preflight", {
|
|
3335
|
+
sessionID,
|
|
3336
|
+
job: job.name || job.id,
|
|
3337
|
+
command: job.preflightCommand,
|
|
3338
|
+
code: preflight.code
|
|
3339
|
+
});
|
|
3340
|
+
if (preflight.code !== 0) {
|
|
3341
|
+
if (runNowRequested)
|
|
3342
|
+
delete job.runNowRequestedAt;
|
|
3343
|
+
job.paused = true;
|
|
3344
|
+
job.failureCount = (job.failureCount || 0) + 1;
|
|
3345
|
+
job.lastPreflightFailure = (job.preflightCommand + `
|
|
3346
|
+
exit=` + preflight.code + `
|
|
3347
|
+
` + preflight.stdout + `
|
|
3348
|
+
` + preflight.stderr).slice(0, 4000);
|
|
3349
|
+
state.jobs = (state.jobs || []).map((candidate) => candidate.id === job.id ? job : candidate);
|
|
3350
|
+
await writeState2(directory, sessionID, state);
|
|
3351
|
+
await notifyJob2(directory, job, "preflight_failed");
|
|
3352
|
+
await toast2(client, "Preflight failed and loop paused: " + job.preflightCommand, "warning");
|
|
3353
|
+
await reschedule(directory, client, sessionID);
|
|
3354
|
+
return { admitted: false, reason: "preflight_failed" };
|
|
3005
3355
|
}
|
|
3006
|
-
guardLoopOwnedUserMessage2(sessionID);
|
|
3007
|
-
const commandBody = { command, arguments: argumentsText, agent };
|
|
3008
|
-
if (model)
|
|
3009
|
-
commandBody.model = `${model.providerID}/${model.modelID}`;
|
|
3010
|
-
await sdkCall2(client.session.command.bind(client.session), { path: { id: sessionID }, body: commandBody }, { path: { sessionID }, body: commandBody }, { sessionID, ...commandBody });
|
|
3011
|
-
return { startsAssistantTurn: true };
|
|
3012
3356
|
}
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3357
|
+
return { admitted: true, job, runNowRequested };
|
|
3358
|
+
}
|
|
3359
|
+
return { dueJobs: dueJobs2, admitJob };
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
// src/source/runtime/executor.js
|
|
3363
|
+
var DEFAULT_ACTIVE_GUARD_MS = 45000;
|
|
3364
|
+
var DEFAULT_BUSY_RETRY_MS2 = 5000;
|
|
3365
|
+
function requireFunction10(value, label) {
|
|
3366
|
+
if (typeof value !== "function")
|
|
3367
|
+
throw new TypeError(`createLoopExecutor requires ${label}`);
|
|
3368
|
+
return value;
|
|
3369
|
+
}
|
|
3370
|
+
function createLoopExecutor(options = {}) {
|
|
3371
|
+
const workspace = options.workspace || {};
|
|
3372
|
+
const goalPolicy = options.goalPolicy || {};
|
|
3373
|
+
const scheduler = options.scheduler || {};
|
|
3374
|
+
const buildPrompt = requireFunction10(workspace.buildPrompt, "workspace.buildPrompt");
|
|
3375
|
+
const ensureBranch = requireFunction10(workspace.ensureBranch, "workspace.ensureBranch");
|
|
3376
|
+
const watchChanged = requireFunction10(workspace.watchChanged, "workspace.watchChanged");
|
|
3377
|
+
const untilReached = requireFunction10(workspace.untilReached, "workspace.untilReached");
|
|
3378
|
+
const createCheckpoint = requireFunction10(workspace.createCheckpoint, "workspace.createCheckpoint");
|
|
3379
|
+
const runGoalChecks = requireFunction10(goalPolicy.runGoalChecks, "goalPolicy.runGoalChecks");
|
|
3380
|
+
const applyGoalNoProgressGuard = requireFunction10(goalPolicy.applyGoalNoProgressGuard, "goalPolicy.applyGoalNoProgressGuard");
|
|
3381
|
+
const rememberSession = requireFunction10(scheduler.rememberSession, "scheduler.rememberSession");
|
|
3382
|
+
const scheduleDueWork = requireFunction10(scheduler.scheduleDueWork, "scheduler.scheduleDueWork");
|
|
3383
|
+
const now2 = typeof options.now === "function" ? options.now : now;
|
|
3384
|
+
const readState2 = typeof options.readState === "function" ? options.readState : readState;
|
|
3385
|
+
const writeState2 = typeof options.writeState === "function" ? options.writeState : writeState;
|
|
3386
|
+
const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
|
|
3387
|
+
const runShellCommand2 = typeof options.runShellCommand === "function" ? options.runShellCommand : runShellCommand;
|
|
3388
|
+
const notifyJob2 = typeof options.notifyJob === "function" ? options.notifyJob : notifyJob;
|
|
3389
|
+
const errorMessage = typeof options.errorMessage === "function" ? options.errorMessage : sdkErrorMessage;
|
|
3390
|
+
const fireSdk2 = typeof options.fireSdk === "function" ? options.fireSdk : fireSdk;
|
|
3391
|
+
const log2 = typeof options.log === "function" ? options.log : log;
|
|
3392
|
+
const toast2 = typeof options.toast === "function" ? options.toast : toast;
|
|
3393
|
+
const dangerousShell2 = typeof options.dangerousShell === "function" ? options.dangerousShell : dangerousShell;
|
|
3394
|
+
const activeGuardMs = Number.isFinite(Number(options.activeGuardMs)) && Number(options.activeGuardMs) > 0 ? Number(options.activeGuardMs) : DEFAULT_ACTIVE_GUARD_MS;
|
|
3395
|
+
const busyRetryMs = Number.isFinite(Number(options.busyRetryMs)) && Number(options.busyRetryMs) > 0 ? Number(options.busyRetryMs) : DEFAULT_BUSY_RETRY_MS2;
|
|
3396
|
+
const activeRuns = new Map;
|
|
3397
|
+
const runLocks = new Map;
|
|
3398
|
+
const statusRuntime = createSessionStatusRuntime({
|
|
3399
|
+
activeRuns,
|
|
3400
|
+
appendLoopLog: appendLoopLog2,
|
|
3401
|
+
now: now2,
|
|
3402
|
+
activeRunCompletionFromMessages: options.activeRunCompletionFromMessages,
|
|
3403
|
+
staleActiveRecoveryMs: options.staleActiveRecoveryMs,
|
|
3404
|
+
sessionStatusCacheMs: options.sessionStatusCacheMs
|
|
3405
|
+
});
|
|
3406
|
+
const {
|
|
3407
|
+
updateSessionStatusFromEvent,
|
|
3408
|
+
staleActiveRun,
|
|
3409
|
+
canFinalizeActiveRun,
|
|
3410
|
+
sessionIsIdle,
|
|
3411
|
+
markSessionStatus,
|
|
3412
|
+
clearSessionStatus
|
|
3413
|
+
} = statusRuntime;
|
|
3414
|
+
const compactionRuntime = createCompactionRuntime({
|
|
3415
|
+
activeRuns,
|
|
3416
|
+
finalizeActiveRun,
|
|
3417
|
+
appendLoopLog: appendLoopLog2,
|
|
3418
|
+
compactSession: options.compactSession,
|
|
3419
|
+
log: log2,
|
|
3420
|
+
errorMessage,
|
|
3421
|
+
now: now2
|
|
3422
|
+
});
|
|
3423
|
+
const actionDispatcher = createActionDispatcher({
|
|
3424
|
+
buildPrompt,
|
|
3425
|
+
compactionRuntime,
|
|
3426
|
+
appendLoopLog: appendLoopLog2,
|
|
3427
|
+
sdkCall: options.sdkCall,
|
|
3428
|
+
normalizedModelRef: options.normalizedModelRef,
|
|
3429
|
+
fireSdk: options.fireSdk,
|
|
3430
|
+
compactTuiCommandName: options.compactTuiCommandName,
|
|
3431
|
+
toast: toast2,
|
|
3432
|
+
guardLoopOwnedUserMessage: options.guardLoopOwnedUserMessage,
|
|
3433
|
+
dangerousShell: dangerousShell2
|
|
3434
|
+
});
|
|
3435
|
+
const finalizationRuntime = createRunFinalizationRuntime({
|
|
3436
|
+
runGoalChecks,
|
|
3437
|
+
applyGoalNoProgressGuard,
|
|
3438
|
+
createCheckpoint,
|
|
3439
|
+
scheduleDueWork,
|
|
3440
|
+
now: now2,
|
|
3441
|
+
writeState: writeState2,
|
|
3442
|
+
appendLoopLog: appendLoopLog2,
|
|
3443
|
+
runShellCommand: runShellCommand2,
|
|
3444
|
+
notifyJob: notifyJob2,
|
|
3445
|
+
toast: toast2,
|
|
3446
|
+
writeGoalReport: options.writeGoalReport,
|
|
3447
|
+
dangerousShell: dangerousShell2
|
|
3448
|
+
});
|
|
3449
|
+
const admissionRuntime = createRunAdmissionRuntime({
|
|
3450
|
+
untilReached,
|
|
3451
|
+
scheduleDueWork,
|
|
3452
|
+
now: now2,
|
|
3453
|
+
pathExists: options.pathExists,
|
|
3454
|
+
writeState: writeState2,
|
|
3455
|
+
appendLoopLog: appendLoopLog2,
|
|
3456
|
+
runShellCommand: runShellCommand2,
|
|
3457
|
+
notifyJob: notifyJob2,
|
|
3458
|
+
toast: toast2,
|
|
3459
|
+
dangerousShell: dangerousShell2
|
|
3460
|
+
});
|
|
3461
|
+
const dueJobs2 = admissionRuntime.dueJobs;
|
|
3462
|
+
function clearActiveRun(sessionID) {
|
|
3463
|
+
const active = activeRuns.get(sessionID);
|
|
3464
|
+
if (active?.timer)
|
|
3465
|
+
clearTimeout(active.timer);
|
|
3466
|
+
compactionRuntime.clearForActiveRun(sessionID, active);
|
|
3467
|
+
activeRuns.delete(sessionID);
|
|
3468
|
+
}
|
|
3469
|
+
function disposeSession(sessionID) {
|
|
3470
|
+
clearActiveRun(sessionID);
|
|
3471
|
+
runLocks.delete(sessionID);
|
|
3472
|
+
compactionRuntime.clear(sessionID);
|
|
3473
|
+
clearSessionStatus(sessionID);
|
|
3474
|
+
}
|
|
3475
|
+
async function recoverActiveDispatchFailure(directory, client, sessionID, jobId, runToken, error) {
|
|
3476
|
+
const active = activeRuns.get(sessionID);
|
|
3477
|
+
if (!active || active.jobId !== jobId || active.runToken !== runToken)
|
|
3478
|
+
return false;
|
|
3479
|
+
clearActiveRun(sessionID);
|
|
3480
|
+
clearSessionStatus(sessionID);
|
|
3481
|
+
const message = errorMessage(error);
|
|
3482
|
+
const state = await readState2(directory, sessionID);
|
|
3483
|
+
const job = (state.jobs || []).find((candidate) => candidate.id === jobId);
|
|
3484
|
+
if (job) {
|
|
3485
|
+
job.failureCount = (job.failureCount || 0) + 1;
|
|
3486
|
+
job.lastFailureReason = "dispatch_failed";
|
|
3487
|
+
job.lastDispatchFailure = message.slice(0, 4000);
|
|
3488
|
+
job.lastDispatchFailureAt = now2();
|
|
3489
|
+
if (job.maxFailures > 0 && job.failureCount >= job.maxFailures) {
|
|
3490
|
+
job.paused = true;
|
|
3491
|
+
await notifyJob2(directory, job, "dispatch_failed");
|
|
3019
3492
|
}
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
if (model)
|
|
3023
|
-
shellBody.model = model;
|
|
3024
|
-
const dispatch2 = fireSdk2(client, "session.shell", client.session.shell.bind(client.session), { path: { id: sessionID }, body: shellBody }, { path: { sessionID }, body: shellBody }, { sessionID, ...shellBody });
|
|
3025
|
-
return { startsAssistantTurn: true, dispatch: dispatch2 };
|
|
3493
|
+
state.jobs = (state.jobs || []).map((candidate) => candidate.id === job.id ? job : candidate);
|
|
3494
|
+
await writeState2(directory, sessionID, state);
|
|
3026
3495
|
}
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
const
|
|
3034
|
-
if (
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3496
|
+
await appendLoopLog2(directory, "dispatch-error", { sessionID, job: job?.name || jobId, error: message });
|
|
3497
|
+
await toast2(client, `Loop dispatch failed${job?.paused ? " and paused" : ""}: ${message}`, job?.paused ? "error" : "warning");
|
|
3498
|
+
await scheduleDueWork(directory, client, sessionID, busyRetryMs);
|
|
3499
|
+
return true;
|
|
3500
|
+
}
|
|
3501
|
+
async function finalizeActiveRun(directory, client, sessionID, finalizeOptions = {}) {
|
|
3502
|
+
const active = activeRuns.get(sessionID);
|
|
3503
|
+
if (!active)
|
|
3504
|
+
return;
|
|
3505
|
+
if (!await canFinalizeActiveRun(directory, client, sessionID, active, finalizeOptions))
|
|
3506
|
+
return false;
|
|
3507
|
+
const recoveredStale = staleActiveRun(sessionID);
|
|
3508
|
+
if (active.compactionOnly) {
|
|
3509
|
+
const pending = compactionRuntime.getPending(sessionID);
|
|
3510
|
+
clearActiveRun(sessionID);
|
|
3511
|
+
clearSessionStatus(sessionID);
|
|
3512
|
+
await appendLoopLog2(directory, pending?.completedAt ? "compact-finished" : "compact-idle-fallback", {
|
|
3513
|
+
sessionID,
|
|
3514
|
+
job: active.job?.name || active.jobId,
|
|
3515
|
+
startedAt: active.startedAt,
|
|
3516
|
+
nativeEvent: Boolean(pending?.completedAt)
|
|
3517
|
+
});
|
|
3518
|
+
await scheduleDueWork(directory, client, sessionID);
|
|
3519
|
+
return true;
|
|
3520
|
+
}
|
|
3521
|
+
clearActiveRun(sessionID);
|
|
3522
|
+
const state = await readState2(directory, sessionID);
|
|
3523
|
+
let job = (state.jobs || []).find((candidate) => candidate.id === active.jobId);
|
|
3524
|
+
if (!job)
|
|
3525
|
+
return;
|
|
3526
|
+
job.lastFinishedAt = now2();
|
|
3527
|
+
if (recoveredStale) {
|
|
3528
|
+
await appendLoopLog2(directory, "active-stale-recovery", {
|
|
3529
|
+
sessionID,
|
|
3530
|
+
job: job.name || job.id,
|
|
3531
|
+
startedAt: active.startedAt
|
|
3532
|
+
});
|
|
3533
|
+
}
|
|
3534
|
+
await finalizationRuntime.finalizeJob(directory, client, sessionID, state, job, active.job);
|
|
3535
|
+
return true;
|
|
3038
3536
|
}
|
|
3537
|
+
const fireAction = actionDispatcher.fireAction;
|
|
3039
3538
|
async function maybeRunDueJobs(directory, client, sessionID, runOptions = {}) {
|
|
3040
3539
|
rememberSession(directory, client, sessionID);
|
|
3041
3540
|
const reschedule = async (minDelayMs = 0) => {
|
|
@@ -3070,74 +3569,18 @@ ${prompt}`;
|
|
|
3070
3569
|
candidate.watchTriggered = true;
|
|
3071
3570
|
}
|
|
3072
3571
|
}
|
|
3073
|
-
const due =
|
|
3572
|
+
const due = dueJobs2(state, Boolean(runOptions.force));
|
|
3074
3573
|
if (!due.length) {
|
|
3075
3574
|
await writeState2(directory, sessionID, state);
|
|
3076
3575
|
await reschedule();
|
|
3077
3576
|
return;
|
|
3078
3577
|
}
|
|
3079
3578
|
job = due[0];
|
|
3080
|
-
const
|
|
3081
|
-
if (
|
|
3082
|
-
state.jobs = (state.jobs || []).filter((candidate) => candidate.id !== job.id);
|
|
3083
|
-
await writeState2(directory, sessionID, state);
|
|
3084
|
-
await notifyJob2(directory, job, "max_runtime_reached");
|
|
3085
|
-
await toast2(client, `Loop stopped by --max-runtime: ${job.name || job.id}`, "success");
|
|
3086
|
-
await appendLoopLog2(directory, "max-runtime", { sessionID, job: job.name || job.id });
|
|
3087
|
-
await reschedule();
|
|
3088
|
-
return;
|
|
3089
|
-
}
|
|
3090
|
-
if (job.stopFile && await pathExists2(path6.resolve(directory, job.stopFile))) {
|
|
3091
|
-
state.jobs = (state.jobs || []).filter((candidate) => candidate.id !== job.id);
|
|
3092
|
-
await writeState2(directory, sessionID, state);
|
|
3093
|
-
await notifyJob2(directory, job, "stop_file");
|
|
3094
|
-
await toast2(client, "Loop stopped by --stop-file: " + job.stopFile, "success");
|
|
3095
|
-
await reschedule();
|
|
3579
|
+
const admission = await admissionRuntime.admitJob(directory, client, sessionID, state, job);
|
|
3580
|
+
if (!admission.admitted)
|
|
3096
3581
|
return;
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
state.jobs = (state.jobs || []).filter((candidate) => candidate.id !== job.id);
|
|
3100
|
-
await writeState2(directory, sessionID, state);
|
|
3101
|
-
await notifyJob2(directory, job, "until_reached");
|
|
3102
|
-
await toast2(client, `Loop stopped by --until: ${job.until}`, "success");
|
|
3103
|
-
await reschedule();
|
|
3104
|
-
return;
|
|
3105
|
-
}
|
|
3106
|
-
if (job.preflightCommand) {
|
|
3107
|
-
if (job.safe && dangerousShell2(job.preflightCommand)) {
|
|
3108
|
-
if (runNowRequested)
|
|
3109
|
-
delete job.runNowRequestedAt;
|
|
3110
|
-
job.paused = true;
|
|
3111
|
-
await writeState2(directory, sessionID, state);
|
|
3112
|
-
await notifyJob2(directory, job, "preflight_blocked");
|
|
3113
|
-
await toast2(client, "Preflight blocked in safe mode and loop paused: " + job.preflightCommand, "error");
|
|
3114
|
-
await reschedule();
|
|
3115
|
-
return;
|
|
3116
|
-
}
|
|
3117
|
-
const preflight = await runShellCommand2(job.preflightCommand, directory, job.timeoutMs || 300000);
|
|
3118
|
-
await appendLoopLog2(directory, "preflight", {
|
|
3119
|
-
sessionID,
|
|
3120
|
-
job: job.name || job.id,
|
|
3121
|
-
command: job.preflightCommand,
|
|
3122
|
-
code: preflight.code
|
|
3123
|
-
});
|
|
3124
|
-
if (preflight.code !== 0) {
|
|
3125
|
-
if (runNowRequested)
|
|
3126
|
-
delete job.runNowRequestedAt;
|
|
3127
|
-
job.paused = true;
|
|
3128
|
-
job.failureCount = (job.failureCount || 0) + 1;
|
|
3129
|
-
job.lastPreflightFailure = (job.preflightCommand + `
|
|
3130
|
-
exit=` + preflight.code + `
|
|
3131
|
-
` + preflight.stdout + `
|
|
3132
|
-
` + preflight.stderr).slice(0, 4000);
|
|
3133
|
-
state.jobs = (state.jobs || []).map((candidate) => candidate.id === job.id ? job : candidate);
|
|
3134
|
-
await writeState2(directory, sessionID, state);
|
|
3135
|
-
await notifyJob2(directory, job, "preflight_failed");
|
|
3136
|
-
await toast2(client, "Preflight failed and loop paused: " + job.preflightCommand, "warning");
|
|
3137
|
-
await reschedule();
|
|
3138
|
-
return;
|
|
3139
|
-
}
|
|
3140
|
-
}
|
|
3582
|
+
job = admission.job;
|
|
3583
|
+
const runNowRequested = admission.runNowRequested;
|
|
3141
3584
|
job = await ensureBranch(directory, job, client, sessionID);
|
|
3142
3585
|
const compactResult = await compactionRuntime.maybeCompact(directory, client, sessionID, job);
|
|
3143
3586
|
job = compactResult.job;
|
|
@@ -3233,7 +3676,7 @@ exit=` + preflight.code + `
|
|
|
3233
3676
|
}
|
|
3234
3677
|
}
|
|
3235
3678
|
return {
|
|
3236
|
-
dueJobs,
|
|
3679
|
+
dueJobs: dueJobs2,
|
|
3237
3680
|
clearActiveRun,
|
|
3238
3681
|
disposeSession,
|
|
3239
3682
|
recoverActiveDispatchFailure,
|
|
@@ -3254,7 +3697,7 @@ exit=` + preflight.code + `
|
|
|
3254
3697
|
// src/source/runtime/goal-steering.js
|
|
3255
3698
|
var DEFAULT_STEERING_SUPPRESSION_MS = 5 * 60000;
|
|
3256
3699
|
var DEFAULT_SEEN_USER_MESSAGE_MS = 10 * 60000;
|
|
3257
|
-
function
|
|
3700
|
+
function requireFunction11(value, label) {
|
|
3258
3701
|
if (typeof value !== "function")
|
|
3259
3702
|
throw new TypeError(`createGoalSteeringRuntime requires ${label}`);
|
|
3260
3703
|
return value;
|
|
@@ -3298,8 +3741,8 @@ function activeGoalJobs(state) {
|
|
|
3298
3741
|
});
|
|
3299
3742
|
}
|
|
3300
3743
|
function createGoalSteeringRuntime(options = {}) {
|
|
3301
|
-
const getActiveRun =
|
|
3302
|
-
const clearActiveRun =
|
|
3744
|
+
const getActiveRun = requireFunction11(options.getActiveRun, "getActiveRun");
|
|
3745
|
+
const clearActiveRun = requireFunction11(options.clearActiveRun, "clearActiveRun");
|
|
3303
3746
|
const isLoopOwnedUserMessage = typeof options.isLoopOwnedUserMessage === "function" ? options.isLoopOwnedUserMessage : () => false;
|
|
3304
3747
|
const readState2 = typeof options.readState === "function" ? options.readState : readState;
|
|
3305
3748
|
const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
|
|
@@ -4138,6 +4581,6 @@ var OpenCodeLoopPlugin2 = async (input = {}) => {
|
|
|
4138
4581
|
};
|
|
4139
4582
|
var v1_default = OpenCodeLoopPlugin2;
|
|
4140
4583
|
export {
|
|
4141
|
-
|
|
4142
|
-
|
|
4584
|
+
OpenCodeLoopPlugin2 as OpenCodeLoopPlugin,
|
|
4585
|
+
v1_default as default
|
|
4143
4586
|
};
|