@bybrawe/opencode-loop 0.5.35 → 0.5.37

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
@@ -801,11 +801,25 @@ function orderedSessionMessages(messages) {
801
801
  return { message, index, created: Number.isFinite(created) ? created : 0 };
802
802
  }).sort((a, b) => a.created - b.created || a.index - b.index).map((entry) => entry.message);
803
803
  }
804
+ function assistantMessageHasMeaningfulActivity(message) {
805
+ const parts = Array.isArray(message?.parts) ? message.parts : [];
806
+ for (const part of parts) {
807
+ if (!part || typeof part !== "object")
808
+ continue;
809
+ if (part.type === "text" && typeof part.text === "string" && part.text.trim())
810
+ return true;
811
+ if (["tool", "file", "patch", "artifact"].includes(String(part.type || "")))
812
+ return true;
813
+ }
814
+ const info = message?.info || message || {};
815
+ return [info.text, info.content, info.summary].some((value) => typeof value === "string" && value.trim());
816
+ }
804
817
  async function activeRunCompletionFromMessages(directory, client, sessionID, active) {
805
818
  const messages = await readRecentSessionMessages(client, sessionID, directory);
806
819
  if (!messages)
807
820
  return "unknown";
808
- const tail = orderedSessionMessages(messages).at(-1);
821
+ const ordered = orderedSessionMessages(messages);
822
+ const tail = ordered.at(-1);
809
823
  const info = tail?.info || tail;
810
824
  if (!info || info.role !== "assistant")
811
825
  return "incomplete";
@@ -816,7 +830,17 @@ async function activeRunCompletionFromMessages(directory, client, sessionID, act
816
830
  const startedAt = Number(active?.startedAt || 0);
817
831
  if (startedAt > 0 && completed < startedAt && (!Number.isFinite(created) || created < startedAt))
818
832
  return "incomplete";
819
- return "completed";
833
+ const relevant = ordered.filter((message) => {
834
+ const candidate = message?.info || message || {};
835
+ if (candidate.role !== "assistant")
836
+ return false;
837
+ if (startedAt <= 0)
838
+ return true;
839
+ const candidateCreated = Number(candidate?.time?.created || 0);
840
+ const candidateCompleted = Number(candidate?.time?.completed || 0);
841
+ return candidateCreated >= startedAt || candidateCompleted >= startedAt;
842
+ });
843
+ return relevant.some(assistantMessageHasMeaningfulActivity) ? "completed" : "empty";
820
844
  }
821
845
  async function resolveCompactionModel(directory, client, sessionID, preferredModel) {
822
846
  const preferred = normalizedModelRef(preferredModel);
@@ -902,9 +926,9 @@ function loopOwnedUserMessageGuardActive(sessionID, messageID) {
902
926
  const entry = loopOwnedUserMessageGuards.get(sessionID);
903
927
  if (!entry || typeof entry !== "object")
904
928
  return false;
905
- for (const [id2, expiresAt] of entry.messageIDs.entries())
929
+ for (const [id, expiresAt] of entry.messageIDs.entries())
906
930
  if (expiresAt < now())
907
- entry.messageIDs.delete(id2);
931
+ entry.messageIDs.delete(id);
908
932
  const id = typeof messageID === "string" ? messageID : "";
909
933
  if (id && entry.messageIDs.has(id))
910
934
  return true;
@@ -1593,8 +1617,8 @@ function createGoalCommandHandlers(options = {}) {
1593
1617
  const addLoop = requireFunction2(options.addLoop, "addLoop");
1594
1618
  const scheduleDueWork = requireFunction2(options.scheduleDueWork, "scheduleDueWork");
1595
1619
  const scheduleIdleWork = requireFunction2(options.scheduleIdleWork, "scheduleIdleWork");
1596
- const toast2 = requireFunction2(options.toast, "toast");
1597
- const say2 = requireFunction2(options.say, "say");
1620
+ const toast = requireFunction2(options.toast, "toast");
1621
+ const say = requireFunction2(options.say, "say");
1598
1622
  const readState2 = typeof options.readState === "function" ? options.readState : readState;
1599
1623
  const writeState2 = typeof options.writeState === "function" ? options.writeState : writeState;
1600
1624
  const setGoalComplete2 = typeof options.setGoalComplete === "function" ? options.setGoalComplete : setGoalComplete;
@@ -1611,8 +1635,8 @@ function createGoalCommandHandlers(options = {}) {
1611
1635
  const rejected = job.goalCompletionRejectedReason ? " | completion-rejected" : "";
1612
1636
  return `${index + 1}. ${job.id}${job.name ? ` (${job.name})` : ""}: ${status} | turns=${job.runCount || 0} | objective=${String(job.action || job.goalFile || "").slice(0, 220)}${checks}${acceptance}${progress}${noProgress}${rejected}`;
1613
1637
  }) : ["No experimental goal jobs."];
1614
- await toast2(client, goals.length ? `${goals.length} experimental goal(s).` : "No experimental goal jobs.", goals.length ? "info" : "warning");
1615
- await say2(client, sessionID, `OpenCode Loop experimental goal status:
1638
+ await toast(client, goals.length ? `${goals.length} experimental goal(s).` : "No experimental goal jobs.", goals.length ? "info" : "warning");
1639
+ await say(client, sessionID, `OpenCode Loop experimental goal status:
1616
1640
  ` + lines.join(`
1617
1641
  `));
1618
1642
  }
@@ -1623,7 +1647,7 @@ function createGoalCommandHandlers(options = {}) {
1623
1647
  state.jobs = (state.jobs || []).map((job, index) => isGoalJob(job) && matchJob(job, target, index) ? (count++, { ...job, paused: true }) : job);
1624
1648
  await writeState2(directory, sessionID, state);
1625
1649
  await scheduleDueWork(directory, client, sessionID);
1626
- await toast2(client, `Paused ${count} experimental goal(s).`, count ? "success" : "warning");
1650
+ await toast(client, `Paused ${count} experimental goal(s).`, count ? "success" : "warning");
1627
1651
  }
1628
1652
  async function resumeGoal(directory, client, sessionID, args) {
1629
1653
  const target = String(args || "").trim() || "goal";
@@ -1636,7 +1660,7 @@ function createGoalCommandHandlers(options = {}) {
1636
1660
  return { ...job, paused: false, enabled: true, goalStatus: job.goalStatus === "blocked" ? "active" : job.goalStatus || "active", lastRunAt: 0, noProgressCount: 0, goalNoProgressReason: "", goalInterruptedReason: "" };
1637
1661
  });
1638
1662
  await writeState2(directory, sessionID, state);
1639
- await toast2(client, `Resumed ${count} experimental goal(s).`, count ? "success" : "warning");
1663
+ await toast(client, `Resumed ${count} experimental goal(s).`, count ? "success" : "warning");
1640
1664
  if (count) {
1641
1665
  await scheduleDueWork(directory, client, sessionID);
1642
1666
  scheduleIdleWork(directory, client, sessionID);
@@ -1649,15 +1673,15 @@ function createGoalCommandHandlers(options = {}) {
1649
1673
  state.jobs = (state.jobs || []).filter((job, index) => !isGoalJob(job) || target && !matchJob(job, target, index));
1650
1674
  await writeState2(directory, sessionID, state);
1651
1675
  await scheduleDueWork(directory, client, sessionID);
1652
- await toast2(client, `Cleared ${before - state.jobs.length} experimental goal(s).`, before !== state.jobs.length ? "success" : "warning");
1676
+ await toast(client, `Cleared ${before - state.jobs.length} experimental goal(s).`, before !== state.jobs.length ? "success" : "warning");
1653
1677
  }
1654
1678
  async function completeGoalCommand(directory, client, sessionID, args) {
1655
1679
  const result = await setGoalComplete2(directory, sessionID, { summary: String(args || "").trim() || "Goal manually marked complete.", evidence: "Marked complete by /loop-goal-done.", manual: true });
1656
- await toast2(client, result.message, result.ok ? "success" : "warning");
1680
+ await toast(client, result.message, result.ok ? "success" : "warning");
1657
1681
  }
1658
1682
  async function blockGoalCommand(directory, client, sessionID, args) {
1659
1683
  const result = await setGoalBlocked2(directory, sessionID, { reason: String(args || "").trim() || "Goal manually marked blocked.", needed: "User input or manual intervention." });
1660
- await toast2(client, result.message, "warning");
1684
+ await toast(client, result.message, "warning");
1661
1685
  }
1662
1686
  async function addGoal(directory, client, sessionID, args) {
1663
1687
  const text = String(args || "").trim();
@@ -1906,8 +1930,8 @@ function createLoopCommandHandlers(options = {}) {
1906
1930
  const stopWatchdog = requireFunction3(options.stopWatchdog, "stopWatchdog");
1907
1931
  const scheduleDueWork = requireFunction3(options.scheduleDueWork, "scheduleDueWork");
1908
1932
  const maybeRunDueJobs = requireFunction3(options.maybeRunDueJobs, "maybeRunDueJobs");
1909
- const toast2 = requireFunction3(options.toast, "toast");
1910
- const say2 = requireFunction3(options.say, "say");
1933
+ const toast = requireFunction3(options.toast, "toast");
1934
+ const say = requireFunction3(options.say, "say");
1911
1935
  const now2 = typeof options.now === "function" ? options.now : now;
1912
1936
  const readState2 = typeof options.readState === "function" ? options.readState : readState;
1913
1937
  const writeState2 = typeof options.writeState === "function" ? options.writeState : writeState;
@@ -1927,7 +1951,7 @@ function createLoopCommandHandlers(options = {}) {
1927
1951
  clearActiveRun(sessionID);
1928
1952
  cancelDueWork(sessionID);
1929
1953
  stopWatchdog(sessionID);
1930
- await toast2(client, "All loops stopped for this session.", "success");
1954
+ await toast(client, "All loops stopped for this session.", "success");
1931
1955
  return;
1932
1956
  }
1933
1957
  const state = await readState2(directory, sessionID);
@@ -1935,7 +1959,7 @@ function createLoopCommandHandlers(options = {}) {
1935
1959
  state.jobs = state.jobs.filter((job, index) => !matchJob(job, target, index));
1936
1960
  await writeState2(directory, sessionID, state);
1937
1961
  await scheduleDueWork(directory, client, sessionID);
1938
- await toast2(client, `Stopped ${before - state.jobs.length} loop(s).`, "success");
1962
+ await toast(client, `Stopped ${before - state.jobs.length} loop(s).`, "success");
1939
1963
  }
1940
1964
  async function updateJobState(directory, client, sessionID, args, updater, message) {
1941
1965
  const target = String(args || "").trim() || "all";
@@ -1944,7 +1968,7 @@ function createLoopCommandHandlers(options = {}) {
1944
1968
  state.jobs = (state.jobs || []).map((job, index) => matchJob(job, target, index) ? (count++, updater(job)) : job);
1945
1969
  await writeState2(directory, sessionID, state);
1946
1970
  await scheduleDueWork(directory, client, sessionID);
1947
- await toast2(client, `${message}: ${count} loop(s).`, count ? "success" : "warning");
1971
+ await toast(client, `${message}: ${count} loop(s).`, count ? "success" : "warning");
1948
1972
  }
1949
1973
  async function statusLoop(directory, client, sessionID) {
1950
1974
  const state = await readState2(directory, sessionID);
@@ -1955,8 +1979,8 @@ function createLoopCommandHandlers(options = {}) {
1955
1979
  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(",");
1956
1980
  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}`;
1957
1981
  }) : ["No active loop jobs."];
1958
- await toast2(client, jobs.length ? `${jobs.length} loop job(s).` : "No active loop jobs.", jobs.length ? "info" : "warning");
1959
- await say2(client, sessionID, `OpenCode loop status:
1982
+ await toast(client, jobs.length ? `${jobs.length} loop job(s).` : "No active loop jobs.", jobs.length ? "info" : "warning");
1983
+ await say(client, sessionID, `OpenCode loop status:
1960
1984
  ` + lines.join(`
1961
1985
  `));
1962
1986
  }
@@ -1966,11 +1990,11 @@ function createLoopCommandHandlers(options = {}) {
1966
1990
  text = (await readFile(path7.join(stateDir(directory), "loop.log"), "utf8")).trim().split(/\r?\n/).slice(-80).join(`
1967
1991
  `) || text;
1968
1992
  } catch {}
1969
- await say2(client, sessionID, `OpenCode loop logs:
1993
+ await say(client, sessionID, `OpenCode loop logs:
1970
1994
  ` + text);
1971
1995
  }
1972
1996
  async function helpLoop(client, sessionID) {
1973
- await say2(client, sessionID, [
1997
+ await say(client, sessionID, [
1974
1998
  "OpenCode Loop help:",
1975
1999
  "/loop continue the project auto-continue forever whenever the session becomes idle",
1976
2000
  "/loop idle continue the project explicit form of the same idle loop",
@@ -2004,7 +2028,7 @@ function createLoopCommandHandlers(options = {}) {
2004
2028
  count += 1;
2005
2029
  }
2006
2030
  await writeState2(directory, sessionID, state);
2007
- await toast2(client, `Marked ${count} loop job(s) due now.`, count ? "success" : "warning");
2031
+ await toast(client, `Marked ${count} loop job(s) due now.`, count ? "success" : "warning");
2008
2032
  if (count)
2009
2033
  await scheduleDueWork(directory, client, sessionID);
2010
2034
  }
@@ -2034,23 +2058,23 @@ function createLoopCommandHandlers(options = {}) {
2034
2058
  }
2035
2059
  if (otherSessions.length > 8)
2036
2060
  lines.push(`- ... ${otherSessions.length - 8} more persisted session(s)`);
2037
- await say2(client, sessionID, lines.join(`
2061
+ await say(client, sessionID, lines.join(`
2038
2062
  `));
2039
2063
  }
2040
2064
  async function initLoop(directory, client, sessionID, args) {
2041
2065
  const target = String(args || "").trim() || "progress.md";
2042
2066
  const full = path7.resolve(directory, target);
2043
2067
  if (await pathExists2(full)) {
2044
- await toast2(client, `${target} already exists.`, "warning");
2068
+ await toast(client, `${target} already exists.`, "warning");
2045
2069
  return;
2046
2070
  }
2047
2071
  await writeFile(full, DEFAULT_PROGRESS_MD, "utf8");
2048
- await toast2(client, `Created ${target}.`, "success");
2072
+ await toast(client, `Created ${target}.`, "success");
2049
2073
  await appendLoopLog2(directory, "init", { sessionID, file: target });
2050
2074
  }
2051
2075
  async function exportLoop(directory, client, sessionID) {
2052
2076
  const state = await readState2(directory, sessionID);
2053
- await say2(client, sessionID, "OpenCode loop state export:\n```json\n" + JSON.stringify(state, null, 2) + "\n```");
2077
+ await say(client, sessionID, "OpenCode loop state export:\n```json\n" + JSON.stringify(state, null, 2) + "\n```");
2054
2078
  }
2055
2079
  return {
2056
2080
  stopLoop,
@@ -2160,8 +2184,8 @@ function createLoopRegistration(options = {}) {
2160
2184
  const snapshotPaths = requireFunction4(options.snapshotPaths, "snapshotPaths");
2161
2185
  const scheduleDueWork = requireFunction4(options.scheduleDueWork, "scheduleDueWork");
2162
2186
  const scheduleIdleWork = requireFunction4(options.scheduleIdleWork, "scheduleIdleWork");
2163
- const toast2 = requireFunction4(options.toast, "toast");
2164
- const say2 = requireFunction4(options.say, "say");
2187
+ const toast = requireFunction4(options.toast, "toast");
2188
+ const say = requireFunction4(options.say, "say");
2165
2189
  const parseLoopArgs2 = typeof options.parseLoopArgs === "function" ? options.parseLoopArgs : parseLoopArgs;
2166
2190
  const normalizeLoopScheduleArgs2 = typeof options.normalizeLoopScheduleArgs === "function" ? options.normalizeLoopScheduleArgs : normalizeLoopScheduleArgs;
2167
2191
  const readState2 = typeof options.readState === "function" ? options.readState : readState;
@@ -2175,12 +2199,12 @@ function createLoopRegistration(options = {}) {
2175
2199
  async function addLoop(directory, client, sessionID, args, defaults = {}) {
2176
2200
  const normalized = normalizeLoopScheduleArgs2(args, defaults);
2177
2201
  if (!normalized.ok) {
2178
- await toast2(client, normalized.error, "warning");
2202
+ await toast(client, normalized.error, "warning");
2179
2203
  return;
2180
2204
  }
2181
2205
  const parsed = parseLoopArgs2(normalized.args, normalized.defaults);
2182
2206
  if (!parsed.ok) {
2183
- await toast2(client, parsed.error, "warning");
2207
+ await toast(client, parsed.error, "warning");
2184
2208
  return;
2185
2209
  }
2186
2210
  parsed.job.scheduleMode = normalized.scheduleMode;
@@ -2219,13 +2243,13 @@ function createLoopRegistration(options = {}) {
2219
2243
  job: parsed.job.name || parsed.job.id,
2220
2244
  goal: dedicatedGoal.id
2221
2245
  });
2222
- 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");
2246
+ await toast(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");
2223
2247
  return;
2224
2248
  }
2225
2249
  }
2226
2250
  if (parsed.job.dryRun) {
2227
- await toast2(client, `Loop dry run: ${jobLabel(parsed.job)}`, "info");
2228
- await say2(client, sessionID, "OpenCode loop dry run:\n```json\n" + JSON.stringify(parsed.job, null, 2) + "\n```");
2251
+ await toast(client, `Loop dry run: ${jobLabel(parsed.job)}`, "info");
2252
+ await say(client, sessionID, "OpenCode loop dry run:\n```json\n" + JSON.stringify(parsed.job, null, 2) + "\n```");
2229
2253
  return;
2230
2254
  }
2231
2255
  const state = await readState2(directory, sessionID);
@@ -2249,7 +2273,7 @@ function createLoopRegistration(options = {}) {
2249
2273
  await scheduleDueWork(directory, client, sessionID);
2250
2274
  if (parsed.job.immediate)
2251
2275
  scheduleIdleWork(directory, client, sessionID);
2252
- await toast2(client, `${replaced ? "Loop replaced" : "Loop added"}: ${jobLabel(parsed.job)}`, "success");
2276
+ await toast(client, `${replaced ? "Loop replaced" : "Loop added"}: ${jobLabel(parsed.job)}`, "success");
2253
2277
  await appendLoopLog2(directory, replaced ? "replace" : "add", {
2254
2278
  sessionID,
2255
2279
  job: parsed.job.name || parsed.job.id,
@@ -2376,19 +2400,19 @@ function clearSessionActivity(sessionID) {
2376
2400
  // src/source/runtime/scheduler-diagnostics.js
2377
2401
  var DEFAULT_DEFERRAL_LOG_THROTTLE_MS = 30000;
2378
2402
  function createSchedulerDiagnostics(options = {}) {
2379
- const now2 = typeof options.now === "function" ? options.now : Date.now;
2380
- const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : async () => {};
2403
+ const now = typeof options.now === "function" ? options.now : Date.now;
2404
+ const appendLoopLog = typeof options.appendLoopLog === "function" ? options.appendLoopLog : async () => {};
2381
2405
  const configured = Number(options.throttleMs);
2382
2406
  const throttleMs = Number.isFinite(configured) && configured >= 0 ? configured : DEFAULT_DEFERRAL_LOG_THROTTLE_MS;
2383
2407
  const lastLogged = new Map;
2384
2408
  async function logDeferral(directory, sessionID, reason, extra = {}) {
2385
2409
  const key = `${sessionID || "unknown"}:${reason || "deferred"}:${extra.source || "runtime"}`;
2386
- const current = now2();
2410
+ const current = now();
2387
2411
  const previous = Number(lastLogged.get(key) || 0);
2388
2412
  if (previous > 0 && current - previous < throttleMs)
2389
2413
  return false;
2390
2414
  lastLogged.set(key, current);
2391
- await appendLoopLog2(directory, "deferred", {
2415
+ await appendLoopLog(directory, "deferred", {
2392
2416
  sessionID,
2393
2417
  reason,
2394
2418
  ...extra
@@ -2434,7 +2458,7 @@ function createSchedulerRuntime(options = {}) {
2434
2458
  if (options.appendLoopLog)
2435
2459
  await options.appendLoopLog(directory, event, extra);
2436
2460
  };
2437
- const toast2 = async (client, message, level) => {
2461
+ const toast = async (client, message, level) => {
2438
2462
  if (options.toast)
2439
2463
  await options.toast(client, message, level);
2440
2464
  };
@@ -2498,7 +2522,7 @@ function createSchedulerRuntime(options = {}) {
2498
2522
  }
2499
2523
  await options.maybeRunDueJobs?.(directory, client, sessionID);
2500
2524
  }).catch((error) => {
2501
- toast2(client, `Loop idle handler failed: ${errorMessage(error)}`, "error").catch(() => {});
2525
+ toast(client, `Loop idle handler failed: ${errorMessage(error)}`, "error").catch(() => {});
2502
2526
  appendLog(directory, "idle-error", { sessionID, error: errorMessage(error) }).catch(() => {});
2503
2527
  });
2504
2528
  }, idleDebounceMs);
@@ -2510,13 +2534,13 @@ function createSchedulerRuntime(options = {}) {
2510
2534
  const timer = setIntervalFn(() => {
2511
2535
  Promise.resolve().then(async () => {
2512
2536
  const state = await readStateFn(directory, sessionID);
2513
- const delay2 = nextDueDelay(state, clock());
2537
+ const delay = nextDueDelay(state, clock());
2514
2538
  const hasJobs = (state.jobs || []).some((job) => job.enabled !== false && !job.paused && (!isGoalJob(job) || !["completed", "blocked", "cleared"].includes(job.goalStatus)));
2515
- if (!hasJobs || !Number.isFinite(delay2)) {
2539
+ if (!hasJobs || !Number.isFinite(delay)) {
2516
2540
  stopWatchdog(sessionID);
2517
2541
  return;
2518
2542
  }
2519
- if (delay2 <= 0)
2543
+ if (delay <= 0)
2520
2544
  await options.maybeRunDueJobs?.(directory, client, sessionID);
2521
2545
  else
2522
2546
  await scheduleDueWork(directory, client, sessionID);
@@ -2533,10 +2557,10 @@ function createSchedulerRuntime(options = {}) {
2533
2557
  async function scheduleDueWork(directory, client, sessionID, minDelayMs = 0) {
2534
2558
  cancelDueWork(sessionID);
2535
2559
  const state = await readStateFn(directory, sessionID);
2536
- const delay2 = nextDueDelay(state, clock());
2537
- if (!Number.isFinite(delay2))
2560
+ const delay = nextDueDelay(state, clock());
2561
+ if (!Number.isFinite(delay))
2538
2562
  return;
2539
- const wait = Math.min(Math.max(delay2, minDelayMs, minDueTimerMs), maxDueTimerMs);
2563
+ const wait = Math.min(Math.max(delay, minDelayMs, minDueTimerMs), maxDueTimerMs);
2540
2564
  const timer = setTimeoutFn(() => {
2541
2565
  dueTimers.delete(sessionID);
2542
2566
  Promise.resolve().then(async () => {
@@ -2553,7 +2577,7 @@ function createSchedulerRuntime(options = {}) {
2553
2577
  }
2554
2578
  await options.maybeRunDueJobs?.(directory, client, sessionID);
2555
2579
  }).catch((error) => {
2556
- toast2(client, `Loop due timer failed: ${errorMessage(error)}`, "error").catch(() => {});
2580
+ toast(client, `Loop due timer failed: ${errorMessage(error)}`, "error").catch(() => {});
2557
2581
  appendLog(directory, "due-timer-error", { sessionID, error: errorMessage(error) }).catch(() => {});
2558
2582
  });
2559
2583
  }, wait);
@@ -2592,9 +2616,9 @@ function requireFunction5(value, name) {
2592
2616
  return value;
2593
2617
  }
2594
2618
  function createGoalExecutionPolicy(options = {}) {
2595
- const runShellCommand2 = requireFunction5(options.runShellCommand, "runShellCommand");
2619
+ const runShellCommand = requireFunction5(options.runShellCommand, "runShellCommand");
2596
2620
  const dangerousShell = requireFunction5(options.dangerousShell, "dangerousShell");
2597
- const toast2 = requireFunction5(options.toast, "toast");
2621
+ const toast = requireFunction5(options.toast, "toast");
2598
2622
  const now2 = typeof options.now === "function" ? options.now : now;
2599
2623
  const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
2600
2624
  async function applyGoalNoProgressGuard(directory, client, sessionID, job, beforeJob) {
@@ -2615,7 +2639,7 @@ function createGoalExecutionPolicy(options = {}) {
2615
2639
  job.paused = true;
2616
2640
  job.goalNoProgressPausedAt = now2();
2617
2641
  job.goalNoProgressReason = `Paused after ${job.noProgressCount} turn(s) without recorded progress. Resume with /loop-goal-resume after adjusting the goal or evidence.`;
2618
- await toast2(client, job.goalNoProgressReason, "warning");
2642
+ await toast(client, job.goalNoProgressReason, "warning");
2619
2643
  await appendLoopLog2(directory, "goal-no-progress-paused", { sessionID, job: job.name || job.id, count: job.noProgressCount, limit });
2620
2644
  }
2621
2645
  return job;
@@ -2629,7 +2653,7 @@ function createGoalExecutionPolicy(options = {}) {
2629
2653
  results.push({ command, code: -1, output: "Blocked dangerous command in safe mode." });
2630
2654
  continue;
2631
2655
  }
2632
- const result = await runShellCommand2(command, directory, job.timeoutMs || 300000);
2656
+ const result = await runShellCommand(command, directory, job.timeoutMs || 300000);
2633
2657
  results.push({ command, code: result.code, output: (result.stdout + `
2634
2658
  ` + result.stderr).slice(0, 1200) });
2635
2659
  }
@@ -2639,7 +2663,7 @@ function createGoalExecutionPolicy(options = {}) {
2639
2663
  if (allPassed) {
2640
2664
  job.goalChecksPassedAt = now2();
2641
2665
  job.failureCount = 0;
2642
- await toast2(client, "Goal checks passed.", "success");
2666
+ await toast(client, "Goal checks passed.", "success");
2643
2667
  if (job.goalCompleteWhenChecksPass) {
2644
2668
  job.goalStatus = "completed";
2645
2669
  job.enabled = false;
@@ -2657,7 +2681,7 @@ exit=${item.code}
2657
2681
  ${item.output}`).join(`
2658
2682
 
2659
2683
  `).slice(0, 4000);
2660
- await toast2(client, "Goal checks still failing; goal will continue on next idle turn.", "warning");
2684
+ await toast(client, "Goal checks still failing; goal will continue on next idle turn.", "warning");
2661
2685
  }
2662
2686
  await appendLoopLog2(directory, "goal-checks", { sessionID, job: job.name || job.id, results: results.map((item) => ({ command: item.command, code: item.code })) });
2663
2687
  return job;
@@ -2692,7 +2716,7 @@ function dangerousShell(command) {
2692
2716
  ].some((pattern) => pattern.test(text));
2693
2717
  }
2694
2718
  function createJobWorkspaceRuntime(options = {}) {
2695
- const toast2 = requireFunction6(options.toast, "toast");
2719
+ const toast = requireFunction6(options.toast, "toast");
2696
2720
  const runProcess2 = typeof options.runProcess === "function" ? options.runProcess : runProcess;
2697
2721
  const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
2698
2722
  const readSmallTextFile2 = typeof options.readSmallTextFile === "function" ? options.readSmallTextFile : readSmallTextFile;
@@ -2736,7 +2760,7 @@ ${text.trim().slice(0, 20000)}`);
2736
2760
  if (result.code !== 0)
2737
2761
  result = await runProcess2("git", ["switch", "-c", branch], directory, 30000);
2738
2762
  job.branchDone = true;
2739
- await toast2(client, result.code === 0 ? `Loop branch active: ${branch}` : `Could not switch/create branch: ${branch}`, result.code === 0 ? "success" : "warning");
2763
+ await toast(client, result.code === 0 ? `Loop branch active: ${branch}` : `Could not switch/create branch: ${branch}`, result.code === 0 ? "success" : "warning");
2740
2764
  await appendLoopLog2(directory, "branch", { sessionID, branch, code: result.code });
2741
2765
  return job;
2742
2766
  }
@@ -2830,7 +2854,7 @@ ${staged.stdout}`);
2830
2854
  await runProcess2("git", ["add", "-A"], directory, 120000);
2831
2855
  await runProcess2("git", ["commit", "-m", `chore: opencode loop checkpoint ${timestamp}`], directory, 120000);
2832
2856
  }
2833
- await toast2(client, `Loop checkpoint saved: ${prefix}`, "success");
2857
+ await toast(client, `Loop checkpoint saved: ${prefix}`, "success");
2834
2858
  }
2835
2859
  return {
2836
2860
  buildPrompt,
@@ -2853,6 +2877,9 @@ function nonNegativeNumber(value, fallback) {
2853
2877
  const number = Number(value);
2854
2878
  return Number.isFinite(number) && number >= 0 ? number : fallback;
2855
2879
  }
2880
+ function settledAssistantCompletion(value) {
2881
+ return value === "completed" || value === "empty";
2882
+ }
2856
2883
  function createSessionStatusRuntime(options = {}) {
2857
2884
  const activeRuns = options.activeRuns;
2858
2885
  if (!(activeRuns instanceof Map))
@@ -2941,15 +2968,15 @@ function createSessionStatusRuntime(options = {}) {
2941
2968
  }
2942
2969
  return attempted ? { type: "unknown", source: "sdk-error" } : undefined;
2943
2970
  }
2944
- async function canFinalizeActiveRun(directory, client, sessionID, active, options2 = {}) {
2971
+ async function canFinalizeActiveRun(directory, client, sessionID, active, options = {}) {
2945
2972
  if (hasActiveToolCalls(sessionID) || hasBusyDescendant(sessionID))
2946
2973
  return false;
2947
- if (!options2.requireIdle && !options2.forceStale)
2974
+ if (!options.requireIdle && !options.forceStale)
2948
2975
  return true;
2949
- const completion = options2.forceStale ? await activeRunCompletionFromMessages2(directory, client, sessionID, active) : undefined;
2950
- if (completion === "completed")
2976
+ const completion = options.forceStale ? await activeRunCompletionFromMessages2(directory, client, sessionID, active) : undefined;
2977
+ if (settledAssistantCompletion(completion))
2951
2978
  return true;
2952
- if (!options2.requireIdle)
2979
+ if (!options.requireIdle)
2953
2980
  return completion === "unknown" && staleActiveRun(sessionID);
2954
2981
  const cached = sessionStatuses.get(sessionID);
2955
2982
  const seenAt = sessionStatusSeenAt.get(sessionID) || 0;
@@ -2961,11 +2988,11 @@ function createSessionStatusRuntime(options = {}) {
2961
2988
  return true;
2962
2989
  }
2963
2990
  if (live?.type) {
2964
- if (live.type === "busy" && options2.forceStale && completion === "unknown" && staleActiveRun(sessionID))
2991
+ if (live.type === "busy" && options.forceStale && completion === "unknown" && staleActiveRun(sessionID))
2965
2992
  return true;
2966
2993
  return false;
2967
2994
  }
2968
- if (options2.forceStale && completion === "unknown" && staleActiveRun(sessionID))
2995
+ if (options.forceStale && completion === "unknown" && staleActiveRun(sessionID))
2969
2996
  return true;
2970
2997
  return cachedIdleAfterRun;
2971
2998
  }
@@ -2977,7 +3004,7 @@ function createSessionStatusRuntime(options = {}) {
2977
3004
  if (!seenAt || now2() - seenAt < sessionStatusCacheMs)
2978
3005
  return false;
2979
3006
  const completion = await activeRunCompletionFromMessages2(directory, client, sessionID, { startedAt: 0 });
2980
- if (completion !== "completed")
3007
+ if (!settledAssistantCompletion(completion))
2981
3008
  return false;
2982
3009
  markSessionStatus(sessionID, "idle");
2983
3010
  await appendLoopLog2(directory, "status-message-idle-recovery", {
@@ -2988,7 +3015,7 @@ function createSessionStatusRuntime(options = {}) {
2988
3015
  });
2989
3016
  return true;
2990
3017
  }
2991
- async function sessionStatusType(client, sessionID, directory, options2 = {}) {
3018
+ async function sessionStatusType(client, sessionID, directory, options = {}) {
2992
3019
  if (hasActiveToolCalls(sessionID) || hasBusyDescendant(sessionID)) {
2993
3020
  markSessionStatus(sessionID, "busy");
2994
3021
  return "busy";
@@ -3008,11 +3035,11 @@ function createSessionStatusRuntime(options = {}) {
3008
3035
  if (live?.type) {
3009
3036
  if (await recoverCompletedTailWithoutActiveRun(directory, client, sessionID, live.type, seenAt))
3010
3037
  return "idle";
3011
- if ((live.type === "busy" || live.type === "retry") && options2.recoverStaleActive !== false) {
3038
+ if ((live.type === "busy" || live.type === "retry") && options.recoverStaleActive !== false) {
3012
3039
  const active = activeRuns.get(sessionID);
3013
3040
  if (active) {
3014
3041
  const completion = await activeRunCompletionFromMessages2(directory, client, sessionID, active);
3015
- if (completion === "completed" || live.type === "busy" && completion === "unknown" && staleActiveRun(sessionID)) {
3042
+ if (settledAssistantCompletion(completion) || live.type === "busy" && completion === "unknown" && staleActiveRun(sessionID)) {
3016
3043
  markSessionStatus(sessionID, "idle");
3017
3044
  const logDetails = {
3018
3045
  sessionID,
@@ -3020,7 +3047,8 @@ function createSessionStatusRuntime(options = {}) {
3020
3047
  startedAt: active.startedAt,
3021
3048
  ...completion === "completed" ? {} : { staleStatus: live.type }
3022
3049
  };
3023
- await appendLoopLog2(directory, completion === "completed" ? "status-message-complete-recovery" : "status-stale-recovery", logDetails);
3050
+ const recoveryEvent = completion === "empty" ? "status-message-empty-recovery" : completion === "completed" ? "status-message-complete-recovery" : "status-stale-recovery";
3051
+ await appendLoopLog2(directory, recoveryEvent, logDetails);
3024
3052
  return "idle";
3025
3053
  }
3026
3054
  }
@@ -3032,8 +3060,8 @@ function createSessionStatusRuntime(options = {}) {
3032
3060
  markSessionStatus(sessionID, fallback);
3033
3061
  return fallback;
3034
3062
  }
3035
- async function sessionIsIdle(client, sessionID, directory, options2 = {}) {
3036
- return await sessionStatusType(client, sessionID, directory, options2) === "idle";
3063
+ async function sessionIsIdle(client, sessionID, directory, options = {}) {
3064
+ return await sessionStatusType(client, sessionID, directory, options) === "idle";
3037
3065
  }
3038
3066
  return {
3039
3067
  markSessionStatus,
@@ -3042,6 +3070,7 @@ function createSessionStatusRuntime(options = {}) {
3042
3070
  staleActiveRun,
3043
3071
  canFinalizeActiveRun,
3044
3072
  readLiveSessionStatus,
3073
+ activeRunCompletion: activeRunCompletionFromMessages2,
3045
3074
  sessionStatusType,
3046
3075
  sessionIsIdle
3047
3076
  };
@@ -3218,8 +3247,8 @@ function createActionDispatcher(options = {}) {
3218
3247
  const shellBody = { command, agent };
3219
3248
  if (model)
3220
3249
  shellBody.model = model;
3221
- const dispatch2 = fireSdk2(client, "session.shell", client.session.shell.bind(client.session), { path: { id: sessionID }, body: shellBody }, { path: { sessionID }, body: shellBody }, { sessionID, ...shellBody });
3222
- return { startsAssistantTurn: true, dispatch: dispatch2 };
3250
+ const dispatch = fireSdk2(client, "session.shell", client.session.shell.bind(client.session), { path: { id: sessionID }, body: shellBody }, { path: { sessionID }, body: shellBody }, { sessionID, ...shellBody });
3251
+ return { startsAssistantTurn: true, dispatch };
3223
3252
  }
3224
3253
  const prompt = await buildPrompt(directory, job);
3225
3254
  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.";
@@ -3532,6 +3561,50 @@ function refundInfrastructureRun(job, snapshot = {}, input = {}) {
3532
3561
  return job;
3533
3562
  }
3534
3563
 
3564
+ // src/source/runtime/empty-turn.js
3565
+ var DEFAULT_MAX_EMPTY_TURNS = 2;
3566
+ function guardsEmptyAssistantTurn(job) {
3567
+ const kind = actionKind(job?.action, job || {});
3568
+ return kind === "prompt" || kind === "goal";
3569
+ }
3570
+ function emptyTurnLimit(job) {
3571
+ const configured = Number(job?.maxEmptyTurns || 0);
3572
+ if (Number.isFinite(configured) && configured > 0)
3573
+ return Math.max(1, Math.floor(configured));
3574
+ return DEFAULT_MAX_EMPTY_TURNS;
3575
+ }
3576
+ function refundEmptyAssistantTurn(job, active = {}, timestamp = Date.now()) {
3577
+ const chargedCount = Number(active?.job?.runCount ?? job?.runCount ?? 0);
3578
+ const currentCount = Number(job?.runCount || 0);
3579
+ if (chargedCount > 0 && currentCount >= chargedCount)
3580
+ job.runCount = Math.max(0, currentCount - 1);
3581
+ if (Number.isFinite(Number(active?.previousLastRunAt)))
3582
+ job.lastRunAt = Number(active.previousLastRunAt);
3583
+ if (active?.disabledByMaxRuns && Number(job?.maxRuns || 0) > 0 && Number(job?.runCount || 0) < Number(job.maxRuns)) {
3584
+ job.enabled = true;
3585
+ }
3586
+ job.emptyTurnCount = Number(job.emptyTurnCount || 0) + 1;
3587
+ job.lastEmptyTurnAt = Number(timestamp) || Date.now();
3588
+ job.lastFailureReason = "empty_turn";
3589
+ const limit = emptyTurnLimit(job);
3590
+ const paused = job.emptyTurnCount >= limit;
3591
+ if (paused) {
3592
+ job.paused = true;
3593
+ delete job.runNowRequestedAt;
3594
+ } else {
3595
+ job.runNowRequestedAt = Math.max(1, Number(timestamp) || Date.now());
3596
+ }
3597
+ return { job, paused, count: job.emptyTurnCount, limit };
3598
+ }
3599
+ function clearEmptyAssistantTurnStreak(job) {
3600
+ if (!job)
3601
+ return job;
3602
+ job.emptyTurnCount = 0;
3603
+ if (job.lastFailureReason === "empty_turn")
3604
+ delete job.lastFailureReason;
3605
+ return job;
3606
+ }
3607
+
3535
3608
  // src/source/runtime/executor.js
3536
3609
  var DEFAULT_ACTIVE_GUARD_MS = 45000;
3537
3610
  var DEFAULT_BUSY_RETRY_MS2 = 5000;
@@ -3585,6 +3658,7 @@ function createLoopExecutor(options = {}) {
3585
3658
  updateSessionStatusFromEvent,
3586
3659
  staleActiveRun,
3587
3660
  canFinalizeActiveRun,
3661
+ activeRunCompletion,
3588
3662
  sessionStatusType,
3589
3663
  sessionIsIdle,
3590
3664
  markSessionStatus,
@@ -3637,7 +3711,7 @@ function createLoopExecutor(options = {}) {
3637
3711
  toast: toast2,
3638
3712
  dangerousShell: dangerousShell2
3639
3713
  });
3640
- const dueJobs2 = admissionRuntime.dueJobs;
3714
+ const dueJobs = admissionRuntime.dueJobs;
3641
3715
  function clearActiveRun(sessionID) {
3642
3716
  const active = activeRuns.get(sessionID);
3643
3717
  if (active?.timer)
@@ -3760,6 +3834,7 @@ function createLoopExecutor(options = {}) {
3760
3834
  return;
3761
3835
  if (!await canFinalizeActiveRun(directory, client, sessionID, active, finalizeOptions))
3762
3836
  return false;
3837
+ const completion = await activeRunCompletion(directory, client, sessionID, active);
3763
3838
  const recoveredStale = staleActiveRun(sessionID);
3764
3839
  if (active.compactionOnly) {
3765
3840
  const pending = compactionRuntime.getPending(sessionID);
@@ -3780,6 +3855,30 @@ function createLoopExecutor(options = {}) {
3780
3855
  if (!job)
3781
3856
  return;
3782
3857
  job.lastFinishedAt = now2();
3858
+ if (completion === "empty" && guardsEmptyAssistantTurn(job)) {
3859
+ const empty = refundEmptyAssistantTurn(job, active, now2());
3860
+ state.jobs = (state.jobs || []).map((candidate) => candidate.id === job.id ? job : candidate);
3861
+ await writeState2(directory, sessionID, state);
3862
+ await appendLoopLog2(directory, "empty-assistant-turn", {
3863
+ sessionID,
3864
+ job: job.name || job.id,
3865
+ count: empty.count,
3866
+ limit: empty.limit,
3867
+ paused: empty.paused,
3868
+ refunded: true
3869
+ });
3870
+ if (empty.paused) {
3871
+ await notifyJob2(directory, job, "empty_turn");
3872
+ await toast2(client, "Loop paused after " + empty.count + " consecutive completed assistant turns with no visible output or tool activity. Resume after changing the model/prompt or use /loop-resume.", "warning");
3873
+ await scheduleDueWork(directory, client, sessionID);
3874
+ } else {
3875
+ await toast2(client, "Loop received an empty completed assistant turn; the logical run was refunded and will retry once.", "warning");
3876
+ await scheduleDueWork(directory, client, sessionID, busyRetryMs);
3877
+ }
3878
+ return true;
3879
+ }
3880
+ if (completion === "completed")
3881
+ clearEmptyAssistantTurnStreak(job);
3783
3882
  if (recoveredStale) {
3784
3883
  await appendLoopLog2(directory, "active-stale-recovery", {
3785
3884
  sessionID,
@@ -3835,7 +3934,7 @@ function createLoopExecutor(options = {}) {
3835
3934
  candidate.watchTriggered = true;
3836
3935
  }
3837
3936
  }
3838
- const due = dueJobs2(state, Boolean(runOptions.force));
3937
+ const due = dueJobs(state, Boolean(runOptions.force));
3839
3938
  if (!due.length) {
3840
3939
  await writeState2(directory, sessionID, state);
3841
3940
  await reschedule();
@@ -3964,7 +4063,7 @@ function createLoopExecutor(options = {}) {
3964
4063
  }
3965
4064
  }
3966
4065
  return {
3967
- dueJobs: dueJobs2,
4066
+ dueJobs,
3968
4067
  clearActiveRun,
3969
4068
  disposeSession,
3970
4069
  recoverActiveDispatchFailure,
@@ -4301,47 +4400,73 @@ function steeringToolRejection(sessionID) {
4301
4400
  output: "Goal lifecycle update deferred because queued user steering is pending. The experimental Goal remains active and unchanged."
4302
4401
  };
4303
4402
  }
4403
+ async function dedicatedGoalToolRejection(directory, sessionID) {
4404
+ const goal = await findDedicatedGoalForSession(directory, sessionID);
4405
+ if (!goal || goal.status === "completed")
4406
+ return;
4407
+ return {
4408
+ title: "Dedicated Goal owns lifecycle",
4409
+ output: [
4410
+ `A dedicated OpenCode Goal is persisted for this session (status=${String(goal.status || "unknown")}).`,
4411
+ "The opencode_loop_goal_* tools control only experimental /loop-goal jobs and will not mutate the dedicated Goal.",
4412
+ "Use opencode_goal_progress for checkpoints, opencode_goal_wait_for_user when user/manual input is required, opencode_goal_blocked for a genuine blocker, or opencode_goal_complete for verified completion."
4413
+ ].join(`
4414
+ `)
4415
+ };
4416
+ }
4304
4417
  function goalTools(defaultDirectory) {
4305
4418
  return {
4306
4419
  opencode_loop_goal_complete: tool({
4307
- description: "Mark the current OpenCode Loop experimental goal as completed. Use only after acceptance criteria are satisfied and you have evidence from tests, typecheck, build, or code inspection.",
4420
+ description: "Experimental /loop-goal ONLY: mark the current OpenCode Loop experimental goal as completed. Never use this tool to control a dedicated @bybrawe/opencode-goal Goal; use opencode_goal_complete there. Use only after acceptance criteria are satisfied and you have evidence from tests, typecheck, build, or code inspection.",
4308
4421
  args: {
4309
4422
  summary: tool.schema.string().describe("Short human-readable summary of what was completed."),
4310
4423
  evidence: tool.schema.string().describe("Concrete evidence that the goal is complete, such as commands run, passing checks, files changed, and important results.")
4311
4424
  },
4312
4425
  execute: async (args, context) => {
4426
+ const directory = context.directory || defaultDirectory;
4427
+ const dedicated = await dedicatedGoalToolRejection(directory, context.sessionID);
4428
+ if (dedicated)
4429
+ return dedicated;
4313
4430
  const steering = steeringToolRejection(context.sessionID);
4314
4431
  if (steering)
4315
4432
  return steering;
4316
- const result = await setGoalComplete(context.directory || defaultDirectory, context.sessionID, args);
4433
+ const result = await setGoalComplete(directory, context.sessionID, args);
4317
4434
  return { title: result.ok ? "Goal completed" : result.rejected ? "Goal completion rejected" : "Goal not found", output: result.message };
4318
4435
  }
4319
4436
  }),
4320
4437
  opencode_loop_goal_blocked: tool({
4321
- description: "Mark the current OpenCode Loop experimental goal as blocked when user input or manual intervention is required.",
4438
+ description: "Experimental /loop-goal ONLY: mark the current OpenCode Loop experimental goal as blocked. Never use this tool for a dedicated @bybrawe/opencode-goal Goal; use opencode_goal_wait_for_user for user/manual input or opencode_goal_blocked for a genuine dedicated-Goal blocker.",
4322
4439
  args: {
4323
4440
  reason: tool.schema.string().describe("Why the goal is blocked."),
4324
4441
  needed: tool.schema.string().describe("What user input, credential, decision, or manual action is needed to continue.")
4325
4442
  },
4326
4443
  execute: async (args, context) => {
4444
+ const directory = context.directory || defaultDirectory;
4445
+ const dedicated = await dedicatedGoalToolRejection(directory, context.sessionID);
4446
+ if (dedicated)
4447
+ return dedicated;
4327
4448
  const steering = steeringToolRejection(context.sessionID);
4328
4449
  if (steering)
4329
4450
  return steering;
4330
- const result = await setGoalBlocked(context.directory || defaultDirectory, context.sessionID, args);
4451
+ const result = await setGoalBlocked(directory, context.sessionID, args);
4331
4452
  return { title: result.ok ? "Goal blocked" : "Goal not found", output: result.message };
4332
4453
  }
4333
4454
  }),
4334
4455
  opencode_loop_goal_progress: tool({
4335
- description: "Record meaningful progress on the current OpenCode Loop experimental goal without completing it.",
4456
+ description: "Experimental /loop-goal ONLY: record meaningful progress on the current OpenCode Loop experimental goal. Never use this tool for a dedicated @bybrawe/opencode-goal Goal; use opencode_goal_progress there.",
4336
4457
  args: {
4337
4458
  summary: tool.schema.string().describe("What useful progress was made."),
4338
4459
  next: tool.schema.string().describe("The next step toward completing the goal.")
4339
4460
  },
4340
4461
  execute: async (args, context) => {
4462
+ const directory = context.directory || defaultDirectory;
4463
+ const dedicated = await dedicatedGoalToolRejection(directory, context.sessionID);
4464
+ if (dedicated)
4465
+ return dedicated;
4341
4466
  const steering = steeringToolRejection(context.sessionID);
4342
4467
  if (steering)
4343
4468
  return steering;
4344
- const result = await setGoalProgress(context.directory || defaultDirectory, context.sessionID, args);
4469
+ const result = await setGoalProgress(directory, context.sessionID, args);
4345
4470
  return { title: result.ok ? "Goal progress" : "Goal not found", output: result.message };
4346
4471
  }
4347
4472
  })
@@ -4408,8 +4533,8 @@ var DEFAULT_SESSION_STALE_MS = 12 * 60 * 60 * 1000;
4408
4533
  function sessionKey(sessionID) {
4409
4534
  return String(sessionID || "").trim();
4410
4535
  }
4411
- function createSessionRegistry({ now: now2 = Date.now, staleAfterMs = DEFAULT_SESSION_STALE_MS } = {}) {
4412
- if (typeof now2 !== "function")
4536
+ function createSessionRegistry({ now = Date.now, staleAfterMs = DEFAULT_SESSION_STALE_MS } = {}) {
4537
+ if (typeof now !== "function")
4413
4538
  throw new TypeError("session registry requires a clock function");
4414
4539
  if (!Number.isFinite(staleAfterMs) || staleAfterMs < 0)
4415
4540
  throw new TypeError("session registry requires a non-negative staleAfterMs");
@@ -4418,7 +4543,7 @@ function createSessionRegistry({ now: now2 = Date.now, staleAfterMs = DEFAULT_SE
4418
4543
  const key = sessionKey(sessionID);
4419
4544
  if (!key)
4420
4545
  throw new TypeError("session registry requires a session ID");
4421
- const seenAt = Number(now2());
4546
+ const seenAt = Number(now());
4422
4547
  if (!Number.isFinite(seenAt))
4423
4548
  throw new TypeError("session registry clock must return a finite number");
4424
4549
  const entry = Object.freeze({ sessionID: key, runtime, seenAt });
@@ -4437,7 +4562,7 @@ function createSessionRegistry({ now: now2 = Date.now, staleAfterMs = DEFAULT_SE
4437
4562
  return false;
4438
4563
  return sessions.delete(key);
4439
4564
  }
4440
- function pruneStale(at = now2()) {
4565
+ function pruneStale(at = now()) {
4441
4566
  const timestamp = Number(at);
4442
4567
  if (!Number.isFinite(timestamp))
4443
4568
  throw new TypeError("session registry prune time must be finite");
@@ -4521,7 +4646,7 @@ function createRuntimeScope() {
4521
4646
  }
4522
4647
 
4523
4648
  // src/source/runtime/timers.js
4524
- function createOwnedTimer(scope, callback, delay2, repeat, api, ref) {
4649
+ function createOwnedTimer(scope, callback, delay, repeat, api, ref) {
4525
4650
  if (!scope?.isActive?.())
4526
4651
  return;
4527
4652
  let active = true;
@@ -4536,7 +4661,7 @@ function createOwnedTimer(scope, callback, delay2, repeat, api, ref) {
4536
4661
  }
4537
4662
  callback(...args);
4538
4663
  });
4539
- handle = repeat ? api.setInterval(invoke, delay2) : api.setTimeout(invoke, delay2);
4664
+ handle = repeat ? api.setInterval(invoke, delay) : api.setTimeout(invoke, delay);
4540
4665
  release = scope.track(() => {
4541
4666
  if (repeat)
4542
4667
  api.clearInterval(handle);
@@ -4562,11 +4687,11 @@ function createOwnedTimer(scope, callback, delay2, repeat, api, ref) {
4562
4687
  }
4563
4688
  function createRuntimeTimers(scope, api = globalThis) {
4564
4689
  return Object.freeze({
4565
- timeout(callback, delay2, options = {}) {
4566
- return createOwnedTimer(scope, callback, delay2, false, api, options.ref !== false);
4690
+ timeout(callback, delay, options = {}) {
4691
+ return createOwnedTimer(scope, callback, delay, false, api, options.ref !== false);
4567
4692
  },
4568
- interval(callback, delay2, options = {}) {
4569
- return createOwnedTimer(scope, callback, delay2, true, api, options.ref !== false);
4693
+ interval(callback, delay, options = {}) {
4694
+ return createOwnedTimer(scope, callback, delay, true, api, options.ref !== false);
4570
4695
  }
4571
4696
  });
4572
4697
  }
@@ -4591,14 +4716,14 @@ function validateRuntime(runtime, sessionID) {
4591
4716
  return runtime;
4592
4717
  }
4593
4718
  function createSessionRuntimeManager({
4594
- now: now2 = Date.now,
4719
+ now = Date.now,
4595
4720
  staleAfterMs = DEFAULT_SESSION_STALE_MS,
4596
4721
  timerAPI = globalThis,
4597
4722
  runtimeFactory = defaultRuntimeFactory
4598
4723
  } = {}) {
4599
4724
  if (typeof runtimeFactory !== "function")
4600
4725
  throw new TypeError("session runtime manager requires a runtime factory");
4601
- const registry = createSessionRegistry({ now: now2, staleAfterMs });
4726
+ const registry = createSessionRegistry({ now, staleAfterMs });
4602
4727
  let disposed = false;
4603
4728
  function observeExternal(sessionID) {
4604
4729
  if (disposed)
@@ -4628,7 +4753,7 @@ function createSessionRuntimeManager({
4628
4753
  current.runtime.dispose(reason);
4629
4754
  return true;
4630
4755
  }
4631
- function pruneStale(at = now2()) {
4756
+ function pruneStale(at = now()) {
4632
4757
  const before = new Map(registry.entries().map((entry) => [entry.sessionID, entry.runtime]));
4633
4758
  const removed = registry.pruneStale(at);
4634
4759
  const errors = [];
@@ -4829,12 +4954,12 @@ function scopedClient(client) {
4829
4954
  });
4830
4955
  }
4831
4956
  function scheduleLegacyCleanup(dispose, isCurrentGeneration) {
4832
- for (const delay2 of [0, 25, 100, 500, 2000, 1e4, 30000]) {
4957
+ for (const delay of [0, 25, 100, 500, 2000, 1e4, 30000]) {
4833
4958
  const timer = setTimeout(() => {
4834
4959
  if (!isCurrentGeneration())
4835
4960
  return;
4836
4961
  Promise.resolve(dispose()).catch(() => {});
4837
- }, delay2);
4962
+ }, delay);
4838
4963
  timer.unref?.();
4839
4964
  }
4840
4965
  }