@inetafrica/open-claudia 3.4.22 → 3.4.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.4.24
4
+
5
+ - A repeating watch keeps polling while its triggered delivery waits for a busy room, waits to retry after a failed or `/stop`-killed run, or runs. A new hit folds into that one pending delivery instead of queueing another, and the delivered prompt says how many more times the check passed and whether its latest run had cleared. On 24 Sep the event-bus watch went unpolled for 7.5 h behind a busy room, and after a killed delivery attempt its polling never resumed. One-shot watches are unchanged, and the 15-minute "⏳ Watch … waiting" line still goes out once per occurrence.
6
+ - A watch whose check times out 3 times in a row now wakes the agent once, marked "Watch check timing out", and keeps polling; any completed check resets the count. A hanging health check used to be logged `idle (check timed out)` and never alarm. The alarm does not use up a one-shot watch.
7
+ - Wakeups more than ~24.8 days out no longer fire at once. `setTimeout` overflows past 2^31-1 ms, which fired a 2028 reminder 61 s after it was set (17 Sep); far wakeups now sleep in chunks and re-plan until due, then fire once. `schedule-wakeup` refuses a `<when>` more than 10 years out, in the CLI and in the loopback route.
8
+ - A queued delivery is no longer stranded when a jobs-store write throws (a full disk, a torn journal). Those paths returned without a retry timer, so the alarm was never tried again; they now keep the retry armed and say so in watch.log. This matches the watch alarms that triggered during long turns on 21 and 23 Sep and were never delivered. A regression test also pins that a delivery queued behind a long turn survives the turn's compaction and runs once at the next idle moment, on the compacted session; that path already worked.
9
+ - `cron-list` and `watch-list` say where a fired job stands (delivery waiting, running, retrying or dead) instead of listing it as still to come. A triggered watch whose delivery was queued read "EXPIRED (fires on next poll)" (21 Sep), and a fired 2028 reminder still read "fires 2028-08-01" (17 Sep).
10
+ - `open-claudia say` and the send-file/voice/photo routes now say which step failed when the channel adapter refuses the upload, instead of a bare `loopback 500: {"ok":false}` (10 Sep). The adapter's own error stays in bot.log.
11
+ - Release gate: `test-provider-pack-review.js` gives each probe child 120 s instead of 30 s and reports a timeout as a timeout rather than "failed (null)" (v3.4.19, job 43703). The parity suite's cap for it rises to 8 minutes so it cannot undercut its children.
12
+
13
+ ## 3.4.23
14
+
15
+ - Watch, wakeup and cron turns now stream their progress (interim narration, and thinking under `/verbose`) like chat turns. The live preview was gated to chat turns only, so a watch-woken turn showed the bell notice and "thinking…" and then nothing until its final answer, however long it worked.
16
+ - `/stop` no longer cancels the messages you send after it. It released the room the moment it killed the run, before that run had unwound, so a message sent in those seconds started, met the stop flag at its pre-spawn checkpoint and was cancelled "before the provider started" — three messages lost after one `/stop` on 24 Sep. The room now stays busy until the stopped run's own unwind releases it and drains the queue (a 30 s backstop covers a child that never reports its exit), and the stop flag names the run it stops, so it can never cancel another.
17
+ - "Queued." now says what the message is waiting on once the wait passes two minutes, e.g. `Queued behind the watch "Phone alarm" (running 42 min). /stop cancels it.`
18
+ - A watch that has triggered but waited 15 minutes behind a busy room posts one plain line per occurrence, so an alarm is never hidden for hours (24 Sep: 2 h and 7.5 h, with nothing in the chat).
19
+
3
20
  ## 3.4.22
4
21
 
5
22
  Supersedes 3.4.21, which was tagged but never published: its build failed because `test-upgrade-path.js` was wired into the test chain without being committed. Nothing shipped from that tag.
package/bin/schedule.js CHANGED
@@ -8,7 +8,8 @@ Background scheduling — manage wakeups and crons.
8
8
 
9
9
  Wakeups (one-shot, resumes the same conversation):
10
10
  open-claudia schedule-wakeup <when> "<prompt>" [--label "<label>"]
11
- <when>: duration (e.g. 30s, 5m, 2h, 1d) OR an ISO datetime (e.g. 2026-05-16T15:00:00Z)
11
+ <when>: duration (e.g. 30s, 5m, 2h, 1d) OR an ISO datetime (e.g. 2026-05-16T15:00:00Z),
12
+ at most 10 years out
12
13
 
13
14
  Crons (recurring):
14
15
  open-claudia cron-add "<5-field cron>" "<prompt>" [--label "<label>"] [--project <name>]
@@ -25,6 +26,8 @@ Watches (silent background monitor — polls a shell check, wakes you only on a
25
26
  datetime. Default 2h. A watch that reaches its timeout without
26
27
  the check ever passing FIRES anyway on the next poll, with the
27
28
  prompt marked "Watch expired" — no watch can end silently.
29
+ A check that times out (30s) 3 times in a row wakes the agent once, marked
30
+ "Watch check timing out"; the watch keeps polling.
28
31
  open-claudia watch-list # watches on this channel + master switch state
29
32
  open-claudia watch-remove <id> # remove a watch by id
30
33
  open-claudia watch-on / watch-off # global master switch for ALL watches (owner)
@@ -37,6 +40,10 @@ switching providers starts a fresh thread.
37
40
  `;
38
41
 
39
42
  const DUR = /^(\d+)\s*(s|sec|secs|m|min|mins|h|hr|hrs|d|day|days)$/i;
43
+ // Mirrors core/scheduler MAX_WAKEUP_HORIZON_MS: a wakeup further out than this
44
+ // is a typo or a mis-parsed <when> (one was set 1273 years out).
45
+ const MAX_WAKEUP_YEARS = 10;
46
+ const MAX_WAKEUP_HORIZON_MS = MAX_WAKEUP_YEARS * 365.25 * 24 * 60 * 60 * 1000;
40
47
 
41
48
  function parseWhen(s) {
42
49
  if (!s) return null;
@@ -63,13 +70,41 @@ function takeFlag(args, name) {
63
70
  return v;
64
71
  }
65
72
 
73
+ // Where a job stands once it has fired. A fired job used to stay listed as a
74
+ // future one ("fires 2028-08-01", "EXPIRED (fires on next poll)") while its
75
+ // delivery was queued, running or dead (17 and 21 Sep).
76
+ function jobProgress(j) {
77
+ const since = j.check && j.watchTriggeredOccurrenceId && j.watchTriggeredOccurrenceId === j.occurrenceId && Number(j.watchTriggeredAt) > 0
78
+ ? `TRIGGERED ${new Date(Number(j.watchTriggeredAt)).toISOString()}, ` : "";
79
+ const state = {
80
+ running: "delivery running",
81
+ deferred: "delivery waiting for a busy room",
82
+ failed: `delivery failed (${j.lastError?.code || "error"}), retrying`,
83
+ "dead-letter": `delivery failed (${j.lastError?.code || "error"}), DEAD: will not fire`,
84
+ }[j.status];
85
+ return state ? `${since}${state}` : "";
86
+ }
87
+
66
88
  function fmtJob(j) {
67
89
  const when = j.kind === "wakeup"
68
90
  ? `fires ${new Date(j.fireAt).toISOString()}`
69
91
  : `schedule "${j.schedule}"`;
70
92
  const proj = j.project ? ` [${j.project}]` : "";
71
93
  const src = j.source ? ` (${j.source})` : "";
72
- return `${j.id} ${j.kind} ${j.label || "(no label)"}${proj}${src} ${when}`;
94
+ const progress = jobProgress(j);
95
+ return `${j.id} ${j.kind} ${j.label || "(no label)"}${proj}${src} ${when}${progress ? ` [${progress}]` : ""}`;
96
+ }
97
+
98
+ function fmtWatch(w, now = Date.now()) {
99
+ const mode = w.repeat ? "repeating" : "one-shot";
100
+ const progress = jobProgress(w);
101
+ const expiry = progress ? ` ${progress}`
102
+ : Number(w.expiresAt) > 0
103
+ ? (now >= Number(w.expiresAt)
104
+ ? " EXPIRED (fires on next poll)"
105
+ : ` expires ${new Date(Number(w.expiresAt)).toISOString()}`)
106
+ : " no expiry (pre-timeout watch)";
107
+ return `${w.id} ${mode} polls "${w.schedule}"${expiry} check: ${w.check} → ${w.label || "(no label)"}`;
73
108
  }
74
109
 
75
110
  async function runScheduleWakeup(args) {
@@ -82,6 +117,11 @@ async function runScheduleWakeup(args) {
82
117
  console.error(`Could not parse <when>: "${rest[0]}". Use a duration (5m, 2h, 1d) or ISO datetime.`);
83
118
  process.exit(2);
84
119
  }
120
+ if (when - Date.now() > MAX_WAKEUP_HORIZON_MS) {
121
+ const at = new Date(when);
122
+ console.error(`<when> "${rest[0]}" is ${Number.isNaN(at.getTime()) ? "past any valid date" : at.toISOString()}, more than ${MAX_WAKEUP_YEARS} years out. Check the date; for a far reminder use a yearly cron-add.`);
123
+ process.exit(2);
124
+ }
85
125
  const prompt = rest.slice(1).join(" ");
86
126
  if (!prompt) { console.error("Missing prompt."); process.exit(2); }
87
127
  try {
@@ -178,15 +218,7 @@ async function runWatchList() {
178
218
  const list = res.watches || [];
179
219
  console.log(`Watches: master switch is ${res.enabled ? "ON" : "OFF"}.`);
180
220
  if (list.length === 0) { console.log("No watches on this channel."); process.exit(0); }
181
- for (const w of list) {
182
- const mode = w.repeat ? "repeating" : "one-shot";
183
- const expiry = Number(w.expiresAt) > 0
184
- ? (Date.now() >= Number(w.expiresAt)
185
- ? " EXPIRED (fires on next poll)"
186
- : ` expires ${new Date(Number(w.expiresAt)).toISOString()}`)
187
- : " no expiry (pre-timeout watch)";
188
- console.log(`${w.id} ${mode} polls "${w.schedule}"${expiry} check: ${w.check} → ${w.label || "(no label)"}`);
189
- }
221
+ for (const w of list) console.log(fmtWatch(w));
190
222
  process.exit(0);
191
223
  } catch (e) { console.error(e.message); process.exit(1); }
192
224
  }
@@ -211,6 +243,8 @@ async function runWatchToggle(enabled) {
211
243
 
212
244
  module.exports = {
213
245
  HELP,
246
+ fmtJob,
247
+ fmtWatch,
214
248
  runScheduleWakeup,
215
249
  runCronAdd,
216
250
  runCronList,
package/core/handlers.js CHANGED
@@ -20,7 +20,7 @@ const {
20
20
  getProviderSettings, resolveProviderSettings,
21
21
  getProjectSessions, getLastProjectSession, userOwnsProviderSession,
22
22
  createSessionCallbackToken, linkIdentity,
23
- roomSettingsFor, setRoomSetting,
23
+ roomSettingsFor, setRoomSetting, cancelFlagFor,
24
24
  } = require("./state");
25
25
  const { canonicalForChannel, canonicalForTelegram, normalizeCanonicalUserId, channelKey, identities, isConfiguredOwnerChannel, removeIdentityMapping } = require("./identity");
26
26
  const { isChatAuthorized, isChatOwner, recordPendingAuthRequest, authRequestLabel, hasOwner, bootstrapOwner } = require("./access");
@@ -1642,6 +1642,17 @@ register({
1642
1642
  },
1643
1643
  });
1644
1644
 
1645
+ // How long /stop waits for a stopped child to report its exit before it
1646
+ // releases the room anyway (SIGKILL follows SIGTERM after 3 s).
1647
+ const STOP_RELEASE_MS = 30 * 1000;
1648
+ function releaseStuckStop(state, proc, stopped) {
1649
+ if (state.runningProcess !== proc) return;
1650
+ if (proc.exitCode != null || proc.signalCode != null) return; // exited: its unwind releases the room
1651
+ console.warn(`[stop] child ${proc.pid} never reported its exit; releasing the room`);
1652
+ state.runningProcess = null;
1653
+ if (state.cancelRequested === stopped) state.cancelRequested = false;
1654
+ }
1655
+
1645
1656
  register({
1646
1657
  name: "stop", description: "Cancel running task",
1647
1658
  handler: async (env) => {
@@ -1655,14 +1666,23 @@ register({
1655
1666
  return n ? ` ${n} queued message${n === 1 ? "" : "s"} will run next.` : "";
1656
1667
  };
1657
1668
  if (state.runningProcess) {
1658
- const pid = state.runningProcess.pid;
1669
+ const proc = state.runningProcess;
1670
+ const pid = proc.pid;
1659
1671
  const { killProcessTree } = require("./process-tree");
1660
1672
  // Quiet unwind: deliverForegroundResult consumes this flag to suppress
1661
- // the "run failed" diagnostic the SIGTERM exit would otherwise emit.
1662
- state.cancelRequested = true;
1673
+ // the "run failed" diagnostic the SIGTERM exit would otherwise emit. It
1674
+ // names the run it stops, so it can never cancel a different one.
1675
+ const stopped = cancelFlagFor(state);
1676
+ state.cancelRequested = stopped;
1663
1677
  killProcessTree(pid, "SIGTERM");
1664
1678
  setTimeout(() => killProcessTree(pid, "SIGKILL"), 3000);
1665
- state.runningProcess = null;
1679
+ // The room stays busy until the stopped run has unwound: its own
1680
+ // finally releases the lock, clears this flag and drains the queue.
1681
+ // Releasing it here let a message sent in those seconds start, meet the
1682
+ // flag and be cancelled "before the provider started" (24 Sep: three
1683
+ // messages lost after one /stop). Backstop: a child that never reports
1684
+ // its exit must not pin the room.
1685
+ setTimeout(() => releaseStuckStop(state, proc, stopped), STOP_RELEASE_MS).unref?.();
1666
1686
  // Stop pending preview flushes but keep statusMessageId/Channel: the
1667
1687
  // killed run's delivery unwind edits the preview to drop its footer.
1668
1688
  if (state.streamInterval) { clearTimeout(state.streamInterval); state.streamInterval = null; }
@@ -1675,7 +1695,7 @@ register({
1675
1695
  } else if (state.preparingRun) {
1676
1696
  // Turn is mid-recall/prompt-build — no process to kill yet. Flag it so
1677
1697
  // the runner's pre-spawn shouldAbort checkpoint bails before spawning.
1678
- state.cancelRequested = true;
1698
+ state.cancelRequested = cancelFlagFor(state);
1679
1699
  if (state.typingHeartbeat) { clearInterval(state.typingHeartbeat); state.typingHeartbeat = null; state.thinkingPhase = false; state.typingTick = null; }
1680
1700
  try { currentAdapter()?.typingStop?.(currentChannelId()); } catch (_) {}
1681
1701
  await sideCancellation;
package/core/loopback.js CHANGED
@@ -58,6 +58,14 @@ function reply(res, code, body) {
58
58
  res.end(data);
59
59
  }
60
60
 
61
+ // An adapter's send* resolves false after logging its own error, so the JSON
62
+ // used to say only {"ok":false} (10 Sep: every `say` failed like that through
63
+ // a Kazee outage). Name the step that failed; the adapter's message may carry
64
+ // request details, so it stays in bot.log.
65
+ function undeliveredReason(adapter, what) {
66
+ return `${adapter.type || adapter.id} adapter did not deliver the ${what}: the channel rejected it or was unreachable (details in bot.log)`;
67
+ }
68
+
61
69
  function authOk(req) {
62
70
  return (req.headers["authorization"] || "") === `Bearer ${token}`;
63
71
  }
@@ -154,7 +162,7 @@ async function handleSend(req, res, url, kind) {
154
162
  } finally {
155
163
  if (kind !== "send-voice") { try { fs.unlinkSync(tmp); } catch (e) {} }
156
164
  }
157
- return reply(res, ok ? 200 : 500, { ok });
165
+ return ok ? reply(res, 200, { ok }) : reply(res, 500, { ok, error: undeliveredReason(adapter, kind === "send-voice" ? "voice note" : kind.slice(5)) });
158
166
  }
159
167
 
160
168
  async function handleJson(req, res, url, kind) {
@@ -215,7 +223,7 @@ async function handleJson(req, res, url, kind) {
215
223
  return reply(res, 500, { error: `voice send failed: ${e.message}` });
216
224
  }
217
225
  try { fs.unlinkSync(voicePath); } catch (e) {}
218
- return reply(res, ok ? 200 : 500, { ok });
226
+ return ok ? reply(res, 200, { ok }) : reply(res, 500, { ok, error: undeliveredReason(adapter, "voice note") });
219
227
  }
220
228
 
221
229
  if (kind === "tts") {
@@ -271,6 +279,9 @@ async function handleJson(req, res, url, kind) {
271
279
  if (kind === "schedule-wakeup") {
272
280
  const fireAt = Number(payload.fireAt);
273
281
  if (!Number.isFinite(fireAt) || fireAt <= 0) return reply(res, 400, { error: "missing/invalid fireAt" });
282
+ if (fireAt - Date.now() > scheduler.MAX_WAKEUP_HORIZON_MS) {
283
+ return reply(res, 400, { error: "fireAt is more than 10 years out; check the date (a far reminder belongs in a yearly cron)" });
284
+ }
274
285
  if (!payload.prompt) return reply(res, 400, { error: "missing prompt" });
275
286
  const job = scheduler.addJob({
276
287
  kind: "wakeup",
package/core/runner.js CHANGED
@@ -13,7 +13,7 @@ const {
13
13
  } = require("./config");
14
14
  const {
15
15
  currentState, saveState, recordSession,
16
- resetSessionUsage, acquireRunLock, getActiveProvider, getProjectKey,
16
+ resetSessionUsage, acquireRunLock, cancelTargets, getActiveProvider, getProjectKey,
17
17
  getProviderSession, setProviderSession, clearProviderSession, quarantineProviderSession, getUsageForSession, commitProviderCompaction,
18
18
  resolveProviderSettings,
19
19
  } = require("./state");
@@ -1698,9 +1698,10 @@ async function deliverForegroundResult(result, runContext, state = currentState(
1698
1698
  // /stop unwind: the SIGTERM (or pre-spawn abort) that follows a user cancel
1699
1699
  // is not a failure worth announcing — /stop already replied "Cancelled.".
1700
1700
  // The flag is consumed here (and re-cleared on the run's unwind) so it can
1701
- // never suppress a later run's genuine failure.
1702
- if (!result.ok && (state.cancelRequested || result.error?.code === "RUN_CANCELLED")) {
1703
- state.cancelRequested = false;
1701
+ // never suppress a later run's genuine failure; it names the run it stopped.
1702
+ const stoppedHere = cancelTargets(state.cancelRequested, runContext);
1703
+ if (!result.ok && (stoppedHere || result.error?.code === "RUN_CANCELLED")) {
1704
+ if (stoppedHere) state.cancelRequested = false;
1704
1705
  return { ok: true, suppressed: "cancelled" };
1705
1706
  }
1706
1707
  const diagnostic = providerRunDiagnostic(result, runContext);
@@ -1871,6 +1872,18 @@ function settleImmediateEffects(result, runContext, { state, store, capture }) {
1871
1872
  const STREAM_PREVIEW_THROTTLE_MS = 2500;
1872
1873
  const STREAM_PREVIEW_FOOTER = "\n\n⏳ working — /stop to cancel";
1873
1874
 
1875
+ // Runs whose interim narration (and, under /verbose, thinking) streams into
1876
+ // the chat: the person's own turns, and scheduled wake-ups (watches, wakeups,
1877
+ // crons). A scheduled run answers in the same room and settles through
1878
+ // deliverForegroundResult, which cleans the preview up exactly as for a
1879
+ // foreground turn; without it a watch-woken turn showed only "thinking..."
1880
+ // until the final answer. Background runs (dream, review, recall, learn,
1881
+ // compaction, subagents, side chats, silent runs) stay silent.
1882
+ const STREAM_PREVIEW_PURPOSES = new Set(["foreground", "scheduled"]);
1883
+ function streamPreviewAllowed(runContext) {
1884
+ return STREAM_PREVIEW_PURPOSES.has(runContext?.purpose);
1885
+ }
1886
+
1874
1887
  // Edited-in-place interim preview: each completed text block the provider
1875
1888
  // emits mid-run is appended to one throttled status message, so the user sees
1876
1889
  // the narration as it happens instead of silence. finalizeStreamPreview
@@ -1911,7 +1924,7 @@ function createStreamPreview({ state, store }) {
1911
1924
  };
1912
1925
 
1913
1926
  const appendChunk = (chunk, runContext) => {
1914
- if (!enabled || runContext?.purpose !== "foreground" || state.cancelRequested) return;
1927
+ if (!enabled || !streamPreviewAllowed(runContext) || state.cancelRequested) return;
1915
1928
  state.streamBuffer = state.streamBuffer ? `${state.streamBuffer}\n\n${chunk}` : chunk;
1916
1929
  if (state.streamBuffer.length > 20000) state.streamBuffer = state.streamBuffer.slice(-16000);
1917
1930
  if (!state.streamInterval) state.streamInterval = setTimeout(flush, STREAM_PREVIEW_THROTTLE_MS);
@@ -1983,7 +1996,7 @@ function createForegroundService({ state, store, stopTyping, capture, ownsAdmiss
1983
1996
  },
1984
1997
  shouldAbort(runContext, { capture: isCapture } = {}) {
1985
1998
  if (isCapture || !ownsAdmissionLock) return false;
1986
- if (!state.cancelRequested) return false;
1999
+ if (!cancelTargets(state.cancelRequested, runContext)) return false;
1987
2000
  state.cancelRequested = false;
1988
2001
  return true;
1989
2002
  },
@@ -2197,7 +2210,7 @@ async function executeAdmittedRun(runContext, state, store, capture, ownsAdmissi
2197
2210
  // A cancel flag that survived this run (e.g. /stop raced a run that
2198
2211
  // finished successfully anyway) must not leak into the next turn, where
2199
2212
  // it would falsely abort a queued run or mute a genuine failure.
2200
- if (ownsAdmissionLock) state.cancelRequested = false;
2213
+ if (ownsAdmissionLock && cancelTargets(state.cancelRequested, runContext)) state.cancelRequested = false;
2201
2214
  if (tracksMainRun && state.activeRunContext === runContext) state.activeRunContext = null;
2202
2215
  stopTyping();
2203
2216
  }
@@ -2265,6 +2278,21 @@ function admitRunContext(prompt, cwd, replyToMsgId, opts = {}) {
2265
2278
  });
2266
2279
  }
2267
2280
 
2281
+ // What a message queued behind a long run is waiting for. A short wait is just
2282
+ // "Queued."; after two minutes it says what is running and how to stop it, so a
2283
+ // busy room never looks like a dead one (24 Sep: an hour behind a watch run).
2284
+ const QUEUED_DETAIL_AFTER_MS = 2 * 60 * 1000;
2285
+ function queuedNotice(activeRunContext, at = Date.now()) {
2286
+ const since = Number(activeRunContext?.admittedAt);
2287
+ if (!Number.isFinite(since) || at - since < QUEUED_DETAIL_AFTER_MS) return "Queued.";
2288
+ const minutes = Math.floor((at - since) / 60000);
2289
+ const header = String(activeRunContext.userPrompt || "").match(/^\[(Watch triggered|Watch expired|Watch check timing out|Scheduled cron|Scheduled wakeup) "([^"]*)"/);
2290
+ const kind = header ? { "Watch triggered": "watch", "Watch expired": "watch", "Watch check timing out": "watch", "Scheduled cron": "scheduled job", "Scheduled wakeup": "wakeup" }[header[1]] : null;
2291
+ const label = header && header[2].length > 60 ? `${header[2].slice(0, 59).trimEnd()}…` : header?.[2];
2292
+ const what = kind ? `the ${kind} "${label}"` : "an earlier message";
2293
+ return `Queued behind ${what} (running ${minutes} min). /stop cancels it.`;
2294
+ }
2295
+
2268
2296
  function runAgent(prompt, cwd, replyToMsgId, opts = {}) {
2269
2297
  const state = currentState();
2270
2298
  const store = chatContext.getStore() || null;
@@ -2294,7 +2322,7 @@ function runAgent(prompt, cwd, replyToMsgId, opts = {}) {
2294
2322
  // task — noise. On a live call, send() is the TTS line: "Queued." would be
2295
2323
  // spoken to the caller. Both stay quiet and just deliver the real reply.
2296
2324
  if (currentAdapter()?.type !== "spaces" && !statusChatterMuted()) {
2297
- send(state.isCompacting ? "Compacting context, will pick this up next…" : "Queued.", { replyTo: replyToMsgId }).catch(() => {});
2325
+ send(state.isCompacting ? "Compacting context, will pick this up next…" : queuedNotice(state.activeRunContext), { replyTo: replyToMsgId }).catch(() => {});
2298
2326
  }
2299
2327
  return deferred.promise;
2300
2328
  }
@@ -2389,7 +2417,9 @@ function runAgentSilent(prompt, cwd, label, opts = {}) {
2389
2417
  }
2390
2418
 
2391
2419
  module.exports = {
2420
+ queuedNotice,
2392
2421
  admitRunContext,
2422
+ streamPreviewAllowed,
2393
2423
  capturedTranscriptMetadata,
2394
2424
  decorateForegroundInvocation,
2395
2425
  createRunnerService,