@bybrawe/opencode-loop 0.5.33 → 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/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)
@@ -1623,8 +1647,185 @@ function createGoalCommandHandlers(options = {}) {
1623
1647
  }
1624
1648
 
1625
1649
  // src/source/opencode/loop-commands.js
1650
+ import { promises as fs6 } from "fs";
1651
+ import path7 from "path";
1652
+
1653
+ // src/source/runtime/companion-goal.js
1626
1654
  import { promises as fs4 } from "fs";
1627
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
+ }
1691
+
1692
+ // src/source/runtime/loop-diagnostics.js
1693
+ import { promises as fs5 } from "fs";
1694
+ import path6 from "path";
1695
+
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;
1718
+ }
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
1628
1829
  var SERVICE2 = "opencode-loop";
1629
1830
  var DEFAULT_PROGRESS_MD = `# Progress
1630
1831
 
@@ -1671,8 +1872,10 @@ function createLoopCommandHandlers(options = {}) {
1671
1872
  const removeState2 = typeof options.removeState === "function" ? options.removeState : removeState;
1672
1873
  const pathExists2 = typeof options.pathExists === "function" ? options.pathExists : pathExists;
1673
1874
  const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
1674
- const readFile = typeof options.readFile === "function" ? options.readFile : (...args) => fs4.readFile(...args);
1675
- const writeFile = typeof options.writeFile === "function" ? options.writeFile : (...args) => fs4.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;
1676
1879
  const runtimeVersion = options.runtimeVersion || process.version;
1677
1880
  const runtimePlatform = options.runtimePlatform || process.platform;
1678
1881
  async function stopLoop(directory, client, sessionID, args) {
@@ -1704,15 +1907,11 @@ function createLoopCommandHandlers(options = {}) {
1704
1907
  async function statusLoop(directory, client, sessionID) {
1705
1908
  const state = await readState2(directory, sessionID);
1706
1909
  const jobs = state.jobs || [];
1910
+ const current = now2();
1707
1911
  const lines = jobs.length ? jobs.map((job, index) => {
1708
- const current = now2();
1709
- const intervalMs = Number(job.intervalMs || 0);
1710
- const lastRunAt = Number(job.lastRunAt || 0);
1711
- const createdAt = Date.parse(job.createdAt || "");
1712
- const dueAt = Number(job.runNowRequestedAt || 0) > 0 ? current : lastRunAt > 0 ? lastRunAt + intervalMs : job.immediate === false && Number.isFinite(createdAt) ? createdAt + intervalMs : current;
1713
- const dueIn = Math.max(0, dueAt - current);
1912
+ const scheduling = describeJobScheduling(job, current);
1714
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(",");
1715
- return `${index + 1}. ${job.id}${job.name ? ` (${job.name})` : ""}: ${jobLabel(job)} | runs=${job.runCount || 0} | failures=${job.failureCount || 0} | due in ${durationToText(dueIn)} | ${flags}`;
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}`;
1716
1915
  }) : ["No active loop jobs."];
1717
1916
  await toast2(client, jobs.length ? `${jobs.length} loop job(s).` : "No active loop jobs.", jobs.length ? "info" : "warning");
1718
1917
  await say2(client, sessionID, `OpenCode loop status:
@@ -1722,7 +1921,7 @@ function createLoopCommandHandlers(options = {}) {
1722
1921
  async function logsLoop(directory, client, sessionID) {
1723
1922
  let text = "No loop log found.";
1724
1923
  try {
1725
- text = (await readFile(path5.join(stateDir(directory), "loop.log"), "utf8")).trim().split(/\r?\n/).slice(-80).join(`
1924
+ text = (await readFile(path7.join(stateDir(directory), "loop.log"), "utf8")).trim().split(/\r?\n/).slice(-80).join(`
1726
1925
  `) || text;
1727
1926
  } catch {}
1728
1927
  await say2(client, sessionID, `OpenCode loop logs:
@@ -1731,18 +1930,20 @@ function createLoopCommandHandlers(options = {}) {
1731
1930
  async function helpLoop(client, sessionID) {
1732
1931
  await say2(client, sessionID, [
1733
1932
  "OpenCode Loop help:",
1734
- "/loop 0s <prompt> Claude Code style auto-continue",
1735
- "/loop 5m --ask-never --safe <prompt> interval autonomous prompt loop",
1736
- "/loop-command 200m /compact OpenCode slash-command loop, waits for idle",
1737
- "/loop-ask 1h did you run tests and tsc --noEmit? scheduled question/check prompt",
1738
- "/loop-shell 10m npm test shell loop, waits for idle",
1739
- "/loop-goal finish the feature and keep tests green experimental persistent goal mode",
1740
- '/loop-goal --check "npm run build" --check "npm test" --complete-when-checks-pass ship it',
1741
- "/loop-goal status | pause | resume | clear manage experimental goals",
1742
- "/loop 200m --command /compact same as command loop",
1743
- '/loop 0s --verify "npm test" <prompt> verify after each assistant turn',
1744
- "/loop 0s --prompt-file loop-prompt.md load prompt from a file",
1745
- "/loop 0s --max-runtime 6h --max-failures 3 <task> stop safely after limits",
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.",
1746
1947
  "/loop-doctor | /loop-init | /loop-export"
1747
1948
  ].join(`
1748
1949
  `));
@@ -1767,22 +1968,36 @@ function createLoopCommandHandlers(options = {}) {
1767
1968
  }
1768
1969
  async function doctorLoop(directory, client, sessionID) {
1769
1970
  const state = await readState2(directory, sessionID);
1770
- await say2(client, sessionID, [
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 = [
1771
1975
  "OpenCode Loop doctor:",
1772
1976
  `- plugin: ${SERVICE2}`,
1773
1977
  `- project directory: ${directory}`,
1774
1978
  `- state directory: ${stateDir(directory)}`,
1775
- `- active jobs: ${(state.jobs || []).length}`,
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}`,
1776
1983
  `- node: ${runtimeVersion}`,
1777
1984
  `- platform: ${runtimePlatform}`,
1778
- "- smoke test: /loop 0s --max-runs 1 --dry-run continue from progress.md",
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",
1779
1988
  "- experimental goal smoke test: /loop-goal --dry-run finish the current task and verify it"
1780
- ].join(`
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(`
1781
1996
  `));
1782
1997
  }
1783
1998
  async function initLoop(directory, client, sessionID, args) {
1784
1999
  const target = String(args || "").trim() || "progress.md";
1785
- const full = path5.resolve(directory, target);
2000
+ const full = path7.resolve(directory, target);
1786
2001
  if (await pathExists2(full)) {
1787
2002
  await toast2(client, `${target} already exists.`, "warning");
1788
2003
  return;
@@ -1808,6 +2023,81 @@ function createLoopCommandHandlers(options = {}) {
1808
2023
  };
1809
2024
  }
1810
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
+
1811
2101
  // src/source/opencode/loop-registration.js
1812
2102
  var DEFAULT_GOAL_ACTIVE_RECOVERY_MS = 180000;
1813
2103
  var FALLBACK_ACTIVE_GUARD_MS = 45000;
@@ -1831,19 +2121,34 @@ function createLoopRegistration(options = {}) {
1831
2121
  const toast2 = requireFunction4(options.toast, "toast");
1832
2122
  const say2 = requireFunction4(options.say, "say");
1833
2123
  const parseLoopArgs2 = typeof options.parseLoopArgs === "function" ? options.parseLoopArgs : parseLoopArgs;
2124
+ const normalizeLoopScheduleArgs2 = typeof options.normalizeLoopScheduleArgs === "function" ? options.normalizeLoopScheduleArgs : normalizeLoopScheduleArgs;
1834
2125
  const readState2 = typeof options.readState === "function" ? options.readState : readState;
1835
2126
  const writeState2 = typeof options.writeState === "function" ? options.writeState : writeState;
1836
2127
  const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
1837
2128
  const normalizedModelRef2 = typeof options.normalizedModelRef === "function" ? options.normalizedModelRef : normalizedModelRef;
1838
2129
  const getSessionExecutionContext2 = typeof options.getSessionExecutionContext === "function" ? options.getSessionExecutionContext : getSessionExecutionContext;
2130
+ const findDedicatedGoalForSession2 = typeof options.findDedicatedGoalForSession === "function" ? options.findDedicatedGoalForSession : findDedicatedGoalForSession;
1839
2131
  const configuredGuard = Number(options.defaultActiveGuardMs);
1840
2132
  const defaultActiveGuardMs = Number.isFinite(configuredGuard) && configuredGuard > 0 ? configuredGuard : FALLBACK_ACTIVE_GUARD_MS;
1841
2133
  async function addLoop(directory, client, sessionID, args, defaults = {}) {
1842
- const parsed = parseLoopArgs2(args, defaults);
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);
1843
2140
  if (!parsed.ok) {
1844
2141
  await toast2(client, parsed.error, "warning");
1845
2142
  return;
1846
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
+ }
1847
2152
  const executionContext = getSessionExecutionContext2(sessionID) || { agent: "build" };
1848
2153
  parsed.job.agent = defaults.agent || executionContext.agent || "build";
1849
2154
  parsed.job.model = normalizedModelRef2(defaults.model) || executionContext.model;
@@ -1863,6 +2168,19 @@ function createLoopRegistration(options = {}) {
1863
2168
  if (!parsed.job.activeRecoveryMs) {
1864
2169
  parsed.job.activeRecoveryMs = isGoalJob(parsed.job) ? DEFAULT_GOAL_ACTIVE_RECOVERY_MS : Math.max(defaultActiveGuardMs, Math.min(90000, (parsed.job.intervalMs || 0) + 1e4));
1865
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
+ }
1866
2184
  if (parsed.job.dryRun) {
1867
2185
  await toast2(client, `Loop dry run: ${jobLabel(parsed.job)}`, "info");
1868
2186
  await say2(client, sessionID, "OpenCode loop dry run:\n```json\n" + JSON.stringify(parsed.job, null, 2) + "\n```");
@@ -1890,7 +2208,13 @@ function createLoopRegistration(options = {}) {
1890
2208
  if (parsed.job.immediate)
1891
2209
  scheduleIdleWork(directory, client, sessionID);
1892
2210
  await toast2(client, `${replaced ? "Loop replaced" : "Loop added"}: ${jobLabel(parsed.job)}`, "success");
1893
- await appendLoopLog2(directory, replaced ? "replace" : "add", { sessionID, job: parsed.job.name || parsed.job.id, label: jobLabel(parsed.job) });
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
+ });
1894
2218
  }
1895
2219
  return { addLoop };
1896
2220
  }
@@ -2007,6 +2331,37 @@ function clearSessionActivity(sessionID) {
2007
2331
  deleteSessionExecutionContext(sessionID);
2008
2332
  }
2009
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
+
2010
2365
  // src/source/runtime/scheduler.js
2011
2366
  var DEFAULT_IDLE_DEBOUNCE_MS = 1200;
2012
2367
  var DEFAULT_BUSY_RETRY_MS = 5000;
@@ -2014,37 +2369,6 @@ var DEFAULT_MIN_DUE_TIMER_MS = 250;
2014
2369
  var DEFAULT_MAX_DUE_TIMER_MS = 2147000000;
2015
2370
  var DEFAULT_HEARTBEAT_MS = 2500;
2016
2371
  var DEFAULT_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
2017
- function jobDueAt(job, current = now()) {
2018
- if (isGoalJob(job) && ["completed", "blocked", "cleared"].includes(job.goalStatus))
2019
- return Infinity;
2020
- if (!job.enabled || job.paused)
2021
- return Infinity;
2022
- if (job.maxRuns > 0 && (job.runCount || 0) >= job.maxRuns)
2023
- return Infinity;
2024
- if (Number(job.runNowRequestedAt || 0) > 0)
2025
- return current;
2026
- if (job.watchPaths?.length)
2027
- return Infinity;
2028
- const created = Date.parse(job.createdAt || "");
2029
- if (job.maxRuntimeMs > 0 && Number.isFinite(created) && current - created >= job.maxRuntimeMs)
2030
- return current;
2031
- if (job.intervalMs === 0)
2032
- return current;
2033
- if (!job.lastRunAt) {
2034
- if (job.immediate === false)
2035
- return (Number.isFinite(created) ? created : current) + (job.intervalMs || 0);
2036
- return current;
2037
- }
2038
- return job.lastRunAt + (job.intervalMs || 0);
2039
- }
2040
- function nextDueDelay(state, current = now()) {
2041
- let soonest = Infinity;
2042
- for (const job of state.jobs || [])
2043
- soonest = Math.min(soonest, jobDueAt(job, current));
2044
- if (!Number.isFinite(soonest))
2045
- return Infinity;
2046
- return Math.max(0, soonest - current);
2047
- }
2048
2372
  function createSchedulerRuntime(options = {}) {
2049
2373
  const idleTimers = new Map;
2050
2374
  const dueTimers = new Map;
@@ -2072,6 +2396,7 @@ function createSchedulerRuntime(options = {}) {
2072
2396
  if (options.toast)
2073
2397
  await options.toast(client, message, level);
2074
2398
  };
2399
+ const diagnostics = createSchedulerDiagnostics({ appendLoopLog: appendLog, now: clock, throttleMs: options.deferralLogThrottleMs });
2075
2400
  function stopHeartbeatIfIdle() {
2076
2401
  if (!knownSessions.size && heartbeatTimer) {
2077
2402
  clearIntervalFn(heartbeatTimer);
@@ -2119,11 +2444,13 @@ function createSchedulerRuntime(options = {}) {
2119
2444
  idleTimers.delete(sessionID);
2120
2445
  Promise.resolve().then(async () => {
2121
2446
  if (!await options.sessionIsIdle?.(client, sessionID, directory)) {
2447
+ await diagnostics.logDeferral(directory, sessionID, "session-busy", { source: "idle", retryMs: busyRetryMs });
2122
2448
  await scheduleDueWork(directory, client, sessionID, busyRetryMs);
2123
2449
  return;
2124
2450
  }
2125
2451
  await options.finalizeActiveRun?.(directory, client, sessionID);
2126
2452
  if (!await options.sessionIsIdle?.(client, sessionID, directory)) {
2453
+ await diagnostics.logDeferral(directory, sessionID, "session-busy-after-finalize", { source: "idle", retryMs: busyRetryMs });
2127
2454
  await scheduleDueWork(directory, client, sessionID, busyRetryMs);
2128
2455
  return;
2129
2456
  }
@@ -2172,11 +2499,13 @@ function createSchedulerRuntime(options = {}) {
2172
2499
  dueTimers.delete(sessionID);
2173
2500
  Promise.resolve().then(async () => {
2174
2501
  if (!await options.sessionIsIdle?.(client, sessionID, directory)) {
2502
+ await diagnostics.logDeferral(directory, sessionID, "session-busy", { source: "due", retryMs: busyRetryMs });
2175
2503
  await scheduleDueWork(directory, client, sessionID, busyRetryMs);
2176
2504
  return;
2177
2505
  }
2178
2506
  await options.finalizeActiveRun?.(directory, client, sessionID);
2179
2507
  if (!await options.sessionIsIdle?.(client, sessionID, directory)) {
2508
+ await diagnostics.logDeferral(directory, sessionID, "session-busy-after-finalize", { source: "due", retryMs: busyRetryMs });
2180
2509
  await scheduleDueWork(directory, client, sessionID, busyRetryMs);
2181
2510
  return;
2182
2511
  }
@@ -2196,6 +2525,7 @@ function createSchedulerRuntime(options = {}) {
2196
2525
  cancelIdleWork(sessionID);
2197
2526
  cancelDueWork(sessionID);
2198
2527
  stopWatchdog(sessionID);
2528
+ diagnostics.clearSession(sessionID);
2199
2529
  knownSessions.delete(sessionID);
2200
2530
  stopHeartbeatIfIdle();
2201
2531
  }
@@ -2294,8 +2624,8 @@ ${item.output}`).join(`
2294
2624
  }
2295
2625
 
2296
2626
  // src/source/runtime/job-workspace.js
2297
- import { promises as fs5 } from "fs";
2298
- import path6 from "path";
2627
+ import { promises as fs7 } from "fs";
2628
+ import path8 from "path";
2299
2629
  var MAX_SCAN_FILES = 200;
2300
2630
  var MAX_SCAN_BYTES = 2000000;
2301
2631
  function requireFunction6(value, name) {
@@ -2330,7 +2660,7 @@ function createJobWorkspaceRuntime(options = {}) {
2330
2660
  return await buildGoalPrompt2(directory, job);
2331
2661
  const sections = [];
2332
2662
  if (job.promptFile) {
2333
- const text = await readSmallTextFile2(path6.resolve(directory, job.promptFile));
2663
+ const text = await readSmallTextFile2(path8.resolve(directory, job.promptFile));
2334
2664
  if (text.trim())
2335
2665
  sections.push(`Instructions from ${job.promptFile}:
2336
2666
  ${text.trim()}`);
@@ -2340,7 +2670,7 @@ ${text.trim()}`);
2340
2670
  if (job.action)
2341
2671
  sections.push(decoratePrompt(job));
2342
2672
  for (const file of job.includeFiles || []) {
2343
- const text = await readSmallTextFile2(path6.resolve(directory, file), 80000);
2673
+ const text = await readSmallTextFile2(path8.resolve(directory, file), 80000);
2344
2674
  if (text.trim())
2345
2675
  sections.push(`Context from ${file}:
2346
2676
  ${text.trim().slice(0, 20000)}`);
@@ -2372,7 +2702,7 @@ ${text.trim().slice(0, 20000)}`);
2372
2702
  const snapshot = {};
2373
2703
  for (const file of files || []) {
2374
2704
  try {
2375
- const stat = await fs5.stat(path6.resolve(directory, file));
2705
+ const stat = await fs7.stat(path8.resolve(directory, file));
2376
2706
  snapshot[file] = `${stat.mtimeMs}:${stat.size}`;
2377
2707
  } catch {
2378
2708
  snapshot[file] = "missing";
@@ -2392,10 +2722,10 @@ ${text.trim().slice(0, 20000)}`);
2392
2722
  }
2393
2723
  async function fileContains(filePath, needle) {
2394
2724
  try {
2395
- const stat = await fs5.stat(filePath);
2725
+ const stat = await fs7.stat(filePath);
2396
2726
  if (!stat.isFile() || stat.size > MAX_SCAN_BYTES)
2397
2727
  return false;
2398
- return (await fs5.readFile(filePath, "utf8")).includes(needle);
2728
+ return (await fs7.readFile(filePath, "utf8")).includes(needle);
2399
2729
  } catch {
2400
2730
  return false;
2401
2731
  }
@@ -2403,9 +2733,9 @@ ${text.trim().slice(0, 20000)}`);
2403
2733
  async function untilReached(directory, job) {
2404
2734
  if (!job.until)
2405
2735
  return false;
2406
- const files = ["progress.md", "PROGRESS.md", "todo.md", "TODO.md", "todolist.md", "TODOLIST.md", path6.join(".opencode", "opencode-loop", "until.txt")];
2736
+ const files = ["progress.md", "PROGRESS.md", "todo.md", "TODO.md", "todolist.md", "TODOLIST.md", path8.join(".opencode", "opencode-loop", "until.txt")];
2407
2737
  for (const file of files)
2408
- if (await fileContains(path6.resolve(directory, file), job.until))
2738
+ if (await fileContains(path8.resolve(directory, file), job.until))
2409
2739
  return true;
2410
2740
  let scanned = 0;
2411
2741
  async function walk(current) {
@@ -2413,7 +2743,7 @@ ${text.trim().slice(0, 20000)}`);
2413
2743
  return false;
2414
2744
  let entries;
2415
2745
  try {
2416
- entries = await fs5.readdir(current, { withFileTypes: true });
2746
+ entries = await fs7.readdir(current, { withFileTypes: true });
2417
2747
  } catch {
2418
2748
  return false;
2419
2749
  }
@@ -2422,7 +2752,7 @@ ${text.trim().slice(0, 20000)}`);
2422
2752
  return false;
2423
2753
  if ([".git", "node_modules", "dist", "build", ".next", "coverage"].includes(entry.name))
2424
2754
  continue;
2425
- const full = path6.join(current, entry.name);
2755
+ const full = path8.join(current, entry.name);
2426
2756
  if (entry.isDirectory()) {
2427
2757
  if (await walk(full))
2428
2758
  return true;
@@ -2446,13 +2776,13 @@ ${text.trim().slice(0, 20000)}`);
2446
2776
  if (!status.stdout.trim())
2447
2777
  return;
2448
2778
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
2449
- const checkpointDir = path6.join(stateDir(directory), "checkpoints", safeID(sessionID));
2779
+ const checkpointDir = path8.join(stateDir(directory), "checkpoints", safeID(sessionID));
2450
2780
  await ensureDir(checkpointDir);
2451
2781
  const diff = await runProcess2("git", ["diff", "--binary"], directory, 120000);
2452
2782
  const staged = await runProcess2("git", ["diff", "--cached", "--binary"], directory, 120000);
2453
2783
  const prefix = `${timestamp}-${safeID(job.name || job.id)}`;
2454
- await fs5.writeFile(path6.join(checkpointDir, `${prefix}.status.txt`), status.stdout + status.stderr);
2455
- await fs5.writeFile(path6.join(checkpointDir, `${prefix}.patch`), `${diff.stdout}
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}
2456
2786
  ${staged.stdout}`);
2457
2787
  if (job.gitCheckpoint) {
2458
2788
  await runProcess2("git", ["add", "-A"], directory, 120000);
@@ -2591,6 +2921,25 @@ function createSessionStatusRuntime(options = {}) {
2591
2921
  const seenAt = sessionStatusSeenAt.get(sessionID) || 0;
2592
2922
  return cached === "idle" && seenAt > (active.startedAt || 0);
2593
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
+ }
2594
2943
  async function sessionStatusType(client, sessionID, directory, options2 = {}) {
2595
2944
  if (hasActiveToolCalls(sessionID) || hasBusyDescendant(sessionID)) {
2596
2945
  markSessionStatus(sessionID, "busy");
@@ -2604,6 +2953,8 @@ function createSessionStatusRuntime(options = {}) {
2604
2953
  return cached;
2605
2954
  const live = await readLiveSessionStatus(client, sessionID, directory);
2606
2955
  if (live?.type) {
2956
+ if (await recoverCompletedTailWithoutActiveRun(directory, client, sessionID, live.type, seenAt))
2957
+ return "idle";
2607
2958
  if ((live.type === "busy" || live.type === "retry") && options2.recoverStaleActive !== false) {
2608
2959
  const active = activeRuns.get(sessionID);
2609
2960
  if (active) {
@@ -2924,7 +3275,7 @@ exit=` + postrun.code + `
2924
3275
  }
2925
3276
 
2926
3277
  // src/source/runtime/run-admission.js
2927
- import path7 from "path";
3278
+ import path9 from "path";
2928
3279
  function requireFunction9(value, label) {
2929
3280
  if (typeof value !== "function")
2930
3281
  throw new TypeError(`createRunAdmissionRuntime requires ${label}`);
@@ -2941,26 +3292,8 @@ function createRunAdmissionRuntime(options = {}) {
2941
3292
  const notifyJob2 = typeof options.notifyJob === "function" ? options.notifyJob : notifyJob;
2942
3293
  const toast2 = typeof options.toast === "function" ? options.toast : toast;
2943
3294
  const dangerousShell2 = typeof options.dangerousShell === "function" ? options.dangerousShell : dangerousShell;
2944
- function dueJobs(state, force = false) {
2945
- const current = now2();
2946
- const due = (state.jobs || []).filter((job) => {
2947
- if (isGoalJob(job) && ["completed", "blocked", "cleared"].includes(job.goalStatus))
2948
- return false;
2949
- if (!job.enabled || job.paused)
2950
- return false;
2951
- if (job.maxRuns > 0 && (job.runCount || 0) >= job.maxRuns)
2952
- return false;
2953
- if (job.maxRuntimeMs > 0 && current - Date.parse(job.createdAt || new Date().toISOString()) >= job.maxRuntimeMs)
2954
- return true;
2955
- if (Number(job.runNowRequestedAt || 0) > 0)
2956
- return true;
2957
- if (force)
2958
- return true;
2959
- if (job.watchPaths?.length)
2960
- return job.watchTriggered === true;
2961
- return job.intervalMs === 0 || !job.lastRunAt || current - job.lastRunAt >= job.intervalMs;
2962
- });
2963
- return due.sort((a, b) => Number(Number(b.runNowRequestedAt || 0) > 0) - Number(Number(a.runNowRequestedAt || 0) > 0));
3295
+ function dueJobs2(state, force = false) {
3296
+ return dueJobs(state, now2(), force);
2964
3297
  }
2965
3298
  async function reschedule(directory, client, sessionID) {
2966
3299
  await scheduleDueWork(directory, client, sessionID);
@@ -2980,7 +3313,7 @@ function createRunAdmissionRuntime(options = {}) {
2980
3313
  if (job.maxRuntimeMs > 0 && now2() - Date.parse(job.createdAt || new Date().toISOString()) >= job.maxRuntimeMs) {
2981
3314
  return await stopAndRemove(directory, client, sessionID, state, job, "max_runtime_reached", `Loop stopped by --max-runtime: ${job.name || job.id}`, "max-runtime");
2982
3315
  }
2983
- if (job.stopFile && await pathExists2(path7.resolve(directory, job.stopFile))) {
3316
+ if (job.stopFile && await pathExists2(path9.resolve(directory, job.stopFile))) {
2984
3317
  return await stopAndRemove(directory, client, sessionID, state, job, "stop_file", "Loop stopped by --stop-file: " + job.stopFile);
2985
3318
  }
2986
3319
  if (await untilReached(directory, job)) {
@@ -3023,7 +3356,7 @@ exit=` + preflight.code + `
3023
3356
  }
3024
3357
  return { admitted: true, job, runNowRequested };
3025
3358
  }
3026
- return { dueJobs, admitJob };
3359
+ return { dueJobs: dueJobs2, admitJob };
3027
3360
  }
3028
3361
 
3029
3362
  // src/source/runtime/executor.js
@@ -3125,7 +3458,7 @@ function createLoopExecutor(options = {}) {
3125
3458
  toast: toast2,
3126
3459
  dangerousShell: dangerousShell2
3127
3460
  });
3128
- const dueJobs = admissionRuntime.dueJobs;
3461
+ const dueJobs2 = admissionRuntime.dueJobs;
3129
3462
  function clearActiveRun(sessionID) {
3130
3463
  const active = activeRuns.get(sessionID);
3131
3464
  if (active?.timer)
@@ -3236,7 +3569,7 @@ function createLoopExecutor(options = {}) {
3236
3569
  candidate.watchTriggered = true;
3237
3570
  }
3238
3571
  }
3239
- const due = dueJobs(state, Boolean(runOptions.force));
3572
+ const due = dueJobs2(state, Boolean(runOptions.force));
3240
3573
  if (!due.length) {
3241
3574
  await writeState2(directory, sessionID, state);
3242
3575
  await reschedule();
@@ -3343,7 +3676,7 @@ function createLoopExecutor(options = {}) {
3343
3676
  }
3344
3677
  }
3345
3678
  return {
3346
- dueJobs,
3679
+ dueJobs: dueJobs2,
3347
3680
  clearActiveRun,
3348
3681
  disposeSession,
3349
3682
  recoverActiveDispatchFailure,