switchroom 0.18.19 → 0.18.20

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.
Files changed (47) hide show
  1. package/dist/cli/ms-365-write-pretool.mjs +92 -20
  2. package/dist/cli/switchroom.js +36 -6
  3. package/dist/host-control/main.js +1 -1
  4. package/package.json +1 -1
  5. package/telegram-plugin/answer-ready-flush.ts +187 -0
  6. package/telegram-plugin/dist/gateway/gateway.js +1073 -182
  7. package/telegram-plugin/format.ts +179 -20
  8. package/telegram-plugin/gateway/cron-session.ts +32 -0
  9. package/telegram-plugin/gateway/gateway.ts +775 -105
  10. package/telegram-plugin/gateway/idle-clear.ts +170 -0
  11. package/telegram-plugin/gateway/inject-handler.ts +11 -0
  12. package/telegram-plugin/gateway/outbound-send-path.ts +9 -9
  13. package/telegram-plugin/gateway/turn-record-status.ts +134 -0
  14. package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +23 -0
  15. package/telegram-plugin/hooks/silent-end-scan.mjs +98 -8
  16. package/telegram-plugin/narrative-flush.ts +181 -0
  17. package/telegram-plugin/pending-work-progress.ts +65 -1
  18. package/telegram-plugin/session-tail.ts +6 -1
  19. package/telegram-plugin/silent-end.ts +182 -0
  20. package/telegram-plugin/stream-reply-handler.ts +14 -5
  21. package/telegram-plugin/subagent-watcher.ts +244 -81
  22. package/telegram-plugin/tests/answer-ready-flush.test.ts +343 -0
  23. package/telegram-plugin/tests/cron-inject-idle-clock.test.ts +54 -0
  24. package/telegram-plugin/tests/emission-authority-facade.test.ts +13 -10
  25. package/telegram-plugin/tests/format-consistency.test.ts +54 -34
  26. package/telegram-plugin/tests/formatting-parse-regression.test.ts +6 -5
  27. package/telegram-plugin/tests/formatting-torture-set.ts +1 -1
  28. package/telegram-plugin/tests/idle-clear.test.ts +315 -37
  29. package/telegram-plugin/tests/narrative-flush.test.ts +213 -0
  30. package/telegram-plugin/tests/narrative-splice-before-finalize.test.ts +167 -0
  31. package/telegram-plugin/tests/outbound-send-path.test.ts +5 -4
  32. package/telegram-plugin/tests/paragraph-normalizer.test.ts +100 -42
  33. package/telegram-plugin/tests/paragraph-spacer-golden.test.ts +150 -0
  34. package/telegram-plugin/tests/per-topic-current-turn.test.ts +4 -1
  35. package/telegram-plugin/tests/silent-end-interrupt-stop-scan.test.ts +194 -0
  36. package/telegram-plugin/tests/silent-end.test.ts +296 -0
  37. package/telegram-plugin/tests/stream-reply-handler.test.ts +12 -9
  38. package/telegram-plugin/tests/subagent-watcher-narrative-early-paint.test.ts +218 -0
  39. package/telegram-plugin/tests/telegram-format.test.ts +36 -23
  40. package/telegram-plugin/tests/turn-flush-safety.test.ts +21 -17
  41. package/telegram-plugin/tests/turn-record-status.test.ts +119 -0
  42. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +218 -1
  43. package/telegram-plugin/tests/worker-feed-terminal-cleanup.test.ts +254 -0
  44. package/telegram-plugin/tests/worker-feed-terminal-state-truthful.test.ts +125 -0
  45. package/telegram-plugin/tool-activity-summary.ts +78 -16
  46. package/telegram-plugin/turn-flush-safety.ts +4 -4
  47. package/telegram-plugin/worker-activity-feed.ts +181 -30
@@ -6824,6 +6824,85 @@ function hardenCardBreaks(text) {
6824
6824
  }
6825
6825
  return restore(pieces.join(""));
6826
6826
  }
6827
+ function addParagraphSpacers(text) {
6828
+ if (!text.includes(`
6829
+
6830
+ `))
6831
+ return text;
6832
+ const nonce = Math.random().toString(36).slice(2);
6833
+ const { masked, restore, placeholder } = maskCodeRegions(text, nonce);
6834
+ if (!masked.includes(`
6835
+
6836
+ `))
6837
+ return restore(masked);
6838
+ const SP = PARAGRAPH_SPACER;
6839
+ const asciiTrim = (line) => line.replace(/^[ \t\r\f\v]+|[ \t\r\f\v]+$/g, "");
6840
+ const isBlankish = (line) => {
6841
+ const t = asciiTrim(line);
6842
+ return t === "" || t === SP;
6843
+ };
6844
+ const blockKind = (line) => {
6845
+ if (isFenceOpenLine(line, placeholder))
6846
+ return "fence";
6847
+ if (isListItemLine(line))
6848
+ return "list";
6849
+ if (isTableRowLine(line) || isTableDelimiterLine(line))
6850
+ return "table";
6851
+ if (isBlockquoteLine(line))
6852
+ return "quote";
6853
+ if (isHeadingLine(line))
6854
+ return "heading";
6855
+ if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trimStart()))
6856
+ return "divider";
6857
+ return "prose";
6858
+ };
6859
+ const SAME_KIND_TIGHT = new Set([
6860
+ "list",
6861
+ "table",
6862
+ "quote",
6863
+ "fence",
6864
+ "divider"
6865
+ ]);
6866
+ const shouldSpaceGap = (above, below) => {
6867
+ const a = blockKind(above);
6868
+ const b = blockKind(below);
6869
+ if (a === b && SAME_KIND_TIGHT.has(a))
6870
+ return false;
6871
+ return true;
6872
+ };
6873
+ const toks = [];
6874
+ for (const line of masked.split(`
6875
+ `)) {
6876
+ const kind = isBlankish(line) ? "gap" : "content";
6877
+ const last = toks[toks.length - 1];
6878
+ if (last && last.kind === kind)
6879
+ last.lines.push(line);
6880
+ else
6881
+ toks.push({ kind, lines: [line] });
6882
+ }
6883
+ const out = [];
6884
+ for (let i = 0;i < toks.length; i++) {
6885
+ const tok = toks[i];
6886
+ if (tok.kind === "content") {
6887
+ out.push(...tok.lines);
6888
+ continue;
6889
+ }
6890
+ const prev = toks[i - 1];
6891
+ const next = toks[i + 1];
6892
+ if (prev?.kind === "content" && next?.kind === "content") {
6893
+ const above = prev.lines[prev.lines.length - 1];
6894
+ const below = next.lines[0];
6895
+ if (shouldSpaceGap(above, below))
6896
+ out.push("", SP, "");
6897
+ else
6898
+ out.push("");
6899
+ } else {
6900
+ out.push(...tok.lines);
6901
+ }
6902
+ }
6903
+ return restore(out.join(`
6904
+ `));
6905
+ }
6827
6906
  function normalizePunctuation(text) {
6828
6907
  if (!/[\u2014\u2013\u2022\u00b7]/.test(text))
6829
6908
  return text;
@@ -7051,10 +7130,11 @@ function splitMarkdownChunks(text, maxLen = RICH_MESSAGE_MAX_CHARS) {
7051
7130
  return chunks.map((c) => stripBoundarySpacers(c, "trailing"));
7052
7131
  }
7053
7132
  function stripBoundarySpacers(chunk, side) {
7133
+ const sp = PARAGRAPH_SPACER;
7054
7134
  if (side === "leading") {
7055
- return chunk.replace(/^(?:[ \t]*\n)+/, "");
7135
+ return chunk.replace(new RegExp(`^(?:[ \\t]*${sp}?[ \\t]*\\n+)+`), "");
7056
7136
  }
7057
- return chunk.replace(/(?:\n[ \t]*)+$/, "");
7137
+ return chunk.replace(new RegExp(`(?:\\n+[ \\t]*${sp}?[ \\t]*)+$`), "");
7058
7138
  }
7059
7139
  function backOffOpenFence(text, cut) {
7060
7140
  if (cut <= 0 || cut >= text.length)
@@ -7103,7 +7183,7 @@ function backOffOpenInline(text, cut) {
7103
7183
  }
7104
7184
  return earliest;
7105
7185
  }
7106
- var RICH_MESSAGE_MAX_CHARS = 32768, INLINE_SPAN_PATTERNS;
7186
+ var RICH_MESSAGE_MAX_CHARS = 32768, PARAGRAPH_SPACER = "\u00a0", INLINE_SPAN_PATTERNS;
7107
7187
  var init_format = __esm(() => {
7108
7188
  INLINE_SPAN_PATTERNS = [
7109
7189
  /`[^`\n]+`/g,
@@ -40217,27 +40297,43 @@ function renderActivityFeedWithNested(lines, childLines, final = false, liveSuff
40217
40297
  });
40218
40298
  }
40219
40299
  var COMBINED_ROW_DESC_MAX = 72;
40300
+ var MAX_COMBINED_BODY_LINES = 13;
40301
+ var PER_WORKER_HEADER_COST = 1;
40302
+ function combinedHistoryDepth(w) {
40303
+ if (w <= 0)
40304
+ return 1;
40305
+ const raw = Math.floor((MAX_COMBINED_BODY_LINES - PER_WORKER_HEADER_COST * w) / w);
40306
+ return Math.max(1, Math.min(STATUS_ROLLING_LINES, raw));
40307
+ }
40220
40308
  function renderCombinedWorkerFeed(rows, opts) {
40221
40309
  if (rows.length === 0)
40222
40310
  return null;
40223
40311
  const maxRows = Math.max(1, Math.floor(opts.maxRows));
40224
- const rowLines = (r) => {
40312
+ const rowHeader = (r) => {
40225
40313
  const desc = escapeMarkdown(truncate(stripMarkdown(r.description).replace(/\s+/g, " ").trim() || "background task", COMBINED_ROW_DESC_MAX));
40226
40314
  const toolWord = r.toolCount === 1 ? "tool" : "tools";
40227
40315
  const modelLabel = formatModelLabel(r.model);
40228
40316
  const modelPart = modelLabel != null ? ` \u00b7 ${escapeMarkdown(modelLabel)}` : "";
40229
- const header = `**${desc}** _\u00b7 ${formatFeedElapsed(r.elapsedMs)} \u00b7 ${r.toolCount} ${toolWord}${modelPart}_`;
40230
- const stepClean = stripMarkdown(r.currentStep).replace(/\s+/g, " ").trim();
40231
- const step = stepClean.length > 0 ? `\u2192 _${escapeMarkdown(truncate(stepClean, STATUS_LINE_MAX))}_` : `\u2192 _starting\u2026_`;
40232
- return [header, step];
40317
+ return `**${desc}** _\u00b7 ${formatFeedElapsed(r.elapsedMs)} \u00b7 ${r.toolCount} ${toolWord}${modelPart}_`;
40318
+ };
40319
+ const rowHistory = (r) => {
40320
+ const src = r.historyLines != null && r.historyLines.length > 0 ? r.historyLines : [r.currentStep];
40321
+ return src.filter((s) => s != null && stripMarkdown(s).replace(/\s+/g, " ").trim().length > 0);
40233
40322
  };
40234
40323
  const compose = (visibleCount) => {
40235
40324
  const shown = rows.slice(0, visibleCount);
40236
40325
  const hidden = rows.length - shown.length;
40326
+ const depth = combinedHistoryDepth(shown.length);
40237
40327
  const out = [`\uD83D\uDEE0 **Workers** \u00b7 _${rows.length} running_`];
40238
40328
  for (const r of shown) {
40239
- const [h, s] = rowLines(r);
40240
- out.push(h, s);
40329
+ out.push(rowHeader(r));
40330
+ const hist = rowHistory(r);
40331
+ if (hist.length === 0) {
40332
+ out.push("\u2192 _starting\u2026_");
40333
+ continue;
40334
+ }
40335
+ const esc = hist.slice(-depth).map(escapeStepLine);
40336
+ renderStepFeed(out, esc, false);
40241
40337
  }
40242
40338
  if (hidden > 0)
40243
40339
  out.push(`_+${hidden} more working\u2026_`);
@@ -40314,7 +40410,7 @@ function isWorkerActivityFeedEnabled(envVal) {
40314
40410
  var DESC_MAX = 80;
40315
40411
  function renderWorkerActivity(v, liveSuffix = "") {
40316
40412
  const desc = truncate(stripMarkdown(v.description).trim() || "background task", DESC_MAX);
40317
- const finished = v.state === "done" || v.state === "failed";
40413
+ const finished = v.state === "done" || v.state === "failed" || v.state === "incomplete";
40318
40414
  const rawSteps = (v.narrativeLines ?? []).filter((s) => s != null && s.trim().length > 0);
40319
40415
  let steps = rawSteps;
40320
40416
  if (steps.length === 0 && !finished) {
@@ -40332,7 +40428,7 @@ function renderWorkerActivity(v, liveSuffix = "") {
40332
40428
  model: v.model
40333
40429
  };
40334
40430
  let result;
40335
- if (finished) {
40431
+ if (finished && v.state !== "incomplete") {
40336
40432
  const text = cleanWorkerResultParagraph(v.latestSummary);
40337
40433
  if (text.length > 0)
40338
40434
  result = { emoji: v.state === "done" ? "\u2705" : "\u26a0\ufe0f", text };
@@ -40386,6 +40482,7 @@ function createWorkerActivityFeed(opts) {
40386
40482
  const firstPaintMin = opts.firstPaintMinMs ?? 8000;
40387
40483
  const heartbeatTickMs = opts.heartbeatTickMs ?? 6000;
40388
40484
  const maxRows = Math.max(1, Math.floor(opts.maxRows ?? 8));
40485
+ const staleWorkerTtlMs = Math.max(1, Math.floor(opts.staleWorkerTtlMs ?? 50 * 60000));
40389
40486
  const reconcilePinFn = opts.reconcilePin ?? (() => {});
40390
40487
  const setIntervalFn = opts.setInterval ?? ((cb, ms) => {
40391
40488
  const t = setInterval(cb, ms);
@@ -40487,6 +40584,7 @@ function createWorkerActivityFeed(opts) {
40487
40584
  elapsedMs: elapsedFor(r),
40488
40585
  toolCount: v.toolCount,
40489
40586
  currentStep,
40587
+ historyLines: r.narrative.length > 0 ? [...r.narrative] : undefined,
40490
40588
  model: v.model
40491
40589
  };
40492
40590
  });
@@ -40606,8 +40704,60 @@ function createWorkerActivityFeed(opts) {
40606
40704
  log(`worker-feed: edit transient error feed=${g.feedKey}: ${err.message}`);
40607
40705
  }
40608
40706
  }
40707
+ function finalizeWorker(group, agentId, row, view) {
40708
+ row.finished = true;
40709
+ row.state = view.state;
40710
+ markFinalized(agentId);
40711
+ group.chain = group.chain.then(() => {
40712
+ const others = runningRows(group).filter((w) => w.agentId !== agentId);
40713
+ if (others.length > 0) {
40714
+ removeWorker(group, agentId);
40715
+ syncPin(group);
40716
+ return doRender(group, { force: true });
40717
+ }
40718
+ const recap = { ...view, narrativeLines: [...row.narrative] };
40719
+ return doRender(group, { force: true, terminalRecap: recap, finishingAgentId: agentId });
40720
+ }).catch((err) => {
40721
+ log(`worker-feed: finalize chain error ${agentId}: ${err.message}`);
40722
+ });
40723
+ return group.chain;
40724
+ }
40725
+ function terminateWorker(agentId) {
40726
+ const g = groupOfAgent(agentId);
40727
+ const row = g?.workers.get(agentId);
40728
+ if (g == null || row == null) {
40729
+ markFinalized(agentId);
40730
+ return Promise.resolve();
40731
+ }
40732
+ if (row.finished) {
40733
+ return g.chain;
40734
+ }
40735
+ const lv = row.lastView;
40736
+ const view = {
40737
+ description: lv?.description ?? "background task",
40738
+ lastTool: null,
40739
+ toolCount: lv?.toolCount ?? 0,
40740
+ latestSummary: "",
40741
+ elapsedMs: liveElapsed(row, nowFn()),
40742
+ state: "incomplete",
40743
+ model: lv?.model
40744
+ };
40745
+ return finalizeWorker(g, agentId, row, view);
40746
+ }
40609
40747
  function heartbeatTick() {
40610
40748
  const now = nowFn();
40749
+ const staleAgentIds = [];
40750
+ for (const g of groups.values()) {
40751
+ for (const row of g.workers.values()) {
40752
+ if (!row.finished && now - row.lastUpdateAt >= staleWorkerTtlMs) {
40753
+ staleAgentIds.push(row.agentId);
40754
+ }
40755
+ }
40756
+ }
40757
+ for (const agentId of staleAgentIds) {
40758
+ log(`worker-feed: TTL reap agent=${agentId} \u2014 no update in ${Math.floor((now - (groupOfAgent(agentId)?.workers.get(agentId)?.lastUpdateAt ?? now)) / 1000)}s (>= ${Math.floor(staleWorkerTtlMs / 1000)}s); force-terminating leaked row`);
40759
+ terminateWorker(agentId);
40760
+ }
40611
40761
  for (const g of [...groups.values()]) {
40612
40762
  if (g.pendingFinalize != null && now >= g.cooldownUntil) {
40613
40763
  const recap = g.pendingFinalize;
@@ -40693,12 +40843,14 @@ function createWorkerActivityFeed(opts) {
40693
40843
  lastView: null,
40694
40844
  state: "running",
40695
40845
  finished: false,
40846
+ lastUpdateAt: nowFn(),
40696
40847
  dispatchAtMs: null,
40697
40848
  stepStartedAtMs: null
40698
40849
  };
40699
40850
  g.workers.set(agentId, row);
40700
40851
  agentIndex.set(agentId, feedKey);
40701
40852
  }
40853
+ row.lastUpdateAt = nowFn();
40702
40854
  accumulateNarrative(row, view);
40703
40855
  row.state = "running";
40704
40856
  row.lastView = { ...view, narrativeLines: [...row.narrative] };
@@ -40717,23 +40869,10 @@ function createWorkerActivityFeed(opts) {
40717
40869
  markFinalized(agentId);
40718
40870
  return Promise.resolve();
40719
40871
  }
40720
- row.finished = true;
40721
- row.state = view.state === "failed" ? "failed" : "done";
40722
- markFinalized(agentId);
40723
- const group = g;
40724
- group.chain = group.chain.then(() => {
40725
- const others = runningRows(group).filter((w) => w.agentId !== agentId);
40726
- if (others.length > 0) {
40727
- removeWorker(group, agentId);
40728
- syncPin(group);
40729
- return doRender(group, { force: true });
40730
- }
40731
- const recap = { ...view, narrativeLines: [...row.narrative] };
40732
- return doRender(group, { force: true, terminalRecap: recap, finishingAgentId: agentId });
40733
- }).catch((err) => {
40734
- log(`worker-feed: finish chain error ${agentId}: ${err.message}`);
40735
- });
40736
- return group.chain;
40872
+ return finalizeWorker(g, agentId, row, view);
40873
+ },
40874
+ terminate(agentId) {
40875
+ return terminateWorker(agentId);
40737
40876
  },
40738
40877
  drop(agentId) {
40739
40878
  markFinalized(agentId);
@@ -45037,6 +45176,9 @@ function runSilentTurnHeartbeatTick(view, deps) {
45037
45176
 
45038
45177
  // narrative-dedup.ts
45039
45178
  var REPLY_TOOLS = new Set(["reply", "stream_reply"]);
45179
+
45180
+ // narrative-dedup.ts
45181
+ var REPLY_TOOLS2 = new Set(["reply", "stream_reply"]);
45040
45182
  function normalizeNarrative(s) {
45041
45183
  return s.replace(/[*_`>#~]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
45042
45184
  }
@@ -45056,6 +45198,83 @@ function isDraftOfReply(textBlock, replyText) {
45056
45198
  return prefixSimilarity(textBlock, replyText) >= DRAFT_SUPPRESS_THRESHOLD;
45057
45199
  }
45058
45200
 
45201
+ // narrative-flush.ts
45202
+ var PENDING_NARRATIVE_FLUSH_MS = 250;
45203
+
45204
+ class NarrativeFlushController {
45205
+ effects;
45206
+ scheduler;
45207
+ flushMs;
45208
+ pending = null;
45209
+ timerShown = null;
45210
+ constructor(effects, scheduler, flushMs) {
45211
+ this.effects = effects;
45212
+ this.scheduler = scheduler;
45213
+ this.flushMs = flushMs;
45214
+ }
45215
+ get pendingText() {
45216
+ return this.pending;
45217
+ }
45218
+ get timerShownText() {
45219
+ return this.timerShown;
45220
+ }
45221
+ stage(text) {
45222
+ this.scheduler.disarm();
45223
+ if (this.pending != null) {
45224
+ this.effects.show(this.pending);
45225
+ }
45226
+ this.pending = text;
45227
+ this.scheduler.arm(() => this.onTimerFire(), this.flushMs);
45228
+ }
45229
+ onTimerFire() {
45230
+ if (this.pending == null)
45231
+ return;
45232
+ const text = this.pending;
45233
+ this.pending = null;
45234
+ this.timerShown = text;
45235
+ this.effects.show(text);
45236
+ }
45237
+ resolveOnTool(toolName, input) {
45238
+ this.scheduler.disarm();
45239
+ const replyText = REPLY_TOOLS2.has(toolName) && typeof input?.text === "string" ? input.text : null;
45240
+ if (replyText != null)
45241
+ this.maybeRetract(replyText);
45242
+ const pending = this.pending;
45243
+ if (pending == null)
45244
+ return;
45245
+ this.pending = null;
45246
+ if (replyText != null && isDraftOfReply(pending, replyText))
45247
+ return;
45248
+ this.effects.show(pending);
45249
+ }
45250
+ flushAtTurnEnd(lastReplyText) {
45251
+ this.scheduler.disarm();
45252
+ if (lastReplyText.length > 0)
45253
+ this.maybeRetract(lastReplyText);
45254
+ const pending = this.pending;
45255
+ if (pending == null)
45256
+ return;
45257
+ this.pending = null;
45258
+ if (lastReplyText.length > 0 && isDraftOfReply(pending, lastReplyText))
45259
+ return;
45260
+ this.effects.show(pending);
45261
+ }
45262
+ teardown() {
45263
+ this.scheduler.disarm();
45264
+ this.pending = null;
45265
+ this.timerShown = null;
45266
+ }
45267
+ maybeRetract(replyText) {
45268
+ const shown = this.timerShown;
45269
+ if (shown == null)
45270
+ return;
45271
+ if (!isDraftOfReply(shown, replyText))
45272
+ return;
45273
+ this.timerShown = null;
45274
+ this.effects.retractShown(shown);
45275
+ }
45276
+ }
45277
+
45059
45278
  // tool-labels.ts
45060
45279
  var MAX_LABEL_CHARS = 60;
45061
45280
  var MAX_BASH_CHARS = 40;
@@ -61788,6 +62007,7 @@ function startTimer(deps) {
61788
62007
  var EDIT_INTERVAL_MS = 60000;
61789
62008
  var POLL_INTERVAL_MS = 5000;
61790
62009
  var MAX_LIFETIME_MS = 30 * 60000;
62010
+ var BACKGROUND_WORK_SUPPRESS_TTL_MS = MAX_LIFETIME_MS;
61791
62011
  var TELEGRAM_MSG_CAP2 = 32768;
61792
62012
  var SUFFIX_RE = /\n\n(?:\u2014 |_)still working \(\d+m\)( \u00b7 message me anytime, I'll keep you posted)?_?$/;
61793
62013
  var stateByKey = new Map;
@@ -61805,6 +62025,7 @@ function ensure(key) {
61805
62025
  if (!s) {
61806
62026
  s = {
61807
62027
  pending: false,
62028
+ dispatchedAt: null,
61808
62029
  anchorMessageId: null,
61809
62030
  anchorOriginalText: "",
61810
62031
  anchorLiteralText: false,
@@ -61818,7 +62039,9 @@ function ensure(key) {
61818
62039
  function noteAsyncDispatch(key) {
61819
62040
  if (!enabled4())
61820
62041
  return;
61821
- ensure(key).pending = true;
62042
+ const s = ensure(key);
62043
+ s.pending = true;
62044
+ s.dispatchedAt = nowMs();
61822
62045
  }
61823
62046
  function noteOutbound3(key, opts) {
61824
62047
  if (!enabled4())
@@ -61848,6 +62071,17 @@ function noteTurnEnd(key) {
61848
62071
  function hasPendingAsyncDispatch(key) {
61849
62072
  return stateByKey.get(key)?.pending === true;
61850
62073
  }
62074
+ function anyPendingAsyncDispatchWithin(ttlMs) {
62075
+ if (ttlMs <= 0)
62076
+ return false;
62077
+ const now = nowMs();
62078
+ for (const s of stateByKey.values()) {
62079
+ if (s.pending === true && s.dispatchedAt != null && now - s.dispatchedAt < ttlMs) {
62080
+ return true;
62081
+ }
62082
+ }
62083
+ return false;
62084
+ }
61851
62085
  function clearPending(key, reason) {
61852
62086
  if (!stateByKey.has(key))
61853
62087
  return;
@@ -62015,6 +62249,30 @@ function clearSilentEndState(turnKey, deps) {
62015
62249
  `);
62016
62250
  } catch {}
62017
62251
  }
62252
+ var CAPTURED_PROSE_MIN_CHARS = 200;
62253
+ function decideCapturedProseDelivery(args, deps) {
62254
+ const minChars = args.minChars ?? CAPTURED_PROSE_MIN_CHARS;
62255
+ const state3 = readSilentEndState(deps);
62256
+ if (state3 == null)
62257
+ return { deliver: false, reason: "no-state" };
62258
+ if (state3.turnKey !== args.turnKey)
62259
+ return { deliver: false, reason: "turnkey-mismatch" };
62260
+ if (typeof state3.turnId === "string" && state3.turnId !== "" && args.turnId != null && args.turnId !== "" && state3.turnId !== args.turnId) {
62261
+ return { deliver: false, reason: "turnid-mismatch" };
62262
+ }
62263
+ const text4 = typeof state3.pendingText === "string" ? state3.pendingText : "";
62264
+ if (text4.trim().length < minChars)
62265
+ return { deliver: false, reason: "no-substantive-prose" };
62266
+ return { deliver: true, text: text4, reason: "captured-prose" };
62267
+ }
62268
+ function settleCapturedProseDelivery(outcome, effects) {
62269
+ if (outcome === "failed") {
62270
+ return { exhausted: effects.recordUndelivered().exhausted };
62271
+ }
62272
+ effects.closeObligation();
62273
+ effects.clearState();
62274
+ return { exhausted: false };
62275
+ }
62018
62276
  function readSilentEndState(deps) {
62019
62277
  const statePath = resolveStatePath2(deps);
62020
62278
  if (!existsSync14(statePath))
@@ -62120,6 +62378,56 @@ function isSilentFlushMarker(text4) {
62120
62378
  return SILENT_MARKERS.has(trimmed.toUpperCase());
62121
62379
  }
62122
62380
  var TRIVIAL_CONFIRMATIONS = new Set(["SENT", "DONE", "OK", "OKAY", "ACK"]);
62381
+ function isTrivialConfirmationLine(line) {
62382
+ let t = line.trim();
62383
+ if (t.length === 0 || t.length > 8)
62384
+ return false;
62385
+ if (/\W$/.test(t))
62386
+ t = t.slice(0, -1);
62387
+ return TRIVIAL_CONFIRMATIONS.has(t.toUpperCase());
62388
+ }
62389
+ function isCompositeSilentNoise(text4) {
62390
+ if (typeof text4 !== "string")
62391
+ return false;
62392
+ const lines = text4.split(`
62393
+ `).map((l) => l.trim()).filter((l) => l.length > 0);
62394
+ if (lines.length === 0)
62395
+ return false;
62396
+ const hasMarker = lines.some((l) => isSilentFlushMarker(l));
62397
+ if (!hasMarker)
62398
+ return false;
62399
+ return lines.every((l) => isSilentFlushMarker(l) || isTrivialConfirmationLine(l));
62400
+ }
62401
+ function endsWithSilentMarker(text4) {
62402
+ if (typeof text4 !== "string")
62403
+ return false;
62404
+ const lines = text4.split(`
62405
+ `).map((l) => l.trim()).filter((l) => l.length > 0);
62406
+ if (lines.length === 0)
62407
+ return false;
62408
+ return isSilentFlushMarker(lines[lines.length - 1]);
62409
+ }
62410
+ function decideTurnFlush(input) {
62411
+ const flushEnabled = input.flushEnabled !== false;
62412
+ if (!flushEnabled)
62413
+ return { kind: "skip", reason: "flag-disabled" };
62414
+ if (input.replyCalled)
62415
+ return { kind: "skip", reason: "reply-called" };
62416
+ if (input.chatId == null)
62417
+ return { kind: "skip", reason: "no-inbound-chat" };
62418
+ const joined = input.capturedText.join(`
62419
+
62420
+ `).trim();
62421
+ if (joined.length === 0)
62422
+ return { kind: "skip", reason: "empty-text" };
62423
+ if (isSilentFlushMarker(joined))
62424
+ return { kind: "skip", reason: "silent-marker" };
62425
+ if (isCompositeSilentNoise(joined))
62426
+ return { kind: "skip", reason: "silent-marker" };
62427
+ if (endsWithSilentMarker(joined))
62428
+ return { kind: "skip", reason: "silent-marker" };
62429
+ return { kind: "flush", text: joined };
62430
+ }
62123
62431
 
62124
62432
  // answer-stream.ts
62125
62433
  var MIN_INITIAL_CHARS = 50;
@@ -66523,6 +66831,86 @@ function hardenCardBreaks2(text4) {
66523
66831
  }
66524
66832
  return restore2(pieces.join(""));
66525
66833
  }
66834
+ var PARAGRAPH_SPACER2 = "\u00a0";
66835
+ function addParagraphSpacers2(text4) {
66836
+ if (!text4.includes(`
66837
+
66838
+ `))
66839
+ return text4;
66840
+ const nonce = Math.random().toString(36).slice(2);
66841
+ const { masked, restore: restore2, placeholder } = maskCodeRegions2(text4, nonce);
66842
+ if (!masked.includes(`
66843
+
66844
+ `))
66845
+ return restore2(masked);
66846
+ const SP = PARAGRAPH_SPACER2;
66847
+ const asciiTrim = (line) => line.replace(/^[ \t\r\f\v]+|[ \t\r\f\v]+$/g, "");
66848
+ const isBlankish = (line) => {
66849
+ const t = asciiTrim(line);
66850
+ return t === "" || t === SP;
66851
+ };
66852
+ const blockKind = (line) => {
66853
+ if (isFenceOpenLine2(line, placeholder))
66854
+ return "fence";
66855
+ if (isListItemLine2(line))
66856
+ return "list";
66857
+ if (isTableRowLine2(line) || isTableDelimiterLine2(line))
66858
+ return "table";
66859
+ if (isBlockquoteLine2(line))
66860
+ return "quote";
66861
+ if (isHeadingLine2(line))
66862
+ return "heading";
66863
+ if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trimStart()))
66864
+ return "divider";
66865
+ return "prose";
66866
+ };
66867
+ const SAME_KIND_TIGHT = new Set([
66868
+ "list",
66869
+ "table",
66870
+ "quote",
66871
+ "fence",
66872
+ "divider"
66873
+ ]);
66874
+ const shouldSpaceGap = (above, below) => {
66875
+ const a = blockKind(above);
66876
+ const b = blockKind(below);
66877
+ if (a === b && SAME_KIND_TIGHT.has(a))
66878
+ return false;
66879
+ return true;
66880
+ };
66881
+ const toks = [];
66882
+ for (const line of masked.split(`
66883
+ `)) {
66884
+ const kind = isBlankish(line) ? "gap" : "content";
66885
+ const last = toks[toks.length - 1];
66886
+ if (last && last.kind === kind)
66887
+ last.lines.push(line);
66888
+ else
66889
+ toks.push({ kind, lines: [line] });
66890
+ }
66891
+ const out = [];
66892
+ for (let i = 0;i < toks.length; i++) {
66893
+ const tok = toks[i];
66894
+ if (tok.kind === "content") {
66895
+ out.push(...tok.lines);
66896
+ continue;
66897
+ }
66898
+ const prev = toks[i - 1];
66899
+ const next = toks[i + 1];
66900
+ if (prev?.kind === "content" && next?.kind === "content") {
66901
+ const above = prev.lines[prev.lines.length - 1];
66902
+ const below = next.lines[0];
66903
+ if (shouldSpaceGap(above, below))
66904
+ out.push("", SP, "");
66905
+ else
66906
+ out.push("");
66907
+ } else {
66908
+ out.push(...tok.lines);
66909
+ }
66910
+ }
66911
+ return restore2(out.join(`
66912
+ `));
66913
+ }
66526
66914
  function normalizePunctuation2(text4) {
66527
66915
  if (!/[\u2014\u2013\u2022\u00b7]/.test(text4))
66528
66916
  return text4;
@@ -66750,10 +67138,11 @@ function splitMarkdownChunks2(text4, maxLen = RICH_MESSAGE_MAX_CHARS2) {
66750
67138
  return chunks.map((c) => stripBoundarySpacers2(c, "trailing"));
66751
67139
  }
66752
67140
  function stripBoundarySpacers2(chunk2, side) {
67141
+ const sp = PARAGRAPH_SPACER2;
66753
67142
  if (side === "leading") {
66754
- return chunk2.replace(/^(?:[ \t]*\n)+/, "");
67143
+ return chunk2.replace(new RegExp(`^(?:[ \\t]*${sp}?[ \\t]*\\n+)+`), "");
66755
67144
  }
66756
- return chunk2.replace(/(?:\n[ \t]*)+$/, "");
67145
+ return chunk2.replace(new RegExp(`(?:\\n+[ \\t]*${sp}?[ \\t]*)+$`), "");
66757
67146
  }
66758
67147
  function backOffOpenFence2(text4, cut) {
66759
67148
  if (cut <= 0 || cut >= text4.length)
@@ -66943,8 +67332,8 @@ function normalizeOutboundBody(rawText, site, redact2) {
66943
67332
  }
66944
67333
  return { text: text4, voiceReplaced };
66945
67334
  }
66946
- function computeEffectiveText(text4, _literalText) {
66947
- return text4;
67335
+ function computeEffectiveText(text4, literalText) {
67336
+ return literalText ? text4 : addParagraphSpacers(text4);
66948
67337
  }
66949
67338
  function computeReplyChunks(args) {
66950
67339
  const { effectiveText, literalText, limit, chunkMode } = args;
@@ -67612,7 +68001,7 @@ function isSilentFlushMarker2(text4) {
67612
68001
  return SILENT_MARKERS2.has(trimmed.toUpperCase());
67613
68002
  }
67614
68003
  var TRIVIAL_CONFIRMATIONS2 = new Set(["SENT", "DONE", "OK", "OKAY", "ACK"]);
67615
- function isTrivialConfirmationLine(line) {
68004
+ function isTrivialConfirmationLine2(line) {
67616
68005
  let t = line.trim();
67617
68006
  if (t.length === 0 || t.length > 8)
67618
68007
  return false;
@@ -67620,7 +68009,7 @@ function isTrivialConfirmationLine(line) {
67620
68009
  t = t.slice(0, -1);
67621
68010
  return TRIVIAL_CONFIRMATIONS2.has(t.toUpperCase());
67622
68011
  }
67623
- function isCompositeSilentNoise(text4) {
68012
+ function isCompositeSilentNoise2(text4) {
67624
68013
  if (typeof text4 !== "string")
67625
68014
  return false;
67626
68015
  const lines = text4.split(`
@@ -67630,9 +68019,9 @@ function isCompositeSilentNoise(text4) {
67630
68019
  const hasMarker = lines.some((l) => isSilentFlushMarker2(l));
67631
68020
  if (!hasMarker)
67632
68021
  return false;
67633
- return lines.every((l) => isSilentFlushMarker2(l) || isTrivialConfirmationLine(l));
68022
+ return lines.every((l) => isSilentFlushMarker2(l) || isTrivialConfirmationLine2(l));
67634
68023
  }
67635
- function endsWithSilentMarker(text4) {
68024
+ function endsWithSilentMarker2(text4) {
67636
68025
  if (typeof text4 !== "string")
67637
68026
  return false;
67638
68027
  const lines = text4.split(`
@@ -67641,7 +68030,7 @@ function endsWithSilentMarker(text4) {
67641
68030
  return false;
67642
68031
  return isSilentFlushMarker2(lines[lines.length - 1]);
67643
68032
  }
67644
- function decideTurnFlush(input) {
68033
+ function decideTurnFlush2(input) {
67645
68034
  const flushEnabled = input.flushEnabled !== false;
67646
68035
  if (!flushEnabled)
67647
68036
  return { kind: "skip", reason: "flag-disabled" };
@@ -67656,9 +68045,9 @@ function decideTurnFlush(input) {
67656
68045
  return { kind: "skip", reason: "empty-text" };
67657
68046
  if (isSilentFlushMarker2(joined))
67658
68047
  return { kind: "skip", reason: "silent-marker" };
67659
- if (isCompositeSilentNoise(joined))
68048
+ if (isCompositeSilentNoise2(joined))
67660
68049
  return { kind: "skip", reason: "silent-marker" };
67661
- if (endsWithSilentMarker(joined))
68050
+ if (endsWithSilentMarker2(joined))
67662
68051
  return { kind: "skip", reason: "silent-marker" };
67663
68052
  return { kind: "flush", text: joined };
67664
68053
  }
@@ -67672,6 +68061,72 @@ function isTurnFlushSafetyEnabled(env = process.env) {
67672
68061
  return true;
67673
68062
  }
67674
68063
 
68064
+ // answer-ready-flush.ts
68065
+ var ANSWER_READY_FLUSH_MS = 1000;
68066
+ function resolveAnswerReadyFlushMs(env) {
68067
+ const raw = env.SWITCHROOM_ANSWER_READY_FLUSH_MS;
68068
+ if (raw == null || raw.trim() === "")
68069
+ return ANSWER_READY_FLUSH_MS;
68070
+ const n = Number(raw);
68071
+ if (!Number.isFinite(n))
68072
+ return ANSWER_READY_FLUSH_MS;
68073
+ if (n <= 0)
68074
+ return 0;
68075
+ return Math.floor(n);
68076
+ }
68077
+ function shouldArmAnswerReadyFlush(input) {
68078
+ if (input.flushWindowMs <= 0)
68079
+ return false;
68080
+ if (input.inFlightToolCount > 0)
68081
+ return false;
68082
+ if (input.hasPendingAsyncDispatch)
68083
+ return false;
68084
+ return decideTurnFlush(input.flush).kind === "flush";
68085
+ }
68086
+
68087
+ class AnswerReadyFlushController {
68088
+ deps;
68089
+ constructor(deps) {
68090
+ this.deps = deps;
68091
+ }
68092
+ get setTimeoutFn() {
68093
+ return this.deps.setTimeoutFn ?? ((fn, ms) => setTimeout(fn, ms));
68094
+ }
68095
+ get clearTimeoutFn() {
68096
+ return this.deps.clearTimeoutFn ?? ((h) => clearTimeout(h));
68097
+ }
68098
+ clear(turn) {
68099
+ if (turn == null)
68100
+ return;
68101
+ const handle = this.deps.getTimerHandle(turn);
68102
+ if (handle != null) {
68103
+ this.clearTimeoutFn(handle);
68104
+ this.deps.setTimerHandle(turn, null);
68105
+ }
68106
+ }
68107
+ reset() {
68108
+ const turn = this.deps.getCurrentTurn();
68109
+ this.clear(turn);
68110
+ if (turn == null)
68111
+ return;
68112
+ const armInput = this.deps.getArmInput(turn);
68113
+ if (!shouldArmAnswerReadyFlush(armInput))
68114
+ return;
68115
+ const handle = this.setTimeoutFn(() => this.onExpiry(turn), armInput.flushWindowMs);
68116
+ this.deps.setTimerHandle(turn, handle);
68117
+ }
68118
+ onExpiry(armedTurn) {
68119
+ const live = this.deps.getCurrentTurn();
68120
+ if (live == null || live !== armedTurn)
68121
+ return;
68122
+ this.deps.setTimerHandle(live, null);
68123
+ if (!shouldArmAnswerReadyFlush(this.deps.getArmInput(live)))
68124
+ return;
68125
+ this.deps.log?.("answer-ready quiescence flush \u2014 delivering composed terminal answer");
68126
+ this.deps.onFlush(live);
68127
+ }
68128
+ }
68129
+
67675
68130
  // gateway/turn-end-gate.ts
67676
68131
  function decideTurnEndGate(snapshot) {
67677
68132
  const { flushDecision, finalAnswerDelivered } = snapshot;
@@ -69098,11 +69553,12 @@ async function injectSlashCommand(agentName3, command, opts = {}) {
69098
69553
  session,
69099
69554
  command: command.trim(),
69100
69555
  settleMs,
69101
- timeoutMs
69556
+ timeoutMs,
69557
+ precondition: opts.precondition
69102
69558
  }));
69103
69559
  }
69104
69560
  async function injectSlashCommandWith(runner, args) {
69105
- const { socket, session, command, settleMs, timeoutMs } = args;
69561
+ const { socket, session, command, settleMs, timeoutMs, precondition } = args;
69106
69562
  let bareVerb;
69107
69563
  try {
69108
69564
  bareVerb = validateInjectCommand(command);
@@ -69132,6 +69588,17 @@ async function injectSlashCommandWith(runner, args) {
69132
69588
  errorMessage: `tmux session "${session}" on socket "${socket}" not found. ` + `Is the agent running under the tmux supervisor (the default)? ` + `If experimental.legacy_pty=true is set, inject is unsupported.`
69133
69589
  };
69134
69590
  }
69591
+ if (precondition && !precondition()) {
69592
+ return {
69593
+ outcome: "skipped",
69594
+ output: "",
69595
+ truncated: false,
69596
+ command: bareVerb,
69597
+ meta,
69598
+ errorCode: "precondition_failed",
69599
+ errorMessage: "inject precondition returned false at write time; send aborted " + "(no keys sent)."
69600
+ };
69601
+ }
69135
69602
  const before = runner.capture(socket, session) ?? "";
69136
69603
  try {
69137
69604
  runner.send(socket, session, ["send-keys", "-l", command]);
@@ -69336,6 +69803,12 @@ _truncated_`;
69336
69803
  }
69337
69804
  return { body: verbHtml, accent: "done" };
69338
69805
  }
69806
+ if (result.outcome === "skipped") {
69807
+ return {
69808
+ body: `${verbHtml} \u2014 skipped (precondition not met at send time)`,
69809
+ accent: "issue"
69810
+ };
69811
+ }
69339
69812
  const code2 = result.errorCode ?? "tmux_failed";
69340
69813
  const msg = result.errorMessage ?? "unknown error";
69341
69814
  if (code2 === "blocked") {
@@ -72018,6 +72491,8 @@ function decideIdleClear(state3, now) {
72018
72491
  return { clear: false };
72019
72492
  if (state3.turnInFlight)
72020
72493
  return { clear: false };
72494
+ if (state3.backgroundWorkInFlight)
72495
+ return { clear: false };
72021
72496
  if (state3.alreadyCleared)
72022
72497
  return { clear: false };
72023
72498
  if (now - lastEventAt(state3) < state3.idleClearMs)
@@ -72029,6 +72504,67 @@ function classifyIdleEvent(kind, durationMs) {
72029
72504
  const synthetic = turnEnded && durationMs === -1;
72030
72505
  return { activity: !synthetic, turnEnded };
72031
72506
  }
72507
+
72508
+ class IdleTracker {
72509
+ lastActivityAt;
72510
+ lastTurnEndedAt = null;
72511
+ alreadyCleared = false;
72512
+ dispatching = false;
72513
+ constructor(startedAt) {
72514
+ this.lastActivityAt = startedAt;
72515
+ }
72516
+ get activityAt() {
72517
+ return this.lastActivityAt;
72518
+ }
72519
+ get turnEndedAt() {
72520
+ return this.lastTurnEndedAt;
72521
+ }
72522
+ get cleared() {
72523
+ return this.alreadyCleared;
72524
+ }
72525
+ get isDispatching() {
72526
+ return this.dispatching;
72527
+ }
72528
+ noteInbound(now) {
72529
+ this.lastActivityAt = now;
72530
+ this.alreadyCleared = false;
72531
+ }
72532
+ noteEvent(kind, now, durationMs) {
72533
+ const signal = classifyIdleEvent(kind, durationMs);
72534
+ if (signal.activity)
72535
+ this.noteInbound(now);
72536
+ if (signal.turnEnded)
72537
+ this.lastTurnEndedAt = now;
72538
+ }
72539
+ stateFor(inputs, alreadyCleared) {
72540
+ return {
72541
+ lastActivityAt: this.lastActivityAt,
72542
+ lastTurnEndedAt: this.lastTurnEndedAt,
72543
+ idleClearMs: inputs.idleClearMs,
72544
+ alreadyCleared,
72545
+ turnInFlight: inputs.turnInFlight,
72546
+ backgroundWorkInFlight: inputs.backgroundWorkInFlight
72547
+ };
72548
+ }
72549
+ decide(now, inputs) {
72550
+ return decideIdleClear(this.stateFor(inputs, this.alreadyCleared), now);
72551
+ }
72552
+ decideIgnoringLatch(now, inputs) {
72553
+ return decideIdleClear(this.stateFor(inputs, false), now);
72554
+ }
72555
+ markClearFired() {
72556
+ this.alreadyCleared = true;
72557
+ }
72558
+ reArm() {
72559
+ this.alreadyCleared = false;
72560
+ }
72561
+ beginDispatch() {
72562
+ this.dispatching = true;
72563
+ }
72564
+ endDispatch() {
72565
+ this.dispatching = false;
72566
+ }
72567
+ }
72032
72568
  function idleDurationToMs(raw) {
72033
72569
  const m = /^(\d+)([smh])$/.exec(raw.trim());
72034
72570
  if (!m)
@@ -74350,6 +74886,9 @@ function cronIdentity(agent) {
74350
74886
  function isCronIdentity(name) {
74351
74887
  return typeof name === "string" && name.endsWith(CRON_IDENTITY_SUFFIX);
74352
74888
  }
74889
+ function isCronInjectFire(meta) {
74890
+ return meta?.source === "cron" || meta?.session === "cron";
74891
+ }
74353
74892
  function resolveInjectTarget(agentName3, meta) {
74354
74893
  return meta?.session === "cron" ? cronIdentity(agentName3) : agentName3;
74355
74894
  }
@@ -75612,6 +76151,44 @@ function maybeRotate(path2, fs2, maxBytes = TURNS_JSONL_MAX_BYTES) {
75612
76151
  return true;
75613
76152
  }
75614
76153
 
76154
+ // gateway/turn-record-status.ts
76155
+ function computeTurnStatus(turn) {
76156
+ switch (turn.deliveryOutcome) {
76157
+ case "failed":
76158
+ return "send_failed";
76159
+ case "delivered":
76160
+ return "complete";
76161
+ case "suppressed":
76162
+ return turn.finalAnswerDelivered ? "complete" : "no_reply";
76163
+ default:
76164
+ return turn.finalAnswerDelivered ? "complete" : "no_reply";
76165
+ }
76166
+ }
76167
+ function backstopSendOutcome(args) {
76168
+ if (args.threw)
76169
+ return "failed";
76170
+ if (args.chunkCount === 0)
76171
+ return "failed";
76172
+ if (args.sentCount < args.chunkCount)
76173
+ return "failed";
76174
+ return "delivered";
76175
+ }
76176
+ function finalizeBackstopSend(turn, send) {
76177
+ const outcome = backstopSendOutcome(send);
76178
+ turn.deliveryOutcome = outcome;
76179
+ return outcome;
76180
+ }
76181
+ function buildTurnRecord(turn, endedAt) {
76182
+ return {
76183
+ ts: Math.floor(endedAt / 1000),
76184
+ agent: turn.agent,
76185
+ duration_ms: turn.startedAt > 0 ? endedAt - turn.startedAt : 0,
76186
+ tools: turn.toolCallCount ?? 0,
76187
+ status: computeTurnStatus(turn),
76188
+ turn_id: turn.turnId
76189
+ };
76190
+ }
76191
+
75615
76192
  // gateway/inbound-delivery-confirm.ts
75616
76193
  function createDeliveryQueue() {
75617
76194
  return { pending: new Map };
@@ -78210,25 +78787,81 @@ function redactSecrets(text4) {
78210
78787
  return out;
78211
78788
  }
78212
78789
 
78213
- // narrative-dedup.ts
78214
- var REPLY_TOOLS2 = new Set(["reply", "stream_reply"]);
78215
- function normalizeNarrative2(s) {
78216
- return s.replace(/[*_`>#~]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
78217
- }
78218
- function prefixSimilarity2(a, b) {
78219
- const x = normalizeNarrative2(a);
78220
- const y = normalizeNarrative2(b);
78221
- if (x.length === 0 || y.length === 0)
78222
- return 0;
78223
- const n = Math.min(x.length, y.length);
78224
- let i = 0;
78225
- while (i < n && x[i] === y[i])
78226
- i++;
78227
- return i / n;
78228
- }
78229
- var DRAFT_SUPPRESS_THRESHOLD2 = 0.8;
78230
- function isDraftOfReply2(textBlock, replyText) {
78231
- return prefixSimilarity2(textBlock, replyText) >= DRAFT_SUPPRESS_THRESHOLD2;
78790
+ // narrative-flush.ts
78791
+ var PENDING_NARRATIVE_FLUSH_MS2 = 250;
78792
+
78793
+ class NarrativeFlushController2 {
78794
+ effects;
78795
+ scheduler;
78796
+ flushMs;
78797
+ pending = null;
78798
+ timerShown = null;
78799
+ constructor(effects, scheduler, flushMs) {
78800
+ this.effects = effects;
78801
+ this.scheduler = scheduler;
78802
+ this.flushMs = flushMs;
78803
+ }
78804
+ get pendingText() {
78805
+ return this.pending;
78806
+ }
78807
+ get timerShownText() {
78808
+ return this.timerShown;
78809
+ }
78810
+ stage(text4) {
78811
+ this.scheduler.disarm();
78812
+ if (this.pending != null) {
78813
+ this.effects.show(this.pending);
78814
+ }
78815
+ this.pending = text4;
78816
+ this.scheduler.arm(() => this.onTimerFire(), this.flushMs);
78817
+ }
78818
+ onTimerFire() {
78819
+ if (this.pending == null)
78820
+ return;
78821
+ const text4 = this.pending;
78822
+ this.pending = null;
78823
+ this.timerShown = text4;
78824
+ this.effects.show(text4);
78825
+ }
78826
+ resolveOnTool(toolName, input) {
78827
+ this.scheduler.disarm();
78828
+ const replyText = REPLY_TOOLS2.has(toolName) && typeof input?.text === "string" ? input.text : null;
78829
+ if (replyText != null)
78830
+ this.maybeRetract(replyText);
78831
+ const pending = this.pending;
78832
+ if (pending == null)
78833
+ return;
78834
+ this.pending = null;
78835
+ if (replyText != null && isDraftOfReply(pending, replyText))
78836
+ return;
78837
+ this.effects.show(pending);
78838
+ }
78839
+ flushAtTurnEnd(lastReplyText) {
78840
+ this.scheduler.disarm();
78841
+ if (lastReplyText.length > 0)
78842
+ this.maybeRetract(lastReplyText);
78843
+ const pending = this.pending;
78844
+ if (pending == null)
78845
+ return;
78846
+ this.pending = null;
78847
+ if (lastReplyText.length > 0 && isDraftOfReply(pending, lastReplyText))
78848
+ return;
78849
+ this.effects.show(pending);
78850
+ }
78851
+ teardown() {
78852
+ this.scheduler.disarm();
78853
+ this.pending = null;
78854
+ this.timerShown = null;
78855
+ }
78856
+ maybeRetract(replyText) {
78857
+ const shown = this.timerShown;
78858
+ if (shown == null)
78859
+ return;
78860
+ if (!isDraftOfReply(shown, replyText))
78861
+ return;
78862
+ this.timerShown = null;
78863
+ this.effects.retractShown(shown);
78864
+ }
78232
78865
  }
78233
78866
 
78234
78867
  // subagent-watcher.ts
@@ -78365,6 +78998,9 @@ var DEFAULT_STALL_THRESHOLD_MS = 60000;
78365
78998
  var DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS = 300000;
78366
78999
  var DEFAULT_SILENT_STALL_TERMINAL_MS = 300000;
78367
79000
  var DEFAULT_INFLIGHT_TERMINAL_CAP_MS = 45 * 60000;
79001
+ function resolveInflightTerminalCapMs(configVal) {
79002
+ return configVal ?? parseEnvMs("SWITCHROOM_SUBAGENT_INFLIGHT_TERMINAL_CAP_MS") ?? DEFAULT_INFLIGHT_TERMINAL_CAP_MS;
79003
+ }
78368
79004
  var DEFAULT_DEFERRAL_LOG_INTERVAL_MS = 60000;
78369
79005
  var LONG_RUNNING_TOOLS = new Set(["Bash"]);
78370
79006
  var MAX_RESURRECTIONS = 1;
@@ -78478,6 +79114,8 @@ function backfillJsonlAgentId(db2, jsonlPath, agentId, log) {
78478
79114
  }
78479
79115
  function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, parentStateDir, onUnstall, onFileVanished, onProgress) {
78480
79116
  try {
79117
+ if (entry.narrativeGate != null)
79118
+ entry.narrativeGate.tick(now);
78481
79119
  const stat = fs2.statSync(entry.filePath);
78482
79120
  if (stat.size < tail.cursor) {
78483
79121
  tail.cursor = 0;
@@ -78538,43 +79176,87 @@ function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, paren
78538
79176
  entry.errorDetail = errInfo.detail.slice(0, SUBAGENT_RESULT_TEXT_MAX);
78539
79177
  }
78540
79178
  const events = projectSubagentLine(line, entry.agentId, startState);
78541
- const fireNarrativeProgress = () => {
78542
- if (onProgress == null || entry.state !== "running" || entry.historical)
78543
- return false;
78544
- try {
78545
- onProgress({
78546
- agentId: entry.agentId,
78547
- description: entry.description,
78548
- latestSummary: entry.lastResultText,
78549
- elapsedMs: now - entry.dispatchedAt,
78550
- prevBucketIdx: entry.lastProgressBucketIdx,
78551
- setBucketIdx: (b) => {
78552
- entry.lastProgressBucketIdx = b;
78553
- },
78554
- lastTool: entry.lastTool,
78555
- toolCount: entry.toolCount,
78556
- model: entry.currentModel
78557
- });
78558
- return true;
78559
- } catch (cbErr) {
78560
- log?.(`subagent-watcher: onProgress callback error ${entry.agentId}: ${cbErr.message}`);
78561
- return false;
78562
- }
79179
+ const buildNarrativeGate = () => {
79180
+ const nowRef = { value: now };
79181
+ let cueFired = false;
79182
+ const fireCue = () => {
79183
+ if (onProgress == null || entry.state !== "running" || entry.historical)
79184
+ return false;
79185
+ try {
79186
+ onProgress({
79187
+ agentId: entry.agentId,
79188
+ description: entry.description,
79189
+ latestSummary: entry.lastResultText,
79190
+ elapsedMs: nowRef.value - entry.dispatchedAt,
79191
+ prevBucketIdx: entry.lastProgressBucketIdx,
79192
+ setBucketIdx: (b) => {
79193
+ entry.lastProgressBucketIdx = b;
79194
+ },
79195
+ lastTool: entry.lastTool,
79196
+ toolCount: entry.toolCount,
79197
+ model: entry.currentModel
79198
+ });
79199
+ return true;
79200
+ } catch (cbErr) {
79201
+ log?.(`subagent-watcher: onProgress callback error ${entry.agentId}: ${cbErr.message}`);
79202
+ return false;
79203
+ }
79204
+ };
79205
+ const scheduler = {
79206
+ armedFn: null,
79207
+ deadline: 0
79208
+ };
79209
+ const controller = new NarrativeFlushController2({
79210
+ show: () => {
79211
+ cueFired = fireCue();
79212
+ },
79213
+ retractShown: () => {
79214
+ log?.(`subagent-watcher: narrative early-paint retract (no-op on replace-on-write worker card) ${entry.agentId}`);
79215
+ }
79216
+ }, {
79217
+ arm: (fn, ms) => {
79218
+ scheduler.armedFn = fn;
79219
+ scheduler.deadline = nowRef.value + ms;
79220
+ },
79221
+ disarm: () => {
79222
+ scheduler.armedFn = null;
79223
+ }
79224
+ }, PENDING_NARRATIVE_FLUSH_MS2);
79225
+ return {
79226
+ tick: (n) => {
79227
+ nowRef.value = n;
79228
+ if (scheduler.armedFn != null && n >= scheduler.deadline) {
79229
+ const fn = scheduler.armedFn;
79230
+ scheduler.armedFn = null;
79231
+ fn();
79232
+ }
79233
+ },
79234
+ stage: (text5) => {
79235
+ controller.stage(text5);
79236
+ },
79237
+ resolveOnTool: (toolName, input) => {
79238
+ cueFired = false;
79239
+ controller.resolveOnTool(toolName ?? "", input);
79240
+ return cueFired;
79241
+ },
79242
+ resolveAtTurnEnd: (lastReplyText) => {
79243
+ cueFired = false;
79244
+ controller.flushAtTurnEnd(lastReplyText);
79245
+ return cueFired;
79246
+ },
79247
+ reset: () => {
79248
+ controller.teardown();
79249
+ scheduler.armedFn = null;
79250
+ }
79251
+ };
78563
79252
  };
78564
79253
  const resolvePendingSubNarrative = (toolName, toolInput) => {
78565
- if (entry.pendingNarrative == null)
79254
+ if (entry.narrativeGate == null)
78566
79255
  return false;
78567
- const pending = entry.pendingNarrative;
78568
- entry.pendingNarrative = null;
78569
- if (toolName != null && REPLY_TOOLS2.has(toolName)) {
78570
- const replyText = typeof toolInput?.text === "string" ? toolInput.text : "";
78571
- if (isDraftOfReply2(pending.text, replyText))
78572
- return false;
78573
- } else if (toolName == null && entry.lastReplyText != null && entry.lastReplyText.length > 0) {
78574
- if (isDraftOfReply2(pending.text, entry.lastReplyText))
78575
- return false;
79256
+ if (toolName == null) {
79257
+ return entry.narrativeGate.resolveAtTurnEnd(entry.lastReplyText ?? "");
78576
79258
  }
78577
- return fireNarrativeProgress();
79259
+ return entry.narrativeGate.resolveOnTool(toolName, toolInput);
78578
79260
  };
78579
79261
  for (const ev of events) {
78580
79262
  const idleSecBeforeBump = Math.round((now - entry.lastActivityAt) / 1000);
@@ -78681,10 +79363,9 @@ function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, paren
78681
79363
  } else if (ev.kind === "sub_agent_text") {
78682
79364
  entry.lastSummaryLine = clipNarrative(ev.text);
78683
79365
  entry.lastResultText = ev.text.trim().slice(0, SUBAGENT_RESULT_TEXT_MAX);
78684
- if (entry.pendingNarrative != null) {
78685
- fireNarrativeProgress();
78686
- }
78687
- entry.pendingNarrative = { text: ev.text };
79366
+ if (entry.narrativeGate == null)
79367
+ entry.narrativeGate = buildNarrativeGate();
79368
+ entry.narrativeGate.stage(ev.text);
78688
79369
  } else if (ev.kind === "sub_agent_tool_result") {
78689
79370
  if (ev.toolUseId != null && ev.toolUseId !== "") {
78690
79371
  entry.inflightToolUseIds.delete(ev.toolUseId);
@@ -78731,7 +79412,7 @@ function startSubagentWatcher(config) {
78731
79412
  const stallThresholdMs = config.stallThresholdMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_STALL_MS") ?? DEFAULT_STALL_THRESHOLD_MS;
78732
79413
  const silentSynthesisStallThresholdMs = config.silentSynthesisStallThresholdMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_SILENT_SYNTH_STALL_MS") ?? DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS;
78733
79414
  const silentStallTerminalMs = config.silentStallTerminalMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_STALL_TERMINAL_MS") ?? DEFAULT_SILENT_STALL_TERMINAL_MS;
78734
- const inflightTerminalCapMs = config.inflightTerminalCapMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_INFLIGHT_TERMINAL_CAP_MS") ?? DEFAULT_INFLIGHT_TERMINAL_CAP_MS;
79415
+ const inflightTerminalCapMs = resolveInflightTerminalCapMs(config.inflightTerminalCapMs);
78735
79416
  const deferralLogIntervalMs = config.deferralLogIntervalMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_DEFERRAL_LOG_INTERVAL_MS") ?? DEFAULT_DEFERRAL_LOG_INTERVAL_MS;
78736
79417
  const inflightPromoteMaxAgeMs = config.inflightPromoteMaxAgeMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_INFLIGHT_MAX_AGE_MS") ?? DEFAULT_INFLIGHT_PROMOTE_MAX_AGE_MS;
78737
79418
  const terminatedAgentIdsCap = config.terminatedAgentIdsCap ?? TERMINATED_AGENT_IDS_CAP;
@@ -78892,7 +79573,8 @@ function startSubagentWatcher(config) {
78892
79573
  }
78893
79574
  entry.toolCount = 0;
78894
79575
  entry.lastTool = null;
78895
- entry.pendingNarrative = null;
79576
+ entry.narrativeGate?.reset();
79577
+ entry.narrativeGate = null;
78896
79578
  tail.cursor = 0;
78897
79579
  tail.pendingPartial = "";
78898
79580
  tail.hasEmittedStart = false;
@@ -78991,6 +79673,13 @@ function startSubagentWatcher(config) {
78991
79673
  }
78992
79674
  terminatedAgentIds.add(agentId);
78993
79675
  log?.(`subagent-watcher: cleaned up terminal agent ${agentId}`);
79676
+ if (config.onTerminalCleanup) {
79677
+ try {
79678
+ config.onTerminalCleanup(agentId);
79679
+ } catch (cbErr) {
79680
+ log?.(`subagent-watcher: onTerminalCleanup callback error ${agentId}: ${cbErr.message}`);
79681
+ }
79682
+ }
78994
79683
  }
78995
79684
  function recordFalseFinish(agentId, filePath, n) {
78996
79685
  let size = 0;
@@ -82034,10 +82723,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
82034
82723
  }
82035
82724
 
82036
82725
  // ../src/build-info.ts
82037
- var VERSION = "0.18.19";
82038
- var COMMIT_SHA = "34c72776";
82039
- var COMMIT_DATE = "2026-07-13T14:06:25+10:00";
82040
- var LATEST_PR = 3212;
82726
+ var VERSION = "0.18.20";
82727
+ var COMMIT_SHA = "f82e440f";
82728
+ var COMMIT_DATE = "2026-07-13T22:44:46+10:00";
82729
+ var LATEST_PR = 3230;
82041
82730
  var COMMITS_AHEAD_OF_TAG = 0;
82042
82731
 
82043
82732
  // gateway/boot-version.ts
@@ -84462,6 +85151,7 @@ function resolveSubagentOriginChat(agentId) {
84462
85151
  }
84463
85152
  }
84464
85153
  var WORKER_FEED_FALLBACK_LOG_CAP = 256;
85154
+ var WORKER_FEED_STALE_TTL_MARGIN_MS = 300000;
84465
85155
  var workerFeedOwnerDmFallbackLogged = new Set;
84466
85156
  function resolveWorkerFeedChat(agentId, fleetChatId) {
84467
85157
  const origin = resolveSubagentOriginChat(agentId);
@@ -85359,14 +86049,14 @@ function releaseTurnBufferGate(key, endingTurn) {
85359
86049
  }
85360
86050
  function emitTurnRecord(turn, endedAt) {
85361
86051
  try {
85362
- const rec = JSON.stringify({
85363
- ts: Math.floor(endedAt / 1000),
86052
+ const rec = JSON.stringify(buildTurnRecord({
85364
86053
  agent: process.env.SWITCHROOM_AGENT_NAME ?? "unknown",
85365
- duration_ms: turn.startedAt > 0 ? endedAt - turn.startedAt : 0,
85366
- tools: turn.toolCallCount ?? 0,
85367
- status: turn.finalAnswerDelivered ? "complete" : "no_reply",
85368
- turn_id: turn.turnId
85369
- }) + `
86054
+ startedAt: turn.startedAt,
86055
+ toolCallCount: turn.toolCallCount ?? 0,
86056
+ turnId: turn.turnId,
86057
+ finalAnswerDelivered: turn.finalAnswerDelivered,
86058
+ deliveryOutcome: turn.deliveryOutcome
86059
+ }, endedAt)) + `
85370
86060
  `;
85371
86061
  const turnsPath = "/state/agent/turns.jsonl";
85372
86062
  maybeRotate(turnsPath, {
@@ -85382,15 +86072,18 @@ function emitTurnRecord(turn, endedAt) {
85382
86072
  appendFileSync6(turnsPath, rec);
85383
86073
  } catch {}
85384
86074
  }
85385
- function endCurrentTurnAtomic(turn) {
86075
+ function endCurrentTurnAtomic(turn, opts) {
85386
86076
  const key = statusKey(turn.sessionChatId, turn.sessionThreadId);
85387
86077
  if (!turnLiveForItsTopic(turn))
85388
- return;
86078
+ return null;
86079
+ clearAnswerReadyFlushTimeout(turn);
85389
86080
  endCurrentTurnForKey(turn, key);
85390
86081
  const turnEndedAt = Date.now();
85391
86082
  process.stderr.write(`telegram gateway: ${formatTurnLifecycle("clear", "turn_end", turn, turnEndedAt)}
85392
86083
  `);
85393
- emitTurnRecord(turn, turnEndedAt);
86084
+ if (opts?.deferRecord !== true) {
86085
+ emitTurnRecord(turn, turnEndedAt);
86086
+ }
85394
86087
  const degraded = detectStatusSurfaceDegraded(turn);
85395
86088
  if (degraded != null) {
85396
86089
  process.stderr.write(`telegram gateway: status-surface DEGRADED reason=${degraded.reason} turnId=${turn.turnId} chat=${turn.sessionChatId} thread=${turn.sessionThreadId ?? "-"} ${degraded.detail}
@@ -85407,8 +86100,10 @@ function endCurrentTurnAtomic(turn) {
85407
86100
  clearTimeout(turn.noReplyDrainTimer);
85408
86101
  turn.noReplyDrainTimer = null;
85409
86102
  }
86103
+ turn.narrativeGate?.teardown();
85410
86104
  purgeReactionTracking(statusKey(turn.sessionChatId, turn.sessionThreadId), turn);
85411
86105
  armNoReplyDrainTimer(turn);
86106
+ return turnEndedAt;
85412
86107
  }
85413
86108
  function maybeProactiveCompact() {
85414
86109
  if (compactDispatching)
@@ -85468,16 +86163,9 @@ function maybeProactiveCompact() {
85468
86163
  compactDispatching = false;
85469
86164
  });
85470
86165
  }
85471
- var lastIdleActivityAt = Date.now();
85472
- var lastIdleTurnEndAt = null;
85473
- var idleAutoCleared = false;
85474
- var idleClearDispatching = false;
86166
+ var idleTracker = new IdleTracker(Date.now());
85475
86167
  function markIdleActivity() {
85476
- lastIdleActivityAt = Date.now();
85477
- idleAutoCleared = false;
85478
- }
85479
- function markIdleTurnEnd() {
85480
- lastIdleTurnEndAt = Date.now();
86168
+ idleTracker.noteInbound(Date.now());
85481
86169
  }
85482
86170
  function resolveIdleClearMs() {
85483
86171
  const env = process.env.SWITCHROOM_IDLE_CLEAR_MS;
@@ -85502,30 +86190,39 @@ function resolveIdleClearMs() {
85502
86190
  }
85503
86191
  }
85504
86192
  function maybeIdleClear() {
85505
- if (idleClearDispatching)
86193
+ if (idleTracker.isDispatching)
85506
86194
  return;
85507
86195
  const agentName3 = process.env.SWITCHROOM_AGENT_NAME;
85508
86196
  if (!agentName3)
85509
86197
  return;
85510
86198
  const idleClearMs = resolveIdleClearMs();
85511
- const decision = decideIdleClear({
85512
- lastActivityAt: lastIdleActivityAt,
85513
- lastTurnEndedAt: lastIdleTurnEndAt,
86199
+ const decision = idleTracker.decide(Date.now(), {
85514
86200
  idleClearMs,
85515
- alreadyCleared: idleAutoCleared,
85516
- turnInFlight: turnInFlightForGate()
85517
- }, Date.now());
86201
+ turnInFlight: turnInFlightForGate(),
86202
+ backgroundWorkInFlight: anyPendingAsyncDispatchWithin(BACKGROUND_WORK_SUPPRESS_TTL_MS)
86203
+ });
85518
86204
  if (!decision.clear)
85519
86205
  return;
85520
- idleAutoCleared = true;
85521
- idleClearDispatching = true;
86206
+ idleTracker.markClearFired();
86207
+ idleTracker.beginDispatch();
85522
86208
  process.stderr.write(`telegram gateway: idle auto-/clear for ${agentName3} (idle >= ${Math.round(idleClearMs / 60000)}m)
85523
86209
  `);
85524
- injectSlashCommand(agentName3, "/clear").catch((err) => {
86210
+ const stillIdleAtWrite = () => idleTracker.decideIgnoringLatch(Date.now(), {
86211
+ idleClearMs: resolveIdleClearMs(),
86212
+ turnInFlight: turnInFlightForGate(),
86213
+ backgroundWorkInFlight: anyPendingAsyncDispatchWithin(BACKGROUND_WORK_SUPPRESS_TTL_MS)
86214
+ }).clear;
86215
+ injectSlashCommand(agentName3, "/clear", { precondition: stillIdleAtWrite }).then((result) => {
86216
+ if (result.outcome === "skipped") {
86217
+ idleTracker.reArm();
86218
+ process.stderr.write(`telegram gateway: idle /clear suppressed for ${agentName3} (activity in check-to-send gap)
86219
+ `);
86220
+ }
86221
+ }).catch((err) => {
85525
86222
  process.stderr.write(`telegram gateway: idle /clear inject failed for ${agentName3}: ${err instanceof Error ? err.message : String(err)}
85526
86223
  `);
85527
86224
  }).finally(() => {
85528
- idleClearDispatching = false;
86225
+ idleTracker.endDispatch();
85529
86226
  });
85530
86227
  }
85531
86228
  async function postCompactCard(occ, cap) {
@@ -86905,6 +87602,7 @@ var STREAM_THROTTLE_MS_OVERRIDE = (() => {
86905
87602
  return Number.isFinite(n) && n >= 0 ? n : undefined;
86906
87603
  })();
86907
87604
  var TURN_FLUSH_SAFETY_ENABLED = isTurnFlushSafetyEnabled();
87605
+ var ANSWER_READY_FLUSH_MS2 = resolveAnswerReadyFlushMs(process.env);
86908
87606
  var ANSWER_STREAM_VISIBLE_ENABLED = parseVisibleAnswerStreamEnabled(process.env.SWITCHROOM_VISIBLE_ANSWER_STREAM);
86909
87607
  var ANSWER_LANE = resolveAnswerLaneConfig({
86910
87608
  visibleEnabled: ANSWER_STREAM_VISIBLE_ENABLED
@@ -87384,6 +88082,7 @@ var SILENCE_FALLBACK_MS = parsePositiveMsEnv("SWITCHROOM_SILENCE_FALLBACK_MS", 3
87384
88082
  var SILENCE_FALLBACK_HARD_MS = parsePositiveMsEnv("SWITCHROOM_SILENCE_FALLBACK_HARD_MS", 900000);
87385
88083
  var SILENCE_FLOOR_MS = parsePositiveMsEnv("SWITCHROOM_SILENCE_FLOOR_MS", 45000);
87386
88084
  var LIVENESS_TERMINAL_HONESTY = process.env.SWITCHROOM_TG_TERMINAL_HONESTY !== "0";
88085
+ var CAPTURED_PROSE_DELIVERY_ENABLED = process.env.SWITCHROOM_TG_CAPTURED_PROSE_DELIVERY !== "0";
87387
88086
  var SILENCE_DEFER_INFLIGHT_TOOLS = process.env.SWITCHROOM_SILENCE_DEFER_INFLIGHT_TOOLS === "1";
87388
88087
  var SILENCE_LIVENESS_PRODUCTION = process.env.SWITCHROOM_SILENCE_LIVENESS_PRODUCTION !== "0";
87389
88088
  var MEMORY_LEGIBILITY_ENABLED = isMemoryLegibilityEnabled(process.env.SWITCHROOM_MEMORY_LEGIBILITY);
@@ -87698,6 +88397,103 @@ function agentHasInFlightBackgroundWork(now) {
87698
88397
  return ageMs != null && ageMs < TURN_ACTIVE_MARKER_FRESH_MS;
87699
88398
  }
87700
88399
  var lastBgWorkDeferLogMs = 0;
88400
+ async function deliverCapturedProse(args) {
88401
+ const { chatId, threadId, statusKeyStr, registryKey, originTurnId, text: text5, turnDurationMs } = args;
88402
+ const now = Date.now();
88403
+ let outcome;
88404
+ const already = outboundDedup.check(chatId, threadId, text5, now, registryKey);
88405
+ if (already == null) {
88406
+ let out = normalizeParagraphBreaks2(repairEscapedWhitespace2(text5));
88407
+ out = redactOutboundText(out, "captured_prose");
88408
+ const chunks = splitMarkdownChunks2(out, RICH_MESSAGE_MAX_CHARS2);
88409
+ const sentIds = [];
88410
+ try {
88411
+ let liveThreadId = threadId;
88412
+ for (const c of chunks) {
88413
+ const sent = await retryWithThreadFallback2(robustApiCall, (tid) => {
88414
+ const opts = {
88415
+ link_preview_options: { is_disabled: true },
88416
+ ...tid != null ? { message_thread_id: tid } : {}
88417
+ };
88418
+ return bot.api.sendRichMessage(chatId, richMessage2(c), opts);
88419
+ }, { threadId: liveThreadId, chat_id: chatId, verb: "captured-prose.sendMessage" });
88420
+ if (liveThreadId != null && sent.message_thread_id == null) {
88421
+ liveThreadId = undefined;
88422
+ }
88423
+ sentIds.push(sent.message_id);
88424
+ }
88425
+ if (HISTORY_ENABLED && sentIds.length > 0) {
88426
+ try {
88427
+ recordOutbound({
88428
+ chat_id: chatId,
88429
+ thread_id: threadId ?? null,
88430
+ message_ids: sentIds,
88431
+ texts: chunks
88432
+ });
88433
+ } catch {}
88434
+ }
88435
+ outboundDedup.record(chatId, threadId, text5, now, registryKey);
88436
+ process.stderr.write(`telegram gateway: captured-prose delivery \u2014 sent ${out.length} chars recovered from transcript scan (chat=${chatId} origin=${originTurnId})
88437
+ `);
88438
+ outcome = "sent";
88439
+ } catch (err) {
88440
+ process.stderr.write(`telegram gateway: captured-prose delivery failed: ${err.message} \u2014 ` + `arming the silent-end re-prompt net (recordUndeliveredTurnEnd) so the answer is recoverable (chat=${chatId} origin=${originTurnId})
88441
+ `);
88442
+ outcome = "failed";
88443
+ }
88444
+ } else {
88445
+ process.stderr.write(`telegram gateway: captured-prose delivery skipped \u2014 this answer already went out ` + `(dedup age=${already.ageMs}ms chat=${chatId} origin=${originTurnId}); settling bookkeeping
88446
+ `);
88447
+ outcome = "skipped-dedup";
88448
+ }
88449
+ const settlement = settleCapturedProseDelivery(outcome, {
88450
+ closeObligation: () => {
88451
+ if (OBLIGATION_LEDGER_ENABLED) {
88452
+ try {
88453
+ obligationLedger.close(originTurnId);
88454
+ } catch {}
88455
+ }
88456
+ },
88457
+ clearState: () => clearSilentEndState(statusKeyStr),
88458
+ recordUndelivered: () => {
88459
+ try {
88460
+ const silentEndDeps = HISTORY_ENABLED ? {
88461
+ hasOutboundDeliveredSince: (cid, sinceMs, tid) => hasOutboundDeliveredSince(cid, sinceMs, tid, 1)
88462
+ } : undefined;
88463
+ return recordUndeliveredTurnEnd({ chatId, threadId: threadId ?? null, turnKey: statusKeyStr }, silentEndDeps);
88464
+ } catch (netErr) {
88465
+ process.stderr.write(`telegram gateway: captured-prose recovery-net arm failed: ${netErr.message} (chat=${chatId} origin=${originTurnId})
88466
+ `);
88467
+ return { exhausted: false };
88468
+ }
88469
+ }
88470
+ });
88471
+ if (outcome === "failed" && settlement.exhausted) {
88472
+ process.stderr.write(`telegram gateway: WARN captured-prose exhausted-boundary fallback \u2014 rich send ` + `failed with the re-prompt budget already spent; attempting a plain-text delivery of the recovered answer before the generic apology (chat=${chatId} origin=${originTurnId})
88473
+ `);
88474
+ const plain = redactOutboundText(text5, "captured_prose");
88475
+ const plainChunks = splitMarkdownChunks2(plain, RICH_MESSAGE_MAX_CHARS2);
88476
+ try {
88477
+ let liveThreadId = threadId;
88478
+ for (const c of plainChunks) {
88479
+ const sent = await retryWithThreadFallback2(robustApiCall, (tid) => bot.api.sendMessage(chatId, c, tid != null ? { message_thread_id: tid } : {}), { threadId: liveThreadId, chat_id: chatId, verb: "captured-prose-plain-fallback.sendMessage" });
88480
+ if (liveThreadId != null && sent.message_thread_id == null) {
88481
+ liveThreadId = undefined;
88482
+ }
88483
+ }
88484
+ outboundDedup.record(chatId, threadId, text5, Date.now(), registryKey);
88485
+ process.stderr.write(`telegram gateway: captured-prose recovered via plain-text fallback (chat=${chatId} origin=${originTurnId})
88486
+ `);
88487
+ } catch (plainErr) {
88488
+ process.stderr.write(`telegram gateway: captured-prose plain-text fallback ALSO failed: ${plainErr.message} \u2014 posting the generic silent-end apology (chat=${chatId} origin=${originTurnId})
88489
+ `);
88490
+ retryWithThreadFallback2(robustApiCall, (tid) => bot.api.sendMessage(chatId, silentEndFallbackText(turnDurationMs), tid != null ? { message_thread_id: tid } : {}), { threadId, chat_id: chatId, verb: "captured-prose-apology-fallback.sendMessage" }).catch((err) => {
88491
+ process.stderr.write(`telegram gateway: captured-prose apology fallback send failed: ${err instanceof Error ? err.message : String(err)}
88492
+ `);
88493
+ });
88494
+ }
88495
+ }
88496
+ }
87701
88497
  function obligationSweep() {
87702
88498
  if (!OBLIGATION_LEDGER_ENABLED)
87703
88499
  return;
@@ -88475,7 +89271,8 @@ var ipcServer = createIpcServer({
88475
89271
  swallowingApiCall(() => bot.api.editMessageText(operator, msg.messageId, richMessage2(msg.text), {}), { chat_id: String(operator), verb: "rollout-status-edit" });
88476
89272
  },
88477
89273
  onInjectInbound(_client, msg) {
88478
- markIdleActivity();
89274
+ if (!isCronInjectFire(msg.inbound.meta))
89275
+ markIdleActivity();
88479
89276
  const promptKey = typeof msg.inbound.meta?.prompt_key === "string" ? msg.inbound.meta.prompt_key : "unknown";
88480
89277
  const source = typeof msg.inbound.meta?.source === "string" ? msg.inbound.meta.source : "unknown";
88481
89278
  const isDurableReplay = inboundSpool != null && typeof msg.inbound.meta?.replay_fire_ms === "string" && msg.inbound.meta.replay_fire_ms.length > 0;
@@ -90488,7 +91285,7 @@ async function executeEditMessage(args) {
90488
91285
  editRawText = normalizeParagraphBreaks2(editRawText);
90489
91286
  editRawText = redactOutboundText(editRawText, "edit_message");
90490
91287
  if (!editLiteralText)
90491
- editRawText = stripExcessBold2(normalizePunctuation2(editRawText));
91288
+ editRawText = addParagraphSpacers2(stripExcessBold2(normalizePunctuation2(editRawText)));
90492
91289
  {
90493
91290
  const scrub = scrubVoice2(editRawText);
90494
91291
  if (scrub.replaced > 0) {
@@ -90703,6 +91500,33 @@ function resetOrphanedReplyTimeout() {
90703
91500
  }, ORPHANED_REPLY_TIMEOUT_MS);
90704
91501
  }
90705
91502
  }
91503
+ var answerReadyFlush = new AnswerReadyFlushController({
91504
+ getCurrentTurn: () => currentTurn,
91505
+ getArmInput: (turn) => ({
91506
+ flush: {
91507
+ chatId: turn.sessionChatId,
91508
+ replyCalled: turn.replyCalled,
91509
+ capturedText: turn.capturedText,
91510
+ flushEnabled: TURN_FLUSH_SAFETY_ENABLED
91511
+ },
91512
+ inFlightToolCount: toolFlightTracker.inFlightCount(),
91513
+ hasPendingAsyncDispatch: hasPendingAsyncDispatch(statusKey(turn.sessionChatId, turn.sessionThreadId)),
91514
+ flushWindowMs: ANSWER_READY_FLUSH_MS2
91515
+ }),
91516
+ getTimerHandle: (turn) => turn.answerReadyFlushTimeoutId,
91517
+ setTimerHandle: (turn, handle) => {
91518
+ turn.answerReadyFlushTimeoutId = handle;
91519
+ },
91520
+ onFlush: () => handleSessionEvent({ kind: "turn_end", durationMs: -1, reason: "answer-ready-quiescence" }),
91521
+ log: (msg) => process.stderr.write(`telegram gateway: ${msg}
91522
+ `)
91523
+ });
91524
+ function clearAnswerReadyFlushTimeout(turn) {
91525
+ answerReadyFlush.clear(turn);
91526
+ }
91527
+ function resetAnswerReadyFlushTimeout() {
91528
+ answerReadyFlush.reset();
91529
+ }
90706
91530
  function closeActivityLane(chatId, threadId) {
90707
91531
  const key = chatKeyWithSuffix2(chatId, threadId, "activity");
90708
91532
  const stream = activeDraftStreams.get(key);
@@ -90736,6 +91560,45 @@ function composeTurnActivity(turn, final = false, liveSuffix = "") {
90736
91560
  };
90737
91561
  return renderActivityFeedWithNested2(turn.mirrorLines, childLines, final, liveSuffix, stepCount, header);
90738
91562
  }
91563
+ function retractNarrativeLine(turn, text5) {
91564
+ const clipped = clipNarrative2(text5);
91565
+ const idx = turn.mirrorLines.lastIndexOf(clipped);
91566
+ if (idx === -1)
91567
+ return;
91568
+ turn.mirrorLines.splice(idx, 1);
91569
+ const rerender = composeTurnActivity(turn);
91570
+ if (rerender == null)
91571
+ return;
91572
+ turn.activityPendingRender = rerender;
91573
+ const ea = emissionAuthorityFor(turn);
91574
+ cardDrainGate(turn, ea, () => {
91575
+ if (ea.mayDrain(turn)) {
91576
+ ea.openOrEditCard("narrative", () => {
91577
+ turn.activityInFlight = drainActivitySummary(turn, "narrative");
91578
+ });
91579
+ }
91580
+ });
91581
+ }
91582
+ function makeNarrativeGate(turn) {
91583
+ let handle = null;
91584
+ return new NarrativeFlushController({
91585
+ show: (text5) => showNarrativeStep(turn, text5),
91586
+ retractShown: (text5) => retractNarrativeLine(turn, text5)
91587
+ }, {
91588
+ arm: (fn, ms) => {
91589
+ if (handle != null)
91590
+ clearTimeout(handle);
91591
+ handle = setTimeout(fn, ms);
91592
+ handle.unref?.();
91593
+ },
91594
+ disarm: () => {
91595
+ if (handle != null) {
91596
+ clearTimeout(handle);
91597
+ handle = null;
91598
+ }
91599
+ }
91600
+ }, PENDING_NARRATIVE_FLUSH_MS);
91601
+ }
90739
91602
  function showNarrativeStep(turn, text5) {
90740
91603
  const rendered = appendActivityLabel(turn.mirrorLines, clipNarrative2(text5));
90741
91604
  if (rendered == null)
@@ -90751,31 +91614,13 @@ function showNarrativeStep(turn, text5) {
90751
91614
  });
90752
91615
  }
90753
91616
  function resolvePendingNarrativeOnTool(turn, toolName, input) {
90754
- const pending2 = turn.pendingNarrative;
90755
- if (pending2 == null)
90756
- return;
90757
- turn.pendingNarrative = null;
90758
- if (REPLY_TOOLS.has(toolName)) {
90759
- const replyText = typeof input?.text === "string" ? input.text : "";
90760
- if (isDraftOfReply(pending2.text, replyText))
90761
- return;
90762
- }
90763
- showNarrativeStep(turn, pending2.text);
91617
+ turn.narrativeGate.resolveOnTool(toolName, input);
90764
91618
  }
90765
91619
  function stagePendingNarrative(turn, text5) {
90766
- if (turn.pendingNarrative != null) {
90767
- showNarrativeStep(turn, turn.pendingNarrative.text);
90768
- }
90769
- turn.pendingNarrative = { text: text5 };
91620
+ turn.narrativeGate.stage(text5);
90770
91621
  }
90771
91622
  function flushPendingNarrativeAtTurnEnd(turn, lastReplyText) {
90772
- const pending2 = turn.pendingNarrative;
90773
- if (pending2 == null)
90774
- return;
90775
- turn.pendingNarrative = null;
90776
- if (lastReplyText.length > 0 && isDraftOfReply(pending2.text, lastReplyText))
90777
- return;
90778
- showNarrativeStep(turn, pending2.text);
91623
+ turn.narrativeGate.flushAtTurnEnd(lastReplyText);
90779
91624
  }
90780
91625
  async function drainActivitySummary(turn, producer = "tool", openFlags) {
90781
91626
  try {
@@ -91115,11 +91960,7 @@ function handleSessionEvent(ev) {
91115
91960
  }
91116
91961
  {
91117
91962
  const durationMs = ev.kind === "turn_end" ? ev.durationMs : undefined;
91118
- const signal = classifyIdleEvent(ev.kind, durationMs);
91119
- if (signal.activity)
91120
- markIdleActivity();
91121
- if (signal.turnEnded)
91122
- markIdleTurnEnd();
91963
+ idleTracker.noteEvent(ev.kind, Date.now(), durationMs);
91123
91964
  }
91124
91965
  switch (ev.kind) {
91125
91966
  case "enqueue": {
@@ -91139,6 +91980,7 @@ function handleSessionEvent(ev) {
91139
91980
  clearTimeout(prior.orphanedReplyTimeoutId);
91140
91981
  prior.orphanedReplyTimeoutId = null;
91141
91982
  }
91983
+ prior?.narrativeGate?.teardown();
91142
91984
  const startedAt = Date.now();
91143
91985
  const enqThreadIdNum = ev.threadId != null ? Number(ev.threadId) : undefined;
91144
91986
  const turnId = deriveTurnId(ev.chatId, enqThreadIdNum ?? null, ev.messageId) ?? `${chatKey2(ev.chatId, enqThreadIdNum ?? null)}#synthetic-${startedAt}`;
@@ -91164,6 +92006,7 @@ function handleSessionEvent(ev) {
91164
92006
  silentAnchorText: "",
91165
92007
  capturedText: [],
91166
92008
  orphanedReplyTimeoutId: null,
92009
+ answerReadyFlushTimeoutId: null,
91167
92010
  liveness: new LivenessTracker(startedAt),
91168
92011
  turnId,
91169
92012
  registryKey: null,
@@ -91179,13 +92022,14 @@ function handleSessionEvent(ev) {
91179
92022
  activityEverOpened: false,
91180
92023
  activityDrainFailures: 0,
91181
92024
  mirrorLines: [],
91182
- pendingNarrative: null,
92025
+ narrativeGate: undefined,
91183
92026
  lastReplyText: "",
91184
92027
  foregroundSubAgents: new Map,
91185
92028
  answerStream: null,
91186
92029
  isDm: isDmChatId2(ev.chatId),
91187
92030
  emissionAuthority: new EmissionAuthority(statusKey(ev.chatId, enqThreadIdNum))
91188
92031
  };
92032
+ next.narrativeGate = makeNarrativeGate(next);
91189
92033
  setCurrentTurn(next, statusKey(ev.chatId, enqThreadIdNum));
91190
92034
  scheduleEarlyLivenessOpen(next);
91191
92035
  process.stderr.write(`telegram gateway: ${formatTurnLifecycle("set", "enqueue", next, startedAt)}
@@ -91257,6 +92101,7 @@ function handleSessionEvent(ev) {
91257
92101
  const turn = currentTurn;
91258
92102
  if (turn == null)
91259
92103
  return;
92104
+ clearAnswerReadyFlushTimeout(turn);
91260
92105
  resolvePendingNarrativeOnTool(turn, ev.toolName, ev.input);
91261
92106
  turn.toolCallCount++;
91262
92107
  touchTurnActiveMarker2(STATE_DIR);
@@ -91291,6 +92136,7 @@ function handleSessionEvent(ev) {
91291
92136
  const turn = currentTurn;
91292
92137
  if (turn == null)
91293
92138
  return;
92139
+ clearAnswerReadyFlushTimeout(turn);
91294
92140
  resetOrphanedReplyTimeout();
91295
92141
  if (isTelegramSurfaceTool2(ev.toolName))
91296
92142
  return;
@@ -91400,6 +92246,7 @@ function handleSessionEvent(ev) {
91400
92246
  preambleSuppressor.onText(ev.text);
91401
92247
  }
91402
92248
  resetOrphanedReplyTimeout();
92249
+ resetAnswerReadyFlushTimeout();
91403
92250
  if (isContextExhaustionText(ev.text) && turn != null) {
91404
92251
  const chatId = turn.sessionChatId;
91405
92252
  const threadId = turn.sessionThreadId;
@@ -91451,7 +92298,7 @@ function handleSessionEvent(ev) {
91451
92298
  return;
91452
92299
  }
91453
92300
  case "turn_end": {
91454
- if (ev.durationMs === -1) {
92301
+ if (ev.durationMs === -1 && ev.reason !== "answer-ready-quiescence") {
91455
92302
  const turn = currentTurn;
91456
92303
  const key = turn != null ? statusKey(turn.sessionChatId, turn.sessionThreadId) : "";
91457
92304
  const recentlyStreaming = turn != null && turn.liveness.recentlyStreaming(Date.now(), ORPHANED_REPLY_STREAM_WINDOW_MS);
@@ -91525,7 +92372,7 @@ function handleSessionEvent(ev) {
91525
92372
  const chatId = turn.sessionChatId;
91526
92373
  const threadId = turn.sessionThreadId;
91527
92374
  const ctrl = activeStatusReactions.get(statusKey(chatId, threadId));
91528
- const flushDecision = streamFinalizedAsAnswer ? { kind: "skip", reason: "reply-called" } : decideTurnFlush({
92375
+ const flushDecision = streamFinalizedAsAnswer ? { kind: "skip", reason: "reply-called" } : decideTurnFlush2({
91529
92376
  chatId: turn.sessionChatId,
91530
92377
  replyCalled: turn.replyCalled,
91531
92378
  capturedText: turn.capturedText,
@@ -91635,7 +92482,7 @@ function handleSessionEvent(ev) {
91635
92482
  }) ?? { wasEmitted: false, turnKey: null };
91636
92483
  const backstopCardMessageId = cardTakeover.wasEmitted && cardTakeover.turnKey != null ? getPinnedProgressCardMessageId?.(cardTakeover.turnKey) ?? null : null;
91637
92484
  const backstopCardTurnKey = cardTakeover.turnKey;
91638
- endCurrentTurnAtomic(turn);
92485
+ const backstopTurnEndedAt = endCurrentTurnAtomic(turn, { deferRecord: true });
91639
92486
  preambleSuppressor.dropNow();
91640
92487
  {
91641
92488
  const tKey = statusKey(chatId, threadId);
@@ -91652,6 +92499,10 @@ function handleSessionEvent(ev) {
91652
92499
  if (recentCount > 0) {
91653
92500
  process.stderr.write(`telegram gateway: turn-flush suppressed \u2014 reply tool sent ${recentCount} message(s) within 2s
91654
92501
  `);
92502
+ if (backstopTurnEndedAt != null) {
92503
+ turn.deliveryOutcome = "suppressed";
92504
+ emitTurnRecord(turn, backstopTurnEndedAt);
92505
+ }
91655
92506
  return;
91656
92507
  }
91657
92508
  } catch {}
@@ -91663,10 +92514,12 @@ function handleSessionEvent(ev) {
91663
92514
  link_preview_options: { is_disabled: true }
91664
92515
  };
91665
92516
  const limit = RICH_MESSAGE_MAX_CHARS2;
91666
- const renderedText = capturedText;
91667
- const htmlChunks = splitMarkdownChunks2(renderedText, limit);
92517
+ let htmlChunks = [];
91668
92518
  const sentIds = [];
92519
+ let sendThrew = false;
91669
92520
  try {
92521
+ const renderedText = addParagraphSpacers2(capturedText);
92522
+ htmlChunks = splitMarkdownChunks2(renderedText, limit);
91670
92523
  let firstSendUsedEdit = false;
91671
92524
  let liveThreadId = backstopThreadId;
91672
92525
  if (backstopCardMessageId != null && htmlChunks.length > 0) {
@@ -91729,10 +92582,20 @@ function handleSessionEvent(ev) {
91729
92582
  unpinProgressCardForChat?.(backstopChatId, backstopThreadId);
91730
92583
  }
91731
92584
  } catch (err) {
92585
+ sendThrew = true;
91732
92586
  process.stderr.write(`telegram gateway: turn-flush send failed: ${err.message}
91733
92587
  `);
91734
92588
  if (backstopCtrl)
91735
92589
  backstopCtrl.finalize("error");
92590
+ } finally {
92591
+ if (backstopTurnEndedAt != null) {
92592
+ finalizeBackstopSend(turn, {
92593
+ threw: sendThrew,
92594
+ sentCount: sentIds.length,
92595
+ chunkCount: htmlChunks.length
92596
+ });
92597
+ emitTurnRecord(turn, backstopTurnEndedAt);
92598
+ }
91736
92599
  }
91737
92600
  })();
91738
92601
  return;
@@ -91774,21 +92637,40 @@ function handleSessionEvent(ev) {
91774
92637
  ended_via: outboundMetrics.outboundCount > 0 ? "reply" : "silent"
91775
92638
  });
91776
92639
  if (turnEndDecision === "reprompt") {
91777
- const silentEndDeps = HISTORY_ENABLED ? {
91778
- hasOutboundDeliveredSince: (cid, sinceMs, tid) => hasOutboundDeliveredSince(cid, sinceMs, tid, 1)
91779
- } : undefined;
91780
- const silentEnd = recordUndeliveredTurnEnd({
91781
- chatId,
91782
- threadId: threadId ?? null,
91783
- turnKey: tKey
91784
- }, silentEndDeps);
91785
- if (silentEnd.exhausted) {
91786
- process.stderr.write(`telegram gateway: WARN silent-end fallback \u2014 agent stayed ` + `silent after the Stop-hook re-prompt; delivering fallback message chat=${chatId} turnKey=${tKey} (#1161)
91787
- `);
91788
- retryWithThreadFallback2(robustApiCall, (tid) => bot.api.sendMessage(chatId, silentEndFallbackText(turnDurationMs), tid != null ? { message_thread_id: tid } : {}), { threadId, chat_id: chatId, verb: "silent-end-fallback.sendMessage" }).catch((err) => {
91789
- process.stderr.write(`telegram gateway: silent-end fallback send failed: ${err instanceof Error ? err.message : String(err)}
92640
+ const proseDecision = CAPTURED_PROSE_DELIVERY_ENABLED ? decideCapturedProseDelivery({
92641
+ turnKey: tKey,
92642
+ turnId: turn.turnId,
92643
+ minChars: CAPTURED_PROSE_MIN_CHARS
92644
+ }) : { deliver: false, reason: "no-state" };
92645
+ if (proseDecision.deliver && proseDecision.text != null) {
92646
+ process.stderr.write(`telegram gateway: captured-prose delivery engaged on first silent-end chat=${chatId} turnKey=${tKey} (#3227)
91790
92647
  `);
92648
+ deliverCapturedProse({
92649
+ chatId,
92650
+ threadId,
92651
+ statusKeyStr: tKey,
92652
+ registryKey: turn.registryKey ?? null,
92653
+ originTurnId: turn.turnId,
92654
+ text: proseDecision.text,
92655
+ turnDurationMs
91791
92656
  });
92657
+ } else {
92658
+ const silentEndDeps = HISTORY_ENABLED ? {
92659
+ hasOutboundDeliveredSince: (cid, sinceMs, tid) => hasOutboundDeliveredSince(cid, sinceMs, tid, 1)
92660
+ } : undefined;
92661
+ const silentEnd = recordUndeliveredTurnEnd({
92662
+ chatId,
92663
+ threadId: threadId ?? null,
92664
+ turnKey: tKey
92665
+ }, silentEndDeps);
92666
+ if (silentEnd.exhausted) {
92667
+ process.stderr.write(`telegram gateway: WARN silent-end fallback \u2014 agent stayed ` + `silent after the Stop-hook re-prompt; delivering fallback message chat=${chatId} turnKey=${tKey} (#1161)
92668
+ `);
92669
+ retryWithThreadFallback2(robustApiCall, (tid) => bot.api.sendMessage(chatId, silentEndFallbackText(turnDurationMs), tid != null ? { message_thread_id: tid } : {}), { threadId, chat_id: chatId, verb: "silent-end-fallback.sendMessage" }).catch((err) => {
92670
+ process.stderr.write(`telegram gateway: silent-end fallback send failed: ${err instanceof Error ? err.message : String(err)}
92671
+ `);
92672
+ });
92673
+ }
91792
92674
  }
91793
92675
  }
91794
92676
  clear(tKey);
@@ -98269,6 +99151,7 @@ var didOneTimeSetup = false;
98269
99151
  },
98270
99152
  floodWaitRemainingMs: probeFloodWaitRemainingMs,
98271
99153
  maxRows: workerFeedMaxRows,
99154
+ staleWorkerTtlMs: resolveInflightTerminalCapMs() + WORKER_FEED_STALE_TTL_MARGIN_MS,
98272
99155
  reconcilePin: ({ feedKey, chatId, messageId }) => {
98273
99156
  if (!PIN_STATUS_WHILE_WORKING)
98274
99157
  return;
@@ -98314,6 +99197,14 @@ var didOneTimeSetup = false;
98314
99197
  process.stderr.write(`telegram gateway: worker ${agentId} NAMED AS LOST \u2014 falsely finalised twice, resurrection chain bound reached (issue #3023)
98315
99198
  `);
98316
99199
  },
99200
+ onTerminalCleanup: (agentId) => {
99201
+ try {
99202
+ workerActivityFeed?.terminate(agentId);
99203
+ } catch (err) {
99204
+ process.stderr.write(`telegram gateway: worker terminal-cleanup feed removal error agent=${agentId}: ${err.message}
99205
+ `);
99206
+ }
99207
+ },
98317
99208
  onFinish: ({ agentId, outcome, description, resultText, toolCount, durationMs, background: entryBackground }) => {
98318
99209
  deferredDoneReactions.promote();
98319
99210
  let fleetChatId = "";